@observertc/observer-js 1.0.0-beta.16 → 1.0.0-beta.17
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/README.md +174 -21
- package/dist/index.d.mts +429 -135
- package/dist/index.d.ts +429 -135
- package/dist/index.js +274 -101
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +271 -100
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -37,8 +37,8 @@ function createLogger(moduleName) {
|
|
|
37
37
|
}
|
|
38
38
|
}();
|
|
39
39
|
}
|
|
40
|
-
function setObserverLogger(
|
|
41
|
-
mainLogger =
|
|
40
|
+
function setObserverLogger(logger10) {
|
|
41
|
+
mainLogger = logger10;
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
// src/ObservedCall.ts
|
|
@@ -3456,7 +3456,6 @@ var UnconsumedTrackDetector = class _UnconsumedTrackDetector {
|
|
|
3456
3456
|
type: UnconsumedTrackTypes.unconsumedPublishedTrack,
|
|
3457
3457
|
timestamp: now,
|
|
3458
3458
|
payload: {
|
|
3459
|
-
type: UnconsumedTrackTypes.unconsumedPublishedTrack,
|
|
3460
3459
|
trackId: outboundTrack.id,
|
|
3461
3460
|
kind: outboundTrack.kind,
|
|
3462
3461
|
publisherClientId: peerConnection?.client.clientId,
|
|
@@ -3602,7 +3601,6 @@ var TrackDeliveryMismatchDetector = class _TrackDeliveryMismatchDetector {
|
|
|
3602
3601
|
type,
|
|
3603
3602
|
timestamp: now,
|
|
3604
3603
|
payload: {
|
|
3605
|
-
type,
|
|
3606
3604
|
trackId: outboundTrackId,
|
|
3607
3605
|
publisherClientId: delivery.publisherClientId,
|
|
3608
3606
|
publisherSending: delivery.publisherSending,
|
|
@@ -3855,12 +3853,9 @@ var CallConcurrentIssueDetector = class _CallConcurrentIssueDetector {
|
|
|
3855
3853
|
this._call.addIssue({
|
|
3856
3854
|
type: issueType,
|
|
3857
3855
|
timestamp: now,
|
|
3856
|
+
conclusion,
|
|
3858
3857
|
payload: {
|
|
3859
|
-
type: issueType,
|
|
3860
3858
|
issueType: type,
|
|
3861
|
-
scope: "call",
|
|
3862
|
-
conclusion,
|
|
3863
|
-
callId: this._call.callId,
|
|
3864
3859
|
clients: group.totalClients,
|
|
3865
3860
|
affectedClients: group.clientIds.length,
|
|
3866
3861
|
affectedRatio: group.affectedRatio,
|
|
@@ -3984,10 +3979,9 @@ var IssueFanOutDetector = class _IssueFanOutDetector {
|
|
|
3984
3979
|
this._call.addIssue({
|
|
3985
3980
|
type,
|
|
3986
3981
|
timestamp: now,
|
|
3982
|
+
conclusion,
|
|
3987
3983
|
payload: {
|
|
3988
|
-
type,
|
|
3989
3984
|
issueType,
|
|
3990
|
-
conclusion,
|
|
3991
3985
|
trackId: publisher.id,
|
|
3992
3986
|
kind: publisher.kind,
|
|
3993
3987
|
publisherClientId: publisher.getPeerConnection().client.clientId,
|
|
@@ -4134,18 +4128,14 @@ var PublisherFaultCorroborationDetector = class _PublisherFaultCorroborationDete
|
|
|
4134
4128
|
this._call.addIssue({
|
|
4135
4129
|
type: PublisherFaultTypes.corroboratedPublisherFault,
|
|
4136
4130
|
timestamp: now,
|
|
4137
|
-
|
|
4138
|
-
|
|
4139
|
-
|
|
4140
|
-
|
|
4141
|
-
|
|
4142
|
-
|
|
4143
|
-
|
|
4144
|
-
|
|
4145
|
-
// Higher than any single-ended finding: two independent parties, one conclusion.
|
|
4146
|
-
confidence: 0.9
|
|
4147
|
-
}
|
|
4148
|
-
}
|
|
4131
|
+
conclusion: {
|
|
4132
|
+
faultDomain: "published-track",
|
|
4133
|
+
summary: `${fault.publisherClientId} reports ${fault.publisherIssueTypes.join(", ")} on track ${fault.trackId} while ${fault.affectedReceivers} of ${fault.receivers} subscribers report ${fault.receiverIssueTypes.join(", ")} \u2014 both ends agree`,
|
|
4134
|
+
recommendation: "the source is implicated, not inferred: check that publisher's capture, encoder and uplink before looking at the SFU or the receivers",
|
|
4135
|
+
// Higher than any single-ended finding: two independent parties, one conclusion.
|
|
4136
|
+
confidence: 0.9
|
|
4137
|
+
},
|
|
4138
|
+
payload: { ...fault }
|
|
4149
4139
|
});
|
|
4150
4140
|
}
|
|
4151
4141
|
}
|
|
@@ -4201,6 +4191,14 @@ var ObservedCall = class extends EventEmitter3 {
|
|
|
4201
4191
|
value: void 0
|
|
4202
4192
|
};
|
|
4203
4193
|
remoteTrackResolver;
|
|
4194
|
+
/**
|
|
4195
|
+
* The accumulating record of this call's life, or `undefined` when no summary was configured.
|
|
4196
|
+
*
|
|
4197
|
+
* Live — read it at any point during the call. It is also delivered once on `call-summary` when
|
|
4198
|
+
* the call closes. See `CallSummary`: an absent section means "not collected", never "nothing
|
|
4199
|
+
* happened".
|
|
4200
|
+
*/
|
|
4201
|
+
summary;
|
|
4204
4202
|
/**
|
|
4205
4203
|
* Published tracks that currently have **no** subscriber linked to them.
|
|
4206
4204
|
*
|
|
@@ -4278,6 +4276,21 @@ var ObservedCall = class extends EventEmitter3 {
|
|
|
4278
4276
|
this.detectors.add(detector);
|
|
4279
4277
|
return this;
|
|
4280
4278
|
}
|
|
4279
|
+
/**
|
|
4280
|
+
* Start accumulating this call's summary, if the observer was configured for summaries.
|
|
4281
|
+
*
|
|
4282
|
+
* Called by `createObservedCall`; you should not need it. It takes no configuration of its own on
|
|
4283
|
+
* purpose: the collector subscribes to exactly the events the observer's `include` requires, so a
|
|
4284
|
+
* per-call section outside that set would be created and then never written to — an empty section
|
|
4285
|
+
* that reads as "nothing happened". One shape per observer is the only shape that can be filled.
|
|
4286
|
+
*
|
|
4287
|
+
* The collector builds it rather than this method, so the resolved configuration never has to
|
|
4288
|
+
* leave the one object that owns it. Returns `undefined` when summaries are off, and is
|
|
4289
|
+
* idempotent: an existing summary is kept, not restarted.
|
|
4290
|
+
*/
|
|
4291
|
+
enableSummary() {
|
|
4292
|
+
return this.summary ??= this.observer.callSummaryCollector?.createSummary(this.callId);
|
|
4293
|
+
}
|
|
4281
4294
|
/**
|
|
4282
4295
|
* Remove a detector from **this call** by name, returning how many were removed.
|
|
4283
4296
|
*
|
|
@@ -4302,12 +4315,13 @@ var ObservedCall = class extends EventEmitter3 {
|
|
|
4302
4315
|
/**
|
|
4303
4316
|
* Raise a call-level (server-side) finding; surfaced on the Observer bus as `call-issue`.
|
|
4304
4317
|
*
|
|
4305
|
-
* `payload`
|
|
4306
|
-
* serialise for.
|
|
4318
|
+
* `payload` is an **object** and holds evidence only — it is delivered to an in-process handler,
|
|
4319
|
+
* so there is nothing to serialise for. `scope` is stamped here, and the `callId` is already on
|
|
4320
|
+
* the event, so neither belongs in the payload. Put the interpretation in `conclusion`.
|
|
4307
4321
|
*/
|
|
4308
4322
|
addIssue(issue) {
|
|
4309
4323
|
if (this.closed) return;
|
|
4310
|
-
this._notify("call-issue", { ...this.eventScope, issue });
|
|
4324
|
+
this._notify("call-issue", { ...this.eventScope, issue: { ...issue, scope: "call" } });
|
|
4311
4325
|
}
|
|
4312
4326
|
close() {
|
|
4313
4327
|
if (this.closed) return;
|
|
@@ -4328,6 +4342,10 @@ var ObservedCall = class extends EventEmitter3 {
|
|
|
4328
4342
|
if (this.startedAt === void 0) this.startedAt = minSampleTimestamps;
|
|
4329
4343
|
if (this.endedAt === void 0) this.endedAt = maxSampleTimestamps;
|
|
4330
4344
|
this.closedAt = Date.now();
|
|
4345
|
+
if (this.summary) {
|
|
4346
|
+
this.observer.callSummaryCollector?.finalise(this);
|
|
4347
|
+
this._notify("call-summary", { ...this.eventScope, summary: this.summary });
|
|
4348
|
+
}
|
|
4331
4349
|
this.detectors.clear();
|
|
4332
4350
|
this.activeIssuesRegistry.clear();
|
|
4333
4351
|
this.emit("close");
|
|
@@ -5088,6 +5106,166 @@ var ActiveIssuesRegistry = class {
|
|
|
5088
5106
|
}
|
|
5089
5107
|
};
|
|
5090
5108
|
|
|
5109
|
+
// src/summaries/CallSummary.ts
|
|
5110
|
+
var defaultCallSummaryConfig = {
|
|
5111
|
+
include: [],
|
|
5112
|
+
maxIssues: 500,
|
|
5113
|
+
maxClientIds: 1e4
|
|
5114
|
+
};
|
|
5115
|
+
function createCallSummary(callId, config) {
|
|
5116
|
+
const summary = { callId, attachments: {} };
|
|
5117
|
+
if (config.include.includes("clients")) {
|
|
5118
|
+
summary.clients = { clientIds: [], peak: 0, joined: 0, left: 0 };
|
|
5119
|
+
}
|
|
5120
|
+
if (config.include.includes("issues")) {
|
|
5121
|
+
summary.issues = [];
|
|
5122
|
+
}
|
|
5123
|
+
if (config.include.includes("turnServers")) {
|
|
5124
|
+
summary.turnServers = { serverUrls: [], clientsRelayed: 0 };
|
|
5125
|
+
}
|
|
5126
|
+
if (config.include.includes("scores")) {
|
|
5127
|
+
summary.scores = { samples: 0 };
|
|
5128
|
+
}
|
|
5129
|
+
return summary;
|
|
5130
|
+
}
|
|
5131
|
+
|
|
5132
|
+
// src/summaries/CallSummaryCollector.ts
|
|
5133
|
+
var logger8 = createLogger("CallSummaryCollector");
|
|
5134
|
+
var CallSummaryCollector = class {
|
|
5135
|
+
constructor(_observer, _config) {
|
|
5136
|
+
this._observer = _observer;
|
|
5137
|
+
this._config = _config;
|
|
5138
|
+
this._subscribeBuiltIns();
|
|
5139
|
+
this._subscribeEnrichers();
|
|
5140
|
+
}
|
|
5141
|
+
_observer;
|
|
5142
|
+
_config;
|
|
5143
|
+
_scratch = /* @__PURE__ */ new WeakMap();
|
|
5144
|
+
_listeners = [];
|
|
5145
|
+
_closed = false;
|
|
5146
|
+
/**
|
|
5147
|
+
* Build a summary for `callId` and start tracking it.
|
|
5148
|
+
*
|
|
5149
|
+
* Creating it here, rather than letting the call create one and hand it over, keeps the resolved
|
|
5150
|
+
* configuration inside the single object that owns it — and makes it impossible to end up with a
|
|
5151
|
+
* summary whose sections nobody subscribed to fill.
|
|
5152
|
+
*/
|
|
5153
|
+
createSummary(callId) {
|
|
5154
|
+
const summary = createCallSummary(callId, this._config);
|
|
5155
|
+
this._scratch.set(summary, { scores: [], turnClientIds: /* @__PURE__ */ new Set() });
|
|
5156
|
+
return summary;
|
|
5157
|
+
}
|
|
5158
|
+
/**
|
|
5159
|
+
* Finalise `call`'s summary: fold in what only makes sense once, and stamp the closing times.
|
|
5160
|
+
*
|
|
5161
|
+
* Percentiles are computed here rather than on every update — a median recomputed per tick over a
|
|
5162
|
+
* growing array is quadratic work to produce a number nobody reads until the end.
|
|
5163
|
+
*/
|
|
5164
|
+
finalise(call) {
|
|
5165
|
+
const summary = call.summary;
|
|
5166
|
+
if (!summary) return;
|
|
5167
|
+
const scratch = this._scratch.get(summary);
|
|
5168
|
+
summary.startedAt = call.startedAt;
|
|
5169
|
+
summary.endedAt = call.endedAt;
|
|
5170
|
+
summary.durationInMs = summary.startedAt !== void 0 && summary.endedAt !== void 0 ? Math.max(0, summary.endedAt - summary.startedAt) : void 0;
|
|
5171
|
+
summary.closedAt = Date.now();
|
|
5172
|
+
if (summary.scores && scratch) {
|
|
5173
|
+
summary.scores.samples = scratch.scores.length;
|
|
5174
|
+
if (0 < scratch.scores.length) {
|
|
5175
|
+
summary.scores.min = Math.min(...scratch.scores);
|
|
5176
|
+
summary.scores.max = Math.max(...scratch.scores);
|
|
5177
|
+
summary.scores.median = percentile(scratch.scores, 0.5);
|
|
5178
|
+
}
|
|
5179
|
+
}
|
|
5180
|
+
if (summary.turnServers && scratch) {
|
|
5181
|
+
summary.turnServers.clientsRelayed = scratch.turnClientIds.size;
|
|
5182
|
+
}
|
|
5183
|
+
}
|
|
5184
|
+
/** Drop every bus subscription. Called when the observer closes. */
|
|
5185
|
+
close() {
|
|
5186
|
+
if (this._closed) return;
|
|
5187
|
+
this._closed = true;
|
|
5188
|
+
for (const { event, listener } of this._listeners) {
|
|
5189
|
+
this._observer.off(event, listener);
|
|
5190
|
+
}
|
|
5191
|
+
this._listeners.length = 0;
|
|
5192
|
+
}
|
|
5193
|
+
/**
|
|
5194
|
+
* Subscribe `listener` to `event`, routed to the summary of the call the event names.
|
|
5195
|
+
*
|
|
5196
|
+
* The `observedCall` is read off the payload rather than closed over, which is what lets one
|
|
5197
|
+
* subscription serve every call.
|
|
5198
|
+
*/
|
|
5199
|
+
_on(event, handler) {
|
|
5200
|
+
const listener = (...args) => {
|
|
5201
|
+
const summary = args[0].observedCall.summary;
|
|
5202
|
+
if (!summary) return;
|
|
5203
|
+
const scratch = this._scratch.get(summary);
|
|
5204
|
+
if (!scratch) return;
|
|
5205
|
+
try {
|
|
5206
|
+
handler(summary, scratch, ...args);
|
|
5207
|
+
} catch (err) {
|
|
5208
|
+
logger8.warn("A call-summary handler for %s threw; continuing. %o", event, err);
|
|
5209
|
+
}
|
|
5210
|
+
};
|
|
5211
|
+
this._observer.on(event, listener);
|
|
5212
|
+
this._listeners.push({ event, listener });
|
|
5213
|
+
}
|
|
5214
|
+
_subscribeBuiltIns() {
|
|
5215
|
+
const include = this._config.include;
|
|
5216
|
+
if (include.includes("clients")) {
|
|
5217
|
+
this._on("client-added", (summary, _scratch, { observedCall, observedClient }) => {
|
|
5218
|
+
const clients = summary.clients;
|
|
5219
|
+
if (!clients) return;
|
|
5220
|
+
clients.joined += 1;
|
|
5221
|
+
clients.peak = Math.max(clients.peak, observedCall.observedClients.size);
|
|
5222
|
+
if (clients.clientIds.length < this._config.maxClientIds) {
|
|
5223
|
+
clients.clientIds.push(observedClient.clientId);
|
|
5224
|
+
} else {
|
|
5225
|
+
summary.truncated = { ...summary.truncated, clientIds: (summary.truncated?.clientIds ?? 0) + 1 };
|
|
5226
|
+
}
|
|
5227
|
+
});
|
|
5228
|
+
this._on("client-closed", (summary) => {
|
|
5229
|
+
if (summary.clients) summary.clients.left += 1;
|
|
5230
|
+
});
|
|
5231
|
+
}
|
|
5232
|
+
if (include.includes("issues")) {
|
|
5233
|
+
this._on("call-issue", (summary, _scratch, { issue }) => {
|
|
5234
|
+
const issues = summary.issues;
|
|
5235
|
+
if (!issues) return;
|
|
5236
|
+
if (issues.length < this._config.maxIssues) issues.push(issue);
|
|
5237
|
+
else summary.truncated = { ...summary.truncated, issues: (summary.truncated?.issues ?? 0) + 1 };
|
|
5238
|
+
});
|
|
5239
|
+
}
|
|
5240
|
+
if (include.includes("scores") || include.includes("turnServers")) {
|
|
5241
|
+
this._on("call-updated", (summary, scratch, { observedCall }) => {
|
|
5242
|
+
if (summary.scores && observedCall.score !== void 0) scratch.scores.push(observedCall.score);
|
|
5243
|
+
if (!summary.turnServers) return;
|
|
5244
|
+
for (const clientId of observedCall.clientsUsedTurn) scratch.turnClientIds.add(clientId);
|
|
5245
|
+
for (const client of observedCall.observedClients.values()) {
|
|
5246
|
+
for (const peerConnection of client.observedPeerConnections.values()) {
|
|
5247
|
+
for (const pair of peerConnection.selectedIceCandiadtePairForTurn) {
|
|
5248
|
+
const url = pair.getLocalCandidate()?.url;
|
|
5249
|
+
if (url && !summary.turnServers.serverUrls.includes(url)) {
|
|
5250
|
+
summary.turnServers.serverUrls.push(url);
|
|
5251
|
+
}
|
|
5252
|
+
}
|
|
5253
|
+
}
|
|
5254
|
+
}
|
|
5255
|
+
});
|
|
5256
|
+
}
|
|
5257
|
+
}
|
|
5258
|
+
_subscribeEnrichers() {
|
|
5259
|
+
const enrich = this._config.enrich;
|
|
5260
|
+
if (!enrich) return;
|
|
5261
|
+
for (const name of Object.keys(enrich)) {
|
|
5262
|
+
const enricher = enrich[name];
|
|
5263
|
+
if (!enricher) continue;
|
|
5264
|
+
this._on(name, (summary, _scratch, ...args) => enricher(summary, ...args));
|
|
5265
|
+
}
|
|
5266
|
+
}
|
|
5267
|
+
};
|
|
5268
|
+
|
|
5091
5269
|
// src/detectors/SfuCongestionDetector.ts
|
|
5092
5270
|
var SfuCongestionDetector = class _SfuCongestionDetector {
|
|
5093
5271
|
constructor(_observer, config = {}) {
|
|
@@ -5216,13 +5394,10 @@ var SfuCongestionDetector = class _SfuCongestionDetector {
|
|
|
5216
5394
|
absoluteIncrease: evaluation.absoluteIncrease,
|
|
5217
5395
|
relativeIncrease: evaluation.relativeIncrease
|
|
5218
5396
|
};
|
|
5219
|
-
this._observer.
|
|
5220
|
-
|
|
5221
|
-
|
|
5222
|
-
|
|
5223
|
-
payload
|
|
5224
|
-
},
|
|
5225
|
-
observer: this._observer
|
|
5397
|
+
this._observer.addIssue({
|
|
5398
|
+
type: this._config.emittedObserverIssueType,
|
|
5399
|
+
timestamp: Date.now(),
|
|
5400
|
+
payload: { ...payload }
|
|
5226
5401
|
});
|
|
5227
5402
|
}
|
|
5228
5403
|
/**
|
|
@@ -5336,11 +5511,9 @@ var ObserverConcurrentIssueDetector = class _ObserverConcurrentIssueDetector {
|
|
|
5336
5511
|
this._observer.addIssue({
|
|
5337
5512
|
type: issueType,
|
|
5338
5513
|
timestamp: now,
|
|
5514
|
+
conclusion,
|
|
5339
5515
|
payload: {
|
|
5340
|
-
type: issueType,
|
|
5341
5516
|
issueType: type,
|
|
5342
|
-
scope: "observer",
|
|
5343
|
-
conclusion,
|
|
5344
5517
|
clients: group.totalClients,
|
|
5345
5518
|
affectedClients: group.clientIds.length,
|
|
5346
5519
|
affectedRatio: group.affectedRatio,
|
|
@@ -5513,16 +5686,13 @@ var ClientPopulationIssueDetector = class _ClientPopulationIssueDetector {
|
|
|
5513
5686
|
this._observer.addIssue({
|
|
5514
5687
|
type: ClientPopulationIssueTypes.clientPopulationIssue,
|
|
5515
5688
|
timestamp: now,
|
|
5516
|
-
|
|
5517
|
-
|
|
5518
|
-
|
|
5519
|
-
|
|
5520
|
-
|
|
5521
|
-
|
|
5522
|
-
|
|
5523
|
-
confidence: this._confidenceOf(rollup)
|
|
5524
|
-
}
|
|
5525
|
-
}
|
|
5689
|
+
conclusion: {
|
|
5690
|
+
faultDomain: "client-population",
|
|
5691
|
+
summary: `'${issueType}' is ${this._riskText(rollup.relativeRisk)} more likely on ${population} than on the rest of the fleet (${rollup.affectedClients}/${rollup.clients} vs ${rollup.controlAffectedClients}/${rollup.controlClients})`,
|
|
5692
|
+
recommendation: "this is not an SFU symptom \u2014 look at what those clients share: a recent release, a browser version, or shared/virtualised hardware",
|
|
5693
|
+
confidence: this._confidenceOf(rollup)
|
|
5694
|
+
},
|
|
5695
|
+
payload: { ...rollup }
|
|
5526
5696
|
});
|
|
5527
5697
|
}
|
|
5528
5698
|
}
|
|
@@ -5624,7 +5794,7 @@ var TurnServerHealthDetector = class _TurnServerHealthDetector {
|
|
|
5624
5794
|
this._observer.addIssue({
|
|
5625
5795
|
type: TurnServerHealthTypes.turnServerDegraded,
|
|
5626
5796
|
timestamp: now,
|
|
5627
|
-
payload: {
|
|
5797
|
+
payload: { ...health, otherServers }
|
|
5628
5798
|
});
|
|
5629
5799
|
}
|
|
5630
5800
|
for (const serverUrl of [...this._streaks.keys()]) {
|
|
@@ -5732,7 +5902,6 @@ var TurnServerOutageDetector = class _TurnServerOutageDetector {
|
|
|
5732
5902
|
type: TurnServerOutageTypes.turnServerOutage,
|
|
5733
5903
|
timestamp: now,
|
|
5734
5904
|
payload: {
|
|
5735
|
-
type: TurnServerOutageTypes.turnServerOutage,
|
|
5736
5905
|
serverUrl,
|
|
5737
5906
|
peakClients: peak,
|
|
5738
5907
|
currentClients: live,
|
|
@@ -6006,17 +6175,13 @@ var SimulcastReceiverValidator = class _SimulcastReceiverValidator {
|
|
|
6006
6175
|
this._observer.addIssue({
|
|
6007
6176
|
type: LOWEST_COMMON_DENOMINATOR_ISSUE,
|
|
6008
6177
|
timestamp: decidedAt,
|
|
6009
|
-
|
|
6010
|
-
|
|
6011
|
-
|
|
6012
|
-
|
|
6013
|
-
|
|
6014
|
-
|
|
6015
|
-
|
|
6016
|
-
recommendation: "check that simulcast/SVC is enabled and layers are chosen per consumer, and that the SFU terminates receiver reports instead of forwarding them; this is a build/config property, not a transient",
|
|
6017
|
-
confidence: 0.8
|
|
6018
|
-
}
|
|
6019
|
-
}
|
|
6178
|
+
conclusion: {
|
|
6179
|
+
faultDomain: "infrastructure",
|
|
6180
|
+
summary: "one bad receiver is dragging a publisher's bitrate down for everyone \u2014 the SFU is not selecting layers per consumer",
|
|
6181
|
+
recommendation: "check that simulcast/SVC is enabled and layers are chosen per consumer, and that the SFU terminates receiver reports instead of forwarding them; this is a build/config property, not a transient",
|
|
6182
|
+
confidence: 0.8
|
|
6183
|
+
},
|
|
6184
|
+
payload: { checks: this._checks, ...outcome.evidence }
|
|
6020
6185
|
});
|
|
6021
6186
|
}
|
|
6022
6187
|
this.onDone(this.report);
|
|
@@ -6119,17 +6284,13 @@ var RemoteTrackResolverValidator = class _RemoteTrackResolverValidator {
|
|
|
6119
6284
|
this._observer.addIssue({
|
|
6120
6285
|
type: UNRESOLVED_TRACK_LINKS_ISSUE,
|
|
6121
6286
|
timestamp: decidedAt,
|
|
6122
|
-
|
|
6123
|
-
|
|
6124
|
-
|
|
6125
|
-
|
|
6126
|
-
|
|
6127
|
-
|
|
6128
|
-
|
|
6129
|
-
recommendation: "check the id the resolver joins on (mediasoup producerId, or your own convention) \u2014 until it links, IssueFanOutDetector, TrackDeliveryMismatchDetector, UnconsumedTrackDetector and SimulcastReceiverValidator all silently do nothing",
|
|
6130
|
-
confidence: 0.9
|
|
6131
|
-
}
|
|
6132
|
-
}
|
|
6287
|
+
conclusion: {
|
|
6288
|
+
faultDomain: "infrastructure",
|
|
6289
|
+
summary: "a RemoteTrackResolver is configured but has never linked a subscriber to a publisher",
|
|
6290
|
+
recommendation: "check the id the resolver joins on (mediasoup producerId, or your own convention) \u2014 until it links, IssueFanOutDetector, TrackDeliveryMismatchDetector, UnconsumedTrackDetector and SimulcastReceiverValidator all silently do nothing",
|
|
6291
|
+
confidence: 0.9
|
|
6292
|
+
},
|
|
6293
|
+
payload: { checks: this._checks, ...outcome.evidence }
|
|
6133
6294
|
});
|
|
6134
6295
|
}
|
|
6135
6296
|
this.onDone(this.report);
|
|
@@ -6240,17 +6401,16 @@ var CodecConsistencyValidator = class _CodecConsistencyValidator {
|
|
|
6240
6401
|
this._observer.addIssue({
|
|
6241
6402
|
type: CODEC_MISMATCH_ISSUE,
|
|
6242
6403
|
timestamp: decidedAt,
|
|
6404
|
+
conclusion: {
|
|
6405
|
+
faultDomain: "infrastructure",
|
|
6406
|
+
summary: outcome.verdict === "codec-split" ? "participants of one call are using different codecs \u2014 an SFU that forwards without transcoding cannot serve all of them" : "the deployment is consistently negotiating a codec other than the expected one",
|
|
6407
|
+
recommendation: outcome.verdict === "codec-split" ? "check codec preferences and any SDP munging; a split usually means one endpoint could not negotiate the preferred codec and the others were not renegotiated with it" : "check codec preferences and endpoint support \u2014 a silent fallback keeps working, at a higher bitrate than you budgeted for",
|
|
6408
|
+
confidence: 0.85
|
|
6409
|
+
},
|
|
6243
6410
|
payload: {
|
|
6244
|
-
type: CODEC_MISMATCH_ISSUE,
|
|
6245
6411
|
verdict: outcome.verdict,
|
|
6246
6412
|
checks: this._checks,
|
|
6247
|
-
evidence: outcome.evidence
|
|
6248
|
-
conclusion: {
|
|
6249
|
-
faultDomain: "infrastructure",
|
|
6250
|
-
summary: outcome.verdict === "codec-split" ? "participants of one call are using different codecs \u2014 an SFU that forwards without transcoding cannot serve all of them" : "the deployment is consistently negotiating a codec other than the expected one",
|
|
6251
|
-
recommendation: outcome.verdict === "codec-split" ? "check codec preferences and any SDP munging; a split usually means one endpoint could not negotiate the preferred codec and the others were not renegotiated with it" : "check codec preferences and endpoint support \u2014 a silent fallback keeps working, at a higher bitrate than you budgeted for",
|
|
6252
|
-
confidence: 0.85
|
|
6253
|
-
}
|
|
6413
|
+
evidence: outcome.evidence
|
|
6254
6414
|
}
|
|
6255
6415
|
});
|
|
6256
6416
|
}
|
|
@@ -6264,7 +6424,7 @@ function mediaKindOf(mimeType) {
|
|
|
6264
6424
|
}
|
|
6265
6425
|
|
|
6266
6426
|
// src/Observer.ts
|
|
6267
|
-
var
|
|
6427
|
+
var logger9 = createLogger("Observer");
|
|
6268
6428
|
var Observer = class extends EventEmitter6 {
|
|
6269
6429
|
observedTURN = new ObservedTURN();
|
|
6270
6430
|
observedCalls = /* @__PURE__ */ new Map();
|
|
@@ -6320,6 +6480,14 @@ var Observer = class extends EventEmitter6 {
|
|
|
6320
6480
|
* ```
|
|
6321
6481
|
*/
|
|
6322
6482
|
callDetectorConfigs = /* @__PURE__ */ new Map();
|
|
6483
|
+
/**
|
|
6484
|
+
* Owns every call's summary: the resolved `config.callSummary`, the bus subscriptions that keep
|
|
6485
|
+
* the summaries current (one per event type, not one per call), and the summaries themselves.
|
|
6486
|
+
*
|
|
6487
|
+
* `undefined` when `config.callSummary` was absent or `null` — so its presence *is* the answer to
|
|
6488
|
+
* "are summaries on", and nothing is subscribed to anything.
|
|
6489
|
+
*/
|
|
6490
|
+
callSummaryCollector;
|
|
6323
6491
|
constructor(config = {}) {
|
|
6324
6492
|
super();
|
|
6325
6493
|
this.setMaxListeners(Infinity);
|
|
@@ -6331,6 +6499,12 @@ var Observer = class extends EventEmitter6 {
|
|
|
6331
6499
|
closeClientIfIdleForMs: 6e4,
|
|
6332
6500
|
...config
|
|
6333
6501
|
};
|
|
6502
|
+
if (this.config.callSummary) {
|
|
6503
|
+
this.callSummaryCollector = new CallSummaryCollector(this, {
|
|
6504
|
+
...defaultCallSummaryConfig,
|
|
6505
|
+
...this.config.callSummary
|
|
6506
|
+
});
|
|
6507
|
+
}
|
|
6334
6508
|
}
|
|
6335
6509
|
get numberOfCalls() {
|
|
6336
6510
|
return this.observedCalls.size;
|
|
@@ -6369,7 +6543,7 @@ var Observer = class extends EventEmitter6 {
|
|
|
6369
6543
|
break;
|
|
6370
6544
|
}
|
|
6371
6545
|
default: {
|
|
6372
|
-
|
|
6546
|
+
logger9.warn("Unknown detector name %s; skipping", name);
|
|
6373
6547
|
return this;
|
|
6374
6548
|
}
|
|
6375
6549
|
}
|
|
@@ -6454,7 +6628,7 @@ var Observer = class extends EventEmitter6 {
|
|
|
6454
6628
|
break;
|
|
6455
6629
|
}
|
|
6456
6630
|
if (!validator) {
|
|
6457
|
-
|
|
6631
|
+
logger9.warn("Unknown validator name %s; skipping", name);
|
|
6458
6632
|
return this;
|
|
6459
6633
|
}
|
|
6460
6634
|
this.validators.add(validator);
|
|
@@ -6485,7 +6659,7 @@ var Observer = class extends EventEmitter6 {
|
|
|
6485
6659
|
try {
|
|
6486
6660
|
validator.cancel(reason);
|
|
6487
6661
|
} catch (err) {
|
|
6488
|
-
|
|
6662
|
+
logger9.warn("Error cancelling validator %s: %o", validator.name, err);
|
|
6489
6663
|
}
|
|
6490
6664
|
}
|
|
6491
6665
|
return matching.length;
|
|
@@ -6496,11 +6670,11 @@ var Observer = class extends EventEmitter6 {
|
|
|
6496
6670
|
}
|
|
6497
6671
|
createObservedCall(settings) {
|
|
6498
6672
|
if (this.closed) {
|
|
6499
|
-
|
|
6673
|
+
logger9.warn("Attempted to create a call (callId: %s) on a closed observer", settings.callId);
|
|
6500
6674
|
return void 0;
|
|
6501
6675
|
}
|
|
6502
6676
|
if (this.observedCalls.has(settings.callId)) {
|
|
6503
|
-
|
|
6677
|
+
logger9.warn("Observed Call with id %s already exists; returning the existing instance", settings.callId);
|
|
6504
6678
|
return this.observedCalls.get(settings.callId);
|
|
6505
6679
|
}
|
|
6506
6680
|
const callSettings = {
|
|
@@ -6515,6 +6689,7 @@ var Observer = class extends EventEmitter6 {
|
|
|
6515
6689
|
callActiveIssuesRegistry
|
|
6516
6690
|
);
|
|
6517
6691
|
observedCall.remoteTrackResolver = this.config.createRemoteTrackResolver?.(observedCall);
|
|
6692
|
+
observedCall.enableSummary();
|
|
6518
6693
|
for (const [name, detectorConfig] of this.callDetectorConfigs) {
|
|
6519
6694
|
observedCall.addDetector(name, detectorConfig);
|
|
6520
6695
|
}
|
|
@@ -6537,11 +6712,11 @@ var Observer = class extends EventEmitter6 {
|
|
|
6537
6712
|
}
|
|
6538
6713
|
createObservedMediasoupRouter(settings) {
|
|
6539
6714
|
if (this.closed) {
|
|
6540
|
-
|
|
6715
|
+
logger9.warn("Attempted to create mediasoup router (id: %d) on a closed observer", settings.router.id);
|
|
6541
6716
|
return void 0;
|
|
6542
6717
|
}
|
|
6543
6718
|
if (this.observedMediasoupRouters.has(settings.router.id)) {
|
|
6544
|
-
|
|
6719
|
+
logger9.warn("Observed Mediasoup Router (id %s) already exists; returning the existing instance", settings.router.id);
|
|
6545
6720
|
return this.observedMediasoupRouters.get(settings.router.id);
|
|
6546
6721
|
}
|
|
6547
6722
|
const observedMediasoupRouter = new ObservedMediasoupRouter(settings);
|
|
@@ -6572,13 +6747,14 @@ var Observer = class extends EventEmitter6 {
|
|
|
6572
6747
|
}
|
|
6573
6748
|
close() {
|
|
6574
6749
|
if (this.closed) {
|
|
6575
|
-
return
|
|
6750
|
+
return logger9.debug("Attempted to close twice");
|
|
6576
6751
|
}
|
|
6577
6752
|
this.closed = true;
|
|
6578
6753
|
for (const call of [...this.observedCalls.values()]) call.close();
|
|
6579
6754
|
this.detectors.clear();
|
|
6580
6755
|
for (const validator of [...this.validators]) validator.cancel("observer closed");
|
|
6581
6756
|
this.validators.clear();
|
|
6757
|
+
this.callSummaryCollector?.close();
|
|
6582
6758
|
this.activeIssuesRegistry.clear();
|
|
6583
6759
|
this._notify("observer-closed", { ...this.eventScope });
|
|
6584
6760
|
}
|
|
@@ -6589,7 +6765,7 @@ var Observer = class extends EventEmitter6 {
|
|
|
6589
6765
|
try {
|
|
6590
6766
|
this.acceptMiddlewares.process({ sample, context });
|
|
6591
6767
|
} catch (err) {
|
|
6592
|
-
|
|
6768
|
+
logger9.warn("An accept middleware threw; dropping the sample. %o", err);
|
|
6593
6769
|
}
|
|
6594
6770
|
if (!sample.callId) {
|
|
6595
6771
|
this._notify("sample-rejected", { ...this.eventScope, reason: "missing-callId", sample });
|
|
@@ -6641,7 +6817,7 @@ var Observer = class extends EventEmitter6 {
|
|
|
6641
6817
|
try {
|
|
6642
6818
|
validator.update();
|
|
6643
6819
|
} catch (err) {
|
|
6644
|
-
|
|
6820
|
+
logger9.warn("Error running validator %s: %o", validator.name, err);
|
|
6645
6821
|
}
|
|
6646
6822
|
}
|
|
6647
6823
|
this._notify("observer-updated", { ...this.eventScope });
|
|
@@ -6654,7 +6830,7 @@ var Observer = class extends EventEmitter6 {
|
|
|
6654
6830
|
*/
|
|
6655
6831
|
addIssue(issue) {
|
|
6656
6832
|
if (this.closed) return;
|
|
6657
|
-
this._notify("observer-issue", { ...this.eventScope, issue });
|
|
6833
|
+
this._notify("observer-issue", { ...this.eventScope, issue: { ...issue, scope: "observer" } });
|
|
6658
6834
|
}
|
|
6659
6835
|
/** Emit an Observer-bus event. */
|
|
6660
6836
|
_notify(type, ...args) {
|
|
@@ -6772,18 +6948,11 @@ var CallHealthAggregator = class {
|
|
|
6772
6948
|
}
|
|
6773
6949
|
};
|
|
6774
6950
|
|
|
6775
|
-
// src/common/
|
|
6776
|
-
function issuePayloadOf(issue) {
|
|
6777
|
-
const payload = issue.payload;
|
|
6778
|
-
if (payload === void 0) return void 0;
|
|
6779
|
-
return typeof payload === "string" ? parseJsonObject(payload) : payload;
|
|
6780
|
-
}
|
|
6951
|
+
// src/common/Issue.ts
|
|
6781
6952
|
function issuePayloadAsString(issue) {
|
|
6782
|
-
|
|
6783
|
-
if (payload === void 0) return void 0;
|
|
6784
|
-
if (typeof payload === "string") return payload;
|
|
6953
|
+
if (issue.payload === void 0) return void 0;
|
|
6785
6954
|
try {
|
|
6786
|
-
return JSON.stringify(payload);
|
|
6955
|
+
return JSON.stringify(issue.payload);
|
|
6787
6956
|
} catch {
|
|
6788
6957
|
return void 0;
|
|
6789
6958
|
}
|
|
@@ -7077,6 +7246,7 @@ export {
|
|
|
7077
7246
|
CallConcurrentIssueDetector,
|
|
7078
7247
|
CallConcurrentIssueTypes,
|
|
7079
7248
|
CallHealthAggregator,
|
|
7249
|
+
CallSummaryCollector,
|
|
7080
7250
|
ClientEventTypes,
|
|
7081
7251
|
ClientMetaTypes,
|
|
7082
7252
|
ClientPopulationIssueDetector,
|
|
@@ -7135,16 +7305,17 @@ export {
|
|
|
7135
7305
|
concludeObserverIssue,
|
|
7136
7306
|
correlation,
|
|
7137
7307
|
counterDelta,
|
|
7308
|
+
createCallSummary,
|
|
7138
7309
|
createDefaultMediasoupRemoteTrackResolverFactory,
|
|
7139
7310
|
createInMemorySink,
|
|
7140
7311
|
createJsonlFileSink,
|
|
7141
7312
|
createJsonlFileSinkFactory,
|
|
7142
7313
|
createLogger,
|
|
7143
7314
|
createP2pRemoteTrackResolverFactory,
|
|
7315
|
+
defaultCallSummaryConfig,
|
|
7144
7316
|
defaultClientHealthThresholds,
|
|
7145
7317
|
isClientIssueResolutionEntry,
|
|
7146
7318
|
issuePayloadAsString,
|
|
7147
|
-
issuePayloadOf,
|
|
7148
7319
|
mannKendall,
|
|
7149
7320
|
mannKendallVerdict,
|
|
7150
7321
|
median,
|