Screenshare: preselect the shared window's audio
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
4afc78ee8f
commit
7b239c3972
3 changed files with 155 additions and 11 deletions
|
|
@ -146,6 +146,77 @@
|
|||
const choice = await sendRendererCommand<StreamPick>(IpcCommands.SCREEN_SHARE_PICKER, {
|
||||
screens: data,
|
||||
skipPicker: false
|
||||
--- a/src/main/venmic.ts
|
||||
+++ b/src/main/venmic.ts
|
||||
@@ -6,6 +6,7 @@
|
||||
|
||||
import type { LinkData, Node, PatchBay as PatchBayType } from "@vencord/venmic";
|
||||
import { app, ipcMain } from "electron";
|
||||
+import { readFileSync } from "fs";
|
||||
import { join } from "path";
|
||||
import { IpcEvents } from "shared/IpcEvents";
|
||||
import { STATIC_DIR } from "shared/paths";
|
||||
@@ -66,6 +67,37 @@
|
||||
);
|
||||
}
|
||||
|
||||
+// FredOS: the XDPH picker (settings/hyprland.nix) writes the shared window's
|
||||
+// pid here; suggest audio from that process or anything it spawned (Proton's
|
||||
+// game exe is usually the window's own process, gamescope's game a child).
|
||||
+function suggestedSources(targets: Node[]): Node[] {
|
||||
+ let root: number;
|
||||
+ try {
|
||||
+ root = Number(readFileSync(join(process.env.XDG_RUNTIME_DIR ?? "/tmp", "xdph-selection-pid"), "utf8"));
|
||||
+ } catch {
|
||||
+ return [];
|
||||
+ }
|
||||
+ if (!root) return [];
|
||||
+
|
||||
+ const fromRoot = (pid: number) => {
|
||||
+ for (let depth = 0; pid > 1 && depth < 32; depth++) {
|
||||
+ if (pid === root) return true;
|
||||
+ try {
|
||||
+ const stat = readFileSync(`/proc/${pid}/stat`, "utf8");
|
||||
+ pid = Number(stat.slice(stat.lastIndexOf(")") + 2).split(" ")[1]); // ppid, after "(comm) state"
|
||||
+ } catch {
|
||||
+ return false;
|
||||
+ }
|
||||
+ }
|
||||
+ return false;
|
||||
+ };
|
||||
+
|
||||
+ const names = targets
|
||||
+ .filter(t => t["application.name"] && fromRoot(Number(t["application.process.id"])))
|
||||
+ .map(t => t["application.name"]);
|
||||
+ return [...new Set(names)].map(name => ({ "application.name": name }));
|
||||
+}
|
||||
+
|
||||
ipcMain.handle(IpcEvents.VIRT_MIC_LIST, () => {
|
||||
const audioPid = getRendererAudioServicePid();
|
||||
|
||||
@@ -75,7 +107,9 @@
|
||||
?.list(granularSelect ? ["node.name"] : undefined)
|
||||
.filter(s => s["application.process.id"] !== audioPid);
|
||||
|
||||
- return targets ? { ok: true, targets, hasPipewirePulse } : { ok: false, isGlibCxxOutdated };
|
||||
+ return targets
|
||||
+ ? { ok: true, targets, hasPipewirePulse, suggested: suggestedSources(targets) }
|
||||
+ : { ok: false, isGlibCxxOutdated };
|
||||
});
|
||||
|
||||
ipcMain.handle(IpcEvents.VIRT_MIC_START, (_, include: Node[]) => {
|
||||
--- a/src/preload/VesktopNative.ts
|
||||
+++ b/src/preload/VesktopNative.ts
|
||||
@@ -88,7 +88,8 @@
|
||||
virtmic: {
|
||||
list: () =>
|
||||
invoke<
|
||||
- { ok: false; isGlibCxxOutdated: boolean } | { ok: true; targets: Node[]; hasPipewirePulse: boolean }
|
||||
+ | { ok: false; isGlibCxxOutdated: boolean }
|
||||
+ | { ok: true; targets: Node[]; hasPipewirePulse: boolean; suggested: Node[] }
|
||||
>(IpcEvents.VIRT_MIC_LIST),
|
||||
start: (include: Node[]) => invoke<void>(IpcEvents.VIRT_MIC_START, include),
|
||||
startSystem: (exclude: Node[]) => invoke<void>(IpcEvents.VIRT_MIC_START_SYSTEM, exclude),
|
||||
--- a/src/renderer/components/ScreenSharePicker.tsx
|
||||
+++ b/src/renderer/components/ScreenSharePicker.tsx
|
||||
@@ -82,6 +82,10 @@
|
||||
|
|
@ -209,7 +280,75 @@
|
|||
<Paragraph>{source.name}</Paragraph>
|
||||
</Card>
|
||||
|
||||
@@ -760,6 +778,7 @@
|
||||
@@ -451,6 +469,7 @@
|
||||
granularSelect={Settings.audio?.granularSelect}
|
||||
setIncludeSources={sources => setSettings(s => ({ ...s, includeSources: sources }))}
|
||||
setExcludeSources={sources => setSettings(s => ({ ...s, excludeSources: sources }))}
|
||||
+ selectionReady={!thumbPending}
|
||||
/>
|
||||
)}
|
||||
</Card>
|
||||
@@ -578,7 +597,8 @@
|
||||
granularSelect,
|
||||
openSettings,
|
||||
setIncludeSources,
|
||||
- setExcludeSources
|
||||
+ setExcludeSources,
|
||||
+ selectionReady
|
||||
}: {
|
||||
includeSources?: AudioSources;
|
||||
excludeSources?: AudioSources;
|
||||
@@ -587,30 +607,38 @@
|
||||
openSettings: () => void;
|
||||
setIncludeSources: (s: AudioSources) => void;
|
||||
setExcludeSources: (s: AudioSources) => void;
|
||||
+ selectionReady: boolean;
|
||||
}) {
|
||||
const [audioSourcesSignal, refreshAudioSources] = useForceUpdater(true);
|
||||
const [sources, _, loading] = useAwaiter(() => VesktopNative.virtmic.list(), {
|
||||
- fallbackValue: { ok: true, targets: [], hasPipewirePulse: true },
|
||||
+ fallbackValue: { ok: true, targets: [], hasPipewirePulse: true, suggested: [] },
|
||||
deps: [audioSourcesSignal]
|
||||
});
|
||||
|
||||
const hasPipewirePulse = sources.ok ? sources.hasPipewirePulse : true;
|
||||
const [ignorePulseWarning, setIgnorePulseWarning] = useState(false);
|
||||
|
||||
- // Once the first source list arrives, preselect whichever of the last
|
||||
- // stream's audio sources are running again (e.g. the same game).
|
||||
+ // Once the shared window is known (its preview arrived), preselect audio:
|
||||
+ // whatever that window's process plays (FredOS picker hint), else
|
||||
+ // whichever of the last stream's sources are running again.
|
||||
const [restored, setRestored] = useState(false);
|
||||
useEffect(() => {
|
||||
- if (restored || loading || !sources.ok) return;
|
||||
+ if (restored || !selectionReady) return;
|
||||
setRestored(true);
|
||||
|
||||
- const last = State.store.screenshareAudio;
|
||||
- if (includeSources !== "None" || !last || last === "None") return;
|
||||
- if (last === "Entire System") return setIncludeSources(last);
|
||||
-
|
||||
- const running = last.filter(l => sources.targets.some(t => hasMatchingProps(l, t)));
|
||||
- if (running.length) setIncludeSources(running);
|
||||
- }, [loading]);
|
||||
+ VesktopNative.virtmic.list().then(fresh => {
|
||||
+ refreshAudioSources();
|
||||
+ if (!fresh.ok || includeSources !== "None") return;
|
||||
+ if (fresh.suggested.length) return setIncludeSources(fresh.suggested);
|
||||
+
|
||||
+ const last = State.store.screenshareAudio;
|
||||
+ if (!last || last === "None") return;
|
||||
+ if (last === "Entire System") return setIncludeSources(last);
|
||||
+
|
||||
+ const running = last.filter(l => fresh.targets.some(t => hasMatchingProps(l, t)));
|
||||
+ if (running.length) setIncludeSources(running);
|
||||
+ });
|
||||
+ }, [selectionReady]);
|
||||
|
||||
if (!sources.ok && sources.isGlibCxxOutdated) {
|
||||
return (
|
||||
@@ -760,6 +788,7 @@
|
||||
settings={settings}
|
||||
setSettings={setSettings}
|
||||
skipPicker={skipPicker}
|
||||
|
|
@ -217,7 +356,7 @@
|
|||
/>
|
||||
)}
|
||||
</Modals.ModalContent>
|
||||
@@ -829,6 +848,18 @@
|
||||
@@ -829,6 +858,18 @@
|
||||
Go Live
|
||||
</Button>
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue