From 253277984b8858d79e7814698344e59ceb48bbca Mon Sep 17 00:00:00 2001 From: V Date: Wed, 21 Jun 2023 20:52:56 +0200 Subject: [PATCH] Add ScreenSharing (#14) --- .vscode/settings.json | 6 + README.md | 1 - src/main/index.ts | 2 + src/main/screenShare.ts | 55 +++++ src/preload/VencordDesktopNative.ts | 3 + src/renderer/components/ScreenSharePicker.tsx | 225 ++++++++++++++++++ src/renderer/components/index.ts | 1 + src/renderer/components/screenSharePicker.css | 126 ++++++++++ src/renderer/utils.ts | 4 + src/shared/IpcEvents.ts | 2 + 10 files changed, 424 insertions(+), 1 deletion(-) create mode 100644 src/main/screenShare.ts create mode 100644 src/renderer/components/ScreenSharePicker.tsx create mode 100644 src/renderer/components/screenSharePicker.css diff --git a/.vscode/settings.json b/.vscode/settings.json index b09ba96..53b335c 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -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" }, diff --git a/README.md b/README.md index 537c313..795a0cc 100644 --- a/README.md +++ b/README.md @@ -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!! diff --git a/src/main/index.ts b/src/main/index.ts index 4730d63..d70f2a2 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -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", () => { diff --git a/src/main/screenShare.ts b/src/main/screenShare.ts new file mode 100644 index 0000000..f04053f --- /dev/null +++ b/src/main/screenShare.ts @@ -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); + }); +} diff --git a/src/preload/VencordDesktopNative.ts b/src/preload/VencordDesktopNative.ts index e0e2d1a..2e56324 100644 --- a/src/preload/VencordDesktopNative.ts +++ b/src/preload/VencordDesktopNative.ts @@ -34,5 +34,8 @@ export const VencordDesktopNative = { }, win: { focus: () => invoke(IpcEvents.FOCUS) + }, + capturer: { + getLargeThumbnail: (id: string) => invoke(IpcEvents.CAPTURER_GET_LARGE_THUMBNAIL, id) } }; diff --git a/src/renderer/components/ScreenSharePicker.tsx b/src/renderer/components/ScreenSharePicker.tsx new file mode 100644 index 0000000..d9d1e5f --- /dev/null +++ b/src/renderer/components/ScreenSharePicker.tsx @@ -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((resolve, reject) => { + const key = openModal( + props => ( + { + props.onClose(); + reject("Aborted"); + }} + /> + ), + { + onCloseRequest() { + closeModal(key); + reject("Aborted"); + } + } + ); + }); +} + +function ScreenPicker({ screens, chooseScreen }: { screens: Source[]; chooseScreen: (id: string) => void }) { + return ( +
+ {screens.map(({ id, name, url }) => ( + + ))} +
+ ); +} + +function StreamSettings({ + source, + settings, + setSettings +}: { + source: Source; + settings: StreamSettings; + setSettings: Dispatch>; +}) { + const [thumb] = useAwaiter(() => VencordDesktopNative.capturer.getLargeThumbnail(source.id), { + fallbackValue: source.url, + deps: [source.id] + }); + + return ( +
+ What you're streaming + + + {source.name} + + + Stream Settings + + + + + Resolution and Frame Rate aren't implemented for now. Locked to 720p 30fps + + + +
+
+ Resolution +
+ {StreamResolutions.map(res => ( + + ))} +
+
+ +
+ Frame Rate +
+ {StreamFps.map(fps => ( + + ))} +
+
+
+ + {isWindows && ( + setSettings(s => ({ ...s, audio: checked }))} + hideBorder + className="vcd-screen-picker-audio" + > + Stream With Audio + + )} +
+
+ ); +} + +function ModalComponent({ + screens, + modalProps, + submit, + close +}: { + screens: Source[]; + modalProps: any; + submit: (data: StreamPick) => void; + close: () => void; +}) { + const [selected, setSelected] = useState(); + const [settings, setSettings] = useState({ + resolution: "1080", + fps: "60", + audio: true + }); + + return ( + + + ScreenShare + + + + + {!selected ? ( + + ) : ( + s.id === selected)!} + settings={settings} + setSettings={setSettings} + /> + )} + + + + + + {selected ? ( + + ) : ( + + )} + + + ); +} diff --git a/src/renderer/components/index.ts b/src/renderer/components/index.ts index ab7b2da..f921a98 100644 --- a/src/renderer/components/index.ts +++ b/src/renderer/components/index.ts @@ -4,4 +4,5 @@ * Copyright (c) 2023 Vendicated and Vencord contributors */ +export * as ScreenShare from "./ScreenSharePicker"; export { default as Settings } from "./Settings"; diff --git a/src/renderer/components/screenSharePicker.css b/src/renderer/components/screenSharePicker.css new file mode 100644 index 0000000..17f00bd --- /dev/null +++ b/src/renderer/components/screenSharePicker.css @@ -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; +} diff --git a/src/renderer/utils.ts b/src/renderer/utils.ts index 14d5238..8c1050f 100644 --- a/src/renderer/utils.ts +++ b/src/renderer/utils.ts @@ -12,3 +12,7 @@ export const isFirstRun = (() => { localStorage.setItem(key, "false"); return true; })(); + +const { platform } = navigator; + +export const isWindows = platform.startsWith("Win"); diff --git a/src/shared/IpcEvents.ts b/src/shared/IpcEvents.ts index 08c7d99..1e024a3 100644 --- a/src/shared/IpcEvents.ts +++ b/src/shared/IpcEvents.ts @@ -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"