Add ScreenSharing (#14)
This commit is contained in:
		
							parent
							
								
									23c0647e6c
								
							
						
					
					
						commit
						253277984b
					
				
					 10 changed files with 424 additions and 1 deletions
				
			
		
							
								
								
									
										6
									
								
								.vscode/settings.json
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										6
									
								
								.vscode/settings.json
									
									
									
									
										vendored
									
									
								
							|  | @ -9,6 +9,12 @@ | |||
|     "[javascript]": { | ||||
|         "editor.defaultFormatter": "esbenp.prettier-vscode" | ||||
|     }, | ||||
|     "[typescriptreact]": { | ||||
|         "editor.defaultFormatter": "esbenp.prettier-vscode" | ||||
|     }, | ||||
|     "[javascriptreact]": { | ||||
|         "editor.defaultFormatter": "esbenp.prettier-vscode" | ||||
|     }, | ||||
|     "[json]": { | ||||
|         "editor.defaultFormatter": "esbenp.prettier-vscode" | ||||
|     }, | ||||
|  |  | |||
|  | @ -5,7 +5,6 @@ Vencord Desktop is a cross platform desktop app aiming to give you a snappier Di | |||
| Vencord Desktop is currently in beta | ||||
| 
 | ||||
| **Not yet supported**: | ||||
| - Screensharing | ||||
| - Global Keybinds | ||||
| 
 | ||||
| Bug reports, feature requests & contributions are highly appreciated!! | ||||
|  |  | |||
|  | @ -13,6 +13,7 @@ import { ICON_PATH } from "../shared/paths"; | |||
| import { DATA_DIR } from "./constants"; | ||||
| import { createFirstLaunchTour } from "./firstLaunch"; | ||||
| import { createWindows, mainWin } from "./mainWindow"; | ||||
| import { registerScreenShareHandler } from "./screenShare"; | ||||
| import { Settings } from "./settings"; | ||||
| 
 | ||||
| if (IS_DEV) { | ||||
|  | @ -51,6 +52,7 @@ function init() { | |||
|         if (process.platform === "win32") app.setAppUserModelId("dev.vencord.desktop"); | ||||
|         else if (process.platform === "darwin") app.dock.setIcon(ICON_PATH); | ||||
| 
 | ||||
|         registerScreenShareHandler(); | ||||
|         bootstrap(); | ||||
| 
 | ||||
|         app.on("activate", () => { | ||||
|  |  | |||
							
								
								
									
										55
									
								
								src/main/screenShare.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										55
									
								
								src/main/screenShare.ts
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,55 @@ | |||
| /* | ||||
|  * SPDX-License-Identifier: GPL-3.0 | ||||
|  * Vencord Desktop, a desktop app aiming to give you a snappier Discord Experience | ||||
|  * Copyright (c) 2023 Vendicated and Vencord contributors | ||||
|  */ | ||||
| 
 | ||||
| import { desktopCapturer, ipcMain, session, Streams } from "electron"; | ||||
| import type { StreamPick } from "renderer/components/ScreenSharePicker"; | ||||
| import { IpcEvents } from "shared/IpcEvents"; | ||||
| 
 | ||||
| export function registerScreenShareHandler() { | ||||
|     ipcMain.handle(IpcEvents.CAPTURER_GET_LARGE_THUMBNAIL, async (_, id: string) => { | ||||
|         const sources = await desktopCapturer.getSources({ | ||||
|             types: ["window", "screen"], | ||||
|             thumbnailSize: { | ||||
|                 width: 1920, | ||||
|                 height: 1080 | ||||
|             } | ||||
|         }); | ||||
|         return sources.find(s => s.id === id)?.thumbnail.toDataURL(); | ||||
|     }); | ||||
| 
 | ||||
|     session.defaultSession.setDisplayMediaRequestHandler(async (request, callback) => { | ||||
|         const sources = await desktopCapturer.getSources({ | ||||
|             types: ["window", "screen"], | ||||
|             thumbnailSize: { | ||||
|                 width: 176, | ||||
|                 height: 99 | ||||
|             } | ||||
|         }); | ||||
| 
 | ||||
|         const data = sources.map(({ id, name, thumbnail }) => ({ | ||||
|             id, | ||||
|             name, | ||||
|             url: thumbnail.toDataURL() | ||||
|         })); | ||||
| 
 | ||||
|         const choice = await request.frame | ||||
|             .executeJavaScript(`VencordDesktop.Components.ScreenShare.openScreenSharePicker(${JSON.stringify(data)})`) | ||||
|             .then(e => e as StreamPick) | ||||
|             .catch(() => null); | ||||
| 
 | ||||
|         if (!choice) return callback({}); | ||||
| 
 | ||||
|         const source = sources.find(s => s.id === choice.id); | ||||
|         if (!source) return callback({}); | ||||
| 
 | ||||
|         const streams: Streams = { | ||||
|             video: source | ||||
|         }; | ||||
|         if (choice.audio && process.platform === "win32") streams.audio = "loopback"; | ||||
| 
 | ||||
|         callback(streams); | ||||
|     }); | ||||
| } | ||||
|  | @ -34,5 +34,8 @@ export const VencordDesktopNative = { | |||
|     }, | ||||
|     win: { | ||||
|         focus: () => invoke<void>(IpcEvents.FOCUS) | ||||
|     }, | ||||
|     capturer: { | ||||
|         getLargeThumbnail: (id: string) => invoke<string>(IpcEvents.CAPTURER_GET_LARGE_THUMBNAIL, id) | ||||
|     } | ||||
| }; | ||||
|  |  | |||
							
								
								
									
										225
									
								
								src/renderer/components/ScreenSharePicker.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										225
									
								
								src/renderer/components/ScreenSharePicker.tsx
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,225 @@ | |||
| /* | ||||
|  * SPDX-License-Identifier: GPL-3.0 | ||||
|  * Vencord Desktop, a desktop app aiming to give you a snappier Discord Experience | ||||
|  * Copyright (c) 2023 Vendicated and Vencord contributors | ||||
|  */ | ||||
| 
 | ||||
| import "./screenSharePicker.css"; | ||||
| 
 | ||||
| import { classes, closeModal, Margins, Modals, openModal, useAwaiter } from "@vencord/types/utils"; | ||||
| import { findByPropsLazy } from "@vencord/types/webpack"; | ||||
| import { Button, Card, Forms, Switch, Text, useState } from "@vencord/types/webpack/common"; | ||||
| import type { Dispatch, SetStateAction } from "react"; | ||||
| import { isWindows } from "renderer/utils"; | ||||
| 
 | ||||
| const StreamResolutions = ["720", "1080", "1440", "Source"] as const; | ||||
| const StreamFps = ["15", "30", "60"] as const; | ||||
| 
 | ||||
| const WarningIconClasses = findByPropsLazy("warning", "error", "container"); | ||||
| 
 | ||||
| export type StreamResolution = (typeof StreamResolutions)[number]; | ||||
| export type StreamFps = (typeof StreamFps)[number]; | ||||
| 
 | ||||
| interface StreamSettings { | ||||
|     resolution: StreamResolution; | ||||
|     fps: StreamFps; | ||||
|     audio: boolean; | ||||
| } | ||||
| 
 | ||||
| export interface StreamPick extends StreamSettings { | ||||
|     id: string; | ||||
| } | ||||
| 
 | ||||
| interface Source { | ||||
|     id: string; | ||||
|     name: string; | ||||
|     url: string; | ||||
| } | ||||
| 
 | ||||
| export function openScreenSharePicker(screens: Source[]) { | ||||
|     return new Promise<StreamPick>((resolve, reject) => { | ||||
|         const key = openModal( | ||||
|             props => ( | ||||
|                 <ModalComponent | ||||
|                     screens={screens} | ||||
|                     modalProps={props} | ||||
|                     submit={resolve} | ||||
|                     close={() => { | ||||
|                         props.onClose(); | ||||
|                         reject("Aborted"); | ||||
|                     }} | ||||
|                 /> | ||||
|             ), | ||||
|             { | ||||
|                 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 | ||||
| }: { | ||||
|     source: Source; | ||||
|     settings: StreamSettings; | ||||
|     setSettings: Dispatch<SetStateAction<StreamSettings>>; | ||||
| }) { | ||||
|     const [thumb] = useAwaiter(() => VencordDesktopNative.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"> | ||||
|                 <Card className={classes(WarningIconClasses.container, WarningIconClasses.warning, Margins.bottom8)}> | ||||
|                     <Forms.FormText> | ||||
|                         Resolution and Frame Rate aren't implemented for now. Locked to 720p 30fps | ||||
|                     </Forms.FormText> | ||||
|                 </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> | ||||
|                 )} | ||||
|             </Card> | ||||
|         </div> | ||||
|     ); | ||||
| } | ||||
| 
 | ||||
| function ModalComponent({ | ||||
|     screens, | ||||
|     modalProps, | ||||
|     submit, | ||||
|     close | ||||
| }: { | ||||
|     screens: Source[]; | ||||
|     modalProps: any; | ||||
|     submit: (data: StreamPick) => void; | ||||
|     close: () => void; | ||||
| }) { | ||||
|     const [selected, setSelected] = useState<string>(); | ||||
|     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} | ||||
|                     /> | ||||
|                 )} | ||||
|             </Modals.ModalContent> | ||||
| 
 | ||||
|             <Modals.ModalFooter className="vcd-screen-picker-footer"> | ||||
|                 <Button | ||||
|                     disabled={!selected} | ||||
|                     onClick={() => { | ||||
|                         submit({ | ||||
|                             id: selected!, | ||||
|                             ...settings | ||||
|                         }); | ||||
|                         close(); | ||||
|                     }} | ||||
|                 > | ||||
|                     Go Live | ||||
|                 </Button> | ||||
| 
 | ||||
|                 {selected ? ( | ||||
|                     <Button color={Button.Colors.TRANSPARENT} onClick={() => setSelected(void 0)}> | ||||
|                         Back | ||||
|                     </Button> | ||||
|                 ) : ( | ||||
|                     <Button color={Button.Colors.TRANSPARENT} onClick={close}> | ||||
|                         Cancel | ||||
|                     </Button> | ||||
|                 )} | ||||
|             </Modals.ModalFooter> | ||||
|         </Modals.ModalRoot> | ||||
|     ); | ||||
| } | ||||
|  | @ -4,4 +4,5 @@ | |||
|  * Copyright (c) 2023 Vendicated and Vencord contributors | ||||
|  */ | ||||
| 
 | ||||
| export * as ScreenShare from "./ScreenSharePicker"; | ||||
| export { default as Settings } from "./Settings"; | ||||
|  |  | |||
							
								
								
									
										126
									
								
								src/renderer/components/screenSharePicker.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										126
									
								
								src/renderer/components/screenSharePicker.css
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,126 @@ | |||
| .vcd-screen-picker-modal { | ||||
|     padding: 1em; | ||||
| } | ||||
| 
 | ||||
| .vcd-screen-picker-header h1 { | ||||
|     margin: 0; | ||||
| } | ||||
| 
 | ||||
| .vcd-screen-picker-footer { | ||||
|     display: flex; | ||||
|     gap: 1em; | ||||
| } | ||||
| 
 | ||||
| .vcd-screen-picker-grid { | ||||
|     display: grid; | ||||
|     grid-template-columns: 1fr 1fr; | ||||
|     gap: 2em 1em; | ||||
| } | ||||
| 
 | ||||
| .vcd-screen-picker-grid input { | ||||
|     appearance: none; | ||||
|     cursor: pointer; | ||||
| } | ||||
| 
 | ||||
| .vcd-screen-picker-selected img { | ||||
|     border: 2px solid var(--brand-experiment); | ||||
|     border-radius: 3px; | ||||
| } | ||||
| 
 | ||||
| .vcd-screen-picker-grid label { | ||||
|     overflow: hidden; | ||||
|     padding: 4px 0px; | ||||
|     cursor: pointer; | ||||
| } | ||||
| 
 | ||||
| .vcd-screen-picker-grid label:hover { | ||||
|     outline: 2px solid var(--brand-experiment); | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
| .vcd-screen-picker-grid div { | ||||
|     white-space: nowrap; | ||||
|     text-overflow: ellipsis; | ||||
|     overflow: hidden; | ||||
|     text-align: center; | ||||
|     font-weight: 600; | ||||
|     margin-inline: 0.5em; | ||||
| } | ||||
| 
 | ||||
| .vcd-screen-picker-card { | ||||
|     padding: 0.5em; | ||||
|     box-sizing: border-box; | ||||
| } | ||||
| 
 | ||||
| .vcd-screen-picker-preview img { | ||||
|     width: 100%; | ||||
|     margin-bottom: 0.5em; | ||||
| } | ||||
| 
 | ||||
| .vcd-screen-picker-preview { | ||||
|     display: flex; | ||||
|     flex-direction: column; | ||||
|     justify-content: center; | ||||
|     align-items: center; | ||||
|     margin-bottom: 1em; | ||||
| } | ||||
| 
 | ||||
| .vcd-screen-picker-radio input { | ||||
|     display: none; | ||||
| } | ||||
| 
 | ||||
| .vcd-screen-picker-radio { | ||||
|     background-color: var(--background-secondary); | ||||
|     border: 1px solid var(--primary-800); | ||||
|     padding: 0.3em; | ||||
|     cursor: pointer; | ||||
| } | ||||
| 
 | ||||
| .vcd-screen-picker-radio h2 { | ||||
|     margin: 0; | ||||
| } | ||||
| 
 | ||||
| .vcd-screen-picker-radio[data-checked="true"] { | ||||
|     background-color: var(--brand-experiment); | ||||
|     border-color: var(--brand-experiment); | ||||
| } | ||||
| 
 | ||||
| .vcd-screen-picker-radio[data-checked="true"] h2 { | ||||
|     color: var(--interactive-active); | ||||
| } | ||||
| 
 | ||||
| .vcd-screen-picker-quality { | ||||
|     display: flex; | ||||
|     gap: 1em; | ||||
| 
 | ||||
|     margin-bottom: 0.5em; | ||||
| 
 | ||||
|     opacity: 0.3; | ||||
| } | ||||
| 
 | ||||
| .vcd-screen-picker-quality section { | ||||
|     flex: 1 1 auto; | ||||
| } | ||||
| 
 | ||||
| .vcd-screen-picker-radios { | ||||
|     display: flex; | ||||
|     width: 100%; | ||||
|     border-radius: 3px; | ||||
| } | ||||
| 
 | ||||
| .vcd-screen-picker-radios label { | ||||
|     flex: 1 1 auto; | ||||
|     text-align: center; | ||||
| } | ||||
| 
 | ||||
| .vcd-screen-picker-radios label:first-child { | ||||
|     border-radius: 3px 0 0 3px; | ||||
| } | ||||
| 
 | ||||
| .vcd-screen-picker-radios label:last-child { | ||||
|     border-radius: 0 3px 3px 0; | ||||
| } | ||||
| 
 | ||||
| .vcd-screen-picker-audio { | ||||
|     margin-bottom: 0; | ||||
| } | ||||
|  | @ -12,3 +12,7 @@ export const isFirstRun = (() => { | |||
|     localStorage.setItem(key, "false"); | ||||
|     return true; | ||||
| })(); | ||||
| 
 | ||||
| const { platform } = navigator; | ||||
| 
 | ||||
| export const isWindows = platform.startsWith("Win"); | ||||
|  |  | |||
|  | @ -28,6 +28,8 @@ export const enum IpcEvents { | |||
| 
 | ||||
|     SPELLCHECK_SET_LANGUAGES = "VCD_SPELLCHECK_SET_LANGUAGES", | ||||
| 
 | ||||
|     CAPTURER_GET_LARGE_THUMBNAIL = "VCD_CAPTURER_GET_LARGE_THUMBNAIL", | ||||
| 
 | ||||
|     AUTOSTART_ENABLED = "VCD_AUTOSTART_ENABLED", | ||||
|     ENABLE_AUTOSTART = "VCD_ENABLE_AUTOSTART", | ||||
|     DISABLE_AUTOSTART = "VCD_DISABLE_AUTOSTART" | ||||
|  |  | |||
		Loading…
	
		Reference in a new issue
	
	 V
						V