Skip to content

Commit 0e81ec3

Browse files
committed
add video device patching as well
1 parent d7441aa commit 0e81ec3

File tree

4 files changed

+149
-79
lines changed

4 files changed

+149
-79
lines changed

src/renderer/components/ScreenSharePicker.tsx

Lines changed: 80 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import {
2020
useState
2121
} from "@vencord/types/webpack/common";
2222
import type { Dispatch, SetStateAction } from "react";
23-
import { patchAudioWithDevice } from "renderer/patches/screenShareAudio";
23+
import { patchDisplayMedia } from "renderer/patches/screenSharePatch";
2424
import { addPatch } from "renderer/patches/shared";
2525
import { isLinux, isWindows } from "renderer/utils";
2626

@@ -43,6 +43,7 @@ interface StreamSettings {
4343

4444
export interface StreamPick extends StreamSettings {
4545
id: string;
46+
cameraId?: string;
4647
}
4748

4849
interface Source {
@@ -51,6 +52,11 @@ interface Source {
5152
url: string;
5253
}
5354

55+
interface Camera {
56+
id: string;
57+
name: string;
58+
}
59+
5460
let currentSettings: StreamSettings | null = null;
5561

5662
addPatch({
@@ -100,6 +106,7 @@ if (isLinux) {
100106

101107
export function openScreenSharePicker(screens: Source[], skipPicker: boolean) {
102108
let didSubmit = false;
109+
103110
return new Promise<StreamPick>((resolve, reject) => {
104111
const key = openModal(
105112
props => (
@@ -109,16 +116,24 @@ export function openScreenSharePicker(screens: Source[], skipPicker: boolean) {
109116
submit={async v => {
110117
didSubmit = true;
111118
if (v.audioSource && v.audioSource !== "None") {
112-
if (v.audioSource === "Entire System") {
113-
await VesktopNative.virtmic.startSystem(v.workaround);
114-
} else {
115-
await VesktopNative.virtmic.start([v.audioSource], v.workaround);
119+
patchDisplayMedia({
120+
audioId: v.audioDevice,
121+
venmic: !!v.audioSource && v.audioSource !== "None",
122+
videoId: v.cameraId
123+
});
124+
125+
if (!v.audioDevice && v.audioSource && v.audioSource !== "None") {
126+
if (v.audioSource === "Entire System") {
127+
await VesktopNative.virtmic.startSystem(v.workaround);
128+
} else {
129+
await VesktopNative.virtmic.start([v.audioSource], v.workaround);
130+
}
116131
}
117-
}
118132

119-
patchAudioWithDevice(v.audioDevice);
133+
patchAudioWithDevice(v.audioDevice);
120134

121-
resolve(v);
135+
resolve(v);
136+
}
122137
}}
123138
close={() => {
124139
props.onClose();
@@ -137,12 +152,26 @@ export function openScreenSharePicker(screens: Source[], skipPicker: boolean) {
137152
});
138153
}
139154

140-
function ScreenPicker({ screens, chooseScreen }: { screens: Source[]; chooseScreen: (id: string) => void }) {
155+
function ScreenPicker({
156+
screens,
157+
chooseScreen,
158+
isDisabled = false
159+
}: {
160+
screens: Source[];
161+
chooseScreen: (id: string) => void;
162+
isDisabled?: boolean;
163+
}) {
141164
return (
142165
<div className="vcd-screen-picker-grid">
143166
{screens.map(({ id, name, url }) => (
144167
<label key={id}>
145-
<input type="radio" name="screen" value={id} onChange={() => chooseScreen(id)} />
168+
<input
169+
type="radio"
170+
name="screen"
171+
value={id}
172+
onChange={() => chooseScreen(id)}
173+
disabled={isDisabled}
174+
/>
146175

147176
<img src={url} alt="" />
148177
<Text variant="text-sm/normal">{name}</Text>
@@ -152,6 +181,37 @@ function ScreenPicker({ screens, chooseScreen }: { screens: Source[]; chooseScre
152181
);
153182
}
154183

184+
function CameraPicker({
185+
camera,
186+
chooseCamera
187+
}: {
188+
camera: string | undefined;
189+
chooseCamera: (id: string | undefined) => void;
190+
}) {
191+
const [cameras] = useAwaiter(
192+
() =>
193+
navigator.mediaDevices
194+
.enumerateDevices()
195+
.then(res =>
196+
res
197+
.filter(d => d.kind === "videoinput")
198+
.map(d => ({ id: d.deviceId, name: d.label }) satisfies Camera)
199+
),
200+
{ fallbackValue: [] }
201+
);
202+
203+
return (
204+
<Select
205+
clearable={true}
206+
options={cameras.map(s => ({ label: s.name, value: s.id }))}
207+
isSelected={s => s === camera}
208+
select={s => chooseCamera(s)}
209+
clear={() => chooseCamera(undefined)}
210+
serialize={String}
211+
/>
212+
);
213+
}
214+
155215
function StreamSettings({
156216
source,
157217
settings,
@@ -174,6 +234,7 @@ function StreamSettings({
174234
return (
175235
<div>
176236
<Forms.FormTitle>What you're streaming</Forms.FormTitle>
237+
177238
<Card className="vcd-screen-picker-card vcd-screen-picker-preview">
178239
<img src={thumb} alt="" />
179240
<Text variant="text-sm/normal">{source.name}</Text>
@@ -256,7 +317,7 @@ function AudioSourceAnyDevice({
256317
audioDevice?: string;
257318
setAudioDevice(s: string): void;
258319
}) {
259-
const [sources, _, loading] = useAwaiter(
320+
const [sources] = useAwaiter(
260321
() =>
261322
navigator.mediaDevices
262323
.enumerateDevices()
@@ -358,11 +419,8 @@ function ModalComponent({
358419
skipPicker: boolean;
359420
}) {
360421
const [selected, setSelected] = useState<string | undefined>(skipPicker ? screens[0].id : void 0);
361-
const [settings, setSettings] = useState<StreamSettings>({
362-
resolution: "1080",
363-
fps: "60",
364-
audio: true
365-
});
422+
const [camera, setCamera] = useState<string | undefined>(undefined);
423+
const [settings, setSettings] = useState<StreamSettings>({ resolution: "1080", fps: "60", audio: true });
366424

367425
return (
368426
<Modals.ModalRoot {...modalProps}>
@@ -373,7 +431,10 @@ function ModalComponent({
373431

374432
<Modals.ModalContent className="vcd-screen-picker-modal">
375433
{!selected ? (
376-
<ScreenPicker screens={screens} chooseScreen={setSelected} />
434+
<>
435+
<ScreenPicker screens={screens} chooseScreen={setSelected} isDisabled={!!camera} />
436+
<CameraPicker camera={camera} chooseCamera={setCamera} />
437+
</>
377438
) : (
378439
<StreamSettings
379440
source={screens.find(s => s.id === selected)!}
@@ -386,7 +447,7 @@ function ModalComponent({
386447

387448
<Modals.ModalFooter className="vcd-screen-picker-footer">
388449
<Button
389-
disabled={!selected}
450+
disabled={!selected && !camera}
390451
onClick={() => {
391452
currentSettings = settings;
392453

@@ -410,6 +471,7 @@ function ModalComponent({
410471

411472
submit({
412473
id: selected!,
474+
cameraId: camera,
413475
...settings
414476
});
415477

src/renderer/patches/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,6 @@
77
// TODO: Possibly auto generate glob if we have more patches in the future
88
import "./enableNotificationsByDefault";
99
import "./platformClass";
10-
import "./screenShareAudio";
1110
import "./spellCheck";
1211
import "./windowsTitleBar";
12+
import "./screenSharePatch";

src/renderer/patches/screenShareAudio.ts

Lines changed: 0 additions & 60 deletions
This file was deleted.
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
/*
2+
* SPDX-License-Identifier: GPL-3.0
3+
* Vesktop, a desktop app aiming to give you a snappier Discord Experience
4+
* Copyright (c) 2023 Vendicated and Vencord contributors
5+
*/
6+
7+
import { isLinux } from "renderer/utils";
8+
9+
const original = navigator.mediaDevices.getDisplayMedia;
10+
11+
interface ScreenSharePatchOptions {
12+
videoId?: string;
13+
audioId?: string;
14+
venmic?: boolean;
15+
}
16+
17+
async function getVirtmic() {
18+
if (!isLinux) throw new Error("getVirtmic can not be called on non-Linux platforms!");
19+
20+
try {
21+
const devices = await navigator.mediaDevices.enumerateDevices();
22+
const audioDevice = devices.find(({ label }) => label === "vencord-screen-share");
23+
return audioDevice?.deviceId;
24+
} catch (error) {
25+
return null;
26+
}
27+
}
28+
29+
export const patchDisplayMedia = (options: ScreenSharePatchOptions) => {
30+
navigator.mediaDevices.getDisplayMedia = async function (apiOptions) {
31+
let stream: MediaStream;
32+
33+
if (options.videoId) {
34+
stream = await navigator.mediaDevices.getUserMedia({ video: { deviceId: { exact: options.videoId } } });
35+
} else {
36+
stream = await original.call(this, apiOptions);
37+
}
38+
39+
if (options.audioId) {
40+
const audio = await navigator.mediaDevices.getUserMedia({
41+
audio: {
42+
deviceId: { exact: options.audioId },
43+
autoGainControl: false,
44+
echoCancellation: false,
45+
noiseSuppression: false
46+
}
47+
});
48+
const tracks = audio.getAudioTracks();
49+
tracks.forEach(t => stream.addTrack(t));
50+
} else if (options.venmic === true) {
51+
const virtmicId = await getVirtmic();
52+
53+
if (virtmicId) {
54+
const audio = await navigator.mediaDevices.getUserMedia({
55+
audio: {
56+
deviceId: { exact: virtmicId },
57+
autoGainControl: false,
58+
echoCancellation: false,
59+
noiseSuppression: false
60+
}
61+
});
62+
audio.getAudioTracks().forEach(t => stream.addTrack(t));
63+
}
64+
}
65+
66+
return stream;
67+
};
68+
};

0 commit comments

Comments
 (0)