@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
|
@@ -3,12 +3,12 @@ Object.defineProperties(exports, {
|
|
|
3
3
|
[Symbol.toStringTag]: { value: "Module" }
|
|
4
4
|
});
|
|
5
5
|
const require_chunk = require("../chunk-emK7D4bc.js");
|
|
6
|
-
const require_dist = require("../dist-
|
|
7
|
-
const require_node = require("../node-
|
|
6
|
+
const require_dist = require("../dist-i4kS_3O7.js");
|
|
7
|
+
const require_node = require("../node-TzgGsCvW.js");
|
|
8
8
|
const require_restream_intent = require("../restream-intent-7uDNqMKs.js");
|
|
9
9
|
const require_remote_restream = require("../remote-restream-CO36Sr30.js");
|
|
10
10
|
const require_retire_root_keys = require("../retire-root-keys-Csb8z2f3.js");
|
|
11
|
-
const require_segment_demux_js = require("../segment-demux-js-
|
|
11
|
+
const require_segment_demux_js = require("../segment-demux-js-C9AYdKkD.js");
|
|
12
12
|
let node_os = require("node:os");
|
|
13
13
|
node_os = require_chunk.__toESM(node_os);
|
|
14
14
|
let node_crypto = require("node:crypto");
|
|
@@ -3085,6 +3085,262 @@ var RtpPacketizer = class {
|
|
|
3085
3085
|
}
|
|
3086
3086
|
};
|
|
3087
3087
|
//#endregion
|
|
3088
|
+
//#region src/stream-broker/stream-broker/broker-mem-probe.ts
|
|
3089
|
+
/** Cadence of the `[broker-mem]` line — aligned with the runner's `[mem]`. */
|
|
3090
|
+
var BROKER_MEM_INTERVAL_MS = 5 * 6e4;
|
|
3091
|
+
var copiedPackets = 0;
|
|
3092
|
+
var copiedBytes = 0;
|
|
3093
|
+
/** O(1) per packet: called by `copyOwned`. No allocation. */
|
|
3094
|
+
function recordCopyOwned(bytes) {
|
|
3095
|
+
copiedPackets += 1;
|
|
3096
|
+
copiedBytes += bytes;
|
|
3097
|
+
}
|
|
3098
|
+
function readCopyOwnedTotals() {
|
|
3099
|
+
return {
|
|
3100
|
+
packets: copiedPackets,
|
|
3101
|
+
bytes: copiedBytes
|
|
3102
|
+
};
|
|
3103
|
+
}
|
|
3104
|
+
/** `RssAnon:` / `RssFile:` out of `/proc/self/status` text; null when absent. */
|
|
3105
|
+
function parseProcStatus(text) {
|
|
3106
|
+
const anon = /^RssAnon:\s+(\d+)\s*kB/m.exec(text);
|
|
3107
|
+
const file = /^RssFile:\s+(\d+)\s*kB/m.exec(text);
|
|
3108
|
+
if (anon === null || file === null) return null;
|
|
3109
|
+
return {
|
|
3110
|
+
rssAnonKb: Number(anon[1]),
|
|
3111
|
+
rssFileKb: Number(file[1])
|
|
3112
|
+
};
|
|
3113
|
+
}
|
|
3114
|
+
/**
|
|
3115
|
+
* Summarise `/proc/self/maps`. Cheaper than `smaps_rollup`'s kernel page walk:
|
|
3116
|
+
* the file is one line per mapping and needs no per-page accounting. The
|
|
3117
|
+
* `[heap]` figure is the brk extent (virtual), which is the number glibc's
|
|
3118
|
+
* main arena grows when fragmented.
|
|
3119
|
+
*
|
|
3120
|
+
* Line shape: `start-end perms offset dev inode [path]` — an anonymous mapping
|
|
3121
|
+
* has no sixth field.
|
|
3122
|
+
*/
|
|
3123
|
+
function parseProcMaps(text) {
|
|
3124
|
+
let heapBytes = 0;
|
|
3125
|
+
let anonMappings = 0;
|
|
3126
|
+
let anonBytes = 0;
|
|
3127
|
+
let from = 0;
|
|
3128
|
+
while (from < text.length) {
|
|
3129
|
+
let to = text.indexOf("\n", from);
|
|
3130
|
+
if (to === -1) to = text.length;
|
|
3131
|
+
const line = text.slice(from, to);
|
|
3132
|
+
from = to + 1;
|
|
3133
|
+
const dash = line.indexOf("-");
|
|
3134
|
+
const space = line.indexOf(" ");
|
|
3135
|
+
if (dash <= 0 || space <= dash) continue;
|
|
3136
|
+
const size = Number.parseInt(line.slice(dash + 1, space), 16) - Number.parseInt(line.slice(0, dash), 16);
|
|
3137
|
+
if (!Number.isFinite(size)) continue;
|
|
3138
|
+
const path = line.split(/\s+/, 6)[5];
|
|
3139
|
+
if (path === void 0 || path === "") {
|
|
3140
|
+
anonMappings += 1;
|
|
3141
|
+
anonBytes += size;
|
|
3142
|
+
} else if (path === "[heap]") heapBytes += size;
|
|
3143
|
+
}
|
|
3144
|
+
return {
|
|
3145
|
+
heapBytes,
|
|
3146
|
+
anonMappings,
|
|
3147
|
+
anonBytes
|
|
3148
|
+
};
|
|
3149
|
+
}
|
|
3150
|
+
/** Opened/closed consumers since the last reading; `drain()` resets. */
|
|
3151
|
+
var ConsumerChurn = class {
|
|
3152
|
+
opened = 0;
|
|
3153
|
+
closed = 0;
|
|
3154
|
+
openedByKind = /* @__PURE__ */ new Map();
|
|
3155
|
+
closedByKind = /* @__PURE__ */ new Map();
|
|
3156
|
+
recordOpen(kind) {
|
|
3157
|
+
this.opened += 1;
|
|
3158
|
+
this.openedByKind.set(kind, (this.openedByKind.get(kind) ?? 0) + 1);
|
|
3159
|
+
}
|
|
3160
|
+
recordClose(kind) {
|
|
3161
|
+
this.closed += 1;
|
|
3162
|
+
this.closedByKind.set(kind, (this.closedByKind.get(kind) ?? 0) + 1);
|
|
3163
|
+
}
|
|
3164
|
+
drain() {
|
|
3165
|
+
const reading = {
|
|
3166
|
+
opened: this.opened,
|
|
3167
|
+
closed: this.closed,
|
|
3168
|
+
openedByKind: Object.fromEntries(this.openedByKind),
|
|
3169
|
+
closedByKind: Object.fromEntries(this.closedByKind)
|
|
3170
|
+
};
|
|
3171
|
+
this.opened = 0;
|
|
3172
|
+
this.closed = 0;
|
|
3173
|
+
this.openedByKind = /* @__PURE__ */ new Map();
|
|
3174
|
+
this.closedByKind = /* @__PURE__ */ new Map();
|
|
3175
|
+
return reading;
|
|
3176
|
+
}
|
|
3177
|
+
};
|
|
3178
|
+
/** The one churn counter of this process (the broker runner). */
|
|
3179
|
+
var consumerChurn = new ConsumerChurn();
|
|
3180
|
+
/**
|
|
3181
|
+
* Sliding-window limiter for the attach/detach lines. `admit(now)` answers
|
|
3182
|
+
* whether this line may be written; the number of lines swallowed since the
|
|
3183
|
+
* last admitted one is returned with it so the next line can say so (a branch
|
|
3184
|
+
* that drops work names it).
|
|
3185
|
+
*/
|
|
3186
|
+
function createLineLimiter(maxPerWindow, windowMs) {
|
|
3187
|
+
let windowStart = 0;
|
|
3188
|
+
let inWindow = 0;
|
|
3189
|
+
let suppressed = 0;
|
|
3190
|
+
return (now) => {
|
|
3191
|
+
if (now - windowStart >= windowMs) {
|
|
3192
|
+
windowStart = now;
|
|
3193
|
+
inWindow = 0;
|
|
3194
|
+
}
|
|
3195
|
+
if (inWindow >= maxPerWindow) {
|
|
3196
|
+
suppressed += 1;
|
|
3197
|
+
return {
|
|
3198
|
+
admit: false,
|
|
3199
|
+
suppressedBefore: 0
|
|
3200
|
+
};
|
|
3201
|
+
}
|
|
3202
|
+
inWindow += 1;
|
|
3203
|
+
const suppressedBefore = suppressed;
|
|
3204
|
+
suppressed = 0;
|
|
3205
|
+
return {
|
|
3206
|
+
admit: true,
|
|
3207
|
+
suppressedBefore
|
|
3208
|
+
};
|
|
3209
|
+
};
|
|
3210
|
+
}
|
|
3211
|
+
var consumerLineLimiter = createLineLimiter(40, 6e4);
|
|
3212
|
+
/**
|
|
3213
|
+
* One consumer attached to / detached from a broker: counted always (the
|
|
3214
|
+
* `[broker-mem]` line reports opened/closed since the last line), written at
|
|
3215
|
+
* info under a rate limit so a step in RSS can be matched to an event. The
|
|
3216
|
+
* line after a suppressed burst says how many were swallowed.
|
|
3217
|
+
*/
|
|
3218
|
+
function reportConsumerEvent(logger, event, kind, deviceId, total, now = Date.now()) {
|
|
3219
|
+
if (event === "attach") consumerChurn.recordOpen(kind);
|
|
3220
|
+
else consumerChurn.recordClose(kind);
|
|
3221
|
+
if (logger === void 0 || logger === null) return;
|
|
3222
|
+
const verdict = consumerLineLimiter(now);
|
|
3223
|
+
if (!verdict.admit) return;
|
|
3224
|
+
const suppressed = verdict.suppressedBefore > 0 ? ` suppressedBefore=${verdict.suppressedBefore}` : "";
|
|
3225
|
+
logger.info(`consumer ${event} kind=${kind} total=${total}${suppressed}`, { ...deviceId !== void 0 ? { tags: { deviceId } } : {} });
|
|
3226
|
+
}
|
|
3227
|
+
var MB = 1024 * 1024;
|
|
3228
|
+
function ringBytesOf(s) {
|
|
3229
|
+
return s.preBufferBytes + s.clipBytes + s.rtpRingBytes + s.keyframeCacheBytes;
|
|
3230
|
+
}
|
|
3231
|
+
function mb(bytes) {
|
|
3232
|
+
return (bytes / MB).toFixed(1);
|
|
3233
|
+
}
|
|
3234
|
+
function kindList(counts) {
|
|
3235
|
+
const keys = Object.keys(counts).sort();
|
|
3236
|
+
return keys.length === 0 ? "none" : keys.map((k) => `${k}:${counts[k]}`).join(",");
|
|
3237
|
+
}
|
|
3238
|
+
/** One greppable `key=value` line; no per-device breakdown beyond the top N. */
|
|
3239
|
+
function formatBrokerMem(input) {
|
|
3240
|
+
const { samples, proc, churn, copied } = input;
|
|
3241
|
+
const consumers = {};
|
|
3242
|
+
let rtsp = 0;
|
|
3243
|
+
let audio = 0;
|
|
3244
|
+
let pipe = 0;
|
|
3245
|
+
let ingesting = 0;
|
|
3246
|
+
let planes = 0;
|
|
3247
|
+
let pre = 0;
|
|
3248
|
+
let clip = 0;
|
|
3249
|
+
let rtp = 0;
|
|
3250
|
+
let kf = 0;
|
|
3251
|
+
let qSum = 0;
|
|
3252
|
+
let qMax = 0;
|
|
3253
|
+
for (const s of samples) {
|
|
3254
|
+
if (s.ingesting) ingesting += 1;
|
|
3255
|
+
if (s.annexBPlaneActive) planes += 1;
|
|
3256
|
+
rtsp += s.rtspSessions;
|
|
3257
|
+
audio += s.audioSubscribers;
|
|
3258
|
+
pipe += s.pipeClients;
|
|
3259
|
+
pre += s.preBufferBytes;
|
|
3260
|
+
clip += s.clipBytes;
|
|
3261
|
+
rtp += s.rtpRingBytes;
|
|
3262
|
+
kf += s.keyframeCacheBytes;
|
|
3263
|
+
qSum += s.writeQueueSumBytes;
|
|
3264
|
+
if (s.writeQueueMaxBytes > qMax) qMax = s.writeQueueMaxBytes;
|
|
3265
|
+
for (const [kind, n] of Object.entries(s.encodedByKind)) consumers[kind] = (consumers[kind] ?? 0) + n;
|
|
3266
|
+
}
|
|
3267
|
+
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(",");
|
|
3268
|
+
const parts = ["[broker-mem]"];
|
|
3269
|
+
if (proc.status !== null) parts.push(`rssAnonMb=${(proc.status.rssAnonKb / 1024).toFixed(0)}`, `rssFileMb=${(proc.status.rssFileKb / 1024).toFixed(0)}`);
|
|
3270
|
+
if (proc.maps !== null) parts.push(`heapMapMb=${mb(proc.maps.heapBytes)}`, `anonMaps=${proc.maps.anonMappings}`, `anonMapsMb=${mb(proc.maps.anonBytes)}`);
|
|
3271
|
+
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}`);
|
|
3272
|
+
return parts.join(" ");
|
|
3273
|
+
}
|
|
3274
|
+
/** Count entries named `name` in `process.getActiveResourcesInfo()`. */
|
|
3275
|
+
function countActiveResources(resources, name) {
|
|
3276
|
+
let n = 0;
|
|
3277
|
+
for (const r of resources) if (r === name) n += 1;
|
|
3278
|
+
return n;
|
|
3279
|
+
}
|
|
3280
|
+
//#endregion
|
|
3281
|
+
//#region src/stream-broker/stream-broker/owned-buffer.ts
|
|
3282
|
+
/**
|
|
3283
|
+
* Buffers that enter a LONG-LIVED ring own their backing store.
|
|
3284
|
+
*
|
|
3285
|
+
* Node serves `Buffer.from(...)`, `Buffer.allocUnsafe(n)` and `Buffer.concat`
|
|
3286
|
+
* below `Buffer.poolSize >>> 1` (4 KiB) as views of shared 8 KiB pool slabs.
|
|
3287
|
+
* That is the right choice for the ingest path — a start code, a 4-byte
|
|
3288
|
+
* interleave header, a 1.3 KB RTP packet that is fanned out and forgotten
|
|
3289
|
+
* within a tick — and the wrong one for anything RETAINED: a 600-byte NAL kept
|
|
3290
|
+
* for 60 s in a clip ring pins its whole slab, and with it every short-lived
|
|
3291
|
+
* packet that happened to be allocated beside it. On the hub that held
|
|
3292
|
+
* 550–980 MB of ArrayBuffers for ~150–250 MB of payload, and the full GCs the
|
|
3293
|
+
* pinned slabs caused ran every 0.2–0.5 s at 40–65 % of a core.
|
|
3294
|
+
*
|
|
3295
|
+
* The ring byte caps counted payload bytes, so they never saw it. Both rings
|
|
3296
|
+
* now pass every entry through {@link detachFromPool} on the way in and
|
|
3297
|
+
* account it with {@link backingStoreBytes}, so the cap measures what the
|
|
3298
|
+
* entry costs. Short-lived traffic stays pooled.
|
|
3299
|
+
*/
|
|
3300
|
+
/** True when `buf` is the whole of its ArrayBuffer — nothing else shares it. */
|
|
3301
|
+
function ownsBackingStore(buf) {
|
|
3302
|
+
return buf.byteOffset === 0 && buf.buffer.byteLength === buf.length;
|
|
3303
|
+
}
|
|
3304
|
+
/**
|
|
3305
|
+
* What retaining `buf` really costs: its own length when it owns its
|
|
3306
|
+
* ArrayBuffer, the whole shared slab when it is a view of one.
|
|
3307
|
+
*/
|
|
3308
|
+
function backingStoreBytes(buf) {
|
|
3309
|
+
return ownsBackingStore(buf) ? buf.length : buf.buffer.byteLength;
|
|
3310
|
+
}
|
|
3311
|
+
/**
|
|
3312
|
+
* `buf` itself when it already owns its ArrayBuffer (decided by inspecting the
|
|
3313
|
+
* buffer, never by its size: whether a large `allocUnsafe` is pooled is a Node
|
|
3314
|
+
* runtime detail), else a copy that does. One copy, never two: a buffer that is
|
|
3315
|
+
* already unpooled is returned as-is.
|
|
3316
|
+
*/
|
|
3317
|
+
function detachFromPool(buf) {
|
|
3318
|
+
if (ownsBackingStore(buf)) return buf;
|
|
3319
|
+
const owned = Buffer.allocUnsafeSlow(buf.length);
|
|
3320
|
+
buf.copy(owned);
|
|
3321
|
+
return owned;
|
|
3322
|
+
}
|
|
3323
|
+
/**
|
|
3324
|
+
* A copy of `src[start, end)` that OWNS its backing store — the shape a reader
|
|
3325
|
+
* hands out when the packet it cuts from its socket buffer is going to be
|
|
3326
|
+
* retained anyway.
|
|
3327
|
+
*
|
|
3328
|
+
* Every source video packet is: `captureRtpForPreBuffer` keeps it for the life
|
|
3329
|
+
* of a GOP, and the restreamer's keyframe cache keeps the keyframe ones. With
|
|
3330
|
+
* the readers cutting packets as `Buffer.from(subarray)` — a pooled view — the
|
|
3331
|
+
* ring then had to copy each one a second time to own it: two copies of every
|
|
3332
|
+
* packet on the ingest path (measured 15.1 + 14.8 MB/s on the hub's load).
|
|
3333
|
+
* Cutting the packet owned up front makes the ring's and the cache's
|
|
3334
|
+
* `detachFromPool` an identity, so the packet is copied once and shared by the
|
|
3335
|
+
* reader's fan-out, the ring, the cache and every session's write queue.
|
|
3336
|
+
*/
|
|
3337
|
+
function copyOwned(src, start, end) {
|
|
3338
|
+
const owned = Buffer.allocUnsafeSlow(end - start);
|
|
3339
|
+
src.copy(owned, 0, start, end);
|
|
3340
|
+
recordCopyOwned(end - start);
|
|
3341
|
+
return owned;
|
|
3342
|
+
}
|
|
3343
|
+
//#endregion
|
|
3088
3344
|
//#region src/stream-broker/rtsp/sdp-builder.ts
|
|
3089
3345
|
function buildSdp(options) {
|
|
3090
3346
|
const { codec, parameterSets, streamName } = options;
|
|
@@ -3694,6 +3950,26 @@ var RtspRestreamer = class {
|
|
|
3694
3950
|
for (const session of this.sessions.values()) if (session.isPrestreamIntent()) n += 1;
|
|
3695
3951
|
return n;
|
|
3696
3952
|
}
|
|
3953
|
+
/**
|
|
3954
|
+
* What this restreamer holds, for the `[broker-mem]` probe: live sessions,
|
|
3955
|
+
* their socket write queues (sum and max) and the keyframe caches. One pass
|
|
3956
|
+
* over the sessions, no allocation beyond the result.
|
|
3957
|
+
*/
|
|
3958
|
+
getMemProbe() {
|
|
3959
|
+
let sum = 0;
|
|
3960
|
+
let max = 0;
|
|
3961
|
+
for (const s of this.sessions.values()) {
|
|
3962
|
+
const q = s.getWriteQueueBytes();
|
|
3963
|
+
sum += q;
|
|
3964
|
+
if (q > max) max = q;
|
|
3965
|
+
}
|
|
3966
|
+
return {
|
|
3967
|
+
sessions: this.sessions.size,
|
|
3968
|
+
writeQueueSumBytes: sum,
|
|
3969
|
+
writeQueueMaxBytes: max,
|
|
3970
|
+
keyframeCacheBytes: this.lastKeyframeNalBytes + this.lastKeyframeRtpBytes
|
|
3971
|
+
};
|
|
3972
|
+
}
|
|
3697
3973
|
/** Per-session diagnostic snapshots consumed by `StreamBroker.listClients`. */
|
|
3698
3974
|
listSessionInfos() {
|
|
3699
3975
|
return [...this.sessions.values()].map((s) => s.getInfo());
|
|
@@ -3705,6 +3981,7 @@ var RtspRestreamer = class {
|
|
|
3705
3981
|
streamName: this.streamName,
|
|
3706
3982
|
total: this.sessions.size
|
|
3707
3983
|
} });
|
|
3984
|
+
reportConsumerEvent(this.logger, "attach", "rtsp", this.deviceId, this.sessions.size);
|
|
3708
3985
|
this._onSessionCountChanged?.();
|
|
3709
3986
|
this.reconcileSentryAudio();
|
|
3710
3987
|
if (this._rtpPassthroughMode) setTimeout(() => this.servePendingSessionsRtp(), 0);
|
|
@@ -3755,7 +4032,7 @@ var RtspRestreamer = class {
|
|
|
3755
4032
|
if (this.collectingKeyframeRtp && !isKeyframe && !isParameterSet) this.collectingKeyframeRtp = false;
|
|
3756
4033
|
if (this.collectingKeyframeRtp) {
|
|
3757
4034
|
this.lastKeyframeRtpPackets.push({
|
|
3758
|
-
data:
|
|
4035
|
+
data: detachFromPool(rtpData),
|
|
3759
4036
|
marker: rtpPacket.marker
|
|
3760
4037
|
});
|
|
3761
4038
|
this.lastKeyframeRtpBytes += rtpData.length;
|
|
@@ -4169,6 +4446,7 @@ var RtspRestreamer = class {
|
|
|
4169
4446
|
streamName: this.streamName,
|
|
4170
4447
|
total: this.sessions.size
|
|
4171
4448
|
} });
|
|
4449
|
+
reportConsumerEvent(this.logger, "detach", "rtsp", this.deviceId, this.sessions.size);
|
|
4172
4450
|
this._onSessionCountChanged?.();
|
|
4173
4451
|
this.reconcileSentryAudio();
|
|
4174
4452
|
}
|
|
@@ -4357,6 +4635,19 @@ var RtspSession = class {
|
|
|
4357
4635
|
lastRtpAt = 0;
|
|
4358
4636
|
/** Total video RTP bytes written to the socket. */
|
|
4359
4637
|
bytesSent = 0;
|
|
4638
|
+
/**
|
|
4639
|
+
* The socket is corked for the access unit in progress — see
|
|
4640
|
+
* {@link writeInterleaved}. Cleared by the packet that ends the picture (RTP
|
|
4641
|
+
* marker, or any audio packet) or by the next-tick safety net, whichever
|
|
4642
|
+
* comes first.
|
|
4643
|
+
*/
|
|
4644
|
+
accessUnitCorked = false;
|
|
4645
|
+
/** Flush the access unit in progress. Idempotent: the marker and the safety net both call it. */
|
|
4646
|
+
uncorkAccessUnit = () => {
|
|
4647
|
+
if (!this.accessUnitCorked) return;
|
|
4648
|
+
this.accessUnitCorked = false;
|
|
4649
|
+
this.socket.uncork();
|
|
4650
|
+
};
|
|
4360
4651
|
/** Client `User-Agent` (from the first request that carries one). Lets the
|
|
4361
4652
|
* broker panel identify a consumer by purpose — e.g. the recorder sets
|
|
4362
4653
|
* `-user_agent recorder` on its RTSP pull — instead of a bare address. */
|
|
@@ -4425,6 +4716,10 @@ var RtspSession = class {
|
|
|
4425
4716
|
this.onClose?.();
|
|
4426
4717
|
});
|
|
4427
4718
|
}
|
|
4719
|
+
/** Bytes the socket has accepted but not yet flushed (corked bytes included). */
|
|
4720
|
+
getWriteQueueBytes() {
|
|
4721
|
+
return this.socket.writableLength;
|
|
4722
|
+
}
|
|
4428
4723
|
getSessionId() {
|
|
4429
4724
|
return this.sessionId;
|
|
4430
4725
|
}
|
|
@@ -4604,7 +4899,7 @@ var RtspSession = class {
|
|
|
4604
4899
|
return "dropped-backlog";
|
|
4605
4900
|
}
|
|
4606
4901
|
}
|
|
4607
|
-
this.writeInterleaved(this.tracks[0].rtpChannel, packet.data);
|
|
4902
|
+
this.writeInterleaved(this.tracks[0].rtpChannel, packet.data, packet.marker);
|
|
4608
4903
|
this.missedKeyframes = 0;
|
|
4609
4904
|
return "written";
|
|
4610
4905
|
}
|
|
@@ -4613,7 +4908,7 @@ var RtspSession = class {
|
|
|
4613
4908
|
if (!this.playing || this.closed || this.muted) return "not-playing";
|
|
4614
4909
|
if (this.tracks.length < 2) return "not-playing";
|
|
4615
4910
|
if (this.socket.writableLength > 2097152) return "dropped-backlog";
|
|
4616
|
-
this.writeInterleaved(this.tracks[1].rtpChannel, packet.data);
|
|
4911
|
+
this.writeInterleaved(this.tracks[1].rtpChannel, packet.data, true);
|
|
4617
4912
|
return "written";
|
|
4618
4913
|
}
|
|
4619
4914
|
/**
|
|
@@ -4635,14 +4930,13 @@ var RtspSession = class {
|
|
|
4635
4930
|
* ## Why sharing the payload is safe, and what would make it unsafe
|
|
4636
4931
|
*
|
|
4637
4932
|
* The payload buffer handed here is a private allocation per packet —
|
|
4638
|
-
* `
|
|
4639
|
-
*
|
|
4640
|
-
*
|
|
4641
|
-
*
|
|
4642
|
-
*
|
|
4643
|
-
*
|
|
4644
|
-
*
|
|
4645
|
-
* to: a pending write holds its own reference, and the rule below is about
|
|
4933
|
+
* `copyOwned(readBuffer, …)` in the readers' `onData`, for video and audio
|
|
4934
|
+
* alike — never a view of the reader's socket buffer, which advances on the
|
|
4935
|
+
* next read. Nothing in the broker writes into a captured packet: every
|
|
4936
|
+
* mutation in this path targets a freshly allocated buffer. (The same object
|
|
4937
|
+
* is what the broker's `rtpRing` and the restreamer's keyframe cache retain —
|
|
4938
|
+
* it owns its backing store, so `detachFromPool` keeps it as is — and that is
|
|
4939
|
+
* fine: a pending write holds its own reference, and the rule below is about
|
|
4646
4940
|
* mutation, not retention.)
|
|
4647
4941
|
*
|
|
4648
4942
|
* The rule to keep: **`socket.write` forbids MUTATION before the write
|
|
@@ -4652,18 +4946,36 @@ var RtspSession = class {
|
|
|
4652
4946
|
* queued. Four bytes from the Buffer pool is a pointer bump; the copy this
|
|
4653
4947
|
* replaces was ~1.4 KB.
|
|
4654
4948
|
*
|
|
4655
|
-
*
|
|
4656
|
-
*
|
|
4949
|
+
* ## One cork per ACCESS UNIT, not per packet
|
|
4950
|
+
*
|
|
4951
|
+
* The pair used to be corked and uncorked on its own: one `writev` per RTP
|
|
4952
|
+
* packet per session, so a 4K keyframe fragmented into a hundred packets and
|
|
4953
|
+
* fanned out to three sessions was three hundred syscalls for one picture no
|
|
4954
|
+
* decoder can use before it is whole. The socket is corked on the first write
|
|
4955
|
+
* of an access unit and uncorked by the packet that ENDS it
|
|
4956
|
+
* (`endOfAccessUnit`: the RTP marker on video, every audio packet), so the
|
|
4957
|
+
* picture leaves in one `writev`. A `process.nextTick` safety net uncorks
|
|
4958
|
+
* whatever is still open before the tick yields to I/O — a marker-less camera
|
|
4959
|
+
* or a lost marker degrades to one `writev` per tick, never to a socket held
|
|
4960
|
+
* corked across a read.
|
|
4961
|
+
*
|
|
4962
|
+
* Backpressure sees through the cork: `writableLength` counts corked bytes,
|
|
4963
|
+
* so the `MAX_WRITE_BUFFER` gate, the latch and the missed-keyframe close
|
|
4964
|
+
* (D447) judge exactly what they judged per packet.
|
|
4657
4965
|
*/
|
|
4658
|
-
writeInterleaved(channel, payload) {
|
|
4966
|
+
writeInterleaved(channel, payload, endOfAccessUnit) {
|
|
4659
4967
|
const header = Buffer.allocUnsafe(4);
|
|
4660
4968
|
header[0] = 36;
|
|
4661
4969
|
header[1] = channel;
|
|
4662
4970
|
header.writeUInt16BE(payload.length, 2);
|
|
4663
|
-
this.
|
|
4971
|
+
if (!this.accessUnitCorked) {
|
|
4972
|
+
this.accessUnitCorked = true;
|
|
4973
|
+
this.socket.cork();
|
|
4974
|
+
process.nextTick(this.uncorkAccessUnit);
|
|
4975
|
+
}
|
|
4664
4976
|
this.socket.write(header);
|
|
4665
4977
|
this.socket.write(payload);
|
|
4666
|
-
this.
|
|
4978
|
+
if (endOfAccessUnit) this.uncorkAccessUnit();
|
|
4667
4979
|
this.lastRtpAt = Date.now();
|
|
4668
4980
|
this.bytesSent += 4 + payload.length;
|
|
4669
4981
|
}
|
|
@@ -6361,6 +6673,10 @@ var EgressTranscodeManager = class {
|
|
|
6361
6673
|
/** In-flight starts, keyed by sharing key — concurrent acquires share one. */
|
|
6362
6674
|
pending = /* @__PURE__ */ new Map();
|
|
6363
6675
|
releaseGraceMs;
|
|
6676
|
+
/** Live egress transcodes (ffmpeg children), for the `[broker-mem]` probe. */
|
|
6677
|
+
activeCount() {
|
|
6678
|
+
return this.entries.size;
|
|
6679
|
+
}
|
|
6364
6680
|
constructor(opts) {
|
|
6365
6681
|
this.opts = opts;
|
|
6366
6682
|
this.releaseGraceMs = opts.releaseGraceMs ?? DEFAULT_RELEASE_GRACE_MS;
|
|
@@ -10087,7 +10403,7 @@ var Rfc4571Reader = class {
|
|
|
10087
10403
|
while (this.readBuffer.length >= 2) {
|
|
10088
10404
|
const length = this.readBuffer.readUInt16BE(0);
|
|
10089
10405
|
if (this.readBuffer.length < 2 + length) return;
|
|
10090
|
-
const rtpPacket =
|
|
10406
|
+
const rtpPacket = copyOwned(this.readBuffer, 2, 2 + length);
|
|
10091
10407
|
this.readBuffer = this.readBuffer.subarray(2 + length);
|
|
10092
10408
|
this.dispatchRtp(rtpPacket);
|
|
10093
10409
|
}
|
|
@@ -10148,6 +10464,93 @@ function totalFragmentBytes(fragments) {
|
|
|
10148
10464
|
for (const fragment of fragments) total += fragment.length;
|
|
10149
10465
|
return total;
|
|
10150
10466
|
}
|
|
10467
|
+
/** H.264: single NAL by its type; STAP-A by every aggregated type; FU-A by the FU header. */
|
|
10468
|
+
function classifyH264Payload(payload) {
|
|
10469
|
+
const nalType = payload[0] & 31;
|
|
10470
|
+
if (nalType === H264_NAL_STAP_A) {
|
|
10471
|
+
let keyframe = false;
|
|
10472
|
+
let parameterSet = false;
|
|
10473
|
+
let offset = 1;
|
|
10474
|
+
while (offset + 2 <= payload.length) {
|
|
10475
|
+
const nalSize = payload.readUInt16BE(offset);
|
|
10476
|
+
offset += 2;
|
|
10477
|
+
if (nalSize === 0 || offset + nalSize > payload.length) break;
|
|
10478
|
+
const type = payload[offset] & 31;
|
|
10479
|
+
keyframe ||= type === H264_NAL_IDR;
|
|
10480
|
+
parameterSet ||= type === H264_NAL_SPS || type === H264_NAL_PPS;
|
|
10481
|
+
offset += nalSize;
|
|
10482
|
+
}
|
|
10483
|
+
return {
|
|
10484
|
+
keyframe,
|
|
10485
|
+
parameterSet
|
|
10486
|
+
};
|
|
10487
|
+
}
|
|
10488
|
+
if (nalType === 28) {
|
|
10489
|
+
if (payload.length < 2) return NO_PAYLOAD_CLASS;
|
|
10490
|
+
return {
|
|
10491
|
+
keyframe: (payload[1] & 31) === H264_NAL_IDR,
|
|
10492
|
+
parameterSet: false
|
|
10493
|
+
};
|
|
10494
|
+
}
|
|
10495
|
+
return {
|
|
10496
|
+
keyframe: nalType === H264_NAL_IDR,
|
|
10497
|
+
parameterSet: nalType === H264_NAL_SPS || nalType === H264_NAL_PPS
|
|
10498
|
+
};
|
|
10499
|
+
}
|
|
10500
|
+
/** H.265: single NAL by its type; AP by every aggregated type; FU by the FU header. */
|
|
10501
|
+
function classifyH265Payload(payload) {
|
|
10502
|
+
if (payload.length < 2) return NO_PAYLOAD_CLASS;
|
|
10503
|
+
const nalType = payload[0] >> 1 & 63;
|
|
10504
|
+
if (nalType === H265_NAL_AP) {
|
|
10505
|
+
let keyframe = false;
|
|
10506
|
+
let parameterSet = false;
|
|
10507
|
+
let offset = 2;
|
|
10508
|
+
while (offset + 2 <= payload.length) {
|
|
10509
|
+
const nalSize = payload.readUInt16BE(offset);
|
|
10510
|
+
offset += 2;
|
|
10511
|
+
if (nalSize === 0 || offset + nalSize > payload.length) break;
|
|
10512
|
+
const type = payload[offset] >> 1 & 63;
|
|
10513
|
+
keyframe ||= isH265IrapType(type);
|
|
10514
|
+
parameterSet ||= isH265ParameterSetType(type);
|
|
10515
|
+
offset += nalSize;
|
|
10516
|
+
}
|
|
10517
|
+
return {
|
|
10518
|
+
keyframe,
|
|
10519
|
+
parameterSet
|
|
10520
|
+
};
|
|
10521
|
+
}
|
|
10522
|
+
if (nalType === 49) {
|
|
10523
|
+
if (payload.length < 3) return NO_PAYLOAD_CLASS;
|
|
10524
|
+
return {
|
|
10525
|
+
keyframe: isH265IrapType(payload[2] & 63),
|
|
10526
|
+
parameterSet: false
|
|
10527
|
+
};
|
|
10528
|
+
}
|
|
10529
|
+
return {
|
|
10530
|
+
keyframe: isH265IrapType(nalType),
|
|
10531
|
+
parameterSet: isH265ParameterSetType(nalType)
|
|
10532
|
+
};
|
|
10533
|
+
}
|
|
10534
|
+
/**
|
|
10535
|
+
* Classify one raw RTP packet (12-byte header + payload) by what it carries —
|
|
10536
|
+
* the per-packet half of {@link RtpDepacketizer.processPacket}, without the
|
|
10537
|
+
* reassembly half. Pure: reads the headers, allocates nothing, keeps no state.
|
|
10538
|
+
* `null` for a packet too short to be RTP with a payload.
|
|
10539
|
+
*/
|
|
10540
|
+
function classifyRtpPacket(codec, rtpData) {
|
|
10541
|
+
if (rtpData.length < 12) return null;
|
|
10542
|
+
const marker = (rtpData[1] & 128) !== 0;
|
|
10543
|
+
const timestamp = rtpData.readUInt32BE(4);
|
|
10544
|
+
const payload = rtpData.subarray(12);
|
|
10545
|
+
if (payload.length === 0) return null;
|
|
10546
|
+
const carried = codec === "h264" ? classifyH264Payload(payload) : classifyH265Payload(payload);
|
|
10547
|
+
return {
|
|
10548
|
+
timestamp,
|
|
10549
|
+
marker,
|
|
10550
|
+
keyframe: carried.keyframe,
|
|
10551
|
+
parameterSet: carried.parameterSet
|
|
10552
|
+
};
|
|
10553
|
+
}
|
|
10151
10554
|
/**
|
|
10152
10555
|
* RTP → Annex-B NAL depacketizer.
|
|
10153
10556
|
*
|
|
@@ -10192,87 +10595,22 @@ var RtpDepacketizer = class {
|
|
|
10192
10595
|
* short to be RTP with a payload.
|
|
10193
10596
|
*/
|
|
10194
10597
|
processPacket(rtpData) {
|
|
10195
|
-
|
|
10196
|
-
|
|
10197
|
-
const timestamp = rtpData.readUInt32BE(4);
|
|
10598
|
+
const carried = this.classifyPacket(rtpData);
|
|
10599
|
+
if (carried === null) return null;
|
|
10198
10600
|
const payload = rtpData.subarray(12);
|
|
10199
|
-
if (
|
|
10200
|
-
|
|
10201
|
-
|
|
10202
|
-
else this.processH265(payload, timestamp, marker);
|
|
10203
|
-
return {
|
|
10204
|
-
timestamp,
|
|
10205
|
-
marker,
|
|
10206
|
-
keyframe: carried.keyframe,
|
|
10207
|
-
parameterSet: carried.parameterSet
|
|
10208
|
-
};
|
|
10209
|
-
}
|
|
10210
|
-
/** H.264: single NAL by its type; STAP-A by every aggregated type; FU-A by the FU header. */
|
|
10211
|
-
classifyH264Payload(payload) {
|
|
10212
|
-
const nalType = payload[0] & 31;
|
|
10213
|
-
if (nalType === H264_NAL_STAP_A) {
|
|
10214
|
-
let keyframe = false;
|
|
10215
|
-
let parameterSet = false;
|
|
10216
|
-
let offset = 1;
|
|
10217
|
-
while (offset + 2 <= payload.length) {
|
|
10218
|
-
const nalSize = payload.readUInt16BE(offset);
|
|
10219
|
-
offset += 2;
|
|
10220
|
-
if (nalSize === 0 || offset + nalSize > payload.length) break;
|
|
10221
|
-
const type = payload[offset] & 31;
|
|
10222
|
-
keyframe ||= type === H264_NAL_IDR;
|
|
10223
|
-
parameterSet ||= type === H264_NAL_SPS || type === H264_NAL_PPS;
|
|
10224
|
-
offset += nalSize;
|
|
10225
|
-
}
|
|
10226
|
-
return {
|
|
10227
|
-
keyframe,
|
|
10228
|
-
parameterSet
|
|
10229
|
-
};
|
|
10230
|
-
}
|
|
10231
|
-
if (nalType === 28) {
|
|
10232
|
-
if (payload.length < 2) return NO_PAYLOAD_CLASS;
|
|
10233
|
-
return {
|
|
10234
|
-
keyframe: (payload[1] & 31) === H264_NAL_IDR,
|
|
10235
|
-
parameterSet: false
|
|
10236
|
-
};
|
|
10237
|
-
}
|
|
10238
|
-
return {
|
|
10239
|
-
keyframe: nalType === H264_NAL_IDR,
|
|
10240
|
-
parameterSet: nalType === H264_NAL_SPS || nalType === H264_NAL_PPS
|
|
10241
|
-
};
|
|
10601
|
+
if (this.codec === "h264") this.processH264(payload, carried.timestamp, carried.marker);
|
|
10602
|
+
else this.processH265(payload, carried.timestamp, carried.marker);
|
|
10603
|
+
return carried;
|
|
10242
10604
|
}
|
|
10243
|
-
/**
|
|
10244
|
-
|
|
10245
|
-
|
|
10246
|
-
|
|
10247
|
-
|
|
10248
|
-
|
|
10249
|
-
|
|
10250
|
-
|
|
10251
|
-
|
|
10252
|
-
const nalSize = payload.readUInt16BE(offset);
|
|
10253
|
-
offset += 2;
|
|
10254
|
-
if (nalSize === 0 || offset + nalSize > payload.length) break;
|
|
10255
|
-
const type = payload[offset] >> 1 & 63;
|
|
10256
|
-
keyframe ||= isH265IrapType(type);
|
|
10257
|
-
parameterSet ||= isH265ParameterSetType(type);
|
|
10258
|
-
offset += nalSize;
|
|
10259
|
-
}
|
|
10260
|
-
return {
|
|
10261
|
-
keyframe,
|
|
10262
|
-
parameterSet
|
|
10263
|
-
};
|
|
10264
|
-
}
|
|
10265
|
-
if (nalType === 49) {
|
|
10266
|
-
if (payload.length < 3) return NO_PAYLOAD_CLASS;
|
|
10267
|
-
return {
|
|
10268
|
-
keyframe: isH265IrapType(payload[2] & 63),
|
|
10269
|
-
parameterSet: false
|
|
10270
|
-
};
|
|
10271
|
-
}
|
|
10272
|
-
return {
|
|
10273
|
-
keyframe: isH265IrapType(nalType),
|
|
10274
|
-
parameterSet: isH265ParameterSetType(nalType)
|
|
10275
|
-
};
|
|
10605
|
+
/**
|
|
10606
|
+
* What this packet CARRIES, and nothing else: no NAL is reassembled, no buffer
|
|
10607
|
+
* allocated, no reassembly state touched. The same verdict `processPacket`
|
|
10608
|
+
* returns, for a broker whose Annex-B plane has no consumer — the source-RTP
|
|
10609
|
+
* ring, the restreamer's keyframe cache and the stats need exactly this and
|
|
10610
|
+
* none of the copies.
|
|
10611
|
+
*/
|
|
10612
|
+
classifyPacket(rtpData) {
|
|
10613
|
+
return classifyRtpPacket(this.codec, rtpData);
|
|
10276
10614
|
}
|
|
10277
10615
|
processH264(payload, timestamp, marker) {
|
|
10278
10616
|
const nalType = payload[0] & 31;
|
|
@@ -10452,11 +10790,13 @@ var RtpAccessUnitTracker = class {
|
|
|
10452
10790
|
const keyframeContent = packet.keyframe || packet.parameterSet;
|
|
10453
10791
|
const keyframeAuStart = keyframeContent && !this.keyframeAu;
|
|
10454
10792
|
if (keyframeContent) this.keyframeAu = true;
|
|
10793
|
+
const pictureStart = !packet.parameterSet && !this.pictureSeen;
|
|
10455
10794
|
if (!packet.parameterSet) this.pictureSeen = true;
|
|
10456
10795
|
return {
|
|
10457
10796
|
auStart,
|
|
10458
10797
|
keyframeAu: this.keyframeAu,
|
|
10459
10798
|
keyframeAuStart,
|
|
10799
|
+
pictureStart,
|
|
10460
10800
|
parameterSet: packet.parameterSet
|
|
10461
10801
|
};
|
|
10462
10802
|
}
|
|
@@ -10782,7 +11122,7 @@ var NativeRtspClient = class {
|
|
|
10782
11122
|
const channel = this.readBuffer[1];
|
|
10783
11123
|
const length = this.readBuffer.readUInt16BE(2);
|
|
10784
11124
|
if (this.readBuffer.length < 4 + length) return;
|
|
10785
|
-
const payload =
|
|
11125
|
+
const payload = copyOwned(this.readBuffer, 4, 4 + length);
|
|
10786
11126
|
this.readBuffer = this.readBuffer.subarray(4 + length);
|
|
10787
11127
|
this.handleInterleavedPacket(channel, payload);
|
|
10788
11128
|
} else {
|
|
@@ -11518,49 +11858,6 @@ var AudioCodecSession = class {
|
|
|
11518
11858
|
}
|
|
11519
11859
|
};
|
|
11520
11860
|
//#endregion
|
|
11521
|
-
//#region src/stream-broker/stream-broker/owned-buffer.ts
|
|
11522
|
-
/**
|
|
11523
|
-
* Buffers that enter a LONG-LIVED ring own their backing store.
|
|
11524
|
-
*
|
|
11525
|
-
* Node serves `Buffer.from(...)`, `Buffer.allocUnsafe(n)` and `Buffer.concat`
|
|
11526
|
-
* below `Buffer.poolSize >>> 1` (4 KiB) as views of shared 8 KiB pool slabs.
|
|
11527
|
-
* That is the right choice for the ingest path — a start code, a 4-byte
|
|
11528
|
-
* interleave header, a 1.3 KB RTP packet that is fanned out and forgotten
|
|
11529
|
-
* within a tick — and the wrong one for anything RETAINED: a 600-byte NAL kept
|
|
11530
|
-
* for 60 s in a clip ring pins its whole slab, and with it every short-lived
|
|
11531
|
-
* packet that happened to be allocated beside it. On the hub that held
|
|
11532
|
-
* 550–980 MB of ArrayBuffers for ~150–250 MB of payload, and the full GCs the
|
|
11533
|
-
* pinned slabs caused ran every 0.2–0.5 s at 40–65 % of a core.
|
|
11534
|
-
*
|
|
11535
|
-
* The ring byte caps counted payload bytes, so they never saw it. Both rings
|
|
11536
|
-
* now pass every entry through {@link detachFromPool} on the way in and
|
|
11537
|
-
* account it with {@link backingStoreBytes}, so the cap measures what the
|
|
11538
|
-
* entry costs. Short-lived traffic stays pooled.
|
|
11539
|
-
*/
|
|
11540
|
-
/** True when `buf` is the whole of its ArrayBuffer — nothing else shares it. */
|
|
11541
|
-
function ownsBackingStore(buf) {
|
|
11542
|
-
return buf.byteOffset === 0 && buf.buffer.byteLength === buf.length;
|
|
11543
|
-
}
|
|
11544
|
-
/**
|
|
11545
|
-
* What retaining `buf` really costs: its own length when it owns its
|
|
11546
|
-
* ArrayBuffer, the whole shared slab when it is a view of one.
|
|
11547
|
-
*/
|
|
11548
|
-
function backingStoreBytes(buf) {
|
|
11549
|
-
return ownsBackingStore(buf) ? buf.length : buf.buffer.byteLength;
|
|
11550
|
-
}
|
|
11551
|
-
/**
|
|
11552
|
-
* `buf` itself when it already owns its ArrayBuffer (decided by inspecting the
|
|
11553
|
-
* buffer, never by its size: whether a large `allocUnsafe` is pooled is a Node
|
|
11554
|
-
* runtime detail), else a copy that does. One copy, never two: a buffer that is
|
|
11555
|
-
* already unpooled is returned as-is.
|
|
11556
|
-
*/
|
|
11557
|
-
function detachFromPool(buf) {
|
|
11558
|
-
if (ownsBackingStore(buf)) return buf;
|
|
11559
|
-
const owned = Buffer.allocUnsafeSlow(buf.length);
|
|
11560
|
-
buf.copy(owned);
|
|
11561
|
-
return owned;
|
|
11562
|
-
}
|
|
11563
|
-
//#endregion
|
|
11564
11861
|
//#region src/stream-broker/stream-broker/encoded-ring-buffer.ts
|
|
11565
11862
|
/**
|
|
11566
11863
|
* Time-based ring buffer for encoded video packets.
|
|
@@ -12500,9 +12797,6 @@ var StreamBroker = class StreamBroker {
|
|
|
12500
12797
|
*/
|
|
12501
12798
|
ffmpegBinaryPathSource = () => null;
|
|
12502
12799
|
rtpDepacketizer = null;
|
|
12503
|
-
/** Flag-only depacketizer for the `push-rtp` source-RTP pre-buffer path
|
|
12504
|
-
* (sets keyframe/param-set flags without re-emitting AnnexB packets). */
|
|
12505
|
-
pushRtpFlagDepacketizer = null;
|
|
12506
12800
|
audioRtpDecoder = null;
|
|
12507
12801
|
audioCodecSession = null;
|
|
12508
12802
|
/**
|
|
@@ -12746,6 +13040,8 @@ var StreamBroker = class StreamBroker {
|
|
|
12746
13040
|
* timeout was chosen against.
|
|
12747
13041
|
*/
|
|
12748
13042
|
static RTP_ACTIVITY_SWEEP_MS = 1e3;
|
|
13043
|
+
/** A source counts as ingesting when its last video packet is younger than this. */
|
|
13044
|
+
static INGESTING_WITHIN_MS = 1e4;
|
|
12749
13045
|
manualStop = false;
|
|
12750
13046
|
stopping = false;
|
|
12751
13047
|
pipeServer;
|
|
@@ -12870,10 +13166,24 @@ var StreamBroker = class StreamBroker {
|
|
|
12870
13166
|
* log alongside `audioRtpSeen`, so a wake-up cycle that brings audio
|
|
12871
13167
|
* back but never video is observable from the structured logs. */
|
|
12872
13168
|
videoRtpSeen = 0;
|
|
12873
|
-
/**
|
|
13169
|
+
/**
|
|
13170
|
+
* What the source RTP packet being handled CARRIES (keyframe / parameter-set
|
|
13171
|
+
* content, read from its payload headers by `classifyRtpPacket`) — the flags
|
|
13172
|
+
* `captureRtpForPreBuffer` reads. Per packet, not per completed NAL: every
|
|
13173
|
+
* fragment of a fragmented IDR carries them, and the ring marks one keyframe
|
|
13174
|
+
* AU for the lot.
|
|
13175
|
+
*/
|
|
12874
13176
|
_lastNalKeyframe = false;
|
|
12875
13177
|
_lastNalParamSet = false;
|
|
12876
13178
|
/**
|
|
13179
|
+
* Whether the Annex-B plane (NAL reassembly + access-unit assembly →
|
|
13180
|
+
* `EncodedPacket`) is being BUILT for this stream. False while nothing
|
|
13181
|
+
* consumes it: the source-RTP path then classifies each packet and copies
|
|
13182
|
+
* nothing. Armed at the next keyframe access unit once a consumer attaches
|
|
13183
|
+
* (`needsAnnexBPlane`), so the first packet it emits is decodable.
|
|
13184
|
+
*/
|
|
13185
|
+
annexBPlaneActive = false;
|
|
13186
|
+
/**
|
|
12877
13187
|
* Source-RTP pre-buffer: a ring of raw source RTP packets trimmed to
|
|
12878
13188
|
* begin at the most recent keyframe access unit — i.e. the current GOP so
|
|
12879
13189
|
* far. A late-joining WebRTC viewer on the repacketizer path replays this
|
|
@@ -13006,6 +13316,33 @@ var StreamBroker = class StreamBroker {
|
|
|
13006
13316
|
return this.lastPacketAt;
|
|
13007
13317
|
}
|
|
13008
13318
|
/**
|
|
13319
|
+
* What this broker holds, for the `[broker-mem]` probe (`broker-mem-probe.ts`).
|
|
13320
|
+
* Ring sizes are the rings' own counters (backing-store bytes), so this is
|
|
13321
|
+
* O(sessions + subscribers) and allocates only the result.
|
|
13322
|
+
*/
|
|
13323
|
+
getMemSample(now) {
|
|
13324
|
+
const encodedByKind = {};
|
|
13325
|
+
for (const entry of this.encodedCallbacks.values()) encodedByKind[entry.attribution.kind] = (encodedByKind[entry.attribution.kind] ?? 0) + 1;
|
|
13326
|
+
for (const entry of this.rtpVideoCallbacks.values()) encodedByKind[entry.attribution.kind] = (encodedByKind[entry.attribution.kind] ?? 0) + 1;
|
|
13327
|
+
const rtsp = this.rtspRestreamer.getMemProbe();
|
|
13328
|
+
return {
|
|
13329
|
+
deviceId: this.numericDeviceId,
|
|
13330
|
+
brokerId: this.deviceId,
|
|
13331
|
+
ingesting: this.lastPacketAt > 0 && now - this.lastPacketAt < StreamBroker.INGESTING_WITHIN_MS,
|
|
13332
|
+
preBufferBytes: this.preBuffer.getBufferedBytes(),
|
|
13333
|
+
clipBytes: this.clipBuffer?.getBufferedBytes() ?? 0,
|
|
13334
|
+
rtpRingBytes: this.rtpRingBytes,
|
|
13335
|
+
keyframeCacheBytes: rtsp.keyframeCacheBytes,
|
|
13336
|
+
rtspSessions: rtsp.sessions,
|
|
13337
|
+
writeQueueSumBytes: rtsp.writeQueueSumBytes,
|
|
13338
|
+
writeQueueMaxBytes: rtsp.writeQueueMaxBytes,
|
|
13339
|
+
encodedByKind,
|
|
13340
|
+
audioSubscribers: this.audioChunkPlane.subscriberCount,
|
|
13341
|
+
pipeClients: this.pipeServer.getClientCount(),
|
|
13342
|
+
annexBPlaneActive: this.annexBPlaneActive
|
|
13343
|
+
};
|
|
13344
|
+
}
|
|
13345
|
+
/**
|
|
13009
13346
|
* Whether a LAZY publisher is being built right now.
|
|
13010
13347
|
*
|
|
13011
13348
|
* A consumer deciding whether it is worth waiting for this source needs to
|
|
@@ -13949,26 +14286,7 @@ var StreamBroker = class StreamBroker {
|
|
|
13949
14286
|
return;
|
|
13950
14287
|
}
|
|
13951
14288
|
}
|
|
13952
|
-
if (packet.type === "video")
|
|
13953
|
-
this.packetCount++;
|
|
13954
|
-
this.totalBytes += packet.data.length;
|
|
13955
|
-
this.bytesInWindow += packet.data.length;
|
|
13956
|
-
this.packetsInWindow++;
|
|
13957
|
-
this.lastPacketAt = Date.now();
|
|
13958
|
-
const now = Date.now();
|
|
13959
|
-
const windowMs = now - this.windowStartMs;
|
|
13960
|
-
if (windowMs >= 2e3) {
|
|
13961
|
-
this.bitrateKbps = Math.round(this.bytesInWindow * 8 / windowMs);
|
|
13962
|
-
this.encodedInputFps = this.packetsInWindow / windowMs * 1e3;
|
|
13963
|
-
this.bytesInWindow = 0;
|
|
13964
|
-
this.packetsInWindow = 0;
|
|
13965
|
-
this.windowStartMs = now;
|
|
13966
|
-
}
|
|
13967
|
-
if (packet.keyframe) {
|
|
13968
|
-
if (this.lastKeyframeMs > 0) this.idrIntervalMs = now - this.lastKeyframeMs;
|
|
13969
|
-
this.lastKeyframeMs = now;
|
|
13970
|
-
}
|
|
13971
|
-
}
|
|
14289
|
+
if (packet.type === "video" && this.rtpDepacketizer === null) this.recordVideoStats(packet.data.length, true, packet.keyframe);
|
|
13972
14290
|
if (packet.type === "video" && !this.isRtpSource()) this.preBuffer.push(packet);
|
|
13973
14291
|
if (packet.type === "video" && this.clipBuffer && packet.isPlaceholder !== true) this.clipBuffer.push(packet);
|
|
13974
14292
|
this.fanOutEncoded(packet);
|
|
@@ -13987,6 +14305,7 @@ var StreamBroker = class StreamBroker {
|
|
|
13987
14305
|
packetsDelivered: 0
|
|
13988
14306
|
};
|
|
13989
14307
|
this.encodedCallbacks.set(callback, entry);
|
|
14308
|
+
reportConsumerEvent(this.logger, "attach", attribution.kind, this.numericDeviceId, this.encodedCallbacks.size + this.rtpVideoCallbacks.size);
|
|
13990
14309
|
if (this.streamingDebug) this.logger?.info("encoded subscriber added", { meta: {
|
|
13991
14310
|
total: this.encodedCallbacks.size,
|
|
13992
14311
|
subscriberId: entry.id,
|
|
@@ -13997,6 +14316,7 @@ var StreamBroker = class StreamBroker {
|
|
|
13997
14316
|
this.checkDemand();
|
|
13998
14317
|
return () => {
|
|
13999
14318
|
this.encodedCallbacks.delete(callback);
|
|
14319
|
+
reportConsumerEvent(this.logger, "detach", attribution.kind, this.numericDeviceId, this.encodedCallbacks.size + this.rtpVideoCallbacks.size);
|
|
14000
14320
|
if (this.streamingDebug) this.logger?.info("encoded subscriber removed", { meta: {
|
|
14001
14321
|
total: this.encodedCallbacks.size,
|
|
14002
14322
|
subscriberId: entry.id
|
|
@@ -14091,6 +14411,7 @@ var StreamBroker = class StreamBroker {
|
|
|
14091
14411
|
packetsDelivered: 0
|
|
14092
14412
|
};
|
|
14093
14413
|
this.rtpVideoCallbacks.set(callback, entry);
|
|
14414
|
+
reportConsumerEvent(this.logger, "attach", attribution.kind, this.numericDeviceId, this.encodedCallbacks.size + this.rtpVideoCallbacks.size);
|
|
14094
14415
|
if (this.streamingDebug || CH_RTP.on) this.logger?.info("rtp video subscriber added", { meta: {
|
|
14095
14416
|
total: this.rtpVideoCallbacks.size,
|
|
14096
14417
|
subscriberId: entry.id,
|
|
@@ -14100,6 +14421,7 @@ var StreamBroker = class StreamBroker {
|
|
|
14100
14421
|
this.checkDemand();
|
|
14101
14422
|
return () => {
|
|
14102
14423
|
this.rtpVideoCallbacks.delete(callback);
|
|
14424
|
+
reportConsumerEvent(this.logger, "detach", attribution.kind, this.numericDeviceId, this.encodedCallbacks.size + this.rtpVideoCallbacks.size);
|
|
14103
14425
|
if (this.streamingDebug || CH_RTP.on) this.logger?.info("rtp video subscriber removed", { meta: {
|
|
14104
14426
|
total: this.rtpVideoCallbacks.size,
|
|
14105
14427
|
subscriberId: entry.id
|
|
@@ -14159,8 +14481,8 @@ var StreamBroker = class StreamBroker {
|
|
|
14159
14481
|
index: auStart,
|
|
14160
14482
|
ts: auPacket.readUInt32BE(4)
|
|
14161
14483
|
});
|
|
14484
|
+
this.trimRtpRingToPreRollWindow();
|
|
14162
14485
|
}
|
|
14163
|
-
this.trimRtpRingToPreRollWindow();
|
|
14164
14486
|
this.rtpRingHasKeyframe = true;
|
|
14165
14487
|
}
|
|
14166
14488
|
this.rtpRingPrevMarker = marker;
|
|
@@ -14429,10 +14751,10 @@ var StreamBroker = class StreamBroker {
|
|
|
14429
14751
|
*
|
|
14430
14752
|
* `push-rtp` providers deliver AnnexB-equivalents separately via
|
|
14431
14753
|
* `pushEncodedPacket()` (see `IStreamBroker` contract) — so this path
|
|
14432
|
-
* does NOT re-emit EncodedPackets; it only
|
|
14433
|
-
* keyframe/param-set flags the pre-buffer capture needs
|
|
14434
|
-
*
|
|
14435
|
-
*
|
|
14754
|
+
* does NOT re-emit EncodedPackets; it only classifies each packet for the
|
|
14755
|
+
* keyframe/param-set flags the pre-buffer capture needs
|
|
14756
|
+
* (`classifyRtpPacket`, no reassembly), keeping the AnnexB consumers
|
|
14757
|
+
* (pipe, restreamer) single-fed by the provider.
|
|
14436
14758
|
*
|
|
14437
14759
|
* The pre-buffer is captured even with zero subscribers attached — a
|
|
14438
14760
|
* viewer that connects mid-GOP must be able to replay the GOP that
|
|
@@ -14442,9 +14764,9 @@ var StreamBroker = class StreamBroker {
|
|
|
14442
14764
|
pushVideoRtp(rtpData) {
|
|
14443
14765
|
if (this.stopping) return;
|
|
14444
14766
|
this.kickRtpActivityWatchdog();
|
|
14445
|
-
this.
|
|
14446
|
-
this.
|
|
14447
|
-
this.
|
|
14767
|
+
const carried = classifyRtpPacket(this.detectedCodec === "h265" ? "h265" : "h264", rtpData);
|
|
14768
|
+
this._lastNalKeyframe = carried?.keyframe ?? false;
|
|
14769
|
+
this._lastNalParamSet = carried?.parameterSet ?? false;
|
|
14448
14770
|
this.captureRtpForPreBuffer(rtpData);
|
|
14449
14771
|
if (this.rtpVideoCallbacks.size === 0) return;
|
|
14450
14772
|
for (const [cb, entry] of this.rtpVideoCallbacks) try {
|
|
@@ -14454,27 +14776,6 @@ var StreamBroker = class StreamBroker {
|
|
|
14454
14776
|
this.logger?.warn("rtp video subscriber threw", { meta: { error: require_dist.errMsg(err) } });
|
|
14455
14777
|
}
|
|
14456
14778
|
}
|
|
14457
|
-
/**
|
|
14458
|
-
* Lazily build a flag-only RTP depacketizer for the `push-rtp` source
|
|
14459
|
-
* path. Unlike the native reader's depacketizer (which emits AnnexB
|
|
14460
|
-
* EncodedPackets via `handleDepacketizedNal`), this callback ONLY sets
|
|
14461
|
-
* `_lastNalKeyframe`/`_lastNalParamSet` — the push provider owns the
|
|
14462
|
-
* AnnexB plane through `pushEncodedPacket`, so re-emitting here would
|
|
14463
|
-
* double-feed the pipe/restreamer. push-rtp has no SDP `onVideoTrack`,
|
|
14464
|
-
* so the codec comes from the source declaration (`detectedCodec`, seeded
|
|
14465
|
-
* in `start()`). Dropped on teardown (`resetRtpPipeline`) so a redial
|
|
14466
|
-
* starts with clean FU reassembly state.
|
|
14467
|
-
*/
|
|
14468
|
-
ensurePushRtpFlagDepacketizer() {
|
|
14469
|
-
if (!this.pushRtpFlagDepacketizer) {
|
|
14470
|
-
const codec = this.detectedCodec === "h265" ? "h265" : "h264";
|
|
14471
|
-
this.pushRtpFlagDepacketizer = new RtpDepacketizer(codec, ({ keyframe, parameterSet }) => {
|
|
14472
|
-
if (keyframe) this._lastNalKeyframe = true;
|
|
14473
|
-
if (parameterSet) this._lastNalParamSet = true;
|
|
14474
|
-
});
|
|
14475
|
-
}
|
|
14476
|
-
return this.pushRtpFlagDepacketizer;
|
|
14477
|
-
}
|
|
14478
14779
|
/** Toggle the per-device streaming debug flag. */
|
|
14479
14780
|
setStreamingDebug(enabled) {
|
|
14480
14781
|
this.streamingDebug = enabled;
|
|
@@ -14521,6 +14822,7 @@ var StreamBroker = class StreamBroker {
|
|
|
14521
14822
|
tag,
|
|
14522
14823
|
accept: options?.accept
|
|
14523
14824
|
});
|
|
14825
|
+
reportConsumerEvent(this.logger, "attach", "audio-chunks", this.numericDeviceId, this.audioChunkPlane.subscriberCount);
|
|
14524
14826
|
this.checkDemand();
|
|
14525
14827
|
return result;
|
|
14526
14828
|
}
|
|
@@ -14531,7 +14833,10 @@ var StreamBroker = class StreamBroker {
|
|
|
14531
14833
|
/** Release an audio-chunk subscription. Returns `true` when it existed. */
|
|
14532
14834
|
unsubscribeAudioChunks(subscriptionId) {
|
|
14533
14835
|
const released = this.audioChunkPlane.unsubscribe(subscriptionId);
|
|
14534
|
-
if (released)
|
|
14836
|
+
if (released) {
|
|
14837
|
+
reportConsumerEvent(this.logger, "detach", "audio-chunks", this.numericDeviceId, this.audioChunkPlane.subscriberCount);
|
|
14838
|
+
this.checkDemand();
|
|
14839
|
+
}
|
|
14535
14840
|
return released;
|
|
14536
14841
|
}
|
|
14537
14842
|
getStats() {
|
|
@@ -14917,10 +15222,17 @@ var StreamBroker = class StreamBroker {
|
|
|
14917
15222
|
seq: this.videoRtpSeen,
|
|
14918
15223
|
bytes: rtpData.length
|
|
14919
15224
|
} });
|
|
14920
|
-
|
|
14921
|
-
this.
|
|
14922
|
-
const carried = this.rtpDepacketizer?.processPacket(rtpData) ?? null;
|
|
15225
|
+
const wantPlane = this.needsAnnexBPlane();
|
|
15226
|
+
if (!wantPlane && this.annexBPlaneActive) this.idleAnnexBPlane();
|
|
15227
|
+
const carried = this.annexBPlaneActive ? this.rtpDepacketizer?.processPacket(rtpData) ?? null : this.rtpDepacketizer?.classifyPacket(rtpData) ?? null;
|
|
15228
|
+
this._lastNalKeyframe = carried?.keyframe ?? false;
|
|
15229
|
+
this._lastNalParamSet = carried?.parameterSet ?? false;
|
|
14923
15230
|
const auFlags = carried ? this.rtpAuTracker.classify(carried) : null;
|
|
15231
|
+
if (wantPlane && !this.annexBPlaneActive && auFlags?.keyframeAuStart === true) {
|
|
15232
|
+
this.armAnnexBPlane();
|
|
15233
|
+
this.rtpDepacketizer?.processPacket(rtpData);
|
|
15234
|
+
}
|
|
15235
|
+
if (auFlags) this.recordVideoStats(rtpData.length, auFlags.pictureStart, auFlags.keyframeAu);
|
|
14924
15236
|
this.rtspRestreamer.pushRtpPassthrough(rtpData, auFlags?.keyframeAu ?? false, auFlags?.parameterSet ?? false);
|
|
14925
15237
|
this.captureRtpForPreBuffer(rtpData);
|
|
14926
15238
|
if (this.rtpVideoCallbacks.size > 0) for (const [cb, entry] of this.rtpVideoCallbacks) try {
|
|
@@ -15136,11 +15448,83 @@ var StreamBroker = class StreamBroker {
|
|
|
15136
15448
|
* not fed from here.
|
|
15137
15449
|
*/
|
|
15138
15450
|
handleDepacketizedNal(depacketized) {
|
|
15139
|
-
if (depacketized.keyframe) this._lastNalKeyframe = true;
|
|
15140
|
-
if (depacketized.parameterSet) this._lastNalParamSet = true;
|
|
15141
15451
|
this.annexBAssembler.push(depacketized);
|
|
15142
15452
|
}
|
|
15143
15453
|
/**
|
|
15454
|
+
* Does anything consume the Annex-B plane right now? An encoded subscriber
|
|
15455
|
+
* (WebRTC Annex-B, fragments, a decode session), a pipe client, a clip ring
|
|
15456
|
+
* (it retains access units continuously, so it needs them built even with
|
|
15457
|
+
* nobody attached), or a restreamer still in Annex-B mode (no camera SDP yet).
|
|
15458
|
+
* The RTSP restream and the WebRTC RTP path are served from the source RTP
|
|
15459
|
+
* and never count. Cheap — four field reads — because it runs per packet.
|
|
15460
|
+
*/
|
|
15461
|
+
needsAnnexBPlane() {
|
|
15462
|
+
return this.encodedCallbacks.size > 0 || this.clipBuffer !== null || this.pipeServer.getClientCount() > 0 || !this.rtspRestreamer.rtpPassthroughMode;
|
|
15463
|
+
}
|
|
15464
|
+
/** The consumers that want the plane, for the arm/idle lines. */
|
|
15465
|
+
annexBPlaneConsumers() {
|
|
15466
|
+
return {
|
|
15467
|
+
encodedSubscribers: this.encodedCallbacks.size,
|
|
15468
|
+
clipRing: this.clipBuffer !== null,
|
|
15469
|
+
pipeClients: this.pipeServer.getClientCount(),
|
|
15470
|
+
restreamerAnnexB: !this.rtspRestreamer.rtpPassthroughMode
|
|
15471
|
+
};
|
|
15472
|
+
}
|
|
15473
|
+
/**
|
|
15474
|
+
* Start building access units from the keyframe AU the current packet opens.
|
|
15475
|
+
* The depacketizer and the assembler are clean here (reset when the plane
|
|
15476
|
+
* went idle, or never used), so nothing stale precedes that keyframe.
|
|
15477
|
+
*/
|
|
15478
|
+
armAnnexBPlane() {
|
|
15479
|
+
this.annexBPlaneActive = true;
|
|
15480
|
+
this.logger?.info("annex-b plane armed at a keyframe access unit", {
|
|
15481
|
+
tags: { deviceId: this.numericDeviceId },
|
|
15482
|
+
meta: this.annexBPlaneConsumers()
|
|
15483
|
+
});
|
|
15484
|
+
}
|
|
15485
|
+
/**
|
|
15486
|
+
* Stop building access units: the last consumer left. The picture in
|
|
15487
|
+
* progress is dropped, not flushed — there is nobody to hand it to, and a
|
|
15488
|
+
* partial picture must never reach the next consumer's clean start. Logged,
|
|
15489
|
+
* once per transition: a plane that is idle on purpose must not read as a
|
|
15490
|
+
* stream that stopped.
|
|
15491
|
+
*/
|
|
15492
|
+
idleAnnexBPlane() {
|
|
15493
|
+
this.annexBPlaneActive = false;
|
|
15494
|
+
this.rtpDepacketizer?.reset();
|
|
15495
|
+
this.annexBAssembler.reset();
|
|
15496
|
+
this.logger?.info("annex-b plane idle — no consumer, NAL reassembly skipped", { tags: { deviceId: this.numericDeviceId } });
|
|
15497
|
+
}
|
|
15498
|
+
/**
|
|
15499
|
+
* One place for the input stats, fed from whichever path owns the pictures:
|
|
15500
|
+
* the source RTP packets while the native reader is up (`onVideoRtp`), the
|
|
15501
|
+
* `EncodedPacket`s otherwise (`pushEncodedPacket`). `bytes` accumulate per
|
|
15502
|
+
* call; `inputFps`, `packetCount` and the IDR interval advance once per
|
|
15503
|
+
* picture — the window is closed on a picture start so a window of N frames
|
|
15504
|
+
* reads N, exactly as it did when every call was one access unit.
|
|
15505
|
+
*/
|
|
15506
|
+
recordVideoStats(bytes, pictureStart, keyframe) {
|
|
15507
|
+
this.totalBytes += bytes;
|
|
15508
|
+
this.bytesInWindow += bytes;
|
|
15509
|
+
if (!pictureStart) return;
|
|
15510
|
+
this.packetCount++;
|
|
15511
|
+
this.packetsInWindow++;
|
|
15512
|
+
const now = Date.now();
|
|
15513
|
+
this.lastPacketAt = now;
|
|
15514
|
+
const windowMs = now - this.windowStartMs;
|
|
15515
|
+
if (windowMs >= 2e3) {
|
|
15516
|
+
this.bitrateKbps = Math.round(this.bytesInWindow * 8 / windowMs);
|
|
15517
|
+
this.encodedInputFps = this.packetsInWindow / windowMs * 1e3;
|
|
15518
|
+
this.bytesInWindow = 0;
|
|
15519
|
+
this.packetsInWindow = 0;
|
|
15520
|
+
this.windowStartMs = now;
|
|
15521
|
+
}
|
|
15522
|
+
if (keyframe) {
|
|
15523
|
+
if (this.lastKeyframeMs > 0) this.idrIntervalMs = now - this.lastKeyframeMs;
|
|
15524
|
+
this.lastKeyframeMs = now;
|
|
15525
|
+
}
|
|
15526
|
+
}
|
|
15527
|
+
/**
|
|
15144
15528
|
* One assembled access unit → one `EncodedPacket` for every Annex-B
|
|
15145
15529
|
* consumer (prebuffer, clip ring, pipe, WebRTC). A keyframe packet is
|
|
15146
15530
|
* self-contained (VPS/SPS/PPS + every IDR slice), which is what a decoder
|
|
@@ -15561,10 +15945,7 @@ var StreamBroker = class StreamBroker {
|
|
|
15561
15945
|
this.annexBAssembler.flush();
|
|
15562
15946
|
this.annexBAssembler.reset();
|
|
15563
15947
|
this.rtpAuTracker.reset();
|
|
15564
|
-
|
|
15565
|
-
this.pushRtpFlagDepacketizer.reset();
|
|
15566
|
-
this.pushRtpFlagDepacketizer = null;
|
|
15567
|
-
}
|
|
15948
|
+
this.annexBPlaneActive = false;
|
|
15568
15949
|
this.resetRtpPreBuffer();
|
|
15569
15950
|
if (this.audioRtpDecoder) {
|
|
15570
15951
|
this.audioRtpDecoder.flush();
|
|
@@ -19282,6 +19663,10 @@ var StreamBrokerManager = class StreamBrokerManager {
|
|
|
19282
19663
|
return { killed: broker.killClient(input.channel, input.handle) };
|
|
19283
19664
|
}
|
|
19284
19665
|
/** Raw broker instances — internal use only. */
|
|
19666
|
+
/** Live egress transcodes (ffmpeg children) — the `[broker-mem]` probe. */
|
|
19667
|
+
getEgressTranscodeCount() {
|
|
19668
|
+
return this.egressTranscodeManager?.activeCount() ?? 0;
|
|
19669
|
+
}
|
|
19285
19670
|
getBrokerInstances() {
|
|
19286
19671
|
return [...this.brokers.values()];
|
|
19287
19672
|
}
|
|
@@ -35154,6 +35539,7 @@ var BrokerWebrtcServer = class {
|
|
|
35154
35539
|
}
|
|
35155
35540
|
};
|
|
35156
35541
|
this.sessions.set(sessionId, entry);
|
|
35542
|
+
reportConsumerEvent(this.logger, "attach", `webrtc:${entry.consumerKind}`, entry.rebind?.deviceId, this.sessions.size);
|
|
35157
35543
|
session.onClosed = () => this.cleanupSession(sessionId);
|
|
35158
35544
|
const locateRecorded = this.locateRecordedSegment;
|
|
35159
35545
|
const readRecorded = this.readRecordedSegmentBytes;
|
|
@@ -36371,6 +36757,7 @@ var BrokerWebrtcServer = class {
|
|
|
36371
36757
|
const entry = this.sessions.get(sessionId);
|
|
36372
36758
|
if (!entry) return;
|
|
36373
36759
|
this.sessions.delete(sessionId);
|
|
36760
|
+
reportConsumerEvent(this.logger, "detach", `webrtc:${entry.consumerKind}`, entry.rebind?.deviceId, this.sessions.size);
|
|
36374
36761
|
entry.pendingSwitch?.cancel();
|
|
36375
36762
|
entry.pendingSwitch = void 0;
|
|
36376
36763
|
let brokerStillServed = false;
|
|
@@ -37092,6 +37479,45 @@ function dialAnswerFromError(err) {
|
|
|
37092
37479
|
};
|
|
37093
37480
|
}
|
|
37094
37481
|
//#endregion
|
|
37482
|
+
//#region src/stream-broker/broker-mem-report.ts
|
|
37483
|
+
/** Linux only: elsewhere (or on a read error) the native view is simply absent. */
|
|
37484
|
+
async function readProc() {
|
|
37485
|
+
if (process.platform !== "linux") return {
|
|
37486
|
+
status: null,
|
|
37487
|
+
maps: null
|
|
37488
|
+
};
|
|
37489
|
+
const [status, maps] = await Promise.all([(0, node_fs_promises.readFile)("/proc/self/status", "utf8").then(parseProcStatus, () => null), (0, node_fs_promises.readFile)("/proc/self/maps", "utf8").then(parseProcMaps, () => null)]);
|
|
37490
|
+
return {
|
|
37491
|
+
status,
|
|
37492
|
+
maps
|
|
37493
|
+
};
|
|
37494
|
+
}
|
|
37495
|
+
async function emitOnce(deps) {
|
|
37496
|
+
const now = Date.now();
|
|
37497
|
+
const proc = await readProc();
|
|
37498
|
+
const resources = process.getActiveResourcesInfo();
|
|
37499
|
+
const line = formatBrokerMem({
|
|
37500
|
+
samples: deps.manager.getBrokerInstances().map((b) => b.getMemSample(now)),
|
|
37501
|
+
webrtcSessions: deps.webrtcSessions(),
|
|
37502
|
+
egressTranscodes: deps.manager.getEgressTranscodeCount(),
|
|
37503
|
+
childProcesses: countActiveResources(resources, "ProcessWrap"),
|
|
37504
|
+
proc,
|
|
37505
|
+
churn: consumerChurn.drain(),
|
|
37506
|
+
copied: readCopyOwnedTotals()
|
|
37507
|
+
});
|
|
37508
|
+
deps.logger.info(line);
|
|
37509
|
+
}
|
|
37510
|
+
/** Start the five-minute `[broker-mem]` heartbeat; returns its stop function. */
|
|
37511
|
+
function startBrokerMemReport(deps) {
|
|
37512
|
+
const timer = setInterval(() => {
|
|
37513
|
+
emitOnce(deps).catch((err) => {
|
|
37514
|
+
deps.logger.warn("broker-mem probe failed", { meta: { error: err instanceof Error ? err.message : String(err) } });
|
|
37515
|
+
});
|
|
37516
|
+
}, BROKER_MEM_INTERVAL_MS);
|
|
37517
|
+
timer.unref();
|
|
37518
|
+
return () => clearInterval(timer);
|
|
37519
|
+
}
|
|
37520
|
+
//#endregion
|
|
37095
37521
|
//#region src/stream-broker/event-loop-stall-monitor.ts
|
|
37096
37522
|
var DEFAULT_INTERVAL_MS = 100;
|
|
37097
37523
|
var DEFAULT_THRESHOLD_MS = 250;
|
|
@@ -37266,6 +37692,8 @@ var StreamBrokerAddon = class extends require_dist.BaseAddon {
|
|
|
37266
37692
|
* leaks those sockets and wedges the next runner's werift ICE agent
|
|
37267
37693
|
* (DTLS never connects until a full hub restart). */
|
|
37268
37694
|
webrtcServer = null;
|
|
37695
|
+
/** Stops the five-minute `[broker-mem]` attribution heartbeat. */
|
|
37696
|
+
stopBrokerMemReport = null;
|
|
37269
37697
|
/** Names this process's event-loop stalls — see onInitialize. */
|
|
37270
37698
|
loopStallMonitor = null;
|
|
37271
37699
|
/** The `log-channels` provider for this process. Stopped on shutdown so a
|
|
@@ -37623,6 +38051,11 @@ var StreamBrokerAddon = class extends require_dist.BaseAddon {
|
|
|
37623
38051
|
this.webrtcServer = webrtcServer;
|
|
37624
38052
|
const webrtcSessionProvider = new WebrtcSessionProvider(webrtcServer, this.brokerManager);
|
|
37625
38053
|
this.metricsSnapshotTimer = setInterval(() => this.emitBrokerMetricsSnapshot(), BROKER_METRICS_SNAPSHOT_INTERVAL_MS);
|
|
38054
|
+
this.stopBrokerMemReport = startBrokerMemReport({
|
|
38055
|
+
logger: this.ctx.logger,
|
|
38056
|
+
manager: this.brokerManager,
|
|
38057
|
+
webrtcSessions: () => webrtcServer.getSessionCount()
|
|
38058
|
+
});
|
|
37626
38059
|
const widgetsProvider = { listWidgets: async () => [{
|
|
37627
38060
|
tab: "device-tab",
|
|
37628
38061
|
label: "Stream Brokers",
|
|
@@ -37727,6 +38160,8 @@ var StreamBrokerAddon = class extends require_dist.BaseAddon {
|
|
|
37727
38160
|
async onShutdown() {
|
|
37728
38161
|
this.logChannels?.stop();
|
|
37729
38162
|
this.logChannels = null;
|
|
38163
|
+
this.stopBrokerMemReport?.();
|
|
38164
|
+
this.stopBrokerMemReport = null;
|
|
37730
38165
|
this.loopStallMonitor?.stop();
|
|
37731
38166
|
this.loopStallMonitor = null;
|
|
37732
38167
|
this.flushProbeSnapshots(Date.now(), true);
|