Upload files to 'src/renderer/components'

normal
aiek 2 months ago
parent 55c77395bb
commit f5af0deb31
  1. 534
      src/renderer/components/ScreenSharePicker.tsx
  2. 7
      src/renderer/components/index.ts
  3. 146
      src/renderer/components/screenSharePicker.css

@ -0,0 +1,534 @@
/*
* SPDX-License-Identifier: GPL-3.0
* Aerocord, a vesktop fork for older microsoft NT releases such as NT 6.0, 6.1, 6.2 and 6.3.
* Credits to vendicated and the rest of the vesktop contribuitors for making Vesktop!
*/
import "./screenSharePicker.css";
import { closeModal, Logger, Margins, Modals, ModalSize, 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;
contentHint?: string;
workaround?: boolean;
onlyDefaultSpeakers?: boolean;
}
export interface StreamPick extends StreamSettings {
id: string;
}
interface Source {
id: string;
name: string;
url: string;
}
export let currentSettings: StreamSettings | null = null;
const logger = new Logger("VesktopScreenShare");
addPatch({
patches: [
{
find: "this.localWant=",
replacement: {
match: /this.localWant=/,
replace: "$self.patchStreamQuality(this);$&"
}
},
{
find: "x-google-max-bitrate",
replacement: [
{
// eslint-disable-next-line no-useless-escape
match: /"x-google-max-bitrate=".concat\(\i\)/,
replace: '"x-google-max-bitrate=".concat("80_000")'
},
{
match: /;level-asymmetry-allowed=1/,
replace: ";b=AS:800000;level-asymmetry-allowed=1"
}
]
}
],
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
});
if (opts?.encode) {
Object.assign(opts.encode, {
framerate,
width,
height,
pixelCount: height * width
});
}
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 className="vcd-screen-picker-settings-grid">
<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>
<div className="vcd-screen-picker-quality">
<section>
<Forms.FormTitle>Content Type</Forms.FormTitle>
<div>
<div className="vcd-screen-picker-radios">
<label
className="vcd-screen-picker-radio"
data-checked={settings.contentHint === "motion"}
>
<Text variant="text-sm/bold">Prefer Smoothness</Text>
<input
type="radio"
name="contenthint"
value="motion"
checked={settings.contentHint === "motion"}
onChange={() => setSettings(s => ({ ...s, contentHint: "motion" }))}
/>
</label>
<label
className="vcd-screen-picker-radio"
data-checked={settings.contentHint === "detail"}
>
<Text variant="text-sm/bold">Prefer Clarity</Text>
<input
type="radio"
name="contenthint"
value="detail"
checked={settings.contentHint === "detail"}
onChange={() => setSettings(s => ({ ...s, contentHint: "detail" }))}
/>
</label>
</div>
<div className="vcd-screen-picker-hint-description">
<p>
Choosing "Prefer Clarity" will result in a significantly lower framerate in
exchange for a much sharper and clearer image.
</p>
</div>
</div>
</section>
</div>
</Card>
</div>
<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}
onlyDefaultSpeakers={settings.onlyDefaultSpeakers}
setAudioSource={source => setSettings(s => ({ ...s, audioSource: source }))}
setWorkaround={value => setSettings(s => ({ ...s, workaround: value }))}
setOnlyDefaultSpeakers={value => setSettings(s => ({ ...s, onlyDefaultSpeakers: value }))}
/>
)}
</div>
</div>
);
}
function AudioSourcePickerLinux({
audioSource,
workaround,
onlyDefaultSpeakers,
setAudioSource,
setWorkaround,
setOnlyDefaultSpeakers
}: {
audioSource?: string;
workaround?: boolean;
onlyDefaultSpeakers?: boolean;
setAudioSource(s: string): void;
setWorkaround(b: boolean): void;
setOnlyDefaultSpeakers(b: boolean): void;
}) {
const [sources, _, loading] = useAwaiter(() => VesktopNative.virtmic.list(), {
fallbackValue: { ok: true, targets: [], hasPipewirePulse: true }
});
const allSources = sources.ok ? ["None", "Entire System", ...sources.targets] : null;
const hasPipewirePulse = sources.ok ? sources.hasPipewirePulse : true;
const [ignorePulseWarning, setIgnorePulseWarning] = useState(false);
return (
<>
<Forms.FormTitle>Audio Settings</Forms.FormTitle>
<Card className="vcd-screen-picker-card">
{loading ? (
<Forms.FormTitle>Loading Audio Sources...</Forms.FormTitle>
) : (
<Forms.FormTitle>Audio Source</Forms.FormTitle>
)}
{!sources.ok && sources.isGlibCxxOutdated && (
<Forms.FormText>
Failed to retrieve Audio Sources because your C++ library is too old to run
<a href="https://github.com/Vencord/venmic" target="_blank">
venmic
</a>
. See{" "}
<a href="https://gist.github.com/Vendicated/b655044ffbb16b2716095a448c6d827a" target="_blank">
this guide
</a>{" "}
for possible solutions.
</Forms.FormText>
)}
{hasPipewirePulse || ignorePulseWarning ? (
allSources && (
<Select
options={allSources.map(s => ({ label: s, value: s, default: s === "None" }))}
isSelected={s => s === audioSource}
select={setAudioSource}
serialize={String}
/>
)
) : (
<Text variant="text-sm/normal">
Could not find pipewire-pulse. This usually means that you do not run pipewire as your main
audio-server. <br />
You can still continue, however, please beware that you can only share audio of apps that are
running under pipewire.
<br />
<a onClick={() => setIgnorePulseWarning(true)}>I know what I'm doing</a>
</Text>
)}
<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>
<Switch
hideBorder
onChange={setOnlyDefaultSpeakers}
disabled={audioSource !== "Entire System"}
value={onlyDefaultSpeakers ?? true}
note={
<>
When sharing entire desktop audio, only share apps that play to the default speakers and
ignore apps that play to other speakers or devices.
</>
}
>
Only Default Speakers
</Switch>
</Card>
</>
);
}
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: "720",
fps: "30",
contentHint: "motion",
audio: true
});
return (
<Modals.ModalRoot {...modalProps} size={ModalSize.MEDIUM}>
<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;
try {
const frameRate = Number(settings.fps);
const height = Number(settings.resolution);
const width = Math.round(height * (16 / 9));
const conn = [...MediaEngineStore.getMediaEngine().connections].find(
connection => connection.streamUserId === UserStore.getCurrentUser().id
);
if (conn) {
conn.videoStreamParameters[0].maxFrameRate = frameRate;
conn.videoStreamParameters[0].maxResolution.height = height;
conn.videoStreamParameters[0].maxResolution.width = width;
}
submit({
id: selected!,
...settings
});
setTimeout(async () => {
const conn = [...MediaEngineStore.getMediaEngine().connections].find(
connection => connection.streamUserId === UserStore.getCurrentUser().id
);
if (!conn) return;
const track = conn.input.stream.getVideoTracks()[0];
const constraints = {
...track.getConstraints(),
frameRate,
width: { min: 640, ideal: width, max: width },
height: { min: 480, ideal: height, max: height },
advanced: [{ width: width, height: height }],
resizeMode: "none"
};
try {
await track.applyConstraints(constraints);
logger.info(
"Applied constraints successfully. New constraints:",
track.getConstraints()
);
} catch (e) {
logger.error("Failed to apply constraints.", e);
}
}, 100);
} catch (error) {
logger.error("Error while submitting stream.", error);
}
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>
);
}

@ -0,0 +1,7 @@
/*
* SPDX-License-Identifier: GPL-3.0
* Aerocord, a vesktop fork for older microsoft NT releases such as NT 6.0, 6.1, 6.2 and 6.3.
* Credits to vendicated and the rest of the vesktop contribuitors for making Vesktop!
*/
export * as ScreenShare from "./ScreenSharePicker";

@ -0,0 +1,146 @@
.vcd-screen-picker-modal {
padding: 1em;
}
.vcd-screen-picker-header h1 {
margin: 0;
}
.vcd-screen-picker-footer {
display: flex;
gap: 1em;
}
.vcd-screen-picker-settings-grid {
gap: 1em;
display: grid;
grid-template-columns: 1fr 1fr;
}
.vcd-screen-picker-settings-grid > div {
display: flex;
flex-direction: column;
}
.vcd-screen-picker-card {
flex-grow: 1;
}
.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;
}
.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;
}
.vcd-screen-picker-hint-description {
color: var(--header-secondary);
font-size: 14px;
line-height: 20px;
font-weight: 400;
}
Loading…
Cancel
Save