diff --git a/CMakeLists.txt b/CMakeLists.txt
index b664337..9feccc8 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -13,24 +13,27 @@ set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -DDEBUG")
string(TIMESTAMP TIMESTAMP %s)
# set(CMAKE_AUTOUIC ON)
-find_package(Qt5 CONFIG REQUIRED COMPONENTS
- Widgets
- WebEngineWidgets
-)
+find_package(Qt5 COMPONENTS Widgets)
+if (Qt5_FOUND)
+ find_package(Qt5 CONFIG REQUIRED COMPONENTS Widgets WebEngineWidgets)
-find_package(KF5Notifications)
-if(KF5Notifications_FOUND)
- add_definitions( -DKNOTIFICATIONS )
-endif()
+ find_package(KF5Notifications)
+ if(KF5Notifications_FOUND)
+ add_definitions( -DKNOTIFICATIONS )
+ endif()
-find_package(KF5XmlGui)
-if(KF5XmlGui_FOUND)
- add_definitions( -DKXMLGUI )
-endif()
+ find_package(KF5XmlGui)
+ if(KF5XmlGui_FOUND)
+ add_definitions( -DKXMLGUI )
+ endif()
-find_package(KF5GlobalAccel)
-if(KF5GlobalAccel_FOUND)
- add_definitions( -DKGLOBALACCEL )
+ find_package(KF5GlobalAccel)
+ if(KF5GlobalAccel_FOUND)
+ 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()
set(discord-screenaudio_SRC
@@ -40,7 +43,7 @@ set(discord-screenaudio_SRC
src/discordpage.cpp
src/streamdialog.cpp
src/log.cpp
- src/webclass.cpp
+ src/userscript.cpp
resources.qrc
)
@@ -67,7 +70,7 @@ add_subdirectory(submodules/rohrkabel)
add_executable(discord-screenaudio ${discord-screenaudio_SRC})
-target_link_libraries(discord-screenaudio Qt5::Widgets Qt5::WebEngineWidgets rohrkabel)
+target_link_libraries(discord-screenaudio Qt::Widgets Qt::WebEngineWidgets rohrkabel)
if(KF5Notifications_FOUND)
target_link_libraries(discord-screenaudio KF5::Notifications)
diff --git a/README.md b/README.md
index ee83eaa..65dbe21 100644
--- a/README.md
+++ b/README.md
@@ -11,7 +11,7 @@ Unlike a lot of other solutions, the audio here is directly fed into the
screenshare and not passed to the user microphone
([see explanation](#how-it-works)).
-![Screenshot_20220925_112945](https://user-images.githubusercontent.com/48161361/192137080-33466cf7-8c56-4373-90c6-01ea74b6fb83.png)
+![Screenshot_20221211_185028](https://user-images.githubusercontent.com/48161361/206920213-58a8091a-d8f9-4bb7-ae3d-3f8581b84d24.png)
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
@@ -50,6 +50,8 @@ You have multiple options:
### Requirements
- Basic building tools
+- An up-to-date system (I can't guarantee that it works on Debian or Ubuntu
+ 20/21)
- CMake
- Qt5 and QtWebEngine
- **PipeWire** (it currently doesn't work with PulseAudio)
@@ -57,7 +59,7 @@ You have multiple options:
- _Kf5Notifications (optional, for better notifications)_
- _KXMLGui and KGlobalAccel (optional, for keybinds)_
-On Debian:
+With apt:
`apt install -y build-essential cmake qtbase5-dev qtwebengine5-dev libkf5notifications-dev libkf5xmlgui-dev libkf5globalaccel-dev pkg-config libpipewire-0.3-dev git`
### Building
@@ -82,7 +84,9 @@ And then to optionally install it, run:
sudo cmake --install build
```
-## How it works
+## FAQ
+
+### How does this work?
This whole project is based on
[this](https://github.com/edisionnano/Screenshare-with-audio-on-Discord-with-Linux)
@@ -91,6 +95,21 @@ Discord. Basically: a virtual microphone is created which captures the
application audio, and this microphone is then fed to the Discord stream by
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`. But please note that due to
+QtWebEngine limitations concerning content security policies, you can't use any
+external files (like `@import` or `url()`).
+
## License
Copyright (C) 2022 Malte Jürgens
diff --git a/assets/de.shorsh.discord-screenaudio.metainfo.xml.in b/assets/de.shorsh.discord-screenaudio.metainfo.xml.in
index bd81780..7d459dc 100644
--- a/assets/de.shorsh.discord-screenaudio.metainfo.xml.in
+++ b/assets/de.shorsh.discord-screenaudio.metainfo.xml.in
@@ -4,6 +4,7 @@
CC0-1.0
GPL-3.0+
discord-screenaudio
+ Malte Jürgens
diff --git a/assets/userscript.js b/assets/userscript.js
index cf00169..b8605a6 100644
--- a/assets/userscript.js
+++ b/assets/userscript.js
@@ -1,5 +1,3 @@
-// From v0.4
-
navigator.mediaDevices.chromiumGetDisplayMedia =
navigator.mediaDevices.getDisplayMedia;
@@ -16,16 +14,16 @@ const getAudioDevice = async (nameOfAudioDevice) => {
let devices = await navigator.mediaDevices.enumerateDevices();
audioDevice = devices.find(({ label }) => label === nameOfAudioDevice);
if (!audioDevice)
- console.log(
- `dsa: Did not find '${nameOfAudioDevice}', trying again in 100ms`
+ userscript.log(
+ `Did not find '${nameOfAudioDevice}', trying again in 100ms`
);
await sleep(100);
}
- console.log(`dsa: Found '${nameOfAudioDevice}'`);
+ userscript.log(`Found '${nameOfAudioDevice}'`);
return audioDevice;
};
-function setGetDisplayMedia(overrideArgs = undefined) {
+function setGetDisplayMedia(video = true, overrideArgs = undefined) {
const getDisplayMedia = async (...args) => {
var id;
try {
@@ -63,6 +61,7 @@ function setGetDisplayMedia(overrideArgs = undefined) {
: args || [{ video: true, audio: true }])
);
gdm.addTrack(track);
+ if (!video) for (const track of gdm.getVideoTracks()) track.enabled = false;
return gdm;
};
navigator.mediaDevices.getDisplayMedia = getDisplayMedia;
@@ -70,137 +69,275 @@ function setGetDisplayMedia(overrideArgs = undefined) {
setGetDisplayMedia();
+let userscript;
+let muteBtn;
+let deafenBtn;
+let streamStartBtn;
+let streamStartBtnInitialDisplay;
+let streamStartBtnClone;
+let resolutionString;
const clonedElements = [];
const hiddenElements = [];
let wasStreamActive = false;
-setInterval(() => {
- const streamActive =
- document.getElementsByClassName("panel-2ZFCRb activityPanel-9icbyU")
- .length > 0;
+function createButton(text, onClick) {
+ 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;
+}
- if (!streamActive && wasStreamActive)
- console.log("!discord-screenaudio-stream-stopped");
- wasStreamActive = streamActive;
+function createSwitch(text, enabled, onClick) {
+ const container = document.createElement("div");
+ container.style.marginBottom = "20px";
+ container.className = "labelRow-NnoUIp";
- if (streamActive) {
- clonedElements.forEach((el) => {
- el.remove();
- });
- clonedElements.length = 0;
+ const label = document.createElement("label");
+ label.innerText = text;
+ label.className = "title-2yADjX";
+ container.appendChild(label);
- hiddenElements.forEach((el) => {
- el.style.display = "block";
- });
- hiddenElements.length = 0;
- } else {
- for (const el of [
- document.getElementsByClassName("actionButtons-2vEOUh")?.[0]?.children[1],
- document.querySelector(
- ".wrapper-3t3Yqv > div > div > div > div > .controlButton-2PMNom"
- ),
- ]) {
- if (!el) continue;
- if (el.classList.contains("discord-screenaudio-cloned")) continue;
- el.classList.add("discord-screenaudio-cloned");
- elClone = el.cloneNode(true);
- elClone.title = "Share Your Screen with Audio";
- elClone.addEventListener("click", () => {
- console.log("!discord-screenaudio-start-stream");
- });
+ const svg = document.createElement("div");
+ container.appendChild(svg);
- const initialDisplay = el.style.display;
-
- window.discordScreenaudioStartStream = (width, height, frameRate) => {
- window.discordScreenaudioResolutionString = `${height}p ${frameRate}FPS`;
- setGetDisplayMedia({
- audio: true,
- video: { width, height, frameRate },
- });
- el.click();
- el.style.display = initialDisplay;
- elClone.remove();
- };
-
- el.style.display = "none";
- el.parentNode.insertBefore(elClone, el);
-
- clonedElements.push(elClone);
- hiddenElements.push(el);
- }
+ function setSvgDisabled() {
+ svg.innerHTML = `
`;
}
- // Add about text in settings
- if (
- document.getElementsByClassName("dirscordScreenaudioAboutText").length == 0
- ) {
- for (const el of document.getElementsByClassName("info-3pQQBb")) {
- let aboutEl;
- if (window.discordScreenaudioKXMLGUI) {
- aboutEl = document.createElement("a");
- aboutEl.addEventListener("click", () => {
- console.log("!discord-screenaudio-about");
- });
- } else {
- aboutEl = document.createElement("div");
- }
- aboutEl.innerText = `discord-screenaudio ${window.discordScreenaudioVersion}`;
- aboutEl.style.fontSize = "12px";
- aboutEl.style.color = "var(--text-muted)";
- aboutEl.style.textTransform = "none";
- aboutEl.classList.add("dirscordScreenaudioAboutText");
- aboutEl.style.cursor = "pointer";
- el.appendChild(aboutEl);
- }
+ function setSvgEnabled() {
+ svg.innerHTML = ``;
}
- // Remove stream settings if stream is active
- document.getElementById("manage-streams-change-windows")?.remove();
- document.querySelector(`[aria-label="Stream Settings"]`)?.remove();
-
- // Add event listener for keybind tab
- if (
- document
- .getElementById("keybinds-tab")
- ?.getElementsByClassName(
- "container-3jbRo5 info-1hMolH fontSize16-3zr6Io browserNotice-1u-Y5o"
- ).length
- ) {
- const el = document
- .getElementById("keybinds-tab")
- .getElementsByClassName("children-1xdcWE")[0];
- const div = document.createElement("div");
- div.style.marginBottom = "50px";
- const button = document.createElement("button");
- button.classList =
- "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.appendChild(div);
+ function updateSvg() {
+ if (enabled) setSvgEnabled();
+ else setSvgDisabled();
}
- const muteBtn = document.getElementsByClassName(
- "button-12Fmur enabled-9OeuTA button-f2h6uQ lookBlank-21BCro colorBrand-I6CyqQ grow-2sR_-F"
- )[0];
- window.discordScreenaudioToggleMute = () => muteBtn.click();
- const deafenBtn = document.getElementsByClassName(
- "button-12Fmur enabled-9OeuTA button-f2h6uQ lookBlank-21BCro colorBrand-I6CyqQ grow-2sR_-F"
- )[1];
- window.discordScreenaudioToggleDeafen = () => deafenBtn.click();
+ container.addEventListener("click", () => {
+ enabled = !enabled;
+ updateSvg();
+ onClick(enabled);
+ });
+ updateSvg();
- if (window.discordScreenaudioResolutionString) {
- for (const el of document.getElementsByClassName(
- "qualityIndicator-39wQDy"
- )) {
- el.innerHTML = window.discordScreenaudioResolutionString;
- }
- }
-}, 500);
+ 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(() => {
+ muteBtn && muteBtn.click();
+ });
+
+ userscript.deafenToggled.connect(() => {
+ 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();
+ });
+
+ setInterval(async () => {
+ const streamActive =
+ document.getElementsByClassName("panel-2ZFCRb activityPanel-9icbyU")
+ .length > 0;
+
+ if (!streamActive && wasStreamActive) userscript.stopVirtmic();
+ wasStreamActive = streamActive;
+
+ if (streamActive) {
+ clonedElements.forEach((el) => {
+ el.remove();
+ });
+ clonedElements.length = 0;
+
+ hiddenElements.forEach((el) => {
+ el.style.display = "block";
+ });
+ hiddenElements.length = 0;
+ } else {
+ for (const el of [
+ document.getElementsByClassName("actionButtons-2vEOUh")?.[0]
+ ?.children[1],
+ document.querySelector(
+ ".wrapper-3t3Yqv > div > div > div > div > .controlButton-2PMNom"
+ ),
+ ]) {
+ if (!el) continue;
+ if (el.classList.contains("discord-screenaudio-cloned")) continue;
+ streamStartBtn = el;
+ streamStartBtn.classList.add("discord-screenaudio-cloned");
+
+ streamStartBtnClone = streamStartBtn.cloneNode(true);
+ streamStartBtnClone.title = "Share Your Screen with Audio";
+ streamStartBtnClone.addEventListener("click", () => {
+ userscript.showStreamDialog();
+ });
+
+ streamStartBtnInitialDisplay = streamStartBtn.style.display;
+
+ streamStartBtn.style.display = "none";
+ streamStartBtn.parentNode.insertBefore(streamStartBtnClone, el);
+
+ clonedElements.push(streamStartBtnClone);
+ hiddenElements.push(streamStartBtn);
+ }
+ }
+
+ // Add about text in settings
+ if (
+ document.getElementsByClassName("dirscordScreenaudioAboutText").length ==
+ 0
+ ) {
+ for (const el of document.getElementsByClassName("info-3pQQBb")) {
+ let aboutEl;
+ if (userscript.kxmlgui) {
+ aboutEl = document.createElement("a");
+ aboutEl.addEventListener("click", () => {
+ userscript.showHelpMenu();
+ });
+ } else {
+ aboutEl = document.createElement("div");
+ }
+ aboutEl.innerText = `discord-screenaudio ${userscript.version}`;
+ aboutEl.style.fontSize = "12px";
+ aboutEl.style.color = "var(--text-muted)";
+ aboutEl.style.textTransform = "none";
+ aboutEl.style.display = "inline-block";
+ aboutEl.style.width = "100%";
+ aboutEl.classList.add("dirscordScreenaudioAboutText");
+ aboutEl.style.cursor = "pointer";
+ el.appendChild(aboutEl);
+ }
+ }
+
+ // Remove stream settings if stream is active
+ document.getElementById("manage-streams-change-windows")?.remove();
+ document.querySelector(`[aria-label="Stream Settings"]`)?.remove();
+
+ // Add event listener for keybind tab
+ if (
+ document
+ .getElementById("keybinds-tab")
+ ?.getElementsByClassName(
+ "container-3jbRo5 info-1hMolH browserNotice-1u-Y5o"
+ ).length
+ ) {
+ const el = document
+ .getElementById("keybinds-tab")
+ .getElementsByClassName("children-1xdcWE")[0];
+ const div = document.createElement("div");
+ div.style.marginBottom = "50px";
+ div.appendChild(
+ createButton("Edit Global Keybinds", () => {
+ userscript.showShortcutsDialog();
+ })
+ );
+ el.innerHTML = "";
+ el.appendChild(div);
+ }
+
+ const buttonContainer =
+ document.getElementsByClassName("container-YkUktl")[0];
+ if (!buttonContainer) {
+ userscript.log(
+ "Cannot locate Mute/Deafen/Settings button container, please report this on GitHub"
+ );
+ }
+
+ muteBtn = buttonContainer
+ ? buttonContainer.getElementsByClassName(
+ "button-12Fmur enabled-9OeuTA button-f2h6uQ lookBlank-21BCro colorBrand-I6CyqQ grow-2sR_-F"
+ )[0]
+ : null;
+
+ deafenBtn = buttonContainer
+ ? buttonContainer.getElementsByClassName(
+ "button-12Fmur enabled-9OeuTA button-f2h6uQ lookBlank-21BCro colorBrand-I6CyqQ grow-2sR_-F"
+ )[1]
+ : null;
+
+ if (resolutionString) {
+ for (const el of document.getElementsByClassName(
+ "qualityIndicator-39wQDy"
+ )) {
+ el.innerHTML = resolutionString;
+ }
+ }
+
+ const accountTab = document.getElementById("my-account-tab");
+ if (accountTab) {
+ const discordScreenaudioSettings = document.getElementById(
+ "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-screenaudio";
+ section.appendChild(title);
+
+ section.appendChild(
+ createButton("Edit Global Keybinds", () => {
+ userscript.showShortcutsDialog();
+ })
+ );
+
+ section.appendChild(
+ createSwitch(
+ "Move discord-screenaudio to the system tray instead of closing",
+ await userscript.getBoolPref("trayIcon", false),
+ (enabled) => {
+ userscript.setTrayIcon(enabled);
+ }
+ )
+ );
+
+ section.appendChild(
+ createSwitch(
+ "Start discord-screenaudio 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);
+}
diff --git a/src/discordpage.cpp b/src/discordpage.cpp
index 7a0712a..b5005f9 100644
--- a/src/discordpage.cpp
+++ b/src/discordpage.cpp
@@ -3,22 +3,10 @@
#include "mainwindow.h"
#include "virtmic.h"
-#ifdef KXMLGUI
-#include
-#include
-#include
-#include
-#include
-
-#ifdef KGLOBALACCEL
-#include
-#endif
-
-#endif
-
#include
#include
#include
+#include
#include
#include
#include
@@ -29,16 +17,40 @@
DiscordPage::DiscordPage(QWidget *parent) : QWebEnginePage(parent) {
setBackgroundColor(QColor("#202225"));
- m_virtmicProcess.setProcessChannelMode(QProcess::ForwardedChannels);
connect(this, &QWebEnginePage::featurePermissionRequested, this,
&DiscordPage::featurePermissionRequested);
- connect(this, &QWebEnginePage::loadStarted, [=]() {
- runJavaScript(QString("window.discordScreenaudioVersion = '%1';")
- .arg(QApplication::applicationVersion()));
- });
+ setupPermissions();
+ 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");
+
+ 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();
+}
+
+void DiscordPage::setupPermissions() {
settings()->setAttribute(QWebEngineSettings::ScreenCaptureEnabled, true);
settings()->setAttribute(QWebEngineSettings::JavascriptCanOpenWindows, true);
settings()->setAttribute(QWebEngineSettings::AllowRunningInsecureContent,
@@ -50,86 +62,19 @@ DiscordPage::DiscordPage(QWidget *parent) : QWebEnginePage(parent) {
false);
settings()->setAttribute(QWebEngineSettings::JavascriptCanOpenWindows, false);
settings()->setAttribute(QWebEngineSettings::ScrollAnimatorEnabled, true);
-
- injectScriptFile("qwebchannel.js", ":/qtwebchannel/qwebchannel.js");
-
- setUrl(QUrl("https://discord.com/app"));
-
- setWebChannel(new QWebChannel(this));
- webChannel()->registerObject("webclass", &m_webClass);
-
- injectScriptFile("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());
- injectScriptText(
- "vencord.js",
- QString("window.discordScreenaudioVencordSettings = `%1`; %2")
- .arg(m_webClass.vencordSend("VencordGetSettings", {}).toString(),
- vencord.readAll()));
- vencord.close();
-
- 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{});
- m_actionCollection->addAction("toggleDeafen", toggleDeafenAction);
- KGlobalAccel::setGlobalShortcut(toggleDeafenAction, QList{});
-
- m_shortcutsDialog = new KShortcutsDialog(KShortcutsEditor::GlobalAction);
- m_shortcutsDialog->addCollection(m_actionCollection);
-#endif
-#endif
-
- connect(&m_streamDialog, &StreamDialog::requestedStreamStart, this,
- &DiscordPage::startStream);
}
-void DiscordPage::injectScriptText(
+void DiscordPage::setupUserStyles() {
+ QString file =
+ QStandardPaths::writableLocation(QStandardPaths::AppConfigLocation) +
+ "/userstyles.css";
+ if (QFileInfo(file).exists()) {
+ qDebug(mainLog) << "Found userstyles:" << file;
+ injectFile(&DiscordPage::injectStylesheet, "userstyles.js", file);
+ }
+}
+
+void DiscordPage::injectScript(
QString name, QString content,
QWebEngineScript::InjectionPoint injectionPoint) {
qDebug(mainLog) << "Injecting " << name;
@@ -145,16 +90,31 @@ void DiscordPage::injectScriptText(
scripts().insert(script);
}
-void DiscordPage::injectScriptFile(
- QString name, QString source,
- QWebEngineScript::InjectionPoint injectionPoint) {
+void DiscordPage::injectScript(QString name, QString content) {
+ injectScript(name, content, QWebEngineScript::DocumentCreation);
+}
+
+void DiscordPage::injectStylesheet(QString name, QString content) {
+ auto script = QString(R"(const stylesheet = document.createElement("style");
+stylesheet.type = "text/css";
+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);
if (!file.open(QIODevice::ReadOnly)) {
qFatal("Failed to load %s with error: %s", source.toLatin1().constData(),
file.errorString().toLatin1().constData());
} else {
- injectScriptText(name, file.readAll(), injectionPoint);
+ (this->*inject)(name, file.readAll());
}
}
@@ -165,11 +125,10 @@ void DiscordPage::featurePermissionRequested(const QUrl &securityOrigin,
QWebEnginePage::PermissionGrantedByUser);
if (feature == QWebEnginePage::Feature::MediaAudioCapture) {
- if (m_virtmicProcess.state() == QProcess::NotRunning) {
+ if (!m_userScript.isVirtmicRunning()) {
qDebug(virtmicLog) << "Starting Virtmic with no target to make sure "
"Discord can find all the audio devices";
- m_virtmicProcess.start(QApplication::arguments()[0],
- {"--virtmic", "None"});
+ m_userScript.startVirtmic("None");
}
}
}
@@ -196,78 +155,81 @@ QWebEnginePage *DiscordPage::createWindow(QWebEnginePage::WebWindowType type) {
return new ExternalPage;
}
-void DiscordPage::stopVirtmic() {
- if (m_virtmicProcess.state() == QProcess::Running) {
- qDebug(virtmicLog) << "Stopping Virtmic";
- m_virtmicProcess.kill();
- m_virtmicProcess.waitForFinished();
- }
-}
-
-void DiscordPage::startVirtmic(QString target) {
- if (target != "None") {
- qDebug(virtmicLog) << "Starting Virtmic with target" << target;
- m_virtmicProcess.start(QApplication::arguments()[0], {"--virtmic", target});
- }
-}
+const QMap cssAnsiColorMap = {{"black", "30"},
+ {"red", "31"},
+ {"green", "32"},
+ {"yellow", "33"},
+ {"blue", "34"},
+ {"magenta", "35"},
+ {"cyan", "36"},
+ {"white", "37"},
+ {"gray", "90"},
+ {"bright-red", "91"},
+ {"bright-green", "92"},
+ {"bright-yellow", "93"},
+ {"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(
QWebEnginePage::JavaScriptConsoleMessageLevel level, const QString &message,
int lineNumber, const QString &sourceID) {
- if (message == "!discord-screenaudio-start-stream") {
- if (m_streamDialog.isHidden())
- m_streamDialog.setHidden(false);
- else
- m_streamDialog.activateWindow();
- m_streamDialog.updateTargets();
- } else if (message == "!discord-screenaudio-stream-stopped") {
- stopVirtmic();
- } else if (message == "!discord-screenaudio-about") {
-#ifdef KXMLGUI
- m_helpMenu->aboutApplication();
-#endif
- } else if (message == "!discord-screenaudio-keybinds") {
-#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
- } else if (message.startsWith("dsa: ")) {
- qDebug(userscriptLog) << message.mid(5).toUtf8().constData();
- } else {
- qDebug(discordLog) << message;
+ auto colorSegments = message.split("%c");
+ for (auto segment : colorSegments.mid(1)) {
+ auto lines = segment.split("\n");
+ QString ansi;
+ uint endOfStyles = lines.length();
+ for (size_t line = 1; line < lines.length(); line++) {
+ if (!lines[line].endsWith(";")) {
+ endOfStyles = line;
+ break;
+ }
+ if (lines[line] == "font-weight: bold;")
+ ansi += "\033[1m";
+ else if (lines[line].startsWith("color: ")) {
+ auto color = lines[line].mid(7).chopped(1);
+ if (cssAnsiColorMap.find(color) != cssAnsiColorMap.end())
+ ansi += "\033[" + cssAnsiColorMap[color] + "m";
+ }
+ }
+ qDebug(discordLog) << (ansi + lines[0].trimmed() + "\033[0m " +
+ ((lines.length() > endOfStyles)
+ ? lines[endOfStyles].trimmed()
+ : ""))
+ .toUtf8()
+ .constData();
+ for (auto line : lines.mid(endOfStyles + 1)) {
+ qDebug(discordLog) << line.toUtf8().constData();
+ }
}
}
-
-void DiscordPage::startStream(QString target, uint width, uint height,
- uint frameRate) {
- stopVirtmic();
- startVirtmic(target);
- // Wait a bit for the virtmic to start
- QTimer::singleShot(target == "None" ? 0 : 200, [=]() {
- runJavaScript(QString("window.discordScreenaudioStartStream(%1, %2, %3);")
- .arg(width)
- .arg(height)
- .arg(frameRate));
- });
-}
-
-void DiscordPage::toggleMute() {
- qDebug(shortcutLog) << "Toggling mute";
- runJavaScript("window.discordScreenaudioToggleMute();");
-}
-
-void DiscordPage::toggleDeafen() {
- qDebug(shortcutLog) << "Toggling deafen";
- runJavaScript("window.discordScreenaudioToggleDeafen();");
-}
diff --git a/src/discordpage.h b/src/discordpage.h
index 42f4e12..3c23fb2 100644
--- a/src/discordpage.h
+++ b/src/discordpage.h
@@ -1,16 +1,9 @@
#pragma once
#include "streamdialog.h"
+#include "userscript.h"
#include "virtmic.h"
-#include "webclass.h"
-#ifdef KXMLGUI
-#include
-#include
-#include
-#endif
-
-#include
#include
#include
#include
@@ -22,16 +15,9 @@ public:
explicit DiscordPage(QWidget *parent = nullptr);
private:
- StreamDialog m_streamDialog;
- QProcess m_virtmicProcess;
- WebClass m_webClass;
-#ifdef KXMLGUI
- KHelpMenu *m_helpMenu;
-#ifdef KGLOBALACCEL
- KActionCollection *m_actionCollection;
- KShortcutsDialog *m_shortcutsDialog;
-#endif
-#endif
+ UserScript m_userScript;
+ void setupPermissions();
+ void setupUserStyles();
bool acceptNavigationRequest(const QUrl &url,
QWebEnginePage::NavigationType type,
bool isMainFrame) override;
@@ -40,21 +26,16 @@ private:
javaScriptConsoleMessage(QWebEnginePage::JavaScriptConsoleMessageLevel level,
const QString &message, int lineNumber,
const QString &sourceID) override;
- void injectScriptText(QString name, QString content,
- QWebEngineScript::InjectionPoint injectionPoint =
- QWebEngineScript::DocumentCreation);
- void injectScriptFile(QString name, QString source,
- QWebEngineScript::InjectionPoint injectionPoint =
- QWebEngineScript::DocumentCreation);
- void stopVirtmic();
- void startVirtmic(QString target);
- void toggleMute();
- void toggleDeafen();
+ void injectScript(QString name, QString content,
+ QWebEngineScript::InjectionPoint injectionPoint);
+ void injectScript(QString name, QString content);
+ void injectStylesheet(QString name, QString content);
+ void injectFile(void (DiscordPage::*inject)(QString, QString), QString name,
+ QString source);
private Q_SLOTS:
void featurePermissionRequested(const QUrl &securityOrigin,
QWebEnginePage::Feature feature);
- void startStream(QString target, uint width, uint height, uint frameRate);
};
// Will immediately get destroyed again but is needed for navigation to
diff --git a/src/main.cpp b/src/main.cpp
index e2cd74b..df1fec8 100644
--- a/src/main.cpp
+++ b/src/main.cpp
@@ -30,6 +30,9 @@ int main(int argc, char *argv[]) {
QCommandLineOption degubOption("remote-debugging",
"Open Chromium Remote Debugging on port 9222");
parser.addOption(degubOption);
+ QCommandLineOption notifySendOption(
+ "notify-send", "Use notify-send instead of QT/KF5 notifications");
+ parser.addOption(notifySendOption);
parser.process(app);
@@ -46,7 +49,7 @@ int main(int argc, char *argv[]) {
"--remote-debugging-port=9222 " +
qgetenv("QTWEBENGINE_CHROMIUM_FLAGS"));
- MainWindow w;
+ MainWindow w(parser.isSet(notifySendOption));
w.show();
return app.exec();
diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp
index bcf8f57..092ec18 100644
--- a/src/mainwindow.cpp
+++ b/src/mainwindow.cpp
@@ -14,6 +14,7 @@
#include
#include
#include
+#include
#include
#include
#include
@@ -24,12 +25,21 @@
MainWindow *MainWindow::m_instance = nullptr;
-MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) {
+MainWindow::MainWindow(bool useNotifySend, QWidget *parent)
+ : QMainWindow(parent) {
assert(MainWindow::m_instance == nullptr);
MainWindow::m_instance = this;
+ m_useNotifySend = useNotifySend;
+ setupSettings();
setupWebView();
+ setupTrayIcon();
resize(1000, 700);
showMaximized();
+ if (m_settings->value("trayIcon", false).toBool() &&
+ m_settings->value("startHidden", false).toBool()) {
+ hide();
+ QTimer::singleShot(0, [=]() { hide(); });
+ }
}
void MainWindow::setupWebView() {
@@ -40,22 +50,37 @@ void MainWindow::setupWebView() {
m_webView = new QWebEngineView(this);
m_webView->setPage(page);
+ if (m_useKF5Notifications || m_useNotifySend)
+ QWebEngineProfile::defaultProfile()->setNotificationPresenter(
+ [&](std::unique_ptr notificationInfo) {
+ if (m_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
- QWebEngineProfile::defaultProfile()->setNotificationPresenter(
- [&](std::unique_ptr 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();
- });
+ 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
+ }
+ });
setCentralWidget(m_webView);
}
@@ -71,6 +96,72 @@ void MainWindow::fullScreenRequested(
}
}
-void MainWindow::closeEvent(QCloseEvent *event) { QApplication::quit(); }
+void MainWindow::setupTrayIcon() {
+ if (m_settings->value("trayIcon", false).toBool() == false ||
+ m_trayIcon != nullptr)
+ return;
+
+ auto aboutAction = new QAction(
+ "discord-screenaudio 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) {
+ if (isVisible()) {
+ hide();
+ } else {
+ show();
+ activateWindow();
+ }
+ }
+ });
+}
+
+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("maltejur", "discord-screenaudio", 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
+ QApplication::quit();
+}
MainWindow *MainWindow::instance() { return m_instance; }
diff --git a/src/mainwindow.h b/src/mainwindow.h
index 39721c8..0a50d62 100644
--- a/src/mainwindow.h
+++ b/src/mainwindow.h
@@ -3,8 +3,11 @@
#include "discordpage.h"
#include
+#include
#include
+#include
#include
+#include
#include
#include
#include
@@ -14,17 +17,33 @@ class MainWindow : public QMainWindow {
Q_OBJECT
public:
- explicit MainWindow(QWidget *parent = nullptr);
+ explicit MainWindow(bool useNotifySend = false, QWidget *parent = nullptr);
static MainWindow *instance();
+ QSettings *settings() const;
private:
void setupWebView();
+ void setupTrayIcon();
+ void cleanTrayIcon();
+ void setupSettings();
QWebEngineView *m_webView;
QWebEngineProfile *prepareProfile();
DiscordPage *m_discordPage;
void closeEvent(QCloseEvent *event) override;
+ QSystemTrayIcon *m_trayIcon = nullptr;
+ QMenu *m_trayIconMenu;
+ QSettings *m_settings;
bool m_wasMaximized;
static MainWindow *m_instance;
+ bool m_useNotifySend;
+#ifdef KNOTIFICATIONS
+ bool m_useKF5Notifications = true;
+#else
+ bool m_useKF5Notifications = false;
+#endif
+
+public Q_SLOTS:
+ void setTrayIcon(bool enabled);
private Q_SLOTS:
void fullScreenRequested(QWebEngineFullScreenRequest fullScreenRequest);
diff --git a/src/streamdialog.cpp b/src/streamdialog.cpp
index 5a151fb..e383dea 100644
--- a/src/streamdialog.cpp
+++ b/src/streamdialog.cpp
@@ -9,73 +9,95 @@
#include
#include
-StreamDialog::StreamDialog() : QWidget() {
+StreamDialog::StreamDialog(QWidget *parent) : QDialog(parent) {
setAttribute(Qt::WA_QuitOnClose, false);
- auto layout = new QVBoxLayout(this);
- layout->setSizeConstraint(QLayout::SetFixedSize);
+ {
+ auto layout = new QVBoxLayout(this);
+ layout->setSizeConstraint(QLayout::SetFixedSize);
- auto targetLabel = new QLabel(this);
- targetLabel->setText("Which app do you want to stream sound from?");
- layout->addWidget(targetLabel);
+ m_videoGroupBox = new QGroupBox(this);
+ m_videoGroupBox->setTitle("Video");
+ m_videoGroupBox->setCheckable(true);
+ layout->addWidget(m_videoGroupBox);
- auto targetHBox = new QHBoxLayout(this);
- layout->addLayout(targetHBox);
+ {
+ auto videoLayout = new QVBoxLayout(m_videoGroupBox);
- m_targetComboBox = new QComboBox(this);
- updateTargets();
- targetHBox->addWidget(m_targetComboBox);
+ auto resolutionLabel = new QLabel(this);
+ resolutionLabel->setText("Resolution");
+ videoLayout->addWidget(resolutionLabel);
- auto refreshTargetsButton = new QPushButton(this);
- refreshTargetsButton->setFixedSize(30, 30);
- refreshTargetsButton->setIcon(QIcon::fromTheme("view-refresh"));
- connect(refreshTargetsButton, &QPushButton::clicked, this,
- &StreamDialog::updateTargets);
- targetHBox->addWidget(refreshTargetsButton);
+ 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 qualityLabel = new QLabel(this);
- qualityLabel->setText("Stream Quality");
- layout->addWidget(qualityLabel);
+ auto framerateLabel = new QLabel(this);
+ framerateLabel->setText("Framerate");
+ videoLayout->addWidget(framerateLabel);
- auto qualityHBox = new QHBoxLayout(this);
- layout->addLayout(qualityHBox);
+ 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_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_audioGroupBox = new QGroupBox(this);
+ m_audioGroupBox->setCheckable(true);
+ m_audioGroupBox->setTitle("Audio");
+ layout->addWidget(m_audioGroupBox);
- 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 audioLayout = new QVBoxLayout(m_audioGroupBox);
- auto button = new QPushButton(this);
- button->setText("Start Stream");
- connect(button, &QPushButton::clicked, this, &StreamDialog::startStream);
- layout->addWidget(button, Qt::AlignRight | Qt::AlignBottom);
+ auto targetLabel = new QLabel(this);
+ targetLabel->setText("Audio Source");
+ audioLayout->addWidget(targetLabel);
- setLayout(layout);
+ {
+ auto targetLayout = new QHBoxLayout();
+ audioLayout->addLayout(targetLayout);
+
+ m_targetComboBox = new QComboBox(this);
+ updateTargets();
+ targetLayout->addWidget(m_targetComboBox);
+
+ auto refreshTargetsButton = new QPushButton(this);
+ refreshTargetsButton->setFixedSize(30, 30);
+ refreshTargetsButton->setIcon(QIcon::fromTheme("view-refresh"));
+ connect(refreshTargetsButton, &QPushButton::clicked, this,
+ &StreamDialog::updateTargets);
+ targetLayout->addWidget(refreshTargetsButton);
+ }
+ }
+
+ auto button = new QPushButton(this);
+ button->setText("Start Stream");
+ connect(button, &QPushButton::clicked, this, &StreamDialog::startStream);
+ layout->addWidget(button, Qt::AlignRight | Qt::AlignBottom);
+ }
setWindowTitle("discord-screenaudio Stream Dialog");
}
void StreamDialog::startStream() {
- auto resolution =
- m_qualityResolutionComboBox->currentData().toString().split('x');
- emit requestedStreamStart(m_targetComboBox->currentText(),
- resolution[0].toUInt(), resolution[1].toUInt(),
- m_qualityFPSComboBox->currentData().toUInt());
+ auto resolution = m_resolutionComboBox->currentData().toString().split('x');
+ emit requestedStreamStart(m_videoGroupBox->isChecked(),
+ m_audioGroupBox->isChecked(), resolution[0].toInt(),
+ resolution[1].toInt(),
+ m_framerateComboBox->currentData().toInt(),
+ m_targetComboBox->currentText());
setHidden(true);
}
@@ -83,7 +105,6 @@ void StreamDialog::updateTargets() {
auto lastTarget = m_targetComboBox->currentText();
m_targetComboBox->clear();
- m_targetComboBox->addItem("[None]");
m_targetComboBox->addItem("[All Desktop Audio]");
for (auto target : Virtmic::getTargets()) {
m_targetComboBox->addItem(target);
diff --git a/src/streamdialog.h b/src/streamdialog.h
index c2f833c..4631ea3 100644
--- a/src/streamdialog.h
+++ b/src/streamdialog.h
@@ -2,22 +2,25 @@
#include
#include
+#include
#include
-class StreamDialog : public QWidget {
+class StreamDialog : public QDialog {
Q_OBJECT
public:
- explicit StreamDialog();
+ explicit StreamDialog(QWidget *parent = nullptr);
private:
QComboBox *m_targetComboBox;
- QComboBox *m_qualityResolutionComboBox;
- QComboBox *m_qualityFPSComboBox;
+ QComboBox *m_resolutionComboBox;
+ QComboBox *m_framerateComboBox;
+ QGroupBox *m_videoGroupBox;
+ QGroupBox *m_audioGroupBox;
Q_SIGNALS:
- void requestedStreamStart(QString target, uint width, uint height,
- uint frameRate);
+ void requestedStreamStart(bool video, bool audio, int width, int height,
+ int frameRate, QString target);
public Q_SLOTS:
void updateTargets();
diff --git a/src/userscript.cpp b/src/userscript.cpp
new file mode 100644
index 0000000..c5021b0
--- /dev/null
+++ b/src/userscript.cpp
@@ -0,0 +1,234 @@
+#include "userscript.h"
+#include "log.h"
+#include "mainwindow.h"
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#ifdef KXMLGUI
+#include
+#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");
+ 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(toggleMuteAction, &QAction::triggered, this,
+ &UserScript::deafenToggled);
+
+ m_actionCollection = new KActionCollection(this);
+ m_actionCollection->addAction("toggleMute", toggleMuteAction);
+ KGlobalAccel::setGlobalShortcut(toggleMuteAction, QList{});
+ m_actionCollection->addAction("toggleDeafen", toggleDeafenAction);
+ KGlobalAccel::setGlobalShortcut(toggleDeafenAction, QList{});
+
+ 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();
+}
+
+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);
+}
diff --git a/src/userscript.h b/src/userscript.h
new file mode 100644
index 0000000..730dc6f
--- /dev/null
+++ b/src/userscript.h
@@ -0,0 +1,71 @@
+#pragma once
+
+#include "streamdialog.h"
+
+#include
+#include
+
+#ifdef KXMLGUI
+#include
+#include
+#include
+#include
+#include
+
+#ifdef KGLOBALACCEL
+#include
+#endif
+
+#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);
+
+private:
+ QProcess m_virtmicProcess;
+ StreamDialog *m_streamDialog;
+ bool m_kxmlgui = false;
+ bool m_kglobalaccel = false;
+#ifdef KXMLGUI
+ KHelpMenu *m_helpMenu;
+#ifdef KGLOBALACCEL
+ KActionCollection *m_actionCollection;
+ KShortcutsDialog *m_shortcutsDialog;
+#endif
+#endif
+ QString m_vencordSettings;
+ void setupHelpMenu();
+ void setupShortcutsDialog();
+ void setupStreamDialog();
+ void setupVirtmic();
+
+Q_SIGNALS:
+ void muteToggled();
+ void deafenToggled();
+ void streamStarted(bool video, int width, int height, int frameRate);
+
+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);
+ QVariant vencordSend(QString event, QVariantList args);
+
+private Q_SLOTS:
+ void startStream(bool video, bool audio, int width, int height, int frameRate,
+ QString target);
+};
diff --git a/src/virtmic.cpp b/src/virtmic.cpp
index ba6d1fc..2c0ba4f 100644
--- a/src/virtmic.cpp
+++ b/src/virtmic.cpp
@@ -22,7 +22,13 @@ QVector getTargets() {
if (global.type == pipewire::node::type) {
auto node = reg.bind(global.id);
auto info = node.info();
- auto name = QString::fromStdString(info.props["application.name"]);
+ QString name;
+ if (info.props.count("application.name") &&
+ info.props["application.name"] != "")
+ name = QString::fromStdString(info.props["application.name"]);
+ else
+ name = QString::fromStdString(
+ info.props["application.process.binary"]);
if (name != "" && !EXCLUDE_TARGETS.contains(name) &&
!targets.contains(name)) {
@@ -67,7 +73,12 @@ void start(QString _target) {
continue;
auto &parent = nodes.at(parent_id);
- auto name = parent.props["application.name"];
+ std::string name;
+ if (parent.props.count("application.name") &&
+ parent.props["application.name"] != "")
+ name = parent.props["application.name"];
+ else
+ name = parent.props["application.process.binary"];
if (name == target ||
(target == "[All Desktop Audio]" &&
@@ -78,8 +89,7 @@ void start(QString _target) {
core.create(
{fl ? virt_fl->info().id : virt_fr->info().id, port_id}));
qDebug(virtmicLog) << QString("Link: %1:%2 -> %3")
- .arg(QString::fromStdString(
- parent.props["application.name"]))
+ .arg(QString::fromStdString(name))
.arg(port_id)
.arg(fl ? virt_fl->info().id
: virt_fr->info().id)
@@ -112,11 +122,17 @@ void start(QString _target) {
[&](const pipewire::global &global) {
if (global.type == pipewire::node::type) {
auto node = reg.bind(global.id);
- if (!node.info().props.count("application.name"))
+ auto info = node.info();
+ std::string name;
+ if (info.props.count("application.name") &&
+ info.props["application.name"] != "")
+ name = info.props["application.name"];
+ else if (info.props.count("application.process.binary")) {
+ name = info.props["application.process.binary"];
+ } else
return;
qDebug(virtmicLog) << QString("Added: %1")
- .arg(QString::fromStdString(
- node.info().props["application.name"]))
+ .arg(QString::fromStdString(name))
.toUtf8()
.data();
@@ -152,9 +168,14 @@ void start(QString _target) {
[&](const std::uint32_t id) {
if (nodes.count(id)) {
auto info = nodes.at(id);
+ std::string name;
+ if (info.props.count("application.name") &&
+ info.props["application.name"] != "")
+ name = info.props["application.name"];
+ else
+ name = info.props["application.process.binary"];
qDebug(virtmicLog) << QString("Removed: %1")
- .arg(QString::fromStdString(
- info.props["application.name"].data()))
+ .arg(QString::fromStdString(name))
.toUtf8()
.data();
nodes.erase(id);
diff --git a/src/webclass.cpp b/src/webclass.cpp
deleted file mode 100644
index 65ad2a5..0000000
--- a/src/webclass.cpp
+++ /dev/null
@@ -1,73 +0,0 @@
-#include "webclass.h"
-
-#include
-#include
-#include
-#include
-#include
-#include
-
-QVariant WebClass::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);
-}
diff --git a/src/webclass.h b/src/webclass.h
deleted file mode 100644
index 8dbc560..0000000
--- a/src/webclass.h
+++ /dev/null
@@ -1,13 +0,0 @@
-#pragma once
-
-#include
-#include
-
-class WebClass : public QObject {
- Q_OBJECT
-public slots:
- QVariant vencordSend(QString event, QVariantList args);
-
-private:
- QString m_vencordSettings;
-};