feat: allow auto streaming of camera devices (#469)

* feat: allow auto streaming of camera devices

* Make My Edits

* Merge StartStream + Respect Sound & Preview Settings

* Proper About

* Redo Logger Naming

* Remove Extra Space

---------

Co-authored-by: thororen1234 <78185467+thororen1234@users.noreply.github.com>
This commit is contained in:
Mart 2025-10-26 17:37:06 -04:00 • committed by GitHub
parent 23f49ddcce
commit 3e340f824f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 125 additions and 31 deletions

View file

@ -1,10 +1,37 @@
import { FluxStore } from "..";
export class MediaEngineStore extends FluxStore {
isSelfMute(): boolean;
isSelfDeaf(): boolean;
isAdvancedVoiceActivitySupported(): boolean;
isAecDumpSupported(): boolean;
isAnyLocalVideoAutoDisabled(): boolean;
isAutomaticGainControlSupported(): boolean;
isDeaf(): boolean;
isEnableHardwareMuteNotice(): boolean;
isEnabled(): boolean;
isExperimentalEncodersSupported(): boolean;
isHardwareMute(): boolean;
isInputProfileCustom(): boolean;
isInteractionRequired(): boolean;
isLocalMute(): boolean;
isLocalMute(userId): boolean;
isLocalVideoAutoDisabled(userId): boolean;
isLocalVideoDisabled(userId): boolean;
isLocalVideoDisabled(): boolean;
isMediaFilterSettingLoading(): boolean;
isMute(): boolean;
isNativeAudioPermissionReady(): boolean;
isNoiseCancellationError(): boolean;
isNoiseCancellationSupported(): boolean;
isNoiseSuppressionSupported(): boolean;
isScreenSharing(): boolean;
isSelfDeaf(): boolean;
isSelfMute(): boolean;
isSelfMutedTemporarily(): boolean;
isSimulcastSupported(): boolean;
isSoundSharing(): boolean;
isSupported(): boolean;
isVideoAvailable(): boolean;
isVideoEnabled(): boolean;
setAv1Enabled(AV1: boolean): void;
setH265Enabled(H265: boolean): void;

View file

@ -4,45 +4,60 @@
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { getUserSettingLazy } from "@api/UserSettings";
import { HeadingSecondary } from "@components/Heading";
import { Paragraph } from "@components/Paragraph";
import { Devs } from "@utils/constants";
import { Devs, EquicordDevs } from "@utils/constants";
import definePlugin from "@utils/types";
import { VoiceState } from "@vencord/discord-types";
import { findByCodeLazy } from "@webpack";
import { ChannelStore, PermissionsBits, PermissionStore, SelectedChannelStore, UserStore } from "@webpack/common";
import { findByCodeLazy, findStoreLazy } from "@webpack";
import { ChannelStore, MediaEngineStore, PermissionsBits, PermissionStore, SelectedChannelStore, UserStore, VoiceActions } from "@webpack/common";
import { getCurrentMedia, settings } from "./utils";
const startStream = findByCodeLazy('type:"STREAM_START"');
let hasStreamed;
const startStream = findByCodeLazy('type:"STREAM_START"');
const StreamPreviewSettings = getUserSettingLazy("voiceAndVideo", "disableStreamPreviews")!;
const ApplicationStreamingSettingsStore = findStoreLazy("ApplicationStreamingSettingsStore");
async function autoStartStream() {
const selected = SelectedChannelStore.getVoiceChannelId();
if (!selected) return;
const channel = ChannelStore.getChannel(selected);
if (channel.type === 13 || !PermissionStore.can(PermissionsBits.STREAM, channel)) return;
const channel = ChannelStore.getChannel(selected);
const isGuildChannel = !channel.isDM() && !channel.isGroupDM();
if (channel.type === 13 || isGuildChannel && !PermissionStore.can(PermissionsBits.STREAM, channel)) return;
if (settings.store.autoDeafen && !MediaEngineStore.isSelfDeaf()) {
VoiceActions.toggleSelfDeaf();
} else if (settings.store.autoMute && !MediaEngineStore.isSelfMute()) {
VoiceActions.toggleSelfMute();
}
const streamMedia = await getCurrentMedia();
const preview = StreamPreviewSettings.getSetting();
const { soundshareEnabled } = ApplicationStreamingSettingsStore.getState();
let sourceId = streamMedia.id;
if (streamMedia.type === "video_device") sourceId = `camera:${streamMedia.id}`;
startStream(channel.guild_id, selected, {
startStream(channel.guild_id ?? null, selected, {
"pid": null,
"sourceId": streamMedia.id,
"sourceId": sourceId,
"sourceName": streamMedia.name,
"audioSourceId": null,
"sound": true,
"previewDisabled": false
"audioSourceId": streamMedia.name,
"sound": soundshareEnabled,
"previewDisabled": preview
});
}
export default definePlugin({
name: "InstantScreenshare",
description: "Instantly screenshare when joining a voice channel",
authors: [Devs.HAHALOSAH, Devs.thororen],
description: "Instantly screenshare when joining a voice channel with support for desktop sources, windows, and video input devices (cameras, capture cards)",
authors: [Devs.HAHALOSAH, Devs.thororen, EquicordDevs.mart],
getCurrentMedia,
settings,
settingsAboutComponent: () => (
<>
<HeadingSecondary>For Linux</HeadingSecondary>
@ -51,8 +66,19 @@ export default definePlugin({
<br />
For X11 it may or may not work :shrug:
</Paragraph>
<br />
<HeadingSecondary>Video Devices</HeadingSecondary>
<Paragraph>
Supports cameras and capture cards (like Elgato HD60X) when enabled in settings
</Paragraph>
<br />
<HeadingSecondary>Regarding Sound & Preview Settings</HeadingSecondary>
<Paragraph>
We use the settings set and used by discord to decide if stream preview and sound should be enabled or not
</Paragraph>
</>
),
flux: {
async VOICE_STATE_UPDATES({ voiceStates }: { voiceStates: VoiceState[]; }) {
const myId = UserStore.getCurrentUser().id;

View file

@ -9,7 +9,7 @@ import { Heading } from "@components/Heading";
import { Paragraph } from "@components/Paragraph";
import { Logger } from "@utils/Logger";
import { OptionType } from "@utils/types";
import { findByCodeLazy } from "@webpack";
import { findByCodeLazy, findByPropsLazy } from "@webpack";
import { MediaEngineStore, SearchableSelect, useEffect, useState } from "@webpack/common";
interface PickerProps {
@ -18,26 +18,52 @@ interface PickerProps {
}
const getDesktopSources = findByCodeLazy("desktop sources");
const configModule = findByPropsLazy("getOutputVolume");
const log = new Logger("InstantScreenShare");
export const settings = definePluginSettings({
streamMedia: {
type: OptionType.COMPONENT,
component: SettingSection,
},
includeVideoDevices: {
type: OptionType.BOOLEAN,
description: "Include video input devices (cameras, capture cards) in the source list",
default: false,
},
autoMute: {
type: OptionType.BOOLEAN,
description: "Automatically mute your microphone when joining a voice channel",
default: false,
},
autoDeafen: {
type: OptionType.BOOLEAN,
description: "Automatically deafen when joining a voice channel (also mutes you)",
default: false,
},
});
export async function getCurrentMedia() {
const media = MediaEngineStore.getMediaEngine();
const sources = [
...(await getDesktopSources(media, ["screen"], null) ?? []),
...(await getDesktopSources(media, ["window", "application"], null) ?? [])
];
const streamMedia = sources.find(screen => screen.id === settings.store.streamMedia);
console.log(sources);
const sources = await getDesktopSources(media, ["screen", "window"], null) ?? [];
if (settings.store.includeVideoDevices) {
try {
const videoDevices = Object.values(configModule.getVideoDevices() || {});
const videoSources = videoDevices.map((device: any) => ({
id: device.id,
name: device.name,
type: "video_device"
}));
sources.push(...videoSources);
} catch (e) {
new log.warn("Failed to get video devices:", e);
}
}
const streamMedia = sources.find(screen => screen.id === settings.store.streamMedia);
if (streamMedia) return streamMedia;
new Logger("InstantScreenShare").error(`Stream Media "${settings.store.streamMedia}" not found. Resetting to default.`);
log.error(`Stream Media "${settings.store.streamMedia}" not found. Resetting to default.`);
settings.store.streamMedia = sources[0];
return sources[0];
@ -63,7 +89,7 @@ function StreamSimplePicker({ streamMediaSelection, streamMedia }: PickerProps)
}
function ScreenSetting() {
const { streamMedia } = settings.use(["streamMedia"]);
const { streamMedia, includeVideoDevices } = settings.use(["streamMedia", "includeVideoDevices"]);
const media = MediaEngineStore.getMediaEngine();
const [streamMediaSelection, setStreamMediaSelection] = useState<any[]>([]);
const [loading, setLoading] = useState(true);
@ -72,10 +98,21 @@ function ScreenSetting() {
let active = true;
async function fetchMedia() {
setLoading(true);
const sources = [
...(await getDesktopSources(media, ["screen"], null) ?? []),
...(await getDesktopSources(media, ["window", "application"], null) ?? [])
];
const sources = await getDesktopSources(media, ["screen", "window"], null) ?? [];
if (includeVideoDevices) {
try {
const videoDevices = Object.values(configModule.getVideoDevices() || {});
const videoSources = videoDevices.map((device: any) => ({
id: device.id,
name: device.name,
type: "video_device"
}));
sources.push(...videoSources);
} catch (e) {
log.warn("Failed to get video devices:", e);
}
}
if (active) {
setStreamMediaSelection(sources);
@ -84,7 +121,7 @@ function ScreenSetting() {
}
fetchMedia();
return () => { active = false; };
}, []);
}, [includeVideoDevices]);
if (loading) return <Paragraph>Loading media sources...</Paragraph>;
if (!streamMediaSelection.length) return <Paragraph>No Media found.</Paragraph>;

View file

@ -1174,6 +1174,10 @@ export const EquicordDevs = Object.freeze({
name: "ZcraftElite",
id: 926788037785047050n
},
mart: {
name: "mja00",
id: 108698087769260032n
},
} satisfies Record<string, Dev>);
// iife so #__PURE__ works correctly