Compare commits
95 Commits
testing_ha
...
master
Author | SHA1 | Date |
---|---|---|
Wizzard | b8d3bbd3e0 | |
Wizzard | a8d7965c43 | |
Wizzard | 1a60367c0a | |
Wizzard | 65b7556523 | |
Wizzard | 4ab61017db | |
Wizzard | f87096f627 | |
Wizzard | 74bbe65688 | |
Wizzard | 89c9a6651e | |
Wizzard | 73d49795ea | |
Wizzard | 33a0f68cf1 | |
Wizzard | 17517d1b46 | |
Wizzard | 12c8a7e49a | |
Wizzard | 1fc8436618 | |
Wizzard | 03f52e221f | |
Wizzard | 18e6598a95 | |
Wizzard | 829729aef2 | |
Wizzard | 58c3ac0b71 | |
Wizzard | 2858eb3854 | |
Malte Jürgens | 94b27f5b6a | |
Malte Jürgens | 8a6f49b949 | |
Malte Jürgens | 47fd620876 | |
Vitalya | c15250498b | |
Vitalya | 6f0303206e | |
Malte Jürgens | 6e86647c95 | |
Vitalya | 798fb3d5e4 | |
Malte Jürgens | 6c9b76ed90 | |
Vitalya | b582584c69 | |
Malte Jürgens | 7b6e8fc473 | |
Malte Jürgens | 8f0a810539 | |
Malte Jürgens | a6eb82948f | |
Malte Jürgens | 24727f398a | |
Daniel Mensinger | c43e9953a5 | |
Malte Jürgens | 08cb713e8c | |
Malte Jürgens | 27faed4a3a | |
Malte Jürgens | 10fff86ca4 | |
Malte Jürgens | 87e84dec5c | |
Malte Jürgens | 4b782133ae | |
Malte Jürgens | d5257207bc | |
Malte Jürgens | 423884ae0c | |
Malte Jürgens | 7ea3d0aab1 | |
Malte Jürgens | 18b15f5cf4 | |
Malte Jürgens | 96bca52d0d | |
Malte Jürgens | 3540774c82 | |
Malte Jürgens | b5435acdd8 | |
Andreas | 38c2b92e2d | |
Malte Jürgens | e447206082 | |
Malte Jürgens | 77f31b63aa | |
Malte Jürgens | 0b9fc100c2 | |
Malte Jürgens | f73524de27 | |
Malte Jürgens | 8a8690fe53 | |
Malte Jürgens | 259e6dc75d | |
Malte Jürgens | 9cbdca0441 | |
Malte Jürgens | a9ddc0216f | |
Malte Jürgens | b39e23d462 | |
llyyr | 331fb8f4ca | |
llyyr | 335a4456ed | |
llyyr | 26a2ca7b05 | |
Malte Jürgens | 30c0526ff7 | |
Malte Jürgens | 3119e1df19 | |
Malte Jürgens | 63180f5d53 | |
Malte Jürgens | 78d43991ab | |
Alve Svarén | 193b69f45f | |
Malte Jürgens | 485ff9634b | |
Malte Jürgens | 74fdef683f | |
Malte Jürgens | 02fdc62fbe | |
Malte Jürgens | d9e3704176 | |
Malte Jürgens | 36c8cfedb7 | |
Malte Jürgens | c52daab420 | |
Malte Jürgens | 8c4fae3410 | |
Malte Jürgens | ac71e9bbcb | |
Malte Jürgens | fc0dbb5b34 | |
Malte Jürgens | 5df24629e6 | |
Malte Jürgens | 1477a9d4c0 | |
Malte Jürgens | 0b12487dfd | |
Malte Jürgens | a10cfda56d | |
Malte Jürgens | 2a809e163e | |
Malte Jürgens | bc317d5531 | |
Malte Jürgens | b4db987217 | |
Malte Jürgens | 9f46e710a9 | |
Malte Jürgens | d6641a7a6e | |
Malte Jürgens | b836be6530 | |
Malte Jürgens | bfb0714b13 | |
Malte Jürgens | 374b854261 | |
Malte Jürgens | 1f6105f76b | |
Malte Jürgens | 3071159332 | |
Malte Jürgens | f2de080e1b | |
Malte Jürgens | f4a60f281d | |
Malte Jürgens | d693535d53 | |
Malte Jürgens | e41af697f7 | |
Malte Jürgens | b0a8815bb8 | |
Malte Jürgens | 150fd4364e | |
Malte Jürgens | a0a2924796 | |
Malte Jürgens | 3c48621427 | |
Malte Jürgens | 9faabe1f3e | |
Kacper Herchel | 68473d04d8 |
|
@ -1,2 +1,20 @@
|
||||||
/build
|
# Ignore build and output directories
|
||||||
.vscode
|
/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
|
||||||
|
|
|
@ -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,17 @@ set(discord-screenaudio_SRC
|
||||||
src/discordpage.cpp
|
src/discordpage.cpp
|
||||||
src/streamdialog.cpp
|
src/streamdialog.cpp
|
||||||
src/log.cpp
|
src/log.cpp
|
||||||
|
src/userscript.cpp
|
||||||
|
src/centralwidget.cpp
|
||||||
|
src/localserver.cpp
|
||||||
|
src/centralwidget.h
|
||||||
|
src/discordpage.h
|
||||||
|
src/localserver.h
|
||||||
|
src/log.h
|
||||||
|
src/mainwindow.h
|
||||||
|
src/streamdialog.h
|
||||||
|
src/userscript.h
|
||||||
|
src/virtmic.h
|
||||||
resources.qrc
|
resources.qrc
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -66,7 +80,7 @@ add_subdirectory(submodules/rohrkabel)
|
||||||
|
|
||||||
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)
|
||||||
|
|
||||||
if(KF5Notifications_FOUND)
|
if(KF5Notifications_FOUND)
|
||||||
target_link_libraries(discord-screenaudio KF5::Notifications)
|
target_link_libraries(discord-screenaudio KF5::Notifications)
|
||||||
|
|
32
README.md
32
README.md
|
@ -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,9 +59,12 @@ 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`
|
||||||
|
|
||||||
|
With dnf:
|
||||||
|
`dnf install @development-tools cmake qt5-qtbase-devel qt5-qtwebengine-devel kf5-knotifications-devel kf5-kxmlgui-devel kf5-kglobalaccel-devel pkgconfig pipewire-devel git`
|
||||||
|
|
||||||
### Building
|
### Building
|
||||||
|
|
||||||
First, clone the repository:
|
First, clone the repository:
|
||||||
|
@ -82,7 +87,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 +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
|
application audio, and this microphone is then fed to the Discord stream by
|
||||||
intercepting a API call of Discord.
|
intercepting a API call of Discord.
|
||||||
|
|
||||||
|
### Drag and drop doesn't work in the Flatpak
|
||||||
|
|
||||||
|
This is due to sandboxing limitations of Flatpak. The main Discord Flatpak has
|
||||||
|
the same problem. If you still want to use drag and drop, you can disable most
|
||||||
|
of Flatpak's sandboxing by installing
|
||||||
|
[Flatseal](https://flathub.org/apps/details/com.github.tchx84.Flatseal) and
|
||||||
|
allowing access to "All system files" under the "Filesystem" section.
|
||||||
|
|
||||||
|
### Is there any way to add custom CSS / a theme?
|
||||||
|
|
||||||
|
Yes, you can add all your styles into
|
||||||
|
`~/.config/discord-screenaudio/userstyles.css` (or
|
||||||
|
`~/.var/app/de.shorsh.discord-screenaudio/config/discord-screenaudio/userstyles.css`
|
||||||
|
if you are using the Flatpak). But please note that due to QtWebEngine
|
||||||
|
limitations concerning content security policies, you can't use any external
|
||||||
|
files (like `@import` or `url()`).
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
Copyright (C) 2022 Malte Jürgens
|
Copyright (C) 2022 Malte Jürgens
|
||||||
|
|
File diff suppressed because one or more lines are too long
|
@ -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);
|
|
@ -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>
|
||||||
|
|
|
@ -1,5 +1,3 @@
|
||||||
// From v0.4
|
|
||||||
|
|
||||||
navigator.mediaDevices.chromiumGetDisplayMedia =
|
navigator.mediaDevices.chromiumGetDisplayMedia =
|
||||||
navigator.mediaDevices.getDisplayMedia;
|
navigator.mediaDevices.getDisplayMedia;
|
||||||
|
|
||||||
|
@ -16,21 +14,21 @@ const getAudioDevice = async (nameOfAudioDevice) => {
|
||||||
let devices = await navigator.mediaDevices.enumerateDevices();
|
let devices = await navigator.mediaDevices.enumerateDevices();
|
||||||
audioDevice = devices.find(({ label }) => label === nameOfAudioDevice);
|
audioDevice = devices.find(({ label }) => label === nameOfAudioDevice);
|
||||||
if (!audioDevice)
|
if (!audioDevice)
|
||||||
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 {
|
||||||
let myDiscordAudioSink = await getAudioDevice(
|
let myDiscordAudioSink = await getAudioDevice(
|
||||||
"discord-screenaudio-virtmic"
|
"discord-awesomeaudio-virtmic"
|
||||||
);
|
);
|
||||||
id = myDiscordAudioSink.deviceId;
|
id = myDiscordAudioSink.deviceId;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
@ -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,17 +69,121 @@ 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 button = document.createElement("button");
|
||||||
|
button.style.marginBottom = "20px";
|
||||||
|
button.classList =
|
||||||
|
"button-ejjZWC lookFilled-1H2Jvj colorBrand-2M3O3N sizeSmall-3R2P2p grow-2T4nbg";
|
||||||
|
button.innerText = text;
|
||||||
|
button.addEventListener("click", onClick);
|
||||||
|
return button;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createSwitch(text, enabled, onClick) {
|
||||||
|
const container = document.createElement("div");
|
||||||
|
container.style.marginBottom = "20px";
|
||||||
|
container.className = "labelRow-NnoUIp";
|
||||||
|
|
||||||
|
const label = document.createElement("label");
|
||||||
|
label.innerText = text;
|
||||||
|
label.className = "title-2yADjX";
|
||||||
|
container.appendChild(label);
|
||||||
|
|
||||||
|
const svg = document.createElement("div");
|
||||||
|
container.appendChild(svg);
|
||||||
|
|
||||||
|
function setSvgDisabled() {
|
||||||
|
svg.innerHTML = `<div class="container-1QtPKm default-colors" style="opacity: 1; background-color: rgb(114, 118, 125);"><svg class="slider-HJFN2i" viewBox="0 0 28 20" preserveAspectRatio="xMinYMid meet" aria-hidden="true" style="left: -3px;"><rect fill="white" x="4" y="0" height="20" width="20" rx="10"></rect><svg viewBox="0 0 20 20" fill="none"><path fill="rgba(114, 118, 125, 1)" d="M5.13231 6.72963L6.7233 5.13864L14.855 13.2704L13.264 14.8614L5.13231 6.72963Z"></path><path fill="rgba(114, 118, 125, 1)" d="M13.2704 5.13864L14.8614 6.72963L6.72963 14.8614L5.13864 13.2704L13.2704 5.13864Z"></path></svg></svg><input id="uid_84" type="checkbox" class="input-125oad" tabindex="0"></div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setSvgEnabled() {
|
||||||
|
svg.innerHTML = `<div class="container-1QtPKm default-colors checked-16gMAN" style="opacity: 1; background-color: rgb(59, 165, 92);"><svg class="slider-HJFN2i" viewBox="0 0 28 20" preserveAspectRatio="xMinYMid meet" aria-hidden="true" style="left: 12px;"><rect fill="white" x="4" y="0" height="20" width="20" rx="10"></rect><svg viewBox="0 0 20 20" fill="none"><path fill="rgba(59, 165, 92, 1)" d="M7.89561 14.8538L6.30462 13.2629L14.3099 5.25755L15.9009 6.84854L7.89561 14.8538Z"></path><path fill="rgba(59, 165, 92, 1)" d="M4.08643 11.0903L5.67742 9.49929L9.4485 13.2704L7.85751 14.8614L4.08643 11.0903Z"></path></svg></svg><input id="uid_74" type="checkbox" class="input-125oad" tabindex="0" checked=""></div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateSvg() {
|
||||||
|
if (enabled) setSvgEnabled();
|
||||||
|
else setSvgDisabled();
|
||||||
|
}
|
||||||
|
|
||||||
|
container.addEventListener("click", () => {
|
||||||
|
enabled = !enabled;
|
||||||
|
updateSvg();
|
||||||
|
onClick(enabled);
|
||||||
|
});
|
||||||
|
updateSvg();
|
||||||
|
|
||||||
|
return container;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fix for broken discord notifications after restart
|
||||||
|
// (https://github.com/maltejur/discord-screenaudio/issues/17)
|
||||||
|
Notification.requestPermission();
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
new QWebChannel(qt.webChannelTransport, (channel) => {
|
||||||
|
userscript = channel.objects.userscript;
|
||||||
|
main();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function main() {
|
||||||
|
userscript.muteToggled.connect(() => {
|
||||||
|
console.log("Toggling mute");
|
||||||
|
muteBtn && muteBtn.click();
|
||||||
|
});
|
||||||
|
|
||||||
|
userscript.deafenToggled.connect(() => {
|
||||||
|
console.log("Toggling deafen");
|
||||||
|
deafenBtn && deafenBtn.click();
|
||||||
|
});
|
||||||
|
|
||||||
|
userscript.streamStarted.connect((video, width, height, frameRate) => {
|
||||||
|
resolutionString = video ? `${height}p ${frameRate}FPS` : "Audio Only";
|
||||||
|
setGetDisplayMedia(video, {
|
||||||
|
audio: true,
|
||||||
|
video: { width, height, frameRate },
|
||||||
|
});
|
||||||
|
streamStartBtn.click();
|
||||||
|
streamStartBtn.style.display = streamStartBtnInitialDisplay;
|
||||||
|
streamStartBtnClone.remove();
|
||||||
|
});
|
||||||
|
|
||||||
|
function updateUserstyles() {
|
||||||
|
userscript.log("Loading userstyles...");
|
||||||
|
userscript.loadingMessage = "Loading userstyles...";
|
||||||
|
let stylesheet = document.getElementById("discordScreenaudioUserstyles");
|
||||||
|
if (stylesheet) {
|
||||||
|
userscript.log("Removing old userstyles...");
|
||||||
|
stylesheet.remove();
|
||||||
|
}
|
||||||
|
stylesheet = document.createElement("style");
|
||||||
|
stylesheet.id = "discordScreenaudioUserstyles";
|
||||||
|
stylesheet.innerText = userscript.userstyles;
|
||||||
|
document.head.appendChild(stylesheet);
|
||||||
|
userscript.log("Finished loading userstyles");
|
||||||
|
userscript.loadingMessage = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
userscript.userstylesChanged.connect(updateUserstyles);
|
||||||
|
setTimeout(() => updateUserstyles());
|
||||||
|
|
||||||
|
setInterval(async () => {
|
||||||
const streamActive =
|
const streamActive =
|
||||||
document.getElementsByClassName("panel-2ZFCRb activityPanel-9icbyU")
|
document.getElementsByClassName("panel-2ZFCRb activityPanel-9icbyU")
|
||||||
.length > 0;
|
.length > 0;
|
||||||
|
|
||||||
if (!streamActive && wasStreamActive)
|
if (!streamActive && wasStreamActive) userscript.stopVirtmic();
|
||||||
console.log("!discord-screenaudio-stream-stopped");
|
|
||||||
wasStreamActive = streamActive;
|
wasStreamActive = streamActive;
|
||||||
|
|
||||||
if (streamActive) {
|
if (streamActive) {
|
||||||
|
@ -95,59 +198,54 @@ setInterval(() => {
|
||||||
hiddenElements.length = 0;
|
hiddenElements.length = 0;
|
||||||
} else {
|
} else {
|
||||||
for (const el of [
|
for (const el of [
|
||||||
document.getElementsByClassName("actionButtons-2vEOUh")?.[0]?.children[1],
|
document.getElementsByClassName("actionButtons-2vEOUh")?.[0]
|
||||||
|
?.children[1],
|
||||||
document.querySelector(
|
document.querySelector(
|
||||||
".wrapper-3t3Yqv > div > div > div > div > .controlButton-2PMNom"
|
".wrapper-3t3Yqv > div > div > div > div > .controlButton-2PMNom"
|
||||||
),
|
),
|
||||||
]) {
|
]) {
|
||||||
if (!el) continue;
|
if (!el) continue;
|
||||||
if (el.classList.contains("discord-screenaudio-cloned")) continue;
|
if (el.classList.contains("discord-screenaudio-cloned")) continue;
|
||||||
el.classList.add("discord-screenaudio-cloned");
|
streamStartBtn = el;
|
||||||
elClone = el.cloneNode(true);
|
streamStartBtn.classList.add("discord-screenaudio-cloned");
|
||||||
elClone.title = "Share Your Screen with Audio";
|
|
||||||
elClone.addEventListener("click", () => {
|
streamStartBtnClone = streamStartBtn.cloneNode(true);
|
||||||
console.log("!discord-screenaudio-start-stream");
|
streamStartBtnClone.title = "Share Your Screen with Audio";
|
||||||
|
streamStartBtnClone.addEventListener("click", () => {
|
||||||
|
userscript.showStreamDialog();
|
||||||
});
|
});
|
||||||
|
|
||||||
const initialDisplay = el.style.display;
|
streamStartBtnInitialDisplay = streamStartBtn.style.display;
|
||||||
|
|
||||||
window.discordScreenaudioStartStream = (width, height, frameRate) => {
|
streamStartBtn.style.display = "none";
|
||||||
window.discordScreenaudioResolutionString = `${height}p ${frameRate}FPS`;
|
streamStartBtn.parentNode.insertBefore(streamStartBtnClone, el);
|
||||||
setGetDisplayMedia({
|
|
||||||
audio: true,
|
|
||||||
video: { width, height, frameRate },
|
|
||||||
});
|
|
||||||
el.click();
|
|
||||||
el.style.display = initialDisplay;
|
|
||||||
elClone.remove();
|
|
||||||
};
|
|
||||||
|
|
||||||
el.style.display = "none";
|
clonedElements.push(streamStartBtnClone);
|
||||||
el.parentNode.insertBefore(elClone, el);
|
hiddenElements.push(streamStartBtn);
|
||||||
|
|
||||||
clonedElements.push(elClone);
|
|
||||||
hiddenElements.push(el);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add about text in settings
|
// Add about text in settings
|
||||||
if (
|
if (
|
||||||
document.getElementsByClassName("dirscordScreenaudioAboutText").length == 0
|
document.getElementsByClassName("dirscordScreenaudioAboutText").length ==
|
||||||
|
0
|
||||||
) {
|
) {
|
||||||
for (const el of document.getElementsByClassName("info-3pQQBb")) {
|
for (const el of document.getElementsByClassName("info-3pQQBb")) {
|
||||||
let aboutEl;
|
let aboutEl;
|
||||||
if (window.discordScreenaudioKXMLGUI) {
|
if (userscript.kxmlgui) {
|
||||||
aboutEl = document.createElement("a");
|
aboutEl = document.createElement("a");
|
||||||
aboutEl.addEventListener("click", () => {
|
aboutEl.addEventListener("click", () => {
|
||||||
console.log("!discord-screenaudio-about");
|
userscript.showHelpMenu();
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
aboutEl = document.createElement("div");
|
aboutEl = document.createElement("div");
|
||||||
}
|
}
|
||||||
aboutEl.innerText = `discord-screenaudio ${window.discordScreenaudioVersion}`;
|
aboutEl.innerText = `discord-awesomeaudio ${userscript.version}`;
|
||||||
aboutEl.style.fontSize = "12px";
|
aboutEl.style.fontSize = "12px";
|
||||||
aboutEl.style.color = "var(--text-muted)";
|
aboutEl.style.color = "var(--text-muted)";
|
||||||
aboutEl.style.textTransform = "none";
|
aboutEl.style.textTransform = "none";
|
||||||
|
aboutEl.style.display = "inline-block";
|
||||||
|
aboutEl.style.width = "100%";
|
||||||
aboutEl.classList.add("dirscordScreenaudioAboutText");
|
aboutEl.classList.add("dirscordScreenaudioAboutText");
|
||||||
aboutEl.style.cursor = "pointer";
|
aboutEl.style.cursor = "pointer";
|
||||||
el.appendChild(aboutEl);
|
el.appendChild(aboutEl);
|
||||||
|
@ -163,7 +261,7 @@ setInterval(() => {
|
||||||
document
|
document
|
||||||
.getElementById("keybinds-tab")
|
.getElementById("keybinds-tab")
|
||||||
?.getElementsByClassName(
|
?.getElementsByClassName(
|
||||||
"container-3jbRo5 info-1hMolH fontSize16-3zr6Io browserNotice-1u-Y5o"
|
"container-3jbRo5 info-1hMolH browserNotice-1u-Y5o"
|
||||||
).length
|
).length
|
||||||
) {
|
) {
|
||||||
const el = document
|
const el = document
|
||||||
|
@ -171,36 +269,114 @@ setInterval(() => {
|
||||||
.getElementsByClassName("children-1xdcWE")[0];
|
.getElementsByClassName("children-1xdcWE")[0];
|
||||||
const div = document.createElement("div");
|
const div = document.createElement("div");
|
||||||
div.style.marginBottom = "50px";
|
div.style.marginBottom = "50px";
|
||||||
const button = document.createElement("button");
|
div.appendChild(
|
||||||
button.classList =
|
createButton("Edit Global Keybinds", () => {
|
||||||
"button-f2h6uQ lookFilled-yCfaCM colorBrand-I6CyqQ sizeSmall-wU2dO- grow-2sR_-F";
|
userscript.showShortcutsDialog();
|
||||||
button.innerText = "Edit Global Keybinds";
|
})
|
||||||
button.addEventListener("click", () => {
|
);
|
||||||
console.log("!discord-screenaudio-keybinds");
|
|
||||||
});
|
|
||||||
div.appendChild(button);
|
|
||||||
el.innerHTML = "";
|
el.innerHTML = "";
|
||||||
el.appendChild(div);
|
el.appendChild(div);
|
||||||
}
|
}
|
||||||
|
|
||||||
const muteBtn = document.getElementsByClassName(
|
const buttonContainer =
|
||||||
"button-12Fmur enabled-9OeuTA button-f2h6uQ lookBlank-21BCro colorBrand-I6CyqQ grow-2sR_-F"
|
document.getElementsByClassName("container-YkUktl")[0];
|
||||||
)[0];
|
if (!buttonContainer) {
|
||||||
window.discordScreenaudioToggleMute = () => muteBtn.click();
|
userscript.log(
|
||||||
const deafenBtn = document.getElementsByClassName(
|
"Cannot locate Mute/Deafen/Settings button container, please report this on GitHub"
|
||||||
"button-12Fmur enabled-9OeuTA button-f2h6uQ lookBlank-21BCro colorBrand-I6CyqQ grow-2sR_-F"
|
);
|
||||||
)[1];
|
}
|
||||||
window.discordScreenaudioToggleDeafen = () => deafenBtn.click();
|
|
||||||
|
|
||||||
if (window.discordScreenaudioResolutionString) {
|
muteBtn = buttonContainer
|
||||||
|
? buttonContainer.getElementsByTagName("button")[0]
|
||||||
|
: null;
|
||||||
|
|
||||||
|
deafenBtn = buttonContainer
|
||||||
|
? buttonContainer.getElementsByTagName("button")[1]
|
||||||
|
: null;
|
||||||
|
|
||||||
|
if (resolutionString) {
|
||||||
for (const el of document.getElementsByClassName(
|
for (const el of document.getElementsByClassName(
|
||||||
"qualityIndicator-39wQDy"
|
"qualityIndicator-39wQDy"
|
||||||
)) {
|
)) {
|
||||||
el.innerHTML = window.discordScreenaudioResolutionString;
|
el.innerHTML = resolutionString;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, 500);
|
|
||||||
|
|
||||||
// Fix for broken discord notifications after restart
|
const accountTab = document.getElementById("my-account-tab");
|
||||||
// (https://github.com/maltejur/discord-screenaudio/issues/17)
|
if (accountTab) {
|
||||||
Notification.requestPermission();
|
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"
|
||||||
|
}
|
||||||
|
};
|
|
@ -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>
|
File diff suppressed because one or more lines are too long
Binary file not shown.
After Width: | Height: | Size: 48 KiB |
|
@ -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>
|
<RCC>
|
||||||
<qresource>
|
<qresource prefix="/">
|
||||||
<file>assets/userscript.js</file>
|
<file>assets/userscript.js</file>
|
||||||
|
<file>assets/arrpc_bridge_mod.js</file>
|
||||||
|
<file>assets/arrpc.js</file>
|
||||||
<file>assets/de.shorsh.discord-screenaudio.png</file>
|
<file>assets/de.shorsh.discord-screenaudio.png</file>
|
||||||
|
<file>assets/vencord/vencord.js</file>
|
||||||
|
<file>assets/vencord/settings.patch</file>
|
||||||
|
<file>assets/vencord/plugin.js</file>
|
||||||
|
<file>assets/vencord/VencordNativeStub.ts</file>
|
||||||
</qresource>
|
</qresource>
|
||||||
</RCC>
|
</RCC>
|
|
@ -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
|
|
@ -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
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 "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,40 @@
|
||||||
#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");
|
||||||
|
QFile vencord(":/assets/vencord/vencord.js");
|
||||||
|
if (!vencord.open(QIODevice::ReadOnly))
|
||||||
|
qFatal("Failed to load vencord source with error: %s",
|
||||||
|
vencord.errorString().toLatin1().constData());
|
||||||
|
injectScript(
|
||||||
|
"vencord.js",
|
||||||
|
QString("window.discordScreenaudioVencordSettings = `%1`; %2")
|
||||||
|
.arg(m_userScript.vencordSend("VencordGetSettings", {}).toString(),
|
||||||
|
vencord.readAll()));
|
||||||
|
vencord.close();
|
||||||
|
setupUserStyles();
|
||||||
|
setupArrpc();
|
||||||
|
}
|
||||||
|
|
||||||
|
void DiscordPage::setupPermissions() {
|
||||||
settings()->setAttribute(QWebEngineSettings::ScreenCaptureEnabled, true);
|
settings()->setAttribute(QWebEngineSettings::ScreenCaptureEnabled, true);
|
||||||
settings()->setAttribute(QWebEngineSettings::JavascriptCanOpenWindows, true);
|
settings()->setAttribute(QWebEngineSettings::JavascriptCanOpenWindows, true);
|
||||||
settings()->setAttribute(QWebEngineSettings::AllowRunningInsecureContent,
|
settings()->setAttribute(QWebEngineSettings::AllowRunningInsecureContent,
|
||||||
|
@ -50,71 +62,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 +150,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 +190,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 +220,102 @@ 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
|
if (endOfStyles < lines.length())
|
||||||
QMessageBox::information(MainWindow::instance(), "discord-screenaudio",
|
qDebug(discordLog) << (ansi + lines[0].trimmed() + "\033[0m " +
|
||||||
"Keybinds are not supported on this platform "
|
lines[endOfStyles].trimmed())
|
||||||
"(KXmlGui and KGlobalAccel are not available).",
|
.toUtf8()
|
||||||
QMessageBox::Ok);
|
.constData();
|
||||||
#endif
|
for (auto line : lines.mid(endOfStyles + 1)) {
|
||||||
} else if (message.startsWith("dsa: ")) {
|
qDebug(discordLog) << line.toUtf8().constData();
|
||||||
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();");
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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();
|
||||||
|
}
|
|
@ -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(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");
|
||||||
|
|
|
@ -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);
|
||||||
|
|
30
src/main.cpp
30
src/main.cpp
|
@ -1,3 +1,4 @@
|
||||||
|
#include "localserver.h"
|
||||||
#include "mainwindow.h"
|
#include "mainwindow.h"
|
||||||
#include "virtmic.h"
|
#include "virtmic.h"
|
||||||
|
|
||||||
|
@ -7,15 +8,19 @@
|
||||||
|
|
||||||
#include <QApplication>
|
#include <QApplication>
|
||||||
#include <QCommandLineParser>
|
#include <QCommandLineParser>
|
||||||
|
#include <QLocalServer>
|
||||||
|
#include <QLocalSocket>
|
||||||
#include <QLoggingCategory>
|
#include <QLoggingCategory>
|
||||||
|
#include <QMessageBox>
|
||||||
|
|
||||||
int main(int argc, char *argv[]) {
|
int main(int argc, char *argv[]) {
|
||||||
QApplication app(argc, argv);
|
QApplication app(argc, argv);
|
||||||
QApplication::setApplicationName("discord-screenaudio");
|
|
||||||
|
QApplication::setApplicationName("discord-awesomeaudio");
|
||||||
QApplication::setWindowIcon(
|
QApplication::setWindowIcon(
|
||||||
QIcon(":assets/de.shorsh.discord-screenaudio.png"));
|
QIcon(":assets/de.shorsh.discord-screenaudio.png"));
|
||||||
QApplication::setApplicationVersion(DISCORD_SCEENAUDIO_VERSION_FULL);
|
QApplication::setApplicationVersion(DISCORD_SCEENAUDIO_VERSION_FULL);
|
||||||
QApplication::setDesktopFileName("de.shorsh.discord-screenaudio");
|
QApplication::setDesktopFileName("lol.deadzone.discord-awesomeaudio");
|
||||||
|
|
||||||
qSetMessagePattern("[%{category}] %{message}");
|
qSetMessagePattern("[%{category}] %{message}");
|
||||||
|
|
||||||
|
@ -30,6 +35,9 @@ int main(int argc, char *argv[]) {
|
||||||
QCommandLineOption degubOption("remote-debugging",
|
QCommandLineOption degubOption("remote-debugging",
|
||||||
"Open Chromium Remote Debugging on port 9222");
|
"Open Chromium Remote Debugging on port 9222");
|
||||||
parser.addOption(degubOption);
|
parser.addOption(degubOption);
|
||||||
|
QCommandLineOption notifySendOption(
|
||||||
|
"notify-send", "Use notify-send instead of QT/KF5 notifications");
|
||||||
|
parser.addOption(notifySendOption);
|
||||||
|
|
||||||
parser.process(app);
|
parser.process(app);
|
||||||
|
|
||||||
|
@ -38,8 +46,6 @@ int main(int argc, char *argv[]) {
|
||||||
}
|
}
|
||||||
|
|
||||||
qputenv("QTWEBENGINE_CHROMIUM_FLAGS",
|
qputenv("QTWEBENGINE_CHROMIUM_FLAGS",
|
||||||
"--ignore-gpu-blacklist --enable-gpu-rasterization "
|
|
||||||
"--enable-native-gpu-memory-buffers --num-raster-threads=4 "
|
|
||||||
"--enable-features=WebRTCPipeWireCapturer " +
|
"--enable-features=WebRTCPipeWireCapturer " +
|
||||||
qgetenv("QTWEBENGINE_CHROMIUM_FLAGS"));
|
qgetenv("QTWEBENGINE_CHROMIUM_FLAGS"));
|
||||||
|
|
||||||
|
@ -48,7 +54,21 @@ 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));
|
||||||
|
|
||||||
|
// Check if discord is already running
|
||||||
|
QString program_name = "discord-awesomeaudio";
|
||||||
|
if (isProgramRunning(program_name)) {
|
||||||
|
// if running show error message
|
||||||
|
showErrorMessage("discord-awesomeaudio is already running");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// open server so we can check if discord is running
|
||||||
|
QLocalServer server;
|
||||||
|
server.listen(program_name);
|
||||||
|
QObject::connect(&server, &QLocalServer::newConnection, []() {});
|
||||||
|
|
||||||
w.show();
|
w.show();
|
||||||
|
|
||||||
return app.exec();
|
return app.exec();
|
||||||
|
|
|
@ -12,52 +12,58 @@
|
||||||
#include <QGridLayout>
|
#include <QGridLayout>
|
||||||
#include <QLabel>
|
#include <QLabel>
|
||||||
#include <QPushButton>
|
#include <QPushButton>
|
||||||
|
#include <QShortcut>
|
||||||
#include <QSpacerItem>
|
#include <QSpacerItem>
|
||||||
#include <QThread>
|
#include <QThread>
|
||||||
|
#include <QTimer>
|
||||||
#include <QUrl>
|
#include <QUrl>
|
||||||
#include <QWebEngineNotification>
|
#include <QWebEngineFullScreenRequest>
|
||||||
#include <QWebEngineProfile>
|
#include <QWebEngineProfile>
|
||||||
#include <QWebEngineScript>
|
#include <QWebEngineUrlRequestInterceptor>
|
||||||
#include <QWebEngineScriptCollection>
|
|
||||||
#include <QWebEngineSettings>
|
|
||||||
#include <QWidget>
|
#include <QWidget>
|
||||||
|
|
||||||
|
// Custom network interceptor class
|
||||||
|
class NetworkInterceptor : public QWebEngineUrlRequestInterceptor {
|
||||||
|
public:
|
||||||
|
NetworkInterceptor(QObject* parent = nullptr) : QWebEngineUrlRequestInterceptor(parent) {}
|
||||||
|
|
||||||
|
void interceptRequest(QWebEngineUrlRequestInfo& info) override {
|
||||||
|
// Modify the request headers here as needed
|
||||||
|
info.setHttpHeader("Access-Control-Allow-Origin", "*");
|
||||||
|
info.setHttpHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
MainWindow *MainWindow::m_instance = nullptr;
|
MainWindow *MainWindow::m_instance = nullptr;
|
||||||
|
|
||||||
MainWindow::MainWindow(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();
|
||||||
|
m_settings->setValue("useNotifySend", useNotifySend);
|
||||||
|
m_centralWidget = new CentralWidget(this);
|
||||||
|
setCentralWidget(m_centralWidget);
|
||||||
|
|
||||||
|
// Create and install the network interceptor
|
||||||
|
NetworkInterceptor* networkInterceptor = new NetworkInterceptor(this);
|
||||||
|
QWebEngineProfile::defaultProfile()->setRequestInterceptor(networkInterceptor);
|
||||||
|
|
||||||
|
setupTrayIcon();
|
||||||
|
setMinimumSize(800, 300);
|
||||||
|
connect(new QShortcut(QKeySequence(Qt::CTRL + Qt::Key_Q), this),
|
||||||
|
&QShortcut::activated, this, &MainWindow::toggleOrCloseWindow);
|
||||||
|
if (m_settings->contains("geometry")) {
|
||||||
|
restoreGeometry(m_settings->value("geometry").toByteArray());
|
||||||
|
} else {
|
||||||
resize(1000, 700);
|
resize(1000, 700);
|
||||||
showMaximized();
|
showMaximized();
|
||||||
}
|
}
|
||||||
|
if (m_settings->value("trayIcon", false).toBool() &&
|
||||||
void MainWindow::setupWebView() {
|
m_settings->value("startHidden", false).toBool()) {
|
||||||
auto page = new DiscordPage(this);
|
hide();
|
||||||
connect(page, &QWebEnginePage::fullScreenRequested, this,
|
QTimer::singleShot(0, [=]() { hide(); });
|
||||||
&MainWindow::fullScreenRequested);
|
}
|
||||||
|
|
||||||
m_webView = new QWebEngineView(this);
|
|
||||||
m_webView->setPage(page);
|
|
||||||
|
|
||||||
#ifdef KNOTIFICATIONS
|
|
||||||
QWebEngineProfile::defaultProfile()->setNotificationPresenter(
|
|
||||||
[&](std::unique_ptr<QWebEngineNotification> notificationInfo) {
|
|
||||||
KNotification *notification = new KNotification("discordNotification");
|
|
||||||
notification->setTitle(notificationInfo->title());
|
|
||||||
notification->setText(notificationInfo->message());
|
|
||||||
notification->setPixmap(QPixmap::fromImage(notificationInfo->icon()));
|
|
||||||
notification->setDefaultAction("View");
|
|
||||||
connect(notification, &KNotification::defaultActivated,
|
|
||||||
[&, notificationInfo = std::move(notificationInfo)]() {
|
|
||||||
notificationInfo->click();
|
|
||||||
activateWindow();
|
|
||||||
});
|
|
||||||
notification->sendEvent();
|
|
||||||
});
|
|
||||||
#endif
|
|
||||||
|
|
||||||
setCentralWidget(m_webView);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void MainWindow::fullScreenRequested(
|
void MainWindow::fullScreenRequested(
|
||||||
|
@ -65,12 +71,95 @@ void MainWindow::fullScreenRequested(
|
||||||
fullScreenRequest.accept();
|
fullScreenRequest.accept();
|
||||||
if (fullScreenRequest.toggleOn()) {
|
if (fullScreenRequest.toggleOn()) {
|
||||||
m_wasMaximized = isMaximized();
|
m_wasMaximized = isMaximized();
|
||||||
|
if (!m_wasMaximized) {
|
||||||
|
showNormal();
|
||||||
|
}
|
||||||
showFullScreen();
|
showFullScreen();
|
||||||
} else {
|
} else {
|
||||||
m_wasMaximized ? showMaximized() : showNormal();
|
m_wasMaximized ? showMaximized() : showNormal();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void MainWindow::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-awesomeaudio v" + QString(DISCORD_SCEENAUDIO_VERSION_FULL), this);
|
||||||
|
aboutAction->setIcon(QIcon(":assets/de.shorsh.discord-screenaudio.png"));
|
||||||
|
aboutAction->setEnabled(false);
|
||||||
|
|
||||||
|
auto exitAction = new QAction("Exit", this);
|
||||||
|
connect(exitAction, &QAction::triggered, []() { QApplication::quit(); });
|
||||||
|
|
||||||
|
m_trayIconMenu = new QMenu(this);
|
||||||
|
m_trayIconMenu->addAction(aboutAction);
|
||||||
|
m_trayIconMenu->addAction(exitAction);
|
||||||
|
|
||||||
|
m_trayIcon = new QSystemTrayIcon(this);
|
||||||
|
m_trayIcon->setContextMenu(m_trayIconMenu);
|
||||||
|
m_trayIcon->setIcon(QIcon(":assets/de.shorsh.discord-screenaudio.png"));
|
||||||
|
m_trayIcon->show();
|
||||||
|
|
||||||
|
connect(m_trayIcon, &QSystemTrayIcon::activated, [this](auto reason) {
|
||||||
|
if (reason == QSystemTrayIcon::Trigger) {
|
||||||
|
toggleOrCloseWindow();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void MainWindow::cleanTrayIcon() {
|
||||||
|
if (m_trayIcon == nullptr)
|
||||||
|
return;
|
||||||
|
m_trayIcon->hide();
|
||||||
|
m_trayIconMenu->deleteLater();
|
||||||
|
m_trayIcon->deleteLater();
|
||||||
|
m_trayIconMenu = nullptr;
|
||||||
|
m_trayIcon = nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
void MainWindow::setupSettings() {
|
||||||
|
m_settings =
|
||||||
|
new QSettings("discord-awesomeaudio", "discord-awesomeaudio", this);
|
||||||
|
m_settings->beginGroup("settings");
|
||||||
|
m_settings->endGroup();
|
||||||
|
}
|
||||||
|
|
||||||
|
QSettings *MainWindow::settings() const { return m_settings; }
|
||||||
|
|
||||||
|
void MainWindow::setTrayIcon(bool enabled) {
|
||||||
|
m_settings->setValue("trayIcon", enabled);
|
||||||
|
if (enabled) {
|
||||||
|
setupTrayIcon();
|
||||||
|
} else {
|
||||||
|
cleanTrayIcon();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void MainWindow::closeEvent(QCloseEvent *event) {
|
||||||
|
if (m_settings->value("trayIcon", false).toBool()) {
|
||||||
|
hide();
|
||||||
|
} else {
|
||||||
|
m_settings->setValue("geometry", saveGeometry());
|
||||||
|
QApplication::quit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
MainWindow *MainWindow::instance() { return m_instance; }
|
MainWindow *MainWindow::instance() { return m_instance; }
|
||||||
|
|
||||||
|
CentralWidget *MainWindow::centralWidget() {
|
||||||
|
return instance()->m_centralWidget;
|
||||||
|
};
|
||||||
|
|
||||||
|
void MainWindow::toggleOrCloseWindow() {
|
||||||
|
if (isVisible()) {
|
||||||
|
if (m_trayIcon == nullptr)
|
||||||
|
QApplication::quit();
|
||||||
|
else
|
||||||
|
hide();
|
||||||
|
} else {
|
||||||
|
show();
|
||||||
|
activateWindow();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,31 +1,40 @@
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "discordpage.h"
|
#include "centralwidget.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 <QWebEngineUrlRequestInterceptor>
|
||||||
#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();
|
||||||
QWebEngineProfile *prepareProfile();
|
void setupSettings();
|
||||||
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);
|
||||||
|
void toggleOrCloseWindow();
|
||||||
};
|
};
|
||||||
|
|
|
@ -9,73 +9,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);
|
auto layout = new QVBoxLayout(this);
|
||||||
layout->setSizeConstraint(QLayout::SetFixedSize);
|
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);
|
||||||
|
|
||||||
|
auto resolutionLabel = new QLabel(this);
|
||||||
|
resolutionLabel->setText("Resolution");
|
||||||
|
videoLayout->addWidget(resolutionLabel);
|
||||||
|
|
||||||
|
m_resolutionComboBox = new QComboBox(this);
|
||||||
|
m_resolutionComboBox->addItem("2160p", "3840x2160");
|
||||||
|
m_resolutionComboBox->addItem("1440p", "2560x1440");
|
||||||
|
m_resolutionComboBox->addItem("1080p", "1920x1080");
|
||||||
|
m_resolutionComboBox->addItem("720p", "1280x720");
|
||||||
|
m_resolutionComboBox->addItem("480p", "854x480");
|
||||||
|
m_resolutionComboBox->addItem("360p", "640x360");
|
||||||
|
m_resolutionComboBox->addItem("240p", "426x240");
|
||||||
|
m_resolutionComboBox->setCurrentText("720p");
|
||||||
|
videoLayout->addWidget(m_resolutionComboBox);
|
||||||
|
|
||||||
|
auto framerateLabel = new QLabel(this);
|
||||||
|
framerateLabel->setText("Framerate");
|
||||||
|
videoLayout->addWidget(framerateLabel);
|
||||||
|
|
||||||
|
m_framerateComboBox = new QComboBox(this);
|
||||||
|
m_framerateComboBox->addItem("144 FPS", 144);
|
||||||
|
m_framerateComboBox->addItem("60 FPS", 60);
|
||||||
|
m_framerateComboBox->addItem("30 FPS", 30);
|
||||||
|
m_framerateComboBox->addItem("15 FPS", 15);
|
||||||
|
m_framerateComboBox->addItem("5 FPS", 5);
|
||||||
|
m_framerateComboBox->setCurrentText("30 FPS");
|
||||||
|
videoLayout->addWidget(m_framerateComboBox);
|
||||||
|
}
|
||||||
|
|
||||||
|
m_audioGroupBox = new QGroupBox(this);
|
||||||
|
m_audioGroupBox->setCheckable(true);
|
||||||
|
m_audioGroupBox->setTitle("Audio");
|
||||||
|
layout->addWidget(m_audioGroupBox);
|
||||||
|
|
||||||
|
{
|
||||||
|
auto audioLayout = new QVBoxLayout(m_audioGroupBox);
|
||||||
|
|
||||||
|
auto targetLabel = new QLabel(this);
|
||||||
|
targetLabel->setText("Audio Source");
|
||||||
|
audioLayout->addWidget(targetLabel);
|
||||||
|
|
||||||
|
{
|
||||||
|
auto targetLayout = new QHBoxLayout();
|
||||||
|
audioLayout->addLayout(targetLayout);
|
||||||
|
|
||||||
m_targetComboBox = new QComboBox(this);
|
m_targetComboBox = new QComboBox(this);
|
||||||
updateTargets();
|
updateTargets();
|
||||||
targetHBox->addWidget(m_targetComboBox);
|
targetLayout->addWidget(m_targetComboBox);
|
||||||
|
|
||||||
auto refreshTargetsButton = new QPushButton(this);
|
auto refreshTargetsButton = new QPushButton(this);
|
||||||
refreshTargetsButton->setFixedSize(30, 30);
|
refreshTargetsButton->setFixedSize(30, 30);
|
||||||
refreshTargetsButton->setIcon(QIcon::fromTheme("view-refresh"));
|
refreshTargetsButton->setIcon(QIcon::fromTheme("view-refresh"));
|
||||||
connect(refreshTargetsButton, &QPushButton::clicked, this,
|
connect(refreshTargetsButton, &QPushButton::clicked, this,
|
||||||
&StreamDialog::updateTargets);
|
&StreamDialog::updateTargets);
|
||||||
targetHBox->addWidget(refreshTargetsButton);
|
targetLayout->addWidget(refreshTargetsButton);
|
||||||
|
}
|
||||||
auto qualityLabel = new QLabel(this);
|
}
|
||||||
qualityLabel->setText("Stream Quality");
|
|
||||||
layout->addWidget(qualityLabel);
|
|
||||||
|
|
||||||
auto qualityHBox = new QHBoxLayout(this);
|
|
||||||
layout->addLayout(qualityHBox);
|
|
||||||
|
|
||||||
m_qualityResolutionComboBox = new QComboBox(this);
|
|
||||||
m_qualityResolutionComboBox->addItem("2160p", "3840x2160");
|
|
||||||
m_qualityResolutionComboBox->addItem("1440p", "2560x1440");
|
|
||||||
m_qualityResolutionComboBox->addItem("1080p", "1920x1080");
|
|
||||||
m_qualityResolutionComboBox->addItem("720p", "1280x720");
|
|
||||||
m_qualityResolutionComboBox->addItem("480p", "854x480");
|
|
||||||
m_qualityResolutionComboBox->addItem("360p", "640x360");
|
|
||||||
m_qualityResolutionComboBox->addItem("240p", "426x240");
|
|
||||||
m_qualityResolutionComboBox->setCurrentText("720p");
|
|
||||||
qualityHBox->addWidget(m_qualityResolutionComboBox);
|
|
||||||
|
|
||||||
m_qualityFPSComboBox = new QComboBox(this);
|
|
||||||
m_qualityFPSComboBox->addItem("144 FPS", 144);
|
|
||||||
m_qualityFPSComboBox->addItem("60 FPS", 60);
|
|
||||||
m_qualityFPSComboBox->addItem("30 FPS", 30);
|
|
||||||
m_qualityFPSComboBox->addItem("15 FPS", 15);
|
|
||||||
m_qualityFPSComboBox->addItem("5 FPS", 5);
|
|
||||||
m_qualityFPSComboBox->setCurrentText("30 FPS");
|
|
||||||
qualityHBox->addWidget(m_qualityFPSComboBox);
|
|
||||||
|
|
||||||
auto button = new QPushButton(this);
|
auto button = new QPushButton(this);
|
||||||
button->setText("Start Stream");
|
button->setText("Start Stream");
|
||||||
connect(button, &QPushButton::clicked, this, &StreamDialog::startStream);
|
connect(button, &QPushButton::clicked, this, &StreamDialog::startStream);
|
||||||
layout->addWidget(button, Qt::AlignRight | Qt::AlignBottom);
|
layout->addWidget(button, Qt::AlignRight | Qt::AlignBottom);
|
||||||
|
}
|
||||||
setLayout(layout);
|
|
||||||
|
|
||||||
setWindowTitle("discord-screenaudio Stream Dialog");
|
setWindowTitle("discord-screenaudio Stream Dialog");
|
||||||
}
|
}
|
||||||
|
|
||||||
void StreamDialog::startStream() {
|
void StreamDialog::startStream() {
|
||||||
auto resolution =
|
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 +105,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);
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
|
@ -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 {
|
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() {
|
QVector<QString> getTargets() {
|
||||||
auto main_loop = pipewire::main_loop();
|
auto main_loop = pipewire::main_loop();
|
||||||
|
@ -22,8 +41,7 @@ QVector<QString> getTargets() {
|
||||||
if (global.type == pipewire::node::type) {
|
if (global.type == pipewire::node::type) {
|
||||||
auto node = reg.bind<pipewire::node>(global.id);
|
auto node = reg.bind<pipewire::node>(global.id);
|
||||||
auto info = node.info();
|
auto info = node.info();
|
||||||
auto name = QString::fromStdString(info.props["application.name"]);
|
QString name = qGetTarget(info.props);
|
||||||
|
|
||||||
if (name != "" && !EXCLUDE_TARGETS.contains(name) &&
|
if (name != "" && !EXCLUDE_TARGETS.contains(name) &&
|
||||||
!targets.contains(name)) {
|
!targets.contains(name)) {
|
||||||
targets.append(name);
|
targets.append(name);
|
||||||
|
@ -67,7 +85,12 @@ void start(QString _target) {
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
auto &parent = nodes.at(parent_id);
|
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 ||
|
if (name == target ||
|
||||||
(target == "[All Desktop Audio]" &&
|
(target == "[All Desktop Audio]" &&
|
||||||
|
@ -78,8 +101,7 @@ void start(QString _target) {
|
||||||
core.create<pipewire::link_factory>(
|
core.create<pipewire::link_factory>(
|
||||||
{fl ? virt_fl->info().id : virt_fr->info().id, port_id}));
|
{fl ? virt_fl->info().id : virt_fr->info().id, port_id}));
|
||||||
qDebug(virtmicLog) << QString("Link: %1:%2 -> %3")
|
qDebug(virtmicLog) << QString("Link: %1:%2 -> %3")
|
||||||
.arg(QString::fromStdString(
|
.arg(QString::fromStdString(name))
|
||||||
parent.props["application.name"]))
|
|
||||||
.arg(port_id)
|
.arg(port_id)
|
||||||
.arg(fl ? virt_fl->info().id
|
.arg(fl ? virt_fl->info().id
|
||||||
: virt_fr->info().id)
|
: virt_fr->info().id)
|
||||||
|
@ -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",
|
auto virtual_mic = core.create("adapter",
|
||||||
{{"node.name", "discord-screenaudio-virtmic"},
|
{{"node.name", "discord-awesomeaudio-virtmic"},
|
||||||
{"media.class", "Audio/Source/Virtual"},
|
{"media.class", "Audio/Source/Virtual"},
|
||||||
{"factory.name", "support.null-audio-sink"},
|
{"factory.name", "support.null-audio-sink"},
|
||||||
{"audio.channels", "2"},
|
{"audio.channels", "2"},
|
||||||
|
@ -112,11 +134,12 @@ void start(QString _target) {
|
||||||
[&](const pipewire::global &global) {
|
[&](const pipewire::global &global) {
|
||||||
if (global.type == pipewire::node::type) {
|
if (global.type == pipewire::node::type) {
|
||||||
auto node = reg.bind<pipewire::node>(global.id);
|
auto node = reg.bind<pipewire::node>(global.id);
|
||||||
if (!node.info().props.count("application.name"))
|
auto info = node.info();
|
||||||
|
std::string name = getTarget(info.props);
|
||||||
|
if (name == nullstr)
|
||||||
return;
|
return;
|
||||||
qDebug(virtmicLog) << QString("Added: %1")
|
qDebug(virtmicLog) << QString("Added: %1")
|
||||||
.arg(QString::fromStdString(
|
.arg(QString::fromStdString(name))
|
||||||
node.info().props["application.name"]))
|
|
||||||
.toUtf8()
|
.toUtf8()
|
||||||
.data();
|
.data();
|
||||||
|
|
||||||
|
@ -152,9 +175,11 @@ void start(QString _target) {
|
||||||
[&](const std::uint32_t id) {
|
[&](const std::uint32_t id) {
|
||||||
if (nodes.count(id)) {
|
if (nodes.count(id)) {
|
||||||
auto info = nodes.at(id);
|
auto info = nodes.at(id);
|
||||||
|
std::string name = getTarget(info.props);
|
||||||
|
if (name == nullstr)
|
||||||
|
return;
|
||||||
qDebug(virtmicLog) << QString("Removed: %1")
|
qDebug(virtmicLog) << QString("Removed: %1")
|
||||||
.arg(QString::fromStdString(
|
.arg(QString::fromStdString(name))
|
||||||
info.props["application.name"].data()))
|
|
||||||
.toUtf8()
|
.toUtf8()
|
||||||
.data();
|
.data();
|
||||||
nodes.erase(id);
|
nodes.erase(id);
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
Subproject commit 0d996633f201b2ab95d8b9dc376a7da2dcca315d
|
Subproject commit 62b2acebe6806c7b0e2ca6a43c6b2419a627b8dc
|
|
@ -0,0 +1,2 @@
|
||||||
|
flatpak remove lol.deadzone.discord-awesomeaudio
|
||||||
|
rm ~/.local/share/applications/discord-awesomeaudio.desktop
|
Loading…
Reference in New Issue