diff --git a/.env.example b/.env.example index ebec903..defb473 100644 --- a/.env.example +++ b/.env.example @@ -4,3 +4,5 @@ # all permissions at the defaults (public repos read only, 0 permissions): # https://github.com/settings/personal-access-tokens/new GITHUB_TOKEN= + +ELECTRON_LAUNCH_FLAGS="--ozone-platform-hint=auto --enable-webrtc-pipewire-capturer --enable-features=WaylandWindowDecorations" \ No newline at end of file diff --git a/package.json b/package.json index a09db0b..0d755f6 100644 --- a/package.json +++ b/package.json @@ -34,11 +34,19 @@ "@typescript-eslint/parser": "^6.7.3", "@vencord/types": "^0.1.2", "dotenv": "^16.3.1", +<<<<<<< HEAD "electron": "^26.2.2", "electron-builder": "^24.6.4", "esbuild": "^0.18.20", "eslint": "^8.50.0", "eslint-config-prettier": "^8.10.0", +======= + "electron": "26.2.3", + "electron-builder": "^24.6.3", + "esbuild": "^0.18.17", + "eslint": "^8.46.0", + "eslint-config-prettier": "^8.9.0", +>>>>>>> 2e5c450b14553561ad6ca505152d2a93766ca138 "eslint-import-resolver-alias": "^1.1.2", "eslint-plugin-license-header": "^0.6.0", "eslint-plugin-path-alias": "^1.0.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6cd004d..d33e4f4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -35,8 +35,13 @@ devDependencies: specifier: ^16.3.1 version: 16.3.1 electron: +<<<<<<< HEAD specifier: ^26.2.2 version: 26.2.2 +======= + specifier: 26.2.3 + version: 26.2.3 +>>>>>>> 2e5c450b14553561ad6ca505152d2a93766ca138 electron-builder: specifier: ^24.6.4 version: 24.6.4 @@ -1610,8 +1615,13 @@ packages: - supports-color dev: true +<<<<<<< HEAD /electron@26.2.2: resolution: {integrity: sha512-Ihb3Zt4XYnHF52DYSq17ySkgFqJV4OT0VnfhUYZASAql7Vembz3VsAq7mB3OALBHXltAW34P8BxTIwTqZaMS3g==} +======= + /electron@26.2.3: + resolution: {integrity: sha512-osdKf9mbhrqE81ITdvQ7TjVOayXfcAlWm8A6EtBt/eFSh7a/FijebGVkgs0S7qWQdhO0KaNZDb1Gx00sWuDQdw==} +>>>>>>> 2e5c450b14553561ad6ca505152d2a93766ca138 engines: {node: '>= 12.20.55'} hasBin: true requiresBuild: true diff --git a/scripts/start.ts b/scripts/start.ts index afe3f75..72ce3ab 100644 --- a/scripts/start.ts +++ b/scripts/start.ts @@ -8,4 +8,4 @@ import "./utils/dotenv"; import { spawnNodeModuleBin } from "./utils/spawn.mjs"; -spawnNodeModuleBin("electron", ["."]); +spawnNodeModuleBin("electron", [".", ...(process.env.ELECTRON_LAUNCH_FLAGS?.split(" ") ?? [])]); diff --git a/src/main/ipc.ts b/src/main/ipc.ts index 6a489fc..4fc0775 100644 --- a/src/main/ipc.ts +++ b/src/main/ipc.ts @@ -4,6 +4,8 @@ * Copyright (c) 2023 Vendicated and Vencord contributors */ +if (process.platform === "linux") import("./virtmic"); + import { execFile } from "child_process"; import { app, dialog, RelaunchOptions, session, shell } from "electron"; import { mkdirSync, readFileSync, watch } from "fs"; diff --git a/src/main/screenShare.ts b/src/main/screenShare.ts index aebedfb..bc2a68e 100644 --- a/src/main/screenShare.ts +++ b/src/main/screenShare.ts @@ -32,7 +32,10 @@ export function registerScreenShareHandler() { } }).catch(() => null); if (sources === null) return callback({}); +<<<<<<< HEAD +======= +>>>>>>> 2e5c450b14553561ad6ca505152d2a93766ca138 const isWayland = process.platform === "linux" && (process.env.XDG_SESSION_TYPE === "wayland" || !!process.env.WAYLAND_DISPLAY); @@ -45,15 +48,27 @@ export function registerScreenShareHandler() { if (isWayland) { const video = data[0]; +<<<<<<< HEAD getAudioFromVirtmic(); callback(video ? { video } : {}); +======= + if (video) + await request.frame.executeJavaScript( + `Vesktop.Components.ScreenShare.openScreenSharePicker(${JSON.stringify([video])}, true)` + ); + + callback(video ? { video: sources[0] } : {}); +>>>>>>> 2e5c450b14553561ad6ca505152d2a93766ca138 return; } const choice = await request.frame - .executeJavaScript(`Vesktop.Components.ScreenShare.openScreenSharePicker(${JSON.stringify(data)})`) + .executeJavaScript(`Vesktop.Components.ScreenShare.openScreenSharePicker(${JSON.stringify(data)}, false)`) .then(e => e as StreamPick) - .catch(() => null); + .catch(e => { + console.error("Error during screenshare picker", e); + return null; + }); if (!choice) return callback({}); diff --git a/src/main/virtmic.ts b/src/main/virtmic.ts new file mode 100644 index 0000000..01afe9b --- /dev/null +++ b/src/main/virtmic.ts @@ -0,0 +1,54 @@ +/* + * SPDX-License-Identifier: GPL-3.0 + * Vesktop, a desktop app aiming to give you a snappier Discord Experience + * Copyright (c) 2023 Vendicated and Vencord contributors + */ + +import { ChildProcess, execFile } from "child_process"; +import { ipcMain } from "electron"; +import { join } from "path"; +import { IpcEvents } from "shared/IpcEvents"; +import { STATIC_DIR } from "shared/paths"; +import { promisify } from "util"; + +const BIN = join(STATIC_DIR, "virtmic/vencord-virtmic"); +const execFileP = promisify(execFile); + +ipcMain.handle(IpcEvents.VIRT_MIC_LIST, async () => { + return execFileP(BIN, ["--list-targets"]) + .then(res => + res.stdout + .trim() + .split("\n") + .map(s => s.trim()) + .filter(Boolean) + ) + .catch(e => { + console.error("virt-mic-list failed", e); + return null; + }); +}); + +let virtMicProc: ChildProcess | null = null; + +function kill() { + virtMicProc?.kill(); + virtMicProc = null; +} + +ipcMain.handle(IpcEvents.VIRT_MIC_START, (_, target: string) => { + kill(); + + return new Promise(resolve => { + virtMicProc = execFile(BIN, [target], { encoding: "utf-8" }); + virtMicProc.stdout?.on("data", (chunk: string) => { + if (chunk.includes("vencord-virtmic")) resolve(true); + }); + virtMicProc.on("error", () => resolve(false)); + virtMicProc.on("exit", () => resolve(false)); + + setTimeout(() => resolve(false), 1000); + }); +}); + +ipcMain.handle(IpcEvents.VIRT_MIC_KILL, () => kill()); diff --git a/src/preload/VesktopNative.ts b/src/preload/VesktopNative.ts index 1706bed..36960fb 100644 --- a/src/preload/VesktopNative.ts +++ b/src/preload/VesktopNative.ts @@ -58,5 +58,12 @@ export const VesktopNative = { }, capturer: { getLargeThumbnail: (id: string) => invoke(IpcEvents.CAPTURER_GET_LARGE_THUMBNAIL, id) + }, + + /** only available on Linux. */ + virtmic: { + list: () => invoke(IpcEvents.VIRT_MIC_LIST), + start: (target: string) => invoke(IpcEvents.VIRT_MIC_START, target), + kill: () => invoke(IpcEvents.VIRT_MIC_KILL) } }; diff --git a/src/renderer/components/ScreenSharePicker.tsx b/src/renderer/components/ScreenSharePicker.tsx index bd7083c..dcb895d 100644 --- a/src/renderer/components/ScreenSharePicker.tsx +++ b/src/renderer/components/ScreenSharePicker.tsx @@ -8,10 +8,10 @@ import "./screenSharePicker.css"; import { closeModal, Modals, openModal, useAwaiter } from "@vencord/types/utils"; import { findStoreLazy } from "@vencord/types/webpack"; -import { Button, Card, Forms, Switch, Text, useState } from "@vencord/types/webpack/common"; +import { Button, Card, Forms, Select, Switch, Text, useState } from "@vencord/types/webpack/common"; import type { Dispatch, SetStateAction } from "react"; import { addPatch } from "renderer/patches/shared"; -import { isWindows } from "renderer/utils"; +import { isLinux, isWindows } from "renderer/utils"; const StreamResolutions = ["480", "720", "1080", "1440"] as const; const StreamFps = ["15", "30", "60"] as const; @@ -25,6 +25,7 @@ interface StreamSettings { resolution: StreamResolution; fps: StreamFps; audio: boolean; + audioSource?: string; } export interface StreamPick extends StreamSettings { @@ -70,18 +71,24 @@ addPatch({ } }); -export function openScreenSharePicker(screens: Source[]) { +export function openScreenSharePicker(screens: Source[], skipPicker: boolean) { + let didSubmit = false; return new Promise((resolve, reject) => { const key = openModal( props => ( { + didSubmit = true; + if (v.audioSource && v.audioSource !== "None") await VesktopNative.virtmic.start(v.audioSource); + resolve(v); + }} close={() => { props.onClose(); - reject("Aborted"); + if (!didSubmit) reject("Aborted"); }} + skipPicker={skipPicker} /> ), { @@ -109,7 +116,7 @@ function ScreenPicker({ screens, chooseScreen }: { screens: Source[]; chooseScre ); } -function StreamSettings({ +export function StreamSettings({ source, settings, setSettings @@ -182,23 +189,65 @@ function StreamSettings({ Stream With Audio )} + + {isLinux && ( + setSettings(s => ({ ...s, audioSource: source }))} + /> + )} ); } +function AudioSourcePickerLinux({ + audioSource, + setAudioSource +}: { + audioSource?: string; + setAudioSource(s: string): void; +}) { + const [sources, _, loading] = useAwaiter(() => VesktopNative.virtmic.list(), { fallbackValue: [] }); + const sourcesWithNone = sources ? ["None", ...sources] : null; + + return ( +
+ Audio + {loading && Loading Audio sources...} + {sourcesWithNone === null && ( + + Failed to retrieve Audio Sources. If you would like to stream with Audio, make sure you're using + Pipewire, not Pulseaudio + + )} + + {sourcesWithNone && ( +