Compare commits

...

64 Commits

Author SHA1 Message Date
Malte Jürgens
e6449ad807 work 2023-03-19 14:39:15 +01:00
Malte Jürgens
e919e3ea25 Merge branch 'master' into virtmic-rewrite 2023-03-17 13:58:08 +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 #114 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
f31901073e update rohrkabel to v1.5 2023-02-24 15:14:38 +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 #107 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 #106 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
Malte Jürgens
b4db987217 fix tray settings 2023-01-23 18:59:25 +01:00
Malte Jürgens
9f46e710a9 fix tray bug 2023-01-14 16:28:23 +01:00
Malte Jürgens
d6641a7a6e Merge pull request #88 from maltejur/tray
Implement Tray Icon
2023-01-14 14:51:46 +00:00
Malte Jürgens
b836be6530 Implement Tray Icon 2023-01-13 23:33:47 +01:00
Malte Jürgens
bfb0714b13 fix keybinds query selector 2023-01-09 22:20:11 +01:00
Malte Jürgens
374b854261 Update README 2023-01-09 22:19:45 +01:00
Malte Jürgens
1f6105f76b allow for building with qt6 2022-12-27 19:00:10 +01:00
Malte Jürgens
3071159332 Update screenshot 2022-12-11 17:55:04 +00:00
Malte Jürgens
f2de080e1b update labels 2022-11-22 16:39:58 +01:00
Malte Jürgens
f4a60f281d reworked stream dialog 2022-11-22 16:35:47 +01:00
Malte Jürgens
d693535d53 Revert "add clipboard permission"
This reverts commit b0a8815bb8.
2022-11-09 22:13:34 +01:00
Malte Jürgens
e41af697f7 fix error 2022-11-05 00:08:25 +01:00
Malte Jürgens
b0a8815bb8 add clipboard permission 2022-11-04 23:20:46 +01:00
Malte Jürgens
150fd4364e add --notify-send option 2022-11-04 23:16:49 +01:00
Malte Jürgens
a0a2924796 remove Vencord folder 2022-11-04 20:45:55 +00:00
Malte Jürgens
3c48621427 add developer field to metainfo 2022-10-28 21:57:32 +02:00
Malte Jürgens
9faabe1f3e Merge pull request #66 from kherchel/fix/keybinds
Fix mute and deafen keybinds disconnecting from the call
2022-10-25 18:18:08 +00:00
Kacper Herchel
68473d04d8 Fix mute and deafen keybinds disconnecting from the call instead of performing their appropriate action 2022-10-25 17:01:10 +02:00
27 changed files with 1423 additions and 602 deletions

1
.gitignore vendored
View File

@@ -1,2 +1,3 @@
/build /build
.vscode .vscode
/submodules/arrpc

3
.gitmodules vendored
View File

@@ -1,3 +1,6 @@
[submodule "submodules/rohrkabel"] [submodule "submodules/rohrkabel"]
path = submodules/rohrkabel path = submodules/rohrkabel
url = https://github.com/Soundux/rohrkabel url = https://github.com/Soundux/rohrkabel
[submodule "submodules/channel"]
path = submodules/channel
url = https://github.com/Soundux/channel.git

View File

@@ -13,24 +13,27 @@ set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -DDEBUG")
string(TIMESTAMP TIMESTAMP %s) string(TIMESTAMP TIMESTAMP %s)
# set(CMAKE_AUTOUIC ON) # set(CMAKE_AUTOUIC ON)
find_package(Qt5 CONFIG REQUIRED COMPONENTS find_package(Qt5 COMPONENTS Widgets)
Widgets if (Qt5_FOUND)
WebEngineWidgets find_package(Qt5 CONFIG REQUIRED COMPONENTS Widgets WebEngineWidgets)
)
find_package(KF5Notifications) find_package(KF5Notifications)
if(KF5Notifications_FOUND) if(KF5Notifications_FOUND)
add_definitions( -DKNOTIFICATIONS ) add_definitions( -DKNOTIFICATIONS )
endif() endif()
find_package(KF5XmlGui) find_package(KF5XmlGui)
if(KF5XmlGui_FOUND) if(KF5XmlGui_FOUND)
add_definitions( -DKXMLGUI ) add_definitions( -DKXMLGUI )
endif() endif()
find_package(KF5GlobalAccel) find_package(KF5GlobalAccel)
if(KF5GlobalAccel_FOUND) if(KF5GlobalAccel_FOUND)
add_definitions( -DKGLOBALACCEL ) add_definitions( -DKGLOBALACCEL )
endif()
else()
message(WARNING "Qt 5 was not found on your system and Qt 6 will be used. You will not be able to use any features using KDE Frameworks.")
find_package(Qt6 CONFIG REQUIRED COMPONENTS Widgets WebEngineWidgets)
endif() endif()
set(discord-screenaudio_SRC set(discord-screenaudio_SRC
@@ -40,6 +43,8 @@ set(discord-screenaudio_SRC
src/discordpage.cpp src/discordpage.cpp
src/streamdialog.cpp src/streamdialog.cpp
src/log.cpp src/log.cpp
src/userscript.cpp
src/centralwidget.cpp
resources.qrc resources.qrc
) )
@@ -58,15 +63,20 @@ if(GIT_FOUND AND EXISTS "${PROJECT_SOURCE_DIR}/.git")
endif() endif()
endif() endif()
if(NOT EXISTS "${PROJECT_SOURCE_DIR}/submodules/rohrkabel/CMakeLists.txt") function(add_git_subdirectory SUBMODULE)
message(FATAL_ERROR "Rohrkabel was not found since you are not in a Git checkout or have GIT_SUBMODULE disabled. Please provide rohrkabel manually to `./submodules/rohrkabel`.") if(NOT EXISTS "${PROJECT_SOURCE_DIR}/submodules/${SUBMODULE}/CMakeLists.txt")
endif() message(FATAL_ERROR "Submodule ${SUBMODULE} was not found since you are not in a Git checkout or have GIT_SUBMODULE disabled. Please provide ${SUBMODULE} manually to `./submodules/${SUBMODULE}`.")
endif()
add_subdirectory(submodules/rohrkabel) add_subdirectory(submodules/${SUBMODULE})
endfunction()
add_git_subdirectory(rohrkabel)
add_git_subdirectory(channel)
add_executable(discord-screenaudio ${discord-screenaudio_SRC}) 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 channel)
if(KF5Notifications_FOUND) if(KF5Notifications_FOUND)
target_link_libraries(discord-screenaudio KF5::Notifications) target_link_libraries(discord-screenaudio KF5::Notifications)

View File

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

1
assets/arrpc.js Normal file

File diff suppressed because one or more lines are too long

View File

@@ -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);

View File

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

View File

@@ -1,5 +1,3 @@
// From v0.4
navigator.mediaDevices.chromiumGetDisplayMedia = navigator.mediaDevices.chromiumGetDisplayMedia =
navigator.mediaDevices.getDisplayMedia; navigator.mediaDevices.getDisplayMedia;
@@ -16,16 +14,16 @@ const getAudioDevice = async (nameOfAudioDevice) => {
let devices = await navigator.mediaDevices.enumerateDevices(); let devices = await navigator.mediaDevices.enumerateDevices();
audioDevice = devices.find(({ label }) => label === nameOfAudioDevice); audioDevice = devices.find(({ label }) => label === nameOfAudioDevice);
if (!audioDevice) if (!audioDevice)
console.log( userscript.log(
`dsa: Did not find '${nameOfAudioDevice}', trying again in 100ms` `Did not find '${nameOfAudioDevice}', trying again in 100ms`
); );
await sleep(100); await sleep(100);
} }
console.log(`dsa: Found '${nameOfAudioDevice}'`); userscript.log(`Found '${nameOfAudioDevice}'`);
return audioDevice; return audioDevice;
}; };
function setGetDisplayMedia(overrideArgs = undefined) { function setGetDisplayMedia(video = true, overrideArgs = undefined) {
const getDisplayMedia = async (...args) => { const getDisplayMedia = async (...args) => {
var id; var id;
try { try {
@@ -63,6 +61,7 @@ function setGetDisplayMedia(overrideArgs = undefined) {
: args || [{ video: true, audio: true }]) : args || [{ video: true, audio: true }])
); );
gdm.addTrack(track); gdm.addTrack(track);
if (!video) for (const track of gdm.getVideoTracks()) track.enabled = false;
return gdm; return gdm;
}; };
navigator.mediaDevices.getDisplayMedia = getDisplayMedia; navigator.mediaDevices.getDisplayMedia = getDisplayMedia;
@@ -70,137 +69,304 @@ function setGetDisplayMedia(overrideArgs = undefined) {
setGetDisplayMedia(); setGetDisplayMedia();
let userscript;
let muteBtn;
let deafenBtn;
let streamStartBtn;
let streamStartBtnInitialDisplay;
let streamStartBtnClone;
let resolutionString;
const clonedElements = []; const clonedElements = [];
const hiddenElements = []; const hiddenElements = [];
let wasStreamActive = false; let wasStreamActive = false;
setInterval(() => { function createButton(text, onClick) {
const streamActive = const button = document.createElement("button");
document.getElementsByClassName("panel-2ZFCRb activityPanel-9icbyU") button.style.marginBottom = "20px";
.length > 0; button.classList =
"button-ejjZWC lookFilled-1H2Jvj colorBrand-2M3O3N sizeSmall-3R2P2p grow-2T4nbg";
button.innerText = text;
button.addEventListener("click", onClick);
return button;
}
if (!streamActive && wasStreamActive) function createSwitch(text, enabled, onClick) {
console.log("!discord-screenaudio-stream-stopped"); const container = document.createElement("div");
wasStreamActive = streamActive; container.style.marginBottom = "20px";
container.className = "labelRow-NnoUIp";
if (streamActive) { const label = document.createElement("label");
clonedElements.forEach((el) => { label.innerText = text;
el.remove(); label.className = "title-2yADjX";
}); container.appendChild(label);
clonedElements.length = 0;
hiddenElements.forEach((el) => { const svg = document.createElement("div");
el.style.display = "block"; container.appendChild(svg);
});
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; function setSvgDisabled() {
svg.innerHTML = `<div class="container-1QtPKm default-colors" style="opacity: 1; background-color: rgb(114, 118, 125);"><svg class="slider-HJFN2i" viewBox="0 0 28 20" preserveAspectRatio="xMinYMid meet" aria-hidden="true" style="left: -3px;"><rect fill="white" x="4" y="0" height="20" width="20" rx="10"></rect><svg viewBox="0 0 20 20" fill="none"><path fill="rgba(114, 118, 125, 1)" d="M5.13231 6.72963L6.7233 5.13864L14.855 13.2704L13.264 14.8614L5.13231 6.72963Z"></path><path fill="rgba(114, 118, 125, 1)" d="M13.2704 5.13864L14.8614 6.72963L6.72963 14.8614L5.13864 13.2704L13.2704 5.13864Z"></path></svg></svg><input id="uid_84" type="checkbox" class="input-125oad" tabindex="0"></div>`;
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);
}
} }
// Add about text in settings function setSvgEnabled() {
if ( 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>`;
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 function updateSvg() {
document.getElementById("manage-streams-change-windows")?.remove(); if (enabled) setSvgEnabled();
document.querySelector(`[aria-label="Stream Settings"]`)?.remove(); else setSvgDisabled();
// 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);
} }
const muteBtn = document.getElementsByClassName( container.addEventListener("click", () => {
"button-12Fmur enabled-9OeuTA button-f2h6uQ lookBlank-21BCro colorBrand-I6CyqQ grow-2sR_-F" enabled = !enabled;
)[0]; updateSvg();
window.discordScreenaudioToggleMute = () => muteBtn.click(); onClick(enabled);
const deafenBtn = document.getElementsByClassName( });
"button-12Fmur enabled-9OeuTA button-f2h6uQ lookBlank-21BCro colorBrand-I6CyqQ grow-2sR_-F" updateSvg();
)[1];
window.discordScreenaudioToggleDeafen = () => deafenBtn.click();
if (window.discordScreenaudioResolutionString) { return container;
for (const el of document.getElementsByClassName( }
"qualityIndicator-39wQDy"
)) {
el.innerHTML = window.discordScreenaudioResolutionString;
}
}
}, 500);
// Fix for broken discord notifications after restart // Fix for broken discord notifications after restart
// (https://github.com/maltejur/discord-screenaudio/issues/17) // (https://github.com/maltejur/discord-screenaudio/issues/17)
Notification.requestPermission(); 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-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.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-screenaudio";
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(
"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);
}

View File

@@ -2,6 +2,8 @@
<RCC> <RCC>
<qresource> <qresource>
<file>assets/userscript.js</file> <file>assets/userscript.js</file>
<file>assets/arrpc_bridge_mod.js</file>
<file>assets/arrpc.js</file>
<file>assets/de.shorsh.discord-screenaudio.png</file> <file>assets/de.shorsh.discord-screenaudio.png</file>
</qresource> </qresource>
</RCC> </RCC>

36
scripts/build_arrpc.sh Executable file
View 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

84
src/centralwidget.cpp Normal file
View File

@@ -0,0 +1,84 @@
#include "centralwidget.h"
#include "discordpage.h"
#include "mainwindow.h"
#include <QWebEngineNotification>
#include <QWebEngineProfile>
#include <QWebEngineScript>
#include <QWebEngineScriptCollection>
#include <QWebEngineSettings>
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;
}
}
}

29
src/centralwidget.h Normal file
View File

@@ -0,0 +1,29 @@
#pragma once
#include <QLabel>
#include <QVBoxLayout>
#include <QWebEnginePage>
#include <QWebEngineProfile>
#include <QWebEngineView>
#include <QWidget>
class CentralWidget : public QWidget {
Q_OBJECT
public:
CentralWidget(QWidget *parent = nullptr);
private:
void setupWebView();
QVBoxLayout *m_layout;
QWebEngineView *m_webView;
#ifdef KNOTIFICATIONS
bool m_useKF5Notifications = true;
#else
bool m_useKF5Notifications = false;
#endif
QLabel *m_loadingLabel = nullptr;
public Q_SLOTS:
void setLoadingIndicator(QString text);
};

View File

@@ -3,24 +3,13 @@
#include "mainwindow.h" #include "mainwindow.h"
#include "virtmic.h" #include "virtmic.h"
#ifdef KXMLGUI
#include <KAboutData>
#include <KHelpMenu>
#include <KShortcutsDialog>
#include <KXmlGuiWindow>
#include <QAction>
#ifdef KGLOBALACCEL
#include <KGlobalAccel>
#endif
#endif
#include <QApplication> #include <QApplication>
#include <QDesktopServices> #include <QDesktopServices>
#include <QFile> #include <QFile>
#include <QFileInfo>
#include <QMessageBox> #include <QMessageBox>
#include <QNetworkReply> #include <QNetworkReply>
#include <QTemporaryFile>
#include <QTimer> #include <QTimer>
#include <QWebChannel> #include <QWebChannel>
#include <QWebEngineScript> #include <QWebEngineScript>
@@ -28,17 +17,31 @@
#include <QWebEngineSettings> #include <QWebEngineSettings>
DiscordPage::DiscordPage(QWidget *parent) : QWebEnginePage(parent) { DiscordPage::DiscordPage(QWidget *parent) : QWebEnginePage(parent) {
setBackgroundColor(QColor("#202225")); setBackgroundColor(QColor("#313338"));
m_virtmicProcess.setProcessChannelMode(QProcess::ForwardedChannels);
connect(this, &QWebEnginePage::featurePermissionRequested, this, connect(this, &QWebEnginePage::featurePermissionRequested, this,
&DiscordPage::featurePermissionRequested); &DiscordPage::featurePermissionRequested);
connect(this, &DiscordPage::fullScreenRequested, MainWindow::instance(),
&MainWindow::fullScreenRequested);
connect(this, &QWebEnginePage::loadStarted, [=]() { setupPermissions();
runJavaScript(QString("window.discordScreenaudioVersion = '%1';")
.arg(QApplication::applicationVersion()));
});
injectFile(&DiscordPage::injectScript, "qwebchannel.js",
":/qtwebchannel/qwebchannel.js");
setUrl(QUrl("https://discord.com/app"));
setWebChannel(new QWebChannel(this));
webChannel()->registerObject("userscript", &m_userScript);
injectFile(&DiscordPage::injectScript, "userscript.js",
":/assets/userscript.js");
setupUserStyles();
setupArrpc();
}
void DiscordPage::setupPermissions() {
settings()->setAttribute(QWebEngineSettings::ScreenCaptureEnabled, true); settings()->setAttribute(QWebEngineSettings::ScreenCaptureEnabled, true);
settings()->setAttribute(QWebEngineSettings::JavascriptCanOpenWindows, true); settings()->setAttribute(QWebEngineSettings::JavascriptCanOpenWindows, true);
settings()->setAttribute(QWebEngineSettings::AllowRunningInsecureContent, settings()->setAttribute(QWebEngineSettings::AllowRunningInsecureContent,
@@ -50,71 +53,87 @@ DiscordPage::DiscordPage(QWidget *parent) : QWebEnginePage(parent) {
false); false);
settings()->setAttribute(QWebEngineSettings::JavascriptCanOpenWindows, false); settings()->setAttribute(QWebEngineSettings::JavascriptCanOpenWindows, false);
settings()->setAttribute(QWebEngineSettings::ScrollAnimatorEnabled, true); settings()->setAttribute(QWebEngineSettings::ScrollAnimatorEnabled, true);
setUrl(QUrl("https://discord.com/app"));
injectScriptFile("userscript.js", ":/assets/userscript.js");
injectScriptText("version.js",
QString("window.discordScreenaudioVersion = '%1';")
.arg(QApplication::applicationVersion()));
#ifdef KXMLGUI
injectScriptText("xmlgui.js", "window.discordScreenaudioKXMLGUI = true;");
KAboutData aboutData(
"discord-screenaudio", "discord-screenaudio",
QApplication::applicationVersion(),
"Custom Discord client with the ability to stream audio on Linux",
KAboutLicense::GPL_V3, "Copyright 2022 (C) Malte Jürgens");
aboutData.setBugAddress("https://github.com/maltejur/discord-screenaudio");
aboutData.addAuthor("Malte Jürgens", "Author", "maltejur@dismail.de",
"https://github.com/maltejur");
aboutData.addCredit("edisionnano",
"For creating and documenting the approach for streaming "
"audio in Discord used in this project.",
QString(),
"https://github.com/edisionnano/"
"Screenshare-with-audio-on-Discord-with-Linux");
aboutData.addCredit(
"Curve", "For creating the Rohrkabel library used in this project.",
QString(), "https://github.com/Curve");
aboutData.addComponent("Rohrkabel", "A C++ RAII Pipewire-API Wrapper", "1.3",
"https://github.com/Soundux/rohrkabel");
m_helpMenu = new KHelpMenu(parent, aboutData);
#ifdef KGLOBALACCEL
injectScriptText("kglobalaccel.js",
"window.discordScreenaudioKGLOBALACCEL = true;");
auto toggleMuteAction = new QAction(this);
toggleMuteAction->setText("Toggle Mute");
toggleMuteAction->setIcon(QIcon::fromTheme("microphone-sensitivity-muted"));
connect(toggleMuteAction, &QAction::triggered, this,
&DiscordPage::toggleMute);
auto toggleDeafenAction = new QAction(this);
toggleDeafenAction->setText("Toggle Deafen");
toggleDeafenAction->setIcon(QIcon::fromTheme("audio-volume-muted"));
connect(toggleDeafenAction, &QAction::triggered, this,
&DiscordPage::toggleDeafen);
m_actionCollection = new KActionCollection(this);
m_actionCollection->addAction("toggleMute", toggleMuteAction);
KGlobalAccel::setGlobalShortcut(toggleMuteAction, QList<QKeySequence>{});
m_actionCollection->addAction("toggleDeafen", toggleDeafenAction);
KGlobalAccel::setGlobalShortcut(toggleDeafenAction, QList<QKeySequence>{});
m_shortcutsDialog = new KShortcutsDialog(KShortcutsEditor::GlobalAction);
m_shortcutsDialog->addCollection(m_actionCollection);
#endif
#endif
connect(&m_streamDialog, &StreamDialog::requestedStreamStart, this,
&DiscordPage::startStream);
} }
void DiscordPage::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; qDebug(mainLog) << "Injecting " << name;
QWebEngineScript script; QWebEngineScript script;
@@ -122,20 +141,36 @@ void DiscordPage::injectScriptText(QString name, QString content) {
script.setSourceCode(content); script.setSourceCode(content);
script.setName(name); script.setName(name);
script.setWorldId(QWebEngineScript::MainWorld); script.setWorldId(QWebEngineScript::MainWorld);
script.setInjectionPoint(QWebEngineScript::DocumentCreation); script.setInjectionPoint(injectionPoint);
script.setRunsOnSubFrames(false); script.setRunsOnSubFrames(false);
scripts().insert(script); 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); QFile file(source);
if (!file.open(QIODevice::ReadOnly)) { if (!file.open(QIODevice::ReadOnly)) {
qFatal("Failed to load %s with error: %s", source.toLatin1().constData(), qFatal("Failed to load %s with error: %s", source.toLatin1().constData(),
file.errorString().toLatin1().constData()); file.errorString().toLatin1().constData());
} else { } else {
injectScriptText(name, file.readAll()); (this->*inject)(name, file.readAll());
} }
} }
@@ -146,11 +181,10 @@ void DiscordPage::featurePermissionRequested(const QUrl &securityOrigin,
QWebEnginePage::PermissionGrantedByUser); QWebEnginePage::PermissionGrantedByUser);
if (feature == QWebEnginePage::Feature::MediaAudioCapture) { 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 " qDebug(virtmicLog) << "Starting Virtmic with no target to make sure "
"Discord can find all the audio devices"; "Discord can find all the audio devices";
m_virtmicProcess.start(QApplication::arguments()[0], m_userScript.startVirtmic("None");
{"--virtmic", "None"});
} }
} }
} }
@@ -177,78 +211,101 @@ QWebEnginePage *DiscordPage::createWindow(QWebEnginePage::WebWindowType type) {
return new ExternalPage; return new ExternalPage;
} }
void DiscordPage::stopVirtmic() { const QMap<QString, QString> cssAnsiColorMap = {{"black", "30"},
if (m_virtmicProcess.state() == QProcess::Running) { {"red", "31"},
qDebug(virtmicLog) << "Stopping Virtmic"; {"green", "32"},
m_virtmicProcess.kill(); {"yellow", "33"},
m_virtmicProcess.waitForFinished(); {"blue", "34"},
} {"magenta", "35"},
} {"cyan", "36"},
{"white", "37"},
void DiscordPage::startVirtmic(QString target) { {"gray", "90"},
if (target != "None") { {"bright-red", "91"},
qDebug(virtmicLog) << "Starting Virtmic with target" << target; {"bright-green", "92"},
m_virtmicProcess.start(QApplication::arguments()[0], {"--virtmic", target}); {"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( void DiscordPage::javaScriptConsoleMessage(
QWebEnginePage::JavaScriptConsoleMessageLevel level, const QString &message, QWebEnginePage::JavaScriptConsoleMessageLevel level, const QString &message,
int lineNumber, const QString &sourceID) { int lineNumber, const QString &sourceID) {
if (message == "!discord-screenaudio-start-stream") { auto colorSegments = message.split("%c");
if (m_streamDialog.isHidden()) if (colorSegments[0] != "") {
m_streamDialog.setHidden(false); for (auto line : colorSegments[0].split("\n"))
else qDebug(discordLog) << line.toUtf8().constData();
m_streamDialog.activateWindow(); }
m_streamDialog.updateTargets(); for (auto segment : colorSegments.mid(1)) {
} else if (message == "!discord-screenaudio-stream-stopped") { auto lines = segment.split("\n");
stopVirtmic(); QString ansi;
} else if (message == "!discord-screenaudio-about") { uint endOfStyles = lines.length();
#ifdef KXMLGUI for (auto line = 1; line < lines.length(); line++) {
m_helpMenu->aboutApplication(); if (!lines[line].endsWith(";")) {
#endif endOfStyles = line;
} else if (message == "!discord-screenaudio-keybinds") { break;
#ifdef KXMLGUI }
#ifdef KGLOBALACCEL if (lines[line] == "font-weight: bold;")
m_shortcutsDialog->show(); ansi += "\033[1m";
#else else if (lines[line].startsWith("color: ")) {
QMessageBox::information(MainWindow::instance(), "discord-screenaudio", auto color = lines[line].mid(7).chopped(1);
"Keybinds are not supported on this platform " if (cssAnsiColorMap.find(color) != cssAnsiColorMap.end())
"(KGlobalAccel is not available).", ansi += "\033[" + cssAnsiColorMap[color] + "m";
QMessageBox::Ok); }
#endif }
#else qDebug(discordLog) << (ansi + lines[0].trimmed() + "\033[0m " +
QMessageBox::information(MainWindow::instance(), "discord-screenaudio", lines[endOfStyles].trimmed())
"Keybinds are not supported on this platform " .toUtf8()
"(KXmlGui and KGlobalAccel are not available).", .constData();
QMessageBox::Ok); for (auto line : lines.mid(endOfStyles + 1)) {
#endif qDebug(discordLog) << line.toUtf8().constData();
} else if (message.startsWith("dsa: ")) { }
qDebug(userscriptLog) << message.mid(5).toUtf8().constData();
} else {
qDebug(discordLog) << message;
} }
} }
void DiscordPage::startStream(QString target, uint width, uint height, UserScript *DiscordPage::userScript() { return &m_userScript; }
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() { void DiscordPage::setupArrpc() {
qDebug(shortcutLog) << "Toggling mute"; QFile nodejs("/usr/bin/node");
runJavaScript("window.discordScreenaudioToggleMute();"); 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() { injectFile(&DiscordPage::injectScript, "arrpc_bridge_mod.js",
qDebug(shortcutLog) << "Toggling deafen"; ":/assets/arrpc_bridge_mod.js");
runJavaScript("window.discordScreenaudioToggleDeafen();"); }
} }

View File

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

View File

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

View File

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

View File

@@ -24,19 +24,15 @@ int main(int argc, char *argv[]) {
"Custom Discord client with the ability to stream audio on Linux"); "Custom Discord client with the ability to stream audio on Linux");
parser.addHelpOption(); parser.addHelpOption();
parser.addVersionOption(); parser.addVersionOption();
QCommandLineOption virtmicOption("virtmic", "Start the Virtual Microphone",
"target");
parser.addOption(virtmicOption);
QCommandLineOption degubOption("remote-debugging", QCommandLineOption degubOption("remote-debugging",
"Open Chromium Remote Debugging on port 9222"); "Open Chromium Remote Debugging on port 9222");
parser.addOption(degubOption); parser.addOption(degubOption);
QCommandLineOption notifySendOption(
"notify-send", "Use notify-send instead of QT/KF5 notifications");
parser.addOption(notifySendOption);
parser.process(app); parser.process(app);
if (parser.isSet(virtmicOption)) {
Virtmic::start(parser.value(virtmicOption));
}
qputenv("QTWEBENGINE_CHROMIUM_FLAGS", qputenv("QTWEBENGINE_CHROMIUM_FLAGS",
"--enable-features=WebRTCPipeWireCapturer " + "--enable-features=WebRTCPipeWireCapturer " +
qgetenv("QTWEBENGINE_CHROMIUM_FLAGS")); qgetenv("QTWEBENGINE_CHROMIUM_FLAGS"));
@@ -46,7 +42,7 @@ int main(int argc, char *argv[]) {
"--remote-debugging-port=9222 " + "--remote-debugging-port=9222 " +
qgetenv("QTWEBENGINE_CHROMIUM_FLAGS")); qgetenv("QTWEBENGINE_CHROMIUM_FLAGS"));
MainWindow w; MainWindow w(parser.isSet(notifySendOption));
w.show(); w.show();
return app.exec(); return app.exec();

View File

@@ -14,50 +14,33 @@
#include <QPushButton> #include <QPushButton>
#include <QSpacerItem> #include <QSpacerItem>
#include <QThread> #include <QThread>
#include <QTimer>
#include <QUrl> #include <QUrl>
#include <QWebEngineNotification> #include <QWebEngineFullScreenRequest>
#include <QWebEngineProfile>
#include <QWebEngineScript>
#include <QWebEngineScriptCollection>
#include <QWebEngineSettings>
#include <QWidget> #include <QWidget>
MainWindow *MainWindow::m_instance = nullptr; MainWindow *MainWindow::m_instance = nullptr;
MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) { MainWindow::MainWindow(bool useNotifySend, QWidget *parent)
: QMainWindow(parent) {
assert(MainWindow::m_instance == nullptr); assert(MainWindow::m_instance == nullptr);
MainWindow::m_instance = this; MainWindow::m_instance = this;
setupWebView(); setupSettings();
resize(1000, 700); m_settings->setValue("useNotifySend", useNotifySend);
showMaximized(); m_centralWidget = new CentralWidget(this);
} setCentralWidget(m_centralWidget);
setupTrayIcon();
void MainWindow::setupWebView() { if (m_settings->contains("geometry")) {
auto page = new DiscordPage(this); restoreGeometry(m_settings->value("geometry").toByteArray());
connect(page, &QWebEnginePage::fullScreenRequested, this, } else {
&MainWindow::fullScreenRequested); resize(1000, 700);
showMaximized();
m_webView = new QWebEngineView(this); }
m_webView->setPage(page); if (m_settings->value("trayIcon", false).toBool() &&
m_settings->value("startHidden", false).toBool()) {
#ifdef KNOTIFICATIONS hide();
QWebEngineProfile::defaultProfile()->setNotificationPresenter( QTimer::singleShot(0, [=]() { hide(); });
[&](std::unique_ptr<QWebEngineNotification> notificationInfo) { }
KNotification *notification = new KNotification("discordNotification");
notification->setTitle(notificationInfo->title());
notification->setText(notificationInfo->message());
notification->setPixmap(QPixmap::fromImage(notificationInfo->icon()));
notification->setDefaultAction("View");
connect(notification, &KNotification::defaultActivated,
[&, notificationInfo = std::move(notificationInfo)]() {
notificationInfo->click();
activateWindow();
});
notification->sendEvent();
});
#endif
setCentralWidget(m_webView);
} }
void MainWindow::fullScreenRequested( void MainWindow::fullScreenRequested(
@@ -71,6 +54,79 @@ 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("discord-screenaudio", "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 {
m_settings->setValue("geometry", saveGeometry());
QApplication::quit();
}
}
MainWindow *MainWindow::instance() { return m_instance; } MainWindow *MainWindow::instance() { return m_instance; }
CentralWidget *MainWindow::centralWidget() {
return instance()->m_centralWidget;
};

View File

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

View File

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

View File

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

184
src/userscript.cpp Normal file
View File

@@ -0,0 +1,184 @@
#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.5",
"https://github.com/Soundux/rohrkabel");
aboutData.addComponent(
"Soundux/channel ",
"A C++ implementation of Rust's std::sync::mpsc::channel", nullptr,
"https://github.com/Soundux/channel");
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);
}

83
src/userscript.h Normal file
View File

@@ -0,0 +1,83 @@
#pragma once
#include "streamdialog.h"
#include <QObject>
#include <QProcess>
#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;
#ifdef KXMLGUI
KHelpMenu *m_helpMenu;
#ifdef KGLOBALACCEL
KActionCollection *m_actionCollection;
KShortcutsDialog *m_shortcutsDialog;
#endif
#endif
void setupHelpMenu();
void setupShortcutsDialog();
void setupStreamDialog();
void setupVirtmic();
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);
private Q_SLOTS:
void startStream(bool video, bool audio, int width, int height, int frameRate,
QString target);
};

View File

@@ -1,175 +1,174 @@
#include "virtmic.h" #include "virtmic.h"
#include "log.h" #include "log.h"
#include <rohrkabel/loop/main.hpp> QThread virtmicThread;
#include <rohrkabel/registry/registry.hpp> std::unique_ptr<pipewire::sender<Virtmic::set_target, Virtmic::terminate>>
senderr;
std::unique_ptr<cr::receiver<Virtmic::new_targets>> receiverr;
namespace Virtmic { void Virtmic::instance() {
if (!virtmicThread.isRunning()) {
const QStringList EXCLUDE_TARGETS{"Chromium input", "discord-screenaudio"}; auto [main_sender, main_receiver] = cr::channel<new_targets>();
auto [pw_sender, pw_receiver] = pipewire::channel<set_target, terminate>();
QVector<QString> getTargets() { Virtmic *virtmic =
auto main_loop = pipewire::main_loop(); new Virtmic(std::move(pw_receiver), std::move(main_sender));
auto context = pipewire::context(main_loop); virtmic->moveToThread(&virtmicThread);
auto core = pipewire::core(context); virtmicThread.start();
auto reg = pipewire::registry(core); QMetaObject::invokeMethod(virtmic, "run");
receiverr = std::make_unique<cr::receiver<Virtmic::new_targets>>(
QVector<QString> targets; std::move(main_receiver));
senderr = std::make_unique<
auto reg_listener = reg.listen<pipewire::registry_listener>(); pipewire::sender<Virtmic::set_target, Virtmic::terminate>>(
reg_listener.on<pipewire::registry_event::global>( std::move(pw_sender));
[&](const pipewire::global &global) { }
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"]);
if (name != "" && !EXCLUDE_TARGETS.contains(name) &&
!targets.contains(name)) {
targets.append(name);
}
}
});
core.update();
return targets;
} }
void start(QString _target) { void Virtmic::setTarget(QString target) {
std::map<std::uint32_t, pipewire::port> ports; senderr.get()->send<Virtmic::set_target>({target});
std::unique_ptr<pipewire::port> virt_fl, virt_fr; }
std::map<std::uint32_t, pipewire::node_info> nodes; void Virtmic::getTargets() { senderr.get()->send<Virtmic::get_targets>(); }
std::map<std::uint32_t, pipewire::link_factory> links;
auto main_loop = pipewire::main_loop(); Virtmic::Virtmic(pipewire::receiver<set_target, terminate> receiver,
auto context = pipewire::context(main_loop); cr::sender<new_targets> sender) {
auto core = pipewire::core(context); m_receiver = std::make_unique<pipewire::receiver<set_target, terminate>>(
auto reg = pipewire::registry(core); std::move(receiver));
m_sender = std::make_unique<cr::sender<new_targets>>(std::move(sender));
auto link = [&](const std::string &target, pipewire::core &core) { virtual_mic = std::make_unique<pipewire::proxy>(
for (const auto &[port_id, port] : ports) { std::move(*core.create("adapter",
if (!virt_fl || !virt_fr) {{"node.name", "discord-screenaudio-virtmic"},
continue; {"media.class", "Audio/Source/Virtual"},
{"factory.name", "support.null-audio-sink"},
if (links.count(port_id)) {"audio.channels", "2"},
continue; {"audio.position", "FL,FR"}},
pipewire::node::type, pipewire::node::version,
if (port.info().direction == pipewire::port_direction::input) pipewire::update_strategy::none)
continue; .get()));
metadata_listener.on<pipewire::registry_event::global>(
if (!port.info().props.count("node.id")) [&](const auto &global) { globalEvent(global); });
continue; metadata_listener.on<pipewire::registry_event::global_removed>(
[&](const std::uint32_t id) { globalRemovedEvent(id); });
auto parent_id = std::stoul(port.info().props["node.id"]); }
if (!nodes.count(parent_id))
continue;
auto &parent = nodes.at(parent_id);
auto name = parent.props["application.name"];
if (name == target ||
(target == "[All Desktop Audio]" &&
!EXCLUDE_TARGETS.contains(QString::fromStdString(name)))) {
auto fl = port.info().props["audio.channel"] == "FL";
links.emplace(
port_id,
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(port_id)
.arg(fl ? virt_fl->info().id
: virt_fr->info().id)
.toUtf8()
.data();
}
}
};
std::string target = _target.toLatin1().toStdString();
auto virtual_mic = core.create("adapter",
{{"node.name", "discord-screenaudio-virtmic"},
{"media.class", "Audio/Source/Virtual"},
{"factory.name", "support.null-audio-sink"},
{"audio.channels", "2"},
{"audio.position", "FL,FR"}},
pipewire::node::type, pipewire::node::version,
pipewire::update_strategy::none);
if (target == "[None]") {
while (true) {
main_loop.run();
}
return;
}
auto reg_events = reg.listen<pipewire::registry_listener>();
reg_events.on<pipewire::registry_event::global>(
[&](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"))
return;
qDebug(virtmicLog) << QString("Added: %1")
.arg(QString::fromStdString(
node.info().props["application.name"]))
.toUtf8()
.data();
if (!nodes.count(global.id)) {
nodes.emplace(global.id, node.info());
link(target, core);
}
}
if (global.type == pipewire::port::type) {
auto port = reg.bind<pipewire::port>(global.id);
auto info = port.info();
if (info.props.count("node.id")) {
auto node_id = std::stoul(info.props["node.id"]);
if (node_id == virtual_mic.id() &&
info.direction == pipewire::port_direction::input) {
if (info.props["audio.channel"] == "FL") {
virt_fl = std::make_unique<pipewire::port>(std::move(port));
} else {
virt_fr = std::make_unique<pipewire::port>(std::move(port));
}
} else {
ports.emplace(global.id, std::move(port));
}
link(target, core);
}
}
});
reg_events.on<pipewire::registry_event::global_removed>(
[&](const std::uint32_t id) {
if (nodes.count(id)) {
auto info = nodes.at(id);
qDebug(virtmicLog) << QString("Removed: %1")
.arg(QString::fromStdString(
info.props["application.name"].data()))
.toUtf8()
.data();
nodes.erase(id);
}
if (ports.count(id)) {
ports.erase(id);
}
if (links.count(id)) {
links.erase(id);
}
});
void Virtmic::run() {
while (true) { while (true) {
main_loop.run(); main_loop.run();
} }
} }
} // namespace Virtmic void Virtmic::link() {
for (const auto &[port_id, port] : ports) {
auto port_info = port.info();
if (!virt_fl || !virt_fr)
continue;
if (links.count(port_id))
continue;
if (port_info.direction == pipewire::port_direction::input)
continue;
if (!port_info.props.count("node.id"))
continue;
auto parent_id = std::stoul(port_info.props["node.id"]);
if (!nodes.count(parent_id))
continue;
auto &parent = nodes[parent_id];
QString name;
if (parent.props.count("application.name") &&
parent.props["application.name"] != "")
name = QString::fromStdString(parent.props["application.name"]);
else
name = QString::fromStdString(parent.props["application.process.binary"]);
if (name == target ||
(target == "[All Desktop Audio]" && !EXCLUDE_TARGETS.contains(name))) {
auto fl = port_info.props["audio.channel"] == "FL";
links.emplace(port_id,
*core.create_simple<pipewire::link>(fl ? virt_fl->info().id
: virt_fr->info().id,
port_id)
.get());
qDebug(virtmicLog) << QString("Link: %1:%2 -> %3")
.arg(name)
.arg(port_id)
.arg(fl ? virt_fl->info().id
: virt_fr->info().id)
.toUtf8()
.data();
}
}
}
void Virtmic::unlink() { links.clear(); }
void Virtmic::globalEvent(const pipewire::global &global) {
if (global.type == pipewire::node::type) {
auto node = *reg.bind<pipewire::node>(global.id).get();
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(name))
.toUtf8()
.data();
if (!nodes.count(global.id)) {
nodes.emplace(global.id, node.info());
link();
}
}
if (global.type == pipewire::port::type) {
auto port = *reg.bind<pipewire::port>(global.id).get();
auto info = port.info();
if (info.props.count("node.id")) {
auto node_id = std::stoul(info.props["node.id"]);
if (node_id == virtual_mic.get()->id() &&
info.direction == pipewire::port_direction::input) {
if (info.props["audio.channel"] == "FL") {
virt_fl = std::make_unique<pipewire::port>(std::move(port));
} else {
virt_fr = std::make_unique<pipewire::port>(std::move(port));
}
} else {
ports.emplace(global.id, std::move(port));
}
link();
}
}
}
void Virtmic::globalRemovedEvent(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(name))
.toUtf8()
.data();
nodes.erase(id);
}
if (ports.count(id)) {
ports.erase(id);
}
if (links.count(id)) {
links.erase(id);
}
}

View File

@@ -1,12 +1,63 @@
#pragma once #pragma once
#include <QString> #include <cr/channel.hpp>
#include <QVector>
#include <iostream> #include <iostream>
#include <rohrkabel/channel/channel.hpp>
#include <rohrkabel/main_loop.hpp>
#include <rohrkabel/registry/registry.hpp>
namespace Virtmic { #include <QMap>
#include <QScopedPointer>
#include <QStringList>
#include <QThread>
QVector<QString> getTargets(); class Virtmic : public QObject {
void start(QString _target); Q_OBJECT
public:
static void setTarget(QString target);
static void getTargets();
} // namespace Virtmic public:
struct set_target {
QString name;
};
struct get_targets {};
struct terminate {};
struct new_targets {
QStringList targets;
};
protected:
static void instance();
protected:
Virtmic(pipewire::receiver<set_target, terminate> receiver,
cr::sender<new_targets> sender);
void run();
private:
std::unique_ptr<pipewire::receiver<set_target, terminate>> m_receiver;
std::unique_ptr<cr::sender<new_targets>> m_sender;
const QStringList EXCLUDE_TARGETS{"Chromium input", "discord-screenaudio"};
QString target;
pipewire::main_loop main_loop = pipewire::main_loop();
pipewire::context context = pipewire::context(main_loop);
pipewire::core core = pipewire::core(context);
pipewire::registry reg = pipewire::registry(core);
pipewire::registry_listener metadata_listener =
reg.listen<pipewire::registry_listener>();
std::unique_ptr<pipewire::proxy> virtual_mic;
std::map<uint32_t, pipewire::port> ports;
std::unique_ptr<pipewire::port> virt_fl, virt_fr;
std::map<uint32_t, pipewire::node_info> nodes;
std::map<uint32_t, pipewire::link> links;
void link();
void unlink();
void globalEvent(const pipewire::global &global);
void globalRemovedEvent(const std::uint32_t id);
};

1
submodules/channel Submodule

Submodule submodules/channel added at 6977815409