Files
Piik/tests/quality.test.ts
T

225 lines
6.8 KiB
TypeScript

import { afterEach, describe, expect, it, vi } from "vitest";
import {
applyCaptureProfile,
captureDisplay,
configureVideoSender,
QUALITY_PROFILES,
senderParameterWarning,
setVideoPaused,
} from "../src/client/media/quality.ts";
function createVideoStream() {
const videoTrack = {
contentHint: "",
enabled: true,
applyConstraints: vi.fn(async () => undefined),
getSettings: () => ({ width: 1920, height: 1080, frameRate: 60 }),
} as unknown as MediaStreamTrack;
const stream = {
getTracks: () => [videoTrack],
getVideoTracks: () => [videoTrack],
getAudioTracks: () => [],
} as unknown as MediaStream;
return { stream, videoTrack };
}
afterEach(() => {
vi.unstubAllGlobals();
});
describe("realtime quality controls", () => {
it("bounds initial capture to the selected profile", async () => {
const { stream, videoTrack } = createVideoStream();
const getDisplayMedia = vi.fn(async () => stream);
vi.stubGlobal("navigator", { mediaDevices: { getDisplayMedia } });
await expect(captureDisplay(QUALITY_PROFILES["1080p30"])).resolves.toBe(
stream,
);
expect(getDisplayMedia).toHaveBeenCalledWith({
video: {
width: { ideal: 1920, max: 1920 },
height: { ideal: 1080, max: 1080 },
frameRate: { ideal: 30, max: 30 },
},
audio: true,
});
expect(videoTrack.contentHint).toBe("motion");
});
it("changes custom capture ceilings without selecting the source again", async () => {
const { stream, videoTrack } = createVideoStream();
await applyCaptureProfile(stream, {
resolution: "1440p",
maxFramerate: 45,
maxBitrate: 9_500_000,
degradationPreference: "balanced",
});
expect(videoTrack.applyConstraints).toHaveBeenCalledWith({
width: { ideal: 2560, max: 2560 },
height: { ideal: 1440, max: 1440 },
frameRate: { ideal: 45, max: 45 },
});
});
it("defaults every recommended profile to clarity-first degradation", () => {
expect(
Object.values(QUALITY_PROFILES).every(
(profile) =>
profile.degradationPreference === "maintain-resolution",
),
).toBe(true);
});
it("reads back every requested sender control after setParameters", async () => {
let applied = { encodings: [] } as unknown as RTCRtpSendParameters;
const setParameters = vi.fn(async (parameters: RTCRtpSendParameters) => {
applied = parameters;
});
const sender = {
track: {
getSettings: () => ({ width: 2560, height: 1440 }),
},
getParameters: () => applied,
setParameters,
} as unknown as RTCRtpSender;
const readback = await configureVideoSender(
sender,
QUALITY_PROFILES["1080p60"],
);
expect(readback).toEqual({
requested: {
maxBitrate: 8_000_000,
maxFramerate: 60,
scaleResolutionDownBy: 4 / 3,
degradationPreference: "maintain-resolution",
scalabilityMode: null,
},
applied: {
maxBitrate: 8_000_000,
maxFramerate: 60,
scaleResolutionDownBy: 4 / 3,
degradationPreference: "maintain-resolution",
scalabilityMode: null,
},
mismatches: [],
});
expect(senderParameterWarning(readback)).toBeNull();
expect(setParameters).toHaveBeenCalledOnce();
});
it("reports fields the browser does not retain", async () => {
const before = { encodings: [{}] } as RTCRtpSendParameters;
const after = {
encodings: [{ maxBitrate: 8_000_000 }],
degradationPreference: "balanced",
} as unknown as RTCRtpSendParameters;
const getParameters = vi
.fn<() => RTCRtpSendParameters>()
.mockReturnValueOnce(before)
.mockReturnValue(after);
const sender = {
track: { getSettings: () => ({ width: 1920, height: 1080 }) },
getParameters,
setParameters: vi.fn(async () => undefined),
} as unknown as RTCRtpSender;
const readback = await configureVideoSender(
sender,
QUALITY_PROFILES["1080p60"],
);
expect(readback.mismatches).toEqual([
"maxFramerate",
"scaleResolutionDownBy",
"degradationPreference",
]);
expect(senderParameterWarning(readback)).toContain("帧率上限");
expect(senderParameterWarning(readback)).toContain("分辨率缩放");
expect(senderParameterWarning(readback)).toContain("质量优先级");
});
it("reports an applied default scalability mode without claiming a request", async () => {
const before = {
encodings: [{}],
} as unknown as RTCRtpSendParameters;
const after = {
encodings: [
{
maxBitrate: 8_000_000,
maxFramerate: 60,
scaleResolutionDownBy: 1,
scalabilityMode: "L1T2",
},
],
degradationPreference: "maintain-resolution",
} as unknown as RTCRtpSendParameters;
const sender = {
track: { getSettings: () => ({ width: 1920, height: 1080 }) },
getParameters: vi
.fn<() => RTCRtpSendParameters>()
.mockReturnValueOnce(before)
.mockReturnValue(after),
setParameters: vi.fn(async () => undefined),
} as unknown as RTCRtpSender;
const readback = await configureVideoSender(
sender,
QUALITY_PROFILES["1080p60"],
);
expect(readback.requested.scalabilityMode).toBeNull();
expect(readback.applied.scalabilityMode).toBe("L1T2");
expect(readback.mismatches).not.toContain("scalabilityMode");
expect(senderParameterWarning(readback)).toBeNull();
});
it("keeps scalability mode unknown when sender encodings are ambiguous", async () => {
const before = { encodings: [{}] } as RTCRtpSendParameters;
const after = {
encodings: [
{
rid: "low",
maxBitrate: 8_000_000,
maxFramerate: 60,
scaleResolutionDownBy: 1,
scalabilityMode: "L1T1",
},
{ rid: "high", scalabilityMode: "L1T2" },
],
degradationPreference: "maintain-resolution",
} as unknown as RTCRtpSendParameters;
const sender = {
track: { getSettings: () => ({ width: 1920, height: 1080 }) },
getParameters: vi
.fn<() => RTCRtpSendParameters>()
.mockReturnValueOnce(before)
.mockReturnValue(after),
setParameters: vi.fn(async () => undefined),
} as unknown as RTCRtpSender;
const readback = await configureVideoSender(
sender,
QUALITY_PROFILES["1080p60"],
);
expect(readback.applied.scalabilityMode).toBeNull();
expect(readback.mismatches).not.toContain("scalabilityMode");
});
it("pauses only the video track and can resume it", () => {
const { stream, videoTrack } = createVideoStream();
expect(setVideoPaused(stream, true)).toBe(true);
expect(videoTrack.enabled).toBe(false);
expect(setVideoPaused(stream, false)).toBe(true);
expect(videoTrack.enabled).toBe(true);
});
});