Compare commits

..

9 Commits

Author SHA1 Message Date
Malte Jürgens
092fbf72b6 bump version 2022-07-27 21:25:24 +02:00
Malte Jürgens
4d3e77bc9a Merge branch 'master' into appimage 2022-07-27 21:24:54 +02:00
Malte Jürgens
ae57550b46 Merge branch 'master' into appimage 2022-07-27 20:45:16 +02:00
Malte Jürgens
6681c0591a Merge branch 'master' into appimage 2022-07-17 19:43:36 +02:00
Malte Jürgens
8597416bb5 still not working 2022-07-16 22:28:16 +02:00
Malte Jürgens
ce94b44997 almost working 2022-07-16 22:03:33 +02:00
Malte Jürgens
2741e1afaa muh working 2022-07-16 20:41:26 +02:00
Malte Jürgens
b2f5f8e7d0 not working but better 2022-07-16 19:09:11 +02:00
Malte Jürgens
c4c32b1615 it doesnt work 2022-07-13 22:00:30 +02:00
37 changed files with 496 additions and 1816 deletions

2
.gitignore vendored
View File

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

3
.gitmodules vendored
View File

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

View File

@@ -1,50 +1,24 @@
cmake_minimum_required(VERSION 3.0) cmake_minimum_required(VERSION 3.11)
project(discord-screenaudio) 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_CXX_STANDARD 17)
set(CMAKE_INCLUDE_CURRENT_DIR ON) set(CMAKE_INCLUDE_CURRENT_DIR ON)
set(CMAKE_AUTOMOC ON) set(CMAKE_AUTOMOC ON)
set(CMAKE_AUTORCC ON) set(CMAKE_AUTORCC ON)
set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -DDEBUG") set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -DDEBUG")
string(TIMESTAMP TIMESTAMP %s)
# set(CMAKE_AUTOUIC ON) # set(CMAKE_AUTOUIC ON)
find_package(Qt5 COMPONENTS Widgets) find_package(Qt5 CONFIG REQUIRED COMPONENTS
if (Qt5_FOUND) Widgets
find_package(Qt5 CONFIG REQUIRED COMPONENTS Widgets WebEngineWidgets) WebEngineWidgets
)
find_package(KF5Notifications) set(${CMAKE_PROJECT_NAME}_SRC
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 src/main.cpp
src/mainwindow.cpp src/mainwindow.cpp
src/virtmic.cpp src/virtmic.cpp
src/discordpage.cpp src/discordpage.cpp
src/streamdialog.cpp src/streamdialog.cpp
src/log.cpp
src/userscript.cpp
src/centralwidget.cpp
resources.qrc resources.qrc
) )
@@ -63,34 +37,18 @@ if(GIT_FOUND AND EXISTS "${PROJECT_SOURCE_DIR}/.git")
endif() endif()
endif() endif()
function(add_git_subdirectory SUBMODULE) if(NOT EXISTS "${PROJECT_SOURCE_DIR}/submodules/rohrkabel/CMakeLists.txt")
if(NOT EXISTS "${PROJECT_SOURCE_DIR}/submodules/${SUBMODULE}/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`.")
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/${SUBMODULE})
endfunction()
add_git_subdirectory(rohrkabel)
add_git_subdirectory(channel)
add_executable(discord-screenaudio ${discord-screenaudio_SRC})
target_link_libraries(discord-screenaudio Qt::Widgets Qt::WebEngineWidgets rohrkabel channel)
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() endif()
install(TARGETS discord-screenaudio DESTINATION bin) add_subdirectory(submodules/rohrkabel)
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) add_executable(${CMAKE_PROJECT_NAME} ${${CMAKE_PROJECT_NAME}_SRC})
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) target_link_libraries(${CMAKE_PROJECT_NAME} Qt5::Widgets Qt5::WebEngineWidgets rohrkabel)
install(TARGETS ${CMAKE_PROJECT_NAME} DESTINATION bin)
install(PROGRAMS assets/${CMAKE_PROJECT_NAME}.desktop DESTINATION share/applications)
install(FILES assets/discord.png DESTINATION share/pixmaps)
add_custom_target(appimage ${CMAKE_SOURCE_DIR}/scripts/build-appimage.sh WORKING_DIRECTORY ${CMAKE_SOURCE_DIR})

View File

@@ -1,17 +1,13 @@
# discord-screenaudio # discord-screenaudio
A custom discord client that supports streaming with audio on Linux, made A very WIP custom discord client that supports streaming with audio on Linux,
possible by the made possible by the
[great work](https://github.com/edisionnano/Screenshare-with-audio-on-Discord-with-Linux) [great work](https://github.com/edisionnano/Screenshare-with-audio-on-Discord-with-Linux)
of [@edisionnano](https://github.com/edisionnano) and the of [@edisionnano](https://github.com/edisionnano) and the
[Rohrkabel library](https://github.com/Soundux/rohrkabel) by [Rohrkabel library](https://github.com/Soundux/rohrkabel) by
[@Curve](https://github.com/Curve). [@Curve](https://github.com/Curve).
Unlike a lot of other solutions, the audio here is directly fed into the ![Screenshot_20220718_194357](https://user-images.githubusercontent.com/48161361/179571245-11ea05f3-fb5e-4aef-9132-2736e122ef04.png)
screenshare and not passed to the user microphone
([see explanation](#how-does-this-work)).
![Screenshot_20221211_185028](https://user-images.githubusercontent.com/48161361/206920213-58a8091a-d8f9-4bb7-ae3d-3f8581b84d24.png)
The purpose of this project is **not** to provide an alternative to the original The purpose of this project is **not** to provide an alternative to the original
Discord client. Rather, it should be used in addition to the original client in Discord client. Rather, it should be used in addition to the original client in
@@ -19,48 +15,31 @@ 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 anything else, this client has way too many things that work less well than in
the original client. 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 ## Known Issues
- Only works with **PipeWire** - Only works with **PipeWire**
- Can only share primary screen on X11 (no other screens or specific - Only works on **X11**
applications) (see - Can only share primary screen (no other screens or specific applications) (see
[#1](https://github.com/maltejur/discord-screenaudio/issues/1)) [#1](https://github.com/maltejur/discord-screenaudio/issues/1))
## Installation ## Installation
You have multiple options: If you are using Arch, you can build and install
[from the AUR](https://aur.archlinux.org/packages/discord-screenaudio). If you
- Install the Flatpak (recommended): aren't, you will have to [build it yourself](#building-from-source).
`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 ## Building from Source
### Requirements ### Requirements
- Basic building tools - Basic building tools
- An up-to-date system (I can't guarantee that it works on Debian or Ubuntu
20/21)
- CMake - CMake
- Qt5 and QtWebEngine - Qt5 and QtWebEngine
- **PipeWire** (it currently doesn't work with PulseAudio) - **PipeWire** (it currently doesn't work with PulseAudio)
- Git - Git
- _Kf5Notifications (optional, for better notifications)_
- _KXMLGui and KGlobalAccel (optional, for keybinds)_
With apt: On Debian:
`apt install -y build-essential cmake qtbase5-dev qtwebengine5-dev libkf5notifications-dev libkf5xmlgui-dev libkf5globalaccel-dev pkg-config libpipewire-0.3-dev git` `apt install -y build-essential cmake qtbase5-dev qtwebengine5-dev pkg-config libpipewire-0.3-dev git`
### Building ### Building
@@ -84,9 +63,7 @@ And then to optionally install it, run:
sudo cmake --install build sudo cmake --install build
``` ```
## FAQ ## How it works
### How does this work?
This whole project is based on This whole project is based on
[this](https://github.com/edisionnano/Screenshare-with-audio-on-Discord-with-Linux) [this](https://github.com/edisionnano/Screenshare-with-audio-on-Discord-with-Linux)
@@ -95,23 +72,6 @@ Discord. Basically: a virtual microphone is created which captures the
application audio, and this microphone is then fed to the Discord stream by application audio, and this microphone is then fed to the Discord stream by
intercepting a API call of Discord. intercepting a API call of Discord.
### Drag and drop doesn't work in the Flatpak
This is due to sandboxing limitations of Flatpak. The main Discord Flatpak has
the same problem. If you still want to use drag and drop, you can disable most
of Flatpak's sandboxing by installing
[Flatseal](https://flathub.org/apps/details/com.github.tchx84.Flatseal) and
allowing access to "All system files" under the "Filesystem" section.
### Is there any way to add custom CSS / a theme?
Yes, you can add all your styles into
`~/.config/discord-screenaudio/userstyles.css` (or
`~/.var/app/de.shorsh.discord-screenaudio/config/discord-screenaudio/userstyles.css`
if you are using the Flatpak). But please note that due to QtWebEngine
limitations concerning content security policies, you can't use any external
files (like `@import` or `url()`).
## License ## License
Copyright (C) 2022 Malte Jürgens Copyright (C) 2022 Malte Jürgens

File diff suppressed because one or more lines are too long

View File

@@ -1 +0,0 @@
setTimeout((i=>{let t,a,s,e={};new WebSocket("ws://127.0.0.1:1337").onmessage=async i=>{if(msg=JSON.parse(i.data),console.log(msg),!t){const i=window.webpackChunkdiscord_app.push([[Symbol()],{},i=>i]),e=i.c;window.webpackChunkdiscord_app.pop();for(const i in e){let a=e[i].exports;if(a=a&&(a.Z??a.ZP),a&&a.register&&a.wait){t=a;break}}const n=i.m;for(const t in n)if(n[t].toString().includes("getAssetImage: size must === [number, number] for Twitch")){const s=i(t),e=Object.values(s).find((i=>"function"==typeof i&&i.toString().includes("apply(")));a=async(i,t)=>(await e(i,[t,void 0]))[0];break}for(const t in n)if(n[t].toString().includes("e.application={")){const a=i(t),e=Object.values(a).find((i=>"function"==typeof i&&i.toString().includes("e.application={")));s=async i=>{let t={};return await e(t,i),t.application};break}}if(msg.activity?.assets?.large_image&&(msg.activity.assets.large_image=await a(msg.activity.application_id,msg.activity.assets.large_image)),msg.activity?.assets?.small_image&&(msg.activity.assets.small_image=await a(msg.activity.application_id,msg.activity.assets.small_image)),msg.activity){const i=msg.activity.application_id;e[i]||(e[i]=await s(i));const t=e[i];msg.activity.name||(msg.activity.name=t.name)}t.dispatch({type:"LOCAL_ACTIVITY_UPDATE",...msg})}}),1e4);

View File

@@ -1,8 +0,0 @@
[Desktop Entry]
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,51 +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>
<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>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

View File

@@ -0,0 +1,7 @@
[Desktop Entry]
Type=Application
Name=discord-screenaudio
Exec=discord-screenaudio
Icon=discord
Terminal=false
Categories=Audio;

View File

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

BIN
assets/discord.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

View File

@@ -1,29 +1,18 @@
// From v0.4
navigator.mediaDevices.chromiumGetDisplayMedia = navigator.mediaDevices.chromiumGetDisplayMedia =
navigator.mediaDevices.getDisplayMedia; navigator.mediaDevices.getDisplayMedia;
function sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
const getAudioDevice = async (nameOfAudioDevice) => { const getAudioDevice = async (nameOfAudioDevice) => {
await navigator.mediaDevices.getUserMedia({ await navigator.mediaDevices.getUserMedia({
audio: true, audio: true,
}); });
let audioDevice;
while (audioDevice === undefined) {
let devices = await navigator.mediaDevices.enumerateDevices(); let devices = await navigator.mediaDevices.enumerateDevices();
audioDevice = devices.find(({ label }) => label === nameOfAudioDevice); let audioDevice = devices.find(({ label }) => label === nameOfAudioDevice);
if (!audioDevice)
userscript.log(
`Did not find '${nameOfAudioDevice}', trying again in 100ms`
);
await sleep(100);
}
userscript.log(`Found '${nameOfAudioDevice}'`);
return audioDevice; return audioDevice;
}; };
function setGetDisplayMedia(video = true, overrideArgs = undefined) { function setGetDisplayMedia(overrideArgs = undefined) {
const getDisplayMedia = async (...args) => { const getDisplayMedia = async (...args) => {
var id; var id;
try { try {
@@ -61,7 +50,6 @@ function setGetDisplayMedia(video = true, overrideArgs = undefined) {
: args || [{ video: true, audio: true }]) : args || [{ video: true, audio: true }])
); );
gdm.addTrack(track); gdm.addTrack(track);
if (!video) for (const track of gdm.getVideoTracks()) track.enabled = false;
return gdm; return gdm;
}; };
navigator.mediaDevices.getDisplayMedia = getDisplayMedia; navigator.mediaDevices.getDisplayMedia = getDisplayMedia;
@@ -69,121 +57,17 @@ function setGetDisplayMedia(video = true, overrideArgs = undefined) {
setGetDisplayMedia(); setGetDisplayMedia();
let userscript;
let muteBtn;
let deafenBtn;
let streamStartBtn;
let streamStartBtnInitialDisplay;
let streamStartBtnClone;
let resolutionString;
const clonedElements = []; const clonedElements = [];
const hiddenElements = []; const hiddenElements = [];
let wasStreamActive = false; let wasStreamActive = false;
function createButton(text, onClick) { setInterval(() => {
const button = document.createElement("button");
button.style.marginBottom = "20px";
button.classList =
"button-ejjZWC lookFilled-1H2Jvj colorBrand-2M3O3N sizeSmall-3R2P2p grow-2T4nbg";
button.innerText = text;
button.addEventListener("click", onClick);
return button;
}
function createSwitch(text, enabled, onClick) {
const container = document.createElement("div");
container.style.marginBottom = "20px";
container.className = "labelRow-NnoUIp";
const label = document.createElement("label");
label.innerText = text;
label.className = "title-2yADjX";
container.appendChild(label);
const svg = document.createElement("div");
container.appendChild(svg);
function setSvgDisabled() {
svg.innerHTML = `<div class="container-1QtPKm default-colors" style="opacity: 1; background-color: rgb(114, 118, 125);"><svg class="slider-HJFN2i" viewBox="0 0 28 20" preserveAspectRatio="xMinYMid meet" aria-hidden="true" style="left: -3px;"><rect fill="white" x="4" y="0" height="20" width="20" rx="10"></rect><svg viewBox="0 0 20 20" fill="none"><path fill="rgba(114, 118, 125, 1)" d="M5.13231 6.72963L6.7233 5.13864L14.855 13.2704L13.264 14.8614L5.13231 6.72963Z"></path><path fill="rgba(114, 118, 125, 1)" d="M13.2704 5.13864L14.8614 6.72963L6.72963 14.8614L5.13864 13.2704L13.2704 5.13864Z"></path></svg></svg><input id="uid_84" type="checkbox" class="input-125oad" tabindex="0"></div>`;
}
function setSvgEnabled() {
svg.innerHTML = `<div class="container-1QtPKm default-colors checked-16gMAN" style="opacity: 1; background-color: rgb(59, 165, 92);"><svg class="slider-HJFN2i" viewBox="0 0 28 20" preserveAspectRatio="xMinYMid meet" aria-hidden="true" style="left: 12px;"><rect fill="white" x="4" y="0" height="20" width="20" rx="10"></rect><svg viewBox="0 0 20 20" fill="none"><path fill="rgba(59, 165, 92, 1)" d="M7.89561 14.8538L6.30462 13.2629L14.3099 5.25755L15.9009 6.84854L7.89561 14.8538Z"></path><path fill="rgba(59, 165, 92, 1)" d="M4.08643 11.0903L5.67742 9.49929L9.4485 13.2704L7.85751 14.8614L4.08643 11.0903Z"></path></svg></svg><input id="uid_74" type="checkbox" class="input-125oad" tabindex="0" checked=""></div>`;
}
function updateSvg() {
if (enabled) setSvgEnabled();
else setSvgDisabled();
}
container.addEventListener("click", () => {
enabled = !enabled;
updateSvg();
onClick(enabled);
});
updateSvg();
return container;
}
// Fix for broken discord notifications after restart
// (https://github.com/maltejur/discord-screenaudio/issues/17)
Notification.requestPermission();
setTimeout(() => {
new QWebChannel(qt.webChannelTransport, (channel) => {
userscript = channel.objects.userscript;
main();
});
});
function main() {
userscript.muteToggled.connect(() => {
console.log("Toggling mute");
muteBtn && muteBtn.click();
});
userscript.deafenToggled.connect(() => {
console.log("Toggling deafen");
deafenBtn && deafenBtn.click();
});
userscript.streamStarted.connect((video, width, height, frameRate) => {
resolutionString = video ? `${height}p ${frameRate}FPS` : "Audio Only";
setGetDisplayMedia(video, {
audio: true,
video: { width, height, frameRate },
});
streamStartBtn.click();
streamStartBtn.style.display = streamStartBtnInitialDisplay;
streamStartBtnClone.remove();
});
function updateUserstyles() {
userscript.log("Loading userstyles...");
userscript.loadingMessage = "Loading userstyles...";
let stylesheet = document.getElementById("discordScreenaudioUserstyles");
if (stylesheet) {
userscript.log("Removing old userstyles...");
stylesheet.remove();
}
stylesheet = document.createElement("style");
stylesheet.id = "discordScreenaudioUserstyles";
stylesheet.innerText = userscript.userstyles;
document.head.appendChild(stylesheet);
userscript.log("Finished loading userstyles");
userscript.loadingMessage = "";
}
userscript.userstylesChanged.connect(updateUserstyles);
setTimeout(() => updateUserstyles());
setInterval(async () => {
const streamActive = const streamActive =
document.getElementsByClassName("panel-2ZFCRb activityPanel-9icbyU") document.getElementsByClassName("panel-2ZFCRb activityPanel-9icbyU")
.length > 0; .length > 0;
if (!streamActive && wasStreamActive) userscript.stopVirtmic(); if (!streamActive && wasStreamActive)
console.log("!discord-screenaudio-stream-stopped");
wasStreamActive = streamActive; wasStreamActive = streamActive;
if (streamActive) { if (streamActive) {
@@ -197,57 +81,46 @@ function main() {
}); });
hiddenElements.length = 0; hiddenElements.length = 0;
} else { } else {
for (const el of [ for (const el of document.querySelectorAll(
document.getElementsByClassName("actionButtons-2vEOUh")?.[0] '[aria-label="Share Your Screen"]'
?.children[1], )) {
document.querySelector( elClone = el.cloneNode(true);
".wrapper-3t3Yqv > div > div > div > div > .controlButton-2PMNom" elClone.ariaLabel = "Share Your Screen with Audio";
), elClone.title = "Share Your Screen with Audio";
]) { elClone.addEventListener("click", () => {
if (!el) continue; console.log("!discord-screenaudio-start-stream");
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; const initialDisplay = el.style.display;
streamStartBtn.style.display = "none"; window.discordScreenaudioStartStream = (width, height, frameRate) => {
streamStartBtn.parentNode.insertBefore(streamStartBtnClone, el); setGetDisplayMedia({
audio: true,
video: { width, height, frameRate },
});
el.click();
el.style.display = initialDisplay;
elClone.remove();
};
clonedElements.push(streamStartBtnClone); el.style.display = "none";
hiddenElements.push(streamStartBtn); el.parentNode.insertBefore(elClone, el);
clonedElements.push(elClone);
hiddenElements.push(el);
} }
} }
// Add about text in settings // Add about text in settings
if ( if (
document.getElementsByClassName("dirscordScreenaudioAboutText").length == document.getElementsByClassName("dirscordScreenaudioAboutText").length == 0
0
) { ) {
for (const el of document.getElementsByClassName("info-3pQQBb")) { for (const el of document.getElementsByClassName("info-3pQQBb")) {
let aboutEl; const aboutEl = document.createElement("div");
if (userscript.kxmlgui) { aboutEl.innerText = "discord-screenaudio v1.0.0-rc.7 (AppImage)";
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.fontSize = "12px";
aboutEl.style.color = "var(--text-muted)"; aboutEl.style.color = "var(--text-muted)";
aboutEl.style.textTransform = "none";
aboutEl.style.display = "inline-block";
aboutEl.style.width = "100%";
aboutEl.classList.add("dirscordScreenaudioAboutText"); aboutEl.classList.add("dirscordScreenaudioAboutText");
aboutEl.style.cursor = "pointer";
el.appendChild(aboutEl); el.appendChild(aboutEl);
} }
} }
@@ -255,118 +128,8 @@ function main() {
// Remove stream settings if stream is active // Remove stream settings if stream is active
document.getElementById("manage-streams-change-windows")?.remove(); document.getElementById("manage-streams-change-windows")?.remove();
document.querySelector(`[aria-label="Stream Settings"]`)?.remove(); document.querySelector(`[aria-label="Stream Settings"]`)?.remove();
}, 500);
// Add event listener for keybind tab // Fix for broken discord notifications after restart
if ( // (https://github.com/maltejur/discord-screenaudio/issues/17)
document Notification.requestPermission();
.getElementById("keybinds-tab")
?.getElementsByClassName(
"container-3jbRo5 info-1hMolH browserNotice-1u-Y5o"
).length
) {
const el = document
.getElementById("keybinds-tab")
.getElementsByClassName("children-1xdcWE")[0];
const div = document.createElement("div");
div.style.marginBottom = "50px";
div.appendChild(
createButton("Edit Global Keybinds", () => {
userscript.showShortcutsDialog();
})
);
el.innerHTML = "";
el.appendChild(div);
}
const buttonContainer =
document.getElementsByClassName("container-YkUktl")[0];
if (!buttonContainer) {
userscript.log(
"Cannot locate Mute/Deafen/Settings button container, please report this on GitHub"
);
}
muteBtn = buttonContainer
? buttonContainer.getElementsByTagName("button")[0]
: null;
deafenBtn = buttonContainer
? buttonContainer.getElementsByTagName("button")[1]
: null;
if (resolutionString) {
for (const el of document.getElementsByClassName(
"qualityIndicator-39wQDy"
)) {
el.innerHTML = resolutionString;
}
}
const accountTab = document.getElementById("my-account-tab");
if (accountTab) {
const discordScreenaudioSettings = document.getElementById(
"discord-screenaudio-settings"
);
if (!discordScreenaudioSettings) {
const firstDivider = accountTab.getElementsByClassName(
"divider-3nqZNm marginTop40-Q4o1tS"
)[0];
if (firstDivider) {
const section = document.createElement("div");
section.className = "marginTop40-Q4o1tS";
section.id = "discord-screenaudio-settings";
const title = document.createElement("h2");
title.className =
"h1-3iMExa title-lXcL8p defaultColor-3Olr-9 defaultMarginh1-1UYutH";
title.innerText = "discord-screenaudio";
section.appendChild(title);
section.appendChild(
createButton("Edit Global Keybinds", () => {
userscript.showShortcutsDialog();
})
);
// section.appendChild(
// createButton("Install Theme", () => {
// userscript.showThemeDialog();
// })
// );
// section.appendChild(
// createButton("Uninstall Theme", () => {
// userscript.installUserStyles("");
// })
// );
section.appendChild(
createSwitch(
"Move discord-screenaudio to the system tray instead of closing",
await userscript.getBoolPref("trayIcon", false),
(enabled) => {
userscript.setTrayIcon(enabled);
}
)
);
section.appendChild(
createSwitch(
"Start discord-screenaudio hidden to tray",
await userscript.getPref("startHidden", false),
(hidden) => {
userscript.setPref("startHidden", hidden);
}
)
);
const divider = document.createElement("div");
divider.className = "divider-3nqZNm marginTop40-Q4o1tS";
firstDivider.after(section);
section.after(divider);
}
}
}
}, 500);
}

View File

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

4
scripts/Dockerfile Normal file
View File

@@ -0,0 +1,4 @@
FROM debian:11
ADD ./prepare-container.sh /tmp/prepare-container.sh
RUN /tmp/prepare-container.sh
WORKDIR /work

32
scripts/build-appimage.sh Executable file
View File

@@ -0,0 +1,32 @@
#!/usr/bin/bash
set -e
# Check if inside of docker container
if [ ! -f /.dockerenv ]; then
# Check if docker is available
if ! command -v docker >/dev/null 2>&1; then
echo "Error: Docker is not available."
exit 1
fi
docker run --rm -u $(id -u) -v "$PWD":/work discord-screenaudio-buildenv bash /work/scripts/build-appimage.sh
exit 0
fi
tmpdir="$(mktemp -d)"
builddir="$tmpdir/build"
appdir="$tmpdir/AppDir"
export CMAKE_GENERATOR="Ninja"
cmake -B "$builddir" -S "$PWD"
cmake --build "$builddir" --config Release
DESTDIR="$appdir" cmake --install "$builddir" --prefix "/usr"
# Include libnss related files
mkdir -p "$appdir/AppDir/usr/lib/"
cp -rv "/usr/lib/x86_64-linux-gnu/nss" "$appdir/usr/lib/"
VERSION="$(cat version)" linuxdeploy \
--appdir "$appdir" \
--icon-file "assets/discord.png" \
--plugin qt \
--exclude-library "libpipewire-0.3.so.0" \
--output appimage

View File

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

37
scripts/prepare-container.sh Executable file
View File

@@ -0,0 +1,37 @@
#!/bin/sh
set -e
cd "$(dirname "$0")"
if [ ! -f /.dockerenv ]; then
# Check if docker is available
if ! command -v docker >/dev/null 2>&1; then
echo "Error: Docker is not available."
exit 1
fi
docker build -t discord-screenaudio-buildenv .
exit 0
fi
echo "-> Installing dependencies with apt..."
export DEBIAN_FRONTEND=noninteractive
apt-get update
apt-get install -y curl build-essential qtbase5-dev qtwebengine5-dev qt5-qmake cmake ninja-build pkg-config git libpipewire-0.3-dev file
echo "-> Installing linuxdeploy..."
tmpdir="$(mktemp -d)"
install_appimage() {
curl -Lo "$tmpdir/$1.AppImage" "$2"
chmod +x "$tmpdir/$1.AppImage"
sed '0,/AI\x02/{s|AI\x02|\x00\x00\x00|}' -i -i "$tmpdir/$1.AppImage"
(cd "$tmpdir" && ./$1.AppImage --appimage-extract)
mv -v "$tmpdir/squashfs-root" "/opt/$1"
ln -sv "/opt/$1/AppRun" "/usr/local/bin/$1"
}
install_appimage "linuxdeploy" "https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-x86_64.AppImage"
install_appimage "linuxdeploy-plugin-qt" "https://github.com/linuxdeploy/linuxdeploy-plugin-qt/releases/download/continuous/linuxdeploy-plugin-qt-x86_64.AppImage"
echo "-> Cleaning up..."
rm -rf "$tmpdir"
rm /tmp/prepare-container.sh
apt-get clean

View File

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

View File

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

View File

@@ -1,15 +1,9 @@
#include "discordpage.h" #include "discordpage.h"
#include "log.h"
#include "mainwindow.h"
#include "virtmic.h" #include "virtmic.h"
#include <QApplication> #include <QApplication>
#include <QDesktopServices> #include <QDesktopServices>
#include <QFile> #include <QFile>
#include <QFileInfo>
#include <QMessageBox>
#include <QNetworkReply>
#include <QTemporaryFile>
#include <QTimer> #include <QTimer>
#include <QWebChannel> #include <QWebChannel>
#include <QWebEngineScript> #include <QWebEngineScript>
@@ -17,31 +11,12 @@
#include <QWebEngineSettings> #include <QWebEngineSettings>
DiscordPage::DiscordPage(QWidget *parent) : QWebEnginePage(parent) { DiscordPage::DiscordPage(QWidget *parent) : QWebEnginePage(parent) {
setBackgroundColor(QColor("#313338")); setBackgroundColor(QColor("#202225"));
m_virtmicProcess.setProcessChannelMode(QProcess::ForwardedChannels);
connect(this, &QWebEnginePage::featurePermissionRequested, this, connect(this, &QWebEnginePage::featurePermissionRequested, this,
&DiscordPage::featurePermissionRequested); &DiscordPage::featurePermissionRequested);
connect(this, &DiscordPage::fullScreenRequested, MainWindow::instance(),
&MainWindow::fullScreenRequested);
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::ScreenCaptureEnabled, true);
settings()->setAttribute(QWebEngineSettings::JavascriptCanOpenWindows, true); settings()->setAttribute(QWebEngineSettings::JavascriptCanOpenWindows, true);
settings()->setAttribute(QWebEngineSettings::AllowRunningInsecureContent, settings()->setAttribute(QWebEngineSettings::AllowRunningInsecureContent,
@@ -52,125 +27,35 @@ void DiscordPage::setupPermissions() {
settings()->setAttribute(QWebEngineSettings::PlaybackRequiresUserGesture, settings()->setAttribute(QWebEngineSettings::PlaybackRequiresUserGesture,
false); false);
settings()->setAttribute(QWebEngineSettings::JavascriptCanOpenWindows, false); settings()->setAttribute(QWebEngineSettings::JavascriptCanOpenWindows, false);
settings()->setAttribute(QWebEngineSettings::ScrollAnimatorEnabled, true);
setUrl(QUrl("https://discord.com/app"));
injectScript(":/assets/userscript.js");
connect(&m_streamDialog, &StreamDialog::requestedStreamStart, this,
&DiscordPage::startStream);
} }
void DiscordPage::setupUserStyles() { void DiscordPage::injectScript(QString source) {
qDebug(userstylesLog).noquote() qDebug() << "[main ] Injecting" << source;
<< "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( QFile userscript(source);
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() { if (!userscript.open(QIODevice::ReadOnly)) {
m_userScript.setProperty( qFatal("Failed to load %s with error: %s", source.toLatin1().constData(),
"loadingMessage", "Loading userstyles: Fetching additional resources..."); userscript.errorString().toLatin1().constData());
bool foundImport = true; } else {
auto match = importRegex.match(m_userStylesContent); QByteArray userscriptJs = userscript.readAll();
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; QWebEngineScript script;
script.setSourceCode(content); script.setSourceCode(userscriptJs);
script.setName(name); script.setName("userscript.js");
script.setWorldId(QWebEngineScript::MainWorld); script.setWorldId(QWebEngineScript::MainWorld);
script.setInjectionPoint(injectionPoint); script.setInjectionPoint(QWebEngineScript::DocumentCreation);
script.setRunsOnSubFrames(false); script.setRunsOnSubFrames(false);
scripts().insert(script); scripts().insert(script);
}
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 {
(this->*inject)(name, file.readAll());
} }
} }
@@ -181,10 +66,11 @@ void DiscordPage::featurePermissionRequested(const QUrl &securityOrigin,
QWebEnginePage::PermissionGrantedByUser); QWebEnginePage::PermissionGrantedByUser);
if (feature == QWebEnginePage::Feature::MediaAudioCapture) { if (feature == QWebEnginePage::Feature::MediaAudioCapture) {
if (!m_userScript.isVirtmicRunning()) { if (m_virtmicProcess.state() == QProcess::NotRunning) {
qDebug(virtmicLog) << "Starting Virtmic with no target to make sure " qDebug() << "[virtmic] Starting Virtmic with no target to make sure "
"Discord can find all the audio devices"; "Discord can find all the audio devices";
m_userScript.startVirtmic("None"); m_virtmicProcess.start(QApplication::arguments()[0],
{"--virtmic", "None"});
} }
} }
} }
@@ -192,10 +78,10 @@ void DiscordPage::featurePermissionRequested(const QUrl &securityOrigin,
bool DiscordPage::acceptNavigationRequest(const QUrl &url, bool DiscordPage::acceptNavigationRequest(const QUrl &url,
QWebEnginePage::NavigationType type, QWebEnginePage::NavigationType type,
bool isMainFrame) { bool isMainFrame) {
if (type == QWebEnginePage::NavigationTypeLinkClicked) { // if (type == QWebEnginePage::NavigationTypeLinkClicked) {
QDesktopServices::openUrl(url); // QDesktopServices::openUrl(url);
return false; // return false;
} // }
return true; return true;
}; };
@@ -211,101 +97,46 @@ QWebEnginePage *DiscordPage::createWindow(QWebEnginePage::WebWindowType type) {
return new ExternalPage; return new ExternalPage;
} }
const QMap<QString, QString> cssAnsiColorMap = {{"black", "30"}, void DiscordPage::stopVirtmic() {
{"red", "31"}, if (m_virtmicProcess.state() == QProcess::Running) {
{"green", "32"}, qDebug() << "[virtmic] Stopping Virtmic";
{"yellow", "33"}, m_virtmicProcess.kill();
{"blue", "34"}, m_virtmicProcess.waitForFinished();
{"magenta", "35"}, }
{"cyan", "36"}, }
{"white", "37"},
{"gray", "90"}, void DiscordPage::startVirtmic(QString target) {
{"bright-red", "91"}, if (target != "None") {
{"bright-green", "92"}, qDebug() << "[virtmic] Starting Virtmic with target" << target;
{"bright-yellow", "93"}, m_virtmicProcess.start(QApplication::arguments()[0], {"--virtmic", target});
{"bright-blue", "94"}, }
{"bright-magenta", "95"}, }
{"bright-cyan", "96"},
{"bright-white", "97"},
{"orange", "38;5;208"},
{"pink", "38;5;205"},
{"brown", "38;5;94"},
{"light-gray", "38;5;251"},
{"dark-gray", "38;5;239"},
{"light-red", "38;5;203"},
{"light-green", "38;5;83"},
{"light-yellow", "38;5;227"},
{"light-blue", "38;5;75"},
{"light-magenta", "38;5;207"},
{"light-cyan", "38;5;87"},
{"turquoise", "38;5;80"},
{"violet", "38;5;92"},
{"purple", "38;5;127"},
{"lavender", "38;5;183"},
{"maroon", "38;5;124"},
{"beige", "38;5;230"},
{"olive", "38;5;142"},
{"indigo", "38;5;54"},
{"teal", "38;5;30"},
{"gold", "38;5;220"},
{"silver", "38;5;7"},
{"navy", "38;5;17"},
{"steel", "38;5;188"},
{"salmon", "38;5;173"},
{"peach", "38;5;217"},
{"khaki", "38;5;179"},
{"coral", "38;5;209"},
{"crimson", "38;5;160"}};
void DiscordPage::javaScriptConsoleMessage( void DiscordPage::javaScriptConsoleMessage(
QWebEnginePage::JavaScriptConsoleMessageLevel level, const QString &message, QWebEnginePage::JavaScriptConsoleMessageLevel level, const QString &message,
int lineNumber, const QString &sourceID) { int lineNumber, const QString &sourceID) {
auto colorSegments = message.split("%c"); if (message == "!discord-screenaudio-start-stream") {
if (colorSegments[0] != "") { if (m_streamDialog.isHidden())
for (auto line : colorSegments[0].split("\n")) m_streamDialog.setHidden(false);
qDebug(discordLog) << line.toUtf8().constData(); else
} m_streamDialog.activateWindow();
for (auto segment : colorSegments.mid(1)) { m_streamDialog.updateTargets();
auto lines = segment.split("\n"); } else if (message == "!discord-screenaudio-stream-stopped") {
QString ansi; stopVirtmic();
uint endOfStyles = lines.length(); } else {
for (auto line = 1; line < lines.length(); line++) { qDebug() << "[discord]" << message;
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();
}
} }
} }
UserScript *DiscordPage::userScript() { return &m_userScript; } void DiscordPage::startStream(QString target, uint width, uint height,
uint frameRate) {
void DiscordPage::setupArrpc() { stopVirtmic();
QFile nodejs("/usr/bin/node"); startVirtmic(target);
if (nodejs.exists()) { // Wait a bit for the virtmic to start
auto arrpcSource = QTemporaryFile::createNativeFile(":/assets/arrpc.js"); QTimer::singleShot(target == "None" ? 0 : 200, [=]() {
qDebug(mainLog).noquote() runJavaScript(QString("window.discordScreenaudioStartStream(%1, %2, %3);")
<< "NodeJS found, starting arRPC located at" << arrpcSource->fileName(); .arg(width)
m_arrpcProcess.setProcessChannelMode(QProcess::ForwardedChannels); .arg(height)
m_arrpcProcess.setProgram(nodejs.fileName()); .arg(frameRate));
m_arrpcProcess.setArguments(QStringList{arrpcSource->fileName()}); });
m_arrpcProcess.start();
injectFile(&DiscordPage::injectScript, "arrpc_bridge_mod.js",
":/assets/arrpc_bridge_mod.js");
}
} }

View File

@@ -1,35 +1,21 @@
#pragma once #pragma once
#include "userscript.h" #include "streamdialog.h"
#include "virtmic.h"
#include <QDir>
#include <QFile>
#include <QNetworkAccessManager>
#include <QProcess> #include <QProcess>
#include <QStandardPaths>
#include <QWebEngineFullScreenRequest> #include <QWebEngineFullScreenRequest>
#include <QWebEnginePage> #include <QWebEnginePage>
#include <QWebEngineScript>
class DiscordPage : public QWebEnginePage { class DiscordPage : public QWebEnginePage {
Q_OBJECT Q_OBJECT
public: public:
explicit DiscordPage(QWidget *parent = nullptr); explicit DiscordPage(QWidget *parent = nullptr);
UserScript *userScript();
private: private:
UserScript m_userScript; StreamDialog m_streamDialog;
QFile *m_userStylesFile; QProcess m_virtmicProcess;
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, bool acceptNavigationRequest(const QUrl &url,
QWebEnginePage::NavigationType type, QWebEnginePage::NavigationType type,
bool isMainFrame) override; bool isMainFrame) override;
@@ -38,19 +24,14 @@ private:
javaScriptConsoleMessage(QWebEnginePage::JavaScriptConsoleMessageLevel level, javaScriptConsoleMessage(QWebEnginePage::JavaScriptConsoleMessageLevel level,
const QString &message, int lineNumber, const QString &message, int lineNumber,
const QString &sourceID) override; const QString &sourceID) override;
void injectScript(QString name, QString content, void injectScript(QString source);
QWebEngineScript::InjectionPoint injectionPoint); void stopVirtmic();
void injectScript(QString name, QString content); void startVirtmic(QString target);
void injectStylesheet(QString name, QString content);
void injectFile(void (DiscordPage::*inject)(QString, QString), QString name,
QString source);
private Q_SLOTS: private Q_SLOTS:
void featurePermissionRequested(const QUrl &securityOrigin, void featurePermissionRequested(const QUrl &securityOrigin,
QWebEnginePage::Feature feature); QWebEnginePage::Feature feature);
void startStream(QString target, uint width, uint height, uint frameRate);
public Q_SLOTS:
void getUserStyles(QString url);
}; };
// Will immediately get destroyed again but is needed for navigation to // Will immediately get destroyed again but is needed for navigation to

View File

@@ -1,8 +0,0 @@
#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");
Q_LOGGING_CATEGORY(userstylesLog, "userstyles");

View File

@@ -1,10 +0,0 @@
#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);
Q_DECLARE_LOGGING_CATEGORY(userstylesLog);

View File

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

View File

@@ -1,10 +1,6 @@
#include "mainwindow.h" #include "mainwindow.h"
#include "virtmic.h" #include "virtmic.h"
#ifdef KNOTIFICATIONS
#include <KNotification>
#endif
#include <QApplication> #include <QApplication>
#include <QColor> #include <QColor>
#include <QComboBox> #include <QComboBox>
@@ -14,33 +10,27 @@
#include <QPushButton> #include <QPushButton>
#include <QSpacerItem> #include <QSpacerItem>
#include <QThread> #include <QThread>
#include <QTimer>
#include <QUrl> #include <QUrl>
#include <QWebEngineFullScreenRequest> #include <QWebEngineScript>
#include <QWebEngineScriptCollection>
#include <QWebEngineSettings>
#include <QWidget> #include <QWidget>
MainWindow *MainWindow::m_instance = nullptr; MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) {
setupWebView();
MainWindow::MainWindow(bool useNotifySend, QWidget *parent)
: QMainWindow(parent) {
assert(MainWindow::m_instance == nullptr);
MainWindow::m_instance = this;
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); resize(1000, 700);
showMaximized(); showMaximized();
} }
if (m_settings->value("trayIcon", false).toBool() &&
m_settings->value("startHidden", false).toBool()) { void MainWindow::setupWebView() {
hide(); auto page = new DiscordPage(this);
QTimer::singleShot(0, [=]() { hide(); }); connect(page, &QWebEnginePage::fullScreenRequested, this,
} &MainWindow::fullScreenRequested);
m_webView = new QWebEngineView(this);
m_webView->setPage(page);
setCentralWidget(m_webView);
} }
void MainWindow::fullScreenRequested( void MainWindow::fullScreenRequested(
@@ -54,79 +44,4 @@ void MainWindow::fullScreenRequested(
} }
} }
void MainWindow::setupTrayIcon() { void MainWindow::closeEvent(QCloseEvent *event) { QApplication::quit(); }
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;
};

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,83 +0,0 @@
#pragma once
#include "streamdialog.h"
#include <QObject>
#include <QProcess>
#ifdef KXMLGUI
#include <KAboutData>
#include <KHelpMenu>
#include <KShortcutsDialog>
#include <KXmlGuiWindow>
#include <QAction>
#ifdef KGLOBALACCEL
#include <KGlobalAccel>
#endif
#endif
#ifdef KNOTIFICATIONS
#include <KNotification>
#endif
class UserScript : public QObject {
Q_OBJECT
public:
UserScript();
bool isVirtmicRunning();
Q_PROPERTY(QString version READ version CONSTANT);
Q_PROPERTY(bool kxmlgui MEMBER m_kxmlgui CONSTANT);
Q_PROPERTY(bool kglobalaccel MEMBER m_kglobalaccel CONSTANT);
Q_PROPERTY(QString userstyles MEMBER m_userstyles NOTIFY userstylesChanged);
Q_PROPERTY(QString loadingMessage MEMBER m_loadingMessage NOTIFY
loadingMessageChanged);
private:
QProcess m_virtmicProcess;
StreamDialog *m_streamDialog;
bool m_kxmlgui = false;
bool m_kglobalaccel = false;
QString m_userstyles;
QString m_loadingMessage;
#ifdef KXMLGUI
KHelpMenu *m_helpMenu;
#ifdef KGLOBALACCEL
KActionCollection *m_actionCollection;
KShortcutsDialog *m_shortcutsDialog;
#endif
#endif
void setupHelpMenu();
void setupShortcutsDialog();
void setupStreamDialog();
void setupVirtmic();
Q_SIGNALS:
void muteToggled();
void deafenToggled();
void streamStarted(bool video, int width, int height, int frameRate);
void userstylesChanged();
void loadingMessageChanged(QString message);
void shouldInstallUserStyles(QString url);
public Q_SLOTS:
void log(QString message);
QString version();
QVariant getPref(QString name, QVariant fallback);
bool getBoolPref(QString name, bool fallback);
void setPref(QString name, QVariant value);
void setTrayIcon(bool value);
void showShortcutsDialog();
void showHelpMenu();
void showStreamDialog();
void stopVirtmic();
void startVirtmic(QString target);
void showThemeDialog();
void installUserStyles(QString url);
private Q_SLOTS:
void startStream(bool video, bool audio, int width, int height, int frameRate,
QString target);
};

View File

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

View File

@@ -1,63 +1,12 @@
#pragma once #pragma once
#include <cr/channel.hpp> #include <QString>
#include <QVector>
#include <iostream> #include <iostream>
#include <rohrkabel/channel/channel.hpp>
#include <rohrkabel/main_loop.hpp>
#include <rohrkabel/registry/registry.hpp>
#include <QMap> namespace Virtmic {
#include <QScopedPointer>
#include <QStringList>
#include <QThread>
class Virtmic : public QObject { QVector<QString> getTargets();
Q_OBJECT void start(QString _target);
public:
static void setTarget(QString target);
static void getTargets();
public: } // namespace Virtmic
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
version Normal file
View File

@@ -0,0 +1 @@
v1.0.0-rc.7

View File

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