Compare commits

...

50 Commits

Author SHA1 Message Date
Malte Jürgens
d6641a7a6e Merge pull request #88 from maltejur/tray
Implement Tray Icon
2023-01-14 14:51:46 +00:00
Malte Jürgens
b836be6530 Implement Tray Icon 2023-01-13 23:33:47 +01:00
Malte Jürgens
bfb0714b13 fix keybinds query selector 2023-01-09 22:20:11 +01:00
Malte Jürgens
374b854261 Update README 2023-01-09 22:19:45 +01:00
Malte Jürgens
1f6105f76b allow for building with qt6 2022-12-27 19:00:10 +01:00
Malte Jürgens
3071159332 Update screenshot 2022-12-11 17:55:04 +00:00
Malte Jürgens
f2de080e1b update labels 2022-11-22 16:39:58 +01:00
Malte Jürgens
f4a60f281d reworked stream dialog 2022-11-22 16:35:47 +01:00
Malte Jürgens
d693535d53 Revert "add clipboard permission"
This reverts commit b0a8815bb8.
2022-11-09 22:13:34 +01:00
Malte Jürgens
e41af697f7 fix error 2022-11-05 00:08:25 +01:00
Malte Jürgens
b0a8815bb8 add clipboard permission 2022-11-04 23:20:46 +01:00
Malte Jürgens
150fd4364e add --notify-send option 2022-11-04 23:16:49 +01:00
Malte Jürgens
a0a2924796 remove Vencord folder 2022-11-04 20:45:55 +00:00
Malte Jürgens
3c48621427 add developer field to metainfo 2022-10-28 21:57:32 +02:00
Malte Jürgens
9faabe1f3e Merge pull request #66 from kherchel/fix/keybinds
Fix mute and deafen keybinds disconnecting from the call
2022-10-25 18:18:08 +00:00
Kacper Herchel
68473d04d8 Fix mute and deafen keybinds disconnecting from the call instead of performing their appropriate action 2022-10-25 17:01:10 +02:00
Malte Jürgens
7c8f72b8d8 fix 2022-10-11 22:17:53 +02:00
Malte Jürgens
27cdd9f9a5 code fixes 2022-10-11 19:48:57 +02:00
Malte Jürgens
d184823ee9 Enable smooth scrolling 2022-10-10 21:54:26 +02:00
Malte Jürgens
dd2beed4eb Implement Keybinds (#57) 2022-10-10 19:50:26 +00:00
Malte Jürgens
3740553aba Merge pull request #54 from D3SOX/improve-desktop-entry
Improve desktop entry
2022-09-29 16:47:49 +00:00
Nico
555f5c62f5 feat(desktop-entry): add categories and comment 2022-09-29 18:45:08 +02:00
Malte Jürgens
af3c62c263 update screenshot 2022-09-25 11:32:23 +02:00
Malte Jürgens
c4f6c33f9d display correct steam resolution 2022-09-21 17:32:41 +02:00
Malte Jürgens
758ffa8cb5 Update README (#48) 2022-09-18 12:04:04 +02:00
Malte Jürgens
753da8b1d3 automatically detect if kf5notifications is installed 2022-08-29 14:23:00 +02:00
Vortex Acherontic
5f37ab314f Changed location of openSUSE package (#34) 2022-08-21 07:21:18 +02:00
Malte Jürgens
c7bff3f428 add refresh button to stream dialog 2022-08-04 15:51:03 +02:00
Malte Jürgens
34e23f7187 cmake: use version from text file without suffix 2022-08-04 15:47:04 +02:00
Malte Jürgens
c325dd6798 allow sharing all desktop audio 2022-08-04 15:27:39 +02:00
Malte Jürgens
b42fde85d1 only allow to share application audio 2022-08-04 14:57:09 +02:00
Malte Jürgens
cc379ada85 fix metainfo again 2022-08-04 11:10:29 +02:00
Malte Jürgens
fdb6145bbd improve logging 2022-08-02 12:41:32 +02:00
Malte Jürgens
8bd4d1896a poll for enumerateDevices() to get available 2022-08-02 11:56:08 +02:00
Malte Jürgens
1a2d09fe3d remove useless main.h again 2022-08-02 11:46:37 +02:00
Malte Jürgens
f01c941c90 add timeout to getAudioDevice 2022-08-01 22:26:44 +02:00
Malte Jürgens
95d391e869 fix remote debugging 2022-08-01 19:59:01 +02:00
Malte Jürgens
af107168db update metainfo description 2022-07-31 21:12:53 +02:00
Malte Jürgens
e7e6c5d82b update readme 2022-07-30 21:14:56 +02:00
Malte Jürgens
ee0c66762d add --enable-features=WebRTCPipeWireCapturer
Co-authored-by: edisionnano <samantas5855@gmail.com>
2022-07-30 21:09:30 +02:00
LukeVader
c3483a42db disclaimer (#23)
* disclaimer

add a disclaimer that no responsibility is on the developers in the case of any repercussions of breaking discord TOS

* Update disclaimer

Co-authored-by: Malte Jürgens <maltejur@dismail.de>

Co-authored-by: Malte Jürgens <maltejur@dismail.de>
2022-07-29 21:01:11 +00:00
Malte Jürgens
d68797cebb make KF5Notifications optional 2022-07-29 16:31:04 +02:00
Malte Jürgens
ef221d4574 implement better notifications 2022-07-29 16:08:19 +02:00
Malte Jürgens
3fa2e3fa3c update installation instructions 2022-07-29 14:41:00 +02:00
Malte Jürgens
bda1bc0df6 fixed invisible stream dialog 2022-07-29 01:03:25 +02:00
Malte Jürgens
cb0f084d42 fix metainfo 2022-07-29 01:02:20 +02:00
Malte Jürgens
674ac146f2 use icon for window 2022-07-28 18:36:09 +02:00
Malte Jürgens
1ab00ee774 include timestamp in metainfo 2022-07-28 18:23:26 +02:00
Malte Jürgens
38bcc30ed4 display version number correctly 2022-07-28 18:13:18 +02:00
Malte Jürgens
204439af94 implement automatic versioning 2022-07-28 17:34:54 +02:00
20 changed files with 965 additions and 175 deletions

1
.gitignore vendored
View File

@@ -1 +1,2 @@
/build
.vscode

View File

@@ -1,17 +1,40 @@
cmake_minimum_required(VERSION 3.0)
project(discord-screenaudio)
include(version.cmake)
determine_version("${CMAKE_CURRENT_SOURCE_DIR}" DISCORD_SCEENAUDIO)
add_definitions( -DDISCORD_SCEENAUDIO_VERSION_FULL="${DISCORD_SCEENAUDIO_VERSION_FULL}" )
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_INCLUDE_CURRENT_DIR ON)
set(CMAKE_AUTOMOC ON)
set(CMAKE_AUTORCC ON)
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(KF5XmlGui)
if(KF5XmlGui_FOUND)
add_definitions( -DKXMLGUI )
endif()
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
src/main.cpp
@@ -19,6 +42,7 @@ set(discord-screenaudio_SRC
src/virtmic.cpp
src/discordpage.cpp
src/streamdialog.cpp
src/log.cpp
resources.qrc
)
@@ -45,9 +69,21 @@ add_subdirectory(submodules/rohrkabel)
add_executable(discord-screenaudio ${discord-screenaudio_SRC})
target_link_libraries(discord-screenaudio Qt5::Widgets Qt5::WebEngineWidgets rohrkabel)
target_link_libraries(discord-screenaudio Qt::Widgets Qt::WebEngineWidgets rohrkabel)
if(KF5Notifications_FOUND)
target_link_libraries(discord-screenaudio KF5::Notifications)
install(FILES assets/discord-screenaudio.notifyrc DESTINATION ${CMAKE_INSTALL_PREFIX}/share/knotifications5)
endif()
if(KF5XmlGui_FOUND)
target_link_libraries(discord-screenaudio KF5::XmlGui)
endif()
if(KF5GlobalAccel_FOUND)
target_link_libraries(discord-screenaudio KF5::GlobalAccel)
endif()
install(TARGETS discord-screenaudio DESTINATION bin)
install(PROGRAMS assets/de.shorsh.discord-screenaudio.png DESTINATION ${CMAKE_INSTALL_PREFIX}/share/icons/hicolor/256x256/apps)
install(FILES assets/de.shorsh.discord-screenaudio.png DESTINATION ${CMAKE_INSTALL_PREFIX}/share/icons/hicolor/256x256/apps)
install(PROGRAMS assets/de.shorsh.discord-screenaudio.desktop DESTINATION ${CMAKE_INSTALL_PREFIX}/share/applications)
install(PROGRAMS assets/de.shorsh.discord-screenaudio.metainfo.xml DESTINATION ${CMAKE_INSTALL_PREFIX}/share/metainfo)
configure_file(assets/de.shorsh.discord-screenaudio.metainfo.xml.in de.shorsh.discord-screenaudio.metainfo.xml)
install(FILES ${CMAKE_BINARY_DIR}/de.shorsh.discord-screenaudio.metainfo.xml DESTINATION ${CMAKE_INSTALL_PREFIX}/share/metainfo)

View File

@@ -1,13 +1,17 @@
# discord-screenaudio
A very WIP custom discord client that supports streaming with audio on Linux,
made possible by the
A custom discord client that supports streaming with audio on Linux, made
possible by the
[great work](https://github.com/edisionnano/Screenshare-with-audio-on-Discord-with-Linux)
of [@edisionnano](https://github.com/edisionnano) and the
[Rohrkabel library](https://github.com/Soundux/rohrkabel) by
[@Curve](https://github.com/Curve).
![Screenshot_20220718_194357](https://user-images.githubusercontent.com/48161361/179571245-11ea05f3-fb5e-4aef-9132-2736e122ef04.png)
Unlike a lot of other solutions, the audio here is directly fed into the
screenshare and not passed to the user microphone
([see explanation](#how-it-works)).
![Screenshot_20221211_185028](https://user-images.githubusercontent.com/48161361/206920213-58a8091a-d8f9-4bb7-ae3d-3f8581b84d24.png)
The purpose of this project is **not** to provide an alternative to the original
Discord client. Rather, it should be used in addition to the original client in
@@ -15,31 +19,48 @@ case you want to stream something, maybe used with a second account. For
anything else, this client has way too many things that work less well than in
the original client.
Technically this could be against
[Discord's TOS](https://discord.com/terms#software-in-discord%E2%80%99s-services),
so be warned. Discord probably won't ban you for using this, but if they do I
told you and it's not my fault.
## Known Issues
- Only works with **PipeWire**
- Only works on **X11**
- Can only share primary screen (no other screens or specific applications) (see
- Can only share primary screen on X11 (no other screens or specific
applications) (see
[#1](https://github.com/maltejur/discord-screenaudio/issues/1))
## Installation
If you are using Arch, you can build and install
[from the AUR](https://aur.archlinux.org/packages/discord-screenaudio). If you
aren't, you will have to [build it yourself](#building-from-source).
You have multiple options:
- Install the Flatpak (recommended):
`flatpak install de.shorsh.discord-screenaudio`
- If you are on Arch, you can build and install
[`discord-screenaudio`](https://aur.archlinux.org/packages/discord-screenaudio)
from the AUR
- If you are on openSUSE, you can use the
[Open Build Service package](https://software.opensuse.org//download.html?project=games%3Atools&package=discord-screenaudio)
by [@VortexAcherontic](https://github.com/VortexAcherontic)
- You can [build it yourself](#building-from-source)
## Building from Source
### 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)
- Git
- _Kf5Notifications (optional, for better notifications)_
- _KXMLGui and KGlobalAccel (optional, for keybinds)_
On Debian:
`apt install -y build-essential cmake qtbase5-dev qtwebengine5-dev pkg-config libpipewire-0.3-dev git`
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

View File

@@ -2,5 +2,7 @@
Type=Application
Name=discord-screenaudio
Exec=discord-screenaudio
Comment=A custom discord client that supports streaming with audio on Linux.
Icon=de.shorsh.discord-screenaudio
Categories=Network;InstantMessaging;
Terminal=false

View File

@@ -1,38 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<component type="desktop-application">
<id>de.shorsh.discord-screenaudio</id>
<metadata_license>CC0-1.0</metadata_license>
<project_license>GPL-3.0+</project_license>
<name>discord-screenaudio</name>
<releases>
<release version="v1.0.0-rc.11" />
</releases>
<summary>
A very WIP custom discord client that supports streaming with audio on
Linux.
</summary>
<description>
<p>A very WIP custom discord client that supports streaming with audio on Linux, made possible by the great work of @edisionnano and the Rohrkabel library by @Curve.</p>
<p>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 case you want to stream something, maybe used with a second account. For anything else, this client has way too many things that work less well than in the original client.</p>
</description>
<launchable type="desktop-id">
discord-screenaudio.desktop
</launchable>
<screenshots>
<screenshot type="default">
<image>
https://user-images.githubusercontent.com/48161361/179571245-11ea05f3-fb5e-4aef-9132-2736e122ef04.png
</image>
</screenshot>
</screenshots>
<url type="homepage">https://github.com/maltejur/discord-screenaudio</url>
<provides>
<binary>discord-screenaudio</binary>
</provides>
</component>

View File

@@ -0,0 +1,51 @@
<?xml version="1.0" encoding="UTF-8"?>
<component type="desktop-application">
<id>de.shorsh.discord-screenaudio</id>
<metadata_license>CC0-1.0</metadata_license>
<project_license>GPL-3.0+</project_license>
<name>discord-screenaudio</name>
<developer_name>Malte Jürgens</developer_name>
<releases>
<release version="${DISCORD_SCEENAUDIO_VERSION_FULL}" timestamp="${TIMESTAMP}" />
</releases>
<summary>A custom discord client that supports streaming with audio on Linux</summary>
<description>
<p>A custom discord client that supports streaming with audio on Linux, made possible by the great work of @edisionnano and the Rohrkabel library by @Curve.</p>
<p>Unlike a lot of other solutions, the audio here is directly fed into the screenshare and not passed to the user microphone.</p>
<p>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 case you want to stream something, maybe used with a second account. For anything else, this client has way too many things that work less well than in the original client.</p>
<p>Technically this could be against Discord's TOS, so be warned. Discord probably won't ban you for using this, but if they do I told you and it's not my fault.</p>
<p>Known issues:</p>
<ul>
<li>Only works with PipeWire</li>
<li>Can only share primary screen on X11</li>
</ul>
</description>
<launchable type="desktop-id">
de.shorsh.discord-screenaudio.desktop
</launchable>
<screenshots>
<screenshot type="default">
<image>
https://user-images.githubusercontent.com/48161361/192137080-33466cf7-8c56-4373-90c6-01ea74b6fb83.png
</image>
</screenshot>
</screenshots>
<url type="homepage">https://github.com/maltejur/discord-screenaudio</url>
<provides>
<binary>discord-screenaudio</binary>
</provides>
<content_rating type="oars-1.0">
<content_attribute id="social-chat">intense</content_attribute>
<content_attribute id="social-info">intense</content_attribute>
<content_attribute id="social-audio">intense</content_attribute>
<content_attribute id="social-contacts">intense</content_attribute>
<content_attribute id="money-purchasing">mild</content_attribute>
</content_rating>
</component>

View File

@@ -0,0 +1,8 @@
[Global]
IconName=de.shorsh.discord-screenaudio
Name=discord-screenaudio
DesktopEntry=de.shorsh.discord-screenaudio.desktop
[Event/discordNotification]
Name=Discord Notification
Action=Popup

View File

@@ -3,16 +3,29 @@
navigator.mediaDevices.chromiumGetDisplayMedia =
navigator.mediaDevices.getDisplayMedia;
function sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
const getAudioDevice = async (nameOfAudioDevice) => {
await navigator.mediaDevices.getUserMedia({
audio: true,
});
let devices = await navigator.mediaDevices.enumerateDevices();
let audioDevice = devices.find(({ label }) => label === nameOfAudioDevice);
let audioDevice;
while (audioDevice === undefined) {
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`
);
await sleep(100);
}
console.log(`dsa: Found '${nameOfAudioDevice}'`);
return audioDevice;
};
function setGetDisplayMedia(overrideArgs = undefined) {
function setGetDisplayMedia(video = true, overrideArgs = undefined) {
const getDisplayMedia = async (...args) => {
var id;
try {
@@ -50,6 +63,7 @@ function setGetDisplayMedia(overrideArgs = undefined) {
: args || [{ video: true, audio: true }])
);
gdm.addTrack(track);
if (!video) for (const track of gdm.getVideoTracks()) track.enabled = false;
return gdm;
};
navigator.mediaDevices.getDisplayMedia = getDisplayMedia;
@@ -61,6 +75,52 @@ const clonedElements = [];
const hiddenElements = [];
let wasStreamActive = false;
function createButton(text, onClick) {
const button = document.createElement("button");
button.style.marginBottom = "20px";
button.classList =
"button-f2h6uQ lookFilled-yCfaCM colorBrand-I6CyqQ sizeSmall-wU2dO- grow-2sR_-F";
button.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-2jl9gK";
const label = document.createElement("label");
label.innerText = text;
label.className = "title-2dsDLn";
container.appendChild(label);
const svg = document.createElement("div");
container.appendChild(svg);
function setSvgDisabled() {
svg.innerHTML = `<div class="container-2nx-BQ default-colors" style="opacity: 1; background-color: rgb(114, 118, 125);"><svg class="slider-32CRPX" viewBox="0 0 28 20" preserveAspectRatio="xMinYMid meet" aria-hidden="true" style="left: -3px;"><rect fill="white" x="4" y="0" height="20" width="20" rx="10"></rect><svg viewBox="0 0 20 20" fill="none"><path fill="rgba(114, 118, 125, 1)" d="M5.13231 6.72963L6.7233 5.13864L14.855 13.2704L13.264 14.8614L5.13231 6.72963Z"></path><path fill="rgba(114, 118, 125, 1)" d="M13.2704 5.13864L14.8614 6.72963L6.72963 14.8614L5.13864 13.2704L13.2704 5.13864Z"></path></svg></svg></div>`;
}
function setSvgEnabled() {
svg.innerHTML = `<div class="container-2nx-BQ default-colors checked-25WXMf" style="opacity: 1; background-color: rgb(59, 165, 92);"><svg class="slider-32CRPX" viewBox="0 0 28 20" preserveAspectRatio="xMinYMid meet" aria-hidden="true" style="left: 12px;"><rect fill="white" x="4" y="0" height="20" width="20" rx="10"></rect><svg viewBox="0 0 20 20" fill="none"><path fill="rgba(59, 165, 92, 1)" d="M7.89561 14.8538L6.30462 13.2629L14.3099 5.25755L15.9009 6.84854L7.89561 14.8538Z"></path><path fill="rgba(59, 165, 92, 1)" d="M4.08643 11.0903L5.67742 9.49929L9.4485 13.2704L7.85751 14.8614L4.08643 11.0903Z"></path></svg></svg></div>`;
}
function updateSvg() {
if (enabled) setSvgEnabled();
else setSvgDisabled();
}
container.addEventListener("click", () => {
enabled = !enabled;
updateSvg();
onClick(enabled);
});
updateSvg();
return container;
}
setInterval(() => {
const streamActive =
document.getElementsByClassName("panel-2ZFCRb activityPanel-9icbyU")
@@ -98,8 +158,16 @@ setInterval(() => {
const initialDisplay = el.style.display;
window.discordScreenaudioStartStream = (width, height, frameRate) => {
setGetDisplayMedia({
window.discordScreenaudioStartStream = (
video,
width,
height,
frameRate
) => {
window.discordScreenaudioResolutionString = video
? `${height}p ${frameRate}FPS`
: "Audio Only";
setGetDisplayMedia(video, {
audio: true,
video: { width, height, frameRate },
});
@@ -121,11 +189,21 @@ setInterval(() => {
document.getElementsByClassName("dirscordScreenaudioAboutText").length == 0
) {
for (const el of document.getElementsByClassName("info-3pQQBb")) {
const aboutEl = document.createElement("div");
aboutEl.innerText = "discord-screenaudio v1.0.0-rc.11";
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);
}
}
@@ -133,6 +211,104 @@ setInterval(() => {
// Remove stream settings if stream is active
document.getElementById("manage-streams-change-windows")?.remove();
document.querySelector(`[aria-label="Stream Settings"]`)?.remove();
// Add event listener for keybind tab
if (
document
.getElementById("keybinds-tab")
?.getElementsByClassName(
"container-3jbRo5 info-1hMolH browserNotice-1u-Y5o"
).length
) {
const el = document
.getElementById("keybinds-tab")
.getElementsByClassName("children-1xdcWE")[0];
const div = document.createElement("div");
div.style.marginBottom = "50px";
div.appendChild(
createButton("Edit Global Keybinds", () => {
console.log("!discord-screenaudio-keybinds");
})
);
el.innerHTML = "";
el.appendChild(div);
}
const buttonContainer =
document.getElementsByClassName("container-YkUktl")[0];
if (!buttonContainer) {
console.log(
"dsa: Cannot locate Mute/Deafen/Settings button container, please report this on GitHub"
);
}
const muteBtn = buttonContainer
? buttonContainer.getElementsByClassName(
"button-12Fmur enabled-9OeuTA button-f2h6uQ lookBlank-21BCro colorBrand-I6CyqQ grow-2sR_-F"
)[0]
: null;
window.discordScreenaudioToggleMute = () => muteBtn && muteBtn.click();
const deafenBtn = buttonContainer
? buttonContainer.getElementsByClassName(
"button-12Fmur enabled-9OeuTA button-f2h6uQ lookBlank-21BCro colorBrand-I6CyqQ grow-2sR_-F"
)[1]
: null;
window.discordScreenaudioToggleDeafen = () => deafenBtn && deafenBtn.click();
if (window.discordScreenaudioResolutionString) {
for (const el of document.getElementsByClassName(
"qualityIndicator-39wQDy"
)) {
el.innerHTML = window.discordScreenaudioResolutionString;
}
}
const accountTab = document.getElementById("my-account-tab");
if (accountTab) {
const discordScreenaudioSettings = document.getElementById(
"discord-screenaudio-settings"
);
if (!discordScreenaudioSettings) {
const firstDivider = accountTab.getElementsByClassName(
"divider-_0um2u marginTop40-Q4o1tS"
)[0];
if (firstDivider) {
const section = document.createElement("div");
section.className = "marginTop40-Q4o1tS";
section.id = "discord-screenaudio-settings";
const title = document.createElement("h2");
title.className =
"h1-34Txb0 title-3hptVQ defaultColor-2cKwKo defaultMarginh1-EURXsm";
title.innerText = "discord-screenaudio";
section.appendChild(title);
section.appendChild(
createButton("Edit Global Keybinds", () => {
console.log("!discord-screenaudio-keybinds");
})
);
section.appendChild(
createSwitch(
"Move discord-screenaudio to the system tray instead of closing",
window.discordScreenaudioTrayEnabled,
(enabled) => {
console.log(`!discord-screenaudio-tray-${enabled}`);
}
)
);
const divider = document.createElement("div");
divider.className = "divider-_0um2u marginTop40-Q4o1tS";
firstDivider.after(section);
section.after(divider);
}
}
}
}, 500);
// Fix for broken discord notifications after restart

View File

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

View File

@@ -1,9 +1,26 @@
#include "discordpage.h"
#include "log.h"
#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 <QMessageBox>
#include <QNetworkReply>
#include <QTimer>
#include <QWebChannel>
#include <QWebEngineScript>
@@ -17,6 +34,11 @@ DiscordPage::DiscordPage(QWidget *parent) : QWebEnginePage(parent) {
connect(this, &QWebEnginePage::featurePermissionRequested, this,
&DiscordPage::featurePermissionRequested);
connect(this, &QWebEnginePage::loadStarted, [=]() {
runJavaScript(QString("window.discordScreenaudioVersion = '%1';")
.arg(QApplication::applicationVersion()));
});
settings()->setAttribute(QWebEngineSettings::ScreenCaptureEnabled, true);
settings()->setAttribute(QWebEngineSettings::JavascriptCanOpenWindows, true);
settings()->setAttribute(QWebEngineSettings::AllowRunningInsecureContent,
@@ -27,35 +49,98 @@ DiscordPage::DiscordPage(QWidget *parent) : QWebEnginePage(parent) {
settings()->setAttribute(QWebEngineSettings::PlaybackRequiresUserGesture,
false);
settings()->setAttribute(QWebEngineSettings::JavascriptCanOpenWindows, false);
settings()->setAttribute(QWebEngineSettings::ScrollAnimatorEnabled, true);
setUrl(QUrl("https://discord.com/app"));
injectScript(":/assets/userscript.js");
injectScriptFile("userscript.js", ":/assets/userscript.js");
injectScriptText("vars.js",
QString("window.discordScreenaudioVersion = '%1'; "
"window.discordScreenaudioTrayEnabled = %2;")
.arg(QApplication::applicationVersion())
.arg(MainWindow::instance()
->settings()
->value("trayIcon", false)
.toBool()));
#ifdef KXMLGUI
injectScriptText("xmlgui.js", "window.discordScreenaudioKXMLGUI = true;");
KAboutData aboutData(
"discord-screenaudio", "discord-screenaudio",
QApplication::applicationVersion(),
"Custom Discord client with the ability to stream audio on Linux",
KAboutLicense::GPL_V3, "Copyright 2022 (C) Malte Jürgens");
aboutData.setBugAddress("https://github.com/maltejur/discord-screenaudio");
aboutData.addAuthor("Malte Jürgens", "Author", "maltejur@dismail.de",
"https://github.com/maltejur");
aboutData.addCredit("edisionnano",
"For creating and documenting the approach for streaming "
"audio in Discord used in this project.",
QString(),
"https://github.com/edisionnano/"
"Screenshare-with-audio-on-Discord-with-Linux");
aboutData.addCredit(
"Curve", "For creating the Rohrkabel library used in this project.",
QString(), "https://github.com/Curve");
aboutData.addComponent("Rohrkabel", "A C++ RAII Pipewire-API Wrapper", "1.3",
"https://github.com/Soundux/rohrkabel");
m_helpMenu = new KHelpMenu(parent, aboutData);
#ifdef KGLOBALACCEL
injectScriptText("kglobalaccel.js",
"window.discordScreenaudioKGLOBALACCEL = true;");
auto toggleMuteAction = new QAction(this);
toggleMuteAction->setText("Toggle Mute");
toggleMuteAction->setIcon(QIcon::fromTheme("microphone-sensitivity-muted"));
connect(toggleMuteAction, &QAction::triggered, this,
&DiscordPage::toggleMute);
auto toggleDeafenAction = new QAction(this);
toggleDeafenAction->setText("Toggle Deafen");
toggleDeafenAction->setIcon(QIcon::fromTheme("audio-volume-muted"));
connect(toggleDeafenAction, &QAction::triggered, this,
&DiscordPage::toggleDeafen);
m_actionCollection = new KActionCollection(this);
m_actionCollection->addAction("toggleMute", toggleMuteAction);
KGlobalAccel::setGlobalShortcut(toggleMuteAction, QList<QKeySequence>{});
m_actionCollection->addAction("toggleDeafen", toggleDeafenAction);
KGlobalAccel::setGlobalShortcut(toggleDeafenAction, QList<QKeySequence>{});
m_shortcutsDialog = new KShortcutsDialog(KShortcutsEditor::GlobalAction);
m_shortcutsDialog->addCollection(m_actionCollection);
#endif
#endif
connect(&m_streamDialog, &StreamDialog::requestedStreamStart, this,
&DiscordPage::startStream);
}
void DiscordPage::injectScript(QString source) {
qDebug() << "[main ] Injecting " << source;
void DiscordPage::injectScriptText(QString name, QString content) {
qDebug(mainLog) << "Injecting " << name;
QFile userscript(source);
QWebEngineScript script;
if (!userscript.open(QIODevice::ReadOnly)) {
script.setSourceCode(content);
script.setName(name);
script.setWorldId(QWebEngineScript::MainWorld);
script.setInjectionPoint(QWebEngineScript::DocumentCreation);
script.setRunsOnSubFrames(false);
scripts().insert(script);
}
void DiscordPage::injectScriptFile(QString name, QString source) {
QFile file(source);
if (!file.open(QIODevice::ReadOnly)) {
qFatal("Failed to load %s with error: %s", source.toLatin1().constData(),
userscript.errorString().toLatin1().constData());
file.errorString().toLatin1().constData());
} else {
QByteArray userscriptJs = userscript.readAll();
QWebEngineScript script;
script.setSourceCode(userscriptJs);
script.setName("userscript.js");
script.setWorldId(QWebEngineScript::MainWorld);
script.setInjectionPoint(QWebEngineScript::DocumentCreation);
script.setRunsOnSubFrames(false);
scripts().insert(script);
injectScriptText(name, file.readAll());
}
}
@@ -67,8 +152,8 @@ void DiscordPage::featurePermissionRequested(const QUrl &securityOrigin,
if (feature == QWebEnginePage::Feature::MediaAudioCapture) {
if (m_virtmicProcess.state() == QProcess::NotRunning) {
qDebug() << "[virtmic] Starting Virtmic with no target to make sure "
"Discord can find all the audio devices";
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"});
}
@@ -78,7 +163,6 @@ void DiscordPage::featurePermissionRequested(const QUrl &securityOrigin,
bool DiscordPage::acceptNavigationRequest(const QUrl &url,
QWebEnginePage::NavigationType type,
bool isMainFrame) {
qDebug() << url;
if (type == QWebEnginePage::NavigationTypeLinkClicked) {
QDesktopServices::openUrl(url);
return false;
@@ -100,17 +184,15 @@ QWebEnginePage *DiscordPage::createWindow(QWebEnginePage::WebWindowType type) {
void DiscordPage::stopVirtmic() {
if (m_virtmicProcess.state() == QProcess::Running) {
qDebug() << "[virtmic] Stopping Virtmic";
qDebug(virtmicLog) << "Stopping Virtmic";
m_virtmicProcess.kill();
m_virtmicProcess.waitForFinished();
}
}
void DiscordPage::startVirtmic(QString target) {
if (target != "None") {
qDebug() << "[virtmic] Starting Virtmic with target" << target;
m_virtmicProcess.start(QApplication::arguments()[0], {"--virtmic", target});
}
qDebug(virtmicLog) << "Starting Virtmic with target" << target;
m_virtmicProcess.start(QApplication::arguments()[0], {"--virtmic", target});
}
void DiscordPage::javaScriptConsoleMessage(
@@ -124,20 +206,58 @@ void DiscordPage::javaScriptConsoleMessage(
m_streamDialog.updateTargets();
} else if (message == "!discord-screenaudio-stream-stopped") {
stopVirtmic();
} else if (message == "!discord-screenaudio-about") {
#ifdef KXMLGUI
m_helpMenu->aboutApplication();
#endif
} else if (message == "!discord-screenaudio-keybinds") {
#ifdef KXMLGUI
#ifdef KGLOBALACCEL
m_shortcutsDialog->show();
#else
QMessageBox::information(MainWindow::instance(), "discord-screenaudio",
"Keybinds are not supported on this platform "
"(KGlobalAccel is not available).",
QMessageBox::Ok);
#endif
#else
QMessageBox::information(MainWindow::instance(), "discord-screenaudio",
"Keybinds are not supported on this platform "
"(KXmlGui and KGlobalAccel are not available).",
QMessageBox::Ok);
#endif
} else if (message == "!discord-screenaudio-tray-true") {
MainWindow::instance()->setTrayIcon(true);
} else if (message == "!discord-screenaudio-tray-false") {
MainWindow::instance()->setTrayIcon(false);
} else if (message.startsWith("dsa: ")) {
qDebug(userscriptLog) << message.mid(5).toUtf8().constData();
} else {
qDebug() << "[discord]" << message;
qDebug(discordLog) << message;
}
}
void DiscordPage::startStream(QString target, uint width, uint height,
uint frameRate) {
void DiscordPage::startStream(bool video, bool audio, uint width, uint height,
uint frameRate, QString target) {
stopVirtmic();
startVirtmic(target);
startVirtmic(audio ? target : "[None]");
// 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));
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));
});
}
void DiscordPage::toggleMute() {
qDebug(shortcutLog) << "Toggling mute";
runJavaScript("window.discordScreenaudioToggleMute();");
}
void DiscordPage::toggleDeafen() {
qDebug(shortcutLog) << "Toggling deafen";
runJavaScript("window.discordScreenaudioToggleDeafen();");
}

View File

@@ -3,6 +3,12 @@
#include "streamdialog.h"
#include "virtmic.h"
#ifdef KXMLGUI
#include <KActionCollection>
#include <KHelpMenu>
#include <KShortcutsDialog>
#endif
#include <QProcess>
#include <QWebEngineFullScreenRequest>
#include <QWebEnginePage>
@@ -16,6 +22,13 @@ public:
private:
StreamDialog m_streamDialog;
QProcess m_virtmicProcess;
#ifdef KXMLGUI
KHelpMenu *m_helpMenu;
#ifdef KGLOBALACCEL
KActionCollection *m_actionCollection;
KShortcutsDialog *m_shortcutsDialog;
#endif
#endif
bool acceptNavigationRequest(const QUrl &url,
QWebEnginePage::NavigationType type,
bool isMainFrame) override;
@@ -24,14 +37,18 @@ private:
javaScriptConsoleMessage(QWebEnginePage::JavaScriptConsoleMessageLevel level,
const QString &message, int lineNumber,
const QString &sourceID) override;
void injectScript(QString source);
void injectScriptText(QString name, QString content);
void injectScriptFile(QString name, QString source);
void stopVirtmic();
void startVirtmic(QString target);
void toggleMute();
void toggleDeafen();
private Q_SLOTS:
void featurePermissionRequested(const QUrl &securityOrigin,
QWebEnginePage::Feature feature);
void startStream(QString target, uint width, uint height, uint frameRate);
void startStream(bool video, bool audio, uint width, uint height,
uint frameRate, QString target);
};
// Will immediately get destroyed again but is needed for navigation to

7
src/log.cpp Normal file
View File

@@ -0,0 +1,7 @@
#include "log.h"
Q_LOGGING_CATEGORY(mainLog, "main");
Q_LOGGING_CATEGORY(discordLog, "discord");
Q_LOGGING_CATEGORY(userscriptLog, "userscript");
Q_LOGGING_CATEGORY(virtmicLog, "virtmic");
Q_LOGGING_CATEGORY(shortcutLog, "shortcut");

9
src/log.h Normal file
View File

@@ -0,0 +1,9 @@
#pragma once
#include <QLoggingCategory>
Q_DECLARE_LOGGING_CATEGORY(mainLog);
Q_DECLARE_LOGGING_CATEGORY(discordLog);
Q_DECLARE_LOGGING_CATEGORY(userscriptLog);
Q_DECLARE_LOGGING_CATEGORY(virtmicLog);
Q_DECLARE_LOGGING_CATEGORY(shortcutLog);

View File

@@ -1,13 +1,23 @@
#include "mainwindow.h"
#include "virtmic.h"
#ifdef KXMLGUI
#include <KAboutData>
#endif
#include <QApplication>
#include <QCommandLineParser>
#include <QLoggingCategory>
int main(int argc, char *argv[]) {
QApplication app(argc, argv);
QApplication::setApplicationName("discord-screenaudio");
QApplication::setApplicationVersion("1.0.0-rc.11");
QApplication::setWindowIcon(
QIcon(":assets/de.shorsh.discord-screenaudio.png"));
QApplication::setApplicationVersion(DISCORD_SCEENAUDIO_VERSION_FULL);
QApplication::setDesktopFileName("de.shorsh.discord-screenaudio");
qSetMessagePattern("[%{category}] %{message}");
QCommandLineParser parser;
parser.setApplicationDescription(
@@ -17,17 +27,29 @@ int main(int argc, char *argv[]) {
QCommandLineOption virtmicOption("virtmic", "Start the Virtual Microphone",
"target");
parser.addOption(virtmicOption);
#ifdef DEBUG
parser.addOption(QCommandLineOption(
"remote-debugging-port", "Chromium Remote Debugging Port", "port"));
#endif
QCommandLineOption degubOption("remote-debugging",
"Open Chromium Remote Debugging on port 9222");
parser.addOption(degubOption);
QCommandLineOption notifySendOption(
"notify-send", "Use notify-send instead of QT/KF5 notifications");
parser.addOption(notifySendOption);
parser.process(app);
if (parser.isSet(virtmicOption)) {
Virtmic::start(parser.value(virtmicOption));
}
MainWindow w;
qputenv("QTWEBENGINE_CHROMIUM_FLAGS",
"--enable-features=WebRTCPipeWireCapturer " +
qgetenv("QTWEBENGINE_CHROMIUM_FLAGS"));
if (parser.isSet(degubOption))
qputenv("QTWEBENGINE_CHROMIUM_FLAGS",
"--remote-debugging-port=9222 " +
qgetenv("QTWEBENGINE_CHROMIUM_FLAGS"));
MainWindow w(parser.isSet(notifySendOption));
w.show();
return app.exec();

View File

@@ -1,6 +1,10 @@
#include "mainwindow.h"
#include "virtmic.h"
#ifdef KNOTIFICATIONS
#include <KNotification>
#endif
#include <QApplication>
#include <QColor>
#include <QComboBox>
@@ -11,13 +15,23 @@
#include <QSpacerItem>
#include <QThread>
#include <QUrl>
#include <QWebEngineNotification>
#include <QWebEngineProfile>
#include <QWebEngineScript>
#include <QWebEngineScriptCollection>
#include <QWebEngineSettings>
#include <QWidget>
MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) {
MainWindow *MainWindow::m_instance = nullptr;
MainWindow::MainWindow(bool useNotifySend, QWidget *parent)
: QMainWindow(parent) {
assert(MainWindow::m_instance == nullptr);
MainWindow::m_instance = this;
m_useNotifySend = useNotifySend;
setupSettings();
setupWebView();
setupTrayIcon();
resize(1000, 700);
showMaximized();
}
@@ -30,6 +44,38 @@ void MainWindow::setupWebView() {
m_webView = new QWebEngineView(this);
m_webView->setPage(page);
if (m_useKF5Notifications || m_useNotifySend)
QWebEngineProfile::defaultProfile()->setNotificationPresenter(
[&](std::unique_ptr<QWebEngineNotification> notificationInfo) {
if (m_useNotifySend) {
auto title = notificationInfo->title();
auto message = notificationInfo->message();
auto image_path =
QString("/tmp/discord-screenaudio-%1.png").arg(title);
notificationInfo->icon().save(image_path);
QProcess::execute("notify-send",
{"--icon", image_path, "--app-name",
"discord-screenaudio", title, message});
} else if (m_useKF5Notifications) {
#ifdef KNOTIFICATIONS
KNotification *notification =
new KNotification("discordNotification");
notification->setTitle(notificationInfo->title());
notification->setText(notificationInfo->message());
notification->setPixmap(
QPixmap::fromImage(notificationInfo->icon()));
notification->setDefaultAction("View");
connect(notification, &KNotification::defaultActivated,
[&, notificationInfo = std::move(notificationInfo)]() {
notificationInfo->click();
show();
activateWindow();
});
notification->sendEvent();
#endif
}
});
setCentralWidget(m_webView);
}
@@ -44,4 +90,70 @@ 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() {
m_trayIcon->hide();
m_trayIconMenu->deleteLater();
m_trayIcon->deleteLater();
m_trayIconMenu = nullptr;
m_trayIcon = nullptr;
}
void MainWindow::setupSettings() {
m_settings = new QSettings("maltejur", "discord-screenaudio", this);
m_settings->beginGroup("settings");
m_settings->endGroup();
}
QSettings *MainWindow::settings() const { return m_settings; }
void MainWindow::setTrayIcon(bool enabled) {
m_settings->setValue("trayIcon", enabled);
if (enabled) {
setupTrayIcon();
} else {
cleanTrayIcon();
}
}
void MainWindow::closeEvent(QCloseEvent *event) {
if (m_settings->value("trayIcon", false).toBool()) {
hide();
} else
QApplication::quit();
}
MainWindow *MainWindow::instance() { return m_instance; }

View File

@@ -3,8 +3,11 @@
#include "discordpage.h"
#include <QMainWindow>
#include <QMenu>
#include <QScopedPointer>
#include <QSettings>
#include <QString>
#include <QSystemTrayIcon>
#include <QVector>
#include <QWebEnginePage>
#include <QWebEngineProfile>
@@ -14,15 +17,33 @@ class MainWindow : public QMainWindow {
Q_OBJECT
public:
explicit MainWindow(QWidget *parent = nullptr);
explicit MainWindow(bool useNotifySend = false, QWidget *parent = nullptr);
static MainWindow *instance();
QSettings *settings() const;
private:
void setupWebView();
void setupTrayIcon();
void cleanTrayIcon();
void setupSettings();
QWebEngineView *m_webView;
QWebEngineProfile *prepareProfile();
DiscordPage *m_discordPage;
void closeEvent(QCloseEvent *event) override;
QSystemTrayIcon *m_trayIcon;
QMenu *m_trayIconMenu;
QSettings *m_settings;
bool m_wasMaximized;
static MainWindow *m_instance;
bool m_useNotifySend;
#ifdef KNOTIFICATIONS
bool m_useKF5Notifications = true;
#else
bool m_useKF5Notifications = false;
#endif
public Q_SLOTS:
void setTrayIcon(bool enabled);
private Q_SLOTS:
void fullScreenRequested(QWebEngineFullScreenRequest fullScreenRequest);

View File

@@ -12,67 +12,110 @@
StreamDialog::StreamDialog() : QWidget() {
setAttribute(Qt::WA_QuitOnClose, false);
auto layout = new QVBoxLayout(this);
{
auto layout = new QVBoxLayout(this);
layout->setSizeConstraint(QLayout::SetFixedSize);
auto targetLabel = new QLabel(this);
targetLabel->setText("Which app do you want to stream sound from?");
layout->addWidget(targetLabel);
m_videoGroupBox = new QGroupBox(this);
m_videoGroupBox->setTitle("Video");
m_videoGroupBox->setCheckable(true);
layout->addWidget(m_videoGroupBox);
m_targetComboBox = new QComboBox(this);
updateTargets();
layout->addWidget(m_targetComboBox);
{
auto videoLayout = new QVBoxLayout(this);
m_videoGroupBox->setLayout(videoLayout);
auto qualityLabel = new QLabel(this);
qualityLabel->setText("Stream Quality");
layout->addWidget(qualityLabel);
auto resolutionLabel = new QLabel(this);
resolutionLabel->setText("Resolution");
videoLayout->addWidget(resolutionLabel);
auto qualityHBox = new QHBoxLayout(this);
layout->addLayout(qualityHBox);
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);
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);
auto framerateLabel = new QLabel(this);
framerateLabel->setText("Framerate");
videoLayout->addWidget(framerateLabel);
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);
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);
}
auto button = new QPushButton(this);
button->setText("Start Stream");
connect(button, &QPushButton::clicked, this, &StreamDialog::startStream);
layout->addWidget(button, Qt::AlignRight | Qt::AlignBottom);
m_audioGroupBox = new QGroupBox(this);
m_audioGroupBox->setCheckable(true);
m_audioGroupBox->setTitle("Audio");
layout->addWidget(m_audioGroupBox);
setLayout(layout);
{
auto audioLayout = new QVBoxLayout(this);
m_audioGroupBox->setLayout(audioLayout);
auto targetLabel = new QLabel(this);
targetLabel->setText("Audio Source");
audioLayout->addWidget(targetLabel);
{
auto targetLayout = new QHBoxLayout(this);
audioLayout->addLayout(targetLayout);
m_targetComboBox = new QComboBox(this);
updateTargets();
targetLayout->addWidget(m_targetComboBox);
auto refreshTargetsButton = new QPushButton(this);
refreshTargetsButton->setFixedSize(30, 30);
refreshTargetsButton->setIcon(QIcon::fromTheme("view-refresh"));
connect(refreshTargetsButton, &QPushButton::clicked, this,
&StreamDialog::updateTargets);
targetLayout->addWidget(refreshTargetsButton);
}
}
auto button = new QPushButton(this);
button->setText("Start Stream");
connect(button, &QPushButton::clicked, this, &StreamDialog::startStream);
layout->addWidget(button, Qt::AlignRight | Qt::AlignBottom);
setLayout(layout);
}
setWindowTitle("discord-screenaudio Stream Dialog");
setFixedSize(0, 0);
}
void StreamDialog::startStream() {
auto resolution =
m_qualityResolutionComboBox->currentData().toString().split('x');
emit requestedStreamStart(m_targetComboBox->currentText(),
auto resolution = m_resolutionComboBox->currentData().toString().split('x');
emit requestedStreamStart(m_videoGroupBox->isChecked(),
m_audioGroupBox->isChecked(),
resolution[0].toUInt(), resolution[1].toUInt(),
m_qualityFPSComboBox->currentData().toUInt());
m_framerateComboBox->currentData().toUInt(),
m_targetComboBox->currentText());
setHidden(true);
}
void StreamDialog::updateTargets() {
auto lastTarget = m_targetComboBox->currentText();
m_targetComboBox->clear();
m_targetComboBox->addItem("None");
m_targetComboBox->addItem("[All Desktop Audio]");
for (auto target : Virtmic::getTargets()) {
m_targetComboBox->addItem(target);
}
if (m_targetComboBox->findText(lastTarget) != -1)
m_targetComboBox->setCurrentText(lastTarget);
else
m_targetComboBox->setCurrentText("[All Desktop Audio]");
}

View File

@@ -2,6 +2,7 @@
#include <QComboBox>
#include <QDialog>
#include <QGroupBox>
#include <QWidget>
class StreamDialog : public QWidget {
@@ -12,12 +13,14 @@ public:
private:
QComboBox *m_targetComboBox;
QComboBox *m_qualityResolutionComboBox;
QComboBox *m_qualityFPSComboBox;
QComboBox *m_resolutionComboBox;
QComboBox *m_framerateComboBox;
QGroupBox *m_videoGroupBox;
QGroupBox *m_audioGroupBox;
Q_SIGNALS:
void requestedStreamStart(QString target, uint width, uint height,
uint frameRate);
void requestedStreamStart(bool video, bool audio, uint width, uint height,
uint frameRate, QString target);
public Q_SLOTS:
void updateTargets();

View File

@@ -1,10 +1,13 @@
#include "virtmic.h"
#include "log.h"
#include <rohrkabel/loop/main.hpp>
#include <rohrkabel/registry/registry.hpp>
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);
@@ -19,11 +22,11 @@ QVector<QString> getTargets() {
if (global.type == pipewire::node::type) {
auto node = reg.bind<pipewire::node>(global.id);
auto info = node.info();
auto name = QString::fromStdString(info.props["application.name"]);
if (info.props.count("node.name")) {
auto name = QString::fromStdString(info.props["node.name"]);
if (!targets.contains(name))
targets.append(name);
if (name != "" && !EXCLUDE_TARGETS.contains(name) &&
!targets.contains(name)) {
targets.append(name);
}
}
});
@@ -64,20 +67,24 @@ void start(QString _target) {
continue;
auto &parent = nodes.at(parent_id);
auto name = parent.props["application.name"];
if (parent.props["node.name"].find(target) != std::string::npos) {
std::cout << "[virtmic] "
<< "Link : " << target << ":" << port_id << " -> ";
if (port.info().props["audio.channel"] == "FL") {
links.emplace(port_id, core.create<pipewire::link_factory>(
{virt_fl->info().id, port_id}));
std::cout << "[virtmic] " << virt_fl->info().id << std::endl;
} else {
links.emplace(port_id, core.create<pipewire::link_factory>(
{virt_fr->info().id, port_id}));
std::cout << "[virtmic] " << virt_fr->info().id << std::endl;
}
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();
}
}
};
@@ -93,7 +100,7 @@ void start(QString _target) {
pipewire::node::type, pipewire::node::version,
pipewire::update_strategy::none);
if (target == "None") {
if (target == "[None]") {
while (true) {
main_loop.run();
}
@@ -105,9 +112,13 @@ void start(QString _target) {
[&](const pipewire::global &global) {
if (global.type == pipewire::node::type) {
auto node = reg.bind<pipewire::node>(global.id);
std::cout << "[virtmic] "
<< "Added : " << node.info().props["node.name"]
<< std::endl;
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());
@@ -141,8 +152,11 @@ void start(QString _target) {
[&](const std::uint32_t id) {
if (nodes.count(id)) {
auto info = nodes.at(id);
std::cout << "[virtmic] "
<< "Removed: " << info.props["node.name"] << std::endl;
qDebug(virtmicLog) << QString("Removed: %1")
.arg(QString::fromStdString(
info.props["application.name"].data()))
.toUtf8()
.data();
nodes.erase(id);
}
if (ports.count(id)) {

164
version.cmake Normal file
View File

@@ -0,0 +1,164 @@
# Copyright (c) 2018-2021 Ribose Inc.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
# 1. Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
# TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS
# BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
# desired length of commit hash
set(GIT_REV_LEN 7)
# call git, store output in var (can fail)
macro(_git var)
execute_process(
COMMAND "${GIT_EXECUTABLE}" ${ARGN}
WORKING_DIRECTORY "${source_dir}"
RESULT_VARIABLE _git_ec
OUTPUT_VARIABLE ${var}
OUTPUT_STRIP_TRAILING_WHITESPACE
ERROR_QUIET
)
endmacro()
function(extract_version_info version var_prefix)
# extract the main components
# v1.9.0-3-g5b92266+1546836556
# v1.9.0-3-g5b92266-dirty+1546836556
string(REGEX MATCH "^v?([0-9]+\\.[0-9]+\\.[0-9]+)(-([0-9]+)-g([0-9a-f]+)(-dirty)?)?(\\+([0-9]+))?$" matches "${version}")
if (NOT matches)
message(FATAL_ERROR "Failed to extract version components from ${version}.")
endif()
set(${var_prefix}_VERSION "${CMAKE_MATCH_1}" PARENT_SCOPE) # 1.9.0
if (NOT CMAKE_MATCH_3)
set(CMAKE_MATCH_3 "0")
endif()
set(${var_prefix}_VERSION_NCOMMITS "${CMAKE_MATCH_3}" PARENT_SCOPE) # 3
if (NOT CMAKE_MATCH_4)
set(CMAKE_MATCH_4 "0")
endif()
set(${var_prefix}_VERSION_GIT_REV "${CMAKE_MATCH_4}" PARENT_SCOPE) # 5b92266
if (CMAKE_MATCH_5 STREQUAL "-dirty")
set(${var_prefix}_VERSION_IS_DIRTY TRUE PARENT_SCOPE)
else()
set(${var_prefix}_VERSION_IS_DIRTY FALSE PARENT_SCOPE)
endif()
# timestamp is optional, default to 0
if (NOT CMAKE_MATCH_7)
set(CMAKE_MATCH_7 "0")
endif()
set(${var_prefix}_VERSION_COMMIT_TIMESTAMP "${CMAKE_MATCH_7}" PARENT_SCOPE) # 1546836556
endfunction()
function(determine_version source_dir var_prefix)
set(has_release_tag NO)
set(has_version_txt NO)
set(local_prefix "_determine_ver")
# find out base version via version.txt
set(base_version "0.0.0")
if (EXISTS "${source_dir}/version.txt")
set(has_version_txt YES)
file(STRINGS "${source_dir}/version.txt" version_file)
extract_version_info("${version_file}" "${local_prefix}")
set(base_version "${${local_prefix}_VERSION}")
message(STATUS "Found version.txt with ${version_file}")
else()
message(STATUS "Found no version.txt.")
endif()
# for GIT_EXECUTABLE
find_package(Git)
# get a description of the version, something like:
# v1.9.1-0-g38ffe82 (a tagged release)
# v1.9.1-0-g38ffe82-dirty (a tagged release with local modifications)
# v1.9.0-3-g5b92266 (post-release snapshot)
# v1.9.0-3-g5b92266-dirty (post-release snapshot with local modifications)
_git(version describe --abbrev=${GIT_REV_LEN} --match "v[0-9]*" --long --dirty)
if (NOT _git_ec EQUAL 0)
# no annotated tags, fake one
message(STATUS "Found no annotated tags.")
_git(revision rev-parse --short=${GIT_REV_LEN} --verify HEAD)
if (_git_ec EQUAL 0)
set(version "v${base_version}-0-g${revision}")
# check if dirty (this won't detect untracked files, but should be ok)
_git(changes diff-index --quiet HEAD --)
if (NOT _git_ec EQUAL 0)
string(APPEND version "-dirty")
endif()
# append the commit timestamp of the most recent commit (only
# in non-release branches -- typically master)
_git(commit_timestamp show -s --format=%ct)
if (_git_ec EQUAL 0)
string(APPEND version "+${commit_timestamp}")
endif()
elseif(has_version_txt)
# Nothing to get from git - so use version.txt completely
set(version "${version_file}")
else()
# Sad case - no git, no version.txt
set(version "v${base_version}")
endif()
else()
set(has_release_tag YES)
message(STATUS "Found annotated tag ${version}")
endif()
extract_version_info("${version}" "${local_prefix}")
if ("${has_version_txt}" AND NOT ${base_version} STREQUAL ${local_prefix}_VERSION)
message(WARNING "Tagged version ${${local_prefix}_VERSION} doesn't match one from the version.txt: ${base_version}")
if (${base_version} VERSION_GREATER ${local_prefix}_VERSION)
set(${local_prefix}_VERSION ${base_version})
endif()
endif()
foreach(suffix VERSION VERSION_NCOMMITS VERSION_GIT_REV VERSION_IS_DIRTY VERSION_COMMIT_TIMESTAMP)
if (NOT DEFINED ${local_prefix}_${suffix})
message(FATAL_ERROR "Unable to determine version.")
endif()
set(${var_prefix}_${suffix} "${${local_prefix}_${suffix}}" PARENT_SCOPE)
message(STATUS "${var_prefix}_${suffix}: ${${local_prefix}_${suffix}}")
endforeach()
# Set VERSION_SUFFIX and VERSION_FULL. When making changes, be aware that
# this is used in packaging as well and will affect ordering.
# | state | version_full |
# |-----------------------------------------------------|
# | exact tag | 0.9.0 |
# | exact tag, dirty | 0.9.0+git20180604 |
# | after tag | 0.9.0+git20180604.1.085039f |
# | no tag, version.txt | 0.9.0 |
# | no tag, no version.txt| 0.0.0+git20180604.2ee02af |
string(TIMESTAMP date "%Y%m%d" UTC)
set(version_suffix "")
if (has_version_txt)
elseif (NOT ${local_prefix}_VERSION_NCOMMITS EQUAL 0)
# 0.9.0+git20150604.4.289818b
string(APPEND version_suffix "+git${date}.${${local_prefix}_VERSION_NCOMMITS}.${${local_prefix}_VERSION_GIT_REV}")
elseif ((NOT has_release_tag) AND ((NOT has_version_txt) OR ("${base_version}" STREQUAL "0.0.0") OR (NOT "${revision}" STREQUAL "")))
# 0.9.0+git20150604.289818b
string(APPEND version_suffix "+git${date}.${${local_prefix}_VERSION_GIT_REV}")
elseif(${local_prefix}_VERSION_IS_DIRTY)
# 0.9.0+git20150604
string(APPEND version_suffix "+git${date}")
endif()
set(version_full "${${local_prefix}_VERSION}${version_suffix}")
# set the results
set(${var_prefix}_VERSION_SUFFIX "${version_suffix}" PARENT_SCOPE)
set(${var_prefix}_VERSION_FULL "${version_full}" PARENT_SCOPE)
# for informational purposes
message(STATUS "${var_prefix}_VERSION_SUFFIX: ${version_suffix}")
message(STATUS "${var_prefix}_VERSION_FULL: ${version_full}")
endfunction()