Files
Piik/tests/candidate-quality-probe.test.ts
T

236 lines
7.1 KiB
TypeScript

import { describe, expect, it } from "vitest";
import {
P2pQualityProbe,
SfuQualityProbe,
p2pCandidateStrictlyImproves,
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("requires a strict P2P receive improvement without a tradeoff", () => {
const current = metrics(2_000, {
frameWidth: 320,
frameHeight: 180,
framesPerSecond: 10,
bitrateKbps: 200,
});
expect(
p2pCandidateStrictlyImproves(
current,
metrics(2_100, {
frameWidth: 1_280,
frameHeight: 720,
framesPerSecond: 30,
bitrateKbps: 150,
}),
),
).toBe(true);
expect(
p2pCandidateStrictlyImproves(
{ ...current, bitrateKbps: null },
metrics(2_100, {
frameWidth: 1_280,
frameHeight: 720,
framesPerSecond: 30,
bitrateKbps: null,
}),
),
).toBe(true);
expect(p2pCandidateStrictlyImproves(current, { ...current })).toBe(false);
expect(
p2pCandidateStrictlyImproves(
current,
metrics(2_100, {
frameWidth: 1_280,
frameHeight: 720,
framesPerSecond: 5,
}),
),
).toBe(false);
expect(
p2pCandidateStrictlyImproves(
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(
p2pCandidateStrictlyImproves(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("keeps the original comparison for 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(p2pCandidateStrictlyImproves(current, candidate)).toBe(true);
});
it("requires three consecutive strict P2P improvements", () => {
const probe = new P2pQualityProbe();
const current = (timestampMs: number) =>
metrics(timestampMs, {
frameWidth: 320,
frameHeight: 180,
framesPerSecond: 10,
});
const candidate = (timestampMs: number) =>
metrics(timestampMs, {
frameWidth: 1_280,
frameHeight: 720,
framesPerSecond: 30,
});
expect(probe.observe(current(1_000), candidate(1_100))).toBe("pending");
expect(probe.observe(current(3_000), candidate(3_100))).toBe("pending");
expect(probe.observe(current(5_000), candidate(5_100))).toBe("approved");
probe.reset();
expect(probe.observe(current(7_000), candidate(7_100))).toBe("pending");
});
it("rejects after three comparable non-improving 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("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");
});
});