Validators
Every detector runs on every tick, because the answer legitimately changes. A validator answers a question that only changes when you deploy. So it is not configured on and left running: you start one, it runs until it can decide, reports once, and the observer drops it.
observer.addValidator("simulcast-receivers", { minChecks: 5 });
observer.on("validation-ready", ({ validator, report }) => {
if (!report.ready) return;
if (report.verdict === "layer-decided-lowest-common-denominator") page(validator, report);
});
onDeploy(() => observer.addValidator("simulcast-receivers")); // check again
observer.validators is the set currently running β normally empty, since each removes itself on
finishing. There is no revalidation timer: a deploy, not elapsed time, is what makes a structural
verdict stale, so re-checking means starting another.
The three shipped validators
| Validator | addValidator name | Question | Also raises |
|---|---|---|---|
SimulcastReceiverValidator π | simulcast-receivers | Does the SFU pick layers per receiver, or drag the publisher down to the worst one? | WORST_RECEIVER_CONTAGION |
RemoteTrackResolverValidator | remote-track-resolver | Is the resolver actually linking anything? | REMOTE_TRACK_LINKS_UNRESOLVED |
CodecConsistencyValidator | codec-consistency | Is everyone on the same codec β and is it the one you think you negotiated? | CODEC_INCONSISTENCY |
π requires a RemoteTrackResolver.
SimulcastReceiverValidator
Simulcast (or SVC) exists so one slow participant does not set everyone’s quality: with several encodings the server hands the struggling receiver a lower layer and leaves the rest alone. Without it β or with a server that relays RTCP end to end, so the publisher’s bandwidth estimate collapses to the slowest receiver β the only way to serve them is to make the source send less.
Both causes look identical from outside. What this check establishes is whether per-receiver adaptation happens at all.
verdict | Meaning |
|---|---|
layer-decided-per-receiver | Verified β a receiver fell far behind and the publisher carried on |
layer-decided-lowest-common-denominator | The publisher followed its worst receiver; everyone gets the slowest participant’s quality |
inconclusive | Cancelled, or the observer closed, before it could decide |
Not finishing is not a pass
The check only runs when a publisher has 3+ receivers and one is at most half the median; plenty of healthy deployments never present that. A validator that never sees it simply keeps running and never reports β it does not quietly succeed.
report.checks counts the times the check genuinely ran, so an inconclusive with checks: 0
says plainly that nothing was verified.
RemoteTrackResolverValidator
This exists because of a specific, nasty failure mode. Four detectors are built on publisher β
subscriber links β IssueFanOutDetector, PublisherFaultCorroborationDetector,
TrackDeliveryMismatchDetector, UnconsumedTrackDetector (plus SimulcastReceiverValidator) β
and every one of them correctly does nothing when the links are missing rather than guessing.
So a resolver wired to the wrong id field leaves all of them permanently silent, and silence is what a healthy deployment looks like too. You would conclude your calls were clean when in fact nothing was ever examined.
Verdicts: links-resolved / no-links-resolved / inconclusive.
Run it at start-up and after changing the resolver or the SFU’s id scheme.
observer.addValidator("remote-track-resolver");CodecConsistencyValidator
Answers two things at once.
A split β participants of one call on different codecs β is a real fault with a confusing symptom: an SFU that forwards without transcoding cannot serve them all, so some pairs see each other and some do not, with no error anywhere. Only something holding every participant at once can see it.
The quieter half is the silent fallback: a deployment configured for VP9 or AV1 drops to VP8
whenever one endpoint cannot negotiate the preference, the call keeps working at a higher bitrate
than budgeted, and the team believes it shipped AV1 months ago. Give it expected and it says so.
observer.addValidator("codec-consistency", {
expected: { video: "video/VP9", audio: "audio/opus" },
});Verdicts: codec-consistent / codec-split / unexpected-codec / inconclusive.
Cancelling
A check that has not decided can be stopped, by name or by instance:
observer.cancelValidator("simulcast-receivers", "sfu redeployed");
// Or one specific instance β observer.validators holds what is running.
for (const validator of observer.validators) {
observer.cancelValidator(validator, "shutting down");
}Cancelling is not silent discarding
The validator finishes inconclusive with your reason, emits validation-ready like any other
completion, and removes itself. That matters twice over: anything waiting on the verdict would
otherwise wait forever, and “we stopped asking” is a materially different outcome from “we asked
and learned nothing” β which is exactly what an inconclusive carrying a reason records.
Pass a real reason; the default tells the reader nothing they could not already infer.
observer.close() cancels whatever is still running with 'observer closed'.
A start-up validation routine
const observer = new Observer({
createRemoteTrackResolver: createDefaultMediasoupRemoteTrackResolverFactory(),
});
function validateDeployment(reason: string) {
observer
.addValidator("remote-track-resolver")
.addValidator("simulcast-receivers", { minChecks: 5 })
.addValidator("codec-consistency", { expected: { video: "video/VP9", audio: "audio/opus" } });
log.info("deployment validation started", { reason });
}
observer.on("validation-ready", ({ validator, report }) => {
log.info("validation settled", { validator, verdict: report.verdict, checks: report.checks });
switch (report.verdict) {
case "no-links-resolved":
// The most important one: every π detector is silently doing nothing.
alerting.page("remote track resolver is not linking anything", report);
break;
case "layer-decided-lowest-common-denominator":
alerting.page("one slow receiver is degrading every participant", report);
break;
case "codec-split":
case "unexpected-codec":
alerting.page("codec negotiation is not what we configured", report);
break;
case "inconclusive":
log.warn("validation could not decide", { validator, checks: report.checks });
break;
}
});
validateDeployment("process start");
onDeploy(() => validateDeployment("deploy"));Run these in staging too
All three questions β are the links wired, does per-receiver adaptation work, is the negotiated codec the intended one β are answerable with a handful of synthetic participants. Validating in staging catches the wiring mistakes before they turn into a month of silent detectors in production.