This commit is contained in:
Ryan Cao 2024-04-09 04:24:02 +02:00 committed by GitHub
commit 51a7961d3a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 191 additions and 58 deletions

View file

@ -20,6 +20,7 @@ import {
useState useState
} from "@vencord/types/webpack/common"; } from "@vencord/types/webpack/common";
import type { Dispatch, SetStateAction } from "react"; import type { Dispatch, SetStateAction } from "react";
import { patchDisplayMedia } from "renderer/patches/screenSharePatch";
import { addPatch } from "renderer/patches/shared"; import { addPatch } from "renderer/patches/shared";
import { isLinux, isWindows } from "renderer/utils"; import { isLinux, isWindows } from "renderer/utils";
@ -37,10 +38,12 @@ interface StreamSettings {
audio: boolean; audio: boolean;
audioSource?: string; audioSource?: string;
workaround?: boolean; workaround?: boolean;
audioDevice?: string;
} }
export interface StreamPick extends StreamSettings { export interface StreamPick extends StreamSettings {
id: string; id: string;
cameraId?: string;
} }
interface Source { interface Source {
@ -49,6 +52,11 @@ interface Source {
url: string; url: string;
} }
interface Camera {
id: string;
name: string;
}
let currentSettings: StreamSettings | null = null; let currentSettings: StreamSettings | null = null;
addPatch({ addPatch({
@ -98,6 +106,7 @@ if (isLinux) {
export function openScreenSharePicker(screens: Source[], skipPicker: boolean) { export function openScreenSharePicker(screens: Source[], skipPicker: boolean) {
let didSubmit = false; let didSubmit = false;
return new Promise<StreamPick>((resolve, reject) => { return new Promise<StreamPick>((resolve, reject) => {
const key = openModal( const key = openModal(
props => ( props => (
@ -106,14 +115,24 @@ export function openScreenSharePicker(screens: Source[], skipPicker: boolean) {
modalProps={props} modalProps={props}
submit={async v => { submit={async v => {
didSubmit = true; didSubmit = true;
if (v.audioSource && v.audioSource !== "None") { if (v.audioSource && v.audioSource !== "None") {
if (!v.audioDevice && v.audioSource && v.audioSource !== "None") {
if (v.audioSource === "Entire System") { if (v.audioSource === "Entire System") {
await VesktopNative.virtmic.startSystem(v.workaround); await VesktopNative.virtmic.startSystem(v.workaround);
} else { } else {
await VesktopNative.virtmic.start([v.audioSource], v.workaround); await VesktopNative.virtmic.start([v.audioSource], v.workaround);
} }
} }
patchDisplayMedia({
audioId: v.audioDevice,
venmic: !!v.audioSource && v.audioSource !== "None",
videoId: v.cameraId
});
resolve(v); resolve(v);
}
}} }}
close={() => { close={() => {
props.onClose(); props.onClose();
@ -132,12 +151,26 @@ export function openScreenSharePicker(screens: Source[], skipPicker: boolean) {
}); });
} }
function ScreenPicker({ screens, chooseScreen }: { screens: Source[]; chooseScreen: (id: string) => void }) { function ScreenPicker({
screens,
chooseScreen,
isDisabled = false
}: {
screens: Source[];
chooseScreen: (id: string) => void;
isDisabled?: boolean;
}) {
return ( return (
<div className="vcd-screen-picker-grid"> <div className="vcd-screen-picker-grid">
{screens.map(({ id, name, url }) => ( {screens.map(({ id, name, url }) => (
<label key={id}> <label key={id}>
<input type="radio" name="screen" value={id} onChange={() => chooseScreen(id)} /> <input
type="radio"
name="screen"
value={id}
onChange={() => chooseScreen(id)}
disabled={isDisabled}
/>
<img src={url} alt="" /> <img src={url} alt="" />
<Text variant="text-sm/normal">{name}</Text> <Text variant="text-sm/normal">{name}</Text>
@ -147,6 +180,37 @@ function ScreenPicker({ screens, chooseScreen }: { screens: Source[]; chooseScre
); );
} }
function CameraPicker({
camera,
chooseCamera
}: {
camera: string | undefined;
chooseCamera: (id: string | undefined) => void;
}) {
const [cameras] = useAwaiter(
() =>
navigator.mediaDevices
.enumerateDevices()
.then(res =>
res
.filter(d => d.kind === "videoinput")
.map(d => ({ id: d.deviceId, name: d.label }) satisfies Camera)
),
{ fallbackValue: [] }
);
return (
<Select
clearable={true}
options={cameras.map(s => ({ label: s.name, value: s.id }))}
isSelected={s => s === camera}
select={s => chooseCamera(s)}
clear={() => chooseCamera(undefined)}
serialize={String}
/>
);
}
function StreamSettings({ function StreamSettings({
source, source,
settings, settings,
@ -169,6 +233,7 @@ function StreamSettings({
return ( return (
<div> <div>
<Forms.FormTitle>What you're streaming</Forms.FormTitle> <Forms.FormTitle>What you're streaming</Forms.FormTitle>
<Card className="vcd-screen-picker-card vcd-screen-picker-preview"> <Card className="vcd-screen-picker-card vcd-screen-picker-preview">
<img src={thumb} alt="" /> <img src={thumb} alt="" />
<Text variant="text-sm/normal">{source.name}</Text> <Text variant="text-sm/normal">{source.name}</Text>
@ -215,6 +280,11 @@ function StreamSettings({
</section> </section>
</div> </div>
<AudioSourceAnyDevice
audioDevice={settings.audioDevice}
setAudioDevice={source => setSettings(s => ({ ...s, audioDevice: source }))}
/>
{isWindows && ( {isWindows && (
<Switch <Switch
value={settings.audio} value={settings.audio}
@ -239,6 +309,37 @@ function StreamSettings({
); );
} }
function AudioSourceAnyDevice({
audioDevice,
setAudioDevice
}: {
audioDevice?: string;
setAudioDevice(s: string): void;
}) {
const [sources] = useAwaiter(
() =>
navigator.mediaDevices
.enumerateDevices()
.then(devices => devices.filter(device => device.kind === "audioinput")),
{ fallbackValue: [] }
);
return (
<section>
<Forms.FormTitle>Audio</Forms.FormTitle>
{sources.length > 0 && (
<Select
options={sources.map((s, idx) => ({ label: s.label, value: s.deviceId, default: idx === 0 }))}
isSelected={s => s === audioDevice}
select={setAudioDevice}
serialize={String}
/>
)}
</section>
);
}
function AudioSourcePickerLinux({ function AudioSourcePickerLinux({
audioSource, audioSource,
workaround, workaround,
@ -316,11 +417,8 @@ function ModalComponent({
skipPicker: boolean; skipPicker: boolean;
}) { }) {
const [selected, setSelected] = useState<string | undefined>(skipPicker ? screens[0].id : void 0); const [selected, setSelected] = useState<string | undefined>(skipPicker ? screens[0].id : void 0);
const [settings, setSettings] = useState<StreamSettings>({ const [camera, setCamera] = useState<string | undefined>(undefined);
resolution: "1080", const [settings, setSettings] = useState<StreamSettings>({ resolution: "1080", fps: "60", audio: true });
fps: "60",
audio: true
});
return ( return (
<Modals.ModalRoot {...modalProps}> <Modals.ModalRoot {...modalProps}>
@ -331,7 +429,10 @@ function ModalComponent({
<Modals.ModalContent className="vcd-screen-picker-modal"> <Modals.ModalContent className="vcd-screen-picker-modal">
{!selected ? ( {!selected ? (
<ScreenPicker screens={screens} chooseScreen={setSelected} /> <>
<ScreenPicker screens={screens} chooseScreen={setSelected} isDisabled={!!camera} />
<CameraPicker camera={camera} chooseCamera={setCamera} />
</>
) : ( ) : (
<StreamSettings <StreamSettings
source={screens.find(s => s.id === selected)!} source={screens.find(s => s.id === selected)!}
@ -344,7 +445,7 @@ function ModalComponent({
<Modals.ModalFooter className="vcd-screen-picker-footer"> <Modals.ModalFooter className="vcd-screen-picker-footer">
<Button <Button
disabled={!selected} disabled={!selected && !camera}
onClick={() => { onClick={() => {
currentSettings = settings; currentSettings = settings;
@ -368,6 +469,7 @@ function ModalComponent({
submit({ submit({
id: selected!, id: selected!,
cameraId: camera,
...settings ...settings
}); });

View file

@ -37,7 +37,6 @@
outline: 2px solid var(--brand-experiment); outline: 2px solid var(--brand-experiment);
} }
.vcd-screen-picker-grid div { .vcd-screen-picker-grid div {
white-space: nowrap; white-space: nowrap;
text-overflow: ellipsis; text-overflow: ellipsis;
@ -100,6 +99,12 @@
flex: 1 1 auto; flex: 1 1 auto;
} }
.vcd-screen-picker-audio {
display: flex;
flex-direction: column;
gap: 1em;
}
.vcd-screen-picker-radios { .vcd-screen-picker-radios {
display: flex; display: flex;
width: 100%; width: 100%;

View file

@ -7,6 +7,6 @@
// TODO: Possibly auto generate glob if we have more patches in the future // TODO: Possibly auto generate glob if we have more patches in the future
import "./enableNotificationsByDefault"; import "./enableNotificationsByDefault";
import "./platformClass"; import "./platformClass";
import "./screenShareAudio";
import "./spellCheck"; import "./spellCheck";
import "./windowsTitleBar"; import "./windowsTitleBar";
import "./screenSharePatch";

View file

@ -1,42 +0,0 @@
/*
* 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 { isLinux } from "renderer/utils";
if (isLinux) {
const original = navigator.mediaDevices.getDisplayMedia;
async function getVirtmic() {
try {
const devices = await navigator.mediaDevices.enumerateDevices();
const audioDevice = devices.find(({ label }) => label === "vencord-screen-share");
return audioDevice?.deviceId;
} catch (error) {
return null;
}
}
navigator.mediaDevices.getDisplayMedia = async function (opts) {
const stream = await original.call(this, opts);
const id = await getVirtmic();
if (id) {
const audio = await navigator.mediaDevices.getUserMedia({
audio: {
deviceId: {
exact: id
},
autoGainControl: false,
echoCancellation: false,
noiseSuppression: false
}
});
audio.getAudioTracks().forEach(t => stream.addTrack(t));
}
return stream;
};
}

View file

@ -0,0 +1,68 @@
/*
* 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 { isLinux } from "renderer/utils";
const original = navigator.mediaDevices.getDisplayMedia;
interface ScreenSharePatchOptions {
videoId?: string;
audioId?: string;
venmic?: boolean;
}
async function getVirtmic() {
if (!isLinux) throw new Error("getVirtmic can not be called on non-Linux platforms!");
try {
const devices = await navigator.mediaDevices.enumerateDevices();
const audioDevice = devices.find(({ label }) => label === "vencord-screen-share");
return audioDevice?.deviceId;
} catch (error) {
return null;
}
}
export const patchDisplayMedia = (options: ScreenSharePatchOptions) => {
navigator.mediaDevices.getDisplayMedia = async function (apiOptions) {
let stream: MediaStream;
if (options.videoId) {
stream = await navigator.mediaDevices.getUserMedia({ video: { deviceId: { exact: options.videoId } } });
} else {
stream = await original.call(this, apiOptions);
}
if (options.audioId) {
const audio = await navigator.mediaDevices.getUserMedia({
audio: {
deviceId: { exact: options.audioId },
autoGainControl: false,
echoCancellation: false,
noiseSuppression: false
}
});
const tracks = audio.getAudioTracks();
tracks.forEach(t => stream.addTrack(t));
} else if (options.venmic === true) {
const virtmicId = await getVirtmic();
if (virtmicId) {
const audio = await navigator.mediaDevices.getUserMedia({
audio: {
deviceId: { exact: virtmicId },
autoGainControl: false,
echoCancellation: false,
noiseSuppression: false
}
});
audio.getAudioTracks().forEach(t => stream.addTrack(t));
}
}
return stream;
};
};