Compare commits
54 Commits
v1.5.0
...
virtmic-re
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e6449ad807 | ||
|
|
e919e3ea25 | ||
|
|
10fff86ca4 | ||
|
|
87e84dec5c | ||
|
|
4b782133ae | ||
|
|
d5257207bc | ||
|
|
423884ae0c | ||
|
|
7ea3d0aab1 | ||
|
|
18b15f5cf4 | ||
|
|
96bca52d0d | ||
|
|
3540774c82 | ||
|
|
b5435acdd8 | ||
|
|
38c2b92e2d | ||
|
|
e447206082 | ||
|
|
f31901073e | ||
|
|
77f31b63aa | ||
|
|
0b9fc100c2 | ||
|
|
f73524de27 | ||
|
|
8a8690fe53 | ||
|
|
259e6dc75d | ||
|
|
9cbdca0441 | ||
|
|
a9ddc0216f | ||
|
|
b39e23d462 | ||
|
|
331fb8f4ca | ||
|
|
335a4456ed | ||
|
|
26a2ca7b05 | ||
|
|
30c0526ff7 | ||
|
|
3119e1df19 | ||
|
|
63180f5d53 | ||
|
|
78d43991ab | ||
|
|
193b69f45f | ||
|
|
485ff9634b | ||
|
|
74fdef683f | ||
|
|
02fdc62fbe | ||
|
|
d9e3704176 | ||
|
|
36c8cfedb7 | ||
|
|
c52daab420 | ||
|
|
8c4fae3410 | ||
|
|
ac71e9bbcb | ||
|
|
fc0dbb5b34 | ||
|
|
5df24629e6 | ||
|
|
1477a9d4c0 | ||
|
|
0b12487dfd | ||
|
|
a10cfda56d | ||
|
|
2a809e163e | ||
|
|
bc317d5531 | ||
|
|
b4db987217 | ||
|
|
9f46e710a9 | ||
|
|
d6641a7a6e | ||
|
|
b836be6530 | ||
|
|
bfb0714b13 | ||
|
|
374b854261 | ||
|
|
1f6105f76b | ||
|
|
3071159332 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,2 +1,3 @@
|
||||
/build
|
||||
.vscode
|
||||
/submodules/arrpc
|
||||
|
||||
3
.gitmodules
vendored
3
.gitmodules
vendored
@@ -1,3 +1,6 @@
|
||||
[submodule "submodules/rohrkabel"]
|
||||
path = submodules/rohrkabel
|
||||
url = https://github.com/Soundux/rohrkabel
|
||||
[submodule "submodules/channel"]
|
||||
path = submodules/channel
|
||||
url = https://github.com/Soundux/channel.git
|
||||
|
||||
@@ -13,24 +13,27 @@ set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -DDEBUG")
|
||||
string(TIMESTAMP TIMESTAMP %s)
|
||||
# set(CMAKE_AUTOUIC ON)
|
||||
|
||||
find_package(Qt5 CONFIG REQUIRED COMPONENTS
|
||||
Widgets
|
||||
WebEngineWidgets
|
||||
)
|
||||
find_package(Qt5 COMPONENTS Widgets)
|
||||
if (Qt5_FOUND)
|
||||
find_package(Qt5 CONFIG REQUIRED COMPONENTS Widgets WebEngineWidgets)
|
||||
|
||||
find_package(KF5Notifications)
|
||||
if(KF5Notifications_FOUND)
|
||||
add_definitions( -DKNOTIFICATIONS )
|
||||
endif()
|
||||
find_package(KF5Notifications)
|
||||
if(KF5Notifications_FOUND)
|
||||
add_definitions( -DKNOTIFICATIONS )
|
||||
endif()
|
||||
|
||||
find_package(KF5XmlGui)
|
||||
if(KF5XmlGui_FOUND)
|
||||
add_definitions( -DKXMLGUI )
|
||||
endif()
|
||||
find_package(KF5XmlGui)
|
||||
if(KF5XmlGui_FOUND)
|
||||
add_definitions( -DKXMLGUI )
|
||||
endif()
|
||||
|
||||
find_package(KF5GlobalAccel)
|
||||
if(KF5GlobalAccel_FOUND)
|
||||
add_definitions( -DKGLOBALACCEL )
|
||||
find_package(KF5GlobalAccel)
|
||||
if(KF5GlobalAccel_FOUND)
|
||||
add_definitions( -DKGLOBALACCEL )
|
||||
endif()
|
||||
else()
|
||||
message(WARNING "Qt 5 was not found on your system and Qt 6 will be used. You will not be able to use any features using KDE Frameworks.")
|
||||
find_package(Qt6 CONFIG REQUIRED COMPONENTS Widgets WebEngineWidgets)
|
||||
endif()
|
||||
|
||||
set(discord-screenaudio_SRC
|
||||
@@ -40,6 +43,8 @@ set(discord-screenaudio_SRC
|
||||
src/discordpage.cpp
|
||||
src/streamdialog.cpp
|
||||
src/log.cpp
|
||||
src/userscript.cpp
|
||||
src/centralwidget.cpp
|
||||
resources.qrc
|
||||
)
|
||||
|
||||
@@ -58,15 +63,20 @@ if(GIT_FOUND AND EXISTS "${PROJECT_SOURCE_DIR}/.git")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
if(NOT EXISTS "${PROJECT_SOURCE_DIR}/submodules/rohrkabel/CMakeLists.txt")
|
||||
message(FATAL_ERROR "Rohrkabel was not found since you are not in a Git checkout or have GIT_SUBMODULE disabled. Please provide rohrkabel manually to `./submodules/rohrkabel`.")
|
||||
endif()
|
||||
function(add_git_subdirectory SUBMODULE)
|
||||
if(NOT EXISTS "${PROJECT_SOURCE_DIR}/submodules/${SUBMODULE}/CMakeLists.txt")
|
||||
message(FATAL_ERROR "Submodule ${SUBMODULE} was not found since you are not in a Git checkout or have GIT_SUBMODULE disabled. Please provide ${SUBMODULE} manually to `./submodules/${SUBMODULE}`.")
|
||||
endif()
|
||||
|
||||
add_subdirectory(submodules/rohrkabel)
|
||||
add_subdirectory(submodules/${SUBMODULE})
|
||||
endfunction()
|
||||
|
||||
add_git_subdirectory(rohrkabel)
|
||||
add_git_subdirectory(channel)
|
||||
|
||||
add_executable(discord-screenaudio ${discord-screenaudio_SRC})
|
||||
|
||||
target_link_libraries(discord-screenaudio Qt5::Widgets Qt5::WebEngineWidgets rohrkabel)
|
||||
target_link_libraries(discord-screenaudio Qt::Widgets Qt::WebEngineWidgets rohrkabel channel)
|
||||
|
||||
if(KF5Notifications_FOUND)
|
||||
target_link_libraries(discord-screenaudio KF5::Notifications)
|
||||
|
||||
29
README.md
29
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
|
||||
screenshare and not passed to the user microphone
|
||||
([see explanation](#how-it-works)).
|
||||
([see explanation](#how-does-this-work)).
|
||||
|
||||

|
||||

|
||||
|
||||
The purpose of this project is **not** to provide an alternative to the original
|
||||
Discord client. Rather, it should be used in addition to the original client in
|
||||
@@ -50,6 +50,8 @@ You have multiple options:
|
||||
### Requirements
|
||||
|
||||
- Basic building tools
|
||||
- An up-to-date system (I can't guarantee that it works on Debian or Ubuntu
|
||||
20/21)
|
||||
- CMake
|
||||
- Qt5 and QtWebEngine
|
||||
- **PipeWire** (it currently doesn't work with PulseAudio)
|
||||
@@ -57,7 +59,7 @@ You have multiple options:
|
||||
- _Kf5Notifications (optional, for better notifications)_
|
||||
- _KXMLGui and KGlobalAccel (optional, for keybinds)_
|
||||
|
||||
On Debian:
|
||||
With apt:
|
||||
`apt install -y build-essential cmake qtbase5-dev qtwebengine5-dev libkf5notifications-dev libkf5xmlgui-dev libkf5globalaccel-dev pkg-config libpipewire-0.3-dev git`
|
||||
|
||||
### Building
|
||||
@@ -82,7 +84,9 @@ And then to optionally install it, run:
|
||||
sudo cmake --install build
|
||||
```
|
||||
|
||||
## How it works
|
||||
## FAQ
|
||||
|
||||
### How does this work?
|
||||
|
||||
This whole project is based on
|
||||
[this](https://github.com/edisionnano/Screenshare-with-audio-on-Discord-with-Linux)
|
||||
@@ -91,6 +95,23 @@ Discord. Basically: a virtual microphone is created which captures the
|
||||
application audio, and this microphone is then fed to the Discord stream by
|
||||
intercepting a API call of Discord.
|
||||
|
||||
### Drag and drop doesn't work in the Flatpak
|
||||
|
||||
This is due to sandboxing limitations of Flatpak. The main Discord Flatpak has
|
||||
the same problem. If you still want to use drag and drop, you can disable most
|
||||
of Flatpak's sandboxing by installing
|
||||
[Flatseal](https://flathub.org/apps/details/com.github.tchx84.Flatseal) and
|
||||
allowing access to "All system files" under the "Filesystem" section.
|
||||
|
||||
### Is there any way to add custom CSS / a theme?
|
||||
|
||||
Yes, you can add all your styles into
|
||||
`~/.config/discord-screenaudio/userstyles.css` (or
|
||||
`~/.var/app/de.shorsh.discord-screenaudio/config/discord-screenaudio/userstyles.css`
|
||||
if you are using the Flatpak). But please note that due to QtWebEngine
|
||||
limitations concerning content security policies, you can't use any external
|
||||
files (like `@import` or `url()`).
|
||||
|
||||
## License
|
||||
|
||||
Copyright (C) 2022 Malte Jürgens
|
||||
|
||||
1
assets/arrpc.js
Normal file
1
assets/arrpc.js
Normal file
File diff suppressed because one or more lines are too long
1
assets/arrpc_bridge_mod.js
Normal file
1
assets/arrpc_bridge_mod.js
Normal file
@@ -0,0 +1 @@
|
||||
setTimeout((i=>{let t,a,s,e={};new WebSocket("ws://127.0.0.1:1337").onmessage=async i=>{if(msg=JSON.parse(i.data),console.log(msg),!t){const i=window.webpackChunkdiscord_app.push([[Symbol()],{},i=>i]),e=i.c;window.webpackChunkdiscord_app.pop();for(const i in e){let a=e[i].exports;if(a=a&&(a.Z??a.ZP),a&&a.register&&a.wait){t=a;break}}const n=i.m;for(const t in n)if(n[t].toString().includes("getAssetImage: size must === [number, number] for Twitch")){const s=i(t),e=Object.values(s).find((i=>"function"==typeof i&&i.toString().includes("apply(")));a=async(i,t)=>(await e(i,[t,void 0]))[0];break}for(const t in n)if(n[t].toString().includes("e.application={")){const a=i(t),e=Object.values(a).find((i=>"function"==typeof i&&i.toString().includes("e.application={")));s=async i=>{let t={};return await e(t,i),t.application};break}}if(msg.activity?.assets?.large_image&&(msg.activity.assets.large_image=await a(msg.activity.application_id,msg.activity.assets.large_image)),msg.activity?.assets?.small_image&&(msg.activity.assets.small_image=await a(msg.activity.application_id,msg.activity.assets.small_image)),msg.activity){const i=msg.activity.application_id;e[i]||(e[i]=await s(i));const t=e[i];msg.activity.name||(msg.activity.name=t.name)}t.dispatch({type:"LOCAL_ACTIVITY_UPDATE",...msg})}}),1e4);
|
||||
@@ -1,5 +1,3 @@
|
||||
// From v0.4
|
||||
|
||||
navigator.mediaDevices.chromiumGetDisplayMedia =
|
||||
navigator.mediaDevices.getDisplayMedia;
|
||||
|
||||
@@ -16,12 +14,12 @@ const getAudioDevice = async (nameOfAudioDevice) => {
|
||||
let devices = await navigator.mediaDevices.enumerateDevices();
|
||||
audioDevice = devices.find(({ label }) => label === nameOfAudioDevice);
|
||||
if (!audioDevice)
|
||||
console.log(
|
||||
`dsa: Did not find '${nameOfAudioDevice}', trying again in 100ms`
|
||||
userscript.log(
|
||||
`Did not find '${nameOfAudioDevice}', trying again in 100ms`
|
||||
);
|
||||
await sleep(100);
|
||||
}
|
||||
console.log(`dsa: Found '${nameOfAudioDevice}'`);
|
||||
userscript.log(`Found '${nameOfAudioDevice}'`);
|
||||
return audioDevice;
|
||||
};
|
||||
|
||||
@@ -71,158 +69,304 @@ function setGetDisplayMedia(video = true, overrideArgs = undefined) {
|
||||
|
||||
setGetDisplayMedia();
|
||||
|
||||
let userscript;
|
||||
let muteBtn;
|
||||
let deafenBtn;
|
||||
let streamStartBtn;
|
||||
let streamStartBtnInitialDisplay;
|
||||
let streamStartBtnClone;
|
||||
let resolutionString;
|
||||
const clonedElements = [];
|
||||
const hiddenElements = [];
|
||||
let wasStreamActive = false;
|
||||
|
||||
setInterval(() => {
|
||||
const streamActive =
|
||||
document.getElementsByClassName("panel-2ZFCRb activityPanel-9icbyU")
|
||||
.length > 0;
|
||||
function createButton(text, onClick) {
|
||||
const button = document.createElement("button");
|
||||
button.style.marginBottom = "20px";
|
||||
button.classList =
|
||||
"button-ejjZWC lookFilled-1H2Jvj colorBrand-2M3O3N sizeSmall-3R2P2p grow-2T4nbg";
|
||||
button.innerText = text;
|
||||
button.addEventListener("click", onClick);
|
||||
return button;
|
||||
}
|
||||
|
||||
if (!streamActive && wasStreamActive)
|
||||
console.log("!discord-screenaudio-stream-stopped");
|
||||
wasStreamActive = streamActive;
|
||||
function createSwitch(text, enabled, onClick) {
|
||||
const container = document.createElement("div");
|
||||
container.style.marginBottom = "20px";
|
||||
container.className = "labelRow-NnoUIp";
|
||||
|
||||
if (streamActive) {
|
||||
clonedElements.forEach((el) => {
|
||||
el.remove();
|
||||
});
|
||||
clonedElements.length = 0;
|
||||
const label = document.createElement("label");
|
||||
label.innerText = text;
|
||||
label.className = "title-2yADjX";
|
||||
container.appendChild(label);
|
||||
|
||||
hiddenElements.forEach((el) => {
|
||||
el.style.display = "block";
|
||||
});
|
||||
hiddenElements.length = 0;
|
||||
} else {
|
||||
for (const el of [
|
||||
document.getElementsByClassName("actionButtons-2vEOUh")?.[0]?.children[1],
|
||||
document.querySelector(
|
||||
".wrapper-3t3Yqv > div > div > div > div > .controlButton-2PMNom"
|
||||
),
|
||||
]) {
|
||||
if (!el) continue;
|
||||
if (el.classList.contains("discord-screenaudio-cloned")) continue;
|
||||
el.classList.add("discord-screenaudio-cloned");
|
||||
elClone = el.cloneNode(true);
|
||||
elClone.title = "Share Your Screen with Audio";
|
||||
elClone.addEventListener("click", () => {
|
||||
console.log("!discord-screenaudio-start-stream");
|
||||
});
|
||||
const svg = document.createElement("div");
|
||||
container.appendChild(svg);
|
||||
|
||||
const initialDisplay = el.style.display;
|
||||
|
||||
window.discordScreenaudioStartStream = (
|
||||
video,
|
||||
width,
|
||||
height,
|
||||
frameRate
|
||||
) => {
|
||||
window.discordScreenaudioResolutionString = video
|
||||
? `${height}p ${frameRate}FPS`
|
||||
: "Audio Only";
|
||||
setGetDisplayMedia(video, {
|
||||
audio: true,
|
||||
video: { width, height, frameRate },
|
||||
});
|
||||
el.click();
|
||||
el.style.display = initialDisplay;
|
||||
elClone.remove();
|
||||
};
|
||||
|
||||
el.style.display = "none";
|
||||
el.parentNode.insertBefore(elClone, el);
|
||||
|
||||
clonedElements.push(elClone);
|
||||
hiddenElements.push(el);
|
||||
}
|
||||
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>`;
|
||||
}
|
||||
|
||||
// Add about text in settings
|
||||
if (
|
||||
document.getElementsByClassName("dirscordScreenaudioAboutText").length == 0
|
||||
) {
|
||||
for (const el of document.getElementsByClassName("info-3pQQBb")) {
|
||||
let aboutEl;
|
||||
if (window.discordScreenaudioKXMLGUI) {
|
||||
aboutEl = document.createElement("a");
|
||||
aboutEl.addEventListener("click", () => {
|
||||
console.log("!discord-screenaudio-about");
|
||||
});
|
||||
} else {
|
||||
aboutEl = document.createElement("div");
|
||||
}
|
||||
aboutEl.innerText = `discord-screenaudio ${window.discordScreenaudioVersion}`;
|
||||
aboutEl.style.fontSize = "12px";
|
||||
aboutEl.style.color = "var(--text-muted)";
|
||||
aboutEl.style.textTransform = "none";
|
||||
aboutEl.classList.add("dirscordScreenaudioAboutText");
|
||||
aboutEl.style.cursor = "pointer";
|
||||
el.appendChild(aboutEl);
|
||||
}
|
||||
function setSvgEnabled() {
|
||||
svg.innerHTML = `<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>`;
|
||||
}
|
||||
|
||||
// Remove stream settings if stream is active
|
||||
document.getElementById("manage-streams-change-windows")?.remove();
|
||||
document.querySelector(`[aria-label="Stream Settings"]`)?.remove();
|
||||
|
||||
// Add event listener for keybind tab
|
||||
if (
|
||||
document
|
||||
.getElementById("keybinds-tab")
|
||||
?.getElementsByClassName(
|
||||
"container-3jbRo5 info-1hMolH fontSize16-3zr6Io browserNotice-1u-Y5o"
|
||||
).length
|
||||
) {
|
||||
const el = document
|
||||
.getElementById("keybinds-tab")
|
||||
.getElementsByClassName("children-1xdcWE")[0];
|
||||
const div = document.createElement("div");
|
||||
div.style.marginBottom = "50px";
|
||||
const button = document.createElement("button");
|
||||
button.classList =
|
||||
"button-f2h6uQ lookFilled-yCfaCM colorBrand-I6CyqQ sizeSmall-wU2dO- grow-2sR_-F";
|
||||
button.innerText = "Edit Global Keybinds";
|
||||
button.addEventListener("click", () => {
|
||||
console.log("!discord-screenaudio-keybinds");
|
||||
});
|
||||
div.appendChild(button);
|
||||
el.innerHTML = "";
|
||||
el.appendChild(div);
|
||||
function updateSvg() {
|
||||
if (enabled) setSvgEnabled();
|
||||
else setSvgDisabled();
|
||||
}
|
||||
|
||||
const buttonContainer =
|
||||
document.getElementsByClassName("container-YkUktl")[0];
|
||||
if (!buttonContainer) {
|
||||
console.log(
|
||||
"dsa: Cannot locate Mute/Deafen/Settings button container, please report this on GitHub"
|
||||
);
|
||||
}
|
||||
container.addEventListener("click", () => {
|
||||
enabled = !enabled;
|
||||
updateSvg();
|
||||
onClick(enabled);
|
||||
});
|
||||
updateSvg();
|
||||
|
||||
const muteBtn = buttonContainer
|
||||
? buttonContainer.getElementsByClassName(
|
||||
"button-12Fmur enabled-9OeuTA button-f2h6uQ lookBlank-21BCro colorBrand-I6CyqQ grow-2sR_-F"
|
||||
)[0]
|
||||
: null;
|
||||
window.discordScreenaudioToggleMute = () => muteBtn && muteBtn.click();
|
||||
|
||||
const deafenBtn = buttonContainer
|
||||
? buttonContainer.getElementsByClassName(
|
||||
"button-12Fmur enabled-9OeuTA button-f2h6uQ lookBlank-21BCro colorBrand-I6CyqQ grow-2sR_-F"
|
||||
)[1]
|
||||
: null;
|
||||
|
||||
window.discordScreenaudioToggleDeafen = () => deafenBtn && deafenBtn.click();
|
||||
|
||||
if (window.discordScreenaudioResolutionString) {
|
||||
for (const el of document.getElementsByClassName(
|
||||
"qualityIndicator-39wQDy"
|
||||
)) {
|
||||
el.innerHTML = window.discordScreenaudioResolutionString;
|
||||
}
|
||||
}
|
||||
}, 500);
|
||||
return container;
|
||||
}
|
||||
|
||||
// Fix for broken discord notifications after restart
|
||||
// (https://github.com/maltejur/discord-screenaudio/issues/17)
|
||||
Notification.requestPermission();
|
||||
|
||||
setTimeout(() => {
|
||||
new QWebChannel(qt.webChannelTransport, (channel) => {
|
||||
userscript = channel.objects.userscript;
|
||||
main();
|
||||
});
|
||||
});
|
||||
|
||||
function main() {
|
||||
userscript.muteToggled.connect(() => {
|
||||
console.log("Toggling mute");
|
||||
muteBtn && muteBtn.click();
|
||||
});
|
||||
|
||||
userscript.deafenToggled.connect(() => {
|
||||
console.log("Toggling deafen");
|
||||
deafenBtn && deafenBtn.click();
|
||||
});
|
||||
|
||||
userscript.streamStarted.connect((video, width, height, frameRate) => {
|
||||
resolutionString = video ? `${height}p ${frameRate}FPS` : "Audio Only";
|
||||
setGetDisplayMedia(video, {
|
||||
audio: true,
|
||||
video: { width, height, frameRate },
|
||||
});
|
||||
streamStartBtn.click();
|
||||
streamStartBtn.style.display = streamStartBtnInitialDisplay;
|
||||
streamStartBtnClone.remove();
|
||||
});
|
||||
|
||||
function updateUserstyles() {
|
||||
userscript.log("Loading userstyles...");
|
||||
userscript.loadingMessage = "Loading userstyles...";
|
||||
let stylesheet = document.getElementById("discordScreenaudioUserstyles");
|
||||
if (stylesheet) {
|
||||
userscript.log("Removing old userstyles...");
|
||||
stylesheet.remove();
|
||||
}
|
||||
stylesheet = document.createElement("style");
|
||||
stylesheet.id = "discordScreenaudioUserstyles";
|
||||
stylesheet.innerText = userscript.userstyles;
|
||||
document.head.appendChild(stylesheet);
|
||||
userscript.log("Finished loading userstyles");
|
||||
userscript.loadingMessage = "";
|
||||
}
|
||||
|
||||
userscript.userstylesChanged.connect(updateUserstyles);
|
||||
setTimeout(() => updateUserstyles());
|
||||
|
||||
setInterval(async () => {
|
||||
const streamActive =
|
||||
document.getElementsByClassName("panel-2ZFCRb activityPanel-9icbyU")
|
||||
.length > 0;
|
||||
|
||||
if (!streamActive && wasStreamActive) userscript.stopVirtmic();
|
||||
wasStreamActive = streamActive;
|
||||
|
||||
if (streamActive) {
|
||||
clonedElements.forEach((el) => {
|
||||
el.remove();
|
||||
});
|
||||
clonedElements.length = 0;
|
||||
|
||||
hiddenElements.forEach((el) => {
|
||||
el.style.display = "block";
|
||||
});
|
||||
hiddenElements.length = 0;
|
||||
} else {
|
||||
for (const el of [
|
||||
document.getElementsByClassName("actionButtons-2vEOUh")?.[0]
|
||||
?.children[1],
|
||||
document.querySelector(
|
||||
".wrapper-3t3Yqv > div > div > div > div > .controlButton-2PMNom"
|
||||
),
|
||||
]) {
|
||||
if (!el) continue;
|
||||
if (el.classList.contains("discord-screenaudio-cloned")) continue;
|
||||
streamStartBtn = el;
|
||||
streamStartBtn.classList.add("discord-screenaudio-cloned");
|
||||
|
||||
streamStartBtnClone = streamStartBtn.cloneNode(true);
|
||||
streamStartBtnClone.title = "Share Your Screen with Audio";
|
||||
streamStartBtnClone.addEventListener("click", () => {
|
||||
userscript.showStreamDialog();
|
||||
});
|
||||
|
||||
streamStartBtnInitialDisplay = streamStartBtn.style.display;
|
||||
|
||||
streamStartBtn.style.display = "none";
|
||||
streamStartBtn.parentNode.insertBefore(streamStartBtnClone, el);
|
||||
|
||||
clonedElements.push(streamStartBtnClone);
|
||||
hiddenElements.push(streamStartBtn);
|
||||
}
|
||||
}
|
||||
|
||||
// Add about text in settings
|
||||
if (
|
||||
document.getElementsByClassName("dirscordScreenaudioAboutText").length ==
|
||||
0
|
||||
) {
|
||||
for (const el of document.getElementsByClassName("info-3pQQBb")) {
|
||||
let aboutEl;
|
||||
if (userscript.kxmlgui) {
|
||||
aboutEl = document.createElement("a");
|
||||
aboutEl.addEventListener("click", () => {
|
||||
userscript.showHelpMenu();
|
||||
});
|
||||
} else {
|
||||
aboutEl = document.createElement("div");
|
||||
}
|
||||
aboutEl.innerText = `discord-screenaudio ${userscript.version}`;
|
||||
aboutEl.style.fontSize = "12px";
|
||||
aboutEl.style.color = "var(--text-muted)";
|
||||
aboutEl.style.textTransform = "none";
|
||||
aboutEl.style.display = "inline-block";
|
||||
aboutEl.style.width = "100%";
|
||||
aboutEl.classList.add("dirscordScreenaudioAboutText");
|
||||
aboutEl.style.cursor = "pointer";
|
||||
el.appendChild(aboutEl);
|
||||
}
|
||||
}
|
||||
|
||||
// Remove stream settings if stream is active
|
||||
document.getElementById("manage-streams-change-windows")?.remove();
|
||||
document.querySelector(`[aria-label="Stream Settings"]`)?.remove();
|
||||
|
||||
// Add event listener for keybind tab
|
||||
if (
|
||||
document
|
||||
.getElementById("keybinds-tab")
|
||||
?.getElementsByClassName(
|
||||
"container-3jbRo5 info-1hMolH browserNotice-1u-Y5o"
|
||||
).length
|
||||
) {
|
||||
const el = document
|
||||
.getElementById("keybinds-tab")
|
||||
.getElementsByClassName("children-1xdcWE")[0];
|
||||
const div = document.createElement("div");
|
||||
div.style.marginBottom = "50px";
|
||||
div.appendChild(
|
||||
createButton("Edit Global Keybinds", () => {
|
||||
userscript.showShortcutsDialog();
|
||||
})
|
||||
);
|
||||
el.innerHTML = "";
|
||||
el.appendChild(div);
|
||||
}
|
||||
|
||||
const buttonContainer =
|
||||
document.getElementsByClassName("container-YkUktl")[0];
|
||||
if (!buttonContainer) {
|
||||
userscript.log(
|
||||
"Cannot locate Mute/Deafen/Settings button container, please report this on GitHub"
|
||||
);
|
||||
}
|
||||
|
||||
muteBtn = buttonContainer
|
||||
? buttonContainer.getElementsByTagName("button")[0]
|
||||
: null;
|
||||
|
||||
deafenBtn = buttonContainer
|
||||
? buttonContainer.getElementsByTagName("button")[1]
|
||||
: null;
|
||||
|
||||
if (resolutionString) {
|
||||
for (const el of document.getElementsByClassName(
|
||||
"qualityIndicator-39wQDy"
|
||||
)) {
|
||||
el.innerHTML = resolutionString;
|
||||
}
|
||||
}
|
||||
|
||||
const accountTab = document.getElementById("my-account-tab");
|
||||
if (accountTab) {
|
||||
const discordScreenaudioSettings = document.getElementById(
|
||||
"discord-screenaudio-settings"
|
||||
);
|
||||
if (!discordScreenaudioSettings) {
|
||||
const firstDivider = accountTab.getElementsByClassName(
|
||||
"divider-3nqZNm marginTop40-Q4o1tS"
|
||||
)[0];
|
||||
if (firstDivider) {
|
||||
const section = document.createElement("div");
|
||||
section.className = "marginTop40-Q4o1tS";
|
||||
section.id = "discord-screenaudio-settings";
|
||||
|
||||
const title = document.createElement("h2");
|
||||
title.className =
|
||||
"h1-3iMExa title-lXcL8p defaultColor-3Olr-9 defaultMarginh1-1UYutH";
|
||||
title.innerText = "discord-screenaudio";
|
||||
section.appendChild(title);
|
||||
|
||||
section.appendChild(
|
||||
createButton("Edit Global Keybinds", () => {
|
||||
userscript.showShortcutsDialog();
|
||||
})
|
||||
);
|
||||
|
||||
// section.appendChild(
|
||||
// createButton("Install Theme", () => {
|
||||
// userscript.showThemeDialog();
|
||||
// })
|
||||
// );
|
||||
|
||||
// section.appendChild(
|
||||
// createButton("Uninstall Theme", () => {
|
||||
// userscript.installUserStyles("");
|
||||
// })
|
||||
// );
|
||||
|
||||
section.appendChild(
|
||||
createSwitch(
|
||||
"Move discord-screenaudio to the system tray instead of closing",
|
||||
await userscript.getBoolPref("trayIcon", false),
|
||||
(enabled) => {
|
||||
userscript.setTrayIcon(enabled);
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
section.appendChild(
|
||||
createSwitch(
|
||||
"Start discord-screenaudio hidden to tray",
|
||||
await userscript.getPref("startHidden", false),
|
||||
(hidden) => {
|
||||
userscript.setPref("startHidden", hidden);
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
const divider = document.createElement("div");
|
||||
divider.className = "divider-3nqZNm marginTop40-Q4o1tS";
|
||||
|
||||
firstDivider.after(section);
|
||||
section.after(divider);
|
||||
}
|
||||
}
|
||||
}
|
||||
}, 500);
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
<RCC>
|
||||
<qresource>
|
||||
<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>
|
||||
</qresource>
|
||||
</RCC>
|
||||
36
scripts/build_arrpc.sh
Executable file
36
scripts/build_arrpc.sh
Executable file
@@ -0,0 +1,36 @@
|
||||
#!/usr/bin/bash
|
||||
set -e
|
||||
|
||||
cd "$(dirname "$0")/../submodules"
|
||||
|
||||
echo_status() {
|
||||
echo
|
||||
echo
|
||||
echo "-> $1..."
|
||||
}
|
||||
|
||||
if [ ! -d "arrpc" ]; then
|
||||
echo_status "Cloning arRPC"
|
||||
git clone https://github.com/OpenAsar/arrpc.git
|
||||
cd arrpc
|
||||
else
|
||||
echo_status "Fetching arRPC changes"
|
||||
cd arrpc
|
||||
git fetch
|
||||
fi
|
||||
|
||||
echo_status "Checking out latest commit"
|
||||
git reset --hard HEAD
|
||||
git checkout main
|
||||
|
||||
echo_status "Installing dependencies"
|
||||
pnpm i -D @vercel/ncc
|
||||
|
||||
echo_status "Patching arRPC"
|
||||
sed -i 's/"type": "module",//' package.json
|
||||
|
||||
echo_status "Building arRPC"
|
||||
pnpm exec ncc build -m src/index.js
|
||||
|
||||
echo_status "Copying built file"
|
||||
cp -v ./dist/index.js ../../assets/arrpc.js
|
||||
84
src/centralwidget.cpp
Normal file
84
src/centralwidget.cpp
Normal file
@@ -0,0 +1,84 @@
|
||||
#include "centralwidget.h"
|
||||
#include "discordpage.h"
|
||||
#include "mainwindow.h"
|
||||
|
||||
#include <QWebEngineNotification>
|
||||
#include <QWebEngineProfile>
|
||||
#include <QWebEngineScript>
|
||||
#include <QWebEngineScriptCollection>
|
||||
#include <QWebEngineSettings>
|
||||
|
||||
CentralWidget::CentralWidget(QWidget *parent) : QWidget(parent) {
|
||||
setStyleSheet("background-color:#313338;");
|
||||
m_layout = new QVBoxLayout(this);
|
||||
m_layout->setMargin(0);
|
||||
m_layout->setSpacing(0);
|
||||
setupWebView();
|
||||
}
|
||||
|
||||
void CentralWidget::setupWebView() {
|
||||
auto page = new DiscordPage(this);
|
||||
|
||||
m_webView = new QWebEngineView(this);
|
||||
m_webView->setPage(page);
|
||||
|
||||
bool useNotifySend = MainWindow::instance()
|
||||
->settings()
|
||||
->value("useNotifySend", false)
|
||||
.toBool();
|
||||
if (m_useKF5Notifications || useNotifySend)
|
||||
QWebEngineProfile::defaultProfile()->setNotificationPresenter(
|
||||
[&](std::unique_ptr<QWebEngineNotification> notificationInfo) {
|
||||
if (useNotifySend) {
|
||||
auto title = notificationInfo->title();
|
||||
auto message = notificationInfo->message();
|
||||
auto image_path =
|
||||
QString("/tmp/discord-screenaudio-%1.png").arg(title);
|
||||
notificationInfo->icon().save(image_path);
|
||||
QProcess::execute("notify-send",
|
||||
{"--icon", image_path, "--app-name",
|
||||
"discord-screenaudio", title, message});
|
||||
} else if (m_useKF5Notifications) {
|
||||
#ifdef KNOTIFICATIONS
|
||||
KNotification *notification =
|
||||
new KNotification("discordNotification");
|
||||
notification->setTitle(notificationInfo->title());
|
||||
notification->setText(notificationInfo->message());
|
||||
notification->setPixmap(
|
||||
QPixmap::fromImage(notificationInfo->icon()));
|
||||
notification->setDefaultAction("View");
|
||||
connect(notification, &KNotification::defaultActivated,
|
||||
[&, notificationInfo = std::move(notificationInfo)]() {
|
||||
notificationInfo->click();
|
||||
show();
|
||||
activateWindow();
|
||||
});
|
||||
notification->sendEvent();
|
||||
#endif
|
||||
}
|
||||
});
|
||||
|
||||
connect(page->userScript(), &UserScript::loadingMessageChanged, this,
|
||||
&CentralWidget::setLoadingIndicator);
|
||||
|
||||
m_layout->addWidget(m_webView);
|
||||
}
|
||||
|
||||
void CentralWidget::setLoadingIndicator(QString text) {
|
||||
if (text != "") {
|
||||
if (m_loadingLabel == nullptr) {
|
||||
m_loadingLabel = new QLabel(this);
|
||||
m_loadingLabel->setMaximumHeight(20);
|
||||
m_loadingLabel->setAlignment(Qt::AlignHCenter);
|
||||
m_loadingLabel->setStyleSheet("color:#dedede;");
|
||||
m_layout->addWidget(m_loadingLabel);
|
||||
}
|
||||
m_loadingLabel->setText(text.mid(0, 100));
|
||||
} else {
|
||||
if (m_loadingLabel != nullptr) {
|
||||
m_layout->removeWidget(m_loadingLabel);
|
||||
m_loadingLabel->deleteLater();
|
||||
m_loadingLabel = nullptr;
|
||||
}
|
||||
}
|
||||
}
|
||||
29
src/centralwidget.h
Normal file
29
src/centralwidget.h
Normal file
@@ -0,0 +1,29 @@
|
||||
#pragma once
|
||||
|
||||
#include <QLabel>
|
||||
#include <QVBoxLayout>
|
||||
#include <QWebEnginePage>
|
||||
#include <QWebEngineProfile>
|
||||
#include <QWebEngineView>
|
||||
#include <QWidget>
|
||||
|
||||
class CentralWidget : public QWidget {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
CentralWidget(QWidget *parent = nullptr);
|
||||
|
||||
private:
|
||||
void setupWebView();
|
||||
QVBoxLayout *m_layout;
|
||||
QWebEngineView *m_webView;
|
||||
#ifdef KNOTIFICATIONS
|
||||
bool m_useKF5Notifications = true;
|
||||
#else
|
||||
bool m_useKF5Notifications = false;
|
||||
#endif
|
||||
QLabel *m_loadingLabel = nullptr;
|
||||
|
||||
public Q_SLOTS:
|
||||
void setLoadingIndicator(QString text);
|
||||
};
|
||||
@@ -3,24 +3,13 @@
|
||||
#include "mainwindow.h"
|
||||
#include "virtmic.h"
|
||||
|
||||
#ifdef KXMLGUI
|
||||
#include <KAboutData>
|
||||
#include <KHelpMenu>
|
||||
#include <KShortcutsDialog>
|
||||
#include <KXmlGuiWindow>
|
||||
#include <QAction>
|
||||
|
||||
#ifdef KGLOBALACCEL
|
||||
#include <KGlobalAccel>
|
||||
#endif
|
||||
|
||||
#endif
|
||||
|
||||
#include <QApplication>
|
||||
#include <QDesktopServices>
|
||||
#include <QFile>
|
||||
#include <QFileInfo>
|
||||
#include <QMessageBox>
|
||||
#include <QNetworkReply>
|
||||
#include <QTemporaryFile>
|
||||
#include <QTimer>
|
||||
#include <QWebChannel>
|
||||
#include <QWebEngineScript>
|
||||
@@ -28,17 +17,31 @@
|
||||
#include <QWebEngineSettings>
|
||||
|
||||
DiscordPage::DiscordPage(QWidget *parent) : QWebEnginePage(parent) {
|
||||
setBackgroundColor(QColor("#202225"));
|
||||
m_virtmicProcess.setProcessChannelMode(QProcess::ForwardedChannels);
|
||||
setBackgroundColor(QColor("#313338"));
|
||||
|
||||
connect(this, &QWebEnginePage::featurePermissionRequested, this,
|
||||
&DiscordPage::featurePermissionRequested);
|
||||
connect(this, &DiscordPage::fullScreenRequested, MainWindow::instance(),
|
||||
&MainWindow::fullScreenRequested);
|
||||
|
||||
connect(this, &QWebEnginePage::loadStarted, [=]() {
|
||||
runJavaScript(QString("window.discordScreenaudioVersion = '%1';")
|
||||
.arg(QApplication::applicationVersion()));
|
||||
});
|
||||
setupPermissions();
|
||||
|
||||
injectFile(&DiscordPage::injectScript, "qwebchannel.js",
|
||||
":/qtwebchannel/qwebchannel.js");
|
||||
|
||||
setUrl(QUrl("https://discord.com/app"));
|
||||
|
||||
setWebChannel(new QWebChannel(this));
|
||||
webChannel()->registerObject("userscript", &m_userScript);
|
||||
|
||||
injectFile(&DiscordPage::injectScript, "userscript.js",
|
||||
":/assets/userscript.js");
|
||||
|
||||
setupUserStyles();
|
||||
setupArrpc();
|
||||
}
|
||||
|
||||
void DiscordPage::setupPermissions() {
|
||||
settings()->setAttribute(QWebEngineSettings::ScreenCaptureEnabled, true);
|
||||
settings()->setAttribute(QWebEngineSettings::JavascriptCanOpenWindows, true);
|
||||
settings()->setAttribute(QWebEngineSettings::AllowRunningInsecureContent,
|
||||
@@ -50,71 +53,87 @@ DiscordPage::DiscordPage(QWidget *parent) : QWebEnginePage(parent) {
|
||||
false);
|
||||
settings()->setAttribute(QWebEngineSettings::JavascriptCanOpenWindows, false);
|
||||
settings()->setAttribute(QWebEngineSettings::ScrollAnimatorEnabled, true);
|
||||
|
||||
setUrl(QUrl("https://discord.com/app"));
|
||||
|
||||
injectScriptFile("userscript.js", ":/assets/userscript.js");
|
||||
|
||||
injectScriptText("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;
|
||||
|
||||
QWebEngineScript script;
|
||||
@@ -122,20 +141,36 @@ void DiscordPage::injectScriptText(QString name, QString content) {
|
||||
script.setSourceCode(content);
|
||||
script.setName(name);
|
||||
script.setWorldId(QWebEngineScript::MainWorld);
|
||||
script.setInjectionPoint(QWebEngineScript::DocumentCreation);
|
||||
script.setInjectionPoint(injectionPoint);
|
||||
script.setRunsOnSubFrames(false);
|
||||
|
||||
scripts().insert(script);
|
||||
}
|
||||
|
||||
void DiscordPage::injectScriptFile(QString name, QString source) {
|
||||
void DiscordPage::injectScript(QString name, QString content) {
|
||||
injectScript(name, content, QWebEngineScript::DocumentCreation);
|
||||
}
|
||||
|
||||
void DiscordPage::injectStylesheet(QString name, QString content) {
|
||||
auto script = QString(R"(const stylesheet = document.createElement("style");
|
||||
stylesheet.id = "%1";
|
||||
stylesheet.innerText = `%2`;
|
||||
document.head.appendChild(stylesheet);
|
||||
)")
|
||||
.arg(name)
|
||||
.arg(content);
|
||||
injectScript(name, script, QWebEngineScript::DocumentReady);
|
||||
}
|
||||
|
||||
void DiscordPage::injectFile(void (DiscordPage::*inject)(QString, QString),
|
||||
QString name, QString source) {
|
||||
QFile file(source);
|
||||
|
||||
if (!file.open(QIODevice::ReadOnly)) {
|
||||
qFatal("Failed to load %s with error: %s", source.toLatin1().constData(),
|
||||
file.errorString().toLatin1().constData());
|
||||
} else {
|
||||
injectScriptText(name, file.readAll());
|
||||
(this->*inject)(name, file.readAll());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -146,11 +181,10 @@ void DiscordPage::featurePermissionRequested(const QUrl &securityOrigin,
|
||||
QWebEnginePage::PermissionGrantedByUser);
|
||||
|
||||
if (feature == QWebEnginePage::Feature::MediaAudioCapture) {
|
||||
if (m_virtmicProcess.state() == QProcess::NotRunning) {
|
||||
if (!m_userScript.isVirtmicRunning()) {
|
||||
qDebug(virtmicLog) << "Starting Virtmic with no target to make sure "
|
||||
"Discord can find all the audio devices";
|
||||
m_virtmicProcess.start(QApplication::arguments()[0],
|
||||
{"--virtmic", "None"});
|
||||
m_userScript.startVirtmic("None");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -177,78 +211,101 @@ QWebEnginePage *DiscordPage::createWindow(QWebEnginePage::WebWindowType type) {
|
||||
return new ExternalPage;
|
||||
}
|
||||
|
||||
void DiscordPage::stopVirtmic() {
|
||||
if (m_virtmicProcess.state() == QProcess::Running) {
|
||||
qDebug(virtmicLog) << "Stopping Virtmic";
|
||||
m_virtmicProcess.kill();
|
||||
m_virtmicProcess.waitForFinished();
|
||||
}
|
||||
}
|
||||
|
||||
void DiscordPage::startVirtmic(QString target) {
|
||||
qDebug(virtmicLog) << "Starting Virtmic with target" << target;
|
||||
m_virtmicProcess.start(QApplication::arguments()[0], {"--virtmic", target});
|
||||
}
|
||||
const QMap<QString, QString> cssAnsiColorMap = {{"black", "30"},
|
||||
{"red", "31"},
|
||||
{"green", "32"},
|
||||
{"yellow", "33"},
|
||||
{"blue", "34"},
|
||||
{"magenta", "35"},
|
||||
{"cyan", "36"},
|
||||
{"white", "37"},
|
||||
{"gray", "90"},
|
||||
{"bright-red", "91"},
|
||||
{"bright-green", "92"},
|
||||
{"bright-yellow", "93"},
|
||||
{"bright-blue", "94"},
|
||||
{"bright-magenta", "95"},
|
||||
{"bright-cyan", "96"},
|
||||
{"bright-white", "97"},
|
||||
{"orange", "38;5;208"},
|
||||
{"pink", "38;5;205"},
|
||||
{"brown", "38;5;94"},
|
||||
{"light-gray", "38;5;251"},
|
||||
{"dark-gray", "38;5;239"},
|
||||
{"light-red", "38;5;203"},
|
||||
{"light-green", "38;5;83"},
|
||||
{"light-yellow", "38;5;227"},
|
||||
{"light-blue", "38;5;75"},
|
||||
{"light-magenta", "38;5;207"},
|
||||
{"light-cyan", "38;5;87"},
|
||||
{"turquoise", "38;5;80"},
|
||||
{"violet", "38;5;92"},
|
||||
{"purple", "38;5;127"},
|
||||
{"lavender", "38;5;183"},
|
||||
{"maroon", "38;5;124"},
|
||||
{"beige", "38;5;230"},
|
||||
{"olive", "38;5;142"},
|
||||
{"indigo", "38;5;54"},
|
||||
{"teal", "38;5;30"},
|
||||
{"gold", "38;5;220"},
|
||||
{"silver", "38;5;7"},
|
||||
{"navy", "38;5;17"},
|
||||
{"steel", "38;5;188"},
|
||||
{"salmon", "38;5;173"},
|
||||
{"peach", "38;5;217"},
|
||||
{"khaki", "38;5;179"},
|
||||
{"coral", "38;5;209"},
|
||||
{"crimson", "38;5;160"}};
|
||||
|
||||
void DiscordPage::javaScriptConsoleMessage(
|
||||
QWebEnginePage::JavaScriptConsoleMessageLevel level, const QString &message,
|
||||
int lineNumber, const QString &sourceID) {
|
||||
if (message == "!discord-screenaudio-start-stream") {
|
||||
if (m_streamDialog.isHidden())
|
||||
m_streamDialog.setHidden(false);
|
||||
else
|
||||
m_streamDialog.activateWindow();
|
||||
m_streamDialog.updateTargets();
|
||||
} else if (message == "!discord-screenaudio-stream-stopped") {
|
||||
stopVirtmic();
|
||||
} else if (message == "!discord-screenaudio-about") {
|
||||
#ifdef KXMLGUI
|
||||
m_helpMenu->aboutApplication();
|
||||
#endif
|
||||
} else if (message == "!discord-screenaudio-keybinds") {
|
||||
#ifdef KXMLGUI
|
||||
#ifdef KGLOBALACCEL
|
||||
m_shortcutsDialog->show();
|
||||
#else
|
||||
QMessageBox::information(MainWindow::instance(), "discord-screenaudio",
|
||||
"Keybinds are not supported on this platform "
|
||||
"(KGlobalAccel is not available).",
|
||||
QMessageBox::Ok);
|
||||
#endif
|
||||
#else
|
||||
QMessageBox::information(MainWindow::instance(), "discord-screenaudio",
|
||||
"Keybinds are not supported on this platform "
|
||||
"(KXmlGui and KGlobalAccel are not available).",
|
||||
QMessageBox::Ok);
|
||||
#endif
|
||||
} else if (message.startsWith("dsa: ")) {
|
||||
qDebug(userscriptLog) << message.mid(5).toUtf8().constData();
|
||||
} else {
|
||||
qDebug(discordLog) << message;
|
||||
auto colorSegments = message.split("%c");
|
||||
if (colorSegments[0] != "") {
|
||||
for (auto line : colorSegments[0].split("\n"))
|
||||
qDebug(discordLog) << line.toUtf8().constData();
|
||||
}
|
||||
for (auto segment : colorSegments.mid(1)) {
|
||||
auto lines = segment.split("\n");
|
||||
QString ansi;
|
||||
uint endOfStyles = lines.length();
|
||||
for (auto line = 1; line < lines.length(); line++) {
|
||||
if (!lines[line].endsWith(";")) {
|
||||
endOfStyles = line;
|
||||
break;
|
||||
}
|
||||
if (lines[line] == "font-weight: bold;")
|
||||
ansi += "\033[1m";
|
||||
else if (lines[line].startsWith("color: ")) {
|
||||
auto color = lines[line].mid(7).chopped(1);
|
||||
if (cssAnsiColorMap.find(color) != cssAnsiColorMap.end())
|
||||
ansi += "\033[" + cssAnsiColorMap[color] + "m";
|
||||
}
|
||||
}
|
||||
qDebug(discordLog) << (ansi + lines[0].trimmed() + "\033[0m " +
|
||||
lines[endOfStyles].trimmed())
|
||||
.toUtf8()
|
||||
.constData();
|
||||
for (auto line : lines.mid(endOfStyles + 1)) {
|
||||
qDebug(discordLog) << line.toUtf8().constData();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void DiscordPage::startStream(bool video, bool audio, uint width, uint height,
|
||||
uint frameRate, QString target) {
|
||||
stopVirtmic();
|
||||
startVirtmic(audio ? target : "[None]");
|
||||
// Wait a bit for the virtmic to start
|
||||
QTimer::singleShot(200, [=]() {
|
||||
runJavaScript(
|
||||
QString("window.discordScreenaudioStartStream(%1, %2, %3, %4);")
|
||||
.arg(video)
|
||||
.arg(video ? width : 32)
|
||||
.arg(video ? height : 16)
|
||||
.arg(video ? frameRate : 1));
|
||||
});
|
||||
}
|
||||
UserScript *DiscordPage::userScript() { return &m_userScript; }
|
||||
|
||||
void DiscordPage::toggleMute() {
|
||||
qDebug(shortcutLog) << "Toggling mute";
|
||||
runJavaScript("window.discordScreenaudioToggleMute();");
|
||||
}
|
||||
void DiscordPage::setupArrpc() {
|
||||
QFile nodejs("/usr/bin/node");
|
||||
if (nodejs.exists()) {
|
||||
auto arrpcSource = QTemporaryFile::createNativeFile(":/assets/arrpc.js");
|
||||
qDebug(mainLog).noquote()
|
||||
<< "NodeJS found, starting arRPC located at" << arrpcSource->fileName();
|
||||
m_arrpcProcess.setProcessChannelMode(QProcess::ForwardedChannels);
|
||||
m_arrpcProcess.setProgram(nodejs.fileName());
|
||||
m_arrpcProcess.setArguments(QStringList{arrpcSource->fileName()});
|
||||
m_arrpcProcess.start();
|
||||
|
||||
void DiscordPage::toggleDeafen() {
|
||||
qDebug(shortcutLog) << "Toggling deafen";
|
||||
runJavaScript("window.discordScreenaudioToggleDeafen();");
|
||||
injectFile(&DiscordPage::injectScript, "arrpc_bridge_mod.js",
|
||||
":/assets/arrpc_bridge_mod.js");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,34 +1,35 @@
|
||||
#pragma once
|
||||
|
||||
#include "streamdialog.h"
|
||||
#include "virtmic.h"
|
||||
|
||||
#ifdef KXMLGUI
|
||||
#include <KActionCollection>
|
||||
#include <KHelpMenu>
|
||||
#include <KShortcutsDialog>
|
||||
#endif
|
||||
#include "userscript.h"
|
||||
|
||||
#include <QDir>
|
||||
#include <QFile>
|
||||
#include <QNetworkAccessManager>
|
||||
#include <QProcess>
|
||||
#include <QStandardPaths>
|
||||
#include <QWebEngineFullScreenRequest>
|
||||
#include <QWebEnginePage>
|
||||
#include <QWebEngineScript>
|
||||
|
||||
class DiscordPage : public QWebEnginePage {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit DiscordPage(QWidget *parent = nullptr);
|
||||
UserScript *userScript();
|
||||
|
||||
private:
|
||||
StreamDialog m_streamDialog;
|
||||
QProcess m_virtmicProcess;
|
||||
#ifdef KXMLGUI
|
||||
KHelpMenu *m_helpMenu;
|
||||
#ifdef KGLOBALACCEL
|
||||
KActionCollection *m_actionCollection;
|
||||
KShortcutsDialog *m_shortcutsDialog;
|
||||
#endif
|
||||
#endif
|
||||
UserScript m_userScript;
|
||||
QFile *m_userStylesFile;
|
||||
QString m_userStylesContent;
|
||||
QNetworkAccessManager m_networkAccessManager;
|
||||
QProcess m_arrpcProcess;
|
||||
const QDir m_configLocation =
|
||||
QStandardPaths::writableLocation(QStandardPaths::AppConfigLocation);
|
||||
void setupPermissions();
|
||||
void setupUserStyles();
|
||||
void setupArrpc();
|
||||
void fetchUserStyles();
|
||||
bool acceptNavigationRequest(const QUrl &url,
|
||||
QWebEnginePage::NavigationType type,
|
||||
bool isMainFrame) override;
|
||||
@@ -37,18 +38,19 @@ private:
|
||||
javaScriptConsoleMessage(QWebEnginePage::JavaScriptConsoleMessageLevel level,
|
||||
const QString &message, int lineNumber,
|
||||
const QString &sourceID) override;
|
||||
void injectScriptText(QString name, QString content);
|
||||
void injectScriptFile(QString name, QString source);
|
||||
void stopVirtmic();
|
||||
void startVirtmic(QString target);
|
||||
void toggleMute();
|
||||
void toggleDeafen();
|
||||
void injectScript(QString name, QString content,
|
||||
QWebEngineScript::InjectionPoint injectionPoint);
|
||||
void injectScript(QString name, QString content);
|
||||
void injectStylesheet(QString name, QString content);
|
||||
void injectFile(void (DiscordPage::*inject)(QString, QString), QString name,
|
||||
QString source);
|
||||
|
||||
private Q_SLOTS:
|
||||
void featurePermissionRequested(const QUrl &securityOrigin,
|
||||
QWebEnginePage::Feature feature);
|
||||
void startStream(bool video, bool audio, uint width, uint height,
|
||||
uint frameRate, QString target);
|
||||
|
||||
public Q_SLOTS:
|
||||
void getUserStyles(QString url);
|
||||
};
|
||||
|
||||
// Will immediately get destroyed again but is needed for navigation to
|
||||
|
||||
@@ -5,3 +5,4 @@ Q_LOGGING_CATEGORY(discordLog, "discord");
|
||||
Q_LOGGING_CATEGORY(userscriptLog, "userscript");
|
||||
Q_LOGGING_CATEGORY(virtmicLog, "virtmic");
|
||||
Q_LOGGING_CATEGORY(shortcutLog, "shortcut");
|
||||
Q_LOGGING_CATEGORY(userstylesLog, "userstyles");
|
||||
|
||||
@@ -7,3 +7,4 @@ Q_DECLARE_LOGGING_CATEGORY(discordLog);
|
||||
Q_DECLARE_LOGGING_CATEGORY(userscriptLog);
|
||||
Q_DECLARE_LOGGING_CATEGORY(virtmicLog);
|
||||
Q_DECLARE_LOGGING_CATEGORY(shortcutLog);
|
||||
Q_DECLARE_LOGGING_CATEGORY(userstylesLog);
|
||||
|
||||
@@ -24,9 +24,6 @@ int main(int argc, char *argv[]) {
|
||||
"Custom Discord client with the ability to stream audio on Linux");
|
||||
parser.addHelpOption();
|
||||
parser.addVersionOption();
|
||||
QCommandLineOption virtmicOption("virtmic", "Start the Virtual Microphone",
|
||||
"target");
|
||||
parser.addOption(virtmicOption);
|
||||
QCommandLineOption degubOption("remote-debugging",
|
||||
"Open Chromium Remote Debugging on port 9222");
|
||||
parser.addOption(degubOption);
|
||||
@@ -36,10 +33,6 @@ int main(int argc, char *argv[]) {
|
||||
|
||||
parser.process(app);
|
||||
|
||||
if (parser.isSet(virtmicOption)) {
|
||||
Virtmic::start(parser.value(virtmicOption));
|
||||
}
|
||||
|
||||
qputenv("QTWEBENGINE_CHROMIUM_FLAGS",
|
||||
"--enable-features=WebRTCPipeWireCapturer " +
|
||||
qgetenv("QTWEBENGINE_CHROMIUM_FLAGS"));
|
||||
|
||||
@@ -14,12 +14,9 @@
|
||||
#include <QPushButton>
|
||||
#include <QSpacerItem>
|
||||
#include <QThread>
|
||||
#include <QTimer>
|
||||
#include <QUrl>
|
||||
#include <QWebEngineNotification>
|
||||
#include <QWebEngineProfile>
|
||||
#include <QWebEngineScript>
|
||||
#include <QWebEngineScriptCollection>
|
||||
#include <QWebEngineSettings>
|
||||
#include <QWebEngineFullScreenRequest>
|
||||
#include <QWidget>
|
||||
|
||||
MainWindow *MainWindow::m_instance = nullptr;
|
||||
@@ -28,52 +25,22 @@ MainWindow::MainWindow(bool useNotifySend, QWidget *parent)
|
||||
: QMainWindow(parent) {
|
||||
assert(MainWindow::m_instance == nullptr);
|
||||
MainWindow::m_instance = this;
|
||||
m_useNotifySend = useNotifySend;
|
||||
setupWebView();
|
||||
resize(1000, 700);
|
||||
showMaximized();
|
||||
}
|
||||
|
||||
void MainWindow::setupWebView() {
|
||||
auto page = new DiscordPage(this);
|
||||
connect(page, &QWebEnginePage::fullScreenRequested, this,
|
||||
&MainWindow::fullScreenRequested);
|
||||
|
||||
m_webView = new QWebEngineView(this);
|
||||
m_webView->setPage(page);
|
||||
|
||||
if (m_useKF5Notifications || m_useNotifySend)
|
||||
QWebEngineProfile::defaultProfile()->setNotificationPresenter(
|
||||
[&](std::unique_ptr<QWebEngineNotification> notificationInfo) {
|
||||
if (m_useNotifySend) {
|
||||
auto title = notificationInfo->title();
|
||||
auto message = notificationInfo->message();
|
||||
auto image_path =
|
||||
QString("/tmp/discord-screenaudio-%1.png").arg(title);
|
||||
notificationInfo->icon().save(image_path);
|
||||
QProcess::execute("notify-send",
|
||||
{"--icon", image_path, "--app-name",
|
||||
"discord-screenaudio", title, message});
|
||||
} else if (m_useKF5Notifications) {
|
||||
#ifdef KNOTIFICATIONS
|
||||
KNotification *notification =
|
||||
new KNotification("discordNotification");
|
||||
notification->setTitle(notificationInfo->title());
|
||||
notification->setText(notificationInfo->message());
|
||||
notification->setPixmap(
|
||||
QPixmap::fromImage(notificationInfo->icon()));
|
||||
notification->setDefaultAction("View");
|
||||
connect(notification, &KNotification::defaultActivated,
|
||||
[&, notificationInfo = std::move(notificationInfo)]() {
|
||||
notificationInfo->click();
|
||||
activateWindow();
|
||||
});
|
||||
notification->sendEvent();
|
||||
#endif
|
||||
}
|
||||
});
|
||||
|
||||
setCentralWidget(m_webView);
|
||||
setupSettings();
|
||||
m_settings->setValue("useNotifySend", useNotifySend);
|
||||
m_centralWidget = new CentralWidget(this);
|
||||
setCentralWidget(m_centralWidget);
|
||||
setupTrayIcon();
|
||||
if (m_settings->contains("geometry")) {
|
||||
restoreGeometry(m_settings->value("geometry").toByteArray());
|
||||
} else {
|
||||
resize(1000, 700);
|
||||
showMaximized();
|
||||
}
|
||||
if (m_settings->value("trayIcon", false).toBool() &&
|
||||
m_settings->value("startHidden", false).toBool()) {
|
||||
hide();
|
||||
QTimer::singleShot(0, [=]() { hide(); });
|
||||
}
|
||||
}
|
||||
|
||||
void MainWindow::fullScreenRequested(
|
||||
@@ -87,6 +54,79 @@ void MainWindow::fullScreenRequested(
|
||||
}
|
||||
}
|
||||
|
||||
void MainWindow::closeEvent(QCloseEvent *event) { QApplication::quit(); }
|
||||
void MainWindow::setupTrayIcon() {
|
||||
if (m_settings->value("trayIcon", false).toBool() == false ||
|
||||
m_trayIcon != nullptr)
|
||||
return;
|
||||
|
||||
auto aboutAction = new QAction(
|
||||
"discord-screenaudio v" + QString(DISCORD_SCEENAUDIO_VERSION_FULL), this);
|
||||
aboutAction->setIcon(QIcon(":assets/de.shorsh.discord-screenaudio.png"));
|
||||
aboutAction->setEnabled(false);
|
||||
|
||||
auto exitAction = new QAction("Exit", this);
|
||||
connect(exitAction, &QAction::triggered, []() { QApplication::quit(); });
|
||||
|
||||
m_trayIconMenu = new QMenu(this);
|
||||
m_trayIconMenu->addAction(aboutAction);
|
||||
m_trayIconMenu->addAction(exitAction);
|
||||
|
||||
m_trayIcon = new QSystemTrayIcon(this);
|
||||
m_trayIcon->setContextMenu(m_trayIconMenu);
|
||||
m_trayIcon->setIcon(QIcon(":assets/de.shorsh.discord-screenaudio.png"));
|
||||
m_trayIcon->show();
|
||||
|
||||
connect(m_trayIcon, &QSystemTrayIcon::activated, [this](auto reason) {
|
||||
if (reason == QSystemTrayIcon::Trigger) {
|
||||
if (isVisible()) {
|
||||
hide();
|
||||
} else {
|
||||
show();
|
||||
activateWindow();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void MainWindow::cleanTrayIcon() {
|
||||
if (m_trayIcon == nullptr)
|
||||
return;
|
||||
m_trayIcon->hide();
|
||||
m_trayIconMenu->deleteLater();
|
||||
m_trayIcon->deleteLater();
|
||||
m_trayIconMenu = nullptr;
|
||||
m_trayIcon = nullptr;
|
||||
}
|
||||
|
||||
void MainWindow::setupSettings() {
|
||||
m_settings =
|
||||
new QSettings("discord-screenaudio", "discord-screenaudio", this);
|
||||
m_settings->beginGroup("settings");
|
||||
m_settings->endGroup();
|
||||
}
|
||||
|
||||
QSettings *MainWindow::settings() const { return m_settings; }
|
||||
|
||||
void MainWindow::setTrayIcon(bool enabled) {
|
||||
m_settings->setValue("trayIcon", enabled);
|
||||
if (enabled) {
|
||||
setupTrayIcon();
|
||||
} else {
|
||||
cleanTrayIcon();
|
||||
}
|
||||
}
|
||||
|
||||
void MainWindow::closeEvent(QCloseEvent *event) {
|
||||
if (m_settings->value("trayIcon", false).toBool()) {
|
||||
hide();
|
||||
} else {
|
||||
m_settings->setValue("geometry", saveGeometry());
|
||||
QApplication::quit();
|
||||
}
|
||||
}
|
||||
|
||||
MainWindow *MainWindow::instance() { return m_instance; }
|
||||
|
||||
CentralWidget *MainWindow::centralWidget() {
|
||||
return instance()->m_centralWidget;
|
||||
};
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
#pragma once
|
||||
|
||||
#include "discordpage.h"
|
||||
#include "centralwidget.h"
|
||||
#include "virtmic.h"
|
||||
|
||||
#include <QMainWindow>
|
||||
#include <QMenu>
|
||||
#include <QScopedPointer>
|
||||
#include <QSettings>
|
||||
#include <QString>
|
||||
#include <QSystemTrayIcon>
|
||||
#include <QVBoxLayout>
|
||||
#include <QVector>
|
||||
#include <QWebEnginePage>
|
||||
#include <QWebEngineProfile>
|
||||
#include <QWebEngineView>
|
||||
|
||||
class MainWindow : public QMainWindow {
|
||||
Q_OBJECT
|
||||
@@ -16,22 +18,23 @@ class MainWindow : public QMainWindow {
|
||||
public:
|
||||
explicit MainWindow(bool useNotifySend = false, QWidget *parent = nullptr);
|
||||
static MainWindow *instance();
|
||||
QSettings *settings() const;
|
||||
static CentralWidget *centralWidget();
|
||||
|
||||
private:
|
||||
void setupWebView();
|
||||
QWebEngineView *m_webView;
|
||||
void setupTrayIcon();
|
||||
void cleanTrayIcon();
|
||||
void setupSettings();
|
||||
QWebEngineProfile *prepareProfile();
|
||||
DiscordPage *m_discordPage;
|
||||
void closeEvent(QCloseEvent *event) override;
|
||||
QSystemTrayIcon *m_trayIcon = nullptr;
|
||||
QMenu *m_trayIconMenu;
|
||||
QSettings *m_settings;
|
||||
bool m_wasMaximized;
|
||||
static MainWindow *m_instance;
|
||||
bool m_useNotifySend;
|
||||
#ifdef KNOTIFICATIONS
|
||||
bool m_useKF5Notifications = true;
|
||||
#else
|
||||
bool m_useKF5Notifications = false;
|
||||
#endif
|
||||
CentralWidget *m_centralWidget;
|
||||
|
||||
private Q_SLOTS:
|
||||
public Q_SLOTS:
|
||||
void setTrayIcon(bool enabled);
|
||||
void fullScreenRequested(QWebEngineFullScreenRequest fullScreenRequest);
|
||||
};
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#include "streamdialog.h"
|
||||
#include "mainwindow.h"
|
||||
#include "virtmic.h"
|
||||
|
||||
#include <QComboBox>
|
||||
@@ -9,7 +10,7 @@
|
||||
#include <QSizePolicy>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
StreamDialog::StreamDialog() : QWidget() {
|
||||
StreamDialog::StreamDialog(QWidget *parent) : QDialog(parent) {
|
||||
setAttribute(Qt::WA_QuitOnClose, false);
|
||||
|
||||
{
|
||||
@@ -22,8 +23,7 @@ StreamDialog::StreamDialog() : QWidget() {
|
||||
layout->addWidget(m_videoGroupBox);
|
||||
|
||||
{
|
||||
auto videoLayout = new QVBoxLayout(this);
|
||||
m_videoGroupBox->setLayout(videoLayout);
|
||||
auto videoLayout = new QVBoxLayout(m_videoGroupBox);
|
||||
|
||||
auto resolutionLabel = new QLabel(this);
|
||||
resolutionLabel->setText("Resolution");
|
||||
@@ -60,15 +60,14 @@ StreamDialog::StreamDialog() : QWidget() {
|
||||
layout->addWidget(m_audioGroupBox);
|
||||
|
||||
{
|
||||
auto audioLayout = new QVBoxLayout(this);
|
||||
m_audioGroupBox->setLayout(audioLayout);
|
||||
auto audioLayout = new QVBoxLayout(m_audioGroupBox);
|
||||
|
||||
auto targetLabel = new QLabel(this);
|
||||
targetLabel->setText("Audio Source");
|
||||
audioLayout->addWidget(targetLabel);
|
||||
|
||||
{
|
||||
auto targetLayout = new QHBoxLayout(this);
|
||||
auto targetLayout = new QHBoxLayout();
|
||||
audioLayout->addLayout(targetLayout);
|
||||
|
||||
m_targetComboBox = new QComboBox(this);
|
||||
@@ -88,8 +87,6 @@ StreamDialog::StreamDialog() : QWidget() {
|
||||
button->setText("Start Stream");
|
||||
connect(button, &QPushButton::clicked, this, &StreamDialog::startStream);
|
||||
layout->addWidget(button, Qt::AlignRight | Qt::AlignBottom);
|
||||
|
||||
setLayout(layout);
|
||||
}
|
||||
|
||||
setWindowTitle("discord-screenaudio Stream Dialog");
|
||||
@@ -98,9 +95,9 @@ StreamDialog::StreamDialog() : QWidget() {
|
||||
void StreamDialog::startStream() {
|
||||
auto resolution = m_resolutionComboBox->currentData().toString().split('x');
|
||||
emit requestedStreamStart(m_videoGroupBox->isChecked(),
|
||||
m_audioGroupBox->isChecked(),
|
||||
resolution[0].toUInt(), resolution[1].toUInt(),
|
||||
m_framerateComboBox->currentData().toUInt(),
|
||||
m_audioGroupBox->isChecked(), resolution[0].toInt(),
|
||||
resolution[1].toInt(),
|
||||
m_framerateComboBox->currentData().toInt(),
|
||||
m_targetComboBox->currentText());
|
||||
setHidden(true);
|
||||
}
|
||||
|
||||
@@ -5,11 +5,11 @@
|
||||
#include <QGroupBox>
|
||||
#include <QWidget>
|
||||
|
||||
class StreamDialog : public QWidget {
|
||||
class StreamDialog : public QDialog {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit StreamDialog();
|
||||
explicit StreamDialog(QWidget *parent = nullptr);
|
||||
|
||||
private:
|
||||
QComboBox *m_targetComboBox;
|
||||
@@ -19,8 +19,8 @@ private:
|
||||
QGroupBox *m_audioGroupBox;
|
||||
|
||||
Q_SIGNALS:
|
||||
void requestedStreamStart(bool video, bool audio, uint width, uint height,
|
||||
uint frameRate, QString target);
|
||||
void requestedStreamStart(bool video, bool audio, int width, int height,
|
||||
int frameRate, QString target);
|
||||
|
||||
public Q_SLOTS:
|
||||
void updateTargets();
|
||||
|
||||
184
src/userscript.cpp
Normal file
184
src/userscript.cpp
Normal file
@@ -0,0 +1,184 @@
|
||||
#include "userscript.h"
|
||||
#include "log.h"
|
||||
#include "mainwindow.h"
|
||||
|
||||
#include <QApplication>
|
||||
#include <QDebug>
|
||||
#include <QFile>
|
||||
#include <QInputDialog>
|
||||
#include <QMessageBox>
|
||||
#include <QStandardPaths>
|
||||
#include <QTimer>
|
||||
|
||||
#ifdef KXMLGUI
|
||||
#include <KActionCollection>
|
||||
#endif
|
||||
|
||||
UserScript::UserScript() : QObject() {
|
||||
setupHelpMenu();
|
||||
setupShortcutsDialog();
|
||||
setupStreamDialog();
|
||||
setupVirtmic();
|
||||
}
|
||||
|
||||
void UserScript::setupHelpMenu() {
|
||||
#ifdef KXMLGUI
|
||||
m_kxmlgui = true;
|
||||
|
||||
KAboutData aboutData(
|
||||
"discord-screenaudio", "discord-screenaudio",
|
||||
QApplication::applicationVersion(),
|
||||
"Custom Discord client with the ability to stream audio on Linux",
|
||||
KAboutLicense::GPL_V3, "Copyright 2022 (C) Malte Jürgens");
|
||||
aboutData.setBugAddress("https://github.com/maltejur/discord-screenaudio");
|
||||
aboutData.addAuthor("Malte Jürgens", "Author", "maltejur@dismail.de",
|
||||
"https://github.com/maltejur");
|
||||
aboutData.addCredit("edisionnano",
|
||||
"For creating and documenting the approach for streaming "
|
||||
"audio in Discord used in this project.",
|
||||
QString(),
|
||||
"https://github.com/edisionnano/"
|
||||
"Screenshare-with-audio-on-Discord-with-Linux");
|
||||
aboutData.addCredit(
|
||||
"Curve", "For creating the Rohrkabel library used in this project.",
|
||||
QString(), "https://github.com/Curve");
|
||||
aboutData.addComponent("Rohrkabel", "A C++ RAII Pipewire-API Wrapper", "1.5",
|
||||
"https://github.com/Soundux/rohrkabel");
|
||||
aboutData.addComponent(
|
||||
"Soundux/channel ",
|
||||
"A C++ implementation of Rust's std::sync::mpsc::channel", nullptr,
|
||||
"https://github.com/Soundux/channel");
|
||||
m_helpMenu = new KHelpMenu(MainWindow::instance(), aboutData);
|
||||
#endif
|
||||
}
|
||||
|
||||
void UserScript::setupShortcutsDialog() {
|
||||
#ifdef KXMLGUI
|
||||
#ifdef KGLOBALACCEL
|
||||
m_kglobalaccel = true;
|
||||
|
||||
auto toggleMuteAction = new QAction(this);
|
||||
toggleMuteAction->setText("Toggle Mute");
|
||||
toggleMuteAction->setIcon(QIcon::fromTheme("microphone-sensitivity-muted"));
|
||||
connect(toggleMuteAction, &QAction::triggered, this,
|
||||
&UserScript::muteToggled);
|
||||
|
||||
auto toggleDeafenAction = new QAction(this);
|
||||
toggleDeafenAction->setText("Toggle Deafen");
|
||||
toggleDeafenAction->setIcon(QIcon::fromTheme("audio-volume-muted"));
|
||||
connect(toggleDeafenAction, &QAction::triggered, this,
|
||||
&UserScript::deafenToggled);
|
||||
|
||||
m_actionCollection = new KActionCollection(this);
|
||||
m_actionCollection->addAction("toggleMute", toggleMuteAction);
|
||||
KGlobalAccel::setGlobalShortcut(toggleMuteAction, QList<QKeySequence>{});
|
||||
m_actionCollection->addAction("toggleDeafen", toggleDeafenAction);
|
||||
KGlobalAccel::setGlobalShortcut(toggleDeafenAction, QList<QKeySequence>{});
|
||||
|
||||
m_shortcutsDialog = new KShortcutsDialog(KShortcutsEditor::GlobalAction);
|
||||
m_shortcutsDialog->addCollection(m_actionCollection);
|
||||
#endif
|
||||
#endif
|
||||
}
|
||||
|
||||
void UserScript::setupStreamDialog() {
|
||||
m_streamDialog = new StreamDialog(MainWindow::instance());
|
||||
connect(m_streamDialog, &StreamDialog::requestedStreamStart, this,
|
||||
&UserScript::startStream);
|
||||
}
|
||||
|
||||
void UserScript::setupVirtmic() {
|
||||
m_virtmicProcess.setProcessChannelMode(QProcess::ForwardedChannels);
|
||||
}
|
||||
|
||||
bool UserScript::isVirtmicRunning() {
|
||||
return m_virtmicProcess.state() != QProcess::NotRunning;
|
||||
}
|
||||
|
||||
QString UserScript::version() { return QApplication::applicationVersion(); }
|
||||
|
||||
QVariant UserScript::getPref(QString name, QVariant fallback) {
|
||||
return MainWindow::instance()->settings()->value(name, fallback);
|
||||
}
|
||||
|
||||
bool UserScript::getBoolPref(QString name, bool fallback) {
|
||||
return getPref(name, fallback).toBool();
|
||||
}
|
||||
|
||||
void UserScript::setPref(QString name, QVariant value) {
|
||||
return MainWindow::instance()->settings()->setValue(name, value);
|
||||
}
|
||||
|
||||
void UserScript::setTrayIcon(bool value) {
|
||||
setPref("trayIcon", value);
|
||||
MainWindow::instance()->setTrayIcon(value);
|
||||
}
|
||||
|
||||
void UserScript::log(QString message) {
|
||||
qDebug(userscriptLog) << message.toUtf8().constData();
|
||||
}
|
||||
|
||||
void UserScript::showShortcutsDialog() {
|
||||
#ifdef KXMLGUI
|
||||
#ifdef KGLOBALACCEL
|
||||
m_shortcutsDialog->show();
|
||||
#else
|
||||
QMessageBox::information(MainWindow::instance(), "discord-screenaudio",
|
||||
"Keybinds are not supported on this platform "
|
||||
"(KGlobalAccel is not available).",
|
||||
QMessageBox::Ok);
|
||||
#endif
|
||||
#else
|
||||
QMessageBox::information(MainWindow::instance(), "discord-screenaudio",
|
||||
"Keybinds are not supported on this platform "
|
||||
"(KXmlGui and KGlobalAccel are not available).",
|
||||
QMessageBox::Ok);
|
||||
#endif
|
||||
}
|
||||
|
||||
void UserScript::showHelpMenu() {
|
||||
#ifdef KXMLGUI
|
||||
m_helpMenu->aboutApplication();
|
||||
#endif
|
||||
}
|
||||
|
||||
void UserScript::stopVirtmic() {
|
||||
if (m_virtmicProcess.state() == QProcess::Running) {
|
||||
qDebug(virtmicLog) << "Stopping Virtmic";
|
||||
m_virtmicProcess.kill();
|
||||
m_virtmicProcess.waitForFinished();
|
||||
}
|
||||
}
|
||||
|
||||
void UserScript::startVirtmic(QString target) {
|
||||
qDebug(virtmicLog) << "Starting Virtmic with target" << target;
|
||||
m_virtmicProcess.start(QApplication::arguments()[0], {"--virtmic", target});
|
||||
}
|
||||
|
||||
void UserScript::startStream(bool video, bool audio, int width, int height,
|
||||
int frameRate, QString target) {
|
||||
stopVirtmic();
|
||||
startVirtmic(audio ? target : "[None]");
|
||||
// Wait a bit for the virtmic to start
|
||||
QTimer::singleShot(
|
||||
200, [=]() { emit streamStarted(video, width, height, frameRate); });
|
||||
}
|
||||
|
||||
void UserScript::showStreamDialog() {
|
||||
if (m_streamDialog->isHidden())
|
||||
m_streamDialog->setHidden(false);
|
||||
else
|
||||
m_streamDialog->activateWindow();
|
||||
m_streamDialog->updateTargets();
|
||||
}
|
||||
|
||||
void UserScript::showThemeDialog() {
|
||||
auto url = QInputDialog::getText(MainWindow::instance(), "Theme Installation",
|
||||
"Please enter the URL of the Theme");
|
||||
if (url != "")
|
||||
emit shouldInstallUserStyles(url);
|
||||
}
|
||||
|
||||
void UserScript::installUserStyles(QString url) {
|
||||
emit shouldInstallUserStyles(url);
|
||||
}
|
||||
83
src/userscript.h
Normal file
83
src/userscript.h
Normal file
@@ -0,0 +1,83 @@
|
||||
#pragma once
|
||||
|
||||
#include "streamdialog.h"
|
||||
|
||||
#include <QObject>
|
||||
#include <QProcess>
|
||||
|
||||
#ifdef KXMLGUI
|
||||
#include <KAboutData>
|
||||
#include <KHelpMenu>
|
||||
#include <KShortcutsDialog>
|
||||
#include <KXmlGuiWindow>
|
||||
#include <QAction>
|
||||
|
||||
#ifdef KGLOBALACCEL
|
||||
#include <KGlobalAccel>
|
||||
#endif
|
||||
|
||||
#endif
|
||||
|
||||
#ifdef KNOTIFICATIONS
|
||||
#include <KNotification>
|
||||
#endif
|
||||
|
||||
class UserScript : public QObject {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
UserScript();
|
||||
bool isVirtmicRunning();
|
||||
Q_PROPERTY(QString version READ version CONSTANT);
|
||||
Q_PROPERTY(bool kxmlgui MEMBER m_kxmlgui CONSTANT);
|
||||
Q_PROPERTY(bool kglobalaccel MEMBER m_kglobalaccel CONSTANT);
|
||||
Q_PROPERTY(QString userstyles MEMBER m_userstyles NOTIFY userstylesChanged);
|
||||
Q_PROPERTY(QString loadingMessage MEMBER m_loadingMessage NOTIFY
|
||||
loadingMessageChanged);
|
||||
|
||||
private:
|
||||
QProcess m_virtmicProcess;
|
||||
StreamDialog *m_streamDialog;
|
||||
bool m_kxmlgui = false;
|
||||
bool m_kglobalaccel = false;
|
||||
QString m_userstyles;
|
||||
QString m_loadingMessage;
|
||||
#ifdef KXMLGUI
|
||||
KHelpMenu *m_helpMenu;
|
||||
#ifdef KGLOBALACCEL
|
||||
KActionCollection *m_actionCollection;
|
||||
KShortcutsDialog *m_shortcutsDialog;
|
||||
#endif
|
||||
#endif
|
||||
void setupHelpMenu();
|
||||
void setupShortcutsDialog();
|
||||
void setupStreamDialog();
|
||||
void setupVirtmic();
|
||||
|
||||
Q_SIGNALS:
|
||||
void muteToggled();
|
||||
void deafenToggled();
|
||||
void streamStarted(bool video, int width, int height, int frameRate);
|
||||
void userstylesChanged();
|
||||
void loadingMessageChanged(QString message);
|
||||
void shouldInstallUserStyles(QString url);
|
||||
|
||||
public Q_SLOTS:
|
||||
void log(QString message);
|
||||
QString version();
|
||||
QVariant getPref(QString name, QVariant fallback);
|
||||
bool getBoolPref(QString name, bool fallback);
|
||||
void setPref(QString name, QVariant value);
|
||||
void setTrayIcon(bool value);
|
||||
void showShortcutsDialog();
|
||||
void showHelpMenu();
|
||||
void showStreamDialog();
|
||||
void stopVirtmic();
|
||||
void startVirtmic(QString target);
|
||||
void showThemeDialog();
|
||||
void installUserStyles(QString url);
|
||||
|
||||
private Q_SLOTS:
|
||||
void startStream(bool video, bool audio, int width, int height, int frameRate,
|
||||
QString target);
|
||||
};
|
||||
321
src/virtmic.cpp
321
src/virtmic.cpp
@@ -1,175 +1,174 @@
|
||||
#include "virtmic.h"
|
||||
#include "log.h"
|
||||
|
||||
#include <rohrkabel/loop/main.hpp>
|
||||
#include <rohrkabel/registry/registry.hpp>
|
||||
QThread virtmicThread;
|
||||
std::unique_ptr<pipewire::sender<Virtmic::set_target, Virtmic::terminate>>
|
||||
senderr;
|
||||
std::unique_ptr<cr::receiver<Virtmic::new_targets>> receiverr;
|
||||
|
||||
namespace Virtmic {
|
||||
|
||||
const QStringList EXCLUDE_TARGETS{"Chromium input", "discord-screenaudio"};
|
||||
|
||||
QVector<QString> getTargets() {
|
||||
auto main_loop = pipewire::main_loop();
|
||||
auto context = pipewire::context(main_loop);
|
||||
auto core = pipewire::core(context);
|
||||
auto reg = pipewire::registry(core);
|
||||
|
||||
QVector<QString> targets;
|
||||
|
||||
auto reg_listener = reg.listen<pipewire::registry_listener>();
|
||||
reg_listener.on<pipewire::registry_event::global>(
|
||||
[&](const pipewire::global &global) {
|
||||
if (global.type == pipewire::node::type) {
|
||||
auto node = reg.bind<pipewire::node>(global.id);
|
||||
auto info = node.info();
|
||||
auto name = QString::fromStdString(info.props["application.name"]);
|
||||
|
||||
if (name != "" && !EXCLUDE_TARGETS.contains(name) &&
|
||||
!targets.contains(name)) {
|
||||
targets.append(name);
|
||||
}
|
||||
}
|
||||
});
|
||||
core.update();
|
||||
|
||||
return targets;
|
||||
void Virtmic::instance() {
|
||||
if (!virtmicThread.isRunning()) {
|
||||
auto [main_sender, main_receiver] = cr::channel<new_targets>();
|
||||
auto [pw_sender, pw_receiver] = pipewire::channel<set_target, terminate>();
|
||||
Virtmic *virtmic =
|
||||
new Virtmic(std::move(pw_receiver), std::move(main_sender));
|
||||
virtmic->moveToThread(&virtmicThread);
|
||||
virtmicThread.start();
|
||||
QMetaObject::invokeMethod(virtmic, "run");
|
||||
receiverr = std::make_unique<cr::receiver<Virtmic::new_targets>>(
|
||||
std::move(main_receiver));
|
||||
senderr = std::make_unique<
|
||||
pipewire::sender<Virtmic::set_target, Virtmic::terminate>>(
|
||||
std::move(pw_sender));
|
||||
}
|
||||
}
|
||||
|
||||
void start(QString _target) {
|
||||
std::map<std::uint32_t, pipewire::port> ports;
|
||||
std::unique_ptr<pipewire::port> virt_fl, virt_fr;
|
||||
void Virtmic::setTarget(QString target) {
|
||||
senderr.get()->send<Virtmic::set_target>({target});
|
||||
}
|
||||
|
||||
std::map<std::uint32_t, pipewire::node_info> nodes;
|
||||
std::map<std::uint32_t, pipewire::link_factory> links;
|
||||
void Virtmic::getTargets() { senderr.get()->send<Virtmic::get_targets>(); }
|
||||
|
||||
auto main_loop = pipewire::main_loop();
|
||||
auto context = pipewire::context(main_loop);
|
||||
auto core = pipewire::core(context);
|
||||
auto reg = pipewire::registry(core);
|
||||
|
||||
auto link = [&](const std::string &target, pipewire::core &core) {
|
||||
for (const auto &[port_id, port] : ports) {
|
||||
if (!virt_fl || !virt_fr)
|
||||
continue;
|
||||
|
||||
if (links.count(port_id))
|
||||
continue;
|
||||
|
||||
if (port.info().direction == pipewire::port_direction::input)
|
||||
continue;
|
||||
|
||||
if (!port.info().props.count("node.id"))
|
||||
continue;
|
||||
|
||||
auto parent_id = std::stoul(port.info().props["node.id"]);
|
||||
|
||||
if (!nodes.count(parent_id))
|
||||
continue;
|
||||
|
||||
auto &parent = nodes.at(parent_id);
|
||||
auto name = parent.props["application.name"];
|
||||
|
||||
if (name == target ||
|
||||
(target == "[All Desktop Audio]" &&
|
||||
!EXCLUDE_TARGETS.contains(QString::fromStdString(name)))) {
|
||||
auto fl = port.info().props["audio.channel"] == "FL";
|
||||
links.emplace(
|
||||
port_id,
|
||||
core.create<pipewire::link_factory>(
|
||||
{fl ? virt_fl->info().id : virt_fr->info().id, port_id}));
|
||||
qDebug(virtmicLog) << QString("Link: %1:%2 -> %3")
|
||||
.arg(QString::fromStdString(
|
||||
parent.props["application.name"]))
|
||||
.arg(port_id)
|
||||
.arg(fl ? virt_fl->info().id
|
||||
: virt_fr->info().id)
|
||||
.toUtf8()
|
||||
.data();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
std::string target = _target.toLatin1().toStdString();
|
||||
|
||||
auto virtual_mic = core.create("adapter",
|
||||
{{"node.name", "discord-screenaudio-virtmic"},
|
||||
{"media.class", "Audio/Source/Virtual"},
|
||||
{"factory.name", "support.null-audio-sink"},
|
||||
{"audio.channels", "2"},
|
||||
{"audio.position", "FL,FR"}},
|
||||
pipewire::node::type, pipewire::node::version,
|
||||
pipewire::update_strategy::none);
|
||||
|
||||
if (target == "[None]") {
|
||||
while (true) {
|
||||
main_loop.run();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
auto reg_events = reg.listen<pipewire::registry_listener>();
|
||||
reg_events.on<pipewire::registry_event::global>(
|
||||
[&](const pipewire::global &global) {
|
||||
if (global.type == pipewire::node::type) {
|
||||
auto node = reg.bind<pipewire::node>(global.id);
|
||||
if (!node.info().props.count("application.name"))
|
||||
return;
|
||||
qDebug(virtmicLog) << QString("Added: %1")
|
||||
.arg(QString::fromStdString(
|
||||
node.info().props["application.name"]))
|
||||
.toUtf8()
|
||||
.data();
|
||||
|
||||
if (!nodes.count(global.id)) {
|
||||
nodes.emplace(global.id, node.info());
|
||||
link(target, core);
|
||||
}
|
||||
}
|
||||
if (global.type == pipewire::port::type) {
|
||||
auto port = reg.bind<pipewire::port>(global.id);
|
||||
auto info = port.info();
|
||||
|
||||
if (info.props.count("node.id")) {
|
||||
auto node_id = std::stoul(info.props["node.id"]);
|
||||
|
||||
if (node_id == virtual_mic.id() &&
|
||||
info.direction == pipewire::port_direction::input) {
|
||||
if (info.props["audio.channel"] == "FL") {
|
||||
virt_fl = std::make_unique<pipewire::port>(std::move(port));
|
||||
} else {
|
||||
virt_fr = std::make_unique<pipewire::port>(std::move(port));
|
||||
}
|
||||
} else {
|
||||
ports.emplace(global.id, std::move(port));
|
||||
}
|
||||
|
||||
link(target, core);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
reg_events.on<pipewire::registry_event::global_removed>(
|
||||
[&](const std::uint32_t id) {
|
||||
if (nodes.count(id)) {
|
||||
auto info = nodes.at(id);
|
||||
qDebug(virtmicLog) << QString("Removed: %1")
|
||||
.arg(QString::fromStdString(
|
||||
info.props["application.name"].data()))
|
||||
.toUtf8()
|
||||
.data();
|
||||
nodes.erase(id);
|
||||
}
|
||||
if (ports.count(id)) {
|
||||
ports.erase(id);
|
||||
}
|
||||
if (links.count(id)) {
|
||||
links.erase(id);
|
||||
}
|
||||
});
|
||||
Virtmic::Virtmic(pipewire::receiver<set_target, terminate> receiver,
|
||||
cr::sender<new_targets> sender) {
|
||||
m_receiver = std::make_unique<pipewire::receiver<set_target, terminate>>(
|
||||
std::move(receiver));
|
||||
m_sender = std::make_unique<cr::sender<new_targets>>(std::move(sender));
|
||||
virtual_mic = std::make_unique<pipewire::proxy>(
|
||||
std::move(*core.create("adapter",
|
||||
{{"node.name", "discord-screenaudio-virtmic"},
|
||||
{"media.class", "Audio/Source/Virtual"},
|
||||
{"factory.name", "support.null-audio-sink"},
|
||||
{"audio.channels", "2"},
|
||||
{"audio.position", "FL,FR"}},
|
||||
pipewire::node::type, pipewire::node::version,
|
||||
pipewire::update_strategy::none)
|
||||
.get()));
|
||||
metadata_listener.on<pipewire::registry_event::global>(
|
||||
[&](const auto &global) { globalEvent(global); });
|
||||
metadata_listener.on<pipewire::registry_event::global_removed>(
|
||||
[&](const std::uint32_t id) { globalRemovedEvent(id); });
|
||||
}
|
||||
|
||||
void Virtmic::run() {
|
||||
while (true) {
|
||||
main_loop.run();
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace Virtmic
|
||||
void Virtmic::link() {
|
||||
for (const auto &[port_id, port] : ports) {
|
||||
auto port_info = port.info();
|
||||
if (!virt_fl || !virt_fr)
|
||||
continue;
|
||||
|
||||
if (links.count(port_id))
|
||||
continue;
|
||||
|
||||
if (port_info.direction == pipewire::port_direction::input)
|
||||
continue;
|
||||
|
||||
if (!port_info.props.count("node.id"))
|
||||
continue;
|
||||
|
||||
auto parent_id = std::stoul(port_info.props["node.id"]);
|
||||
|
||||
if (!nodes.count(parent_id))
|
||||
continue;
|
||||
|
||||
auto &parent = nodes[parent_id];
|
||||
QString name;
|
||||
if (parent.props.count("application.name") &&
|
||||
parent.props["application.name"] != "")
|
||||
name = QString::fromStdString(parent.props["application.name"]);
|
||||
else
|
||||
name = QString::fromStdString(parent.props["application.process.binary"]);
|
||||
|
||||
if (name == target ||
|
||||
(target == "[All Desktop Audio]" && !EXCLUDE_TARGETS.contains(name))) {
|
||||
auto fl = port_info.props["audio.channel"] == "FL";
|
||||
links.emplace(port_id,
|
||||
*core.create_simple<pipewire::link>(fl ? virt_fl->info().id
|
||||
: virt_fr->info().id,
|
||||
port_id)
|
||||
.get());
|
||||
qDebug(virtmicLog) << QString("Link: %1:%2 -> %3")
|
||||
.arg(name)
|
||||
.arg(port_id)
|
||||
.arg(fl ? virt_fl->info().id
|
||||
: virt_fr->info().id)
|
||||
.toUtf8()
|
||||
.data();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Virtmic::unlink() { links.clear(); }
|
||||
|
||||
void Virtmic::globalEvent(const pipewire::global &global) {
|
||||
if (global.type == pipewire::node::type) {
|
||||
auto node = *reg.bind<pipewire::node>(global.id).get();
|
||||
auto info = node.info();
|
||||
std::string name;
|
||||
if (info.props.count("application.name") &&
|
||||
info.props["application.name"] != "")
|
||||
name = info.props["application.name"];
|
||||
else if (info.props.count("application.process.binary")) {
|
||||
name = info.props["application.process.binary"];
|
||||
} else
|
||||
return;
|
||||
qDebug(virtmicLog) << QString("Added: %1")
|
||||
.arg(QString::fromStdString(name))
|
||||
.toUtf8()
|
||||
.data();
|
||||
|
||||
if (!nodes.count(global.id)) {
|
||||
nodes.emplace(global.id, node.info());
|
||||
link();
|
||||
}
|
||||
}
|
||||
if (global.type == pipewire::port::type) {
|
||||
auto port = *reg.bind<pipewire::port>(global.id).get();
|
||||
auto info = port.info();
|
||||
|
||||
if (info.props.count("node.id")) {
|
||||
auto node_id = std::stoul(info.props["node.id"]);
|
||||
|
||||
if (node_id == virtual_mic.get()->id() &&
|
||||
info.direction == pipewire::port_direction::input) {
|
||||
if (info.props["audio.channel"] == "FL") {
|
||||
virt_fl = std::make_unique<pipewire::port>(std::move(port));
|
||||
} else {
|
||||
virt_fr = std::make_unique<pipewire::port>(std::move(port));
|
||||
}
|
||||
} else {
|
||||
ports.emplace(global.id, std::move(port));
|
||||
}
|
||||
|
||||
link();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Virtmic::globalRemovedEvent(const std::uint32_t id) {
|
||||
if (nodes.count(id)) {
|
||||
auto info = nodes.at(id);
|
||||
std::string name;
|
||||
if (info.props.count("application.name") &&
|
||||
info.props["application.name"] != "")
|
||||
name = info.props["application.name"];
|
||||
else
|
||||
name = info.props["application.process.binary"];
|
||||
qDebug(virtmicLog) << QString("Removed: %1")
|
||||
.arg(QString::fromStdString(name))
|
||||
.toUtf8()
|
||||
.data();
|
||||
nodes.erase(id);
|
||||
}
|
||||
if (ports.count(id)) {
|
||||
ports.erase(id);
|
||||
}
|
||||
if (links.count(id)) {
|
||||
links.erase(id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,63 @@
|
||||
#pragma once
|
||||
|
||||
#include <QString>
|
||||
#include <QVector>
|
||||
#include <cr/channel.hpp>
|
||||
#include <iostream>
|
||||
#include <rohrkabel/channel/channel.hpp>
|
||||
#include <rohrkabel/main_loop.hpp>
|
||||
#include <rohrkabel/registry/registry.hpp>
|
||||
|
||||
namespace Virtmic {
|
||||
#include <QMap>
|
||||
#include <QScopedPointer>
|
||||
#include <QStringList>
|
||||
#include <QThread>
|
||||
|
||||
QVector<QString> getTargets();
|
||||
void start(QString _target);
|
||||
class Virtmic : public QObject {
|
||||
Q_OBJECT
|
||||
public:
|
||||
static void setTarget(QString target);
|
||||
static void getTargets();
|
||||
|
||||
} // namespace Virtmic
|
||||
public:
|
||||
struct set_target {
|
||||
QString name;
|
||||
};
|
||||
struct get_targets {};
|
||||
struct terminate {};
|
||||
struct new_targets {
|
||||
QStringList targets;
|
||||
};
|
||||
|
||||
protected:
|
||||
static void instance();
|
||||
|
||||
protected:
|
||||
Virtmic(pipewire::receiver<set_target, terminate> receiver,
|
||||
cr::sender<new_targets> sender);
|
||||
void run();
|
||||
|
||||
private:
|
||||
std::unique_ptr<pipewire::receiver<set_target, terminate>> m_receiver;
|
||||
std::unique_ptr<cr::sender<new_targets>> m_sender;
|
||||
|
||||
const QStringList EXCLUDE_TARGETS{"Chromium input", "discord-screenaudio"};
|
||||
QString target;
|
||||
|
||||
pipewire::main_loop main_loop = pipewire::main_loop();
|
||||
pipewire::context context = pipewire::context(main_loop);
|
||||
pipewire::core core = pipewire::core(context);
|
||||
pipewire::registry reg = pipewire::registry(core);
|
||||
|
||||
pipewire::registry_listener metadata_listener =
|
||||
reg.listen<pipewire::registry_listener>();
|
||||
std::unique_ptr<pipewire::proxy> virtual_mic;
|
||||
|
||||
std::map<uint32_t, pipewire::port> ports;
|
||||
std::unique_ptr<pipewire::port> virt_fl, virt_fr;
|
||||
std::map<uint32_t, pipewire::node_info> nodes;
|
||||
std::map<uint32_t, pipewire::link> links;
|
||||
|
||||
void link();
|
||||
void unlink();
|
||||
void globalEvent(const pipewire::global &global);
|
||||
void globalRemovedEvent(const std::uint32_t id);
|
||||
};
|
||||
|
||||
1
submodules/channel
Submodule
1
submodules/channel
Submodule
Submodule submodules/channel added at 6977815409
Submodule submodules/rohrkabel updated: 04bfb921c4...8a7705be07
Reference in New Issue
Block a user