diff --git a/src/livekit/options.test.ts b/src/livekit/options.test.ts new file mode 100644 index 000000000..29b806086 --- /dev/null +++ b/src/livekit/options.test.ts @@ -0,0 +1,165 @@ +/* +Copyright 2026 Element Creations Ltd. + +SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +Please see LICENSE in the repository root for full details. +*/ + +import { describe, expect, it, vi } from "vitest"; +import { VideoPresets } from "livekit-client"; + +import { buildLiveKitOptions, getLiveKitOptions } from "./options"; +import { Config } from "../config/Config"; + +vi.mock("../config/Config", () => ({ + Config: { + get: vi.fn(), + }, +})); + +describe("buildLiveKitOptions", () => { + it("returns sensible defaults with no config", () => { + const opts = buildLiveKitOptions(); + expect(opts.adaptiveStream).toBe(true); + expect(opts.dynacast).toBe(true); + expect(opts.videoCaptureDefaults?.resolution).toEqual( + VideoPresets.h720.resolution, + ); + expect(opts.publishDefaults?.videoCodec).toBe("vp8"); + expect(opts.publishDefaults?.videoEncoding).toEqual({ + maxBitrate: 1_700_000, + maxFramerate: 30, + }); + expect(opts.publishDefaults?.screenShareEncoding).toEqual({ + maxBitrate: 5_000_000, + maxFramerate: 30, + }); + expect(opts.publishDefaults?.videoSimulcastLayers).toEqual([ + VideoPresets.h180, + VideoPresets.h360, + ]); + }); + + it("applies video codec from config", () => { + const opts = buildLiveKitOptions({ video_codec: "vp9" }); + expect(opts.publishDefaults?.videoCodec).toBe("vp9"); + }); + + it("applies video resolution and encoding from config", () => { + const opts = buildLiveKitOptions({ + video: { + max_resolution: 1080, + max_bitrate: 3_000_000, + max_framerate: 60, + }, + }); + expect(opts.videoCaptureDefaults?.resolution).toEqual( + VideoPresets.h1080.resolution, + ); + expect(opts.publishDefaults?.videoEncoding).toEqual({ + maxBitrate: 3_000_000, + maxFramerate: 60, + }); + }); + + it("applies screen share encoding from config", () => { + const opts = buildLiveKitOptions({ + screen_share: { + max_bitrate: 8_000_000, + max_framerate: 15, + }, + }); + expect(opts.publishDefaults?.screenShareEncoding).toEqual({ + maxBitrate: 8_000_000, + maxFramerate: 15, + }); + }); + + it("uses 720p screen share preset for low resolution", () => { + const opts = buildLiveKitOptions({ + screen_share: { + max_resolution: 720, + }, + }); + expect(opts.publishDefaults?.screenShareEncoding).toEqual({ + maxBitrate: 2_000_000, + maxFramerate: 30, + }); + }); + + it("applies custom video simulcast layers", () => { + const opts = buildLiveKitOptions({ + video: { + simulcast_layers: [ + { height: 180, bitrate: 100_000 }, + { height: 360, bitrate: 300_000 }, + { height: 540, bitrate: 600_000 }, + ], + max_framerate: 24, + }, + }); + const layers = opts.publishDefaults?.videoSimulcastLayers; + expect(layers).toHaveLength(3); + expect(layers?.[0]).toMatchObject({ + width: 320, + height: 180, + encoding: { maxBitrate: 100_000, maxFramerate: 24 }, + }); + expect(layers?.[2]).toMatchObject({ + width: 960, + height: 540, + encoding: { maxBitrate: 600_000, maxFramerate: 24 }, + }); + }); + + it("applies custom screen share simulcast layers", () => { + const opts = buildLiveKitOptions({ + screen_share: { + simulcast_layers: [{ height: 540, bitrate: 1_000_000, framerate: 5 }], + }, + }); + const layers = opts.publishDefaults?.screenShareSimulcastLayers; + expect(layers).toHaveLength(1); + expect(layers?.[0]).toMatchObject({ + width: 960, + height: 540, + encoding: { maxBitrate: 1_000_000, maxFramerate: 5 }, + }); + }); + + it("does not include screenShareSimulcastLayers when not configured", () => { + const opts = buildLiveKitOptions(); + expect(opts.publishDefaults?.screenShareSimulcastLayers).toBeUndefined(); + }); + + it("backupCodec always uses stock VP8 720p encoding", () => { + const opts = buildLiveKitOptions({ + video_codec: "av1", + video: { max_bitrate: 10_000_000, max_framerate: 60 }, + }); + const backup = opts.publishDefaults?.backupCodec as { + codec: string; + encoding: { maxBitrate: number; maxFramerate: number }; + }; + expect(backup.codec).toBe("vp8"); + expect(backup.encoding).toEqual(VideoPresets.h720.encoding); + }); +}); + +describe("getLiveKitOptions", () => { + it("reads from Config singleton", () => { + vi.mocked(Config.get).mockReturnValue({ + media_quality: { video_codec: "h264" }, + } as ReturnType); + const opts = getLiveKitOptions(); + expect(opts.publishDefaults?.videoCodec).toBe("h264"); + }); + + it("falls back to defaults when Config throws", () => { + vi.mocked(Config.get).mockImplementation(() => { + throw new Error("Config not initialized"); + }); + const opts = getLiveKitOptions(); + expect(opts.publishDefaults?.videoCodec).toBe("vp8"); + }); +}); diff --git a/src/settings/settings.test.ts b/src/settings/settings.test.ts new file mode 100644 index 000000000..5280e58ea --- /dev/null +++ b/src/settings/settings.test.ts @@ -0,0 +1,111 @@ +/* +Copyright 2026 Element Creations Ltd. + +SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +Please see LICENSE in the repository root for full details. +*/ + +import { describe, expect, it, beforeEach } from "vitest"; + +import { + Setting, + parseResolution, + seedSettingsFromConfig, + screenShareCodec, + screenShareResolution, + screenShareFramerate, + screenShareBitrate, + cameraCodec, + cameraResolution, + cameraFramerate, + cameraBitrate, +} from "./settings"; + +beforeEach(() => { + localStorage.clear(); +}); + +describe("parseResolution", () => { + it("parses a WIDTHxHEIGHT string", () => { + expect(parseResolution("1920x1080")).toEqual({ + width: 1920, + height: 1080, + }); + }); + + it("parses non-standard resolutions", () => { + expect(parseResolution("640x360")).toEqual({ width: 640, height: 360 }); + }); +}); + +describe("Setting", () => { + describe("seedFromConfig", () => { + it("updates value when no localStorage value exists", () => { + const setting = new Setting("test-seed", "default"); + setting.seedFromConfig("from-config"); + expect(setting.getValue()).toBe("from-config"); + }); + + it("does not override an existing localStorage value", () => { + localStorage.setItem( + "matrix-setting-test-seed-existing", + JSON.stringify("user-set"), + ); + const setting = new Setting("test-seed-existing", "default"); + expect(setting.getValue()).toBe("user-set"); + setting.seedFromConfig("from-config"); + expect(setting.getValue()).toBe("user-set"); + }); + }); +}); + +describe("seedSettingsFromConfig", () => { + it("does nothing when config is undefined", () => { + const codecBefore = screenShareCodec.getValue(); + seedSettingsFromConfig(undefined); + expect(screenShareCodec.getValue()).toBe(codecBefore); + }); + + it("seeds video codec to both camera and screen share", () => { + seedSettingsFromConfig({ video_codec: "av1" }); + expect(screenShareCodec.getValue()).toBe("av1"); + expect(cameraCodec.getValue()).toBe("av1"); + }); + + it("seeds screen share settings from config", () => { + seedSettingsFromConfig({ + screen_share: { + max_resolution: 720, + max_framerate: 15, + max_bitrate: 2_000_000, + }, + }); + expect(screenShareResolution.getValue()).toBe("1280x720"); + expect(screenShareFramerate.getValue()).toBe(15); + expect(screenShareBitrate.getValue()).toBe(2_000_000); + }); + + it("seeds camera/video settings from config", () => { + seedSettingsFromConfig({ + video: { + max_resolution: 1080, + max_framerate: 60, + max_bitrate: 4_000_000, + }, + }); + expect(cameraResolution.getValue()).toBe("1920x1080"); + expect(cameraFramerate.getValue()).toBe(60); + expect(cameraBitrate.getValue()).toBe(4_000_000); + }); + + it("only seeds provided fields, leaves others at defaults", () => { + const defaultFramerate = screenShareFramerate.getValue(); + seedSettingsFromConfig({ + screen_share: { + max_bitrate: 3_000_000, + }, + }); + expect(screenShareBitrate.getValue()).toBe(3_000_000); + expect(screenShareFramerate.getValue()).toBe(defaultFramerate); + }); +}); diff --git a/src/state/CallViewModel/remoteMembers/ECConnectionFactory.test.ts b/src/state/CallViewModel/remoteMembers/ECConnectionFactory.test.ts index cf955f2cd..411ae2001 100644 --- a/src/state/CallViewModel/remoteMembers/ECConnectionFactory.test.ts +++ b/src/state/CallViewModel/remoteMembers/ECConnectionFactory.test.ts @@ -26,6 +26,11 @@ import { echoCancellationSetting, noiseSuppressionSetting, autoGainControlSetting, + advancedCamera, + cameraResolution, + cameraFramerate, + cameraBitrate, + cameraCodec, } from "../../../settings/settings.ts"; // At the top of your test file, after imports @@ -149,6 +154,114 @@ describe("ECConnectionFactory - ControlledAudioDevice", () => { ); }); +describe("ECConnectionFactory - Camera quality settings", () => { + test("it uses default video options when advancedCamera is disabled", () => { + const RoomConstructor = vi.mocked(LivekitRoom); + advancedCamera.setValue(false); + + const ecConnectionFactory = new ECConnectionFactory( + mockClient, + "!roomid:example.org", + mockMediaDevices({}), + new BehaviorSubject({ + supported: true, + processor: undefined, + }), + undefined, + false, + ); + ecConnectionFactory.createConnection( + testScope, + exampleTransport, + ownMemberMock, + logger, + ); + + // publishDefaults should use config defaults (vp8), not custom settings + expect(RoomConstructor).toHaveBeenCalledWith( + expect.objectContaining({ + publishDefaults: expect.objectContaining({ + videoCodec: "vp8", + }), + }), + ); + }); + + test("it applies custom camera resolution, encoding, and codec when advancedCamera is enabled", () => { + const RoomConstructor = vi.mocked(LivekitRoom); + + advancedCamera.setValue(true); + cameraResolution.setValue("1920x1080"); + cameraFramerate.setValue(60); + cameraBitrate.setValue(4_000_000); + cameraCodec.setValue("vp9"); + + const ecConnectionFactory = new ECConnectionFactory( + mockClient, + "!roomid:example.org", + mockMediaDevices({}), + new BehaviorSubject({ + supported: true, + processor: undefined, + }), + undefined, + false, + ); + ecConnectionFactory.createConnection( + testScope, + exampleTransport, + ownMemberMock, + logger, + ); + + expect(RoomConstructor).toHaveBeenCalledWith( + expect.objectContaining({ + videoCaptureDefaults: expect.objectContaining({ + resolution: { width: 1920, height: 1080, frameRate: 60 }, + }), + publishDefaults: expect.objectContaining({ + videoEncoding: { maxBitrate: 4_000_000, maxFramerate: 60 }, + videoCodec: "vp9", + }), + }), + ); + }); + + test("it applies autoGainControl from settings", () => { + const RoomConstructor = vi.mocked(LivekitRoom); + + autoGainControlSetting.setValue(false); + echoCancellationSetting.setValue(true); + noiseSuppressionSetting.setValue(true); + + const ecConnectionFactory = new ECConnectionFactory( + mockClient, + "!roomid:example.org", + mockMediaDevices({}), + new BehaviorSubject({ + supported: true, + processor: undefined, + }), + undefined, + false, + ); + ecConnectionFactory.createConnection( + testScope, + exampleTransport, + ownMemberMock, + logger, + ); + + expect(RoomConstructor).toHaveBeenCalledWith( + expect.objectContaining({ + audioCaptureDefaults: expect.objectContaining({ + autoGainControl: false, + }), + }), + ); + }); +}); + afterEach(() => { testScope.end(); fetchMock.reset(); diff --git a/vite.config.ts b/vite.config.ts index 8807826c7..801ea79aa 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -102,7 +102,6 @@ export default ({ console.log("Allowed vite paths:", allow); return { - base: "./", server: { port: 3000, fs: { allow },