From 50fc6844f9c1dd93b82251212abeae9e1d6aaaf1 Mon Sep 17 00:00:00 2001 From: Curve Date: Fri, 31 May 2024 00:44:56 +0200 Subject: [PATCH] feat(ScreenShare): add granular selection --- package.json | 2 +- pnpm-lock.yaml | 27 +-- src/main/venmic.ts | 27 +-- src/preload/VesktopNative.ts | 6 +- src/renderer/components/ScreenSharePicker.tsx | 187 +++++++++++++----- 5 files changed, 161 insertions(+), 88 deletions(-) diff --git a/package.json b/package.json index 5bbec49..6052cb6 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,7 @@ "arrpc": "github:OpenAsar/arrpc#6960a8fd4d65d566da93dbdb8a7ca474aa0a3c9c" }, "optionalDependencies": { - "@vencord/venmic": "^3.5.0" + "@vencord/venmic": "^4.0.1" }, "devDependencies": { "@fal-works/esbuild-plugin-global-externals": "^2.1.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6221301..44f55fe 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -13,8 +13,8 @@ importers: version: https://codeload.github.com/OpenAsar/arrpc/tar.gz/6960a8fd4d65d566da93dbdb8a7ca474aa0a3c9c optionalDependencies: '@vencord/venmic': - specifier: ^3.5.0 - version: 3.5.0 + specifier: ^4.0.1 + version: 4.0.1 devDependencies: '@fal-works/esbuild-plugin-global-externals': specifier: ^2.1.2 @@ -603,8 +603,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@4.0.1': + resolution: {integrity: sha512-3NcX1IOwA3n/wyDG0AfAVqKMzTP7yD8p1oXxcJXAVlx9NQLFKXXjUmRIC13fpDUxEfmU9hCCuqnn9BkVr7DIWA==} engines: {node: '>=14.15'} os: [linux] @@ -1441,6 +1441,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 +1562,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 +2157,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 +3034,7 @@ snapshots: standalone-electron-types: 1.0.0 type-fest: 3.13.1 - '@vencord/venmic@3.5.0': + '@vencord/venmic@4.0.1': dependencies: cmake-js: 7.3.0 node-addon-api: 8.0.0 @@ -3077,7 +3080,7 @@ snapshots: 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: '@develar/schema-utils': 2.6.5 '@electron/notarize': 2.2.1 @@ -3091,7 +3094,7 @@ snapshots: builder-util-runtime: 9.2.4 chromium-pickle-js: 0.2.0 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 electron-builder-squirrel-windows: 24.13.3(dmg-builder@24.13.3) electron-publish: 24.13.1 @@ -3566,9 +3569,9 @@ snapshots: '@types/react': 17.0.2 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: - 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-runtime: 9.2.4 fs-extra: 10.1.0 @@ -3614,7 +3617,7 @@ snapshots: electron-builder-squirrel-windows@24.13.3(dmg-builder@24.13.3): 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 builder-util: 24.13.1 fs-extra: 10.1.0 @@ -3624,11 +3627,11 @@ snapshots: electron-builder@24.13.3(electron-builder-squirrel-windows@24.13.3(dmg-builder@24.13.3)): 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-runtime: 9.2.4 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 is-ci: 3.0.1 lazy-val: 1.0.5 diff --git a/src/main/venmic.ts b/src/main/venmic.ts index 94be05c..69efbe1 100644 --- a/src/main/venmic.ts +++ b/src/main/venmic.ts @@ -67,32 +67,26 @@ function getRendererAudioServicePid() { ); } -ipcMain.handle(IpcEvents.VIRT_MIC_LIST, (_, props: string[]) => { +ipcMain.handle(IpcEvents.VIRT_MIC_LIST, (_, props?: string[]) => { const audioPid = getRendererAudioServicePid(); - const list = obtainVenmic() + const targets = obtainVenmic() ?.list(props) .filter(s => s["application.process.id"] !== audioPid); - const sameProps = (x: Node, y: Node) => props.every(prop => x[prop] === y[prop]); - const unique = list?.filter((x, i, array) => array.findIndex(y => sameProps(x, y)) === i); - - return list ? { ok: true, targets: unique, 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, (_, targets: Node[], workaround?: boolean) => { const pid = getRendererAudioServicePid(); const data: LinkData = { - include: targets.map(target => ({ key: "application.name", value: target })), - exclude: [{ key: "application.process.id", value: pid }] + include: targets, + exclude: [{ "application.process.id": pid }] }; 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); @@ -102,15 +96,12 @@ ipcMain.handle(IpcEvents.VIRT_MIC_START_SYSTEM, (_, workaround?: boolean, onlyDe const pid = getRendererAudioServicePid(); const data: LinkData = { - exclude: [{ key: "application.process.id", value: pid }], + exclude: [{ "application.process.id": pid }], only_default_speakers: onlyDefaultSpeakers }; 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 643cb36..9e047b7 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,10 +64,9 @@ export const VesktopNative = { virtmic: { list: (props?: string[]) => invoke< - | { ok: false; isGlibCxxOutdated: boolean } - | { ok: true; targets: Record[]; hasPipewirePulse: boolean } + { ok: false; isGlibCxxOutdated: boolean } | { ok: true; targets: Node[]; hasPipewirePulse: boolean } >(IpcEvents.VIRT_MIC_LIST, props), - start: (targets: string[], workaround?: boolean) => invoke(IpcEvents.VIRT_MIC_START, targets, workaround), + start: (targets: Node[], workaround?: boolean) => invoke(IpcEvents.VIRT_MIC_START, targets, workaround), startSystem: (workaround?: boolean, onlyDefaultSpeakers?: boolean) => invoke(IpcEvents.VIRT_MIC_START_SYSTEM, workaround, onlyDefaultSpeakers), stop: () => invoke(IpcEvents.VIRT_MIC_STOP) diff --git a/src/renderer/components/ScreenSharePicker.tsx b/src/renderer/components/ScreenSharePicker.tsx index 82a532b..ffa35a6 100644 --- a/src/renderer/components/ScreenSharePicker.tsx +++ b/src/renderer/components/ScreenSharePicker.tsx @@ -19,6 +19,7 @@ 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 { isLinux, isWindows } from "renderer/utils"; @@ -31,15 +32,25 @@ 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; - audioSources?: string[]; + audioSources?: AudioSources; contentHint?: string; workaround?: boolean; onlyDefaultSpeakers?: boolean; - selectByProcess?: boolean; + granularSelect?: boolean; } export interface StreamPick extends StreamSettings { @@ -119,8 +130,8 @@ export function openScreenSharePicker(screens: Source[], skipPicker: boolean) { modalProps={props} submit={async v => { didSubmit = true; - if (v.audioSources && v.audioSources?.[0] !== "None") { - if (v.audioSources?.[0] === "Entire System") { + if (v.audioSources && v.audioSources !== "None") { + if (v.audioSources === "Entire System") { await VesktopNative.virtmic.startSystem(v.workaround); } else { await VesktopNative.virtmic.start(v.audioSources, v.workaround); @@ -295,11 +306,11 @@ function StreamSettings({ audioSources={settings.audioSources} workaround={settings.workaround} onlyDefaultSpeakers={settings.onlyDefaultSpeakers} - selectByProcess={settings.selectByProcess} - setAudioSources={source => setSettings(s => ({ ...s, audioSources: source }))} + granularSelect={settings.granularSelect} + setAudioSources={sources => setSettings(s => ({ ...s, audioSources: sources }))} setWorkaround={value => setSettings(s => ({ ...s, workaround: value }))} setOnlyDefaultSpeakers={value => setSettings(s => ({ ...s, onlyDefaultSpeakers: value }))} - setSelectByProcess={value => setSettings(s => ({ ...s, selectByProcess: value }))} + setGranularSelect={value => setSettings(s => ({ ...s, granularSelect: value }))} /> )} @@ -307,62 +318,126 @@ function StreamSettings({ ); } +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) { + return rtn; + } + + rtn.push({ + name: `${first.name} [${mediaName}]`, + value: { ...firstValues, "media.name": mediaName } + }); + + return rtn; +} + function AudioSourcePickerLinux({ audioSources, workaround, onlyDefaultSpeakers, - selectByProcess, + granularSelect, setAudioSources, setWorkaround, - setOnlyDefaultSpeakers + setOnlyDefaultSpeakers, + setGranularSelect }: { - audioSources?: string[]; + audioSources?: AudioSources; workaround?: boolean; onlyDefaultSpeakers?: boolean; - selectByProcess?: boolean; - setAudioSources(s: string[]): void; + granularSelect?: boolean; + setAudioSources(s: AudioSources): void; setWorkaround(b: boolean): void; setOnlyDefaultSpeakers(b: boolean): void; - setSelectByProcess(b: boolean): void; + setGranularSelect(b: boolean): void; }) { - const properties = selectByProcess ? ["application.process.id", "media.name"] : undefined; + const properties = granularSelect ? ["application.process.id"] : undefined; const [sources, _, loading] = useAwaiter(() => VesktopNative.virtmic.list(properties), { fallbackValue: { ok: true, targets: [], hasPipewirePulse: true } }); - const specialSources = ["None", "Entire System"]; + const specialSources: SpecialSource[] = ["None", "Entire System"]; const allSources = sources.ok ? [...specialSources, ...sources.targets] : null; const hasPipewirePulse = sources.ok ? sources.hasPipewirePulse : true; const [ignorePulseWarning, setIgnorePulseWarning] = useState(false); - const getName = (x: any) => { - if (specialSources.includes(x)) { - return x; + const isSelected = (value: AudioSource) => { + if (!audioSources) { + return false; } - if (!selectByProcess) { - return x["application.name"]; + if (isSpecialSource(audioSources) || isSpecialSource(value)) { + return audioSources === value; } - let name = `${x["application.name"] ?? "Unknown"}`; - - if (x["media.name"]) { - name += ` - ${x["media.name"]} `; - } - - return `${name} - ${x["application.process.id"]}`; + return audioSources.some(source => hasMatchingProps(source, value)); }; - const getValue = (x: any) => { - if (specialSources.includes(x)) { - return x; + const update = (value: SpecialSource | Node) => { + if (isSpecialSource(value)) { + setAudioSources(value); + return; } - return x["object.id"]; + if (isSpecialSource(audioSources)) { + setAudioSources([value]); + return; + } + + if (isSelected(value)) { + setAudioSources(audioSources?.filter(x => !hasMatchingProps(x, value)) ?? "None"); + return; + } + + setAudioSources([...(audioSources || []), value]); }; + const uniqueName = (value: AudioItem, index: number, list: AudioItem[]) => + list.findIndex(x => x.name === value.name) === index; + return ( <> Audio Settings @@ -390,31 +465,33 @@ function AudioSourcePickerLinux({ {hasPipewirePulse || ignorePulseWarning ? ( allSources && (