-
Notifications
You must be signed in to change notification settings - Fork 190
feat: advanced media quality settings UI and config-driven defaults #3736
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
emmick4
wants to merge
9
commits into
element-hq:livekit
Choose a base branch
from
emmick4:livekit
base: livekit
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
9 commits
Select commit
Hold shift + click to select a range
390dc22
feat: configurable media quality via config.json
emmick4 342b61f
feat: add user-configurable screen share quality settings UI
emmick4 bc38d15
fix: replace InputField type="select" with native select elements
emmick4 b043230
feat: camera quality settings, audio processing toggles, config-seede…
emmick4 297470a
Use relative base path for vite build
emmick4 98f63c7
Clean up: deduplicate settings UI, fix backupCodec, remove dead export
emmick4 8642646
Add unit tests, revert vite base path change
emmick4 dedf4e1
refactor: move media settings to developer tab, centralize config def…
emmick4 79b9335
fix: update i18n translations and developer settings snapshot
emmick4 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 DEFAULT_CONFIG defaults when only resolution is set", () => { | ||
| const opts = buildLiveKitOptions({ | ||
| screen_share: { | ||
| max_resolution: 720, | ||
| }, | ||
| }); | ||
| // Bitrate and framerate fall back to DEFAULT_CONFIG, not the preset | ||
| expect(opts.publishDefaults?.screenShareEncoding).toEqual({ | ||
| maxBitrate: 5_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<typeof Config.get>); | ||
| const opts = getLiveKitOptions(); | ||
| expect(opts.publishDefaults?.videoCodec).toBe("h264"); | ||
| }); | ||
|
|
||
| it("throws when Config is not initialized", () => { | ||
| vi.mocked(Config.get).mockImplementation(() => { | ||
| throw new Error("Config not initialized"); | ||
| }); | ||
| expect(() => getLiveKitOptions()).toThrow("Config not initialized"); | ||
| }); | ||
| }); |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It seems all of these config options (with the exception of
screen_share.simulcast_layers) have default values that they imply when unset. In that case, could you set the defaults by adding them toDEFAULT_CONFIGandResolvedConfigOptionslater in this file, rather than inbuildPublishOptions?I like that the ConfigOptions file alone has authority over what the defaults are, that way.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good call, added
media_qualitywith all the scalar defaults toDEFAULT_CONFIGandResolvedConfigOptions.buildPublishOptionsnow falls back toDEFAULT_CONFIG.media_qualityinstead of inline values