Compare commits

...

77 Commits

Author SHA1 Message Date
Wizzard
b8d3bbd3e0 additional blacklist 2023-07-26 16:41:42 -04:00
Wizzard
a8d7965c43 Fixes 2023-07-12 18:28:56 -04:00
Wizzard
1a60367c0a Formatting and small changes 2023-07-12 16:49:47 -04:00
Wizzard
65b7556523 possible fullscreen fix 2023-07-12 15:51:38 -04:00
Wizzard
4ab61017db Script modifications 2023-07-11 23:56:00 -04:00
Wizzard
f87096f627 vencord half working 2023-07-11 23:21:30 -04:00
Wizzard
74bbe65688 Vencord fixes 2023-07-11 23:04:56 -04:00
Wizzard
89c9a6651e Vencord presetup 2023-07-11 22:31:01 -04:00
Wizzard
73d49795ea additional blacklist 2023-07-11 22:05:12 -04:00
Wizzard
33a0f68cf1 Mic check 2023-07-11 22:00:24 -04:00
Wizzard
17517d1b46 Fixed script again 2023-07-11 21:41:38 -04:00
12c8a7e49a revert 1fc84366183a908cce5bedfad7725dd779f2d9d8
revert Ignore mics
2023-07-12 01:26:19 +00:00
Wizzard
1fc8436618 Ignore mics 2023-07-11 21:14:20 -04:00
Wizzard
03f52e221f added checks for flatpak-builder 2023-07-11 20:59:42 -04:00
Wizzard
18e6598a95 Added header files to CMakeLists and rebranded 2023-07-11 20:52:16 -04:00
Wizzard
829729aef2 update script 2023-07-11 19:57:12 -04:00
Wizzard
58c3ac0b71 Scripts 2023-07-11 19:50:07 -04:00
Wizzard
2858eb3854 Scruots 2023-07-11 19:49:39 -04:00
Malte Jürgens
94b27f5b6a
implement ctrl+q keybind 2023-06-23 17:48:54 +02:00
Malte Jürgens
8a6f49b949
Merge pull request from Vitalya-code/master
Move two functions into separate file
2023-04-27 18:04:34 +00:00
Malte Jürgens
47fd620876
fix formatting 2023-04-27 20:04:04 +02:00
Vitalya
c15250498b Move server function to the bottom and change "utils" file name to "localserver" 2023-04-27 19:52:28 +03:00
Vitalya
6f0303206e Move two functions into separate file 2023-04-27 17:21:03 +03:00
Malte Jürgens
6e86647c95
Merge pull request from Vitalya-code/master
Shows error message to user if discord is running
2023-04-25 16:33:21 +00:00
Vitalya
798fb3d5e4 Minor change in error message
Change "Discord" to "discord-screenaudio" in error message
2023-04-25 15:07:25 +03:00
Malte Jürgens
6c9b76ed90
format code 2023-04-24 22:41:36 +02:00
Vitalya
b582584c69 Shows error message to user if discord is running 2023-04-24 19:58:56 +03:00
Malte Jürgens
7b6e8fc473
fix segfault in console code 2023-04-18 21:31:22 +02:00
Malte Jürgens
8f0a810539
virtmic: also use nodes which only have a node.name 2023-04-17 15:42:19 +02:00
Malte Jürgens
a6eb82948f
add dnf dependencies 2023-04-17 09:51:10 +02:00
Malte Jürgens
24727f398a
Merge pull request from mensinda/latin2utf8
Do not convert to Latin1 and use UTF-8 instead
2023-04-03 17:26:55 +00:00
Daniel Mensinger
c43e9953a5
Do not convert to Latin1 and use UTF-8 instead 2023-04-03 17:20:53 +02:00
Malte Jürgens
08cb713e8c
add arRPC component to about 2023-03-23 23:14:00 +01:00
Malte Jürgens
27faed4a3a
set minimum window size 2023-03-19 14:44:25 +01:00
Malte Jürgens
10fff86ca4
minify js assets 2023-03-13 11:59:28 +01:00
Malte Jürgens
87e84dec5c
change config file location 2023-03-04 13:29:30 +01:00
Malte Jürgens
4b782133ae
Merge branch 'arrpc' 2023-03-04 13:15:11 +01:00
Malte Jürgens
d5257207bc
dont show theme buttons for now 2023-03-02 23:15:41 +01:00
Malte Jürgens
423884ae0c
remove channel submodule 2023-03-02 17:29:10 +01:00
Malte Jürgens
7ea3d0aab1
remove arrpc submodule 2023-03-02 17:25:00 +01:00
Malte Jürgens
18b15f5cf4
Merge branch 'master' into arrpc 2023-03-02 17:23:08 +01:00
Malte Jürgens
96bca52d0d
fix keybinds 2023-03-02 17:21:04 +01:00
Malte Jürgens
3540774c82
testing arrpc server 2023-03-02 16:43:42 +01:00
Malte Jürgens
b5435acdd8
Merge pull request from AndreasYNY/master
Add arrpc bridge_mod
2023-03-01 11:20:57 +00:00
Andreas
38c2b92e2d Add arrpc bridge_mod
This is a very bad way to do it, but why not still need ARRPC to be running.
2023-02-28 21:36:02 +07:00
Malte Jürgens
e447206082
remember geometry 2023-02-27 20:38:14 +01:00
Malte Jürgens
77f31b63aa
fix path for flatpak in faq 2023-02-19 00:38:33 +01:00
Malte Jürgens
0b9fc100c2
fix 2023-02-19 00:36:27 +01:00
Malte Jürgens
f73524de27
create config dir if it doesn't exist 2023-02-19 00:35:09 +01:00
Malte Jürgens
8a8690fe53
console log config location 2023-02-18 23:53:17 +01:00
Malte Jürgens
259e6dc75d
remove kjob 2023-02-18 15:24:58 +01:00
Malte Jürgens
9cbdca0441
Merge branch 'master' of https://github.com/maltejur/discord-screenaudio 2023-02-18 15:24:30 +01:00
Malte Jürgens
a9ddc0216f
Merge pull request from llyyr/fix-various-build
Fix various build issues
2023-02-18 14:23:29 +00:00
Malte Jürgens
b39e23d462
make themes work better 2023-02-18 15:23:14 +01:00
llyyr
331fb8f4ca fix comparison of different signedness 2023-02-18 08:42:09 +05:30
llyyr
335a4456ed KJob is part of Kf5Notifications and optional 2023-02-18 08:41:17 +05:30
llyyr
26a2ca7b05 add missing include 2023-02-18 08:40:19 +05:30
Malte Jürgens
30c0526ff7
filter out stuff we dont want better 2023-02-17 17:13:49 +01:00
Malte Jürgens
3119e1df19
show userstyles status on the bottom of the page 2023-02-17 16:20:29 +01:00
Malte Jürgens
63180f5d53
load userstyles on demand 2023-02-17 15:47:36 +01:00
Malte Jürgens
78d43991ab
Merge pull request from alvesvaren/patch-1
Fix link in readme
2023-02-16 11:41:56 +00:00
Alve Svarén
193b69f45f
Fix link in readme 2023-02-16 11:32:47 +01:00
Malte Jürgens
485ff9634b
fix log 2023-02-15 15:49:04 +01:00
Malte Jürgens
74fdef683f
try to prefetch external userstyle urls 2023-02-15 15:19:43 +01:00
Malte Jürgens
02fdc62fbe
add faq about userstyles.css 2023-02-15 02:35:48 +01:00
Malte Jürgens
d9e3704176
fix about styles 2023-02-15 02:27:23 +01:00
Malte Jürgens
36c8cfedb7
fix qlayout error 2023-02-15 02:21:41 +01:00
Malte Jürgens
c52daab420
allow for custom user styles 2023-02-15 01:59:22 +01:00
Malte Jürgens
8c4fae3410
fix keybind button 2023-02-15 01:35:23 +01:00
Malte Jürgens
ac71e9bbcb
make discord log look better 2023-02-15 01:30:22 +01:00
Malte Jürgens
fc0dbb5b34
make userscript properties constant 2023-02-15 00:33:52 +01:00
Malte Jürgens
5df24629e6
add faq about drag and drop in flatpak 2023-02-12 17:21:10 +01:00
Malte Jürgens
1477a9d4c0
use qtwebchannel to communicate with userscript 2023-02-12 17:20:33 +01:00
Malte Jürgens
0b12487dfd
add setting to start hidden to tray 2023-02-07 11:53:10 +01:00
Malte Jürgens
a10cfda56d
fix usage of application.process.binary 2023-01-26 01:19:38 +01:00
Malte Jürgens
2a809e163e
fix class names for tray settings 2023-01-26 00:45:14 +01:00
Malte Jürgens
bc317d5531
use application.process.binary if application.name is not available 2023-01-25 18:47:08 +01:00
35 changed files with 1823 additions and 567 deletions

22
.gitignore vendored

@ -1,2 +1,20 @@
/build
.vscode
# Ignore build and output directories
/build/
/.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

@ -43,6 +43,17 @@ set(discord-screenaudio_SRC
src/discordpage.cpp
src/streamdialog.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
)

@ -9,7 +9,7 @@ of [@edisionnano](https://github.com/edisionnano) and the
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)).
([see explanation](#how-does-this-work)).
![Screenshot_20221211_185028](https://user-images.githubusercontent.com/48161361/206920213-58a8091a-d8f9-4bb7-ae3d-3f8581b84d24.png)
@ -62,6 +62,9 @@ You have multiple options:
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`
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
First, clone the repository:
@ -84,7 +87,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)
@ -93,6 +98,23 @@ 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` (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
Copyright (C) 2022 Malte Jürgens

1
assets/arrpc.js Normal file

File diff suppressed because one or more lines are too long

@ -0,0 +1 @@
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);

@ -1,5 +1,3 @@
// From v0.4
navigator.mediaDevices.chromiumGetDisplayMedia =
navigator.mediaDevices.getDisplayMedia;
@ -16,12 +14,12 @@ 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;
};
@ -30,7 +28,7 @@ function setGetDisplayMedia(video = true, overrideArgs = undefined) {
var id;
try {
let myDiscordAudioSink = await getAudioDevice(
"discord-screenaudio-virtmic"
"discord-awesomeaudio-virtmic"
);
id = myDiscordAudioSink.deviceId;
} catch (error) {
@ -71,6 +69,13 @@ function setGetDisplayMedia(video = true, overrideArgs = undefined) {
setGetDisplayMedia();
let userscript;
let muteBtn;
let deafenBtn;
let streamStartBtn;
let streamStartBtnInitialDisplay;
let streamStartBtnClone;
let resolutionString;
const clonedElements = [];
const hiddenElements = [];
let wasStreamActive = false;
@ -79,7 +84,7 @@ function createButton(text, onClick) {
const button = document.createElement("button");
button.style.marginBottom = "20px";
button.classList =
"button-f2h6uQ lookFilled-yCfaCM colorBrand-I6CyqQ sizeSmall-wU2dO- grow-2sR_-F";
"button-ejjZWC lookFilled-1H2Jvj colorBrand-2M3O3N sizeSmall-3R2P2p grow-2T4nbg";
button.innerText = text;
button.addEventListener("click", onClick);
return button;
@ -88,22 +93,22 @@ function createButton(text, onClick) {
function createSwitch(text, enabled, onClick) {
const container = document.createElement("div");
container.style.marginBottom = "20px";
container.className = "labelRow-2jl9gK";
container.className = "labelRow-NnoUIp";
const label = document.createElement("label");
label.innerText = text;
label.className = "title-2dsDLn";
label.className = "title-2yADjX";
container.appendChild(label);
const svg = document.createElement("div");
container.appendChild(svg);
function setSvgDisabled() {
svg.innerHTML = `<div class="container-2nx-BQ default-colors" style="opacity: 1; background-color: rgb(114, 118, 125);"><svg class="slider-32CRPX" 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></div>`;
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-2nx-BQ default-colors checked-25WXMf" style="opacity: 1; background-color: rgb(59, 165, 92);"><svg class="slider-32CRPX" 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></div>`;
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() {
@ -121,197 +126,257 @@ function createSwitch(text, enabled, onClick) {
return container;
}
setInterval(() => {
const streamActive =
document.getElementsByClassName("panel-2ZFCRb activityPanel-9icbyU")
.length > 0;
if (!streamActive && wasStreamActive)
console.log("!discord-screenaudio-stream-stopped");
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;
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 initialDisplay = el.style.display;
window.discordScreenaudioStartStream = (
video,
width,
height,
frameRate
) => {
window.discordScreenaudioResolutionString = video
? `${height}p ${frameRate}FPS`
: "Audio Only";
setGetDisplayMedia(video, {
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);
}
}
// 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);
}
}
// 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", () => {
console.log("!discord-screenaudio-keybinds");
})
);
el.innerHTML = "";
el.appendChild(div);
}
const buttonContainer =
document.getElementsByClassName("container-YkUktl")[0];
if (!buttonContainer) {
console.log(
"dsa: Cannot locate Mute/Deafen/Settings button container, please report this on GitHub"
);
}
const muteBtn = buttonContainer
? buttonContainer.getElementsByClassName(
"button-12Fmur enabled-9OeuTA button-f2h6uQ lookBlank-21BCro colorBrand-I6CyqQ grow-2sR_-F"
)[0]
: null;
window.discordScreenaudioToggleMute = () => muteBtn && muteBtn.click();
const deafenBtn = buttonContainer
? buttonContainer.getElementsByClassName(
"button-12Fmur enabled-9OeuTA button-f2h6uQ lookBlank-21BCro colorBrand-I6CyqQ grow-2sR_-F"
)[1]
: null;
window.discordScreenaudioToggleDeafen = () => deafenBtn && deafenBtn.click();
if (window.discordScreenaudioResolutionString) {
for (const el of document.getElementsByClassName(
"qualityIndicator-39wQDy"
)) {
el.innerHTML = window.discordScreenaudioResolutionString;
}
}
const accountTab = document.getElementById("my-account-tab");
if (accountTab) {
const discordScreenaudioSettings = document.getElementById(
"discord-screenaudio-settings"
);
if (!discordScreenaudioSettings) {
const firstDivider = accountTab.getElementsByClassName(
"divider-_0um2u 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-34Txb0 title-3hptVQ defaultColor-2cKwKo defaultMarginh1-EURXsm";
title.innerText = "discord-screenaudio";
section.appendChild(title);
section.appendChild(
createButton("Edit Global Keybinds", () => {
console.log("!discord-screenaudio-keybinds");
})
);
section.appendChild(
createSwitch(
"Move discord-screenaudio to the system tray instead of closing",
window.discordScreenaudioTrayEnabled,
(enabled) => {
window.discordScreenaudioTrayEnabled = enabled;
console.log(`!discord-screenaudio-tray-${enabled}`);
}
)
);
const divider = document.createElement("div");
divider.className = "divider-_0um2u marginTop40-Q4o1tS";
firstDivider.after(section);
section.after(divider);
}
}
}
}, 500);
// 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 =
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-awesomeaudio ${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.getElementsByTagName("button")[0]
: null;
deafenBtn = buttonContainer
? buttonContainer.getElementsByTagName("button")[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-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);
}

@ -0,0 +1,85 @@
/*
* 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"
}
};

14
assets/vencord/plugin.js Normal file

@ -0,0 +1,14 @@
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: [],
});

@ -0,0 +1,15 @@
--- 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>

268
assets/vencord/vencord.js Normal file

File diff suppressed because one or more lines are too long

Binary file not shown.

After

(image error) Size: 48 KiB

42
install.sh Executable file

@ -0,0 +1,42 @@
#!/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

@ -0,0 +1,44 @@
{
"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;"
}
}

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

36
scripts/build_arrpc.sh Executable file

@ -0,0 +1,36 @@
#!/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

39
scripts/build_vencord.sh Executable file

@ -0,0 +1,39 @@
#!/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

85
src/centralwidget.cpp Normal file

@ -0,0 +1,85 @@
#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;
}
}
}

35
src/centralwidget.h Normal file

@ -0,0 +1,35 @@
#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

@ -3,24 +3,13 @@
#include "mainwindow.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 <QDesktopServices>
#include <QFile>
#include <QFileInfo>
#include <QMessageBox>
#include <QNetworkReply>
#include <QTemporaryFile>
#include <QTimer>
#include <QWebChannel>
#include <QWebEngineScript>
@ -28,17 +17,40 @@
#include <QWebEngineSettings>
DiscordPage::DiscordPage(QWidget *parent) : QWebEnginePage(parent) {
setBackgroundColor(QColor("#202225"));
m_virtmicProcess.setProcessChannelMode(QProcess::ForwardedChannels);
setBackgroundColor(QColor("#313338"));
connect(this, &QWebEnginePage::featurePermissionRequested, this,
&DiscordPage::featurePermissionRequested);
connect(this, &DiscordPage::fullScreenRequested, MainWindow::instance(),
&MainWindow::fullScreenRequested);
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");
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::JavascriptCanOpenWindows, true);
settings()->setAttribute(QWebEngineSettings::AllowRunningInsecureContent,
@ -50,76 +62,87 @@ DiscordPage::DiscordPage(QWidget *parent) : QWebEnginePage(parent) {
false);
settings()->setAttribute(QWebEngineSettings::JavascriptCanOpenWindows, false);
settings()->setAttribute(QWebEngineSettings::ScrollAnimatorEnabled, true);
setUrl(QUrl("https://discord.com/app"));
injectScriptFile("userscript.js", ":/assets/userscript.js");
injectScriptText("vars.js",
QString("window.discordScreenaudioVersion = '%1'; "
"window.discordScreenaudioTrayEnabled = %2;")
.arg(QApplication::applicationVersion())
.arg(MainWindow::instance()
->settings()
->value("trayIcon", false)
.toBool()));
#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::injectScriptText(QString name, QString content) {
void DiscordPage::setupUserStyles() {
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;
QWebEngineScript script;
@ -127,20 +150,36 @@ void DiscordPage::injectScriptText(QString name, QString content) {
script.setSourceCode(content);
script.setName(name);
script.setWorldId(QWebEngineScript::MainWorld);
script.setInjectionPoint(QWebEngineScript::DocumentCreation);
script.setInjectionPoint(injectionPoint);
script.setRunsOnSubFrames(false);
scripts().insert(script);
}
void DiscordPage::injectScriptFile(QString name, QString source) {
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.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());
(this->*inject)(name, file.readAll());
}
}
@ -151,11 +190,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");
}
}
}
@ -182,82 +220,102 @@ 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) {
qDebug(virtmicLog) << "Starting Virtmic with target" << target;
m_virtmicProcess.start(QApplication::arguments()[0], {"--virtmic", target});
}
const QMap<QString, QString> 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 == "!discord-screenaudio-tray-true") {
MainWindow::instance()->setTrayIcon(true);
} else if (message == "!discord-screenaudio-tray-false") {
MainWindow::instance()->setTrayIcon(false);
} else if (message.startsWith("dsa: ")) {
qDebug(userscriptLog) << message.mid(5).toUtf8().constData();
} else {
qDebug(discordLog) << message;
auto colorSegments = message.split("%c");
if (colorSegments[0] != "") {
for (auto line : colorSegments[0].split("\n"))
qDebug(discordLog) << line.toUtf8().constData();
}
for (auto segment : colorSegments.mid(1)) {
auto lines = segment.split("\n");
QString ansi;
uint endOfStyles = lines.length();
for (auto 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";
}
}
if (endOfStyles < lines.length())
qDebug(discordLog) << (ansi + lines[0].trimmed() + "\033[0m " +
lines[endOfStyles].trimmed())
.toUtf8()
.constData();
for (auto line : lines.mid(endOfStyles + 1)) {
qDebug(discordLog) << line.toUtf8().constData();
}
}
}
void DiscordPage::startStream(bool video, bool audio, uint width, uint height,
uint frameRate, QString target) {
stopVirtmic();
startVirtmic(audio ? target : "[None]");
// Wait a bit for the virtmic to start
QTimer::singleShot(200, [=]() {
runJavaScript(
QString("window.discordScreenaudioStartStream(%1, %2, %3, %4);")
.arg(video)
.arg(video ? width : 32)
.arg(video ? height : 16)
.arg(video ? frameRate : 1));
});
}
UserScript *DiscordPage::userScript() { return &m_userScript; }
void DiscordPage::toggleMute() {
qDebug(shortcutLog) << "Toggling mute";
runJavaScript("window.discordScreenaudioToggleMute();");
}
void DiscordPage::setupArrpc() {
QFile nodejs("/usr/bin/node");
if (nodejs.exists()) {
auto arrpcSource = QTemporaryFile::createNativeFile(":/assets/arrpc.js");
qDebug(mainLog).noquote()
<< "NodeJS found, starting arRPC located at" << arrpcSource->fileName();
m_arrpcProcess.setProcessChannelMode(QProcess::ForwardedChannels);
m_arrpcProcess.setProgram(nodejs.fileName());
m_arrpcProcess.setArguments(QStringList{arrpcSource->fileName()});
m_arrpcProcess.start();
void DiscordPage::toggleDeafen() {
qDebug(shortcutLog) << "Toggling deafen";
runJavaScript("window.discordScreenaudioToggleDeafen();");
injectFile(&DiscordPage::injectScript, "arrpc_bridge_mod.js",
":/assets/arrpc_bridge_mod.js");
}
}

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

22
src/localserver.cpp Normal file

@ -0,0 +1,22 @@
#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();
}

9
src/localserver.h Normal file

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

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

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

@ -1,3 +1,4 @@
#include "localserver.h"
#include "mainwindow.h"
#include "virtmic.h"
@ -7,15 +8,19 @@
#include <QApplication>
#include <QCommandLineParser>
#include <QLocalServer>
#include <QLocalSocket>
#include <QLoggingCategory>
#include <QMessageBox>
int main(int argc, char *argv[]) {
QApplication app(argc, argv);
QApplication::setApplicationName("discord-screenaudio");
QApplication::setApplicationName("discord-awesomeaudio");
QApplication::setWindowIcon(
QIcon(":assets/de.shorsh.discord-screenaudio.png"));
QApplication::setApplicationVersion(DISCORD_SCEENAUDIO_VERSION_FULL);
QApplication::setDesktopFileName("de.shorsh.discord-screenaudio");
QApplication::setDesktopFileName("lol.deadzone.discord-awesomeaudio");
qSetMessagePattern("[%{category}] %{message}");
@ -50,6 +55,20 @@ int main(int argc, char *argv[]) {
qgetenv("QTWEBENGINE_CHROMIUM_FLAGS"));
MainWindow w(parser.isSet(notifySendOption));
// 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();
return app.exec();

@ -12,150 +12,154 @@
#include <QGridLayout>
#include <QLabel>
#include <QPushButton>
#include <QShortcut>
#include <QSpacerItem>
#include <QThread>
#include <QTimer>
#include <QUrl>
#include <QWebEngineNotification>
#include <QWebEngineFullScreenRequest>
#include <QWebEngineProfile>
#include <QWebEngineScript>
#include <QWebEngineScriptCollection>
#include <QWebEngineSettings>
#include <QWebEngineUrlRequestInterceptor>
#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(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();
}
assert(MainWindow::m_instance == nullptr);
MainWindow::m_instance = this;
setupSettings();
m_settings->setValue("useNotifySend", useNotifySend);
m_centralWidget = new CentralWidget(this);
setCentralWidget(m_centralWidget);
void MainWindow::setupWebView() {
auto page = new DiscordPage(this);
connect(page, &QWebEnginePage::fullScreenRequested, this,
&MainWindow::fullScreenRequested);
// Create and install the network interceptor
NetworkInterceptor* networkInterceptor = new NetworkInterceptor(this);
QWebEngineProfile::defaultProfile()->setRequestInterceptor(networkInterceptor);
m_webView = new QWebEngineView(this);
m_webView->setPage(page);
if (m_useKF5Notifications || m_useNotifySend)
QWebEngineProfile::defaultProfile()->setNotificationPresenter(
[&](std::unique_ptr<QWebEngineNotification> 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
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);
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);
showMaximized();
}
if (m_settings->value("trayIcon", false).toBool() &&
m_settings->value("startHidden", false).toBool()) {
hide();
QTimer::singleShot(0, [=]() { hide(); });
}
}
void MainWindow::fullScreenRequested(
QWebEngineFullScreenRequest fullScreenRequest) {
fullScreenRequest.accept();
if (fullScreenRequest.toggleOn()) {
m_wasMaximized = isMaximized();
showFullScreen();
} else {
m_wasMaximized ? showMaximized() : showNormal();
}
QWebEngineFullScreenRequest fullScreenRequest) {
fullScreenRequest.accept();
if (fullScreenRequest.toggleOn()) {
m_wasMaximized = isMaximized();
if (!m_wasMaximized) {
showNormal();
}
showFullScreen();
} else {
m_wasMaximized ? showMaximized() : showNormal();
}
}
void MainWindow::setupTrayIcon() {
if (m_settings->value("trayIcon", false).toBool() == false ||
m_trayIcon != nullptr)
return;
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 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(); });
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_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();
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();
}
}
});
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;
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();
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();
}
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();
if (m_settings->value("trayIcon", false).toBool()) {
hide();
} else {
m_settings->setValue("geometry", saveGeometry());
QApplication::quit();
}
}
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();
}
}

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

@ -9,7 +9,7 @@
#include <QSizePolicy>
#include <QVBoxLayout>
StreamDialog::StreamDialog() : QWidget() {
StreamDialog::StreamDialog(QWidget *parent) : QDialog(parent) {
setAttribute(Qt::WA_QuitOnClose, false);
{
@ -22,8 +22,7 @@ StreamDialog::StreamDialog() : QWidget() {
layout->addWidget(m_videoGroupBox);
{
auto videoLayout = new QVBoxLayout(this);
m_videoGroupBox->setLayout(videoLayout);
auto videoLayout = new QVBoxLayout(m_videoGroupBox);
auto resolutionLabel = new QLabel(this);
resolutionLabel->setText("Resolution");
@ -60,15 +59,14 @@ StreamDialog::StreamDialog() : QWidget() {
layout->addWidget(m_audioGroupBox);
{
auto audioLayout = new QVBoxLayout(this);
m_audioGroupBox->setLayout(audioLayout);
auto audioLayout = new QVBoxLayout(m_audioGroupBox);
auto targetLabel = new QLabel(this);
targetLabel->setText("Audio Source");
audioLayout->addWidget(targetLabel);
{
auto targetLayout = new QHBoxLayout(this);
auto targetLayout = new QHBoxLayout();
audioLayout->addLayout(targetLayout);
m_targetComboBox = new QComboBox(this);
@ -88,8 +86,6 @@ StreamDialog::StreamDialog() : QWidget() {
button->setText("Start Stream");
connect(button, &QPushButton::clicked, this, &StreamDialog::startStream);
layout->addWidget(button, Qt::AlignRight | Qt::AlignBottom);
setLayout(layout);
}
setWindowTitle("discord-screenaudio Stream Dialog");
@ -98,9 +94,9 @@ StreamDialog::StreamDialog() : QWidget() {
void StreamDialog::startStream() {
auto resolution = m_resolutionComboBox->currentData().toString().split('x');
emit requestedStreamStart(m_videoGroupBox->isChecked(),
m_audioGroupBox->isChecked(),
resolution[0].toUInt(), resolution[1].toUInt(),
m_framerateComboBox->currentData().toUInt(),
m_audioGroupBox->isChecked(), resolution[0].toInt(),
resolution[1].toInt(),
m_framerateComboBox->currentData().toInt(),
m_targetComboBox->currentText());
setHidden(true);
}

@ -5,11 +5,11 @@
#include <QGroupBox>
#include <QWidget>
class StreamDialog : public QWidget {
class StreamDialog : public QDialog {
Q_OBJECT
public:
explicit StreamDialog();
explicit StreamDialog(QWidget *parent = nullptr);
private:
QComboBox *m_targetComboBox;
@ -19,8 +19,8 @@ private:
QGroupBox *m_audioGroupBox;
Q_SIGNALS:
void requestedStreamStart(bool video, bool audio, uint width, uint height,
uint frameRate, QString target);
void requestedStreamStart(bool video, bool audio, int width, int height,
int frameRate, QString target);
public Q_SLOTS:
void updateTargets();

248
src/userscript.cpp Normal file

@ -0,0 +1,248 @@
#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);
}

89
src/userscript.h Normal file

@ -0,0 +1,89 @@
#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);
};

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

1
submodules/Vencord Submodule

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

2
uninstall.sh Executable file

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

3
update.sh Executable file

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