@camstack/addon-pipeline 1.2.365 → 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/stream-broker/index.js +316 -1
- package/dist/stream-broker/index.mjs +316 -1
- package/package.json +1 -1
|
@@ -3085,6 +3085,199 @@ 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
|
|
3088
3281
|
//#region src/stream-broker/stream-broker/owned-buffer.ts
|
|
3089
3282
|
/**
|
|
3090
3283
|
* Buffers that enter a LONG-LIVED ring own their backing store.
|
|
@@ -3144,6 +3337,7 @@ function detachFromPool(buf) {
|
|
|
3144
3337
|
function copyOwned(src, start, end) {
|
|
3145
3338
|
const owned = Buffer.allocUnsafeSlow(end - start);
|
|
3146
3339
|
src.copy(owned, 0, start, end);
|
|
3340
|
+
recordCopyOwned(end - start);
|
|
3147
3341
|
return owned;
|
|
3148
3342
|
}
|
|
3149
3343
|
//#endregion
|
|
@@ -3756,6 +3950,26 @@ var RtspRestreamer = class {
|
|
|
3756
3950
|
for (const session of this.sessions.values()) if (session.isPrestreamIntent()) n += 1;
|
|
3757
3951
|
return n;
|
|
3758
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
|
+
}
|
|
3759
3973
|
/** Per-session diagnostic snapshots consumed by `StreamBroker.listClients`. */
|
|
3760
3974
|
listSessionInfos() {
|
|
3761
3975
|
return [...this.sessions.values()].map((s) => s.getInfo());
|
|
@@ -3767,6 +3981,7 @@ var RtspRestreamer = class {
|
|
|
3767
3981
|
streamName: this.streamName,
|
|
3768
3982
|
total: this.sessions.size
|
|
3769
3983
|
} });
|
|
3984
|
+
reportConsumerEvent(this.logger, "attach", "rtsp", this.deviceId, this.sessions.size);
|
|
3770
3985
|
this._onSessionCountChanged?.();
|
|
3771
3986
|
this.reconcileSentryAudio();
|
|
3772
3987
|
if (this._rtpPassthroughMode) setTimeout(() => this.servePendingSessionsRtp(), 0);
|
|
@@ -4231,6 +4446,7 @@ var RtspRestreamer = class {
|
|
|
4231
4446
|
streamName: this.streamName,
|
|
4232
4447
|
total: this.sessions.size
|
|
4233
4448
|
} });
|
|
4449
|
+
reportConsumerEvent(this.logger, "detach", "rtsp", this.deviceId, this.sessions.size);
|
|
4234
4450
|
this._onSessionCountChanged?.();
|
|
4235
4451
|
this.reconcileSentryAudio();
|
|
4236
4452
|
}
|
|
@@ -4500,6 +4716,10 @@ var RtspSession = class {
|
|
|
4500
4716
|
this.onClose?.();
|
|
4501
4717
|
});
|
|
4502
4718
|
}
|
|
4719
|
+
/** Bytes the socket has accepted but not yet flushed (corked bytes included). */
|
|
4720
|
+
getWriteQueueBytes() {
|
|
4721
|
+
return this.socket.writableLength;
|
|
4722
|
+
}
|
|
4503
4723
|
getSessionId() {
|
|
4504
4724
|
return this.sessionId;
|
|
4505
4725
|
}
|
|
@@ -6453,6 +6673,10 @@ var EgressTranscodeManager = class {
|
|
|
6453
6673
|
/** In-flight starts, keyed by sharing key — concurrent acquires share one. */
|
|
6454
6674
|
pending = /* @__PURE__ */ new Map();
|
|
6455
6675
|
releaseGraceMs;
|
|
6676
|
+
/** Live egress transcodes (ffmpeg children), for the `[broker-mem]` probe. */
|
|
6677
|
+
activeCount() {
|
|
6678
|
+
return this.entries.size;
|
|
6679
|
+
}
|
|
6456
6680
|
constructor(opts) {
|
|
6457
6681
|
this.opts = opts;
|
|
6458
6682
|
this.releaseGraceMs = opts.releaseGraceMs ?? DEFAULT_RELEASE_GRACE_MS;
|
|
@@ -12816,6 +13040,8 @@ var StreamBroker = class StreamBroker {
|
|
|
12816
13040
|
* timeout was chosen against.
|
|
12817
13041
|
*/
|
|
12818
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;
|
|
12819
13045
|
manualStop = false;
|
|
12820
13046
|
stopping = false;
|
|
12821
13047
|
pipeServer;
|
|
@@ -13090,6 +13316,33 @@ var StreamBroker = class StreamBroker {
|
|
|
13090
13316
|
return this.lastPacketAt;
|
|
13091
13317
|
}
|
|
13092
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
|
+
/**
|
|
13093
13346
|
* Whether a LAZY publisher is being built right now.
|
|
13094
13347
|
*
|
|
13095
13348
|
* A consumer deciding whether it is worth waiting for this source needs to
|
|
@@ -14052,6 +14305,7 @@ var StreamBroker = class StreamBroker {
|
|
|
14052
14305
|
packetsDelivered: 0
|
|
14053
14306
|
};
|
|
14054
14307
|
this.encodedCallbacks.set(callback, entry);
|
|
14308
|
+
reportConsumerEvent(this.logger, "attach", attribution.kind, this.numericDeviceId, this.encodedCallbacks.size + this.rtpVideoCallbacks.size);
|
|
14055
14309
|
if (this.streamingDebug) this.logger?.info("encoded subscriber added", { meta: {
|
|
14056
14310
|
total: this.encodedCallbacks.size,
|
|
14057
14311
|
subscriberId: entry.id,
|
|
@@ -14062,6 +14316,7 @@ var StreamBroker = class StreamBroker {
|
|
|
14062
14316
|
this.checkDemand();
|
|
14063
14317
|
return () => {
|
|
14064
14318
|
this.encodedCallbacks.delete(callback);
|
|
14319
|
+
reportConsumerEvent(this.logger, "detach", attribution.kind, this.numericDeviceId, this.encodedCallbacks.size + this.rtpVideoCallbacks.size);
|
|
14065
14320
|
if (this.streamingDebug) this.logger?.info("encoded subscriber removed", { meta: {
|
|
14066
14321
|
total: this.encodedCallbacks.size,
|
|
14067
14322
|
subscriberId: entry.id
|
|
@@ -14156,6 +14411,7 @@ var StreamBroker = class StreamBroker {
|
|
|
14156
14411
|
packetsDelivered: 0
|
|
14157
14412
|
};
|
|
14158
14413
|
this.rtpVideoCallbacks.set(callback, entry);
|
|
14414
|
+
reportConsumerEvent(this.logger, "attach", attribution.kind, this.numericDeviceId, this.encodedCallbacks.size + this.rtpVideoCallbacks.size);
|
|
14159
14415
|
if (this.streamingDebug || CH_RTP.on) this.logger?.info("rtp video subscriber added", { meta: {
|
|
14160
14416
|
total: this.rtpVideoCallbacks.size,
|
|
14161
14417
|
subscriberId: entry.id,
|
|
@@ -14165,6 +14421,7 @@ var StreamBroker = class StreamBroker {
|
|
|
14165
14421
|
this.checkDemand();
|
|
14166
14422
|
return () => {
|
|
14167
14423
|
this.rtpVideoCallbacks.delete(callback);
|
|
14424
|
+
reportConsumerEvent(this.logger, "detach", attribution.kind, this.numericDeviceId, this.encodedCallbacks.size + this.rtpVideoCallbacks.size);
|
|
14168
14425
|
if (this.streamingDebug || CH_RTP.on) this.logger?.info("rtp video subscriber removed", { meta: {
|
|
14169
14426
|
total: this.rtpVideoCallbacks.size,
|
|
14170
14427
|
subscriberId: entry.id
|
|
@@ -14565,6 +14822,7 @@ var StreamBroker = class StreamBroker {
|
|
|
14565
14822
|
tag,
|
|
14566
14823
|
accept: options?.accept
|
|
14567
14824
|
});
|
|
14825
|
+
reportConsumerEvent(this.logger, "attach", "audio-chunks", this.numericDeviceId, this.audioChunkPlane.subscriberCount);
|
|
14568
14826
|
this.checkDemand();
|
|
14569
14827
|
return result;
|
|
14570
14828
|
}
|
|
@@ -14575,7 +14833,10 @@ var StreamBroker = class StreamBroker {
|
|
|
14575
14833
|
/** Release an audio-chunk subscription. Returns `true` when it existed. */
|
|
14576
14834
|
unsubscribeAudioChunks(subscriptionId) {
|
|
14577
14835
|
const released = this.audioChunkPlane.unsubscribe(subscriptionId);
|
|
14578
|
-
if (released)
|
|
14836
|
+
if (released) {
|
|
14837
|
+
reportConsumerEvent(this.logger, "detach", "audio-chunks", this.numericDeviceId, this.audioChunkPlane.subscriberCount);
|
|
14838
|
+
this.checkDemand();
|
|
14839
|
+
}
|
|
14579
14840
|
return released;
|
|
14580
14841
|
}
|
|
14581
14842
|
getStats() {
|
|
@@ -19402,6 +19663,10 @@ var StreamBrokerManager = class StreamBrokerManager {
|
|
|
19402
19663
|
return { killed: broker.killClient(input.channel, input.handle) };
|
|
19403
19664
|
}
|
|
19404
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
|
+
}
|
|
19405
19670
|
getBrokerInstances() {
|
|
19406
19671
|
return [...this.brokers.values()];
|
|
19407
19672
|
}
|
|
@@ -35274,6 +35539,7 @@ var BrokerWebrtcServer = class {
|
|
|
35274
35539
|
}
|
|
35275
35540
|
};
|
|
35276
35541
|
this.sessions.set(sessionId, entry);
|
|
35542
|
+
reportConsumerEvent(this.logger, "attach", `webrtc:${entry.consumerKind}`, entry.rebind?.deviceId, this.sessions.size);
|
|
35277
35543
|
session.onClosed = () => this.cleanupSession(sessionId);
|
|
35278
35544
|
const locateRecorded = this.locateRecordedSegment;
|
|
35279
35545
|
const readRecorded = this.readRecordedSegmentBytes;
|
|
@@ -36491,6 +36757,7 @@ var BrokerWebrtcServer = class {
|
|
|
36491
36757
|
const entry = this.sessions.get(sessionId);
|
|
36492
36758
|
if (!entry) return;
|
|
36493
36759
|
this.sessions.delete(sessionId);
|
|
36760
|
+
reportConsumerEvent(this.logger, "detach", `webrtc:${entry.consumerKind}`, entry.rebind?.deviceId, this.sessions.size);
|
|
36494
36761
|
entry.pendingSwitch?.cancel();
|
|
36495
36762
|
entry.pendingSwitch = void 0;
|
|
36496
36763
|
let brokerStillServed = false;
|
|
@@ -37212,6 +37479,45 @@ function dialAnswerFromError(err) {
|
|
|
37212
37479
|
};
|
|
37213
37480
|
}
|
|
37214
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
|
|
37215
37521
|
//#region src/stream-broker/event-loop-stall-monitor.ts
|
|
37216
37522
|
var DEFAULT_INTERVAL_MS = 100;
|
|
37217
37523
|
var DEFAULT_THRESHOLD_MS = 250;
|
|
@@ -37386,6 +37692,8 @@ var StreamBrokerAddon = class extends require_dist.BaseAddon {
|
|
|
37386
37692
|
* leaks those sockets and wedges the next runner's werift ICE agent
|
|
37387
37693
|
* (DTLS never connects until a full hub restart). */
|
|
37388
37694
|
webrtcServer = null;
|
|
37695
|
+
/** Stops the five-minute `[broker-mem]` attribution heartbeat. */
|
|
37696
|
+
stopBrokerMemReport = null;
|
|
37389
37697
|
/** Names this process's event-loop stalls — see onInitialize. */
|
|
37390
37698
|
loopStallMonitor = null;
|
|
37391
37699
|
/** The `log-channels` provider for this process. Stopped on shutdown so a
|
|
@@ -37743,6 +38051,11 @@ var StreamBrokerAddon = class extends require_dist.BaseAddon {
|
|
|
37743
38051
|
this.webrtcServer = webrtcServer;
|
|
37744
38052
|
const webrtcSessionProvider = new WebrtcSessionProvider(webrtcServer, this.brokerManager);
|
|
37745
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
|
+
});
|
|
37746
38059
|
const widgetsProvider = { listWidgets: async () => [{
|
|
37747
38060
|
tab: "device-tab",
|
|
37748
38061
|
label: "Stream Brokers",
|
|
@@ -37847,6 +38160,8 @@ var StreamBrokerAddon = class extends require_dist.BaseAddon {
|
|
|
37847
38160
|
async onShutdown() {
|
|
37848
38161
|
this.logChannels?.stop();
|
|
37849
38162
|
this.logChannels = null;
|
|
38163
|
+
this.stopBrokerMemReport?.();
|
|
38164
|
+
this.stopBrokerMemReport = null;
|
|
37850
38165
|
this.loopStallMonitor?.stop();
|
|
37851
38166
|
this.loopStallMonitor = null;
|
|
37852
38167
|
this.flushProbeSnapshots(Date.now(), true);
|
|
@@ -3081,6 +3081,199 @@ 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
|
|
3084
3277
|
//#region src/stream-broker/stream-broker/owned-buffer.ts
|
|
3085
3278
|
/**
|
|
3086
3279
|
* Buffers that enter a LONG-LIVED ring own their backing store.
|
|
@@ -3140,6 +3333,7 @@ function detachFromPool(buf) {
|
|
|
3140
3333
|
function copyOwned(src, start, end) {
|
|
3141
3334
|
const owned = Buffer.allocUnsafeSlow(end - start);
|
|
3142
3335
|
src.copy(owned, 0, start, end);
|
|
3336
|
+
recordCopyOwned(end - start);
|
|
3143
3337
|
return owned;
|
|
3144
3338
|
}
|
|
3145
3339
|
//#endregion
|
|
@@ -3752,6 +3946,26 @@ var RtspRestreamer = class {
|
|
|
3752
3946
|
for (const session of this.sessions.values()) if (session.isPrestreamIntent()) n += 1;
|
|
3753
3947
|
return n;
|
|
3754
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
|
+
}
|
|
3755
3969
|
/** Per-session diagnostic snapshots consumed by `StreamBroker.listClients`. */
|
|
3756
3970
|
listSessionInfos() {
|
|
3757
3971
|
return [...this.sessions.values()].map((s) => s.getInfo());
|
|
@@ -3763,6 +3977,7 @@ var RtspRestreamer = class {
|
|
|
3763
3977
|
streamName: this.streamName,
|
|
3764
3978
|
total: this.sessions.size
|
|
3765
3979
|
} });
|
|
3980
|
+
reportConsumerEvent(this.logger, "attach", "rtsp", this.deviceId, this.sessions.size);
|
|
3766
3981
|
this._onSessionCountChanged?.();
|
|
3767
3982
|
this.reconcileSentryAudio();
|
|
3768
3983
|
if (this._rtpPassthroughMode) setTimeout(() => this.servePendingSessionsRtp(), 0);
|
|
@@ -4227,6 +4442,7 @@ var RtspRestreamer = class {
|
|
|
4227
4442
|
streamName: this.streamName,
|
|
4228
4443
|
total: this.sessions.size
|
|
4229
4444
|
} });
|
|
4445
|
+
reportConsumerEvent(this.logger, "detach", "rtsp", this.deviceId, this.sessions.size);
|
|
4230
4446
|
this._onSessionCountChanged?.();
|
|
4231
4447
|
this.reconcileSentryAudio();
|
|
4232
4448
|
}
|
|
@@ -4496,6 +4712,10 @@ var RtspSession = class {
|
|
|
4496
4712
|
this.onClose?.();
|
|
4497
4713
|
});
|
|
4498
4714
|
}
|
|
4715
|
+
/** Bytes the socket has accepted but not yet flushed (corked bytes included). */
|
|
4716
|
+
getWriteQueueBytes() {
|
|
4717
|
+
return this.socket.writableLength;
|
|
4718
|
+
}
|
|
4499
4719
|
getSessionId() {
|
|
4500
4720
|
return this.sessionId;
|
|
4501
4721
|
}
|
|
@@ -6449,6 +6669,10 @@ var EgressTranscodeManager = class {
|
|
|
6449
6669
|
/** In-flight starts, keyed by sharing key — concurrent acquires share one. */
|
|
6450
6670
|
pending = /* @__PURE__ */ new Map();
|
|
6451
6671
|
releaseGraceMs;
|
|
6672
|
+
/** Live egress transcodes (ffmpeg children), for the `[broker-mem]` probe. */
|
|
6673
|
+
activeCount() {
|
|
6674
|
+
return this.entries.size;
|
|
6675
|
+
}
|
|
6452
6676
|
constructor(opts) {
|
|
6453
6677
|
this.opts = opts;
|
|
6454
6678
|
this.releaseGraceMs = opts.releaseGraceMs ?? DEFAULT_RELEASE_GRACE_MS;
|
|
@@ -12812,6 +13036,8 @@ var StreamBroker = class StreamBroker {
|
|
|
12812
13036
|
* timeout was chosen against.
|
|
12813
13037
|
*/
|
|
12814
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;
|
|
12815
13041
|
manualStop = false;
|
|
12816
13042
|
stopping = false;
|
|
12817
13043
|
pipeServer;
|
|
@@ -13086,6 +13312,33 @@ var StreamBroker = class StreamBroker {
|
|
|
13086
13312
|
return this.lastPacketAt;
|
|
13087
13313
|
}
|
|
13088
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
|
+
/**
|
|
13089
13342
|
* Whether a LAZY publisher is being built right now.
|
|
13090
13343
|
*
|
|
13091
13344
|
* A consumer deciding whether it is worth waiting for this source needs to
|
|
@@ -14048,6 +14301,7 @@ var StreamBroker = class StreamBroker {
|
|
|
14048
14301
|
packetsDelivered: 0
|
|
14049
14302
|
};
|
|
14050
14303
|
this.encodedCallbacks.set(callback, entry);
|
|
14304
|
+
reportConsumerEvent(this.logger, "attach", attribution.kind, this.numericDeviceId, this.encodedCallbacks.size + this.rtpVideoCallbacks.size);
|
|
14051
14305
|
if (this.streamingDebug) this.logger?.info("encoded subscriber added", { meta: {
|
|
14052
14306
|
total: this.encodedCallbacks.size,
|
|
14053
14307
|
subscriberId: entry.id,
|
|
@@ -14058,6 +14312,7 @@ var StreamBroker = class StreamBroker {
|
|
|
14058
14312
|
this.checkDemand();
|
|
14059
14313
|
return () => {
|
|
14060
14314
|
this.encodedCallbacks.delete(callback);
|
|
14315
|
+
reportConsumerEvent(this.logger, "detach", attribution.kind, this.numericDeviceId, this.encodedCallbacks.size + this.rtpVideoCallbacks.size);
|
|
14061
14316
|
if (this.streamingDebug) this.logger?.info("encoded subscriber removed", { meta: {
|
|
14062
14317
|
total: this.encodedCallbacks.size,
|
|
14063
14318
|
subscriberId: entry.id
|
|
@@ -14152,6 +14407,7 @@ var StreamBroker = class StreamBroker {
|
|
|
14152
14407
|
packetsDelivered: 0
|
|
14153
14408
|
};
|
|
14154
14409
|
this.rtpVideoCallbacks.set(callback, entry);
|
|
14410
|
+
reportConsumerEvent(this.logger, "attach", attribution.kind, this.numericDeviceId, this.encodedCallbacks.size + this.rtpVideoCallbacks.size);
|
|
14155
14411
|
if (this.streamingDebug || CH_RTP.on) this.logger?.info("rtp video subscriber added", { meta: {
|
|
14156
14412
|
total: this.rtpVideoCallbacks.size,
|
|
14157
14413
|
subscriberId: entry.id,
|
|
@@ -14161,6 +14417,7 @@ var StreamBroker = class StreamBroker {
|
|
|
14161
14417
|
this.checkDemand();
|
|
14162
14418
|
return () => {
|
|
14163
14419
|
this.rtpVideoCallbacks.delete(callback);
|
|
14420
|
+
reportConsumerEvent(this.logger, "detach", attribution.kind, this.numericDeviceId, this.encodedCallbacks.size + this.rtpVideoCallbacks.size);
|
|
14164
14421
|
if (this.streamingDebug || CH_RTP.on) this.logger?.info("rtp video subscriber removed", { meta: {
|
|
14165
14422
|
total: this.rtpVideoCallbacks.size,
|
|
14166
14423
|
subscriberId: entry.id
|
|
@@ -14561,6 +14818,7 @@ var StreamBroker = class StreamBroker {
|
|
|
14561
14818
|
tag,
|
|
14562
14819
|
accept: options?.accept
|
|
14563
14820
|
});
|
|
14821
|
+
reportConsumerEvent(this.logger, "attach", "audio-chunks", this.numericDeviceId, this.audioChunkPlane.subscriberCount);
|
|
14564
14822
|
this.checkDemand();
|
|
14565
14823
|
return result;
|
|
14566
14824
|
}
|
|
@@ -14571,7 +14829,10 @@ var StreamBroker = class StreamBroker {
|
|
|
14571
14829
|
/** Release an audio-chunk subscription. Returns `true` when it existed. */
|
|
14572
14830
|
unsubscribeAudioChunks(subscriptionId) {
|
|
14573
14831
|
const released = this.audioChunkPlane.unsubscribe(subscriptionId);
|
|
14574
|
-
if (released)
|
|
14832
|
+
if (released) {
|
|
14833
|
+
reportConsumerEvent(this.logger, "detach", "audio-chunks", this.numericDeviceId, this.audioChunkPlane.subscriberCount);
|
|
14834
|
+
this.checkDemand();
|
|
14835
|
+
}
|
|
14575
14836
|
return released;
|
|
14576
14837
|
}
|
|
14577
14838
|
getStats() {
|
|
@@ -19398,6 +19659,10 @@ var StreamBrokerManager = class StreamBrokerManager {
|
|
|
19398
19659
|
return { killed: broker.killClient(input.channel, input.handle) };
|
|
19399
19660
|
}
|
|
19400
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
|
+
}
|
|
19401
19666
|
getBrokerInstances() {
|
|
19402
19667
|
return [...this.brokers.values()];
|
|
19403
19668
|
}
|
|
@@ -35270,6 +35535,7 @@ var BrokerWebrtcServer = class {
|
|
|
35270
35535
|
}
|
|
35271
35536
|
};
|
|
35272
35537
|
this.sessions.set(sessionId, entry);
|
|
35538
|
+
reportConsumerEvent(this.logger, "attach", `webrtc:${entry.consumerKind}`, entry.rebind?.deviceId, this.sessions.size);
|
|
35273
35539
|
session.onClosed = () => this.cleanupSession(sessionId);
|
|
35274
35540
|
const locateRecorded = this.locateRecordedSegment;
|
|
35275
35541
|
const readRecorded = this.readRecordedSegmentBytes;
|
|
@@ -36487,6 +36753,7 @@ var BrokerWebrtcServer = class {
|
|
|
36487
36753
|
const entry = this.sessions.get(sessionId);
|
|
36488
36754
|
if (!entry) return;
|
|
36489
36755
|
this.sessions.delete(sessionId);
|
|
36756
|
+
reportConsumerEvent(this.logger, "detach", `webrtc:${entry.consumerKind}`, entry.rebind?.deviceId, this.sessions.size);
|
|
36490
36757
|
entry.pendingSwitch?.cancel();
|
|
36491
36758
|
entry.pendingSwitch = void 0;
|
|
36492
36759
|
let brokerStillServed = false;
|
|
@@ -37208,6 +37475,45 @@ function dialAnswerFromError(err) {
|
|
|
37208
37475
|
};
|
|
37209
37476
|
}
|
|
37210
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
|
|
37211
37517
|
//#region src/stream-broker/event-loop-stall-monitor.ts
|
|
37212
37518
|
var DEFAULT_INTERVAL_MS = 100;
|
|
37213
37519
|
var DEFAULT_THRESHOLD_MS = 250;
|
|
@@ -37382,6 +37688,8 @@ var StreamBrokerAddon = class extends BaseAddon {
|
|
|
37382
37688
|
* leaks those sockets and wedges the next runner's werift ICE agent
|
|
37383
37689
|
* (DTLS never connects until a full hub restart). */
|
|
37384
37690
|
webrtcServer = null;
|
|
37691
|
+
/** Stops the five-minute `[broker-mem]` attribution heartbeat. */
|
|
37692
|
+
stopBrokerMemReport = null;
|
|
37385
37693
|
/** Names this process's event-loop stalls — see onInitialize. */
|
|
37386
37694
|
loopStallMonitor = null;
|
|
37387
37695
|
/** The `log-channels` provider for this process. Stopped on shutdown so a
|
|
@@ -37739,6 +38047,11 @@ var StreamBrokerAddon = class extends BaseAddon {
|
|
|
37739
38047
|
this.webrtcServer = webrtcServer;
|
|
37740
38048
|
const webrtcSessionProvider = new WebrtcSessionProvider(webrtcServer, this.brokerManager);
|
|
37741
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
|
+
});
|
|
37742
38055
|
const widgetsProvider = { listWidgets: async () => [{
|
|
37743
38056
|
tab: "device-tab",
|
|
37744
38057
|
label: "Stream Brokers",
|
|
@@ -37843,6 +38156,8 @@ var StreamBrokerAddon = class extends BaseAddon {
|
|
|
37843
38156
|
async onShutdown() {
|
|
37844
38157
|
this.logChannels?.stop();
|
|
37845
38158
|
this.logChannels = null;
|
|
38159
|
+
this.stopBrokerMemReport?.();
|
|
38160
|
+
this.stopBrokerMemReport = null;
|
|
37846
38161
|
this.loopStallMonitor?.stop();
|
|
37847
38162
|
this.loopStallMonitor = null;
|
|
37848
38163
|
this.flushProbeSnapshots(Date.now(), true);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@camstack/addon-pipeline",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.366",
|
|
4
4
|
"description": "Pipeline bundle — runner, detection, motion, audio + stream broker. Multi-entry npm package shipping pipeline addons under a single bundle.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"camstack",
|