Compare commits

..

1 Commits

Author SHA1 Message Date
Malte Jürgens bdabf68c9b
add chromium flags for hardware acceleration 2022-10-15 20:28:38 +02:00
36 changed files with 466 additions and 1981 deletions

22
.gitignore vendored
View File

@ -1,20 +1,2 @@
# Ignore build and output directories /build
/build/ .vscode
/.flatpak-builder/
# Ignore flatpak-builder cache
/.flatpak-builder-cache/
# Ignore flatpak-builder log files
/*.log
# Ignore flatpak-builder generated files
/*.flatpak
/*.flatpak-builder
/*.flatpak-origin
# Ignore specific files or directories
/override.json
/metadata/
/manifest.json
/CMakeLists.txt.user

View File

@ -13,27 +13,24 @@ set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -DDEBUG")
string(TIMESTAMP TIMESTAMP %s) string(TIMESTAMP TIMESTAMP %s)
# set(CMAKE_AUTOUIC ON) # set(CMAKE_AUTOUIC ON)
find_package(Qt5 COMPONENTS Widgets) find_package(Qt5 CONFIG REQUIRED COMPONENTS
if (Qt5_FOUND) Widgets
find_package(Qt5 CONFIG REQUIRED COMPONENTS Widgets WebEngineWidgets) WebEngineWidgets
)
find_package(KF5Notifications) find_package(KF5Notifications)
if(KF5Notifications_FOUND) if(KF5Notifications_FOUND)
add_definitions( -DKNOTIFICATIONS ) add_definitions( -DKNOTIFICATIONS )
endif() endif()
find_package(KF5XmlGui) find_package(KF5XmlGui)
if(KF5XmlGui_FOUND) if(KF5XmlGui_FOUND)
add_definitions( -DKXMLGUI ) add_definitions( -DKXMLGUI )
endif() endif()
find_package(KF5GlobalAccel) find_package(KF5GlobalAccel)
if(KF5GlobalAccel_FOUND) if(KF5GlobalAccel_FOUND)
add_definitions( -DKGLOBALACCEL ) add_definitions( -DKGLOBALACCEL )
endif()
else()
message(WARNING "Qt 5 was not found on your system and Qt 6 will be used. You will not be able to use any features using KDE Frameworks.")
find_package(Qt6 CONFIG REQUIRED COMPONENTS Widgets WebEngineWidgets)
endif() endif()
set(discord-screenaudio_SRC set(discord-screenaudio_SRC
@ -43,17 +40,6 @@ set(discord-screenaudio_SRC
src/discordpage.cpp src/discordpage.cpp
src/streamdialog.cpp src/streamdialog.cpp
src/log.cpp src/log.cpp
src/userscript.cpp
src/centralwidget.cpp
src/localserver.cpp
src/centralwidget.h
src/discordpage.h
src/localserver.h
src/log.h
src/mainwindow.h
src/streamdialog.h
src/userscript.h
src/virtmic.h
resources.qrc resources.qrc
) )
@ -80,7 +66,7 @@ add_subdirectory(submodules/rohrkabel)
add_executable(discord-screenaudio ${discord-screenaudio_SRC}) add_executable(discord-screenaudio ${discord-screenaudio_SRC})
target_link_libraries(discord-screenaudio Qt::Widgets Qt::WebEngineWidgets rohrkabel) target_link_libraries(discord-screenaudio Qt5::Widgets Qt5::WebEngineWidgets rohrkabel)
if(KF5Notifications_FOUND) if(KF5Notifications_FOUND)
target_link_libraries(discord-screenaudio KF5::Notifications) target_link_libraries(discord-screenaudio KF5::Notifications)

View File

@ -9,9 +9,9 @@ of [@edisionnano](https://github.com/edisionnano) and the
Unlike a lot of other solutions, the audio here is directly fed into the Unlike a lot of other solutions, the audio here is directly fed into the
screenshare and not passed to the user microphone screenshare and not passed to the user microphone
([see explanation](#how-does-this-work)). ([see explanation](#how-it-works)).
![Screenshot_20221211_185028](https://user-images.githubusercontent.com/48161361/206920213-58a8091a-d8f9-4bb7-ae3d-3f8581b84d24.png) ![Screenshot_20220925_112945](https://user-images.githubusercontent.com/48161361/192137080-33466cf7-8c56-4373-90c6-01ea74b6fb83.png)
The purpose of this project is **not** to provide an alternative to the original The purpose of this project is **not** to provide an alternative to the original
Discord client. Rather, it should be used in addition to the original client in Discord client. Rather, it should be used in addition to the original client in
@ -50,8 +50,6 @@ You have multiple options:
### Requirements ### Requirements
- Basic building tools - Basic building tools
- An up-to-date system (I can't guarantee that it works on Debian or Ubuntu
20/21)
- CMake - CMake
- Qt5 and QtWebEngine - Qt5 and QtWebEngine
- **PipeWire** (it currently doesn't work with PulseAudio) - **PipeWire** (it currently doesn't work with PulseAudio)
@ -59,12 +57,9 @@ You have multiple options:
- _Kf5Notifications (optional, for better notifications)_ - _Kf5Notifications (optional, for better notifications)_
- _KXMLGui and KGlobalAccel (optional, for keybinds)_ - _KXMLGui and KGlobalAccel (optional, for keybinds)_
With apt: On Debian:
`apt install -y build-essential cmake qtbase5-dev qtwebengine5-dev libkf5notifications-dev libkf5xmlgui-dev libkf5globalaccel-dev pkg-config libpipewire-0.3-dev git` `apt install -y build-essential cmake qtbase5-dev qtwebengine5-dev libkf5notifications-dev libkf5xmlgui-dev libkf5globalaccel-dev pkg-config libpipewire-0.3-dev git`
With dnf:
`dnf install @development-tools cmake qt5-qtbase-devel qt5-qtwebengine-devel kf5-knotifications-devel kf5-kxmlgui-devel kf5-kglobalaccel-devel pkgconfig pipewire-devel git`
### Building ### Building
First, clone the repository: First, clone the repository:
@ -87,9 +82,7 @@ And then to optionally install it, run:
sudo cmake --install build sudo cmake --install build
``` ```
## FAQ ## How it works
### How does this work?
This whole project is based on This whole project is based on
[this](https://github.com/edisionnano/Screenshare-with-audio-on-Discord-with-Linux) [this](https://github.com/edisionnano/Screenshare-with-audio-on-Discord-with-Linux)
@ -98,23 +91,6 @@ Discord. Basically: a virtual microphone is created which captures the
application audio, and this microphone is then fed to the Discord stream by application audio, and this microphone is then fed to the Discord stream by
intercepting a API call of Discord. intercepting a API call of Discord.
### Drag and drop doesn't work in the Flatpak
This is due to sandboxing limitations of Flatpak. The main Discord Flatpak has
the same problem. If you still want to use drag and drop, you can disable most
of Flatpak's sandboxing by installing
[Flatseal](https://flathub.org/apps/details/com.github.tchx84.Flatseal) and
allowing access to "All system files" under the "Filesystem" section.
### Is there any way to add custom CSS / a theme?
Yes, you can add all your styles into
`~/.config/discord-screenaudio/userstyles.css` (or
`~/.var/app/de.shorsh.discord-screenaudio/config/discord-screenaudio/userstyles.css`
if you are using the Flatpak). But please note that due to QtWebEngine
limitations concerning content security policies, you can't use any external
files (like `@import` or `url()`).
## License ## License
Copyright (C) 2022 Malte Jürgens Copyright (C) 2022 Malte Jürgens

File diff suppressed because one or more lines are too long

View File

@ -1 +0,0 @@
setTimeout((i=>{let t,a,s,e={};new WebSocket("ws://127.0.0.1:1337").onmessage=async i=>{if(msg=JSON.parse(i.data),console.log(msg),!t){const i=window.webpackChunkdiscord_app.push([[Symbol()],{},i=>i]),e=i.c;window.webpackChunkdiscord_app.pop();for(const i in e){let a=e[i].exports;if(a=a&&(a.Z??a.ZP),a&&a.register&&a.wait){t=a;break}}const n=i.m;for(const t in n)if(n[t].toString().includes("getAssetImage: size must === [number, number] for Twitch")){const s=i(t),e=Object.values(s).find((i=>"function"==typeof i&&i.toString().includes("apply(")));a=async(i,t)=>(await e(i,[t,void 0]))[0];break}for(const t in n)if(n[t].toString().includes("e.application={")){const a=i(t),e=Object.values(a).find((i=>"function"==typeof i&&i.toString().includes("e.application={")));s=async i=>{let t={};return await e(t,i),t.application};break}}if(msg.activity?.assets?.large_image&&(msg.activity.assets.large_image=await a(msg.activity.application_id,msg.activity.assets.large_image)),msg.activity?.assets?.small_image&&(msg.activity.assets.small_image=await a(msg.activity.application_id,msg.activity.assets.small_image)),msg.activity){const i=msg.activity.application_id;e[i]||(e[i]=await s(i));const t=e[i];msg.activity.name||(msg.activity.name=t.name)}t.dispatch({type:"LOCAL_ACTIVITY_UPDATE",...msg})}}),1e4);

View File

@ -4,7 +4,6 @@
<metadata_license>CC0-1.0</metadata_license> <metadata_license>CC0-1.0</metadata_license>
<project_license>GPL-3.0+</project_license> <project_license>GPL-3.0+</project_license>
<name>discord-screenaudio</name> <name>discord-screenaudio</name>
<developer_name>Malte Jürgens</developer_name>
<releases> <releases>
<release version="${DISCORD_SCEENAUDIO_VERSION_FULL}" timestamp="${TIMESTAMP}" /> <release version="${DISCORD_SCEENAUDIO_VERSION_FULL}" timestamp="${TIMESTAMP}" />
</releases> </releases>

View File

@ -1,3 +1,5 @@
// From v0.4
navigator.mediaDevices.chromiumGetDisplayMedia = navigator.mediaDevices.chromiumGetDisplayMedia =
navigator.mediaDevices.getDisplayMedia; navigator.mediaDevices.getDisplayMedia;
@ -14,21 +16,21 @@ const getAudioDevice = async (nameOfAudioDevice) => {
let devices = await navigator.mediaDevices.enumerateDevices(); let devices = await navigator.mediaDevices.enumerateDevices();
audioDevice = devices.find(({ label }) => label === nameOfAudioDevice); audioDevice = devices.find(({ label }) => label === nameOfAudioDevice);
if (!audioDevice) if (!audioDevice)
userscript.log( console.log(
`Did not find '${nameOfAudioDevice}', trying again in 100ms` `dsa: Did not find '${nameOfAudioDevice}', trying again in 100ms`
); );
await sleep(100); await sleep(100);
} }
userscript.log(`Found '${nameOfAudioDevice}'`); console.log(`dsa: Found '${nameOfAudioDevice}'`);
return audioDevice; return audioDevice;
}; };
function setGetDisplayMedia(video = true, overrideArgs = undefined) { function setGetDisplayMedia(overrideArgs = undefined) {
const getDisplayMedia = async (...args) => { const getDisplayMedia = async (...args) => {
var id; var id;
try { try {
let myDiscordAudioSink = await getAudioDevice( let myDiscordAudioSink = await getAudioDevice(
"discord-awesomeaudio-virtmic" "discord-screenaudio-virtmic"
); );
id = myDiscordAudioSink.deviceId; id = myDiscordAudioSink.deviceId;
} catch (error) { } catch (error) {
@ -61,7 +63,6 @@ function setGetDisplayMedia(video = true, overrideArgs = undefined) {
: args || [{ video: true, audio: true }]) : args || [{ video: true, audio: true }])
); );
gdm.addTrack(track); gdm.addTrack(track);
if (!video) for (const track of gdm.getVideoTracks()) track.enabled = false;
return gdm; return gdm;
}; };
navigator.mediaDevices.getDisplayMedia = getDisplayMedia; navigator.mediaDevices.getDisplayMedia = getDisplayMedia;
@ -69,121 +70,17 @@ function setGetDisplayMedia(video = true, overrideArgs = undefined) {
setGetDisplayMedia(); setGetDisplayMedia();
let userscript;
let muteBtn;
let deafenBtn;
let streamStartBtn;
let streamStartBtnInitialDisplay;
let streamStartBtnClone;
let resolutionString;
const clonedElements = []; const clonedElements = [];
const hiddenElements = []; const hiddenElements = [];
let wasStreamActive = false; let wasStreamActive = false;
function createButton(text, onClick) { setInterval(() => {
const button = document.createElement("button");
button.style.marginBottom = "20px";
button.classList =
"button-ejjZWC lookFilled-1H2Jvj colorBrand-2M3O3N sizeSmall-3R2P2p grow-2T4nbg";
button.innerText = text;
button.addEventListener("click", onClick);
return button;
}
function createSwitch(text, enabled, onClick) {
const container = document.createElement("div");
container.style.marginBottom = "20px";
container.className = "labelRow-NnoUIp";
const label = document.createElement("label");
label.innerText = text;
label.className = "title-2yADjX";
container.appendChild(label);
const svg = document.createElement("div");
container.appendChild(svg);
function setSvgDisabled() {
svg.innerHTML = `<div class="container-1QtPKm default-colors" style="opacity: 1; background-color: rgb(114, 118, 125);"><svg class="slider-HJFN2i" viewBox="0 0 28 20" preserveAspectRatio="xMinYMid meet" aria-hidden="true" style="left: -3px;"><rect fill="white" x="4" y="0" height="20" width="20" rx="10"></rect><svg viewBox="0 0 20 20" fill="none"><path fill="rgba(114, 118, 125, 1)" d="M5.13231 6.72963L6.7233 5.13864L14.855 13.2704L13.264 14.8614L5.13231 6.72963Z"></path><path fill="rgba(114, 118, 125, 1)" d="M13.2704 5.13864L14.8614 6.72963L6.72963 14.8614L5.13864 13.2704L13.2704 5.13864Z"></path></svg></svg><input id="uid_84" type="checkbox" class="input-125oad" tabindex="0"></div>`;
}
function setSvgEnabled() {
svg.innerHTML = `<div class="container-1QtPKm default-colors checked-16gMAN" style="opacity: 1; background-color: rgb(59, 165, 92);"><svg class="slider-HJFN2i" viewBox="0 0 28 20" preserveAspectRatio="xMinYMid meet" aria-hidden="true" style="left: 12px;"><rect fill="white" x="4" y="0" height="20" width="20" rx="10"></rect><svg viewBox="0 0 20 20" fill="none"><path fill="rgba(59, 165, 92, 1)" d="M7.89561 14.8538L6.30462 13.2629L14.3099 5.25755L15.9009 6.84854L7.89561 14.8538Z"></path><path fill="rgba(59, 165, 92, 1)" d="M4.08643 11.0903L5.67742 9.49929L9.4485 13.2704L7.85751 14.8614L4.08643 11.0903Z"></path></svg></svg><input id="uid_74" type="checkbox" class="input-125oad" tabindex="0" checked=""></div>`;
}
function updateSvg() {
if (enabled) setSvgEnabled();
else setSvgDisabled();
}
container.addEventListener("click", () => {
enabled = !enabled;
updateSvg();
onClick(enabled);
});
updateSvg();
return container;
}
// Fix for broken discord notifications after restart
// (https://github.com/maltejur/discord-screenaudio/issues/17)
Notification.requestPermission();
setTimeout(() => {
new QWebChannel(qt.webChannelTransport, (channel) => {
userscript = channel.objects.userscript;
main();
});
});
function main() {
userscript.muteToggled.connect(() => {
console.log("Toggling mute");
muteBtn && muteBtn.click();
});
userscript.deafenToggled.connect(() => {
console.log("Toggling deafen");
deafenBtn && deafenBtn.click();
});
userscript.streamStarted.connect((video, width, height, frameRate) => {
resolutionString = video ? `${height}p ${frameRate}FPS` : "Audio Only";
setGetDisplayMedia(video, {
audio: true,
video: { width, height, frameRate },
});
streamStartBtn.click();
streamStartBtn.style.display = streamStartBtnInitialDisplay;
streamStartBtnClone.remove();
});
function updateUserstyles() {
userscript.log("Loading userstyles...");
userscript.loadingMessage = "Loading userstyles...";
let stylesheet = document.getElementById("discordScreenaudioUserstyles");
if (stylesheet) {
userscript.log("Removing old userstyles...");
stylesheet.remove();
}
stylesheet = document.createElement("style");
stylesheet.id = "discordScreenaudioUserstyles";
stylesheet.innerText = userscript.userstyles;
document.head.appendChild(stylesheet);
userscript.log("Finished loading userstyles");
userscript.loadingMessage = "";
}
userscript.userstylesChanged.connect(updateUserstyles);
setTimeout(() => updateUserstyles());
setInterval(async () => {
const streamActive = const streamActive =
document.getElementsByClassName("panel-2ZFCRb activityPanel-9icbyU") document.getElementsByClassName("panel-2ZFCRb activityPanel-9icbyU")
.length > 0; .length > 0;
if (!streamActive && wasStreamActive) userscript.stopVirtmic(); if (!streamActive && wasStreamActive)
console.log("!discord-screenaudio-stream-stopped");
wasStreamActive = streamActive; wasStreamActive = streamActive;
if (streamActive) { if (streamActive) {
@ -198,54 +95,59 @@ function main() {
hiddenElements.length = 0; hiddenElements.length = 0;
} else { } else {
for (const el of [ for (const el of [
document.getElementsByClassName("actionButtons-2vEOUh")?.[0] document.getElementsByClassName("actionButtons-2vEOUh")?.[0]?.children[1],
?.children[1],
document.querySelector( document.querySelector(
".wrapper-3t3Yqv > div > div > div > div > .controlButton-2PMNom" ".wrapper-3t3Yqv > div > div > div > div > .controlButton-2PMNom"
), ),
]) { ]) {
if (!el) continue; if (!el) continue;
if (el.classList.contains("discord-screenaudio-cloned")) continue; if (el.classList.contains("discord-screenaudio-cloned")) continue;
streamStartBtn = el; el.classList.add("discord-screenaudio-cloned");
streamStartBtn.classList.add("discord-screenaudio-cloned"); elClone = el.cloneNode(true);
elClone.title = "Share Your Screen with Audio";
streamStartBtnClone = streamStartBtn.cloneNode(true); elClone.addEventListener("click", () => {
streamStartBtnClone.title = "Share Your Screen with Audio"; console.log("!discord-screenaudio-start-stream");
streamStartBtnClone.addEventListener("click", () => {
userscript.showStreamDialog();
}); });
streamStartBtnInitialDisplay = streamStartBtn.style.display; const initialDisplay = el.style.display;
streamStartBtn.style.display = "none"; window.discordScreenaudioStartStream = (width, height, frameRate) => {
streamStartBtn.parentNode.insertBefore(streamStartBtnClone, el); window.discordScreenaudioResolutionString = `${height}p ${frameRate}FPS`;
setGetDisplayMedia({
audio: true,
video: { width, height, frameRate },
});
el.click();
el.style.display = initialDisplay;
elClone.remove();
};
clonedElements.push(streamStartBtnClone); el.style.display = "none";
hiddenElements.push(streamStartBtn); el.parentNode.insertBefore(elClone, el);
clonedElements.push(elClone);
hiddenElements.push(el);
} }
} }
// Add about text in settings // Add about text in settings
if ( if (
document.getElementsByClassName("dirscordScreenaudioAboutText").length == document.getElementsByClassName("dirscordScreenaudioAboutText").length == 0
0
) { ) {
for (const el of document.getElementsByClassName("info-3pQQBb")) { for (const el of document.getElementsByClassName("info-3pQQBb")) {
let aboutEl; let aboutEl;
if (userscript.kxmlgui) { if (window.discordScreenaudioKXMLGUI) {
aboutEl = document.createElement("a"); aboutEl = document.createElement("a");
aboutEl.addEventListener("click", () => { aboutEl.addEventListener("click", () => {
userscript.showHelpMenu(); console.log("!discord-screenaudio-about");
}); });
} else { } else {
aboutEl = document.createElement("div"); aboutEl = document.createElement("div");
} }
aboutEl.innerText = `discord-awesomeaudio ${userscript.version}`; aboutEl.innerText = `discord-screenaudio ${window.discordScreenaudioVersion}`;
aboutEl.style.fontSize = "12px"; aboutEl.style.fontSize = "12px";
aboutEl.style.color = "var(--text-muted)"; aboutEl.style.color = "var(--text-muted)";
aboutEl.style.textTransform = "none"; aboutEl.style.textTransform = "none";
aboutEl.style.display = "inline-block";
aboutEl.style.width = "100%";
aboutEl.classList.add("dirscordScreenaudioAboutText"); aboutEl.classList.add("dirscordScreenaudioAboutText");
aboutEl.style.cursor = "pointer"; aboutEl.style.cursor = "pointer";
el.appendChild(aboutEl); el.appendChild(aboutEl);
@ -261,7 +163,7 @@ function main() {
document document
.getElementById("keybinds-tab") .getElementById("keybinds-tab")
?.getElementsByClassName( ?.getElementsByClassName(
"container-3jbRo5 info-1hMolH browserNotice-1u-Y5o" "container-3jbRo5 info-1hMolH fontSize16-3zr6Io browserNotice-1u-Y5o"
).length ).length
) { ) {
const el = document const el = document
@ -269,114 +171,36 @@ function main() {
.getElementsByClassName("children-1xdcWE")[0]; .getElementsByClassName("children-1xdcWE")[0];
const div = document.createElement("div"); const div = document.createElement("div");
div.style.marginBottom = "50px"; div.style.marginBottom = "50px";
div.appendChild( const button = document.createElement("button");
createButton("Edit Global Keybinds", () => { button.classList =
userscript.showShortcutsDialog(); "button-f2h6uQ lookFilled-yCfaCM colorBrand-I6CyqQ sizeSmall-wU2dO- grow-2sR_-F";
}) button.innerText = "Edit Global Keybinds";
); button.addEventListener("click", () => {
console.log("!discord-screenaudio-keybinds");
});
div.appendChild(button);
el.innerHTML = ""; el.innerHTML = "";
el.appendChild(div); el.appendChild(div);
} }
const buttonContainer = const muteBtn = document.getElementsByClassName(
document.getElementsByClassName("container-YkUktl")[0]; "button-12Fmur enabled-9OeuTA button-f2h6uQ lookBlank-21BCro colorBrand-I6CyqQ grow-2sR_-F"
if (!buttonContainer) { )[0];
userscript.log( window.discordScreenaudioToggleMute = () => muteBtn.click();
"Cannot locate Mute/Deafen/Settings button container, please report this on GitHub" const deafenBtn = document.getElementsByClassName(
); "button-12Fmur enabled-9OeuTA button-f2h6uQ lookBlank-21BCro colorBrand-I6CyqQ grow-2sR_-F"
} )[1];
window.discordScreenaudioToggleDeafen = () => deafenBtn.click();
muteBtn = buttonContainer if (window.discordScreenaudioResolutionString) {
? buttonContainer.getElementsByTagName("button")[0]
: null;
deafenBtn = buttonContainer
? buttonContainer.getElementsByTagName("button")[1]
: null;
if (resolutionString) {
for (const el of document.getElementsByClassName( for (const el of document.getElementsByClassName(
"qualityIndicator-39wQDy" "qualityIndicator-39wQDy"
)) { )) {
el.innerHTML = resolutionString; el.innerHTML = window.discordScreenaudioResolutionString;
} }
} }
}, 500);
const accountTab = document.getElementById("my-account-tab"); // Fix for broken discord notifications after restart
if (accountTab) { // (https://github.com/maltejur/discord-screenaudio/issues/17)
const discordScreenaudioSettings = document.getElementById( Notification.requestPermission();
"discord-screenaudio-settings"
);
if (!discordScreenaudioSettings) {
const firstDivider = accountTab.getElementsByClassName(
"divider-3nqZNm marginTop40-Q4o1tS"
)[0];
if (firstDivider) {
const section = document.createElement("div");
section.className = "marginTop40-Q4o1tS";
section.id = "discord-screenaudio-settings";
const title = document.createElement("h2");
title.className =
"h1-3iMExa title-lXcL8p defaultColor-3Olr-9 defaultMarginh1-1UYutH";
title.innerText = "discord-awesomeaudio";
section.appendChild(title);
section.appendChild(
createButton("Edit Global Keybinds", () => {
userscript.showShortcutsDialog();
})
);
// section.appendChild(
// createButton("Install Theme", () => {
// userscript.showThemeDialog();
// })
// );
// section.appendChild(
// createButton("Uninstall Theme", () => {
// userscript.installUserStyles("");
// })
// );
section.appendChild(
createSwitch(
"Allow audio from your mic (and audio you pass through) to become stereo",
await userscript.getBoolPref("stereoMic", false),
(enabled) => {
userscript.setStereoMic(enabled);
}
)
);
section.appendChild(
createSwitch(
"Move discord-awesomeaudio to the system tray instead of closing",
await userscript.getBoolPref("trayIcon", false),
(enabled) => {
userscript.setTrayIcon(enabled);
}
)
);
section.appendChild(
createSwitch(
"Start discord-awesomeaudio hidden to tray",
await userscript.getPref("startHidden", false),
(hidden) => {
userscript.setPref("startHidden", hidden);
}
)
);
const divider = document.createElement("div");
divider.className = "divider-3nqZNm marginTop40-Q4o1tS";
firstDivider.after(section);
section.after(divider);
}
}
}
}, 500);
}

View File

@ -1,85 +0,0 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2022 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
/// <reference path="../src/modules.d.ts" />
/// <reference path="../src/globals.d.ts" />
import monacoHtml from "~fileContent/../src/components/monacoWin.html";
import * as DataStore from "../src/api/DataStore";
import { debounce } from "../src/utils";
import { getTheme, Theme } from "../src/utils/discord";
// Discord deletes this so need to store in variable
const { localStorage } = window;
// listeners for ipc.on
const cssListeners = new Set<(css: string) => void>();
const NOOP = () => { };
const NOOP_ASYNC = async () => { };
const setCssDebounced = debounce((css: string) => VencordNative.quickCss.set(css));
// probably should make this less cursed at some point
window.VencordNative = {
native: {
getVersions: () => ({}),
openExternal: async (url) => void open(url, "_blank")
},
updater: {
getRepo: async () => ({ ok: true, value: "https://github.com/Vendicated/Vencord" }),
getUpdates: async () => ({ ok: true, value: [] }),
update: async () => ({ ok: true, value: false }),
rebuild: async () => ({ ok: true, value: true }),
},
quickCss: {
get: () => DataStore.get("VencordQuickCss").then(s => s ?? ""),
set: async (css: string) => {
await DataStore.set("VencordQuickCss", css);
cssListeners.forEach(l => l(css));
},
addChangeListener(cb) {
cssListeners.add(cb);
},
openFile: NOOP_ASYNC,
async openEditor() {
const features = `popup,width=${Math.min(window.innerWidth, 1000)},height=${Math.min(window.innerHeight, 1000)}`;
const win = open("about:blank", "VencordQuickCss", features);
if (!win) {
alert("Failed to open QuickCSS popup. Make sure to allow popups!");
return;
}
win.setCss = setCssDebounced;
win.getCurrentCss = () => VencordNative.quickCss.get();
win.getTheme = () =>
getTheme() === Theme.Light
? "vs-light"
: "vs-dark";
win.document.write(monacoHtml);
},
},
settings: {
get: () => localStorage.getItem("VencordSettings") || "{}",
set: async (s: string) => localStorage.setItem("VencordSettings", s),
getSettingsDir: async () => "LocalStorage"
}
};

View File

@ -1,14 +0,0 @@
import definePlugin from "../utils/types";
export default definePlugin({
name: "discord-awesomeaudio",
authors: [
{
name: "retard",
id: 205966226709676099n,
},
],
required: true,
description: "UI patches for discord-screenaudio.",
patches: [],
});

View File

@ -1,15 +0,0 @@
--- a/src/components/VencordSettings/VencordTab.tsx
+++ b/src/components/VencordSettings/VencordTab.tsx
@@ -87,10 +87,10 @@ function VencordSettings() {
<Card className={cl("quick-actions-card")}>
{IS_WEB ? (
<Button
- onClick={() => require("../Monaco").launchMonacoEditor()}
+ onClick={() => VencordNative.ipc.send(IpcEvents.OPEN_EXTERNAL, settingsDir)}
size={Button.Sizes.SMALL}
disabled={settingsDir === "Loading..."}>
- Open QuickCSS File
+ Launch Directory
</Button>
) : (
<React.Fragment>

File diff suppressed because one or more lines are too long

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

View File

@ -1,42 +0,0 @@
#!/bin/bash
is_package_installed() {
if command -v "$1" >/dev/null 2>&1; then
return 0
else
return 1
fi
}
if [[ -f /etc/os-release ]]; then
. /etc/os-release
if [[ "$ID" == "ubuntu" || "$ID_LIKE" == "ubuntu" ]]; then
if ! is_package_installed flatpak-builder; then
sudo apt update
sudo apt install -y flatpak-builder
fi
elif [[ "$ID" == "fedora" || "$ID_LIKE" == "fedora" ]]; then
if ! is_package_installed flatpak-builder; then
sudo dnf install -y flatpak-builder
fi
elif [[ "$ID" == "void" ]]; then
if ! is_package_installed flatpak-builder; then
sudo xbps-install -Sy flatpak-builder
fi
fi
fi
flatpak install flathub org.kde.Sdk//5.15-22.08
flatpak install flathub io.qt.qtwebengine.BaseApp//5.15-22.08
flatpak-builder build-dir lol.deadzone.discord-awesomeaudio.json --install --user --force-clean
echo "[Desktop Entry]
Name=Discord Awesome Audio
Exec=flatpak run lol.deadzone.discord-awesomeaudio
Icon=/path/to/application/icon.png
Type=Application
Categories=AudioVideo;Network;" > ~/.local/share/applications/discord-awesomeaudio.desktop
chmod +x ~/.local/share/applications/discord-awesomeaudio.desktop

View File

@ -1,44 +0,0 @@
{
"app-id": "lol.deadzone.discord-awesomeaudio",
"runtime": "org.kde.Platform",
"runtime-version": "5.15-22.08",
"sdk": "org.kde.Sdk",
"base": "io.qt.qtwebengine.BaseApp",
"base-version": "5.15-22.08",
"command": "discord-screenaudio",
"finish-args": [
"--share=ipc",
"--share=network",
"--socket=wayland",
"--socket=fallback-x11",
"--socket=pulseaudio",
"--filesystem=xdg-videos:ro",
"--filesystem=xdg-pictures:ro",
"--filesystem=xdg-download",
"--filesystem=xdg-run/pipewire-0",
"--device=all",
"--talk-name=org.kde.StatusNotifierWatcher",
"--env=QTWEBENGINEPROCESS_PATH=/app/bin/QtWebEngineProcess",
"--talk-name=org.freedesktop.Notifications",
"--talk-name=org.kde.kglobalaccel"
],
"modules": [
{
"name": "discord-screenaudio",
"buildsystem": "cmake-ninja",
"config-opts": [
"-DCMAKE_BUILD_TYPE=RelWithDebInfo",
"-DGIT_SUBMODULE=OFF"
],
"sources": [
{
"type": "dir",
"path": "."
}
]
}
],
"finish-install": {
"desktop-file": "[Desktop Entry]\nName=Discord Awesome Audio\nExec=flatpak run lol.deadzone.discord-awesomeaudio\nIcon=/path/to/application/icon.png\nType=Application\nCategories=AudioVideo;Network;"
}
}

View File

@ -1,12 +1,7 @@
<!DOCTYPE RCC>
<RCC> <RCC>
<qresource prefix="/"> <qresource>
<file>assets/userscript.js</file> <file>assets/userscript.js</file>
<file>assets/arrpc_bridge_mod.js</file>
<file>assets/arrpc.js</file>
<file>assets/de.shorsh.discord-screenaudio.png</file> <file>assets/de.shorsh.discord-screenaudio.png</file>
<file>assets/vencord/vencord.js</file>
<file>assets/vencord/settings.patch</file>
<file>assets/vencord/plugin.js</file>
<file>assets/vencord/VencordNativeStub.ts</file>
</qresource> </qresource>
</RCC> </RCC>

View File

@ -1,36 +0,0 @@
#!/usr/bin/bash
set -e
cd "$(dirname "$0")/../submodules"
echo_status() {
echo
echo
echo "-> $1..."
}
if [ ! -d "arrpc" ]; then
echo_status "Cloning arRPC"
git clone https://github.com/OpenAsar/arrpc.git
cd arrpc
else
echo_status "Fetching arRPC changes"
cd arrpc
git fetch
fi
echo_status "Checking out latest commit"
git reset --hard HEAD
git checkout main
echo_status "Installing dependencies"
pnpm i -D @vercel/ncc
echo_status "Patching arRPC"
sed -i 's/"type": "module",//' package.json
echo_status "Building arRPC"
pnpm exec ncc build -m src/index.js
echo_status "Copying built file"
cp -v ./dist/index.js ../../assets/arrpc.js

View File

@ -1,39 +0,0 @@
#!/usr/bin/bash
set -e
cd "$(dirname "$0")/../submodules"
echo_status() {
echo
echo
echo "-> $1..."
}
if [ ! -d "Vencord" ]; then
echo_status "Cloning Vencord"
git clone https://github.com/Vendicated/Vencord.git
cd Vencord
else
echo_status "Fetching Vencord changes"
cd Vencord
git fetch
fi
echo_status "Checking out latest commit"
git reset --hard HEAD
git checkout main
#git reset --hard devbuild
echo_status "Installing dependencies"
pnpm i
echo_status "Patching Vencord"
cp -v ../../assets/vencord/plugin.js ./src/plugins/discord-screenaudio.js
cp -v ../../assets/vencord/VencordNativeStub.ts ./browser/VencordNativeStub.ts
patch -p1 -i ../../assets/vencord/settings.patch
echo_status "Building Vencord"
pnpm run buildWeb
echo_status "Copying built file"
cp -v ./dist/browser.js ../../assets/vencord/vencord.js

View File

@ -1,85 +0,0 @@
#include "centralwidget.h"
#include "discordpage.h"
#include "mainwindow.h"
#include <QWebEngineNotification>
#include <QWebEngineProfile>
#include <QWebEngineScript>
#include <QWebEngineScriptCollection>
#include <QWebEngineSettings>
#include <QWebEngineView>
CentralWidget::CentralWidget(QWidget *parent) : QWidget(parent) {
setStyleSheet("background-color:#313338;");
m_layout = new QVBoxLayout(this);
m_layout->setMargin(0);
m_layout->setSpacing(0);
setupWebView();
}
void CentralWidget::setupWebView() {
auto page = new DiscordPage(this);
m_webView = new QWebEngineView(this);
m_webView->setPage(page);
bool useNotifySend = MainWindow::instance()
->settings()
->value("useNotifySend", false)
.toBool();
if (m_useKF5Notifications || useNotifySend)
QWebEngineProfile::defaultProfile()->setNotificationPresenter(
[&](std::unique_ptr<QWebEngineNotification> notificationInfo) {
if (useNotifySend) {
auto title = notificationInfo->title();
auto message = notificationInfo->message();
auto image_path =
QString("/tmp/discord-screenaudio-%1.png").arg(title);
notificationInfo->icon().save(image_path);
QProcess::execute("notify-send",
{"--icon", image_path, "--app-name",
"discord-screenaudio", title, message});
} else if (m_useKF5Notifications) {
#ifdef KNOTIFICATIONS
KNotification *notification =
new KNotification("discordNotification");
notification->setTitle(notificationInfo->title());
notification->setText(notificationInfo->message());
notification->setPixmap(
QPixmap::fromImage(notificationInfo->icon()));
notification->setDefaultAction("View");
connect(notification, &KNotification::defaultActivated,
[&, notificationInfo = std::move(notificationInfo)]() {
notificationInfo->click();
show();
activateWindow();
});
notification->sendEvent();
#endif
}
});
connect(page->userScript(), &UserScript::loadingMessageChanged, this,
&CentralWidget::setLoadingIndicator);
m_layout->addWidget(m_webView);
}
void CentralWidget::setLoadingIndicator(QString text) {
if (text != "") {
if (m_loadingLabel == nullptr) {
m_loadingLabel = new QLabel(this);
m_loadingLabel->setMaximumHeight(20);
m_loadingLabel->setAlignment(Qt::AlignHCenter);
m_loadingLabel->setStyleSheet("color:#dedede;");
m_layout->addWidget(m_loadingLabel);
}
m_loadingLabel->setText(text.mid(0, 100));
} else {
if (m_loadingLabel != nullptr) {
m_layout->removeWidget(m_loadingLabel);
m_loadingLabel->deleteLater();
m_loadingLabel = nullptr;
}
}
}

View File

@ -1,35 +0,0 @@
#ifndef CENTRALWIDGET_H
#define CENTRALWIDGET_H
#include <QLabel>
#include <QVBoxLayout>
#include <QWebEnginePage>
#include <QWebEngineProfile>
#include <QWebEngineView>
#include <QWidget>
#ifdef KNOTIFICATIONS
constexpr bool USE_KF5_NOTIFICATIONS = true;
#else
constexpr bool USE_KF5_NOTIFICATIONS = false;
#endif
class CentralWidget : public QWidget {
Q_OBJECT
public:
CentralWidget(QWidget *parent = nullptr);
public slots:
void setLoadingIndicator(QString text);
private:
void setupWebView();
QVBoxLayout *m_layout;
QWebEngineView *m_webView;
QLabel *m_loadingLabel;
bool m_useKF5Notifications;
};
#endif // CENTRALWIDGET_H

View File

@ -3,13 +3,24 @@
#include "mainwindow.h" #include "mainwindow.h"
#include "virtmic.h" #include "virtmic.h"
#ifdef KXMLGUI
#include <KAboutData>
#include <KHelpMenu>
#include <KShortcutsDialog>
#include <KXmlGuiWindow>
#include <QAction>
#ifdef KGLOBALACCEL
#include <KGlobalAccel>
#endif
#endif
#include <QApplication> #include <QApplication>
#include <QDesktopServices> #include <QDesktopServices>
#include <QFile> #include <QFile>
#include <QFileInfo>
#include <QMessageBox> #include <QMessageBox>
#include <QNetworkReply> #include <QNetworkReply>
#include <QTemporaryFile>
#include <QTimer> #include <QTimer>
#include <QWebChannel> #include <QWebChannel>
#include <QWebEngineScript> #include <QWebEngineScript>
@ -17,40 +28,17 @@
#include <QWebEngineSettings> #include <QWebEngineSettings>
DiscordPage::DiscordPage(QWidget *parent) : QWebEnginePage(parent) { DiscordPage::DiscordPage(QWidget *parent) : QWebEnginePage(parent) {
setBackgroundColor(QColor("#313338")); setBackgroundColor(QColor("#202225"));
m_virtmicProcess.setProcessChannelMode(QProcess::ForwardedChannels);
connect(this, &QWebEnginePage::featurePermissionRequested, this, connect(this, &QWebEnginePage::featurePermissionRequested, this,
&DiscordPage::featurePermissionRequested); &DiscordPage::featurePermissionRequested);
connect(this, &DiscordPage::fullScreenRequested, MainWindow::instance(),
&MainWindow::fullScreenRequested);
setupPermissions(); connect(this, &QWebEnginePage::loadStarted, [=]() {
runJavaScript(QString("window.discordScreenaudioVersion = '%1';")
.arg(QApplication::applicationVersion()));
});
injectFile(&DiscordPage::injectScript, "qwebchannel.js",
":/qtwebchannel/qwebchannel.js");
setUrl(QUrl("https://discord.com/app"));
setWebChannel(new QWebChannel(this));
webChannel()->registerObject("userscript", &m_userScript);
injectFile(&DiscordPage::injectScript, "userscript.js",
":/assets/userscript.js");
QFile vencord(":/assets/vencord/vencord.js");
if (!vencord.open(QIODevice::ReadOnly))
qFatal("Failed to load vencord source with error: %s",
vencord.errorString().toLatin1().constData());
injectScript(
"vencord.js",
QString("window.discordScreenaudioVencordSettings = `%1`; %2")
.arg(m_userScript.vencordSend("VencordGetSettings", {}).toString(),
vencord.readAll()));
vencord.close();
setupUserStyles();
setupArrpc();
}
void DiscordPage::setupPermissions() {
settings()->setAttribute(QWebEngineSettings::ScreenCaptureEnabled, true); settings()->setAttribute(QWebEngineSettings::ScreenCaptureEnabled, true);
settings()->setAttribute(QWebEngineSettings::JavascriptCanOpenWindows, true); settings()->setAttribute(QWebEngineSettings::JavascriptCanOpenWindows, true);
settings()->setAttribute(QWebEngineSettings::AllowRunningInsecureContent, settings()->setAttribute(QWebEngineSettings::AllowRunningInsecureContent,
@ -62,87 +50,71 @@ void DiscordPage::setupPermissions() {
false); false);
settings()->setAttribute(QWebEngineSettings::JavascriptCanOpenWindows, false); settings()->setAttribute(QWebEngineSettings::JavascriptCanOpenWindows, false);
settings()->setAttribute(QWebEngineSettings::ScrollAnimatorEnabled, true); settings()->setAttribute(QWebEngineSettings::ScrollAnimatorEnabled, true);
setUrl(QUrl("https://discord.com/app"));
injectScriptFile("userscript.js", ":/assets/userscript.js");
injectScriptText("version.js",
QString("window.discordScreenaudioVersion = '%1';")
.arg(QApplication::applicationVersion()));
#ifdef KXMLGUI
injectScriptText("xmlgui.js", "window.discordScreenaudioKXMLGUI = true;");
KAboutData aboutData(
"discord-screenaudio", "discord-screenaudio",
QApplication::applicationVersion(),
"Custom Discord client with the ability to stream audio on Linux",
KAboutLicense::GPL_V3, "Copyright 2022 (C) Malte Jürgens");
aboutData.setBugAddress("https://github.com/maltejur/discord-screenaudio");
aboutData.addAuthor("Malte Jürgens", "Author", "maltejur@dismail.de",
"https://github.com/maltejur");
aboutData.addCredit("edisionnano",
"For creating and documenting the approach for streaming "
"audio in Discord used in this project.",
QString(),
"https://github.com/edisionnano/"
"Screenshare-with-audio-on-Discord-with-Linux");
aboutData.addCredit(
"Curve", "For creating the Rohrkabel library used in this project.",
QString(), "https://github.com/Curve");
aboutData.addComponent("Rohrkabel", "A C++ RAII Pipewire-API Wrapper", "1.3",
"https://github.com/Soundux/rohrkabel");
m_helpMenu = new KHelpMenu(parent, aboutData);
#ifdef KGLOBALACCEL
injectScriptText("kglobalaccel.js",
"window.discordScreenaudioKGLOBALACCEL = true;");
auto toggleMuteAction = new QAction(this);
toggleMuteAction->setText("Toggle Mute");
toggleMuteAction->setIcon(QIcon::fromTheme("microphone-sensitivity-muted"));
connect(toggleMuteAction, &QAction::triggered, this,
&DiscordPage::toggleMute);
auto toggleDeafenAction = new QAction(this);
toggleDeafenAction->setText("Toggle Deafen");
toggleDeafenAction->setIcon(QIcon::fromTheme("audio-volume-muted"));
connect(toggleDeafenAction, &QAction::triggered, this,
&DiscordPage::toggleDeafen);
m_actionCollection = new KActionCollection(this);
m_actionCollection->addAction("toggleMute", toggleMuteAction);
KGlobalAccel::setGlobalShortcut(toggleMuteAction, QList<QKeySequence>{});
m_actionCollection->addAction("toggleDeafen", toggleDeafenAction);
KGlobalAccel::setGlobalShortcut(toggleDeafenAction, QList<QKeySequence>{});
m_shortcutsDialog = new KShortcutsDialog(KShortcutsEditor::GlobalAction);
m_shortcutsDialog->addCollection(m_actionCollection);
#endif
#endif
connect(&m_streamDialog, &StreamDialog::requestedStreamStart, this,
&DiscordPage::startStream);
} }
void DiscordPage::setupUserStyles() { void DiscordPage::injectScriptText(QString name, QString content) {
qDebug(userstylesLog).noquote()
<< "Looking for userstyles in" << m_configLocation.absolutePath();
m_userStylesFile =
new QFile(m_configLocation.absoluteFilePath("userstyles.css"));
if (m_userStylesFile->exists()) {
qDebug(userstylesLog).noquote()
<< "Found userstyles:" << m_userStylesFile->fileName();
m_userStylesFile->open(QIODevice::ReadOnly);
m_userStylesContent = m_userStylesFile->readAll();
m_userStylesFile->close();
fetchUserStyles();
}
connect(&m_userScript, &UserScript::shouldInstallUserStyles, this,
&DiscordPage::getUserStyles);
}
const QRegularExpression importRegex(
R"r(@import (?:url\(|)['"]{0,1}(?!.*usrbgs?\.css)([^'"]+?)['"]{0,1}(?:|\));)r");
const QRegularExpression urlRegex(
R"r(url\(['"]{0,1}((?!https:\/\/fonts.gstatic.com)(?!data:)(?!.*\.woff2)(?!.*\.ttf)[^'"]+?)['"]{0,1}\))r");
void DiscordPage::fetchUserStyles() {
m_userScript.setProperty(
"loadingMessage", "Loading userstyles: Fetching additional resources...");
bool foundImport = true;
auto match = importRegex.match(m_userStylesContent);
if (!match.hasMatch()) {
foundImport = false;
match = urlRegex.match(m_userStylesContent);
}
if (match.hasMatch()) {
auto url = match.captured(1);
qDebug(userstylesLog) << "Fetching" << url;
m_userScript.setProperty(
"loadingMessage",
QString("Loading userstyles: Fetching %1...").arg(url));
QNetworkRequest request(url);
auto reply = m_networkAccessManager.get(request);
connect(reply, &QNetworkReply::finished, [=]() {
QByteArray content = "";
if (reply->error() == QNetworkReply::NoError) {
if (!reply->attribute(QNetworkRequest::RedirectionTargetAttribute)
.isNull())
content =
reply->attribute(QNetworkRequest::RedirectionTargetAttribute)
.toByteArray();
else
content = reply->readAll();
} else
qDebug(userstylesLog) << reply->errorString().toUtf8().constData();
reply->deleteLater();
m_userStylesContent = m_userStylesContent.replace(
match.captured(0), foundImport
? content
: "url(data:application/octet-stream;base64," +
content.toBase64() + ")");
fetchUserStyles();
});
return;
}
qDebug(userstylesLog) << "Injecting userstyles";
m_userScript.setProperty("userstyles", m_userStylesContent);
m_userScript.setProperty("loadingMessage", "");
if (!m_configLocation.exists())
m_configLocation.mkpath(".");
m_userStylesFile->open(QIODevice::WriteOnly);
m_userStylesFile->write(m_userStylesContent.toUtf8());
m_userStylesFile->close();
}
void DiscordPage::getUserStyles(QString url) {
m_userStylesContent = url == "" ? "" : QString("@import url(%1);").arg(url);
fetchUserStyles();
}
void DiscordPage::injectScript(
QString name, QString content,
QWebEngineScript::InjectionPoint injectionPoint) {
qDebug(mainLog) << "Injecting " << name; qDebug(mainLog) << "Injecting " << name;
QWebEngineScript script; QWebEngineScript script;
@ -150,36 +122,20 @@ void DiscordPage::injectScript(
script.setSourceCode(content); script.setSourceCode(content);
script.setName(name); script.setName(name);
script.setWorldId(QWebEngineScript::MainWorld); script.setWorldId(QWebEngineScript::MainWorld);
script.setInjectionPoint(injectionPoint); script.setInjectionPoint(QWebEngineScript::DocumentCreation);
script.setRunsOnSubFrames(false); script.setRunsOnSubFrames(false);
scripts().insert(script); scripts().insert(script);
} }
void DiscordPage::injectScript(QString name, QString content) { void DiscordPage::injectScriptFile(QString name, QString source) {
injectScript(name, content, QWebEngineScript::DocumentCreation);
}
void DiscordPage::injectStylesheet(QString name, QString content) {
auto script = QString(R"(const stylesheet = document.createElement("style");
stylesheet.id = "%1";
stylesheet.innerText = `%2`;
document.head.appendChild(stylesheet);
)")
.arg(name)
.arg(content);
injectScript(name, script, QWebEngineScript::DocumentReady);
}
void DiscordPage::injectFile(void (DiscordPage::*inject)(QString, QString),
QString name, QString source) {
QFile file(source); QFile file(source);
if (!file.open(QIODevice::ReadOnly)) { if (!file.open(QIODevice::ReadOnly)) {
qFatal("Failed to load %s with error: %s", source.toLatin1().constData(), qFatal("Failed to load %s with error: %s", source.toLatin1().constData(),
file.errorString().toLatin1().constData()); file.errorString().toLatin1().constData());
} else { } else {
(this->*inject)(name, file.readAll()); injectScriptText(name, file.readAll());
} }
} }
@ -190,10 +146,11 @@ void DiscordPage::featurePermissionRequested(const QUrl &securityOrigin,
QWebEnginePage::PermissionGrantedByUser); QWebEnginePage::PermissionGrantedByUser);
if (feature == QWebEnginePage::Feature::MediaAudioCapture) { if (feature == QWebEnginePage::Feature::MediaAudioCapture) {
if (!m_userScript.isVirtmicRunning()) { if (m_virtmicProcess.state() == QProcess::NotRunning) {
qDebug(virtmicLog) << "Starting Virtmic with no target to make sure " qDebug(virtmicLog) << "Starting Virtmic with no target to make sure "
"Discord can find all the audio devices"; "Discord can find all the audio devices";
m_userScript.startVirtmic("None"); m_virtmicProcess.start(QApplication::arguments()[0],
{"--virtmic", "None"});
} }
} }
} }
@ -220,102 +177,78 @@ QWebEnginePage *DiscordPage::createWindow(QWebEnginePage::WebWindowType type) {
return new ExternalPage; return new ExternalPage;
} }
const QMap<QString, QString> cssAnsiColorMap = {{"black", "30"}, void DiscordPage::stopVirtmic() {
{"red", "31"}, if (m_virtmicProcess.state() == QProcess::Running) {
{"green", "32"}, qDebug(virtmicLog) << "Stopping Virtmic";
{"yellow", "33"}, m_virtmicProcess.kill();
{"blue", "34"}, m_virtmicProcess.waitForFinished();
{"magenta", "35"}, }
{"cyan", "36"}, }
{"white", "37"},
{"gray", "90"}, void DiscordPage::startVirtmic(QString target) {
{"bright-red", "91"}, if (target != "None") {
{"bright-green", "92"}, qDebug(virtmicLog) << "Starting Virtmic with target" << target;
{"bright-yellow", "93"}, m_virtmicProcess.start(QApplication::arguments()[0], {"--virtmic", target});
{"bright-blue", "94"}, }
{"bright-magenta", "95"}, }
{"bright-cyan", "96"},
{"bright-white", "97"},
{"orange", "38;5;208"},
{"pink", "38;5;205"},
{"brown", "38;5;94"},
{"light-gray", "38;5;251"},
{"dark-gray", "38;5;239"},
{"light-red", "38;5;203"},
{"light-green", "38;5;83"},
{"light-yellow", "38;5;227"},
{"light-blue", "38;5;75"},
{"light-magenta", "38;5;207"},
{"light-cyan", "38;5;87"},
{"turquoise", "38;5;80"},
{"violet", "38;5;92"},
{"purple", "38;5;127"},
{"lavender", "38;5;183"},
{"maroon", "38;5;124"},
{"beige", "38;5;230"},
{"olive", "38;5;142"},
{"indigo", "38;5;54"},
{"teal", "38;5;30"},
{"gold", "38;5;220"},
{"silver", "38;5;7"},
{"navy", "38;5;17"},
{"steel", "38;5;188"},
{"salmon", "38;5;173"},
{"peach", "38;5;217"},
{"khaki", "38;5;179"},
{"coral", "38;5;209"},
{"crimson", "38;5;160"}};
void DiscordPage::javaScriptConsoleMessage( void DiscordPage::javaScriptConsoleMessage(
QWebEnginePage::JavaScriptConsoleMessageLevel level, const QString &message, QWebEnginePage::JavaScriptConsoleMessageLevel level, const QString &message,
int lineNumber, const QString &sourceID) { int lineNumber, const QString &sourceID) {
auto colorSegments = message.split("%c"); if (message == "!discord-screenaudio-start-stream") {
if (colorSegments[0] != "") { if (m_streamDialog.isHidden())
for (auto line : colorSegments[0].split("\n")) m_streamDialog.setHidden(false);
qDebug(discordLog) << line.toUtf8().constData(); else
} m_streamDialog.activateWindow();
for (auto segment : colorSegments.mid(1)) { m_streamDialog.updateTargets();
auto lines = segment.split("\n"); } else if (message == "!discord-screenaudio-stream-stopped") {
QString ansi; stopVirtmic();
uint endOfStyles = lines.length(); } else if (message == "!discord-screenaudio-about") {
for (auto line = 1; line < lines.length(); line++) { #ifdef KXMLGUI
if (!lines[line].endsWith(";")) { m_helpMenu->aboutApplication();
endOfStyles = line; #endif
break; } else if (message == "!discord-screenaudio-keybinds") {
} #ifdef KXMLGUI
if (lines[line] == "font-weight: bold;") #ifdef KGLOBALACCEL
ansi += "\033[1m"; m_shortcutsDialog->show();
else if (lines[line].startsWith("color: ")) { #else
auto color = lines[line].mid(7).chopped(1); QMessageBox::information(MainWindow::instance(), "discord-screenaudio",
if (cssAnsiColorMap.find(color) != cssAnsiColorMap.end()) "Keybinds are not supported on this platform "
ansi += "\033[" + cssAnsiColorMap[color] + "m"; "(KGlobalAccel is not available).",
} QMessageBox::Ok);
} #endif
if (endOfStyles < lines.length()) #else
qDebug(discordLog) << (ansi + lines[0].trimmed() + "\033[0m " + QMessageBox::information(MainWindow::instance(), "discord-screenaudio",
lines[endOfStyles].trimmed()) "Keybinds are not supported on this platform "
.toUtf8() "(KXmlGui and KGlobalAccel are not available).",
.constData(); QMessageBox::Ok);
for (auto line : lines.mid(endOfStyles + 1)) { #endif
qDebug(discordLog) << line.toUtf8().constData(); } else if (message.startsWith("dsa: ")) {
} qDebug(userscriptLog) << message.mid(5).toUtf8().constData();
} else {
qDebug(discordLog) << message;
} }
} }
UserScript *DiscordPage::userScript() { return &m_userScript; } void DiscordPage::startStream(QString target, uint width, uint height,
uint frameRate) {
void DiscordPage::setupArrpc() { stopVirtmic();
QFile nodejs("/usr/bin/node"); startVirtmic(target);
if (nodejs.exists()) { // Wait a bit for the virtmic to start
auto arrpcSource = QTemporaryFile::createNativeFile(":/assets/arrpc.js"); QTimer::singleShot(target == "None" ? 0 : 200, [=]() {
qDebug(mainLog).noquote() runJavaScript(QString("window.discordScreenaudioStartStream(%1, %2, %3);")
<< "NodeJS found, starting arRPC located at" << arrpcSource->fileName(); .arg(width)
m_arrpcProcess.setProcessChannelMode(QProcess::ForwardedChannels); .arg(height)
m_arrpcProcess.setProgram(nodejs.fileName()); .arg(frameRate));
m_arrpcProcess.setArguments(QStringList{arrpcSource->fileName()}); });
m_arrpcProcess.start(); }
injectFile(&DiscordPage::injectScript, "arrpc_bridge_mod.js", void DiscordPage::toggleMute() {
":/assets/arrpc_bridge_mod.js"); qDebug(shortcutLog) << "Toggling mute";
} runJavaScript("window.discordScreenaudioToggleMute();");
}
void DiscordPage::toggleDeafen() {
qDebug(shortcutLog) << "Toggling deafen";
runJavaScript("window.discordScreenaudioToggleDeafen();");
} }

View File

@ -1,35 +1,34 @@
#pragma once #pragma once
#include "userscript.h" #include "streamdialog.h"
#include "virtmic.h"
#ifdef KXMLGUI
#include <KActionCollection>
#include <KHelpMenu>
#include <KShortcutsDialog>
#endif
#include <QDir>
#include <QFile>
#include <QNetworkAccessManager>
#include <QProcess> #include <QProcess>
#include <QStandardPaths>
#include <QWebEngineFullScreenRequest> #include <QWebEngineFullScreenRequest>
#include <QWebEnginePage> #include <QWebEnginePage>
#include <QWebEngineScript>
class DiscordPage : public QWebEnginePage { class DiscordPage : public QWebEnginePage {
Q_OBJECT Q_OBJECT
public: public:
explicit DiscordPage(QWidget *parent = nullptr); explicit DiscordPage(QWidget *parent = nullptr);
UserScript *userScript();
private: private:
UserScript m_userScript; StreamDialog m_streamDialog;
QFile *m_userStylesFile; QProcess m_virtmicProcess;
QString m_userStylesContent; #ifdef KXMLGUI
QNetworkAccessManager m_networkAccessManager; KHelpMenu *m_helpMenu;
QProcess m_arrpcProcess; #ifdef KGLOBALACCEL
const QDir m_configLocation = KActionCollection *m_actionCollection;
QStandardPaths::writableLocation(QStandardPaths::AppConfigLocation); KShortcutsDialog *m_shortcutsDialog;
void setupPermissions(); #endif
void setupUserStyles(); #endif
void setupArrpc();
void fetchUserStyles();
bool acceptNavigationRequest(const QUrl &url, bool acceptNavigationRequest(const QUrl &url,
QWebEnginePage::NavigationType type, QWebEnginePage::NavigationType type,
bool isMainFrame) override; bool isMainFrame) override;
@ -38,19 +37,17 @@ private:
javaScriptConsoleMessage(QWebEnginePage::JavaScriptConsoleMessageLevel level, javaScriptConsoleMessage(QWebEnginePage::JavaScriptConsoleMessageLevel level,
const QString &message, int lineNumber, const QString &message, int lineNumber,
const QString &sourceID) override; const QString &sourceID) override;
void injectScript(QString name, QString content, void injectScriptText(QString name, QString content);
QWebEngineScript::InjectionPoint injectionPoint); void injectScriptFile(QString name, QString source);
void injectScript(QString name, QString content); void stopVirtmic();
void injectStylesheet(QString name, QString content); void startVirtmic(QString target);
void injectFile(void (DiscordPage::*inject)(QString, QString), QString name, void toggleMute();
QString source); void toggleDeafen();
private Q_SLOTS: private Q_SLOTS:
void featurePermissionRequested(const QUrl &securityOrigin, void featurePermissionRequested(const QUrl &securityOrigin,
QWebEnginePage::Feature feature); QWebEnginePage::Feature feature);
void startStream(QString target, uint width, uint height, uint frameRate);
public Q_SLOTS:
void getUserStyles(QString url);
}; };
// Will immediately get destroyed again but is needed for navigation to // Will immediately get destroyed again but is needed for navigation to

View File

@ -1,22 +0,0 @@
#include "localserver.h"
bool isProgramRunning(const QString &program_name) {
QLocalSocket socket;
socket.connectToServer(program_name);
if (socket.waitForConnected()) {
return true; // program is already running
}
return false;
}
void showErrorMessage(const char *text) {
QMessageBox msgBox;
msgBox.setIcon(QMessageBox::Critical);
msgBox.setText(text);
msgBox.setStandardButtons(QMessageBox::Ok);
msgBox.setDefaultButton(QMessageBox::Ok);
msgBox.setWindowIcon(QIcon(":assets/de.shorsh.discord-screenaudio.png"));
msgBox.exec();
}

View File

@ -1,9 +0,0 @@
#pragma once
#include "mainwindow.h"
#include <QLocalServer>
#include <QLocalSocket>
#include <QMessageBox>
bool isProgramRunning(const QString &program_name);
void showErrorMessage(const char *text);

View File

@ -5,4 +5,3 @@ Q_LOGGING_CATEGORY(discordLog, "discord");
Q_LOGGING_CATEGORY(userscriptLog, "userscript"); Q_LOGGING_CATEGORY(userscriptLog, "userscript");
Q_LOGGING_CATEGORY(virtmicLog, "virtmic"); Q_LOGGING_CATEGORY(virtmicLog, "virtmic");
Q_LOGGING_CATEGORY(shortcutLog, "shortcut"); Q_LOGGING_CATEGORY(shortcutLog, "shortcut");
Q_LOGGING_CATEGORY(userstylesLog, "userstyles");

View File

@ -7,4 +7,3 @@ Q_DECLARE_LOGGING_CATEGORY(discordLog);
Q_DECLARE_LOGGING_CATEGORY(userscriptLog); Q_DECLARE_LOGGING_CATEGORY(userscriptLog);
Q_DECLARE_LOGGING_CATEGORY(virtmicLog); Q_DECLARE_LOGGING_CATEGORY(virtmicLog);
Q_DECLARE_LOGGING_CATEGORY(shortcutLog); Q_DECLARE_LOGGING_CATEGORY(shortcutLog);
Q_DECLARE_LOGGING_CATEGORY(userstylesLog);

View File

@ -1,4 +1,3 @@
#include "localserver.h"
#include "mainwindow.h" #include "mainwindow.h"
#include "virtmic.h" #include "virtmic.h"
@ -8,19 +7,15 @@
#include <QApplication> #include <QApplication>
#include <QCommandLineParser> #include <QCommandLineParser>
#include <QLocalServer>
#include <QLocalSocket>
#include <QLoggingCategory> #include <QLoggingCategory>
#include <QMessageBox>
int main(int argc, char *argv[]) { int main(int argc, char *argv[]) {
QApplication app(argc, argv); QApplication app(argc, argv);
QApplication::setApplicationName("discord-screenaudio");
QApplication::setApplicationName("discord-awesomeaudio");
QApplication::setWindowIcon( QApplication::setWindowIcon(
QIcon(":assets/de.shorsh.discord-screenaudio.png")); QIcon(":assets/de.shorsh.discord-screenaudio.png"));
QApplication::setApplicationVersion(DISCORD_SCEENAUDIO_VERSION_FULL); QApplication::setApplicationVersion(DISCORD_SCEENAUDIO_VERSION_FULL);
QApplication::setDesktopFileName("lol.deadzone.discord-awesomeaudio"); QApplication::setDesktopFileName("de.shorsh.discord-screenaudio");
qSetMessagePattern("[%{category}] %{message}"); qSetMessagePattern("[%{category}] %{message}");
@ -35,9 +30,6 @@ int main(int argc, char *argv[]) {
QCommandLineOption degubOption("remote-debugging", QCommandLineOption degubOption("remote-debugging",
"Open Chromium Remote Debugging on port 9222"); "Open Chromium Remote Debugging on port 9222");
parser.addOption(degubOption); parser.addOption(degubOption);
QCommandLineOption notifySendOption(
"notify-send", "Use notify-send instead of QT/KF5 notifications");
parser.addOption(notifySendOption);
parser.process(app); parser.process(app);
@ -46,6 +38,8 @@ int main(int argc, char *argv[]) {
} }
qputenv("QTWEBENGINE_CHROMIUM_FLAGS", qputenv("QTWEBENGINE_CHROMIUM_FLAGS",
"--ignore-gpu-blacklist --enable-gpu-rasterization "
"--enable-native-gpu-memory-buffers --num-raster-threads=4 "
"--enable-features=WebRTCPipeWireCapturer " + "--enable-features=WebRTCPipeWireCapturer " +
qgetenv("QTWEBENGINE_CHROMIUM_FLAGS")); qgetenv("QTWEBENGINE_CHROMIUM_FLAGS"));
@ -54,21 +48,7 @@ int main(int argc, char *argv[]) {
"--remote-debugging-port=9222 " + "--remote-debugging-port=9222 " +
qgetenv("QTWEBENGINE_CHROMIUM_FLAGS")); qgetenv("QTWEBENGINE_CHROMIUM_FLAGS"));
MainWindow w(parser.isSet(notifySendOption)); MainWindow w;
// Check if discord is already running
QString program_name = "discord-awesomeaudio";
if (isProgramRunning(program_name)) {
// if running show error message
showErrorMessage("discord-awesomeaudio is already running");
return 1;
}
// open server so we can check if discord is running
QLocalServer server;
server.listen(program_name);
QObject::connect(&server, &QLocalServer::newConnection, []() {});
w.show(); w.show();
return app.exec(); return app.exec();

View File

@ -12,58 +12,52 @@
#include <QGridLayout> #include <QGridLayout>
#include <QLabel> #include <QLabel>
#include <QPushButton> #include <QPushButton>
#include <QShortcut>
#include <QSpacerItem> #include <QSpacerItem>
#include <QThread> #include <QThread>
#include <QTimer>
#include <QUrl> #include <QUrl>
#include <QWebEngineFullScreenRequest> #include <QWebEngineNotification>
#include <QWebEngineProfile> #include <QWebEngineProfile>
#include <QWebEngineUrlRequestInterceptor> #include <QWebEngineScript>
#include <QWebEngineScriptCollection>
#include <QWebEngineSettings>
#include <QWidget> #include <QWidget>
// Custom network interceptor class
class NetworkInterceptor : public QWebEngineUrlRequestInterceptor {
public:
NetworkInterceptor(QObject* parent = nullptr) : QWebEngineUrlRequestInterceptor(parent) {}
void interceptRequest(QWebEngineUrlRequestInfo& info) override {
// Modify the request headers here as needed
info.setHttpHeader("Access-Control-Allow-Origin", "*");
info.setHttpHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
}
};
MainWindow *MainWindow::m_instance = nullptr; MainWindow *MainWindow::m_instance = nullptr;
MainWindow::MainWindow(bool useNotifySend, QWidget *parent) MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) {
: QMainWindow(parent) {
assert(MainWindow::m_instance == nullptr); assert(MainWindow::m_instance == nullptr);
MainWindow::m_instance = this; MainWindow::m_instance = this;
setupSettings(); setupWebView();
m_settings->setValue("useNotifySend", useNotifySend);
m_centralWidget = new CentralWidget(this);
setCentralWidget(m_centralWidget);
// Create and install the network interceptor
NetworkInterceptor* networkInterceptor = new NetworkInterceptor(this);
QWebEngineProfile::defaultProfile()->setRequestInterceptor(networkInterceptor);
setupTrayIcon();
setMinimumSize(800, 300);
connect(new QShortcut(QKeySequence(Qt::CTRL + Qt::Key_Q), this),
&QShortcut::activated, this, &MainWindow::toggleOrCloseWindow);
if (m_settings->contains("geometry")) {
restoreGeometry(m_settings->value("geometry").toByteArray());
} else {
resize(1000, 700); resize(1000, 700);
showMaximized(); showMaximized();
} }
if (m_settings->value("trayIcon", false).toBool() &&
m_settings->value("startHidden", false).toBool()) { void MainWindow::setupWebView() {
hide(); auto page = new DiscordPage(this);
QTimer::singleShot(0, [=]() { hide(); }); connect(page, &QWebEnginePage::fullScreenRequested, this,
} &MainWindow::fullScreenRequested);
m_webView = new QWebEngineView(this);
m_webView->setPage(page);
#ifdef KNOTIFICATIONS
QWebEngineProfile::defaultProfile()->setNotificationPresenter(
[&](std::unique_ptr<QWebEngineNotification> notificationInfo) {
KNotification *notification = new KNotification("discordNotification");
notification->setTitle(notificationInfo->title());
notification->setText(notificationInfo->message());
notification->setPixmap(QPixmap::fromImage(notificationInfo->icon()));
notification->setDefaultAction("View");
connect(notification, &KNotification::defaultActivated,
[&, notificationInfo = std::move(notificationInfo)]() {
notificationInfo->click();
activateWindow();
});
notification->sendEvent();
});
#endif
setCentralWidget(m_webView);
} }
void MainWindow::fullScreenRequested( void MainWindow::fullScreenRequested(
@ -71,95 +65,12 @@ void MainWindow::fullScreenRequested(
fullScreenRequest.accept(); fullScreenRequest.accept();
if (fullScreenRequest.toggleOn()) { if (fullScreenRequest.toggleOn()) {
m_wasMaximized = isMaximized(); m_wasMaximized = isMaximized();
if (!m_wasMaximized) {
showNormal();
}
showFullScreen(); showFullScreen();
} else { } else {
m_wasMaximized ? showMaximized() : showNormal(); m_wasMaximized ? showMaximized() : showNormal();
} }
} }
void MainWindow::setupTrayIcon() { void MainWindow::closeEvent(QCloseEvent *event) { QApplication::quit(); }
if (m_settings->value("trayIcon", false).toBool() == false ||
m_trayIcon != nullptr)
return;
auto aboutAction = new QAction(
"discord-awesomeaudio v" + QString(DISCORD_SCEENAUDIO_VERSION_FULL), this);
aboutAction->setIcon(QIcon(":assets/de.shorsh.discord-screenaudio.png"));
aboutAction->setEnabled(false);
auto exitAction = new QAction("Exit", this);
connect(exitAction, &QAction::triggered, []() { QApplication::quit(); });
m_trayIconMenu = new QMenu(this);
m_trayIconMenu->addAction(aboutAction);
m_trayIconMenu->addAction(exitAction);
m_trayIcon = new QSystemTrayIcon(this);
m_trayIcon->setContextMenu(m_trayIconMenu);
m_trayIcon->setIcon(QIcon(":assets/de.shorsh.discord-screenaudio.png"));
m_trayIcon->show();
connect(m_trayIcon, &QSystemTrayIcon::activated, [this](auto reason) {
if (reason == QSystemTrayIcon::Trigger) {
toggleOrCloseWindow();
}
});
}
void MainWindow::cleanTrayIcon() {
if (m_trayIcon == nullptr)
return;
m_trayIcon->hide();
m_trayIconMenu->deleteLater();
m_trayIcon->deleteLater();
m_trayIconMenu = nullptr;
m_trayIcon = nullptr;
}
void MainWindow::setupSettings() {
m_settings =
new QSettings("discord-awesomeaudio", "discord-awesomeaudio", this);
m_settings->beginGroup("settings");
m_settings->endGroup();
}
QSettings *MainWindow::settings() const { return m_settings; }
void MainWindow::setTrayIcon(bool enabled) {
m_settings->setValue("trayIcon", enabled);
if (enabled) {
setupTrayIcon();
} else {
cleanTrayIcon();
}
}
void MainWindow::closeEvent(QCloseEvent *event) {
if (m_settings->value("trayIcon", false).toBool()) {
hide();
} else {
m_settings->setValue("geometry", saveGeometry());
QApplication::quit();
}
}
MainWindow *MainWindow::instance() { return m_instance; } MainWindow *MainWindow::instance() { return m_instance; }
CentralWidget *MainWindow::centralWidget() {
return instance()->m_centralWidget;
};
void MainWindow::toggleOrCloseWindow() {
if (isVisible()) {
if (m_trayIcon == nullptr)
QApplication::quit();
else
hide();
} else {
show();
activateWindow();
}
}

View File

@ -1,40 +1,31 @@
#pragma once #pragma once
#include "centralwidget.h" #include "discordpage.h"
#include <QMainWindow> #include <QMainWindow>
#include <QMenu>
#include <QScopedPointer> #include <QScopedPointer>
#include <QSettings>
#include <QString> #include <QString>
#include <QSystemTrayIcon>
#include <QVBoxLayout>
#include <QVector> #include <QVector>
#include <QWebEngineUrlRequestInterceptor> #include <QWebEnginePage>
#include <QWebEngineProfile>
#include <QWebEngineView>
class MainWindow : public QMainWindow { class MainWindow : public QMainWindow {
Q_OBJECT Q_OBJECT
public: public:
explicit MainWindow(bool useNotifySend = false, QWidget *parent = nullptr); explicit MainWindow(QWidget *parent = nullptr);
static MainWindow *instance(); static MainWindow *instance();
QSettings *settings() const;
static CentralWidget *centralWidget();
private: private:
void setupTrayIcon(); void setupWebView();
void cleanTrayIcon(); QWebEngineView *m_webView;
void setupSettings(); QWebEngineProfile *prepareProfile();
DiscordPage *m_discordPage;
void closeEvent(QCloseEvent *event) override; void closeEvent(QCloseEvent *event) override;
QSystemTrayIcon *m_trayIcon = nullptr;
QMenu *m_trayIconMenu;
QSettings *m_settings;
bool m_wasMaximized; bool m_wasMaximized;
static MainWindow *m_instance; static MainWindow *m_instance;
CentralWidget *m_centralWidget;
public Q_SLOTS: private Q_SLOTS:
void setTrayIcon(bool enabled);
void fullScreenRequested(QWebEngineFullScreenRequest fullScreenRequest); void fullScreenRequested(QWebEngineFullScreenRequest fullScreenRequest);
void toggleOrCloseWindow();
}; };

View File

@ -9,95 +9,73 @@
#include <QSizePolicy> #include <QSizePolicy>
#include <QVBoxLayout> #include <QVBoxLayout>
StreamDialog::StreamDialog(QWidget *parent) : QDialog(parent) { StreamDialog::StreamDialog() : QWidget() {
setAttribute(Qt::WA_QuitOnClose, false); setAttribute(Qt::WA_QuitOnClose, false);
{
auto layout = new QVBoxLayout(this); auto layout = new QVBoxLayout(this);
layout->setSizeConstraint(QLayout::SetFixedSize); layout->setSizeConstraint(QLayout::SetFixedSize);
m_videoGroupBox = new QGroupBox(this);
m_videoGroupBox->setTitle("Video");
m_videoGroupBox->setCheckable(true);
layout->addWidget(m_videoGroupBox);
{
auto videoLayout = new QVBoxLayout(m_videoGroupBox);
auto resolutionLabel = new QLabel(this);
resolutionLabel->setText("Resolution");
videoLayout->addWidget(resolutionLabel);
m_resolutionComboBox = new QComboBox(this);
m_resolutionComboBox->addItem("2160p", "3840x2160");
m_resolutionComboBox->addItem("1440p", "2560x1440");
m_resolutionComboBox->addItem("1080p", "1920x1080");
m_resolutionComboBox->addItem("720p", "1280x720");
m_resolutionComboBox->addItem("480p", "854x480");
m_resolutionComboBox->addItem("360p", "640x360");
m_resolutionComboBox->addItem("240p", "426x240");
m_resolutionComboBox->setCurrentText("720p");
videoLayout->addWidget(m_resolutionComboBox);
auto framerateLabel = new QLabel(this);
framerateLabel->setText("Framerate");
videoLayout->addWidget(framerateLabel);
m_framerateComboBox = new QComboBox(this);
m_framerateComboBox->addItem("144 FPS", 144);
m_framerateComboBox->addItem("60 FPS", 60);
m_framerateComboBox->addItem("30 FPS", 30);
m_framerateComboBox->addItem("15 FPS", 15);
m_framerateComboBox->addItem("5 FPS", 5);
m_framerateComboBox->setCurrentText("30 FPS");
videoLayout->addWidget(m_framerateComboBox);
}
m_audioGroupBox = new QGroupBox(this);
m_audioGroupBox->setCheckable(true);
m_audioGroupBox->setTitle("Audio");
layout->addWidget(m_audioGroupBox);
{
auto audioLayout = new QVBoxLayout(m_audioGroupBox);
auto targetLabel = new QLabel(this); auto targetLabel = new QLabel(this);
targetLabel->setText("Audio Source"); targetLabel->setText("Which app do you want to stream sound from?");
audioLayout->addWidget(targetLabel); layout->addWidget(targetLabel);
{ auto targetHBox = new QHBoxLayout(this);
auto targetLayout = new QHBoxLayout(); layout->addLayout(targetHBox);
audioLayout->addLayout(targetLayout);
m_targetComboBox = new QComboBox(this); m_targetComboBox = new QComboBox(this);
updateTargets(); updateTargets();
targetLayout->addWidget(m_targetComboBox); targetHBox->addWidget(m_targetComboBox);
auto refreshTargetsButton = new QPushButton(this); auto refreshTargetsButton = new QPushButton(this);
refreshTargetsButton->setFixedSize(30, 30); refreshTargetsButton->setFixedSize(30, 30);
refreshTargetsButton->setIcon(QIcon::fromTheme("view-refresh")); refreshTargetsButton->setIcon(QIcon::fromTheme("view-refresh"));
connect(refreshTargetsButton, &QPushButton::clicked, this, connect(refreshTargetsButton, &QPushButton::clicked, this,
&StreamDialog::updateTargets); &StreamDialog::updateTargets);
targetLayout->addWidget(refreshTargetsButton); targetHBox->addWidget(refreshTargetsButton);
}
} auto qualityLabel = new QLabel(this);
qualityLabel->setText("Stream Quality");
layout->addWidget(qualityLabel);
auto qualityHBox = new QHBoxLayout(this);
layout->addLayout(qualityHBox);
m_qualityResolutionComboBox = new QComboBox(this);
m_qualityResolutionComboBox->addItem("2160p", "3840x2160");
m_qualityResolutionComboBox->addItem("1440p", "2560x1440");
m_qualityResolutionComboBox->addItem("1080p", "1920x1080");
m_qualityResolutionComboBox->addItem("720p", "1280x720");
m_qualityResolutionComboBox->addItem("480p", "854x480");
m_qualityResolutionComboBox->addItem("360p", "640x360");
m_qualityResolutionComboBox->addItem("240p", "426x240");
m_qualityResolutionComboBox->setCurrentText("720p");
qualityHBox->addWidget(m_qualityResolutionComboBox);
m_qualityFPSComboBox = new QComboBox(this);
m_qualityFPSComboBox->addItem("144 FPS", 144);
m_qualityFPSComboBox->addItem("60 FPS", 60);
m_qualityFPSComboBox->addItem("30 FPS", 30);
m_qualityFPSComboBox->addItem("15 FPS", 15);
m_qualityFPSComboBox->addItem("5 FPS", 5);
m_qualityFPSComboBox->setCurrentText("30 FPS");
qualityHBox->addWidget(m_qualityFPSComboBox);
auto button = new QPushButton(this); auto button = new QPushButton(this);
button->setText("Start Stream"); button->setText("Start Stream");
connect(button, &QPushButton::clicked, this, &StreamDialog::startStream); connect(button, &QPushButton::clicked, this, &StreamDialog::startStream);
layout->addWidget(button, Qt::AlignRight | Qt::AlignBottom); layout->addWidget(button, Qt::AlignRight | Qt::AlignBottom);
}
setLayout(layout);
setWindowTitle("discord-screenaudio Stream Dialog"); setWindowTitle("discord-screenaudio Stream Dialog");
} }
void StreamDialog::startStream() { void StreamDialog::startStream() {
auto resolution = m_resolutionComboBox->currentData().toString().split('x'); auto resolution =
emit requestedStreamStart(m_videoGroupBox->isChecked(), m_qualityResolutionComboBox->currentData().toString().split('x');
m_audioGroupBox->isChecked(), resolution[0].toInt(), emit requestedStreamStart(m_targetComboBox->currentText(),
resolution[1].toInt(), resolution[0].toUInt(), resolution[1].toUInt(),
m_framerateComboBox->currentData().toInt(), m_qualityFPSComboBox->currentData().toUInt());
m_targetComboBox->currentText());
setHidden(true); setHidden(true);
} }
@ -105,6 +83,7 @@ void StreamDialog::updateTargets() {
auto lastTarget = m_targetComboBox->currentText(); auto lastTarget = m_targetComboBox->currentText();
m_targetComboBox->clear(); m_targetComboBox->clear();
m_targetComboBox->addItem("[None]");
m_targetComboBox->addItem("[All Desktop Audio]"); m_targetComboBox->addItem("[All Desktop Audio]");
for (auto target : Virtmic::getTargets()) { for (auto target : Virtmic::getTargets()) {
m_targetComboBox->addItem(target); m_targetComboBox->addItem(target);

View File

@ -2,25 +2,22 @@
#include <QComboBox> #include <QComboBox>
#include <QDialog> #include <QDialog>
#include <QGroupBox>
#include <QWidget> #include <QWidget>
class StreamDialog : public QDialog { class StreamDialog : public QWidget {
Q_OBJECT Q_OBJECT
public: public:
explicit StreamDialog(QWidget *parent = nullptr); explicit StreamDialog();
private: private:
QComboBox *m_targetComboBox; QComboBox *m_targetComboBox;
QComboBox *m_resolutionComboBox; QComboBox *m_qualityResolutionComboBox;
QComboBox *m_framerateComboBox; QComboBox *m_qualityFPSComboBox;
QGroupBox *m_videoGroupBox;
QGroupBox *m_audioGroupBox;
Q_SIGNALS: Q_SIGNALS:
void requestedStreamStart(bool video, bool audio, int width, int height, void requestedStreamStart(QString target, uint width, uint height,
int frameRate, QString target); uint frameRate);
public Q_SLOTS: public Q_SLOTS:
void updateTargets(); void updateTargets();

View File

@ -1,248 +0,0 @@
#include "userscript.h"
#include "log.h"
#include "mainwindow.h"
#include <QApplication>
#include <QDebug>
#include <QFile>
#include <QInputDialog>
#include <QMessageBox>
#include <QStandardPaths>
#include <QTimer>
#ifdef KXMLGUI
#include <KActionCollection>
#endif
UserScript::UserScript() : QObject() {
setupHelpMenu();
setupShortcutsDialog();
setupStreamDialog();
setupVirtmic();
}
void UserScript::setupHelpMenu() {
#ifdef KXMLGUI
m_kxmlgui = true;
KAboutData aboutData(
"discord-screenaudio", "discord-screenaudio",
QApplication::applicationVersion(),
"Custom Discord client with the ability to stream audio on Linux",
KAboutLicense::GPL_V3, "Copyright 2022 (C) Malte Jürgens");
aboutData.setBugAddress("https://github.com/maltejur/discord-screenaudio");
aboutData.addAuthor("Malte Jürgens", "Author", "maltejur@dismail.de",
"https://github.com/maltejur");
aboutData.addCredit("edisionnano",
"For creating and documenting the approach for streaming "
"audio in Discord used in this project.",
QString(),
"https://github.com/edisionnano/"
"Screenshare-with-audio-on-Discord-with-Linux");
aboutData.addCredit(
"Curve", "For creating the Rohrkabel library used in this project.",
QString(), "https://github.com/Curve");
aboutData.addComponent("Rohrkabel", "A C++ RAII Pipewire-API Wrapper", "1.3",
"https://github.com/Soundux/rohrkabel");
aboutData.addComponent("arRPC",
"An open implementation of Discord's local RPC "
"servers<br>Copyright (c) 2022 OpenAsar",
nullptr, "https://github.com/OpenAsar/arrpc");
m_helpMenu = new KHelpMenu(MainWindow::instance(), aboutData);
#endif
}
void UserScript::setupShortcutsDialog() {
#ifdef KXMLGUI
#ifdef KGLOBALACCEL
m_kglobalaccel = true;
auto toggleMuteAction = new QAction(this);
toggleMuteAction->setText("Toggle Mute");
toggleMuteAction->setIcon(QIcon::fromTheme("microphone-sensitivity-muted"));
connect(toggleMuteAction, &QAction::triggered, this,
&UserScript::muteToggled);
auto toggleDeafenAction = new QAction(this);
toggleDeafenAction->setText("Toggle Deafen");
toggleDeafenAction->setIcon(QIcon::fromTheme("audio-volume-muted"));
connect(toggleDeafenAction, &QAction::triggered, this,
&UserScript::deafenToggled);
m_actionCollection = new KActionCollection(this);
m_actionCollection->addAction("toggleMute", toggleMuteAction);
KGlobalAccel::setGlobalShortcut(toggleMuteAction, QList<QKeySequence>{});
m_actionCollection->addAction("toggleDeafen", toggleDeafenAction);
KGlobalAccel::setGlobalShortcut(toggleDeafenAction, QList<QKeySequence>{});
m_shortcutsDialog = new KShortcutsDialog(KShortcutsEditor::GlobalAction);
m_shortcutsDialog->addCollection(m_actionCollection);
#endif
#endif
}
void UserScript::setupStreamDialog() {
m_streamDialog = new StreamDialog(MainWindow::instance());
connect(m_streamDialog, &StreamDialog::requestedStreamStart, this, &UserScript::startStream);
}
void UserScript::setupVirtmic() {
m_virtmicProcess.setProcessChannelMode(QProcess::ForwardedChannels);
}
bool UserScript::isVirtmicRunning() {
return m_virtmicProcess.state() != QProcess::NotRunning;
}
QString UserScript::version() { return QApplication::applicationVersion(); }
QVariant UserScript::getPref(QString name, QVariant fallback) {
return MainWindow::instance()->settings()->value(name, fallback);
}
bool UserScript::getBoolPref(QString name, bool fallback) {
return getPref(name, fallback).toBool();
}
void UserScript::setPref(QString name, QVariant value) {
return MainWindow::instance()->settings()->setValue(name, value);
}
void UserScript::setTrayIcon(bool value) {
setPref("trayIcon", value);
MainWindow::instance()->setTrayIcon(value);
}
void UserScript::log(QString message) {
qDebug(userscriptLog) << message.toUtf8().constData();
}
void UserScript::showShortcutsDialog() {
#ifdef KXMLGUI
#ifdef KGLOBALACCEL
m_shortcutsDialog->show();
#else
QMessageBox::information(MainWindow::instance(), "discord-screenaudio",
"Keybinds are not supported on this platform "
"(KGlobalAccel is not available).",
QMessageBox::Ok);
#endif
#else
QMessageBox::information(MainWindow::instance(), "discord-screenaudio",
"Keybinds are not supported on this platform "
"(KXmlGui and KGlobalAccel are not available).",
QMessageBox::Ok);
#endif
}
void UserScript::showHelpMenu() {
#ifdef KXMLGUI
m_helpMenu->aboutApplication();
#endif
}
void UserScript::stopVirtmic() {
if (m_virtmicProcess.state() == QProcess::Running) {
qDebug(virtmicLog) << "Stopping Virtmic";
m_virtmicProcess.kill();
m_virtmicProcess.waitForFinished();
}
}
void UserScript::startVirtmic(QString target) {
qDebug(virtmicLog) << "Starting Virtmic with target" << target;
m_virtmicProcess.start(QApplication::arguments()[0], {"--virtmic", target});
}
void UserScript::startStream(bool video, bool audio, int width, int height, int frameRate, QString target) {
stopVirtmic();
startVirtmic(audio ? target : "[None]");
// Wait a bit for the virtmic to start
QTimer::singleShot(200, [=]() {
emit streamStarted(video, width, height, frameRate);
});
}
void UserScript::showStreamDialog() {
if (m_streamDialog->isHidden())
m_streamDialog->setHidden(false);
else
m_streamDialog->activateWindow();
m_streamDialog->updateTargets();
}
void UserScript::showThemeDialog() {
auto url = QInputDialog::getText(MainWindow::instance(), "Theme Installation", "Please enter the URL of the Theme");
if (url != "")
emit shouldInstallUserStyles(url);
}
void UserScript::installUserStyles(QString url) {
emit shouldInstallUserStyles(url);
}
QVariant UserScript::vencordSend(QString event, QVariantList args) {
QString configFolder =
QStandardPaths::writableLocation(QStandardPaths::AppConfigLocation) +
"/vencord";
QString quickCssFile = configFolder + "/quickCss.css";
QString settingsFile = configFolder + "/settings.json";
if (!QDir().exists(configFolder))
QDir().mkpath(configFolder);
if (event == "VencordGetRepo") {
return true;
}
if (event == "VencordGetSettingsDir") {
return configFolder;
}
if (event == "VencordGetQuickCss") {
if (QFile::exists(quickCssFile)) {
QFile file(quickCssFile);
if (!file.open(QIODevice::ReadOnly))
qFatal("Failed to load %s with error: %s",
quickCssFile.toLatin1().constData(),
file.errorString().toLatin1().constData());
auto content = file.readAll();
file.close();
return QString(content);
} else
return "";
}
if (event == "VencordGetSettings") {
if (QFile::exists(settingsFile)) {
QFile file(settingsFile);
if (!file.open(QIODevice::ReadOnly))
qFatal("Failed to load %s with error: %s",
settingsFile.toLatin1().constData(),
file.errorString().toLatin1().constData());
auto content = file.readAll();
file.close();
return QString(content);
} else
return "{}";
}
if (event == "VencordSetSettings") {
QFile file(settingsFile);
if (!file.open(QIODevice::WriteOnly))
qFatal("Failed to load %s with error: %s",
settingsFile.toLatin1().constData(),
file.errorString().toLatin1().constData());
file.write(args[0].toString().toUtf8());
file.close();
return true;
}
if (event == "VencordGetUpdates") {
return QVariantMap{{"ok", true}, {"value", QVariantList()}};
}
if (event == "VencordOpenExternal") {
QDesktopServices::openUrl(QUrl(args[0].toString()));
return true;
}
if (event == "VencordOpenQuickCss") {
return true;
}
assert(false);
}

View File

@ -1,89 +0,0 @@
#pragma once
#include "streamdialog.h"
#include <QProcess>
#include <QObject>
#include <QDir>
#include <QDesktopServices>
#ifdef KXMLGUI
#include <KAboutData>
#include <KHelpMenu>
#include <KShortcutsDialog>
#include <KXmlGuiWindow>
#include <QAction>
#ifdef KGLOBALACCEL
#include <KGlobalAccel>
#endif
#endif
#ifdef KNOTIFICATIONS
#include <KNotification>
#endif
class UserScript : public QObject {
Q_OBJECT
public:
UserScript();
bool isVirtmicRunning();
Q_PROPERTY(QString version READ version CONSTANT)
Q_PROPERTY(bool kxmlgui MEMBER m_kxmlgui CONSTANT)
Q_PROPERTY(bool kglobalaccel MEMBER m_kglobalaccel CONSTANT)
Q_PROPERTY(QString userstyles MEMBER m_userstyles NOTIFY userstylesChanged)
Q_PROPERTY(QString loadingMessage MEMBER m_loadingMessage NOTIFY loadingMessageChanged)
private:
QProcess m_virtmicProcess;
StreamDialog *m_streamDialog;
bool m_kxmlgui = false;
bool m_kglobalaccel = false;
QString m_userstyles;
QString m_loadingMessage;
QString m_vencordSettings;
void setupHelpMenu();
void setupShortcutsDialog();
void setupStreamDialog();
void setupVirtmic();
#ifdef KXMLGUI
KHelpMenu *m_helpMenu;
#ifdef KGLOBALACCEL
KActionCollection *m_actionCollection;
KShortcutsDialog *m_shortcutsDialog;
#endif
#endif
Q_SIGNALS:
void muteToggled();
void deafenToggled();
void streamStarted(bool video, int width, int height, int frameRate);
void userstylesChanged();
void loadingMessageChanged(QString message);
void shouldInstallUserStyles(QString url);
public Q_SLOTS:
void log(QString message);
QString version();
QVariant getPref(QString name, QVariant fallback);
bool getBoolPref(QString name, bool fallback);
void setPref(QString name, QVariant value);
void setTrayIcon(bool value);
void showShortcutsDialog();
void showHelpMenu();
void showStreamDialog();
void stopVirtmic();
void startVirtmic(QString target);
void showThemeDialog();
void installUserStyles(QString url);
QVariant vencordSend(QString event, QVariantList args);
private Q_SLOTS:
void startStream(bool video, bool audio, int width, int height, int frameRate, QString target);
};

View File

@ -6,26 +6,7 @@
namespace Virtmic { namespace Virtmic {
const QStringList EXCLUDE_TARGETS{"Chromium input", "discord-awesomeaudio", "pavucontrol", "PulseAudio Volume Control", "OBS", "OBS-Monitor", "OBS/OBS-MONITOR"}; const QStringList EXCLUDE_TARGETS{"Chromium input", "discord-screenaudio"};
const std::string nullstr = "";
const std::string &getTarget(const pipewire::spa::dict &props) {
if (props.count("media.class") &&
props.at("media.class") == "Stream/Output/Audio") {
if (props.count("application.name") && props.at("application.name") != "")
return props.at("application.name");
else if (props.count("application.process.binary") &&
props.at("application.process.binary") != "")
return props.at("application.process.binary");
else
return props.at("node.name");
} else
return nullstr;
}
QString qGetTarget(const pipewire::spa::dict &props) {
return QString::fromStdString(getTarget(props));
}
QVector<QString> getTargets() { QVector<QString> getTargets() {
auto main_loop = pipewire::main_loop(); auto main_loop = pipewire::main_loop();
@ -41,7 +22,8 @@ QVector<QString> getTargets() {
if (global.type == pipewire::node::type) { if (global.type == pipewire::node::type) {
auto node = reg.bind<pipewire::node>(global.id); auto node = reg.bind<pipewire::node>(global.id);
auto info = node.info(); auto info = node.info();
QString name = qGetTarget(info.props); auto name = QString::fromStdString(info.props["application.name"]);
if (name != "" && !EXCLUDE_TARGETS.contains(name) && if (name != "" && !EXCLUDE_TARGETS.contains(name) &&
!targets.contains(name)) { !targets.contains(name)) {
targets.append(name); targets.append(name);
@ -85,12 +67,7 @@ void start(QString _target) {
continue; continue;
auto &parent = nodes.at(parent_id); auto &parent = nodes.at(parent_id);
std::string name = getTarget(parent.props); auto name = parent.props["application.name"];
if (EXCLUDE_TARGETS.contains(QString::fromStdString(name)))
continue;
if (parent.props.count("media.class") && (parent.props.at("media.class") == "Audio/Source" || parent.props.at("media.class") == "Audio/Source/Virtual"))
continue;
if (name == target || if (name == target ||
(target == "[All Desktop Audio]" && (target == "[All Desktop Audio]" &&
@ -101,7 +78,8 @@ void start(QString _target) {
core.create<pipewire::link_factory>( core.create<pipewire::link_factory>(
{fl ? virt_fl->info().id : virt_fr->info().id, port_id})); {fl ? virt_fl->info().id : virt_fr->info().id, port_id}));
qDebug(virtmicLog) << QString("Link: %1:%2 -> %3") qDebug(virtmicLog) << QString("Link: %1:%2 -> %3")
.arg(QString::fromStdString(name)) .arg(QString::fromStdString(
parent.props["application.name"]))
.arg(port_id) .arg(port_id)
.arg(fl ? virt_fl->info().id .arg(fl ? virt_fl->info().id
: virt_fr->info().id) : virt_fr->info().id)
@ -111,10 +89,10 @@ void start(QString _target) {
} }
}; };
std::string target = _target.toUtf8().toStdString(); std::string target = _target.toLatin1().toStdString();
auto virtual_mic = core.create("adapter", auto virtual_mic = core.create("adapter",
{{"node.name", "discord-awesomeaudio-virtmic"}, {{"node.name", "discord-screenaudio-virtmic"},
{"media.class", "Audio/Source/Virtual"}, {"media.class", "Audio/Source/Virtual"},
{"factory.name", "support.null-audio-sink"}, {"factory.name", "support.null-audio-sink"},
{"audio.channels", "2"}, {"audio.channels", "2"},
@ -134,12 +112,11 @@ void start(QString _target) {
[&](const pipewire::global &global) { [&](const pipewire::global &global) {
if (global.type == pipewire::node::type) { if (global.type == pipewire::node::type) {
auto node = reg.bind<pipewire::node>(global.id); auto node = reg.bind<pipewire::node>(global.id);
auto info = node.info(); if (!node.info().props.count("application.name"))
std::string name = getTarget(info.props);
if (name == nullstr)
return; return;
qDebug(virtmicLog) << QString("Added: %1") qDebug(virtmicLog) << QString("Added: %1")
.arg(QString::fromStdString(name)) .arg(QString::fromStdString(
node.info().props["application.name"]))
.toUtf8() .toUtf8()
.data(); .data();
@ -175,11 +152,9 @@ void start(QString _target) {
[&](const std::uint32_t id) { [&](const std::uint32_t id) {
if (nodes.count(id)) { if (nodes.count(id)) {
auto info = nodes.at(id); auto info = nodes.at(id);
std::string name = getTarget(info.props);
if (name == nullstr)
return;
qDebug(virtmicLog) << QString("Removed: %1") qDebug(virtmicLog) << QString("Removed: %1")
.arg(QString::fromStdString(name)) .arg(QString::fromStdString(
info.props["application.name"].data()))
.toUtf8() .toUtf8()
.data(); .data();
nodes.erase(id); nodes.erase(id);

@ -1 +1 @@
Subproject commit 62b2acebe6806c7b0e2ca6a43c6b2419a627b8dc Subproject commit 0d996633f201b2ab95d8b9dc376a7da2dcca315d

View File

@ -1,2 +0,0 @@
flatpak remove lol.deadzone.discord-awesomeaudio
rm ~/.local/share/applications/discord-awesomeaudio.desktop

View File

@ -1,3 +0,0 @@
git pull
./uninstall.sh
./install.sh