Merge branch 'main' into main

This commit is contained in:
Tiagoquix 2024-06-17 18:38:19 -03:00 committed by GitHub
commit 034dd5be45
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 558 additions and 265 deletions

View file

@ -24,10 +24,10 @@
"updateMeta": "tsx scripts/utils/updateMeta.mts" "updateMeta": "tsx scripts/utils/updateMeta.mts"
}, },
"dependencies": { "dependencies": {
"arrpc": "github:OpenAsar/arrpc#6960a8fd4d65d566da93dbdb8a7ca474aa0a3c9c" "arrpc": "github:OpenAsar/arrpc#c62ec6a04c8d870530aa6944257fe745f6c59a24"
}, },
"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",
@ -37,7 +37,7 @@
"@typescript-eslint/parser": "^7.2.0", "@typescript-eslint/parser": "^7.2.0",
"@vencord/types": "^1.8.4", "@vencord/types": "^1.8.4",
"dotenv": "^16.4.5", "dotenv": "^16.4.5",
"electron": "^29.1.1", "electron": "^31.0.1",
"electron-builder": "^24.13.3", "electron-builder": "^24.13.3",
"esbuild": "^0.20.1", "esbuild": "^0.20.1",
"eslint": "^8.57.0", "eslint": "^8.57.0",
@ -165,5 +165,10 @@
"publish": { "publish": {
"provider": "github" "provider": "github"
} }
},
"pnpm": {
"patchedDependencies": {
"arrpc@3.4.0": "patches/arrpc@3.4.0.patch"
}
} }
} }

14
patches/arrpc@3.4.0.patch Normal file
View file

@ -0,0 +1,14 @@
diff --git a/src/process/index.js b/src/process/index.js
index 97ea6514b54dd9c5df588c78f0397d31ab5f882a..c2bdbd6aaa5611bc6ff1d993beeb380b1f5ec575 100644
--- a/src/process/index.js
+++ b/src/process/index.js
@@ -5,8 +5,7 @@ import fs from 'node:fs';
import { dirname, join } from 'path';
import { fileURLToPath } from 'url';
-const __dirname = dirname(fileURLToPath(import.meta.url));
-const DetectableDB = JSON.parse(fs.readFileSync(join(__dirname, 'detectable.json'), 'utf8'));
+const DetectableDB = require('./detectable.json');
import * as Natives from './native/index.js';
const Native = Natives[process.platform];

View file

@ -4,17 +4,22 @@ settings:
autoInstallPeers: true autoInstallPeers: true
excludeLinksFromLockfile: false excludeLinksFromLockfile: false
patchedDependencies:
arrpc@3.4.0:
hash: biyukfa6dww2wxujy4eyvkhrti
path: patches/arrpc@3.4.0.patch
importers: importers:
.: .:
dependencies: dependencies:
arrpc: arrpc:
specifier: github:OpenAsar/arrpc#6960a8fd4d65d566da93dbdb8a7ca474aa0a3c9c specifier: github:OpenAsar/arrpc#c62ec6a04c8d870530aa6944257fe745f6c59a24
version: https://codeload.github.com/OpenAsar/arrpc/tar.gz/6960a8fd4d65d566da93dbdb8a7ca474aa0a3c9c version: https://codeload.github.com/OpenAsar/arrpc/tar.gz/c62ec6a04c8d870530aa6944257fe745f6c59a24(patch_hash=biyukfa6dww2wxujy4eyvkhrti)
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
@ -38,8 +43,8 @@ importers:
specifier: ^16.4.5 specifier: ^16.4.5
version: 16.4.5 version: 16.4.5
electron: electron:
specifier: ^29.1.1 specifier: ^31.0.1
version: 29.1.1 version: 31.0.1
electron-builder: electron-builder:
specifier: ^24.13.3 specifier: ^24.13.3
version: 24.13.3(electron-builder-squirrel-windows@24.13.3(dmg-builder@24.13.3)) version: 24.13.3(electron-builder-squirrel-windows@24.13.3(dmg-builder@24.13.3))
@ -603,8 +608,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]
@ -727,9 +732,9 @@ packages:
resolution: {integrity: sha512-bMxMKAjg13EBSVscxTaYA4mRc5t1UAXa2kXiGTNfZ079HIWXEkKmkgFrh/nJqamaLSrXO5H4WFFkPEaLJWbs3A==} resolution: {integrity: sha512-bMxMKAjg13EBSVscxTaYA4mRc5t1UAXa2kXiGTNfZ079HIWXEkKmkgFrh/nJqamaLSrXO5H4WFFkPEaLJWbs3A==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
arrpc@https://codeload.github.com/OpenAsar/arrpc/tar.gz/6960a8fd4d65d566da93dbdb8a7ca474aa0a3c9c: arrpc@https://codeload.github.com/OpenAsar/arrpc/tar.gz/c62ec6a04c8d870530aa6944257fe745f6c59a24:
resolution: {tarball: https://codeload.github.com/OpenAsar/arrpc/tar.gz/6960a8fd4d65d566da93dbdb8a7ca474aa0a3c9c} resolution: {tarball: https://codeload.github.com/OpenAsar/arrpc/tar.gz/c62ec6a04c8d870530aa6944257fe745f6c59a24}
version: 3.3.1 version: 3.4.0
hasBin: true hasBin: true
assert-plus@1.0.0: assert-plus@1.0.0:
@ -1080,8 +1085,8 @@ packages:
electron-publish@24.13.1: electron-publish@24.13.1:
resolution: {integrity: sha512-2ZgdEqJ8e9D17Hwp5LEq5mLQPjqU3lv/IALvgp+4W8VeNhryfGhYEQC/PgDPMrnWUp+l60Ou5SJLsu+k4mhQ8A==} resolution: {integrity: sha512-2ZgdEqJ8e9D17Hwp5LEq5mLQPjqU3lv/IALvgp+4W8VeNhryfGhYEQC/PgDPMrnWUp+l60Ou5SJLsu+k4mhQ8A==}
electron@29.1.1: electron@31.0.1:
resolution: {integrity: sha512-cXN15NgCi7MkzGo5/23ZQbii+0UfhmUiDjACunmzcUofYCjF42XhFbL7JZnwgI0qtBCCeJU8qZNZt9lU91gUFw==} resolution: {integrity: sha512-2eBcp4iqLkTsml6mMq+iqrS5u3kJ/2mpOLP7Mj7lo0uNK3OyfNqRS9z1ArsHjBF2/HV250Te/O9nKrwQRTX/+g==}
engines: {node: '>= 12.20.55'} engines: {node: '>= 12.20.55'}
hasBin: true hasBin: true
@ -1441,6 +1446,7 @@ packages:
glob@7.2.3: glob@7.2.3:
resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==}
deprecated: Glob versions prior to v9 are no longer supported
global-agent@3.0.0: global-agent@3.0.0:
resolution: {integrity: sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q==} resolution: {integrity: sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q==}
@ -1561,6 +1567,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==}
@ -2155,6 +2162,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:
@ -3031,7 +3039,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
@ -3220,7 +3228,7 @@ snapshots:
is-array-buffer: 3.0.4 is-array-buffer: 3.0.4
is-shared-array-buffer: 1.0.3 is-shared-array-buffer: 1.0.3
arrpc@https://codeload.github.com/OpenAsar/arrpc/tar.gz/6960a8fd4d65d566da93dbdb8a7ca474aa0a3c9c: arrpc@https://codeload.github.com/OpenAsar/arrpc/tar.gz/c62ec6a04c8d870530aa6944257fe745f6c59a24(patch_hash=biyukfa6dww2wxujy4eyvkhrti):
dependencies: dependencies:
ws: 8.13.0 ws: 8.13.0
transitivePeerDependencies: transitivePeerDependencies:
@ -3651,7 +3659,7 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
electron@29.1.1: electron@31.0.1:
dependencies: dependencies:
'@electron/get': 2.0.3 '@electron/get': 2.0.3
'@types/node': 20.11.26 '@types/node': 20.11.26

View file

@ -12,11 +12,13 @@ export function registerMediaPermissionsHandler() {
session.defaultSession.setPermissionRequestHandler(async (_webContents, permission, callback, details) => { session.defaultSession.setPermissionRequestHandler(async (_webContents, permission, callback, details) => {
let granted = true; let granted = true;
if (details.mediaTypes?.includes("audio")) { if ("mediaTypes" in details) {
granted = await systemPreferences.askForMediaAccess("microphone"); if (details.mediaTypes?.includes("audio")) {
} granted &&= await systemPreferences.askForMediaAccess("microphone");
if (details.mediaTypes?.includes("video")) { }
granted &&= await systemPreferences.askForMediaAccess("camera"); if (details.mediaTypes?.includes("video")) {
granted &&= await systemPreferences.askForMediaAccess("camera");
}
} }
callback(granted); callback(granted);

View file

@ -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);

View file

@ -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: {

View file

@ -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,230 +302,346 @@ 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"> <img
<img src={thumb}
src={thumb} alt=""
alt="" className={isLinux ? "vcd-screen-picker-preview-img-linux" : "vcd-screen-picker-preview-img"}
className={isLinux ? "vcd-screen-picker-preview-img-linux" : "vcd-screen-picker-preview-img"} />
/> <Text variant="text-sm/normal">{source.name}</Text>
<Text variant="text-sm/normal">{source.name}</Text> </Card>
</Card>
<Forms.FormTitle>Stream Settings</Forms.FormTitle> <Forms.FormTitle>Stream Settings</Forms.FormTitle>
<Card className="vcd-screen-picker-card"> <Card className="vcd-screen-picker-card">
<div className="vcd-screen-picker-quality"> <div className="vcd-screen-picker-quality">
<section> <section>
<Forms.FormTitle>Resolution</Forms.FormTitle> <Forms.FormTitle>Resolution</Forms.FormTitle>
<div className="vcd-screen-picker-radios">
{StreamResolutions.map(res => (
<label className="vcd-screen-picker-radio" data-checked={settings.resolution === res}>
<Text variant="text-sm/bold">{res}</Text>
<input
type="radio"
name="resolution"
value={res}
checked={settings.resolution === res}
onChange={() => setSettings(s => ({ ...s, resolution: res }))}
/>
</label>
))}
</div>
</section>
<section>
<Forms.FormTitle>Frame Rate</Forms.FormTitle>
<div className="vcd-screen-picker-radios">
{StreamFps.map(fps => (
<label className="vcd-screen-picker-radio" data-checked={settings.fps === fps}>
<Text variant="text-sm/bold">{fps}</Text>
<input
type="radio"
name="fps"
value={fps}
checked={settings.fps === fps}
onChange={() => setSettings(s => ({ ...s, fps }))}
/>
</label>
))}
</div>
</section>
</div>
<div className="vcd-screen-picker-quality">
<section>
<Forms.FormTitle>Content Type</Forms.FormTitle>
<div>
<div className="vcd-screen-picker-radios"> <div className="vcd-screen-picker-radios">
{StreamResolutions.map(res => ( <label
<label className="vcd-screen-picker-radio"
className="vcd-screen-picker-radio" data-checked={settings.contentHint === "motion"}
data-checked={settings.resolution === res}
>
<Text variant="text-sm/bold">{res}</Text>
<input
type="radio"
name="resolution"
value={res}
checked={settings.resolution === res}
onChange={() => setSettings(s => ({ ...s, resolution: res }))}
/>
</label>
))}
</div>
</section>
<section>
<Forms.FormTitle>Frame Rate</Forms.FormTitle>
<div className="vcd-screen-picker-radios">
{StreamFps.map(fps => (
<label className="vcd-screen-picker-radio" data-checked={settings.fps === fps}>
<Text variant="text-sm/bold">{fps}</Text>
<input
type="radio"
name="fps"
value={fps}
checked={settings.fps === fps}
onChange={() => setSettings(s => ({ ...s, fps }))}
/>
</label>
))}
</div>
</section>
</div>
<div className="vcd-screen-picker-quality">
<section>
<Forms.FormTitle>Content Type</Forms.FormTitle>
<div>
<div className="vcd-screen-picker-radios">
<label
className="vcd-screen-picker-radio"
data-checked={settings.contentHint === "motion"}
>
<Text variant="text-sm/bold">Prefer Smoothness</Text>
<input
type="radio"
name="contenthint"
value="motion"
checked={settings.contentHint === "motion"}
onChange={() => setSettings(s => ({ ...s, contentHint: "motion" }))}
/>
</label>
<label
className="vcd-screen-picker-radio"
data-checked={settings.contentHint === "detail"}
>
<Text variant="text-sm/bold">Prefer Clarity</Text>
<input
type="radio"
name="contenthint"
value="detail"
checked={settings.contentHint === "detail"}
onChange={() => setSettings(s => ({ ...s, contentHint: "detail" }))}
/>
</label>
</div>
<div className="vcd-screen-picker-hint-description">
<p>
Choosing "Prefer Clarity" will result in a significantly lower framerate in
exchange for a much sharper and clearer image.
</p>
</div>
</div>
{isWindows && (
<Switch
value={settings.audio}
onChange={checked => setSettings(s => ({ ...s, audio: checked }))}
hideBorder
className="vcd-screen-picker-audio"
> >
Stream With Audio <Text variant="text-sm/bold">Prefer Smoothness</Text>
</Switch> <input
)} type="radio"
</section> name="contenthint"
</div> value="motion"
</Card> checked={settings.contentHint === "motion"}
</div> onChange={() => setSettings(s => ({ ...s, contentHint: "motion" }))}
/>
</label>
<label
className="vcd-screen-picker-radio"
data-checked={settings.contentHint === "detail"}
>
<Text variant="text-sm/bold">Prefer Clarity</Text>
<input
type="radio"
name="contenthint"
value="detail"
checked={settings.contentHint === "detail"}
onChange={() => setSettings(s => ({ ...s, contentHint: "detail" }))}
/>
</label>
</div>
<div className="vcd-screen-picker-hint-description">
<p>
Choosing "Prefer Clarity" will result in a significantly lower framerate in exchange
for a much sharper and clearer image.
</p>
</div>
</div>
{isWindows && (
<Switch
value={settings.audio}
onChange={checked => setSettings(s => ({ ...s, audio: checked }))}
hideBorder
className="vcd-screen-picker-audio"
>
Stream With Audio
</Switch>
)}
</section>
</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 (
<Forms.FormText>
Failed to retrieve Audio Sources because your C++ library is too old to run
<a href="https://github.com/Vencord/venmic" target="_blank">
venmic
</a>
. See{" "}
<a href="https://gist.github.com/Vendicated/b655044ffbb16b2716095a448c6d827a" target="_blank">
this guide
</a>{" "}
for possible solutions.
</Forms.FormText>
);
}
if (!hasPipewirePulse && !ignorePulseWarning) {
return (
<Text variant="text-sm/normal">
Could not find pipewire-pulse. See{" "}
<a href="https://gist.github.com/the-spyke/2de98b22ff4f978ebf0650c90e82027e#install" target="_blank">
this guide
</a>{" "}
on how to switch to pipewire. <br />
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>
);
}
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 ( return (
<> <>
<Forms.FormTitle>Audio Settings</Forms.FormTitle> <div className={includeSources === "Entire System" ? "vcd-screen-picker-quality" : undefined}>
<Card className="vcd-screen-picker-card"> <section>
{loading ? ( <Forms.FormTitle>{loading ? "Loading Sources..." : "Audio Sources"}</Forms.FormTitle>
<Forms.FormTitle>Loading Audio Sources...</Forms.FormTitle> <Select
) : ( options={allSources.map(({ name, value }) => ({
<Forms.FormTitle>Audio Source</Forms.FormTitle> label: name,
)} value: value,
default: name === "None"
{!sources.ok && sources.isGlibCxxOutdated && ( }))}
<Forms.FormText> isSelected={isItemSelected(includeSources)}
Failed to retrieve Audio Sources because your C++ library is too old to run select={updateItems(setIncludeSources, includeSources)}
<a href="https://github.com/Vencord/venmic" target="_blank"> serialize={String}
venmic popoutPosition="top"
</a> closeOnSelect={false}
. See{" "} />
<a href="https://gist.github.com/Vendicated/b655044ffbb16b2716095a448c6d827a" target="_blank"> </section>
this guide {includeSources === "Entire System" && (
</a>{" "} <section>
for possible solutions. <Forms.FormTitle>Exclude Sources</Forms.FormTitle>
</Forms.FormText>
)}
{hasPipewirePulse || ignorePulseWarning ? (
allSources && (
<Select <Select
options={allSources.map(s => ({ label: s, value: s, default: s === "None" }))} options={allSources
isSelected={s => s === audioSource} .filter(x => x.name !== "Entire System")
select={setAudioSource} .map(({ name, value }) => ({
label: name,
value: value,
default: name === "None"
}))}
isSelected={isItemSelected(excludeSources)}
select={updateItems(setExcludeSources, excludeSources)}
serialize={String} serialize={String}
popoutPosition="top"
closeOnSelect={false}
/> />
) </section>
) : (
<Text variant="text-sm/normal">
Could not find pipewire-pulse. This usually means that you do not run pipewire as your main
audio-server. <br />
You can still continue, however, please beware that you can only share audio of apps that are
running under pipewire.
<br />
<a onClick={() => setIgnorePulseWarning(true)}>I know what I'm doing</a>
</Text>
)} )}
</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={ Open Audio Settings
<> </Button>
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={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 (

View file

@ -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%;

View file

@ -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 {