 2bd8ca96df
			
		
	
	
		2bd8ca96df
		
			
		
	
	
	
	
		
			
			* feat(screenshare): add workaround * refactor: review suggestions * chore(deps): bump venmic * chore(deps): bump venmic
		
			
				
	
	
		
			392 lines
		
	
	
	
		
			13 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
			
		
		
	
	
			392 lines
		
	
	
	
		
			13 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
| /*
 | |
|  * 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, Margins, Modals, 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 type { Dispatch, SetStateAction } from "react";
 | |
| import { addPatch } from "renderer/patches/shared";
 | |
| 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];
 | |
| 
 | |
| interface StreamSettings {
 | |
|     resolution: StreamResolution;
 | |
|     fps: StreamFps;
 | |
|     audio: boolean;
 | |
|     audioSource?: string;
 | |
|     workaround?: boolean;
 | |
| }
 | |
| 
 | |
| export interface StreamPick extends StreamSettings {
 | |
|     id: string;
 | |
| }
 | |
| 
 | |
| interface Source {
 | |
|     id: string;
 | |
|     name: string;
 | |
|     url: string;
 | |
| }
 | |
| 
 | |
| let currentSettings: StreamSettings | null = null;
 | |
| 
 | |
| 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
 | |
|         });
 | |
|         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<StreamPick>((resolve, reject) => {
 | |
|         const key = openModal(
 | |
|             props => (
 | |
|                 <ModalComponent
 | |
|                     screens={screens}
 | |
|                     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);
 | |
|                             }
 | |
|                         }
 | |
|                         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 (
 | |
|         <div className="vcd-screen-picker-grid">
 | |
|             {screens.map(({ id, name, url }) => (
 | |
|                 <label key={id}>
 | |
|                     <input type="radio" name="screen" value={id} onChange={() => chooseScreen(id)} />
 | |
| 
 | |
|                     <img src={url} alt="" />
 | |
|                     <Text variant="text-sm/normal">{name}</Text>
 | |
|                 </label>
 | |
|             ))}
 | |
|         </div>
 | |
|     );
 | |
| }
 | |
| 
 | |
| function StreamSettings({
 | |
|     source,
 | |
|     settings,
 | |
|     setSettings,
 | |
|     skipPicker
 | |
| }: {
 | |
|     source: Source;
 | |
|     settings: StreamSettings;
 | |
|     setSettings: Dispatch<SetStateAction<StreamSettings>>;
 | |
|     skipPicker: boolean;
 | |
| }) {
 | |
|     const [thumb] = useAwaiter(
 | |
|         () => (skipPicker ? Promise.resolve(source.url) : VesktopNative.capturer.getLargeThumbnail(source.id)),
 | |
|         {
 | |
|             fallbackValue: source.url,
 | |
|             deps: [source.id]
 | |
|         }
 | |
|     );
 | |
| 
 | |
|     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>
 | |
|             </Card>
 | |
| 
 | |
|             <Forms.FormTitle>Stream Settings</Forms.FormTitle>
 | |
| 
 | |
|             <Card className="vcd-screen-picker-card">
 | |
|                 <div className="vcd-screen-picker-quality">
 | |
|                     <section>
 | |
|                         <Forms.FormTitle>Resolution</Forms.FormTitle>
 | |
|                         <div className="vcd-screen-picker-radios">
 | |
|                             {StreamResolutions.map(res => (
 | |
|                                 <label className="vcd-screen-picker-radio" data-checked={settings.resolution === res}>
 | |
|                                     <Text variant="text-sm/bold">{res}</Text>
 | |
|                                     <input
 | |
|                                         type="radio"
 | |
|                                         name="resolution"
 | |
|                                         value={res}
 | |
|                                         checked={settings.resolution === res}
 | |
|                                         onChange={() => setSettings(s => ({ ...s, resolution: res }))}
 | |
|                                     />
 | |
|                                 </label>
 | |
|                             ))}
 | |
|                         </div>
 | |
|                     </section>
 | |
| 
 | |
|                     <section>
 | |
|                         <Forms.FormTitle>Frame Rate</Forms.FormTitle>
 | |
|                         <div className="vcd-screen-picker-radios">
 | |
|                             {StreamFps.map(fps => (
 | |
|                                 <label className="vcd-screen-picker-radio" data-checked={settings.fps === fps}>
 | |
|                                     <Text variant="text-sm/bold">{fps}</Text>
 | |
|                                     <input
 | |
|                                         type="radio"
 | |
|                                         name="fps"
 | |
|                                         value={fps}
 | |
|                                         checked={settings.fps === fps}
 | |
|                                         onChange={() => setSettings(s => ({ ...s, fps }))}
 | |
|                                     />
 | |
|                                 </label>
 | |
|                             ))}
 | |
|                         </div>
 | |
|                     </section>
 | |
|                 </div>
 | |
| 
 | |
|                 {isWindows && (
 | |
|                     <Switch
 | |
|                         value={settings.audio}
 | |
|                         onChange={checked => setSettings(s => ({ ...s, audio: checked }))}
 | |
|                         hideBorder
 | |
|                         className="vcd-screen-picker-audio"
 | |
|                     >
 | |
|                         Stream With Audio
 | |
|                     </Switch>
 | |
|                 )}
 | |
| 
 | |
|                 {isLinux && (
 | |
|                     <AudioSourcePickerLinux
 | |
|                         audioSource={settings.audioSource}
 | |
|                         workaround={settings.workaround}
 | |
|                         setAudioSource={source => setSettings(s => ({ ...s, audioSource: source }))}
 | |
|                         setWorkaround={workaround => setSettings(s => ({ ...s, workaround: workaround }))}
 | |
|                     />
 | |
|                 )}
 | |
|             </Card>
 | |
|         </div>
 | |
|     );
 | |
| }
 | |
| 
 | |
| function AudioSourcePickerLinux({
 | |
|     audioSource,
 | |
|     workaround,
 | |
|     setAudioSource,
 | |
|     setWorkaround
 | |
| }: {
 | |
|     audioSource?: string;
 | |
|     workaround?: boolean;
 | |
|     setAudioSource(s: string): void;
 | |
|     setWorkaround(b: boolean): void;
 | |
| }) {
 | |
|     const [sources, _, loading] = useAwaiter(() => VesktopNative.virtmic.list(), {
 | |
|         fallbackValue: { ok: true, targets: [] }
 | |
|     });
 | |
|     const allSources = sources.ok ? ["None", "Entire System", ...sources.targets] : null;
 | |
| 
 | |
|     return (
 | |
|         <section>
 | |
|             <Forms.FormTitle>Audio</Forms.FormTitle>
 | |
|             {loading && <Forms.FormTitle>Loading Audio sources...</Forms.FormTitle>}
 | |
|             {!sources.ok &&
 | |
|                 (sources.isGlibcxxToOld ? (
 | |
|                     <Forms.FormText>
 | |
|                         Failed to retrieve Audio Sources because your C++ library is too old to run venmic. If you would
 | |
|                         like to stream with Audio, see{" "}
 | |
|                         <a href="https://gist.github.com/Vendicated/b655044ffbb16b2716095a448c6d827a" target="_blank">
 | |
|                             this guide
 | |
|                         </a>
 | |
|                     </Forms.FormText>
 | |
|                 ) : (
 | |
|                     <Forms.FormText>
 | |
|                         Failed to retrieve Audio Sources. If you would like to stream with Audio, make sure you're using
 | |
|                         Pipewire, not Pulseaudio
 | |
|                     </Forms.FormText>
 | |
|                 ))}
 | |
| 
 | |
|             {allSources && (
 | |
|                 <Select
 | |
|                     options={allSources.map(s => ({ label: s, value: s, default: s === "None" }))}
 | |
|                     isSelected={s => s === audioSource}
 | |
|                     select={setAudioSource}
 | |
|                     serialize={String}
 | |
|                 />
 | |
|             )}
 | |
| 
 | |
|             <Forms.FormDivider className={Margins.top16 + " " + Margins.bottom16} />
 | |
| 
 | |
|             <Switch
 | |
|                 onChange={setWorkaround}
 | |
|                 value={workaround ?? 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
 | |
|             </Switch>
 | |
|         </section>
 | |
|     );
 | |
| }
 | |
| 
 | |
| function ModalComponent({
 | |
|     screens,
 | |
|     modalProps,
 | |
|     submit,
 | |
|     close,
 | |
|     skipPicker
 | |
| }: {
 | |
|     screens: Source[];
 | |
|     modalProps: any;
 | |
|     submit: (data: StreamPick) => void;
 | |
|     close: () => void;
 | |
|     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
 | |
|     });
 | |
| 
 | |
|     return (
 | |
|         <Modals.ModalRoot {...modalProps}>
 | |
|             <Modals.ModalHeader className="vcd-screen-picker-header">
 | |
|                 <Forms.FormTitle tag="h2">ScreenShare</Forms.FormTitle>
 | |
|                 <Modals.ModalCloseButton onClick={close} />
 | |
|             </Modals.ModalHeader>
 | |
| 
 | |
|             <Modals.ModalContent className="vcd-screen-picker-modal">
 | |
|                 {!selected ? (
 | |
|                     <ScreenPicker screens={screens} chooseScreen={setSelected} />
 | |
|                 ) : (
 | |
|                     <StreamSettings
 | |
|                         source={screens.find(s => s.id === selected)!}
 | |
|                         settings={settings}
 | |
|                         setSettings={setSettings}
 | |
|                         skipPicker={skipPicker}
 | |
|                     />
 | |
|                 )}
 | |
|             </Modals.ModalContent>
 | |
| 
 | |
|             <Modals.ModalFooter className="vcd-screen-picker-footer">
 | |
|                 <Button
 | |
|                     disabled={!selected}
 | |
|                     onClick={() => {
 | |
|                         currentSettings = settings;
 | |
| 
 | |
|                         // If there are 2 connections, the second one is the existing stream.
 | |
|                         // In that case, we patch its quality
 | |
|                         const conn = [...MediaEngineStore.getMediaEngine().connections][1];
 | |
|                         if (conn && conn.videoStreamParameters.length > 0) {
 | |
|                             const height = Number(settings.resolution);
 | |
|                             const width = Math.round(height * (16 / 9));
 | |
|                             Object.assign(conn.videoStreamParameters[0], {
 | |
|                                 maxFrameRate: Number(settings.fps),
 | |
|                                 maxPixelCount: width * height,
 | |
|                                 maxBitrate: 8000000,
 | |
|                                 maxResolution: {
 | |
|                                     type: "fixed",
 | |
|                                     width,
 | |
|                                     height
 | |
|                                 }
 | |
|                             });
 | |
|                         }
 | |
| 
 | |
|                         submit({
 | |
|                             id: selected!,
 | |
|                             ...settings
 | |
|                         });
 | |
| 
 | |
|                         close();
 | |
|                     }}
 | |
|                 >
 | |
|                     Go Live
 | |
|                 </Button>
 | |
| 
 | |
|                 {selected && !skipPicker ? (
 | |
|                     <Button color={Button.Colors.TRANSPARENT} onClick={() => setSelected(void 0)}>
 | |
|                         Back
 | |
|                     </Button>
 | |
|                 ) : (
 | |
|                     <Button color={Button.Colors.TRANSPARENT} onClick={close}>
 | |
|                         Cancel
 | |
|                     </Button>
 | |
|                 )}
 | |
|             </Modals.ModalFooter>
 | |
|         </Modals.ModalRoot>
 | |
|     );
 | |
| }
 |