diff --git a/package.json b/package.json index 5bbec49..67571bd 100644 --- a/package.json +++ b/package.json @@ -24,10 +24,10 @@ "updateMeta": "tsx scripts/utils/updateMeta.mts" }, "dependencies": { - "arrpc": "github:OpenAsar/arrpc#6960a8fd4d65d566da93dbdb8a7ca474aa0a3c9c" + "arrpc": "github:OpenAsar/arrpc#c62ec6a04c8d870530aa6944257fe745f6c59a24" }, "optionalDependencies": { - "@vencord/venmic": "^3.5.0" + "@vencord/venmic": "^6.1.0" }, "devDependencies": { "@fal-works/esbuild-plugin-global-externals": "^2.1.2", @@ -37,7 +37,7 @@ "@typescript-eslint/parser": "^7.2.0", "@vencord/types": "^1.8.4", "dotenv": "^16.4.5", - "electron": "^29.1.1", + "electron": "^31.0.1", "electron-builder": "^24.13.3", "esbuild": "^0.20.1", "eslint": "^8.57.0", @@ -165,5 +165,10 @@ "publish": { "provider": "github" } + }, + "pnpm": { + "patchedDependencies": { + "arrpc@3.4.0": "patches/arrpc@3.4.0.patch" + } } } diff --git a/patches/arrpc@3.4.0.patch b/patches/arrpc@3.4.0.patch new file mode 100644 index 0000000..8dfd9f6 --- /dev/null +++ b/patches/arrpc@3.4.0.patch @@ -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]; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6221301..9cbb7be 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -4,17 +4,22 @@ settings: autoInstallPeers: true excludeLinksFromLockfile: false +patchedDependencies: + arrpc@3.4.0: + hash: biyukfa6dww2wxujy4eyvkhrti + path: patches/arrpc@3.4.0.patch + importers: .: dependencies: arrpc: - specifier: github:OpenAsar/arrpc#6960a8fd4d65d566da93dbdb8a7ca474aa0a3c9c - version: https://codeload.github.com/OpenAsar/arrpc/tar.gz/6960a8fd4d65d566da93dbdb8a7ca474aa0a3c9c + specifier: github:OpenAsar/arrpc#c62ec6a04c8d870530aa6944257fe745f6c59a24 + version: https://codeload.github.com/OpenAsar/arrpc/tar.gz/c62ec6a04c8d870530aa6944257fe745f6c59a24(patch_hash=biyukfa6dww2wxujy4eyvkhrti) optionalDependencies: '@vencord/venmic': - specifier: ^3.5.0 - version: 3.5.0 + specifier: ^6.1.0 + version: 6.1.0 devDependencies: '@fal-works/esbuild-plugin-global-externals': specifier: ^2.1.2 @@ -38,8 +43,8 @@ importers: specifier: ^16.4.5 version: 16.4.5 electron: - specifier: ^29.1.1 - version: 29.1.1 + specifier: ^31.0.1 + version: 31.0.1 electron-builder: specifier: ^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': resolution: {integrity: sha512-ogLqIOHVO+5zxKUVxAfGIAUZoEfIomrlg6f0cZ/2yd5vBAn1fA9Gi/NASoKfHZuJt8ZcYw329bgn0ah/VufqMg==} - '@vencord/venmic@3.5.0': - resolution: {integrity: sha512-kPvrPcIeMkuqQriuiQAJ9rEBeqGR2nmFBuUtbZRGyiNRF9RDAfWSJYqhHVm6F7wbcqrSZio6FazZuBo0LvjJRw==} + '@vencord/venmic@6.1.0': + resolution: {integrity: sha512-YiCtzml/W8tYbGhu3jm5jfbbEnl2slKKARNK0jO+8qV979k9eFnfIRTxvhMN/SWq1h8ZNJdXVwvXpffQwq0RuA==} engines: {node: '>=14.15'} os: [linux] @@ -727,9 +732,9 @@ packages: resolution: {integrity: sha512-bMxMKAjg13EBSVscxTaYA4mRc5t1UAXa2kXiGTNfZ079HIWXEkKmkgFrh/nJqamaLSrXO5H4WFFkPEaLJWbs3A==} engines: {node: '>= 0.4'} - arrpc@https://codeload.github.com/OpenAsar/arrpc/tar.gz/6960a8fd4d65d566da93dbdb8a7ca474aa0a3c9c: - resolution: {tarball: https://codeload.github.com/OpenAsar/arrpc/tar.gz/6960a8fd4d65d566da93dbdb8a7ca474aa0a3c9c} - version: 3.3.1 + arrpc@https://codeload.github.com/OpenAsar/arrpc/tar.gz/c62ec6a04c8d870530aa6944257fe745f6c59a24: + resolution: {tarball: https://codeload.github.com/OpenAsar/arrpc/tar.gz/c62ec6a04c8d870530aa6944257fe745f6c59a24} + version: 3.4.0 hasBin: true assert-plus@1.0.0: @@ -1080,8 +1085,8 @@ packages: electron-publish@24.13.1: resolution: {integrity: sha512-2ZgdEqJ8e9D17Hwp5LEq5mLQPjqU3lv/IALvgp+4W8VeNhryfGhYEQC/PgDPMrnWUp+l60Ou5SJLsu+k4mhQ8A==} - electron@29.1.1: - resolution: {integrity: sha512-cXN15NgCi7MkzGo5/23ZQbii+0UfhmUiDjACunmzcUofYCjF42XhFbL7JZnwgI0qtBCCeJU8qZNZt9lU91gUFw==} + electron@31.0.1: + resolution: {integrity: sha512-2eBcp4iqLkTsml6mMq+iqrS5u3kJ/2mpOLP7Mj7lo0uNK3OyfNqRS9z1ArsHjBF2/HV250Te/O9nKrwQRTX/+g==} engines: {node: '>= 12.20.55'} hasBin: true @@ -1441,6 +1446,7 @@ packages: glob@7.2.3: resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} + deprecated: Glob versions prior to v9 are no longer supported global-agent@3.0.0: resolution: {integrity: sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q==} @@ -1561,6 +1567,7 @@ packages: inflight@1.0.6: 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: resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} @@ -2155,6 +2162,7 @@ packages: rimraf@3.0.2: resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} + deprecated: Rimraf versions prior to v4 are no longer supported hasBin: true roarr@2.15.4: @@ -3031,7 +3039,7 @@ snapshots: standalone-electron-types: 1.0.0 type-fest: 3.13.1 - '@vencord/venmic@3.5.0': + '@vencord/venmic@6.1.0': dependencies: cmake-js: 7.3.0 node-addon-api: 8.0.0 @@ -3220,7 +3228,7 @@ snapshots: is-array-buffer: 3.0.4 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: ws: 8.13.0 transitivePeerDependencies: @@ -3651,7 +3659,7 @@ snapshots: transitivePeerDependencies: - supports-color - electron@29.1.1: + electron@31.0.1: dependencies: '@electron/get': 2.0.3 '@types/node': 20.11.26 diff --git a/src/main/mediaPermissions.ts b/src/main/mediaPermissions.ts index 1f6cf46..f7765be 100644 --- a/src/main/mediaPermissions.ts +++ b/src/main/mediaPermissions.ts @@ -12,11 +12,13 @@ export function registerMediaPermissionsHandler() { session.defaultSession.setPermissionRequestHandler(async (_webContents, permission, callback, details) => { let granted = true; - if (details.mediaTypes?.includes("audio")) { - granted = await systemPreferences.askForMediaAccess("microphone"); - } - if (details.mediaTypes?.includes("video")) { - granted &&= await systemPreferences.askForMediaAccess("camera"); + if ("mediaTypes" in details) { + if (details.mediaTypes?.includes("audio")) { + granted &&= await systemPreferences.askForMediaAccess("microphone"); + } + if (details.mediaTypes?.includes("video")) { + granted &&= await systemPreferences.askForMediaAccess("camera"); + } } callback(granted); diff --git a/src/main/venmic.ts b/src/main/venmic.ts index 404c0d2..885fd2d 100644 --- a/src/main/venmic.ts +++ b/src/main/venmic.ts @@ -4,13 +4,13 @@ * 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 { join } from "path"; import { IpcEvents } from "shared/IpcEvents"; import { STATIC_DIR } from "shared/paths"; -type LinkData = Parameters[0]; +import { Settings } from "./settings"; let PatchBay: typeof PatchBayType | undefined; let patchBayInstance: PatchBayType | undefined; @@ -69,47 +69,64 @@ function getRendererAudioServicePid() { ipcMain.handle(IpcEvents.VIRT_MIC_LIST, () => { const audioPid = getRendererAudioServicePid(); - const list = obtainVenmic() - ?.list() - .filter(s => s["application.process.id"] !== audioPid) - .map(s => s["application.name"]); + const { granularSelect } = Settings.store.audio ?? {}; - 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 { ignoreDevices, ignoreInputMedia, ignoreVirtual, workaround } = Settings.store.audio ?? {}; const data: LinkData = { - include: targets.map(target => ({ key: "application.name", value: target })), - exclude: [{ key: "application.process.id", value: pid }] + include, + 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) { - data.workaround = [ - { key: "application.process.id", value: pid }, - { key: "media.name", value: "RecordStream" } - ]; + data.workaround = [{ "application.process.id": pid, "media.name": "RecordStream" }]; } 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 { workaround, ignoreDevices, ignoreInputMedia, ignoreVirtual, onlySpeakers, onlyDefaultSpeakers } = + Settings.store.audio ?? {}; + 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 }; + if (ignoreInputMedia ?? true) { + data.exclude.push({ "media.class": "Stream/Input/Audio" }); + } + + if (ignoreVirtual) { + data.exclude.push({ "node.virtual": "true" }); + } + if (workaround) { - data.workaround = [ - { key: "application.process.id", value: pid }, - { key: "media.name", value: "RecordStream" } - ]; + data.workaround = [{ "application.process.id": pid, "media.name": "RecordStream" }]; } return obtainVenmic()?.link(data); diff --git a/src/preload/VesktopNative.ts b/src/preload/VesktopNative.ts index bea57d4..c8fc13f 100644 --- a/src/preload/VesktopNative.ts +++ b/src/preload/VesktopNative.ts @@ -4,6 +4,7 @@ * Copyright (c) 2023 Vendicated and Vencord contributors */ +import { Node } from "@vencord/venmic"; import { ipcRenderer } from "electron"; import type { Settings } from "shared/settings"; import type { LiteralUnion } from "type-fest"; @@ -63,11 +64,10 @@ export const VesktopNative = { virtmic: { list: () => 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), - start: (targets: string[], workaround?: boolean) => invoke(IpcEvents.VIRT_MIC_START, targets, workaround), - startSystem: (workaround?: boolean, onlyDefaultSpeakers?: boolean) => - invoke(IpcEvents.VIRT_MIC_START_SYSTEM, workaround, onlyDefaultSpeakers), + start: (include: Node[]) => invoke(IpcEvents.VIRT_MIC_START, include), + startSystem: (exclude: Node[]) => invoke(IpcEvents.VIRT_MIC_START_SYSTEM, exclude), stop: () => invoke(IpcEvents.VIRT_MIC_STOP) }, arrpc: { diff --git a/src/renderer/components/ScreenSharePicker.tsx b/src/renderer/components/ScreenSharePicker.tsx index ac9fc0b..c306c53 100644 --- a/src/renderer/components/ScreenSharePicker.tsx +++ b/src/renderer/components/ScreenSharePicker.tsx @@ -6,7 +6,7 @@ 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 { Button, @@ -19,8 +19,10 @@ import { UserStore, useState } from "@vencord/types/webpack/common"; +import { Node } from "@vencord/venmic"; import type { Dispatch, SetStateAction } from "react"; import { addPatch } from "renderer/patches/shared"; +import { useSettings } from "renderer/settings"; import { isLinux, isWindows } from "renderer/utils"; const StreamResolutions = ["480", "720", "1080", "1440"] as const; @@ -31,14 +33,23 @@ const MediaEngineStore = findStoreLazy("MediaEngineStore"); export type StreamResolution = (typeof StreamResolutions)[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 { resolution: StreamResolution; fps: StreamFps; audio: boolean; - audioSource?: string; contentHint?: string; - workaround?: boolean; - onlyDefaultSpeakers?: boolean; + includeSources?: AudioSources; + excludeSources?: AudioSources; } export interface StreamPick extends StreamSettings { @@ -118,13 +129,17 @@ export function openScreenSharePicker(screens: Source[], skipPicker: boolean) { modalProps={props} submit={async v => { didSubmit = true; - if (v.audioSource && v.audioSource !== "None") { - if (v.audioSource === "Entire System") { - await VesktopNative.virtmic.startSystem(v.workaround); + + if (v.includeSources && v.includeSources !== "None") { + if (v.includeSources === "Entire System") { + await VesktopNative.virtmic.startSystem( + !v.excludeSources || isSpecialSource(v.excludeSources) ? [] : v.excludeSources + ); } else { - await VesktopNative.virtmic.start([v.audioSource], v.workaround); + await VesktopNative.virtmic.start(v.includeSources); } } + resolve(v); }} 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 ( + + + Venmic Settings + + + + (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 + + (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 + + (Settings.audio = { ...Settings.audio, onlyDefaultSpeakers: v })} + value={Settings.audio?.onlyDefaultSpeakers ?? true} + note={ + <> + When sharing entire desktop audio, only share apps that play to the default speakers. + You may want to disable this when using "mix bussing". + + } + > + Only Default Speakers + + (Settings.audio = { ...Settings.audio, ignoreInputMedia: v })} + value={Settings.audio?.ignoreInputMedia ?? true} + note={<>Exclude nodes that are intended to capture audio.} + > + Ignore Inputs + + (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 + + (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 + + { + Settings.audio = { ...Settings.audio, granularSelect: value }; + setAudioSources("None"); + }} + value={Settings.audio?.granularSelect ?? false} + note={<>Allow to select applications more granularly.} + > + Granular Selection + + + + + + + ); +} + function StreamSettings({ source, settings, @@ -170,6 +292,8 @@ function StreamSettings({ setSettings: Dispatch>; skipPicker: boolean; }) { + const Settings = useSettings(); + const [thumb] = useAwaiter( () => (skipPicker ? Promise.resolve(source.url) : VesktopNative.capturer.getLargeThumbnail(source.id)), { @@ -178,230 +302,346 @@ function StreamSettings({ } ); + const openSettings = () => { + const key = openModal(props => ( + props.onClose()} + setAudioSources={sources => + setSettings(s => ({ ...s, includeSources: sources, excludeSources: sources })) + } + /> + )); + }; + return ( -
-
- What you're streaming - - - {source.name} - +
+ What you're streaming + + + {source.name} + - Stream Settings + Stream Settings - -
-
- Resolution + +
+
+ Resolution +
+ {StreamResolutions.map(res => ( + + ))} +
+
+ +
+ Frame Rate +
+ {StreamFps.map(fps => ( + + ))} +
+
+
+
+
+ Content Type +
- {StreamResolutions.map(res => ( - - ))} -
-
- -
- Frame Rate -
- {StreamFps.map(fps => ( - - ))} -
-
-
-
-
- Content Type -
-
- - -
-
-

- Choosing "Prefer Clarity" will result in a significantly lower framerate in - exchange for a much sharper and clearer image. -

-
-
- {isWindows && ( - setSettings(s => ({ ...s, audio: checked }))} - hideBorder - className="vcd-screen-picker-audio" + - )} -
-
-
-
+ Prefer Smoothness + setSettings(s => ({ ...s, contentHint: "motion" }))} + /> + + +
+
+

+ Choosing "Prefer Clarity" will result in a significantly lower framerate in exchange + for a much sharper and clearer image. +

+
+
+ {isWindows && ( + setSettings(s => ({ ...s, audio: checked }))} + hideBorder + className="vcd-screen-picker-audio" + > + Stream With Audio + + )} + +
-
{isLinux && ( setSettings(s => ({ ...s, audioSource: source }))} - setWorkaround={value => setSettings(s => ({ ...s, workaround: value }))} - setOnlyDefaultSpeakers={value => setSettings(s => ({ ...s, onlyDefaultSpeakers: value }))} + openSettings={openSettings} + includeSources={settings.includeSources} + excludeSources={settings.excludeSources} + granularSelect={Settings.audio?.granularSelect} + setIncludeSources={sources => setSettings(s => ({ ...s, includeSources: sources }))} + setExcludeSources={sources => setSettings(s => ({ ...s, excludeSources: sources }))} /> )} -
+ ); } +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({ - audioSource, - workaround, - onlyDefaultSpeakers, - setAudioSource, - setWorkaround, - setOnlyDefaultSpeakers + includeSources, + excludeSources, + granularSelect, + openSettings, + setIncludeSources, + setExcludeSources }: { - audioSource?: string; - workaround?: boolean; - onlyDefaultSpeakers?: boolean; - setAudioSource(s: string): void; - setWorkaround(b: boolean): void; - setOnlyDefaultSpeakers(b: boolean): void; + includeSources?: AudioSources; + excludeSources?: AudioSources; + granularSelect?: boolean; + openSettings: () => void; + setIncludeSources: (s: AudioSources) => void; + setExcludeSources: (s: AudioSources) => void; }) { const [sources, _, loading] = useAwaiter(() => VesktopNative.virtmic.list(), { fallbackValue: { ok: true, targets: [], hasPipewirePulse: true } }); - const allSources = sources.ok ? ["None", "Entire System", ...sources.targets] : null; const hasPipewirePulse = sources.ok ? sources.hasPipewirePulse : true; - const [ignorePulseWarning, setIgnorePulseWarning] = useState(false); + if (!sources.ok && sources.isGlibCxxOutdated) { + return ( + + Failed to retrieve Audio Sources because your C++ library is too old to run + + venmic + + . See{" "} + + this guide + {" "} + for possible solutions. + + ); + } + + if (!hasPipewirePulse && !ignorePulseWarning) { + return ( + + Could not find pipewire-pulse. See{" "} + + this guide + {" "} + on how to switch to pipewire.
+ You can still continue, however, please{" "} + beware that you can only share audio of apps that are running under pipewire.{" "} + setIgnorePulseWarning(true)}>I know what I'm doing! +
+ ); + } + + 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 ( <> - Audio Settings - - {loading ? ( - Loading Audio Sources... - ) : ( - Audio Source - )} - - {!sources.ok && sources.isGlibCxxOutdated && ( - - Failed to retrieve Audio Sources because your C++ library is too old to run - - venmic - - . See{" "} - - this guide - {" "} - for possible solutions. - - )} - - {hasPipewirePulse || ignorePulseWarning ? ( - allSources && ( +
+
+ {loading ? "Loading Sources..." : "Audio Sources"} + ({ label: s, value: s, default: s === "None" }))} - isSelected={s => s === audioSource} - select={setAudioSource} + 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} /> - ) - ) : ( - - Could not find pipewire-pulse. This usually means that you do not run pipewire as your main - audio-server.
- You can still continue, however, please beware that you can only share audio of apps that are - running under pipewire. -
- setIgnorePulseWarning(true)}>I know what I'm doing -
+
)} - - - - - 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 - - - - 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 - - +
+ ); } @@ -424,7 +664,8 @@ function ModalComponent({ resolution: "1080", fps: "60", contentHint: "motion", - audio: true + audio: true, + includeSources: "None" }); return ( diff --git a/src/renderer/components/screenSharePicker.css b/src/renderer/components/screenSharePicker.css index 645f73c..4f49323 100644 --- a/src/renderer/components/screenSharePicker.css +++ b/src/renderer/components/screenSharePicker.css @@ -11,17 +11,6 @@ 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 { flex-grow: 1; } @@ -68,7 +57,7 @@ } .vcd-screen-picker-preview-img-linux { - width: 100%; + width: 60%; margin-bottom: 0.5em; } @@ -120,6 +109,11 @@ flex: 1 1 auto; } +.vcd-screen-picker-settings-button { + margin-left: auto; + margin-top: 0.3rem; +} + .vcd-screen-picker-radios { display: flex; width: 100%; @@ -148,4 +142,4 @@ font-size: 14px; line-height: 20px; font-weight: 400; -} \ No newline at end of file +} diff --git a/src/shared/settings.d.ts b/src/shared/settings.d.ts index a30d5cd..3eb96a0 100644 --- a/src/shared/settings.d.ts +++ b/src/shared/settings.d.ts @@ -30,6 +30,18 @@ export interface Settings { splashBackground?: string; spellCheckLanguages?: string[]; + + audio?: { + workaround?: boolean; + granularSelect?: boolean; + + ignoreVirtual?: boolean; + ignoreDevices?: boolean; + ignoreInputMedia?: boolean; + + onlySpeakers?: boolean; + onlyDefaultSpeakers?: boolean; + }; } export interface State {