linux audioshare: add granular selection, more options, better ui (#621)
This commit is contained in:
parent
1a4d173bb4
commit
da7f13288f
7 changed files with 520 additions and 254 deletions
|
@ -27,7 +27,7 @@
|
||||||
"arrpc": "github:OpenAsar/arrpc#6960a8fd4d65d566da93dbdb8a7ca474aa0a3c9c"
|
"arrpc": "github:OpenAsar/arrpc#6960a8fd4d65d566da93dbdb8a7ca474aa0a3c9c"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"@vencord/venmic": "^3.5.0"
|
"@vencord/venmic": "^6.1.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@fal-works/esbuild-plugin-global-externals": "^2.1.2",
|
"@fal-works/esbuild-plugin-global-externals": "^2.1.2",
|
||||||
|
|
|
@ -13,8 +13,8 @@ importers:
|
||||||
version: https://codeload.github.com/OpenAsar/arrpc/tar.gz/6960a8fd4d65d566da93dbdb8a7ca474aa0a3c9c
|
version: https://codeload.github.com/OpenAsar/arrpc/tar.gz/6960a8fd4d65d566da93dbdb8a7ca474aa0a3c9c
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@vencord/venmic':
|
'@vencord/venmic':
|
||||||
specifier: ^3.5.0
|
specifier: ^6.1.0
|
||||||
version: 3.5.0
|
version: 6.1.0
|
||||||
devDependencies:
|
devDependencies:
|
||||||
'@fal-works/esbuild-plugin-global-externals':
|
'@fal-works/esbuild-plugin-global-externals':
|
||||||
specifier: ^2.1.2
|
specifier: ^2.1.2
|
||||||
|
@ -603,8 +603,8 @@ packages:
|
||||||
'@vencord/types@1.8.4':
|
'@vencord/types@1.8.4':
|
||||||
resolution: {integrity: sha512-ogLqIOHVO+5zxKUVxAfGIAUZoEfIomrlg6f0cZ/2yd5vBAn1fA9Gi/NASoKfHZuJt8ZcYw329bgn0ah/VufqMg==}
|
resolution: {integrity: sha512-ogLqIOHVO+5zxKUVxAfGIAUZoEfIomrlg6f0cZ/2yd5vBAn1fA9Gi/NASoKfHZuJt8ZcYw329bgn0ah/VufqMg==}
|
||||||
|
|
||||||
'@vencord/venmic@3.5.0':
|
'@vencord/venmic@6.1.0':
|
||||||
resolution: {integrity: sha512-kPvrPcIeMkuqQriuiQAJ9rEBeqGR2nmFBuUtbZRGyiNRF9RDAfWSJYqhHVm6F7wbcqrSZio6FazZuBo0LvjJRw==}
|
resolution: {integrity: sha512-YiCtzml/W8tYbGhu3jm5jfbbEnl2slKKARNK0jO+8qV979k9eFnfIRTxvhMN/SWq1h8ZNJdXVwvXpffQwq0RuA==}
|
||||||
engines: {node: '>=14.15'}
|
engines: {node: '>=14.15'}
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
|
||||||
|
@ -1562,6 +1562,7 @@ packages:
|
||||||
|
|
||||||
inflight@1.0.6:
|
inflight@1.0.6:
|
||||||
resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==}
|
resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==}
|
||||||
|
deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.
|
||||||
|
|
||||||
inherits@2.0.4:
|
inherits@2.0.4:
|
||||||
resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==}
|
resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==}
|
||||||
|
@ -2156,6 +2157,7 @@ packages:
|
||||||
|
|
||||||
rimraf@3.0.2:
|
rimraf@3.0.2:
|
||||||
resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==}
|
resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==}
|
||||||
|
deprecated: Rimraf versions prior to v4 are no longer supported
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
roarr@2.15.4:
|
roarr@2.15.4:
|
||||||
|
@ -3032,7 +3034,7 @@ snapshots:
|
||||||
standalone-electron-types: 1.0.0
|
standalone-electron-types: 1.0.0
|
||||||
type-fest: 3.13.1
|
type-fest: 3.13.1
|
||||||
|
|
||||||
'@vencord/venmic@3.5.0':
|
'@vencord/venmic@6.1.0':
|
||||||
dependencies:
|
dependencies:
|
||||||
cmake-js: 7.3.0
|
cmake-js: 7.3.0
|
||||||
node-addon-api: 8.0.0
|
node-addon-api: 8.0.0
|
||||||
|
@ -3078,7 +3080,7 @@ snapshots:
|
||||||
|
|
||||||
app-builder-bin@4.0.0: {}
|
app-builder-bin@4.0.0: {}
|
||||||
|
|
||||||
app-builder-lib@24.13.3(dmg-builder@24.13.3(electron-builder-squirrel-windows@24.13.3(dmg-builder@24.13.3)))(electron-builder-squirrel-windows@24.13.3(dmg-builder@24.13.3)):
|
app-builder-lib@24.13.3(dmg-builder@24.13.3(electron-builder-squirrel-windows@24.13.3))(electron-builder-squirrel-windows@24.13.3(dmg-builder@24.13.3)):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@develar/schema-utils': 2.6.5
|
'@develar/schema-utils': 2.6.5
|
||||||
'@electron/notarize': 2.2.1
|
'@electron/notarize': 2.2.1
|
||||||
|
@ -3092,7 +3094,7 @@ snapshots:
|
||||||
builder-util-runtime: 9.2.4
|
builder-util-runtime: 9.2.4
|
||||||
chromium-pickle-js: 0.2.0
|
chromium-pickle-js: 0.2.0
|
||||||
debug: 4.3.4
|
debug: 4.3.4
|
||||||
dmg-builder: 24.13.3(electron-builder-squirrel-windows@24.13.3(dmg-builder@24.13.3))
|
dmg-builder: 24.13.3(electron-builder-squirrel-windows@24.13.3)
|
||||||
ejs: 3.1.9
|
ejs: 3.1.9
|
||||||
electron-builder-squirrel-windows: 24.13.3(dmg-builder@24.13.3)
|
electron-builder-squirrel-windows: 24.13.3(dmg-builder@24.13.3)
|
||||||
electron-publish: 24.13.1
|
electron-publish: 24.13.1
|
||||||
|
@ -3567,9 +3569,9 @@ snapshots:
|
||||||
'@types/react': 17.0.2
|
'@types/react': 17.0.2
|
||||||
moment: 2.30.1
|
moment: 2.30.1
|
||||||
|
|
||||||
dmg-builder@24.13.3(electron-builder-squirrel-windows@24.13.3(dmg-builder@24.13.3)):
|
dmg-builder@24.13.3(electron-builder-squirrel-windows@24.13.3):
|
||||||
dependencies:
|
dependencies:
|
||||||
app-builder-lib: 24.13.3(dmg-builder@24.13.3(electron-builder-squirrel-windows@24.13.3(dmg-builder@24.13.3)))(electron-builder-squirrel-windows@24.13.3(dmg-builder@24.13.3))
|
app-builder-lib: 24.13.3(dmg-builder@24.13.3(electron-builder-squirrel-windows@24.13.3))(electron-builder-squirrel-windows@24.13.3(dmg-builder@24.13.3))
|
||||||
builder-util: 24.13.1
|
builder-util: 24.13.1
|
||||||
builder-util-runtime: 9.2.4
|
builder-util-runtime: 9.2.4
|
||||||
fs-extra: 10.1.0
|
fs-extra: 10.1.0
|
||||||
|
@ -3615,7 +3617,7 @@ snapshots:
|
||||||
|
|
||||||
electron-builder-squirrel-windows@24.13.3(dmg-builder@24.13.3):
|
electron-builder-squirrel-windows@24.13.3(dmg-builder@24.13.3):
|
||||||
dependencies:
|
dependencies:
|
||||||
app-builder-lib: 24.13.3(dmg-builder@24.13.3(electron-builder-squirrel-windows@24.13.3(dmg-builder@24.13.3)))(electron-builder-squirrel-windows@24.13.3(dmg-builder@24.13.3))
|
app-builder-lib: 24.13.3(dmg-builder@24.13.3(electron-builder-squirrel-windows@24.13.3))(electron-builder-squirrel-windows@24.13.3(dmg-builder@24.13.3))
|
||||||
archiver: 5.3.2
|
archiver: 5.3.2
|
||||||
builder-util: 24.13.1
|
builder-util: 24.13.1
|
||||||
fs-extra: 10.1.0
|
fs-extra: 10.1.0
|
||||||
|
@ -3625,11 +3627,11 @@ snapshots:
|
||||||
|
|
||||||
electron-builder@24.13.3(electron-builder-squirrel-windows@24.13.3(dmg-builder@24.13.3)):
|
electron-builder@24.13.3(electron-builder-squirrel-windows@24.13.3(dmg-builder@24.13.3)):
|
||||||
dependencies:
|
dependencies:
|
||||||
app-builder-lib: 24.13.3(dmg-builder@24.13.3(electron-builder-squirrel-windows@24.13.3(dmg-builder@24.13.3)))(electron-builder-squirrel-windows@24.13.3(dmg-builder@24.13.3))
|
app-builder-lib: 24.13.3(dmg-builder@24.13.3(electron-builder-squirrel-windows@24.13.3))(electron-builder-squirrel-windows@24.13.3(dmg-builder@24.13.3))
|
||||||
builder-util: 24.13.1
|
builder-util: 24.13.1
|
||||||
builder-util-runtime: 9.2.4
|
builder-util-runtime: 9.2.4
|
||||||
chalk: 4.1.2
|
chalk: 4.1.2
|
||||||
dmg-builder: 24.13.3(electron-builder-squirrel-windows@24.13.3(dmg-builder@24.13.3))
|
dmg-builder: 24.13.3(electron-builder-squirrel-windows@24.13.3)
|
||||||
fs-extra: 10.1.0
|
fs-extra: 10.1.0
|
||||||
is-ci: 3.0.1
|
is-ci: 3.0.1
|
||||||
lazy-val: 1.0.5
|
lazy-val: 1.0.5
|
||||||
|
|
|
@ -4,13 +4,13 @@
|
||||||
* Copyright (c) 2023 Vendicated and Vencord contributors
|
* Copyright (c) 2023 Vendicated and Vencord contributors
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { PatchBay as PatchBayType } from "@vencord/venmic";
|
import type { LinkData, Node, PatchBay as PatchBayType } from "@vencord/venmic";
|
||||||
import { app, ipcMain } from "electron";
|
import { app, ipcMain } from "electron";
|
||||||
import { join } from "path";
|
import { join } from "path";
|
||||||
import { IpcEvents } from "shared/IpcEvents";
|
import { IpcEvents } from "shared/IpcEvents";
|
||||||
import { STATIC_DIR } from "shared/paths";
|
import { STATIC_DIR } from "shared/paths";
|
||||||
|
|
||||||
type LinkData = Parameters<PatchBayType["link"]>[0];
|
import { Settings } from "./settings";
|
||||||
|
|
||||||
let PatchBay: typeof PatchBayType | undefined;
|
let PatchBay: typeof PatchBayType | undefined;
|
||||||
let patchBayInstance: PatchBayType | undefined;
|
let patchBayInstance: PatchBayType | undefined;
|
||||||
|
@ -69,47 +69,64 @@ function getRendererAudioServicePid() {
|
||||||
ipcMain.handle(IpcEvents.VIRT_MIC_LIST, () => {
|
ipcMain.handle(IpcEvents.VIRT_MIC_LIST, () => {
|
||||||
const audioPid = getRendererAudioServicePid();
|
const audioPid = getRendererAudioServicePid();
|
||||||
|
|
||||||
const list = obtainVenmic()
|
const { granularSelect } = Settings.store.audio ?? {};
|
||||||
?.list()
|
|
||||||
.filter(s => s["application.process.id"] !== audioPid)
|
|
||||||
.map(s => s["application.name"]);
|
|
||||||
|
|
||||||
const uniqueTargets = [...new Set(list)];
|
const targets = obtainVenmic()
|
||||||
|
?.list(granularSelect ? ["application.process.id"] : undefined)
|
||||||
|
.filter(s => s["application.process.id"] !== audioPid);
|
||||||
|
|
||||||
return list ? { ok: true, targets: uniqueTargets, hasPipewirePulse } : { ok: false, isGlibCxxOutdated };
|
return targets ? { ok: true, targets, hasPipewirePulse } : { ok: false, isGlibCxxOutdated };
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle(IpcEvents.VIRT_MIC_START, (_, targets: string[], workaround?: boolean) => {
|
ipcMain.handle(IpcEvents.VIRT_MIC_START, (_, include: Node[]) => {
|
||||||
const pid = getRendererAudioServicePid();
|
const pid = getRendererAudioServicePid();
|
||||||
|
const { ignoreDevices, ignoreInputMedia, ignoreVirtual, workaround } = Settings.store.audio ?? {};
|
||||||
|
|
||||||
const data: LinkData = {
|
const data: LinkData = {
|
||||||
include: targets.map(target => ({ key: "application.name", value: target })),
|
include,
|
||||||
exclude: [{ key: "application.process.id", value: pid }]
|
exclude: [{ "application.process.id": pid }],
|
||||||
|
ignore_devices: ignoreDevices
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (ignoreInputMedia ?? true) {
|
||||||
|
data.exclude.push({ "media.class": "Stream/Input/Audio" });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ignoreVirtual) {
|
||||||
|
data.exclude.push({ "node.virtual": "true" });
|
||||||
|
}
|
||||||
|
|
||||||
if (workaround) {
|
if (workaround) {
|
||||||
data.workaround = [
|
data.workaround = [{ "application.process.id": pid, "media.name": "RecordStream" }];
|
||||||
{ key: "application.process.id", value: pid },
|
|
||||||
{ key: "media.name", value: "RecordStream" }
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return obtainVenmic()?.link(data);
|
return obtainVenmic()?.link(data);
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle(IpcEvents.VIRT_MIC_START_SYSTEM, (_, workaround?: boolean, onlyDefaultSpeakers?: boolean) => {
|
ipcMain.handle(IpcEvents.VIRT_MIC_START_SYSTEM, (_, exclude: Node[]) => {
|
||||||
const pid = getRendererAudioServicePid();
|
const pid = getRendererAudioServicePid();
|
||||||
|
|
||||||
|
const { workaround, ignoreDevices, ignoreInputMedia, ignoreVirtual, onlySpeakers, onlyDefaultSpeakers } =
|
||||||
|
Settings.store.audio ?? {};
|
||||||
|
|
||||||
const data: LinkData = {
|
const data: LinkData = {
|
||||||
exclude: [{ key: "application.process.id", value: pid }],
|
include: [],
|
||||||
|
exclude: [{ "application.process.id": pid }, ...exclude],
|
||||||
|
only_speakers: onlySpeakers,
|
||||||
|
ignore_devices: ignoreDevices,
|
||||||
only_default_speakers: onlyDefaultSpeakers
|
only_default_speakers: onlyDefaultSpeakers
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (ignoreInputMedia ?? true) {
|
||||||
|
data.exclude.push({ "media.class": "Stream/Input/Audio" });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ignoreVirtual) {
|
||||||
|
data.exclude.push({ "node.virtual": "true" });
|
||||||
|
}
|
||||||
|
|
||||||
if (workaround) {
|
if (workaround) {
|
||||||
data.workaround = [
|
data.workaround = [{ "application.process.id": pid, "media.name": "RecordStream" }];
|
||||||
{ key: "application.process.id", value: pid },
|
|
||||||
{ key: "media.name", value: "RecordStream" }
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return obtainVenmic()?.link(data);
|
return obtainVenmic()?.link(data);
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
* Copyright (c) 2023 Vendicated and Vencord contributors
|
* Copyright (c) 2023 Vendicated and Vencord contributors
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { Node } from "@vencord/venmic";
|
||||||
import { ipcRenderer } from "electron";
|
import { ipcRenderer } from "electron";
|
||||||
import type { Settings } from "shared/settings";
|
import type { Settings } from "shared/settings";
|
||||||
import type { LiteralUnion } from "type-fest";
|
import type { LiteralUnion } from "type-fest";
|
||||||
|
@ -63,11 +64,10 @@ export const VesktopNative = {
|
||||||
virtmic: {
|
virtmic: {
|
||||||
list: () =>
|
list: () =>
|
||||||
invoke<
|
invoke<
|
||||||
{ ok: false; isGlibCxxOutdated: boolean } | { ok: true; targets: string[]; hasPipewirePulse: boolean }
|
{ ok: false; isGlibCxxOutdated: boolean } | { ok: true; targets: Node[]; hasPipewirePulse: boolean }
|
||||||
>(IpcEvents.VIRT_MIC_LIST),
|
>(IpcEvents.VIRT_MIC_LIST),
|
||||||
start: (targets: string[], workaround?: boolean) => invoke<void>(IpcEvents.VIRT_MIC_START, targets, workaround),
|
start: (include: Node[]) => invoke<void>(IpcEvents.VIRT_MIC_START, include),
|
||||||
startSystem: (workaround?: boolean, onlyDefaultSpeakers?: boolean) =>
|
startSystem: (exclude: Node[]) => invoke<void>(IpcEvents.VIRT_MIC_START_SYSTEM, exclude),
|
||||||
invoke<void>(IpcEvents.VIRT_MIC_START_SYSTEM, workaround, onlyDefaultSpeakers),
|
|
||||||
stop: () => invoke<void>(IpcEvents.VIRT_MIC_STOP)
|
stop: () => invoke<void>(IpcEvents.VIRT_MIC_STOP)
|
||||||
},
|
},
|
||||||
arrpc: {
|
arrpc: {
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
|
|
||||||
import "./screenSharePicker.css";
|
import "./screenSharePicker.css";
|
||||||
|
|
||||||
import { closeModal, Logger, Margins, Modals, ModalSize, openModal, useAwaiter } from "@vencord/types/utils";
|
import { closeModal, Logger, Modals, ModalSize, openModal, useAwaiter } from "@vencord/types/utils";
|
||||||
import { findStoreLazy, onceReady } from "@vencord/types/webpack";
|
import { findStoreLazy, onceReady } from "@vencord/types/webpack";
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
|
@ -19,8 +19,10 @@ import {
|
||||||
UserStore,
|
UserStore,
|
||||||
useState
|
useState
|
||||||
} from "@vencord/types/webpack/common";
|
} from "@vencord/types/webpack/common";
|
||||||
|
import { Node } from "@vencord/venmic";
|
||||||
import type { Dispatch, SetStateAction } from "react";
|
import type { Dispatch, SetStateAction } from "react";
|
||||||
import { addPatch } from "renderer/patches/shared";
|
import { addPatch } from "renderer/patches/shared";
|
||||||
|
import { useSettings } from "renderer/settings";
|
||||||
import { isLinux, isWindows } from "renderer/utils";
|
import { isLinux, isWindows } from "renderer/utils";
|
||||||
|
|
||||||
const StreamResolutions = ["480", "720", "1080", "1440"] as const;
|
const StreamResolutions = ["480", "720", "1080", "1440"] as const;
|
||||||
|
@ -31,14 +33,23 @@ const MediaEngineStore = findStoreLazy("MediaEngineStore");
|
||||||
export type StreamResolution = (typeof StreamResolutions)[number];
|
export type StreamResolution = (typeof StreamResolutions)[number];
|
||||||
export type StreamFps = (typeof StreamFps)[number];
|
export type StreamFps = (typeof StreamFps)[number];
|
||||||
|
|
||||||
|
type SpecialSource = "None" | "Entire System";
|
||||||
|
|
||||||
|
type AudioSource = SpecialSource | Node;
|
||||||
|
type AudioSources = SpecialSource | Node[];
|
||||||
|
|
||||||
|
interface AudioItem {
|
||||||
|
name: string;
|
||||||
|
value: AudioSource;
|
||||||
|
}
|
||||||
|
|
||||||
interface StreamSettings {
|
interface StreamSettings {
|
||||||
resolution: StreamResolution;
|
resolution: StreamResolution;
|
||||||
fps: StreamFps;
|
fps: StreamFps;
|
||||||
audio: boolean;
|
audio: boolean;
|
||||||
audioSource?: string;
|
|
||||||
contentHint?: string;
|
contentHint?: string;
|
||||||
workaround?: boolean;
|
includeSources?: AudioSources;
|
||||||
onlyDefaultSpeakers?: boolean;
|
excludeSources?: AudioSources;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface StreamPick extends StreamSettings {
|
export interface StreamPick extends StreamSettings {
|
||||||
|
@ -118,13 +129,17 @@ export function openScreenSharePicker(screens: Source[], skipPicker: boolean) {
|
||||||
modalProps={props}
|
modalProps={props}
|
||||||
submit={async v => {
|
submit={async v => {
|
||||||
didSubmit = true;
|
didSubmit = true;
|
||||||
if (v.audioSource && v.audioSource !== "None") {
|
|
||||||
if (v.audioSource === "Entire System") {
|
if (v.includeSources && v.includeSources !== "None") {
|
||||||
await VesktopNative.virtmic.startSystem(v.workaround);
|
if (v.includeSources === "Entire System") {
|
||||||
|
await VesktopNative.virtmic.startSystem(
|
||||||
|
!v.excludeSources || isSpecialSource(v.excludeSources) ? [] : v.excludeSources
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
await VesktopNative.virtmic.start([v.audioSource], v.workaround);
|
await VesktopNative.virtmic.start(v.includeSources);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
resolve(v);
|
resolve(v);
|
||||||
}}
|
}}
|
||||||
close={() => {
|
close={() => {
|
||||||
|
@ -159,6 +174,113 @@ function ScreenPicker({ screens, chooseScreen }: { screens: Source[]; chooseScre
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function AudioSettingsModal({
|
||||||
|
modalProps,
|
||||||
|
close,
|
||||||
|
setAudioSources
|
||||||
|
}: {
|
||||||
|
modalProps: any;
|
||||||
|
close: () => void;
|
||||||
|
setAudioSources: (s: AudioSources) => void;
|
||||||
|
}) {
|
||||||
|
const Settings = useSettings();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modals.ModalRoot {...modalProps} size={ModalSize.MEDIUM}>
|
||||||
|
<Modals.ModalHeader className="vcd-screen-picker-header">
|
||||||
|
<Forms.FormTitle tag="h2">Venmic Settings</Forms.FormTitle>
|
||||||
|
<Modals.ModalCloseButton onClick={close} />
|
||||||
|
</Modals.ModalHeader>
|
||||||
|
<Modals.ModalContent className="vcd-screen-picker-modal">
|
||||||
|
<Switch
|
||||||
|
hideBorder
|
||||||
|
onChange={v => (Settings.audio = { ...Settings.audio, workaround: v })}
|
||||||
|
value={Settings.audio?.workaround ?? false}
|
||||||
|
note={
|
||||||
|
<>
|
||||||
|
Work around an issue that causes the microphone to be shared instead of the correct audio.
|
||||||
|
Only enable if you're experiencing this issue.
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Microphone Workaround
|
||||||
|
</Switch>
|
||||||
|
<Switch
|
||||||
|
hideBorder
|
||||||
|
onChange={v => (Settings.audio = { ...Settings.audio, onlySpeakers: v })}
|
||||||
|
value={Settings.audio?.onlySpeakers ?? true}
|
||||||
|
note={
|
||||||
|
<>
|
||||||
|
When sharing entire desktop audio, only share apps that play to a speaker. You may want to
|
||||||
|
disable this when using "mix bussing".
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Only Speakers
|
||||||
|
</Switch>
|
||||||
|
<Switch
|
||||||
|
hideBorder
|
||||||
|
onChange={v => (Settings.audio = { ...Settings.audio, onlyDefaultSpeakers: v })}
|
||||||
|
value={Settings.audio?.onlyDefaultSpeakers ?? true}
|
||||||
|
note={
|
||||||
|
<>
|
||||||
|
When sharing entire desktop audio, only share apps that play to the <b>default</b> speakers.
|
||||||
|
You may want to disable this when using "mix bussing".
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Only Default Speakers
|
||||||
|
</Switch>
|
||||||
|
<Switch
|
||||||
|
hideBorder
|
||||||
|
onChange={v => (Settings.audio = { ...Settings.audio, ignoreInputMedia: v })}
|
||||||
|
value={Settings.audio?.ignoreInputMedia ?? true}
|
||||||
|
note={<>Exclude nodes that are intended to capture audio.</>}
|
||||||
|
>
|
||||||
|
Ignore Inputs
|
||||||
|
</Switch>
|
||||||
|
<Switch
|
||||||
|
hideBorder
|
||||||
|
onChange={v => (Settings.audio = { ...Settings.audio, ignoreVirtual: v })}
|
||||||
|
value={Settings.audio?.ignoreVirtual ?? false}
|
||||||
|
note={
|
||||||
|
<>
|
||||||
|
Exclude virtual nodes, such as nodes belonging to loopbacks. This might be useful when using
|
||||||
|
"mix bussing".
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Ignore Virtual
|
||||||
|
</Switch>
|
||||||
|
<Switch
|
||||||
|
hideBorder
|
||||||
|
onChange={v => (Settings.audio = { ...Settings.audio, ignoreDevices: v })}
|
||||||
|
value={Settings.audio?.ignoreDevices ?? true}
|
||||||
|
note={<>Exclude device nodes, such as nodes belonging to microphones or speakers.</>}
|
||||||
|
>
|
||||||
|
Ignore Devices
|
||||||
|
</Switch>
|
||||||
|
<Switch
|
||||||
|
hideBorder
|
||||||
|
onChange={value => {
|
||||||
|
Settings.audio = { ...Settings.audio, granularSelect: value };
|
||||||
|
setAudioSources("None");
|
||||||
|
}}
|
||||||
|
value={Settings.audio?.granularSelect ?? false}
|
||||||
|
note={<>Allow to select applications more granularly.</>}
|
||||||
|
>
|
||||||
|
Granular Selection
|
||||||
|
</Switch>
|
||||||
|
</Modals.ModalContent>
|
||||||
|
<Modals.ModalFooter className="vcd-screen-picker-footer">
|
||||||
|
<Button color={Button.Colors.TRANSPARENT} onClick={close}>
|
||||||
|
Back
|
||||||
|
</Button>
|
||||||
|
</Modals.ModalFooter>
|
||||||
|
</Modals.ModalRoot>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function StreamSettings({
|
function StreamSettings({
|
||||||
source,
|
source,
|
||||||
settings,
|
settings,
|
||||||
|
@ -170,6 +292,8 @@ function StreamSettings({
|
||||||
setSettings: Dispatch<SetStateAction<StreamSettings>>;
|
setSettings: Dispatch<SetStateAction<StreamSettings>>;
|
||||||
skipPicker: boolean;
|
skipPicker: boolean;
|
||||||
}) {
|
}) {
|
||||||
|
const Settings = useSettings();
|
||||||
|
|
||||||
const [thumb] = useAwaiter(
|
const [thumb] = useAwaiter(
|
||||||
() => (skipPicker ? Promise.resolve(source.url) : VesktopNative.capturer.getLargeThumbnail(source.id)),
|
() => (skipPicker ? Promise.resolve(source.url) : VesktopNative.capturer.getLargeThumbnail(source.id)),
|
||||||
{
|
{
|
||||||
|
@ -178,8 +302,19 @@ function StreamSettings({
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const openSettings = () => {
|
||||||
|
const key = openModal(props => (
|
||||||
|
<AudioSettingsModal
|
||||||
|
modalProps={props}
|
||||||
|
close={() => props.onClose()}
|
||||||
|
setAudioSources={sources =>
|
||||||
|
setSettings(s => ({ ...s, includeSources: sources, excludeSources: sources }))
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={isLinux ? "vcd-screen-picker-settings-grid" : ""}>
|
|
||||||
<div>
|
<div>
|
||||||
<Forms.FormTitle>What you're streaming</Forms.FormTitle>
|
<Forms.FormTitle>What you're streaming</Forms.FormTitle>
|
||||||
<Card className="vcd-screen-picker-card vcd-screen-picker-preview">
|
<Card className="vcd-screen-picker-card vcd-screen-picker-preview">
|
||||||
|
@ -199,10 +334,7 @@ function StreamSettings({
|
||||||
<Forms.FormTitle>Resolution</Forms.FormTitle>
|
<Forms.FormTitle>Resolution</Forms.FormTitle>
|
||||||
<div className="vcd-screen-picker-radios">
|
<div className="vcd-screen-picker-radios">
|
||||||
{StreamResolutions.map(res => (
|
{StreamResolutions.map(res => (
|
||||||
<label
|
<label className="vcd-screen-picker-radio" data-checked={settings.resolution === res}>
|
||||||
className="vcd-screen-picker-radio"
|
|
||||||
data-checked={settings.resolution === res}
|
|
||||||
>
|
|
||||||
<Text variant="text-sm/bold">{res}</Text>
|
<Text variant="text-sm/bold">{res}</Text>
|
||||||
<input
|
<input
|
||||||
type="radio"
|
type="radio"
|
||||||
|
@ -268,8 +400,8 @@ function StreamSettings({
|
||||||
</div>
|
</div>
|
||||||
<div className="vcd-screen-picker-hint-description">
|
<div className="vcd-screen-picker-hint-description">
|
||||||
<p>
|
<p>
|
||||||
Choosing "Prefer Clarity" will result in a significantly lower framerate in
|
Choosing "Prefer Clarity" will result in a significantly lower framerate in exchange
|
||||||
exchange for a much sharper and clearer image.
|
for a much sharper and clearer image.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -285,60 +417,145 @@ function StreamSettings({
|
||||||
)}
|
)}
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
{isLinux && (
|
{isLinux && (
|
||||||
<AudioSourcePickerLinux
|
<AudioSourcePickerLinux
|
||||||
audioSource={settings.audioSource}
|
openSettings={openSettings}
|
||||||
workaround={settings.workaround}
|
includeSources={settings.includeSources}
|
||||||
onlyDefaultSpeakers={settings.onlyDefaultSpeakers}
|
excludeSources={settings.excludeSources}
|
||||||
setAudioSource={source => setSettings(s => ({ ...s, audioSource: source }))}
|
granularSelect={Settings.audio?.granularSelect}
|
||||||
setWorkaround={value => setSettings(s => ({ ...s, workaround: value }))}
|
setIncludeSources={sources => setSettings(s => ({ ...s, includeSources: sources }))}
|
||||||
setOnlyDefaultSpeakers={value => setSettings(s => ({ ...s, onlyDefaultSpeakers: value }))}
|
setExcludeSources={sources => setSettings(s => ({ ...s, excludeSources: sources }))}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isSpecialSource(value?: AudioSource | AudioSources): value is SpecialSource {
|
||||||
|
return typeof value === "string";
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasMatchingProps(value: Node, other: Node) {
|
||||||
|
return Object.keys(value).every(key => value[key] === other[key]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function mapToAudioItem(node: AudioSource, granularSelect?: boolean): AudioItem[] {
|
||||||
|
if (isSpecialSource(node)) {
|
||||||
|
return [{ name: node, value: node }];
|
||||||
|
}
|
||||||
|
|
||||||
|
const rtn: AudioItem[] = [];
|
||||||
|
|
||||||
|
const name = node["application.name"];
|
||||||
|
|
||||||
|
if (name) {
|
||||||
|
rtn.push({ name: name, value: { "application.name": name } });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!granularSelect) {
|
||||||
|
return rtn;
|
||||||
|
}
|
||||||
|
|
||||||
|
const binary = node["application.process.binary"];
|
||||||
|
|
||||||
|
if (!name) {
|
||||||
|
rtn.push({ name: binary, value: { "application.process.binary": binary } });
|
||||||
|
}
|
||||||
|
|
||||||
|
const pid = node["application.process.id"];
|
||||||
|
|
||||||
|
const first = rtn[0];
|
||||||
|
const firstValues = first.value as Node;
|
||||||
|
|
||||||
|
rtn.push({
|
||||||
|
name: `${first.name} (${pid})`,
|
||||||
|
value: { ...firstValues, "application.process.id": pid }
|
||||||
|
});
|
||||||
|
|
||||||
|
const mediaName = node["media.name"];
|
||||||
|
|
||||||
|
if (mediaName) {
|
||||||
|
rtn.push({
|
||||||
|
name: `${first.name} [${mediaName}]`,
|
||||||
|
value: { ...firstValues, "media.name": mediaName }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const mediaClass = node["media.class"];
|
||||||
|
|
||||||
|
if (!mediaClass) {
|
||||||
|
return rtn;
|
||||||
|
}
|
||||||
|
|
||||||
|
rtn.push({
|
||||||
|
name: `${first.name} [${mediaClass}]`,
|
||||||
|
value: { ...firstValues, "media.class": mediaClass }
|
||||||
|
});
|
||||||
|
|
||||||
|
return rtn;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isItemSelected(sources?: AudioSources) {
|
||||||
|
return (value: AudioSource) => {
|
||||||
|
if (!sources) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isSpecialSource(sources) || isSpecialSource(value)) {
|
||||||
|
return sources === value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return sources.some(source => hasMatchingProps(source, value));
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateItems(setSources: (s: AudioSources) => void, sources?: AudioSources) {
|
||||||
|
return (value: AudioSource) => {
|
||||||
|
if (isSpecialSource(value)) {
|
||||||
|
setSources(value);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isSpecialSource(sources)) {
|
||||||
|
setSources([value]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isItemSelected(sources)(value)) {
|
||||||
|
setSources(sources?.filter(x => !hasMatchingProps(x, value)) ?? "None");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setSources([...(sources || []), value]);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function AudioSourcePickerLinux({
|
function AudioSourcePickerLinux({
|
||||||
audioSource,
|
includeSources,
|
||||||
workaround,
|
excludeSources,
|
||||||
onlyDefaultSpeakers,
|
granularSelect,
|
||||||
setAudioSource,
|
openSettings,
|
||||||
setWorkaround,
|
setIncludeSources,
|
||||||
setOnlyDefaultSpeakers
|
setExcludeSources
|
||||||
}: {
|
}: {
|
||||||
audioSource?: string;
|
includeSources?: AudioSources;
|
||||||
workaround?: boolean;
|
excludeSources?: AudioSources;
|
||||||
onlyDefaultSpeakers?: boolean;
|
granularSelect?: boolean;
|
||||||
setAudioSource(s: string): void;
|
openSettings: () => void;
|
||||||
setWorkaround(b: boolean): void;
|
setIncludeSources: (s: AudioSources) => void;
|
||||||
setOnlyDefaultSpeakers(b: boolean): void;
|
setExcludeSources: (s: AudioSources) => void;
|
||||||
}) {
|
}) {
|
||||||
const [sources, _, loading] = useAwaiter(() => VesktopNative.virtmic.list(), {
|
const [sources, _, loading] = useAwaiter(() => VesktopNative.virtmic.list(), {
|
||||||
fallbackValue: { ok: true, targets: [], hasPipewirePulse: true }
|
fallbackValue: { ok: true, targets: [], hasPipewirePulse: true }
|
||||||
});
|
});
|
||||||
|
|
||||||
const allSources = sources.ok ? ["None", "Entire System", ...sources.targets] : null;
|
|
||||||
const hasPipewirePulse = sources.ok ? sources.hasPipewirePulse : true;
|
const hasPipewirePulse = sources.ok ? sources.hasPipewirePulse : true;
|
||||||
|
|
||||||
const [ignorePulseWarning, setIgnorePulseWarning] = useState(false);
|
const [ignorePulseWarning, setIgnorePulseWarning] = useState(false);
|
||||||
|
|
||||||
|
if (!sources.ok && sources.isGlibCxxOutdated) {
|
||||||
return (
|
return (
|
||||||
<>
|
|
||||||
<Forms.FormTitle>Audio Settings</Forms.FormTitle>
|
|
||||||
<Card className="vcd-screen-picker-card">
|
|
||||||
{loading ? (
|
|
||||||
<Forms.FormTitle>Loading Audio Sources...</Forms.FormTitle>
|
|
||||||
) : (
|
|
||||||
<Forms.FormTitle>Audio Source</Forms.FormTitle>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{!sources.ok && sources.isGlibCxxOutdated && (
|
|
||||||
<Forms.FormText>
|
<Forms.FormText>
|
||||||
Failed to retrieve Audio Sources because your C++ library is too old to run
|
Failed to retrieve Audio Sources because your C++ library is too old to run
|
||||||
<a href="https://github.com/Vencord/venmic" target="_blank">
|
<a href="https://github.com/Vencord/venmic" target="_blank">
|
||||||
|
@ -350,58 +567,81 @@ function AudioSourcePickerLinux({
|
||||||
</a>{" "}
|
</a>{" "}
|
||||||
for possible solutions.
|
for possible solutions.
|
||||||
</Forms.FormText>
|
</Forms.FormText>
|
||||||
)}
|
);
|
||||||
|
}
|
||||||
|
|
||||||
{hasPipewirePulse || ignorePulseWarning ? (
|
if (!hasPipewirePulse && !ignorePulseWarning) {
|
||||||
allSources && (
|
return (
|
||||||
<Select
|
|
||||||
options={allSources.map(s => ({ label: s, value: s, default: s === "None" }))}
|
|
||||||
isSelected={s => s === audioSource}
|
|
||||||
select={setAudioSource}
|
|
||||||
serialize={String}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
) : (
|
|
||||||
<Text variant="text-sm/normal">
|
<Text variant="text-sm/normal">
|
||||||
Could not find pipewire-pulse. This usually means that you do not run pipewire as your main
|
Could not find pipewire-pulse. See{" "}
|
||||||
audio-server. <br />
|
<a href="https://gist.github.com/the-spyke/2de98b22ff4f978ebf0650c90e82027e#install" target="_blank">
|
||||||
You can still continue, however, please beware that you can only share audio of apps that are
|
this guide
|
||||||
running under pipewire.
|
</a>{" "}
|
||||||
<br />
|
on how to switch to pipewire. <br />
|
||||||
<a onClick={() => setIgnorePulseWarning(true)}>I know what I'm doing</a>
|
You can still continue, however, please{" "}
|
||||||
|
<b>beware that you can only share audio of apps that are running under pipewire</b>.{" "}
|
||||||
|
<a onClick={() => setIgnorePulseWarning(true)}>I know what I'm doing!</a>
|
||||||
</Text>
|
</Text>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const specialSources: SpecialSource[] = ["None", "Entire System"] as const;
|
||||||
|
|
||||||
|
const uniqueName = (value: AudioItem, index: number, list: AudioItem[]) =>
|
||||||
|
list.findIndex(x => x.name === value.name) === index;
|
||||||
|
|
||||||
|
const allSources = sources.ok
|
||||||
|
? [...specialSources, ...sources.targets]
|
||||||
|
.map(target => mapToAudioItem(target, granularSelect))
|
||||||
|
.flat()
|
||||||
|
.filter(uniqueName)
|
||||||
|
: [];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className={includeSources === "Entire System" ? "vcd-screen-picker-quality" : undefined}>
|
||||||
|
<section>
|
||||||
|
<Forms.FormTitle>{loading ? "Loading Sources..." : "Audio Sources"}</Forms.FormTitle>
|
||||||
|
<Select
|
||||||
|
options={allSources.map(({ name, value }) => ({
|
||||||
|
label: name,
|
||||||
|
value: value,
|
||||||
|
default: name === "None"
|
||||||
|
}))}
|
||||||
|
isSelected={isItemSelected(includeSources)}
|
||||||
|
select={updateItems(setIncludeSources, includeSources)}
|
||||||
|
serialize={String}
|
||||||
|
popoutPosition="top"
|
||||||
|
closeOnSelect={false}
|
||||||
|
/>
|
||||||
|
</section>
|
||||||
|
{includeSources === "Entire System" && (
|
||||||
|
<section>
|
||||||
|
<Forms.FormTitle>Exclude Sources</Forms.FormTitle>
|
||||||
|
<Select
|
||||||
|
options={allSources
|
||||||
|
.filter(x => x.name !== "Entire System")
|
||||||
|
.map(({ name, value }) => ({
|
||||||
|
label: name,
|
||||||
|
value: value,
|
||||||
|
default: name === "None"
|
||||||
|
}))}
|
||||||
|
isSelected={isItemSelected(excludeSources)}
|
||||||
|
select={updateItems(setExcludeSources, excludeSources)}
|
||||||
|
serialize={String}
|
||||||
|
popoutPosition="top"
|
||||||
|
closeOnSelect={false}
|
||||||
|
/>
|
||||||
|
</section>
|
||||||
)}
|
)}
|
||||||
|
</div>
|
||||||
<Forms.FormDivider className={Margins.top16 + " " + Margins.bottom16} />
|
<Button
|
||||||
|
color={Button.Colors.TRANSPARENT}
|
||||||
<Switch
|
onClick={openSettings}
|
||||||
onChange={setWorkaround}
|
className="vcd-screen-picker-settings-button"
|
||||||
value={workaround ?? false}
|
|
||||||
note={
|
|
||||||
<>
|
|
||||||
Work around an issue that causes the microphone to be shared instead of the correct audio.
|
|
||||||
Only enable if you're experiencing this issue.
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
Microphone Workaround
|
Open Audio Settings
|
||||||
</Switch>
|
</Button>
|
||||||
|
|
||||||
<Switch
|
|
||||||
hideBorder
|
|
||||||
onChange={setOnlyDefaultSpeakers}
|
|
||||||
disabled={audioSource !== "Entire System"}
|
|
||||||
value={onlyDefaultSpeakers ?? true}
|
|
||||||
note={
|
|
||||||
<>
|
|
||||||
When sharing entire desktop audio, only share apps that play to the default speakers and
|
|
||||||
ignore apps that play to other speakers or devices.
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
Only Default Speakers
|
|
||||||
</Switch>
|
|
||||||
</Card>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -424,7 +664,8 @@ function ModalComponent({
|
||||||
resolution: "1080",
|
resolution: "1080",
|
||||||
fps: "60",
|
fps: "60",
|
||||||
contentHint: "motion",
|
contentHint: "motion",
|
||||||
audio: true
|
audio: true,
|
||||||
|
includeSources: "None"
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -11,17 +11,6 @@
|
||||||
gap: 1em;
|
gap: 1em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.vcd-screen-picker-settings-grid {
|
|
||||||
gap: 1em;
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 1fr 1fr;
|
|
||||||
}
|
|
||||||
|
|
||||||
.vcd-screen-picker-settings-grid>div {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.vcd-screen-picker-card {
|
.vcd-screen-picker-card {
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
}
|
}
|
||||||
|
@ -68,7 +57,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.vcd-screen-picker-preview-img-linux {
|
.vcd-screen-picker-preview-img-linux {
|
||||||
width: 100%;
|
width: 60%;
|
||||||
margin-bottom: 0.5em;
|
margin-bottom: 0.5em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -120,6 +109,11 @@
|
||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.vcd-screen-picker-settings-button {
|
||||||
|
margin-left: auto;
|
||||||
|
margin-top: 0.3rem;
|
||||||
|
}
|
||||||
|
|
||||||
.vcd-screen-picker-radios {
|
.vcd-screen-picker-radios {
|
||||||
display: flex;
|
display: flex;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
12
src/shared/settings.d.ts
vendored
12
src/shared/settings.d.ts
vendored
|
@ -30,6 +30,18 @@ export interface Settings {
|
||||||
splashBackground?: string;
|
splashBackground?: string;
|
||||||
|
|
||||||
spellCheckLanguages?: string[];
|
spellCheckLanguages?: string[];
|
||||||
|
|
||||||
|
audio?: {
|
||||||
|
workaround?: boolean;
|
||||||
|
granularSelect?: boolean;
|
||||||
|
|
||||||
|
ignoreVirtual?: boolean;
|
||||||
|
ignoreDevices?: boolean;
|
||||||
|
ignoreInputMedia?: boolean;
|
||||||
|
|
||||||
|
onlySpeakers?: boolean;
|
||||||
|
onlyDefaultSpeakers?: boolean;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface State {
|
export interface State {
|
||||||
|
|
Loading…
Reference in a new issue