Compare commits
	
		
			69 Commits
		
	
	
		
			v1.0.0-rc.
			...
			v1.7.0
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | 02fdc62fbe | ||
|  | d9e3704176 | ||
|  | 36c8cfedb7 | ||
|  | c52daab420 | ||
|  | 8c4fae3410 | ||
|  | ac71e9bbcb | ||
|  | fc0dbb5b34 | ||
|  | 5df24629e6 | ||
|  | 1477a9d4c0 | ||
|  | 0b12487dfd | ||
|  | a10cfda56d | ||
|  | 2a809e163e | ||
|  | bc317d5531 | ||
|  | b4db987217 | ||
|  | 9f46e710a9 | ||
|  | d6641a7a6e | ||
|  | b836be6530 | ||
|  | bfb0714b13 | ||
|  | 374b854261 | ||
|  | 1f6105f76b | ||
|  | 3071159332 | ||
|  | f2de080e1b | ||
|  | f4a60f281d | ||
|  | d693535d53 | ||
|  | e41af697f7 | ||
|  | b0a8815bb8 | ||
|  | 150fd4364e | ||
|  | a0a2924796 | ||
|  | 3c48621427 | ||
|  | 9faabe1f3e | ||
|  | 68473d04d8 | ||
|  | 7c8f72b8d8 | ||
|  | 27cdd9f9a5 | ||
|  | d184823ee9 | ||
|  | dd2beed4eb | ||
|  | 3740553aba | ||
|  | 555f5c62f5 | ||
|  | af3c62c263 | ||
|  | c4f6c33f9d | ||
|  | 758ffa8cb5 | ||
|  | 753da8b1d3 | ||
|  | 5f37ab314f | ||
|  | c7bff3f428 | ||
|  | 34e23f7187 | ||
|  | c325dd6798 | ||
|  | b42fde85d1 | ||
|  | cc379ada85 | ||
|  | fdb6145bbd | ||
|  | 8bd4d1896a | ||
|  | 1a2d09fe3d | ||
|  | f01c941c90 | ||
|  | 95d391e869 | ||
|  | af107168db | ||
|  | e7e6c5d82b | ||
|  | ee0c66762d | ||
|  | c3483a42db | ||
|  | d68797cebb | ||
|  | ef221d4574 | ||
|  | 3fa2e3fa3c | ||
|  | bda1bc0df6 | ||
|  | cb0f084d42 | ||
|  | 674ac146f2 | ||
|  | 1ab00ee774 | ||
|  | 38bcc30ed4 | ||
|  | 204439af94 | ||
|  | 8fc529102c | ||
|  | a7a8346b63 | ||
|  | 88b129f863 | ||
|  | f4fc2e1f19 | 
							
								
								
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -1 +1,2 @@ | ||||
| /build | ||||
| .vscode | ||||
|   | ||||
| @@ -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,8 @@ set(discord-screenaudio_SRC | ||||
|   src/virtmic.cpp | ||||
|   src/discordpage.cpp | ||||
|   src/streamdialog.cpp | ||||
|   src/log.cpp | ||||
|   src/userscript.cpp | ||||
|   resources.qrc | ||||
| ) | ||||
|  | ||||
| @@ -45,9 +70,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/discord.png DESTINATION ${CMAKE_INSTALL_PREFIX}/share/pixmaps) | ||||
| install(PROGRAMS assets/discord-screenaudio.desktop DESTINATION ${CMAKE_INSTALL_PREFIX}/share/applications) | ||||
| install(PROGRAMS assets/de.shorsh.discord-screenaudio.metainfo.xml DESTINATION ${CMAKE_INSTALL_PREFIX}/share/metainfo) | ||||
| 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) | ||||
| 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) | ||||
|   | ||||
							
								
								
									
										60
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										60
									
								
								README.md
									
									
									
									
									
								
							| @@ -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). | ||||
|  | ||||
|  | ||||
| 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)). | ||||
|  | ||||
|  | ||||
|  | ||||
| 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 | ||||
|  | ||||
| @@ -63,7 +84,9 @@ And then to optionally install it, run: | ||||
| sudo cmake --install build | ||||
| ``` | ||||
|  | ||||
| ## How it works | ||||
| ## FAQ | ||||
|  | ||||
| ### How does this work? | ||||
|  | ||||
| This whole project is based on | ||||
| [this](https://github.com/edisionnano/Screenshare-with-audio-on-Discord-with-Linux) | ||||
| @@ -72,6 +95,21 @@ Discord. Basically: a virtual microphone is created which captures the | ||||
| application audio, and this microphone is then fed to the Discord stream by | ||||
| intercepting a API call of Discord. | ||||
|  | ||||
| ### Drag and drop doesn't work in the Flatpak | ||||
|  | ||||
| This is due to sandboxing limitations of Flatpak. The main Discord Flatpak has | ||||
| the same problem. If you still want to use drag and drop, you can disable most | ||||
| of Flatpak's sandboxing by installing | ||||
| [Flatseal](https://flathub.org/apps/details/com.github.tchx84.Flatseal) and | ||||
| allowing access to "All system files" under the "Filesystem" section. | ||||
|  | ||||
| ### Is there any way to add custom CSS / a theme? | ||||
|  | ||||
| Yes, you can add all your styles into | ||||
| `~/.config/discord-screenaudio/userstyles.css`. But please note that due to | ||||
| QtWebEngine limitations concerning content security policies, you can't use any | ||||
| external files (like `@import` or `url()`). | ||||
|  | ||||
| ## License | ||||
|  | ||||
| Copyright (C) 2022 Malte Jürgens | ||||
|   | ||||
							
								
								
									
										8
									
								
								assets/de.shorsh.discord-screenaudio.desktop
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								assets/de.shorsh.discord-screenaudio.desktop
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,8 @@ | ||||
| [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 | ||||
| @@ -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.9" /> | ||||
|   </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> | ||||
							
								
								
									
										51
									
								
								assets/de.shorsh.discord-screenaudio.metainfo.xml.in
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										51
									
								
								assets/de.shorsh.discord-screenaudio.metainfo.xml.in
									
									
									
									
									
										Normal 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> | ||||
							
								
								
									
										
											BIN
										
									
								
								assets/de.shorsh.discord-screenaudio.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								assets/de.shorsh.discord-screenaudio.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 48 KiB | 
| @@ -1,6 +0,0 @@ | ||||
| [Desktop Entry] | ||||
| Type=Application | ||||
| Name=discord-screenaudio | ||||
| Exec=discord-screenaudio | ||||
| Icon=discord | ||||
| Terminal=false | ||||
							
								
								
									
										8
									
								
								assets/discord-screenaudio.notifyrc
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								assets/discord-screenaudio.notifyrc
									
									
									
									
									
										Normal 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 | ||||
										
											Binary file not shown.
										
									
								
							| Before Width: | Height: | Size: 6.8 KiB | 
| @@ -1,18 +1,29 @@ | ||||
| // From v0.4 | ||||
|  | ||||
| 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) | ||||
|       userscript.log( | ||||
|         `Did not find '${nameOfAudioDevice}', trying again in 100ms` | ||||
|       ); | ||||
|     await sleep(100); | ||||
|   } | ||||
|   userscript.log(`Found '${nameOfAudioDevice}'`); | ||||
|   return audioDevice; | ||||
| }; | ||||
|  | ||||
| function setGetDisplayMedia(overrideArgs = undefined) { | ||||
| function setGetDisplayMedia(video = true, overrideArgs = undefined) { | ||||
|   const getDisplayMedia = async (...args) => { | ||||
|     var id; | ||||
|     try { | ||||
| @@ -50,6 +61,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; | ||||
| @@ -57,84 +69,275 @@ function setGetDisplayMedia(overrideArgs = undefined) { | ||||
|  | ||||
| setGetDisplayMedia(); | ||||
|  | ||||
| let userscript; | ||||
| let muteBtn; | ||||
| let deafenBtn; | ||||
| let streamStartBtn; | ||||
| let streamStartBtnInitialDisplay; | ||||
| let streamStartBtnClone; | ||||
| let resolutionString; | ||||
| const clonedElements = []; | ||||
| const hiddenElements = []; | ||||
| let wasStreamActive = false; | ||||
|  | ||||
| setInterval(() => { | ||||
|   const streamActive = | ||||
|     document.getElementsByClassName("panel-2ZFCRb activityPanel-9icbyU") | ||||
|       .length > 0; | ||||
| function createButton(text, onClick) { | ||||
|   const button = document.createElement("button"); | ||||
|   button.style.marginBottom = "20px"; | ||||
|   button.classList = | ||||
|     "button-ejjZWC lookFilled-1H2Jvj colorBrand-2M3O3N sizeSmall-3R2P2p grow-2T4nbg"; | ||||
|   button.innerText = text; | ||||
|   button.addEventListener("click", onClick); | ||||
|   return button; | ||||
| } | ||||
|  | ||||
|   if (!streamActive && wasStreamActive) | ||||
|     console.log("!discord-screenaudio-stream-stopped"); | ||||
|   wasStreamActive = streamActive; | ||||
| function createSwitch(text, enabled, onClick) { | ||||
|   const container = document.createElement("div"); | ||||
|   container.style.marginBottom = "20px"; | ||||
|   container.className = "labelRow-NnoUIp"; | ||||
|  | ||||
|   if (streamActive) { | ||||
|     clonedElements.forEach((el) => { | ||||
|       el.remove(); | ||||
|     }); | ||||
|     clonedElements.length = 0; | ||||
|   const label = document.createElement("label"); | ||||
|   label.innerText = text; | ||||
|   label.className = "title-2yADjX"; | ||||
|   container.appendChild(label); | ||||
|  | ||||
|     hiddenElements.forEach((el) => { | ||||
|       el.style.display = "block"; | ||||
|     }); | ||||
|     hiddenElements.length = 0; | ||||
|   } else { | ||||
|     for (const el of [ | ||||
|       document.getElementsByClassName("actionButtons-2vEOUh")?.[0]?.children[1], | ||||
|       document.querySelector( | ||||
|         ".wrapper-3t3Yqv > div > div > div > div > .controlButton-2PMNom" | ||||
|       ), | ||||
|     ]) { | ||||
|       if (!el) continue; | ||||
|       if (el.classList.contains("discord-screenaudio-cloned")) continue; | ||||
|       el.classList.add("discord-screenaudio-cloned"); | ||||
|       elClone = el.cloneNode(true); | ||||
|       elClone.title = "Share Your Screen with Audio"; | ||||
|       elClone.addEventListener("click", () => { | ||||
|         console.log("!discord-screenaudio-start-stream"); | ||||
|       }); | ||||
|   const svg = document.createElement("div"); | ||||
|   container.appendChild(svg); | ||||
|  | ||||
|       const initialDisplay = el.style.display; | ||||
|  | ||||
|       window.discordScreenaudioStartStream = (width, height, frameRate) => { | ||||
|         setGetDisplayMedia({ | ||||
|           audio: true, | ||||
|           video: { width, height, frameRate }, | ||||
|         }); | ||||
|         el.click(); | ||||
|         el.style.display = initialDisplay; | ||||
|         elClone.remove(); | ||||
|       }; | ||||
|  | ||||
|       el.style.display = "none"; | ||||
|       el.parentNode.insertBefore(elClone, el); | ||||
|  | ||||
|       clonedElements.push(elClone); | ||||
|       hiddenElements.push(el); | ||||
|     } | ||||
|   function setSvgDisabled() { | ||||
|     svg.innerHTML = `<div class="container-1QtPKm default-colors" style="opacity: 1; background-color: rgb(114, 118, 125);"><svg class="slider-HJFN2i" viewBox="0 0 28 20" preserveAspectRatio="xMinYMid meet" aria-hidden="true" style="left: -3px;"><rect fill="white" x="4" y="0" height="20" width="20" rx="10"></rect><svg viewBox="0 0 20 20" fill="none"><path fill="rgba(114, 118, 125, 1)" d="M5.13231 6.72963L6.7233 5.13864L14.855 13.2704L13.264 14.8614L5.13231 6.72963Z"></path><path fill="rgba(114, 118, 125, 1)" d="M13.2704 5.13864L14.8614 6.72963L6.72963 14.8614L5.13864 13.2704L13.2704 5.13864Z"></path></svg></svg><input id="uid_84" type="checkbox" class="input-125oad" tabindex="0"></div>`; | ||||
|   } | ||||
|  | ||||
|   // Add about text in settings | ||||
|   if ( | ||||
|     document.getElementsByClassName("dirscordScreenaudioAboutText").length == 0 | ||||
|   ) { | ||||
|     for (const el of document.getElementsByClassName("info-3pQQBb")) { | ||||
|       const aboutEl = document.createElement("div"); | ||||
|       aboutEl.innerText = "discord-screenaudio v1.0.0-rc.9"; | ||||
|       aboutEl.style.fontSize = "12px"; | ||||
|       aboutEl.style.color = "var(--text-muted)"; | ||||
|       aboutEl.classList.add("dirscordScreenaudioAboutText"); | ||||
|       el.appendChild(aboutEl); | ||||
|     } | ||||
|   function setSvgEnabled() { | ||||
|     svg.innerHTML = `<div class="container-1QtPKm default-colors checked-16gMAN" style="opacity: 1; background-color: rgb(59, 165, 92);"><svg class="slider-HJFN2i" viewBox="0 0 28 20" preserveAspectRatio="xMinYMid meet" aria-hidden="true" style="left: 12px;"><rect fill="white" x="4" y="0" height="20" width="20" rx="10"></rect><svg viewBox="0 0 20 20" fill="none"><path fill="rgba(59, 165, 92, 1)" d="M7.89561 14.8538L6.30462 13.2629L14.3099 5.25755L15.9009 6.84854L7.89561 14.8538Z"></path><path fill="rgba(59, 165, 92, 1)" d="M4.08643 11.0903L5.67742 9.49929L9.4485 13.2704L7.85751 14.8614L4.08643 11.0903Z"></path></svg></svg><input id="uid_74" type="checkbox" class="input-125oad" tabindex="0" checked=""></div>`; | ||||
|   } | ||||
|  | ||||
|   // Remove stream settings if stream is active | ||||
|   document.getElementById("manage-streams-change-windows")?.remove(); | ||||
|   document.querySelector(`[aria-label="Stream Settings"]`)?.remove(); | ||||
| }, 500); | ||||
|   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(() => { | ||||
|     muteBtn && muteBtn.click(); | ||||
|   }); | ||||
|  | ||||
|   userscript.deafenToggled.connect(() => { | ||||
|     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(); | ||||
|   }); | ||||
|  | ||||
|   setInterval(async () => { | ||||
|     const streamActive = | ||||
|       document.getElementsByClassName("panel-2ZFCRb activityPanel-9icbyU") | ||||
|         .length > 0; | ||||
|  | ||||
|     if (!streamActive && wasStreamActive) userscript.stopVirtmic(); | ||||
|     wasStreamActive = streamActive; | ||||
|  | ||||
|     if (streamActive) { | ||||
|       clonedElements.forEach((el) => { | ||||
|         el.remove(); | ||||
|       }); | ||||
|       clonedElements.length = 0; | ||||
|  | ||||
|       hiddenElements.forEach((el) => { | ||||
|         el.style.display = "block"; | ||||
|       }); | ||||
|       hiddenElements.length = 0; | ||||
|     } else { | ||||
|       for (const el of [ | ||||
|         document.getElementsByClassName("actionButtons-2vEOUh")?.[0] | ||||
|           ?.children[1], | ||||
|         document.querySelector( | ||||
|           ".wrapper-3t3Yqv > div > div > div > div > .controlButton-2PMNom" | ||||
|         ), | ||||
|       ]) { | ||||
|         if (!el) continue; | ||||
|         if (el.classList.contains("discord-screenaudio-cloned")) continue; | ||||
|         streamStartBtn = el; | ||||
|         streamStartBtn.classList.add("discord-screenaudio-cloned"); | ||||
|  | ||||
|         streamStartBtnClone = streamStartBtn.cloneNode(true); | ||||
|         streamStartBtnClone.title = "Share Your Screen with Audio"; | ||||
|         streamStartBtnClone.addEventListener("click", () => { | ||||
|           userscript.showStreamDialog(); | ||||
|         }); | ||||
|  | ||||
|         streamStartBtnInitialDisplay = streamStartBtn.style.display; | ||||
|  | ||||
|         streamStartBtn.style.display = "none"; | ||||
|         streamStartBtn.parentNode.insertBefore(streamStartBtnClone, el); | ||||
|  | ||||
|         clonedElements.push(streamStartBtnClone); | ||||
|         hiddenElements.push(streamStartBtn); | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     // Add about text in settings | ||||
|     if ( | ||||
|       document.getElementsByClassName("dirscordScreenaudioAboutText").length == | ||||
|       0 | ||||
|     ) { | ||||
|       for (const el of document.getElementsByClassName("info-3pQQBb")) { | ||||
|         let aboutEl; | ||||
|         if (userscript.kxmlgui) { | ||||
|           aboutEl = document.createElement("a"); | ||||
|           aboutEl.addEventListener("click", () => { | ||||
|             userscript.showHelpMenu(); | ||||
|           }); | ||||
|         } else { | ||||
|           aboutEl = document.createElement("div"); | ||||
|         } | ||||
|         aboutEl.innerText = `discord-screenaudio ${userscript.version}`; | ||||
|         aboutEl.style.fontSize = "12px"; | ||||
|         aboutEl.style.color = "var(--text-muted)"; | ||||
|         aboutEl.style.textTransform = "none"; | ||||
|         aboutEl.style.display = "inline-block"; | ||||
|         aboutEl.style.width = "100%"; | ||||
|         aboutEl.classList.add("dirscordScreenaudioAboutText"); | ||||
|         aboutEl.style.cursor = "pointer"; | ||||
|         el.appendChild(aboutEl); | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     // Remove stream settings if stream is active | ||||
|     document.getElementById("manage-streams-change-windows")?.remove(); | ||||
|     document.querySelector(`[aria-label="Stream Settings"]`)?.remove(); | ||||
|  | ||||
|     // Add event listener for keybind tab | ||||
|     if ( | ||||
|       document | ||||
|         .getElementById("keybinds-tab") | ||||
|         ?.getElementsByClassName( | ||||
|           "container-3jbRo5 info-1hMolH browserNotice-1u-Y5o" | ||||
|         ).length | ||||
|     ) { | ||||
|       const el = document | ||||
|         .getElementById("keybinds-tab") | ||||
|         .getElementsByClassName("children-1xdcWE")[0]; | ||||
|       const div = document.createElement("div"); | ||||
|       div.style.marginBottom = "50px"; | ||||
|       div.appendChild( | ||||
|         createButton("Edit Global Keybinds", () => { | ||||
|           userscript.showShortcutsDialog(); | ||||
|         }) | ||||
|       ); | ||||
|       el.innerHTML = ""; | ||||
|       el.appendChild(div); | ||||
|     } | ||||
|  | ||||
|     const buttonContainer = | ||||
|       document.getElementsByClassName("container-YkUktl")[0]; | ||||
|     if (!buttonContainer) { | ||||
|       userscript.log( | ||||
|         "Cannot locate Mute/Deafen/Settings button container, please report this on GitHub" | ||||
|       ); | ||||
|     } | ||||
|  | ||||
|     muteBtn = buttonContainer | ||||
|       ? buttonContainer.getElementsByClassName( | ||||
|           "button-12Fmur enabled-9OeuTA button-f2h6uQ lookBlank-21BCro colorBrand-I6CyqQ grow-2sR_-F" | ||||
|         )[0] | ||||
|       : null; | ||||
|  | ||||
|     deafenBtn = buttonContainer | ||||
|       ? buttonContainer.getElementsByClassName( | ||||
|           "button-12Fmur enabled-9OeuTA button-f2h6uQ lookBlank-21BCro colorBrand-I6CyqQ grow-2sR_-F" | ||||
|         )[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( | ||||
|             createSwitch( | ||||
|               "Move discord-screenaudio to the system tray instead of closing", | ||||
|               await userscript.getBoolPref("trayIcon", false), | ||||
|               (enabled) => { | ||||
|                 userscript.setTrayIcon(enabled); | ||||
|               } | ||||
|             ) | ||||
|           ); | ||||
|  | ||||
|           section.appendChild( | ||||
|             createSwitch( | ||||
|               "Start discord-screenaudio hidden to tray", | ||||
|               await userscript.getPref("startHidden", false), | ||||
|               (hidden) => { | ||||
|                 userscript.setPref("startHidden", hidden); | ||||
|               } | ||||
|             ) | ||||
|           ); | ||||
|  | ||||
|           const divider = document.createElement("div"); | ||||
|           divider.className = "divider-3nqZNm marginTop40-Q4o1tS"; | ||||
|  | ||||
|           firstDivider.after(section); | ||||
|           section.after(divider); | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|   }, 500); | ||||
| } | ||||
|   | ||||
| @@ -2,5 +2,6 @@ | ||||
| <RCC> | ||||
|   <qresource> | ||||
|     <file>assets/userscript.js</file> | ||||
|     <file>assets/de.shorsh.discord-screenaudio.png</file> | ||||
|   </qresource> | ||||
| </RCC> | ||||
| @@ -1,9 +1,14 @@ | ||||
| #include "discordpage.h" | ||||
| #include "log.h" | ||||
| #include "mainwindow.h" | ||||
| #include "virtmic.h" | ||||
|  | ||||
| #include <QApplication> | ||||
| #include <QDesktopServices> | ||||
| #include <QFile> | ||||
| #include <QFileInfo> | ||||
| #include <QMessageBox> | ||||
| #include <QNetworkReply> | ||||
| #include <QTimer> | ||||
| #include <QWebChannel> | ||||
| #include <QWebEngineScript> | ||||
| @@ -12,11 +17,27 @@ | ||||
|  | ||||
| DiscordPage::DiscordPage(QWidget *parent) : QWebEnginePage(parent) { | ||||
|   setBackgroundColor(QColor("#202225")); | ||||
|   m_virtmicProcess.setProcessChannelMode(QProcess::ForwardedChannels); | ||||
|  | ||||
|   connect(this, &QWebEnginePage::featurePermissionRequested, this, | ||||
|           &DiscordPage::featurePermissionRequested); | ||||
|  | ||||
|   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(); | ||||
| } | ||||
|  | ||||
| void DiscordPage::setupPermissions() { | ||||
|   settings()->setAttribute(QWebEngineSettings::ScreenCaptureEnabled, true); | ||||
|   settings()->setAttribute(QWebEngineSettings::JavascriptCanOpenWindows, true); | ||||
|   settings()->setAttribute(QWebEngineSettings::AllowRunningInsecureContent, | ||||
| @@ -27,35 +48,60 @@ DiscordPage::DiscordPage(QWidget *parent) : QWebEnginePage(parent) { | ||||
|   settings()->setAttribute(QWebEngineSettings::PlaybackRequiresUserGesture, | ||||
|                            false); | ||||
|   settings()->setAttribute(QWebEngineSettings::JavascriptCanOpenWindows, false); | ||||
|  | ||||
|   setUrl(QUrl("https://discord.com/app")); | ||||
|  | ||||
|   injectScript(":/assets/userscript.js"); | ||||
|  | ||||
|   connect(&m_streamDialog, &StreamDialog::requestedStreamStart, this, | ||||
|           &DiscordPage::startStream); | ||||
|   settings()->setAttribute(QWebEngineSettings::ScrollAnimatorEnabled, true); | ||||
| } | ||||
|  | ||||
| void DiscordPage::injectScript(QString source) { | ||||
|   qDebug() << "[main   ] Injecting " << source; | ||||
| void DiscordPage::setupUserStyles() { | ||||
|   QString file = | ||||
|       QStandardPaths::writableLocation(QStandardPaths::AppConfigLocation) + | ||||
|       "/userstyles.css"; | ||||
|   if (QFileInfo(file).exists()) { | ||||
|     qDebug(mainLog) << "Found userstyles:" << file; | ||||
|     injectFile(&DiscordPage::injectStylesheet, "userstyles.js", file); | ||||
|   } | ||||
| } | ||||
|  | ||||
|   QFile userscript(source); | ||||
| void DiscordPage::injectScript( | ||||
|     QString name, QString content, | ||||
|     QWebEngineScript::InjectionPoint injectionPoint) { | ||||
|   qDebug(mainLog) << "Injecting " << name; | ||||
|  | ||||
|   if (!userscript.open(QIODevice::ReadOnly)) { | ||||
|   QWebEngineScript script; | ||||
|  | ||||
|   script.setSourceCode(content); | ||||
|   script.setName(name); | ||||
|   script.setWorldId(QWebEngineScript::MainWorld); | ||||
|   script.setInjectionPoint(injectionPoint); | ||||
|   script.setRunsOnSubFrames(false); | ||||
|  | ||||
|   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.type = "text/css"; | ||||
| 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(), | ||||
|            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); | ||||
|     (this->*inject)(name, file.readAll()); | ||||
|   } | ||||
| } | ||||
|  | ||||
| @@ -66,11 +112,10 @@ void DiscordPage::featurePermissionRequested(const QUrl &securityOrigin, | ||||
|                        QWebEnginePage::PermissionGrantedByUser); | ||||
|  | ||||
|   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"; | ||||
|       m_virtmicProcess.start(QApplication::arguments()[0], | ||||
|                              {"--virtmic", "None"}); | ||||
|     if (!m_userScript.isVirtmicRunning()) { | ||||
|       qDebug(virtmicLog) << "Starting Virtmic with no target to make sure " | ||||
|                             "Discord can find all the audio devices"; | ||||
|       m_userScript.startVirtmic("None"); | ||||
|     } | ||||
|   } | ||||
| } | ||||
| @@ -78,7 +123,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; | ||||
| @@ -98,46 +142,79 @@ QWebEnginePage *DiscordPage::createWindow(QWebEnginePage::WebWindowType type) { | ||||
|   return new ExternalPage; | ||||
| } | ||||
|  | ||||
| void DiscordPage::stopVirtmic() { | ||||
|   if (m_virtmicProcess.state() == QProcess::Running) { | ||||
|     qDebug() << "[virtmic] 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}); | ||||
|   } | ||||
| } | ||||
| const QMap<QString, QString> cssAnsiColorMap = {{"black", "30"}, | ||||
|                                                 {"red", "31"}, | ||||
|                                                 {"green", "32"}, | ||||
|                                                 {"yellow", "33"}, | ||||
|                                                 {"blue", "34"}, | ||||
|                                                 {"magenta", "35"}, | ||||
|                                                 {"cyan", "36"}, | ||||
|                                                 {"white", "37"}, | ||||
|                                                 {"gray", "90"}, | ||||
|                                                 {"bright-red", "91"}, | ||||
|                                                 {"bright-green", "92"}, | ||||
|                                                 {"bright-yellow", "93"}, | ||||
|                                                 {"bright-blue", "94"}, | ||||
|                                                 {"bright-magenta", "95"}, | ||||
|                                                 {"bright-cyan", "96"}, | ||||
|                                                 {"bright-white", "97"}, | ||||
|                                                 {"orange", "38;5;208"}, | ||||
|                                                 {"pink", "38;5;205"}, | ||||
|                                                 {"brown", "38;5;94"}, | ||||
|                                                 {"light-gray", "38;5;251"}, | ||||
|                                                 {"dark-gray", "38;5;239"}, | ||||
|                                                 {"light-red", "38;5;203"}, | ||||
|                                                 {"light-green", "38;5;83"}, | ||||
|                                                 {"light-yellow", "38;5;227"}, | ||||
|                                                 {"light-blue", "38;5;75"}, | ||||
|                                                 {"light-magenta", "38;5;207"}, | ||||
|                                                 {"light-cyan", "38;5;87"}, | ||||
|                                                 {"turquoise", "38;5;80"}, | ||||
|                                                 {"violet", "38;5;92"}, | ||||
|                                                 {"purple", "38;5;127"}, | ||||
|                                                 {"lavender", "38;5;183"}, | ||||
|                                                 {"maroon", "38;5;124"}, | ||||
|                                                 {"beige", "38;5;230"}, | ||||
|                                                 {"olive", "38;5;142"}, | ||||
|                                                 {"indigo", "38;5;54"}, | ||||
|                                                 {"teal", "38;5;30"}, | ||||
|                                                 {"gold", "38;5;220"}, | ||||
|                                                 {"silver", "38;5;7"}, | ||||
|                                                 {"navy", "38;5;17"}, | ||||
|                                                 {"steel", "38;5;188"}, | ||||
|                                                 {"salmon", "38;5;173"}, | ||||
|                                                 {"peach", "38;5;217"}, | ||||
|                                                 {"khaki", "38;5;179"}, | ||||
|                                                 {"coral", "38;5;209"}, | ||||
|                                                 {"crimson", "38;5;160"}}; | ||||
|  | ||||
| void DiscordPage::javaScriptConsoleMessage( | ||||
|     QWebEnginePage::JavaScriptConsoleMessageLevel level, const QString &message, | ||||
|     int lineNumber, const QString &sourceID) { | ||||
|   if (message == "!discord-screenaudio-start-stream") { | ||||
|     if (m_streamDialog.isHidden()) | ||||
|       m_streamDialog.setHidden(false); | ||||
|     else | ||||
|       m_streamDialog.activateWindow(); | ||||
|     m_streamDialog.updateTargets(); | ||||
|   } else if (message == "!discord-screenaudio-stream-stopped") { | ||||
|     stopVirtmic(); | ||||
|   } else { | ||||
|     qDebug() << "[discord]" << message; | ||||
|   auto colorSegments = message.split("%c"); | ||||
|   for (auto segment : colorSegments.mid(1)) { | ||||
|     auto lines = segment.split("\n"); | ||||
|     QString ansi; | ||||
|     uint endOfStyles = lines.length(); | ||||
|     for (size_t line = 1; line < lines.length(); line++) { | ||||
|       if (!lines[line].endsWith(";")) { | ||||
|         endOfStyles = line; | ||||
|         break; | ||||
|       } | ||||
|       if (lines[line] == "font-weight: bold;") | ||||
|         ansi += "\033[1m"; | ||||
|       else if (lines[line].startsWith("color: ")) { | ||||
|         auto color = lines[line].mid(7).chopped(1); | ||||
|         if (cssAnsiColorMap.find(color) != cssAnsiColorMap.end()) | ||||
|           ansi += "\033[" + cssAnsiColorMap[color] + "m"; | ||||
|       } | ||||
|     } | ||||
|     qDebug(discordLog) << (ansi + lines[0].trimmed() + "\033[0m " + | ||||
|                            lines[endOfStyles].trimmed()) | ||||
|                               .toUtf8() | ||||
|                               .constData(); | ||||
|     for (auto line : lines.mid(endOfStyles + 1)) { | ||||
|       qDebug(discordLog) << line.toUtf8().constData(); | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | ||||
| void DiscordPage::startStream(QString target, uint width, uint height, | ||||
|                               uint frameRate) { | ||||
|   stopVirtmic(); | ||||
|   startVirtmic(target); | ||||
|   // Wait a bit for the virtmic to start | ||||
|   QTimer::singleShot(target == "None" ? 0 : 200, [=]() { | ||||
|     runJavaScript(QString("window.discordScreenaudioStartStream(%1, %2, %3);") | ||||
|                       .arg(width) | ||||
|                       .arg(height) | ||||
|                       .arg(frameRate)); | ||||
|   }); | ||||
| } | ||||
|   | ||||
| @@ -1,11 +1,10 @@ | ||||
| #pragma once | ||||
|  | ||||
| #include "streamdialog.h" | ||||
| #include "virtmic.h" | ||||
| #include "userscript.h" | ||||
|  | ||||
| #include <QProcess> | ||||
| #include <QWebEngineFullScreenRequest> | ||||
| #include <QWebEnginePage> | ||||
| #include <QWebEngineScript> | ||||
|  | ||||
| class DiscordPage : public QWebEnginePage { | ||||
|   Q_OBJECT | ||||
| @@ -14,8 +13,9 @@ public: | ||||
|   explicit DiscordPage(QWidget *parent = nullptr); | ||||
|  | ||||
| private: | ||||
|   StreamDialog m_streamDialog; | ||||
|   QProcess m_virtmicProcess; | ||||
|   UserScript m_userScript; | ||||
|   void setupPermissions(); | ||||
|   void setupUserStyles(); | ||||
|   bool acceptNavigationRequest(const QUrl &url, | ||||
|                                QWebEnginePage::NavigationType type, | ||||
|                                bool isMainFrame) override; | ||||
| @@ -24,14 +24,16 @@ private: | ||||
|   javaScriptConsoleMessage(QWebEnginePage::JavaScriptConsoleMessageLevel level, | ||||
|                            const QString &message, int lineNumber, | ||||
|                            const QString &sourceID) override; | ||||
|   void injectScript(QString source); | ||||
|   void stopVirtmic(); | ||||
|   void startVirtmic(QString target); | ||||
|   void injectScript(QString name, QString content, | ||||
|                     QWebEngineScript::InjectionPoint injectionPoint); | ||||
|   void injectScript(QString name, QString content); | ||||
|   void injectStylesheet(QString name, QString content); | ||||
|   void injectFile(void (DiscordPage::*inject)(QString, QString), QString name, | ||||
|                   QString source); | ||||
|  | ||||
| private Q_SLOTS: | ||||
|   void featurePermissionRequested(const QUrl &securityOrigin, | ||||
|                                   QWebEnginePage::Feature feature); | ||||
|   void startStream(QString target, uint width, uint height, uint frameRate); | ||||
| }; | ||||
|  | ||||
| // Will immediately get destroyed again but is needed for navigation to | ||||
|   | ||||
							
								
								
									
										7
									
								
								src/log.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								src/log.cpp
									
									
									
									
									
										Normal 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
									
								
							
							
						
						
									
										9
									
								
								src/log.h
									
									
									
									
									
										Normal 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); | ||||
							
								
								
									
										34
									
								
								src/main.cpp
									
									
									
									
									
								
							
							
						
						
									
										34
									
								
								src/main.cpp
									
									
									
									
									
								
							| @@ -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.9"); | ||||
|   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(); | ||||
|   | ||||
| @@ -1,6 +1,10 @@ | ||||
| #include "mainwindow.h" | ||||
| #include "virtmic.h" | ||||
|  | ||||
| #ifdef KNOTIFICATIONS | ||||
| #include <KNotification> | ||||
| #endif | ||||
|  | ||||
| #include <QApplication> | ||||
| #include <QColor> | ||||
| #include <QComboBox> | ||||
| @@ -10,16 +14,32 @@ | ||||
| #include <QPushButton> | ||||
| #include <QSpacerItem> | ||||
| #include <QThread> | ||||
| #include <QTimer> | ||||
| #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(); | ||||
|   if (m_settings->value("trayIcon", false).toBool() && | ||||
|       m_settings->value("startHidden", false).toBool()) { | ||||
|     hide(); | ||||
|     QTimer::singleShot(0, [=]() { hide(); }); | ||||
|   } | ||||
| } | ||||
|  | ||||
| void MainWindow::setupWebView() { | ||||
| @@ -30,6 +50,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 +96,72 @@ void MainWindow::fullScreenRequested( | ||||
|   } | ||||
| } | ||||
|  | ||||
| void MainWindow::closeEvent(QCloseEvent *event) { QApplication::quit(); } | ||||
| void MainWindow::setupTrayIcon() { | ||||
|   if (m_settings->value("trayIcon", false).toBool() == false || | ||||
|       m_trayIcon != nullptr) | ||||
|     return; | ||||
|  | ||||
|   auto aboutAction = new QAction( | ||||
|       "discord-screenaudio v" + QString(DISCORD_SCEENAUDIO_VERSION_FULL), this); | ||||
|   aboutAction->setIcon(QIcon(":assets/de.shorsh.discord-screenaudio.png")); | ||||
|   aboutAction->setEnabled(false); | ||||
|  | ||||
|   auto exitAction = new QAction("Exit", this); | ||||
|   connect(exitAction, &QAction::triggered, []() { QApplication::quit(); }); | ||||
|  | ||||
|   m_trayIconMenu = new QMenu(this); | ||||
|   m_trayIconMenu->addAction(aboutAction); | ||||
|   m_trayIconMenu->addAction(exitAction); | ||||
|  | ||||
|   m_trayIcon = new QSystemTrayIcon(this); | ||||
|   m_trayIcon->setContextMenu(m_trayIconMenu); | ||||
|   m_trayIcon->setIcon(QIcon(":assets/de.shorsh.discord-screenaudio.png")); | ||||
|   m_trayIcon->show(); | ||||
|  | ||||
|   connect(m_trayIcon, &QSystemTrayIcon::activated, [this](auto reason) { | ||||
|     if (reason == QSystemTrayIcon::Trigger) { | ||||
|       if (isVisible()) { | ||||
|         hide(); | ||||
|       } else { | ||||
|         show(); | ||||
|         activateWindow(); | ||||
|       } | ||||
|     } | ||||
|   }); | ||||
| } | ||||
|  | ||||
| void MainWindow::cleanTrayIcon() { | ||||
|   if (m_trayIcon == nullptr) | ||||
|     return; | ||||
|   m_trayIcon->hide(); | ||||
|   m_trayIconMenu->deleteLater(); | ||||
|   m_trayIcon->deleteLater(); | ||||
|   m_trayIconMenu = nullptr; | ||||
|   m_trayIcon = nullptr; | ||||
| } | ||||
|  | ||||
| void MainWindow::setupSettings() { | ||||
|   m_settings = new QSettings("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; } | ||||
|   | ||||
| @@ -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 = nullptr; | ||||
|   QMenu *m_trayIconMenu; | ||||
|   QSettings *m_settings; | ||||
|   bool m_wasMaximized; | ||||
|   static MainWindow *m_instance; | ||||
|   bool m_useNotifySend; | ||||
| #ifdef KNOTIFICATIONS | ||||
|   bool m_useKF5Notifications = true; | ||||
| #else | ||||
|   bool m_useKF5Notifications = false; | ||||
| #endif | ||||
|  | ||||
| public Q_SLOTS: | ||||
|   void setTrayIcon(bool enabled); | ||||
|  | ||||
| private Q_SLOTS: | ||||
|   void fullScreenRequested(QWebEngineFullScreenRequest fullScreenRequest); | ||||
|   | ||||
| @@ -9,70 +9,109 @@ | ||||
| #include <QSizePolicy> | ||||
| #include <QVBoxLayout> | ||||
|  | ||||
| StreamDialog::StreamDialog() : QWidget() { | ||||
| StreamDialog::StreamDialog(QWidget *parent) : QDialog(parent) { | ||||
|   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(m_videoGroupBox); | ||||
|  | ||||
|   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(m_audioGroupBox); | ||||
|  | ||||
|       auto targetLabel = new QLabel(this); | ||||
|       targetLabel->setText("Audio Source"); | ||||
|       audioLayout->addWidget(targetLabel); | ||||
|  | ||||
|       { | ||||
|         auto targetLayout = new QHBoxLayout(); | ||||
|         audioLayout->addLayout(targetLayout); | ||||
|  | ||||
|         m_targetComboBox = new QComboBox(this); | ||||
|         updateTargets(); | ||||
|         targetLayout->addWidget(m_targetComboBox); | ||||
|  | ||||
|         auto refreshTargetsButton = new QPushButton(this); | ||||
|         refreshTargetsButton->setFixedSize(30, 30); | ||||
|         refreshTargetsButton->setIcon(QIcon::fromTheme("view-refresh")); | ||||
|         connect(refreshTargetsButton, &QPushButton::clicked, this, | ||||
|                 &StreamDialog::updateTargets); | ||||
|         targetLayout->addWidget(refreshTargetsButton); | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     auto button = new QPushButton(this); | ||||
|     button->setText("Start Stream"); | ||||
|     connect(button, &QPushButton::clicked, this, &StreamDialog::startStream); | ||||
|     layout->addWidget(button, Qt::AlignRight | Qt::AlignBottom); | ||||
|   } | ||||
|  | ||||
|   setWindowTitle("discord-screenaudio Stream Dialog"); | ||||
|   setFixedSize(0, 0); | ||||
| } | ||||
|  | ||||
| void StreamDialog::startStream() { | ||||
|   auto resolution = | ||||
|       m_qualityResolutionComboBox->currentData().toString().split('x'); | ||||
|   emit requestedStreamStart(m_targetComboBox->currentText(), | ||||
|                             resolution[0].toUInt(), resolution[1].toUInt(), | ||||
|                             m_qualityFPSComboBox->currentData().toUInt()); | ||||
|   auto resolution = m_resolutionComboBox->currentData().toString().split('x'); | ||||
|   emit requestedStreamStart(m_videoGroupBox->isChecked(), | ||||
|                             m_audioGroupBox->isChecked(), resolution[0].toInt(), | ||||
|                             resolution[1].toInt(), | ||||
|                             m_framerateComboBox->currentData().toInt(), | ||||
|                             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]"); | ||||
| } | ||||
|   | ||||
| @@ -2,22 +2,25 @@ | ||||
|  | ||||
| #include <QComboBox> | ||||
| #include <QDialog> | ||||
| #include <QGroupBox> | ||||
| #include <QWidget> | ||||
|  | ||||
| class StreamDialog : public QWidget { | ||||
| class StreamDialog : public QDialog { | ||||
|   Q_OBJECT | ||||
|  | ||||
| public: | ||||
|   explicit StreamDialog(); | ||||
|   explicit StreamDialog(QWidget *parent = nullptr); | ||||
|  | ||||
| private: | ||||
|   QComboBox *m_targetComboBox; | ||||
|   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, int width, int height, | ||||
|                             int frameRate, QString target); | ||||
|  | ||||
| public Q_SLOTS: | ||||
|   void updateTargets(); | ||||
|   | ||||
							
								
								
									
										165
									
								
								src/userscript.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										165
									
								
								src/userscript.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,165 @@ | ||||
| #include "userscript.h" | ||||
| #include "log.h" | ||||
| #include "mainwindow.h" | ||||
|  | ||||
| #include <QApplication> | ||||
| #include <QDebug> | ||||
| #include <QTimer> | ||||
|  | ||||
| #ifdef KXMLGUI | ||||
| #include <KActionCollection> | ||||
| #endif | ||||
|  | ||||
| UserScript::UserScript() : QObject() { | ||||
|   setupHelpMenu(); | ||||
|   setupShortcutsDialog(); | ||||
|   setupStreamDialog(); | ||||
|   setupVirtmic(); | ||||
| } | ||||
|  | ||||
| void UserScript::setupHelpMenu() { | ||||
| #ifdef KXMLGUI | ||||
|   m_kxmlgui = true; | ||||
|  | ||||
|   KAboutData aboutData( | ||||
|       "discord-screenaudio", "discord-screenaudio", | ||||
|       QApplication::applicationVersion(), | ||||
|       "Custom Discord client with the ability to stream audio on Linux", | ||||
|       KAboutLicense::GPL_V3, "Copyright 2022 (C) Malte Jürgens"); | ||||
|   aboutData.setBugAddress("https://github.com/maltejur/discord-screenaudio"); | ||||
|   aboutData.addAuthor("Malte Jürgens", "Author", "maltejur@dismail.de", | ||||
|                       "https://github.com/maltejur"); | ||||
|   aboutData.addCredit("edisionnano", | ||||
|                       "For creating and documenting the approach for streaming " | ||||
|                       "audio in Discord used in this project.", | ||||
|                       QString(), | ||||
|                       "https://github.com/edisionnano/" | ||||
|                       "Screenshare-with-audio-on-Discord-with-Linux"); | ||||
|   aboutData.addCredit( | ||||
|       "Curve", "For creating the Rohrkabel library used in this project.", | ||||
|       QString(), "https://github.com/Curve"); | ||||
|   aboutData.addComponent("Rohrkabel", "A C++ RAII Pipewire-API Wrapper", "1.3", | ||||
|                          "https://github.com/Soundux/rohrkabel"); | ||||
|   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(toggleMuteAction, &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(); | ||||
| } | ||||
							
								
								
									
										69
									
								
								src/userscript.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										69
									
								
								src/userscript.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,69 @@ | ||||
| #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 | ||||
|  | ||||
| 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); | ||||
|  | ||||
| private: | ||||
|   QProcess m_virtmicProcess; | ||||
|   StreamDialog *m_streamDialog; | ||||
|   bool m_kxmlgui = false; | ||||
|   bool m_kglobalaccel = false; | ||||
| #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); | ||||
|  | ||||
| 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); | ||||
|  | ||||
| private Q_SLOTS: | ||||
|   void startStream(bool video, bool audio, int width, int height, int frameRate, | ||||
|                    QString target); | ||||
| }; | ||||
| @@ -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,17 @@ QVector<QString> getTargets() { | ||||
|         if (global.type == pipewire::node::type) { | ||||
|           auto node = reg.bind<pipewire::node>(global.id); | ||||
|           auto info = node.info(); | ||||
|           QString name; | ||||
|           if (info.props.count("application.name") && | ||||
|               info.props["application.name"] != "") | ||||
|             name = QString::fromStdString(info.props["application.name"]); | ||||
|           else | ||||
|             name = QString::fromStdString( | ||||
|                 info.props["application.process.binary"]); | ||||
|  | ||||
|           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 +73,28 @@ void start(QString _target) { | ||||
|         continue; | ||||
|  | ||||
|       auto &parent = nodes.at(parent_id); | ||||
|       std::string name; | ||||
|       if (parent.props.count("application.name") && | ||||
|           parent.props["application.name"] != "") | ||||
|         name = parent.props["application.name"]; | ||||
|       else | ||||
|         name = parent.props["application.process.binary"]; | ||||
|  | ||||
|       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(name)) | ||||
|                                   .arg(port_id) | ||||
|                                   .arg(fl ? virt_fl->info().id | ||||
|                                           : virt_fr->info().id) | ||||
|                                   .toUtf8() | ||||
|                                   .data(); | ||||
|       } | ||||
|     } | ||||
|   }; | ||||
| @@ -93,7 +110,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 +122,19 @@ 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; | ||||
|           auto info = node.info(); | ||||
|           std::string name; | ||||
|           if (info.props.count("application.name") && | ||||
|               info.props["application.name"] != "") | ||||
|             name = info.props["application.name"]; | ||||
|           else if (info.props.count("application.process.binary")) { | ||||
|             name = info.props["application.process.binary"]; | ||||
|           } else | ||||
|             return; | ||||
|           qDebug(virtmicLog) << QString("Added: %1") | ||||
|                                     .arg(QString::fromStdString(name)) | ||||
|                                     .toUtf8() | ||||
|                                     .data(); | ||||
|  | ||||
|           if (!nodes.count(global.id)) { | ||||
|             nodes.emplace(global.id, node.info()); | ||||
| @@ -141,8 +168,16 @@ 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; | ||||
|           std::string name; | ||||
|           if (info.props.count("application.name") && | ||||
|               info.props["application.name"] != "") | ||||
|             name = info.props["application.name"]; | ||||
|           else | ||||
|             name = info.props["application.process.binary"]; | ||||
|           qDebug(virtmicLog) << QString("Removed: %1") | ||||
|                                     .arg(QString::fromStdString(name)) | ||||
|                                     .toUtf8() | ||||
|                                     .data(); | ||||
|           nodes.erase(id); | ||||
|         } | ||||
|         if (ports.count(id)) { | ||||
|   | ||||
							
								
								
									
										164
									
								
								version.cmake
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										164
									
								
								version.cmake
									
									
									
									
									
										Normal 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() | ||||
|  | ||||
		Reference in New Issue
	
	Block a user