Files
Piik/tests/candidate-quality-probe.test.ts
T
AAA be311fb618 fix(media): fix quality convergence and video track ratcheting (#334)
* Fix quality convergence evidence lifecycle

* Own outbound video track generations
2026-08-30 10:16:45 +08:00

340 lines
11 KiB
TypeScript

import { describe, expect, it } from "vitest";
import {
P2pQualityProbe,
SfuQualityProbe,
p2pCandidateDoesNotRegress,
sfuCandidateDoesNotRegress,
} from "../src/client/media/candidate-quality-probe.ts";
import {
EMPTY_METRICS,
type ConnectionMetrics,
} from "../src/client/types.ts";
function metrics(
timestampMs: number,
overrides: Partial<ConnectionMetrics> = {},
): ConnectionMetrics {
return {
...EMPTY_METRICS,
sampleTimestampMs: timestampMs,
sampleWindowMs: 2_000,
frameWidth: 1920,
frameHeight: 1080,
framesPerSecond: 60,
bitrateKbps: 5_000,
intervalFramesDecoded: 120,
intervalFreezeCount: 0,
intervalFreezeDurationMs: 0,
intervalPauseCount: 0,
intervalPauseDurationMs: 0,
...overrides,
};
}
describe("SFU quality probe", () => {
it("accepts only complete same-Viewer non-regressing windows", () => {
const current = metrics(2_000);
expect(sfuCandidateDoesNotRegress(current, metrics(2_100))).toBe(true);
expect(
sfuCandidateDoesNotRegress(
current,
metrics(2_100, { bitrateKbps: 4_999 }),
),
).toBe(false);
expect(
sfuCandidateDoesNotRegress(
current,
metrics(2_100, { frameHeight: 720 }),
),
).toBe(false);
expect(
sfuCandidateDoesNotRegress(
current,
metrics(2_100, { intervalFreezeCount: 1 }),
),
).toBe(false);
expect(
sfuCandidateDoesNotRegress(
current,
metrics(8_000),
),
).toBeNull();
expect(
sfuCandidateDoesNotRegress(
current,
metrics(2_100, { bitrateKbps: null }),
),
).toBeNull();
});
it("accepts clean P2P receive windows that do not regress", () => {
const current = metrics(2_000, {
frameWidth: 320,
frameHeight: 180,
framesPerSecond: 10,
bitrateKbps: 200,
});
expect(
p2pCandidateDoesNotRegress(
current,
metrics(2_100, {
frameWidth: 1_280,
frameHeight: 720,
framesPerSecond: 30,
bitrateKbps: 150,
}),
),
).toBe(true);
expect(
p2pCandidateDoesNotRegress(
{ ...current, bitrateKbps: null },
metrics(2_100, {
frameWidth: 1_280,
frameHeight: 720,
framesPerSecond: 30,
bitrateKbps: null,
}),
),
).toBe(true);
expect(p2pCandidateDoesNotRegress(current, { ...current })).toBe(true);
expect(
p2pCandidateDoesNotRegress(
current,
metrics(2_100, {
frameWidth: 1_280,
frameHeight: 720,
framesPerSecond: 5,
}),
),
).toBe(false);
expect(
p2pCandidateDoesNotRegress(
current,
metrics(2_100, {
frameWidth: 160,
frameHeight: 90,
framesPerSecond: 60,
}),
),
).toBe(false);
expect(
p2pCandidateDoesNotRegress(
current,
metrics(2_100, { intervalFreezeCount: 1 }),
),
).toBe(false);
});
it("keeps nearby but non-overlapping samples pending", () => {
const probe = new P2pQualityProbe();
const current = (timestampMs: number) =>
metrics(timestampMs, {
sampleWindowMs: 1_000,
frameWidth: 320,
frameHeight: 180,
framesPerSecond: 10,
});
const candidate = (timestampMs: number) =>
metrics(timestampMs, {
sampleWindowMs: 1_000,
frameWidth: 1_280,
frameHeight: 720,
framesPerSecond: 30,
});
expect(
p2pCandidateDoesNotRegress(current(10_000), candidate(11_100)),
).toBeNull();
for (const timestampMs of [10_000, 13_000, 16_000]) {
expect(
probe.observe(current(timestampMs), candidate(timestampMs + 1_100)),
).toBe("pending");
}
});
it("compares overlapping samples", () => {
const current = metrics(10_000, {
sampleWindowMs: 1_000,
frameWidth: 320,
frameHeight: 180,
framesPerSecond: 10,
});
const candidate = metrics(10_900, {
sampleWindowMs: 1_000,
frameWidth: 1_280,
frameHeight: 720,
framesPerSecond: 30,
});
expect(p2pCandidateDoesNotRegress(current, candidate)).toBe(true);
});
it("requires three consecutive non-regressing P2P windows", () => {
const probe = new P2pQualityProbe();
expect(probe.observe(metrics(1_000), metrics(1_100))).toBe("pending");
expect(probe.observe(metrics(3_000), metrics(3_100))).toBe("pending");
expect(probe.observe(metrics(5_000), metrics(5_100))).toBe("approved");
probe.reset();
expect(probe.observe(metrics(7_000), metrics(7_100))).toBe("pending");
});
it("rejects after three comparable regressing P2P windows", () => {
const probe = new P2pQualityProbe();
const candidate = (timestampMs: number) =>
metrics(timestampMs, { framesPerSecond: 30 });
expect(probe.observe(metrics(1_000), candidate(1_100))).toBe("pending");
expect(probe.observe(metrics(3_000), candidate(3_100))).toBe("pending");
expect(probe.observe(metrics(5_000), candidate(5_100))).toBe("rejected");
});
it("keeps unknown evidence pending instead of counting it as rejection", () => {
const probe = new P2pQualityProbe();
expect(probe.observe(metrics(1_000), metrics(1_100))).toBe("pending");
expect(probe.observe(metrics(3_000), metrics(3_100))).toBe("pending");
expect(
probe.observe(
metrics(5_000),
metrics(5_100, { intervalFreezeCount: null }),
),
).toBe("pending");
expect(probe.observe(metrics(7_000), metrics(7_100))).toBe("pending");
});
it("requires three consecutive windows and resets after a regression", () => {
const probe = new SfuQualityProbe();
expect(probe.observe(metrics(1_000), metrics(1_100))).toBe("pending");
expect(probe.observe(metrics(3_000), metrics(3_100))).toBe("pending");
expect(
probe.observe(metrics(5_000), metrics(5_100, { framesPerSecond: 30 })),
).toBe("pending");
expect(probe.observe(metrics(7_000), metrics(7_100))).toBe("pending");
expect(probe.observe(metrics(9_000), metrics(9_100))).toBe("pending");
expect(probe.observe(metrics(11_000), metrics(11_100))).toBe("approved");
});
it("resets when either side lacks a new comparable window", () => {
const probe = new SfuQualityProbe();
expect(probe.observe(metrics(1_000), metrics(1_100))).toBe("pending");
expect(probe.observe(metrics(3_000), metrics(3_100))).toBe("pending");
expect(probe.observe(metrics(3_000), metrics(5_100))).toBe("pending");
expect(probe.observe(metrics(7_000), metrics(7_100))).toBe("pending");
expect(probe.observe(metrics(9_000), metrics(9_100))).toBe("pending");
expect(probe.observe(metrics(11_000), metrics(11_100))).toBe("approved");
probe.reset();
expect(probe.observe(metrics(13_000), metrics(13_100))).toBe("pending");
expect(
probe.observe(
metrics(15_000),
metrics(15_100, { intervalFreezeDurationMs: null }),
),
).toBe("pending");
expect(probe.observe(metrics(17_000), metrics(17_100))).toBe("pending");
});
});
describe("zero-frame current route", () => {
const zeroFrames = (timestampMs: number) =>
metrics(timestampMs, { intervalFramesDecoded: 0 });
const degraded = (timestampMs: number) =>
metrics(timestampMs, {
frameWidth: 320,
frameHeight: 180,
framesPerSecond: 10,
});
it("approves clean delivery on both candidate route types", () => {
for (const probe of [new P2pQualityProbe(), new SfuQualityProbe()]) {
expect(probe.observe(zeroFrames(1_000), metrics(1_100))).toBe("pending");
expect(probe.observe(zeroFrames(3_000), metrics(3_100))).toBe("pending");
expect(probe.observe(zeroFrames(5_000), metrics(5_100))).toBe("approved");
}
});
it("continues a real improvement when the current route then stops decoding", () => {
const probe = new P2pQualityProbe();
expect(probe.observe(degraded(1_000), metrics(1_100))).toBe("pending");
expect(probe.observe(zeroFrames(3_000), metrics(3_100))).toBe("pending");
expect(probe.observe(zeroFrames(5_000), metrics(5_100))).toBe("approved");
});
it("never approves when both routes stop decoding", () => {
for (const probe of [new P2pQualityProbe(), new SfuQualityProbe()]) {
for (let index = 0; index < 15; index += 1) {
const timestampMs = 1_000 + index * 2_000;
expect(
probe.observe(zeroFrames(timestampMs), zeroFrames(timestampMs + 100)),
).toBe("pending");
}
}
});
it("rejects a candidate that decodes only with freezes", () => {
for (const probe of [new P2pQualityProbe(), new SfuQualityProbe()]) {
for (const timestampMs of [1_000, 3_000]) {
expect(
probe.observe(
zeroFrames(timestampMs),
metrics(timestampMs + 100, { intervalFreezeCount: 1 }),
),
).toBe("pending");
}
expect(
probe.observe(
zeroFrames(5_000),
metrics(5_100, { intervalFreezeCount: 1 }),
),
).toBe("rejected");
}
});
it("keeps incomplete and non-overlapping candidates unknown", () => {
const incomplete = new SfuQualityProbe();
for (const timestampMs of [1_000, 3_000, 5_000]) {
expect(
incomplete.observe(
zeroFrames(timestampMs),
metrics(timestampMs + 100, { bitrateKbps: null }),
),
).toBe("pending");
}
const nonOverlapping = new P2pQualityProbe();
for (const timestampMs of [1_000, 4_000, 7_000]) {
expect(
nonOverlapping.observe(
zeroFrames(timestampMs),
metrics(timestampMs + 2_100),
),
).toBe("pending");
}
});
it("clears accumulated evidence when the current window is missing", () => {
const probe = new P2pQualityProbe();
expect(probe.observe(degraded(1_000), metrics(1_100))).toBe("pending");
expect(probe.observe(degraded(3_000), metrics(3_100))).toBe("pending");
expect(probe.observe(null, metrics(5_100))).toBe("pending");
expect(probe.observe(degraded(7_000), metrics(7_100))).toBe("pending");
expect(probe.observe(degraded(9_000), metrics(9_100))).toBe("pending");
expect(probe.observe(degraded(11_000), metrics(11_100))).toBe("approved");
});
it("ignores a duplicate timestamp but clears a late candidate window", () => {
const probe = new P2pQualityProbe();
expect(probe.observe(degraded(1_000), metrics(1_100))).toBe("pending");
expect(probe.observe(degraded(3_000), metrics(3_100))).toBe("pending");
expect(probe.observe(degraded(5_000), metrics(3_100))).toBe("pending");
expect(probe.observe(degraded(7_000), metrics(7_100))).toBe("approved");
probe.reset();
expect(probe.observe(degraded(9_000), metrics(9_100))).toBe("pending");
expect(probe.observe(degraded(11_000), metrics(11_100))).toBe("pending");
expect(probe.observe(degraded(13_000), metrics(10_100))).toBe("pending");
expect(probe.observe(degraded(15_000), metrics(15_100))).toBe("pending");
expect(probe.observe(degraded(17_000), metrics(17_100))).toBe("pending");
expect(probe.observe(degraded(19_000), metrics(19_100))).toBe("approved");
});
});