Merge 8ae191f657
into 8c6941b8e9
This commit is contained in:
commit
51a7961d3a
5 changed files with 191 additions and 58 deletions
|
@ -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
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -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%;
|
||||||
|
|
|
@ -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";
|
||||||
|
|
|
@ -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;
|
|
||||||
};
|
|
||||||
}
|
|
68
src/renderer/patches/screenSharePatch.ts
Normal file
68
src/renderer/patches/screenSharePatch.ts
Normal 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;
|
||||||
|
};
|
||||||
|
};
|
Reference in a new issue