/* * 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 "./screenSharePicker.css"; import { closeModal, Logger, Modals, ModalSize, openModal, useAwaiter } from "@vencord/types/utils"; import { findStoreLazy, onceReady } from "@vencord/types/webpack"; import { Button, Card, FluxDispatcher, Forms, Select, Switch, Text, 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; const StreamFps = ["15", "30", "60"] as const; 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?: AudioSources; contentHint?: string; } export interface StreamPick extends StreamSettings { id: string; } interface Source { id: string; name: string; url: string; } export let currentSettings: StreamSettings | null = null; const logger = new Logger("VesktopScreenShare"); addPatch({ patches: [ { find: "this.localWant=", replacement: { match: /this.localWant=/, replace: "$self.patchStreamQuality(this);$&" } } ], patchStreamQuality(opts: any) { if (!currentSettings) return; const framerate = Number(currentSettings.fps); const height = Number(currentSettings.resolution); const width = Math.round(height * (16 / 9)); Object.assign(opts, { bitrateMin: 500000, bitrateMax: 8000000, bitrateTarget: 600000 }); if (opts?.encode) { Object.assign(opts.encode, { framerate, width, height, pixelCount: height * width }); } Object.assign(opts.capture, { framerate, width, height, pixelCount: height * width }); } }); if (isLinux) { onceReady.then(() => { FluxDispatcher.subscribe("STREAM_CLOSE", ({ streamKey }: { streamKey: string }) => { const owner = streamKey.split(":").at(-1); if (owner !== UserStore.getCurrentUser().id) { return; } VesktopNative.virtmic.stop(); }); }); } export function openScreenSharePicker(screens: Source[], skipPicker: boolean) { let didSubmit = false; return new Promise((resolve, reject) => { const key = openModal( props => ( { didSubmit = true; if (v.audioSources && v.audioSources !== "None") { if (v.audioSources === "Entire System") { await VesktopNative.virtmic.startSystem(); } else { await VesktopNative.virtmic.start(v.audioSources); } } resolve(v); }} close={() => { props.onClose(); if (!didSubmit) reject("Aborted"); }} skipPicker={skipPicker} /> ), { onCloseRequest() { closeModal(key); reject("Aborted"); } } ); }); } function ScreenPicker({ screens, chooseScreen }: { screens: Source[]; chooseScreen: (id: string) => void }) { return (
{screens.map(({ id, name, url }) => ( ))}
); } function AudioSettingsModal({ modalProps, close, setAudioSources }: { modalProps: any; close: () => void; setAudioSources: (s: AudioSources) => void; }) { const Settings = useSettings(); return ( Venmic Settings (Settings.audioWorkaround = v)} value={Settings.audioWorkaround ?? 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.audioOnlySpeakers = v)} value={Settings.audioOnlySpeakers ?? 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.audioOnlyDefaultSpeakers = v)} value={Settings.audioOnlyDefaultSpeakers ?? 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.audioIgnoreInputMedia = v)} value={Settings.audioIgnoreInputMedia ?? true} note={<>Exclude nodes that are intended to capture audio.} > Ignore Inputs (Settings.audioIgnoreVirtual = v)} value={Settings.audioIgnoreVirtual ?? true} note={ <> Exclude virtual nodes, such as nodes belonging to sinks. This might be useful when using "mix bussing". } > Ignore Virtual (Settings.audioIgnoreDevices = v)} value={Settings.audioIgnoreDevices ?? true} note={<>Exclude device nodes, such as nodes belonging to microphones or speakers.} > Ignore Devices { Settings.audioGranularSelect = value; setAudioSources("None"); }} value={Settings.audioGranularSelect ?? false} note={<>Allow to select applications more granularly.} > Granular Selection ); } function StreamSettings({ source, settings, setSettings, skipPicker }: { source: Source; settings: StreamSettings; setSettings: Dispatch>; skipPicker: boolean; }) { const Settings = useSettings(); const [thumb] = useAwaiter( () => (skipPicker ? Promise.resolve(source.url) : VesktopNative.capturer.getLargeThumbnail(source.id)), { fallbackValue: source.url, deps: [source.id] } ); const openSettings = () => { const key = openModal(props => ( props.onClose()} setAudioSources={sources => setSettings(s => ({ ...s, audioSources: sources }))} /> )); }; return (
What you're streaming {source.name} Stream Settings
Resolution
{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" > Stream With Audio )}
{isLinux && ( setSettings(s => ({ ...s, audioSources: 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 AudioSourcePickerLinux({ audioSources, granularSelect, setAudioSources, openSettings }: { audioSources?: AudioSources; granularSelect?: boolean; openSettings: () => void; setAudioSources: (s: AudioSources) => void; }) { const [sources, _, loading] = useAwaiter(() => VesktopNative.virtmic.list(), { fallbackValue: { ok: true, targets: [], hasPipewirePulse: true } }); 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 isSelected = (value: AudioSource) => { if (!audioSources) { return false; } if (isSpecialSource(audioSources) || isSpecialSource(value)) { return audioSources === value; } return audioSources.some(source => hasMatchingProps(source, value)); }; const update = (value: SpecialSource | Node) => { if (isSpecialSource(value)) { setAudioSources(value); return; } 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 (
{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 && ( <>