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 | ||||
| } from "@vencord/types/webpack/common"; | ||||
| import type { Dispatch, SetStateAction } from "react"; | ||||
| import { patchDisplayMedia } from "renderer/patches/screenSharePatch"; | ||||
| import { addPatch } from "renderer/patches/shared"; | ||||
| import { isLinux, isWindows } from "renderer/utils"; | ||||
| 
 | ||||
|  | @ -37,10 +38,12 @@ interface StreamSettings { | |||
|     audio: boolean; | ||||
|     audioSource?: string; | ||||
|     workaround?: boolean; | ||||
|     audioDevice?: string; | ||||
| } | ||||
| 
 | ||||
| export interface StreamPick extends StreamSettings { | ||||
|     id: string; | ||||
|     cameraId?: string; | ||||
| } | ||||
| 
 | ||||
| interface Source { | ||||
|  | @ -49,6 +52,11 @@ interface Source { | |||
|     url: string; | ||||
| } | ||||
| 
 | ||||
| interface Camera { | ||||
|     id: string; | ||||
|     name: string; | ||||
| } | ||||
| 
 | ||||
| let currentSettings: StreamSettings | null = null; | ||||
| 
 | ||||
| addPatch({ | ||||
|  | @ -98,6 +106,7 @@ if (isLinux) { | |||
| 
 | ||||
| export function openScreenSharePicker(screens: Source[], skipPicker: boolean) { | ||||
|     let didSubmit = false; | ||||
| 
 | ||||
|     return new Promise<StreamPick>((resolve, reject) => { | ||||
|         const key = openModal( | ||||
|             props => ( | ||||
|  | @ -106,14 +115,24 @@ export function openScreenSharePicker(screens: Source[], skipPicker: boolean) { | |||
|                     modalProps={props} | ||||
|                     submit={async v => { | ||||
|                         didSubmit = true; | ||||
| 
 | ||||
|                         if (v.audioSource && v.audioSource !== "None") { | ||||
|                             if (v.audioSource === "Entire System") { | ||||
|                                 await VesktopNative.virtmic.startSystem(v.workaround); | ||||
|                             } else { | ||||
|                                 await VesktopNative.virtmic.start([v.audioSource], v.workaround); | ||||
|                             if (!v.audioDevice && v.audioSource && v.audioSource !== "None") { | ||||
|                                 if (v.audioSource === "Entire System") { | ||||
|                                     await VesktopNative.virtmic.startSystem(v.workaround); | ||||
|                                 } else { | ||||
|                                     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={() => { | ||||
|                         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 ( | ||||
|         <div className="vcd-screen-picker-grid"> | ||||
|             {screens.map(({ id, name, url }) => ( | ||||
|                 <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="" /> | ||||
|                     <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({ | ||||
|     source, | ||||
|     settings, | ||||
|  | @ -169,6 +233,7 @@ function StreamSettings({ | |||
|     return ( | ||||
|         <div> | ||||
|             <Forms.FormTitle>What you're streaming</Forms.FormTitle> | ||||
| 
 | ||||
|             <Card className="vcd-screen-picker-card vcd-screen-picker-preview"> | ||||
|                 <img src={thumb} alt="" /> | ||||
|                 <Text variant="text-sm/normal">{source.name}</Text> | ||||
|  | @ -215,6 +280,11 @@ function StreamSettings({ | |||
|                     </section> | ||||
|                 </div> | ||||
| 
 | ||||
|                 <AudioSourceAnyDevice | ||||
|                     audioDevice={settings.audioDevice} | ||||
|                     setAudioDevice={source => setSettings(s => ({ ...s, audioDevice: source }))} | ||||
|                 /> | ||||
| 
 | ||||
|                 {isWindows && ( | ||||
|                     <Switch | ||||
|                         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({ | ||||
|     audioSource, | ||||
|     workaround, | ||||
|  | @ -316,11 +417,8 @@ function ModalComponent({ | |||
|     skipPicker: boolean; | ||||
| }) { | ||||
|     const [selected, setSelected] = useState<string | undefined>(skipPicker ? screens[0].id : void 0); | ||||
|     const [settings, setSettings] = useState<StreamSettings>({ | ||||
|         resolution: "1080", | ||||
|         fps: "60", | ||||
|         audio: true | ||||
|     }); | ||||
|     const [camera, setCamera] = useState<string | undefined>(undefined); | ||||
|     const [settings, setSettings] = useState<StreamSettings>({ resolution: "1080", fps: "60", audio: true }); | ||||
| 
 | ||||
|     return ( | ||||
|         <Modals.ModalRoot {...modalProps}> | ||||
|  | @ -331,7 +429,10 @@ function ModalComponent({ | |||
| 
 | ||||
|             <Modals.ModalContent className="vcd-screen-picker-modal"> | ||||
|                 {!selected ? ( | ||||
|                     <ScreenPicker screens={screens} chooseScreen={setSelected} /> | ||||
|                     <> | ||||
|                         <ScreenPicker screens={screens} chooseScreen={setSelected} isDisabled={!!camera} /> | ||||
|                         <CameraPicker camera={camera} chooseCamera={setCamera} /> | ||||
|                     </> | ||||
|                 ) : ( | ||||
|                     <StreamSettings | ||||
|                         source={screens.find(s => s.id === selected)!} | ||||
|  | @ -344,7 +445,7 @@ function ModalComponent({ | |||
| 
 | ||||
|             <Modals.ModalFooter className="vcd-screen-picker-footer"> | ||||
|                 <Button | ||||
|                     disabled={!selected} | ||||
|                     disabled={!selected && !camera} | ||||
|                     onClick={() => { | ||||
|                         currentSettings = settings; | ||||
| 
 | ||||
|  | @ -368,6 +469,7 @@ function ModalComponent({ | |||
| 
 | ||||
|                         submit({ | ||||
|                             id: selected!, | ||||
|                             cameraId: camera, | ||||
|                             ...settings | ||||
|                         }); | ||||
| 
 | ||||
|  |  | |||
|  | @ -37,7 +37,6 @@ | |||
|     outline: 2px solid var(--brand-experiment); | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
| .vcd-screen-picker-grid div { | ||||
|     white-space: nowrap; | ||||
|     text-overflow: ellipsis; | ||||
|  | @ -100,6 +99,12 @@ | |||
|     flex: 1 1 auto; | ||||
| } | ||||
| 
 | ||||
| .vcd-screen-picker-audio { | ||||
|     display: flex; | ||||
|     flex-direction: column; | ||||
|     gap: 1em; | ||||
| } | ||||
| 
 | ||||
| .vcd-screen-picker-radios { | ||||
|     display: flex; | ||||
|     width: 100%; | ||||
|  |  | |||
|  | @ -7,6 +7,6 @@ | |||
| // TODO: Possibly auto generate glob if we have more patches in the future
 | ||||
| import "./enableNotificationsByDefault"; | ||||
| import "./platformClass"; | ||||
| import "./screenShareAudio"; | ||||
| import "./spellCheck"; | ||||
| 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; | ||||
|     }; | ||||
| }; | ||||
		Loading…
	
		Reference in a new issue
	
	 Ryan Cao
						Ryan Cao