Compare commits
	
		
			62 Commits
		
	
	
		
			v1.4.1
			...
			virtmic-re
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | e6449ad807 | ||
|  | e919e3ea25 | ||
|  | 10fff86ca4 | ||
|  | 87e84dec5c | ||
|  | 4b782133ae | ||
|  | d5257207bc | ||
|  | 423884ae0c | ||
|  | 7ea3d0aab1 | ||
|  | 18b15f5cf4 | ||
|  | 96bca52d0d | ||
|  | 3540774c82 | ||
|  | b5435acdd8 | ||
|  | 38c2b92e2d | ||
|  | e447206082 | ||
|  | f31901073e | ||
|  | 77f31b63aa | ||
|  | 0b9fc100c2 | ||
|  | f73524de27 | ||
|  | 8a8690fe53 | ||
|  | 259e6dc75d | ||
|  | 9cbdca0441 | ||
|  | a9ddc0216f | ||
|  | b39e23d462 | ||
|  | 331fb8f4ca | ||
|  | 335a4456ed | ||
|  | 26a2ca7b05 | ||
|  | 30c0526ff7 | ||
|  | 3119e1df19 | ||
|  | 63180f5d53 | ||
|  | 78d43991ab | ||
|  | 193b69f45f | ||
|  | 485ff9634b | ||
|  | 74fdef683f | ||
|  | 02fdc62fbe | ||
|  | d9e3704176 | ||
|  | 36c8cfedb7 | ||
|  | c52daab420 | ||
|  | 8c4fae3410 | ||
|  | ac71e9bbcb | ||
|  | fc0dbb5b34 | ||
|  | 5df24629e6 | ||
|  | 1477a9d4c0 | ||
|  | 0b12487dfd | ||
|  | a10cfda56d | ||
|  | 2a809e163e | ||
|  | bc317d5531 | ||
|  | b4db987217 | ||
|  | 9f46e710a9 | ||
|  | d6641a7a6e | ||
|  | b836be6530 | ||
|  | bfb0714b13 | ||
|  | 374b854261 | ||
|  | 1f6105f76b | ||
|  | 3071159332 | ||
|  | f2de080e1b | ||
|  | f4a60f281d | ||
|  | d693535d53 | ||
|  | e41af697f7 | ||
|  | b0a8815bb8 | ||
|  | 150fd4364e | ||
|  | a0a2924796 | ||
|  | 3c48621427 | 
							
								
								
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -1,2 +1,3 @@ | ||||
| /build | ||||
| .vscode | ||||
| /submodules/arrpc | ||||
|   | ||||
							
								
								
									
										3
									
								
								.gitmodules
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										3
									
								
								.gitmodules
									
									
									
									
										vendored
									
									
								
							| @@ -1,3 +1,6 @@ | ||||
| [submodule "submodules/rohrkabel"] | ||||
| 	path = submodules/rohrkabel | ||||
| 	url = https://github.com/Soundux/rohrkabel | ||||
| [submodule "submodules/channel"] | ||||
| 	path = submodules/channel | ||||
| 	url = https://github.com/Soundux/channel.git | ||||
|   | ||||
| @@ -13,24 +13,27 @@ set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -DDEBUG") | ||||
| string(TIMESTAMP TIMESTAMP %s) | ||||
| # set(CMAKE_AUTOUIC ON) | ||||
|  | ||||
| find_package(Qt5 CONFIG REQUIRED COMPONENTS  | ||||
|   Widgets | ||||
|   WebEngineWidgets | ||||
| ) | ||||
| find_package(Qt5 COMPONENTS Widgets) | ||||
| if (Qt5_FOUND) | ||||
|   find_package(Qt5 CONFIG REQUIRED COMPONENTS Widgets WebEngineWidgets) | ||||
|  | ||||
| find_package(KF5Notifications) | ||||
| if(KF5Notifications_FOUND) | ||||
|   add_definitions( -DKNOTIFICATIONS ) | ||||
| endif() | ||||
|   find_package(KF5Notifications) | ||||
|   if(KF5Notifications_FOUND) | ||||
|     add_definitions( -DKNOTIFICATIONS ) | ||||
|   endif() | ||||
|  | ||||
| find_package(KF5XmlGui) | ||||
| if(KF5XmlGui_FOUND) | ||||
|   add_definitions( -DKXMLGUI ) | ||||
| endif() | ||||
|   find_package(KF5XmlGui) | ||||
|   if(KF5XmlGui_FOUND) | ||||
|     add_definitions( -DKXMLGUI ) | ||||
|   endif() | ||||
|  | ||||
| find_package(KF5GlobalAccel) | ||||
| if(KF5GlobalAccel_FOUND) | ||||
|   add_definitions( -DKGLOBALACCEL ) | ||||
|   find_package(KF5GlobalAccel) | ||||
|   if(KF5GlobalAccel_FOUND) | ||||
|     add_definitions( -DKGLOBALACCEL ) | ||||
|   endif() | ||||
| else() | ||||
|   message(WARNING "Qt 5 was not found on your system and Qt 6 will be used. You will not be able to use any features using KDE Frameworks.") | ||||
|   find_package(Qt6 CONFIG REQUIRED COMPONENTS Widgets WebEngineWidgets) | ||||
| endif() | ||||
|  | ||||
| set(discord-screenaudio_SRC | ||||
| @@ -40,6 +43,8 @@ set(discord-screenaudio_SRC | ||||
|   src/discordpage.cpp | ||||
|   src/streamdialog.cpp | ||||
|   src/log.cpp | ||||
|   src/userscript.cpp | ||||
|   src/centralwidget.cpp | ||||
|   resources.qrc | ||||
| ) | ||||
|  | ||||
| @@ -58,15 +63,20 @@ if(GIT_FOUND AND EXISTS "${PROJECT_SOURCE_DIR}/.git") | ||||
|   endif() | ||||
| endif() | ||||
|  | ||||
| if(NOT EXISTS "${PROJECT_SOURCE_DIR}/submodules/rohrkabel/CMakeLists.txt") | ||||
|   message(FATAL_ERROR "Rohrkabel was not found since you are not in a Git checkout or have GIT_SUBMODULE disabled. Please provide rohrkabel manually to `./submodules/rohrkabel`.") | ||||
| endif() | ||||
| function(add_git_subdirectory SUBMODULE) | ||||
|   if(NOT EXISTS "${PROJECT_SOURCE_DIR}/submodules/${SUBMODULE}/CMakeLists.txt") | ||||
|     message(FATAL_ERROR "Submodule ${SUBMODULE} was not found since you are not in a Git checkout or have GIT_SUBMODULE disabled. Please provide ${SUBMODULE} manually to `./submodules/${SUBMODULE}`.") | ||||
|   endif() | ||||
|  | ||||
| add_subdirectory(submodules/rohrkabel) | ||||
|   add_subdirectory(submodules/${SUBMODULE}) | ||||
| endfunction() | ||||
|  | ||||
| add_git_subdirectory(rohrkabel) | ||||
| add_git_subdirectory(channel) | ||||
|  | ||||
| add_executable(discord-screenaudio ${discord-screenaudio_SRC}) | ||||
|  | ||||
| target_link_libraries(discord-screenaudio Qt5::Widgets Qt5::WebEngineWidgets rohrkabel) | ||||
| target_link_libraries(discord-screenaudio Qt::Widgets Qt::WebEngineWidgets rohrkabel channel) | ||||
|  | ||||
| if(KF5Notifications_FOUND) | ||||
|   target_link_libraries(discord-screenaudio KF5::Notifications) | ||||
|   | ||||
							
								
								
									
										29
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										29
									
								
								README.md
									
									
									
									
									
								
							| @@ -9,9 +9,9 @@ of [@edisionnano](https://github.com/edisionnano) and the | ||||
|  | ||||
| Unlike a lot of other solutions, the audio here is directly fed into the | ||||
| screenshare and not passed to the user microphone | ||||
| ([see explanation](#how-it-works)). | ||||
| ([see explanation](#how-does-this-work)). | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| The purpose of this project is **not** to provide an alternative to the original | ||||
| Discord client. Rather, it should be used in addition to the original client in | ||||
| @@ -50,6 +50,8 @@ You have multiple options: | ||||
| ### Requirements | ||||
|  | ||||
| - Basic building tools | ||||
| - An up-to-date system (I can't guarantee that it works on Debian or Ubuntu | ||||
|   20/21) | ||||
| - CMake | ||||
| - Qt5 and QtWebEngine | ||||
| - **PipeWire** (it currently doesn't work with PulseAudio) | ||||
| @@ -57,7 +59,7 @@ You have multiple options: | ||||
| - _Kf5Notifications (optional, for better notifications)_ | ||||
| - _KXMLGui and KGlobalAccel (optional, for keybinds)_ | ||||
|  | ||||
| On Debian: | ||||
| With apt: | ||||
| `apt install -y build-essential cmake qtbase5-dev qtwebengine5-dev libkf5notifications-dev libkf5xmlgui-dev libkf5globalaccel-dev pkg-config libpipewire-0.3-dev git` | ||||
|  | ||||
| ### Building | ||||
| @@ -82,7 +84,9 @@ And then to optionally install it, run: | ||||
| sudo cmake --install build | ||||
| ``` | ||||
|  | ||||
| ## How it works | ||||
| ## FAQ | ||||
|  | ||||
| ### How does this work? | ||||
|  | ||||
| This whole project is based on | ||||
| [this](https://github.com/edisionnano/Screenshare-with-audio-on-Discord-with-Linux) | ||||
| @@ -91,6 +95,23 @@ Discord. Basically: a virtual microphone is created which captures the | ||||
| application audio, and this microphone is then fed to the Discord stream by | ||||
| intercepting a API call of Discord. | ||||
|  | ||||
| ### Drag and drop doesn't work in the Flatpak | ||||
|  | ||||
| This is due to sandboxing limitations of Flatpak. The main Discord Flatpak has | ||||
| the same problem. If you still want to use drag and drop, you can disable most | ||||
| of Flatpak's sandboxing by installing | ||||
| [Flatseal](https://flathub.org/apps/details/com.github.tchx84.Flatseal) and | ||||
| allowing access to "All system files" under the "Filesystem" section. | ||||
|  | ||||
| ### Is there any way to add custom CSS / a theme? | ||||
|  | ||||
| Yes, you can add all your styles into | ||||
| `~/.config/discord-screenaudio/userstyles.css` (or | ||||
| `~/.var/app/de.shorsh.discord-screenaudio/config/discord-screenaudio/userstyles.css` | ||||
| if you are using the Flatpak). But please note that due to QtWebEngine | ||||
| limitations concerning content security policies, you can't use any external | ||||
| files (like `@import` or `url()`). | ||||
|  | ||||
| ## License | ||||
|  | ||||
| Copyright (C) 2022 Malte Jürgens | ||||
|   | ||||
							
								
								
									
										1
									
								
								assets/arrpc.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								assets/arrpc.js
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
							
								
								
									
										1
									
								
								assets/arrpc_bridge_mod.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								assets/arrpc_bridge_mod.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | ||||
| setTimeout((i=>{let t,a,s,e={};new WebSocket("ws://127.0.0.1:1337").onmessage=async i=>{if(msg=JSON.parse(i.data),console.log(msg),!t){const i=window.webpackChunkdiscord_app.push([[Symbol()],{},i=>i]),e=i.c;window.webpackChunkdiscord_app.pop();for(const i in e){let a=e[i].exports;if(a=a&&(a.Z??a.ZP),a&&a.register&&a.wait){t=a;break}}const n=i.m;for(const t in n)if(n[t].toString().includes("getAssetImage: size must === [number, number] for Twitch")){const s=i(t),e=Object.values(s).find((i=>"function"==typeof i&&i.toString().includes("apply(")));a=async(i,t)=>(await e(i,[t,void 0]))[0];break}for(const t in n)if(n[t].toString().includes("e.application={")){const a=i(t),e=Object.values(a).find((i=>"function"==typeof i&&i.toString().includes("e.application={")));s=async i=>{let t={};return await e(t,i),t.application};break}}if(msg.activity?.assets?.large_image&&(msg.activity.assets.large_image=await a(msg.activity.application_id,msg.activity.assets.large_image)),msg.activity?.assets?.small_image&&(msg.activity.assets.small_image=await a(msg.activity.application_id,msg.activity.assets.small_image)),msg.activity){const i=msg.activity.application_id;e[i]||(e[i]=await s(i));const t=e[i];msg.activity.name||(msg.activity.name=t.name)}t.dispatch({type:"LOCAL_ACTIVITY_UPDATE",...msg})}}),1e4); | ||||
| @@ -4,6 +4,7 @@ | ||||
|   <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> | ||||
|   | ||||
| @@ -1,5 +1,3 @@ | ||||
| // From v0.4 | ||||
|  | ||||
| navigator.mediaDevices.chromiumGetDisplayMedia = | ||||
|   navigator.mediaDevices.getDisplayMedia; | ||||
|  | ||||
| @@ -16,16 +14,16 @@ const getAudioDevice = async (nameOfAudioDevice) => { | ||||
|     let devices = await navigator.mediaDevices.enumerateDevices(); | ||||
|     audioDevice = devices.find(({ label }) => label === nameOfAudioDevice); | ||||
|     if (!audioDevice) | ||||
|       console.log( | ||||
|         `dsa: Did not find '${nameOfAudioDevice}', trying again in 100ms` | ||||
|       userscript.log( | ||||
|         `Did not find '${nameOfAudioDevice}', trying again in 100ms` | ||||
|       ); | ||||
|     await sleep(100); | ||||
|   } | ||||
|   console.log(`dsa: Found '${nameOfAudioDevice}'`); | ||||
|   userscript.log(`Found '${nameOfAudioDevice}'`); | ||||
|   return audioDevice; | ||||
| }; | ||||
|  | ||||
| function setGetDisplayMedia(overrideArgs = undefined) { | ||||
| function setGetDisplayMedia(video = true, overrideArgs = undefined) { | ||||
|   const getDisplayMedia = async (...args) => { | ||||
|     var id; | ||||
|     try { | ||||
| @@ -63,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; | ||||
| @@ -70,144 +69,304 @@ 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) => { | ||||
|         window.discordScreenaudioResolutionString = `${height}p ${frameRate}FPS`; | ||||
|         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")) { | ||||
|       let aboutEl; | ||||
|       if (window.discordScreenaudioKXMLGUI) { | ||||
|         aboutEl = document.createElement("a"); | ||||
|         aboutEl.addEventListener("click", () => { | ||||
|           console.log("!discord-screenaudio-about"); | ||||
|         }); | ||||
|       } else { | ||||
|         aboutEl = document.createElement("div"); | ||||
|       } | ||||
|       aboutEl.innerText = `discord-screenaudio ${window.discordScreenaudioVersion}`; | ||||
|       aboutEl.style.fontSize = "12px"; | ||||
|       aboutEl.style.color = "var(--text-muted)"; | ||||
|       aboutEl.style.textTransform = "none"; | ||||
|       aboutEl.classList.add("dirscordScreenaudioAboutText"); | ||||
|       aboutEl.style.cursor = "pointer"; | ||||
|       el.appendChild(aboutEl); | ||||
|     } | ||||
|   function setSvgEnabled() { | ||||
|     svg.innerHTML = `<div class="container-1QtPKm default-colors checked-16gMAN" style="opacity: 1; background-color: rgb(59, 165, 92);"><svg class="slider-HJFN2i" viewBox="0 0 28 20" preserveAspectRatio="xMinYMid meet" aria-hidden="true" style="left: 12px;"><rect fill="white" x="4" y="0" height="20" width="20" rx="10"></rect><svg viewBox="0 0 20 20" fill="none"><path fill="rgba(59, 165, 92, 1)" d="M7.89561 14.8538L6.30462 13.2629L14.3099 5.25755L15.9009 6.84854L7.89561 14.8538Z"></path><path fill="rgba(59, 165, 92, 1)" d="M4.08643 11.0903L5.67742 9.49929L9.4485 13.2704L7.85751 14.8614L4.08643 11.0903Z"></path></svg></svg><input id="uid_74" type="checkbox" class="input-125oad" tabindex="0" checked=""></div>`; | ||||
|   } | ||||
|  | ||||
|   // Remove stream settings if stream is active | ||||
|   document.getElementById("manage-streams-change-windows")?.remove(); | ||||
|   document.querySelector(`[aria-label="Stream Settings"]`)?.remove(); | ||||
|  | ||||
|   // Add event listener for keybind tab | ||||
|   if ( | ||||
|     document | ||||
|       .getElementById("keybinds-tab") | ||||
|       ?.getElementsByClassName( | ||||
|         "container-3jbRo5 info-1hMolH fontSize16-3zr6Io browserNotice-1u-Y5o" | ||||
|       ).length | ||||
|   ) { | ||||
|     const el = document | ||||
|       .getElementById("keybinds-tab") | ||||
|       .getElementsByClassName("children-1xdcWE")[0]; | ||||
|     const div = document.createElement("div"); | ||||
|     div.style.marginBottom = "50px"; | ||||
|     const button = document.createElement("button"); | ||||
|     button.classList = | ||||
|       "button-f2h6uQ lookFilled-yCfaCM colorBrand-I6CyqQ sizeSmall-wU2dO- grow-2sR_-F"; | ||||
|     button.innerText = "Edit Global Keybinds"; | ||||
|     button.addEventListener("click", () => { | ||||
|       console.log("!discord-screenaudio-keybinds"); | ||||
|     }); | ||||
|     div.appendChild(button); | ||||
|     el.innerHTML = ""; | ||||
|     el.appendChild(div); | ||||
|   function updateSvg() { | ||||
|     if (enabled) setSvgEnabled(); | ||||
|     else setSvgDisabled(); | ||||
|   } | ||||
|  | ||||
|   const buttonContainer = document.getElementsByClassName("container-YkUktl")[0]; | ||||
|   if (!buttonContainer) { | ||||
|     console.log('dsa: Cannot locate Mute/Deafen/Settings button container, please report this on GitHub'); | ||||
|   } | ||||
|    | ||||
|   const muteBtn = buttonContainer ? buttonContainer.getElementsByClassName( | ||||
|     "button-12Fmur enabled-9OeuTA button-f2h6uQ lookBlank-21BCro colorBrand-I6CyqQ grow-2sR_-F" | ||||
|   )[0] : null; | ||||
|   window.discordScreenaudioToggleMute = () => muteBtn && muteBtn.click(); | ||||
|    | ||||
|   const deafenBtn = buttonContainer ? buttonContainer.getElementsByClassName( | ||||
|     "button-12Fmur enabled-9OeuTA button-f2h6uQ lookBlank-21BCro colorBrand-I6CyqQ grow-2sR_-F" | ||||
|   )[1] : null; | ||||
|    | ||||
|   window.discordScreenaudioToggleDeafen = () => deafenBtn && deafenBtn.click(); | ||||
|   container.addEventListener("click", () => { | ||||
|     enabled = !enabled; | ||||
|     updateSvg(); | ||||
|     onClick(enabled); | ||||
|   }); | ||||
|   updateSvg(); | ||||
|  | ||||
|   if (window.discordScreenaudioResolutionString) { | ||||
|     for (const el of document.getElementsByClassName( | ||||
|       "qualityIndicator-39wQDy" | ||||
|     )) { | ||||
|       el.innerHTML = window.discordScreenaudioResolutionString; | ||||
|     } | ||||
|   } | ||||
| }, 500); | ||||
|   return container; | ||||
| } | ||||
|  | ||||
| // Fix for broken discord notifications after restart | ||||
| // (https://github.com/maltejur/discord-screenaudio/issues/17) | ||||
| Notification.requestPermission(); | ||||
|  | ||||
| setTimeout(() => { | ||||
|   new QWebChannel(qt.webChannelTransport, (channel) => { | ||||
|     userscript = channel.objects.userscript; | ||||
|     main(); | ||||
|   }); | ||||
| }); | ||||
|  | ||||
| function main() { | ||||
|   userscript.muteToggled.connect(() => { | ||||
|     console.log("Toggling mute"); | ||||
|     muteBtn && muteBtn.click(); | ||||
|   }); | ||||
|  | ||||
|   userscript.deafenToggled.connect(() => { | ||||
|     console.log("Toggling deafen"); | ||||
|     deafenBtn && deafenBtn.click(); | ||||
|   }); | ||||
|  | ||||
|   userscript.streamStarted.connect((video, width, height, frameRate) => { | ||||
|     resolutionString = video ? `${height}p ${frameRate}FPS` : "Audio Only"; | ||||
|     setGetDisplayMedia(video, { | ||||
|       audio: true, | ||||
|       video: { width, height, frameRate }, | ||||
|     }); | ||||
|     streamStartBtn.click(); | ||||
|     streamStartBtn.style.display = streamStartBtnInitialDisplay; | ||||
|     streamStartBtnClone.remove(); | ||||
|   }); | ||||
|  | ||||
|   function updateUserstyles() { | ||||
|     userscript.log("Loading userstyles..."); | ||||
|     userscript.loadingMessage = "Loading userstyles..."; | ||||
|     let stylesheet = document.getElementById("discordScreenaudioUserstyles"); | ||||
|     if (stylesheet) { | ||||
|       userscript.log("Removing old userstyles..."); | ||||
|       stylesheet.remove(); | ||||
|     } | ||||
|     stylesheet = document.createElement("style"); | ||||
|     stylesheet.id = "discordScreenaudioUserstyles"; | ||||
|     stylesheet.innerText = userscript.userstyles; | ||||
|     document.head.appendChild(stylesheet); | ||||
|     userscript.log("Finished loading userstyles"); | ||||
|     userscript.loadingMessage = ""; | ||||
|   } | ||||
|  | ||||
|   userscript.userstylesChanged.connect(updateUserstyles); | ||||
|   setTimeout(() => updateUserstyles()); | ||||
|  | ||||
|   setInterval(async () => { | ||||
|     const streamActive = | ||||
|       document.getElementsByClassName("panel-2ZFCRb activityPanel-9icbyU") | ||||
|         .length > 0; | ||||
|  | ||||
|     if (!streamActive && wasStreamActive) userscript.stopVirtmic(); | ||||
|     wasStreamActive = streamActive; | ||||
|  | ||||
|     if (streamActive) { | ||||
|       clonedElements.forEach((el) => { | ||||
|         el.remove(); | ||||
|       }); | ||||
|       clonedElements.length = 0; | ||||
|  | ||||
|       hiddenElements.forEach((el) => { | ||||
|         el.style.display = "block"; | ||||
|       }); | ||||
|       hiddenElements.length = 0; | ||||
|     } else { | ||||
|       for (const el of [ | ||||
|         document.getElementsByClassName("actionButtons-2vEOUh")?.[0] | ||||
|           ?.children[1], | ||||
|         document.querySelector( | ||||
|           ".wrapper-3t3Yqv > div > div > div > div > .controlButton-2PMNom" | ||||
|         ), | ||||
|       ]) { | ||||
|         if (!el) continue; | ||||
|         if (el.classList.contains("discord-screenaudio-cloned")) continue; | ||||
|         streamStartBtn = el; | ||||
|         streamStartBtn.classList.add("discord-screenaudio-cloned"); | ||||
|  | ||||
|         streamStartBtnClone = streamStartBtn.cloneNode(true); | ||||
|         streamStartBtnClone.title = "Share Your Screen with Audio"; | ||||
|         streamStartBtnClone.addEventListener("click", () => { | ||||
|           userscript.showStreamDialog(); | ||||
|         }); | ||||
|  | ||||
|         streamStartBtnInitialDisplay = streamStartBtn.style.display; | ||||
|  | ||||
|         streamStartBtn.style.display = "none"; | ||||
|         streamStartBtn.parentNode.insertBefore(streamStartBtnClone, el); | ||||
|  | ||||
|         clonedElements.push(streamStartBtnClone); | ||||
|         hiddenElements.push(streamStartBtn); | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     // Add about text in settings | ||||
|     if ( | ||||
|       document.getElementsByClassName("dirscordScreenaudioAboutText").length == | ||||
|       0 | ||||
|     ) { | ||||
|       for (const el of document.getElementsByClassName("info-3pQQBb")) { | ||||
|         let aboutEl; | ||||
|         if (userscript.kxmlgui) { | ||||
|           aboutEl = document.createElement("a"); | ||||
|           aboutEl.addEventListener("click", () => { | ||||
|             userscript.showHelpMenu(); | ||||
|           }); | ||||
|         } else { | ||||
|           aboutEl = document.createElement("div"); | ||||
|         } | ||||
|         aboutEl.innerText = `discord-screenaudio ${userscript.version}`; | ||||
|         aboutEl.style.fontSize = "12px"; | ||||
|         aboutEl.style.color = "var(--text-muted)"; | ||||
|         aboutEl.style.textTransform = "none"; | ||||
|         aboutEl.style.display = "inline-block"; | ||||
|         aboutEl.style.width = "100%"; | ||||
|         aboutEl.classList.add("dirscordScreenaudioAboutText"); | ||||
|         aboutEl.style.cursor = "pointer"; | ||||
|         el.appendChild(aboutEl); | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     // Remove stream settings if stream is active | ||||
|     document.getElementById("manage-streams-change-windows")?.remove(); | ||||
|     document.querySelector(`[aria-label="Stream Settings"]`)?.remove(); | ||||
|  | ||||
|     // Add event listener for keybind tab | ||||
|     if ( | ||||
|       document | ||||
|         .getElementById("keybinds-tab") | ||||
|         ?.getElementsByClassName( | ||||
|           "container-3jbRo5 info-1hMolH browserNotice-1u-Y5o" | ||||
|         ).length | ||||
|     ) { | ||||
|       const el = document | ||||
|         .getElementById("keybinds-tab") | ||||
|         .getElementsByClassName("children-1xdcWE")[0]; | ||||
|       const div = document.createElement("div"); | ||||
|       div.style.marginBottom = "50px"; | ||||
|       div.appendChild( | ||||
|         createButton("Edit Global Keybinds", () => { | ||||
|           userscript.showShortcutsDialog(); | ||||
|         }) | ||||
|       ); | ||||
|       el.innerHTML = ""; | ||||
|       el.appendChild(div); | ||||
|     } | ||||
|  | ||||
|     const buttonContainer = | ||||
|       document.getElementsByClassName("container-YkUktl")[0]; | ||||
|     if (!buttonContainer) { | ||||
|       userscript.log( | ||||
|         "Cannot locate Mute/Deafen/Settings button container, please report this on GitHub" | ||||
|       ); | ||||
|     } | ||||
|  | ||||
|     muteBtn = buttonContainer | ||||
|       ? buttonContainer.getElementsByTagName("button")[0] | ||||
|       : null; | ||||
|  | ||||
|     deafenBtn = buttonContainer | ||||
|       ? buttonContainer.getElementsByTagName("button")[1] | ||||
|       : null; | ||||
|  | ||||
|     if (resolutionString) { | ||||
|       for (const el of document.getElementsByClassName( | ||||
|         "qualityIndicator-39wQDy" | ||||
|       )) { | ||||
|         el.innerHTML = resolutionString; | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     const accountTab = document.getElementById("my-account-tab"); | ||||
|     if (accountTab) { | ||||
|       const discordScreenaudioSettings = document.getElementById( | ||||
|         "discord-screenaudio-settings" | ||||
|       ); | ||||
|       if (!discordScreenaudioSettings) { | ||||
|         const firstDivider = accountTab.getElementsByClassName( | ||||
|           "divider-3nqZNm marginTop40-Q4o1tS" | ||||
|         )[0]; | ||||
|         if (firstDivider) { | ||||
|           const section = document.createElement("div"); | ||||
|           section.className = "marginTop40-Q4o1tS"; | ||||
|           section.id = "discord-screenaudio-settings"; | ||||
|  | ||||
|           const title = document.createElement("h2"); | ||||
|           title.className = | ||||
|             "h1-3iMExa title-lXcL8p defaultColor-3Olr-9 defaultMarginh1-1UYutH"; | ||||
|           title.innerText = "discord-screenaudio"; | ||||
|           section.appendChild(title); | ||||
|  | ||||
|           section.appendChild( | ||||
|             createButton("Edit Global Keybinds", () => { | ||||
|               userscript.showShortcutsDialog(); | ||||
|             }) | ||||
|           ); | ||||
|  | ||||
|           // section.appendChild( | ||||
|           //   createButton("Install Theme", () => { | ||||
|           //     userscript.showThemeDialog(); | ||||
|           //   }) | ||||
|           // ); | ||||
|  | ||||
|           // section.appendChild( | ||||
|           //   createButton("Uninstall Theme", () => { | ||||
|           //     userscript.installUserStyles(""); | ||||
|           //   }) | ||||
|           // ); | ||||
|  | ||||
|           section.appendChild( | ||||
|             createSwitch( | ||||
|               "Move discord-screenaudio to the system tray instead of closing", | ||||
|               await userscript.getBoolPref("trayIcon", false), | ||||
|               (enabled) => { | ||||
|                 userscript.setTrayIcon(enabled); | ||||
|               } | ||||
|             ) | ||||
|           ); | ||||
|  | ||||
|           section.appendChild( | ||||
|             createSwitch( | ||||
|               "Start discord-screenaudio hidden to tray", | ||||
|               await userscript.getPref("startHidden", false), | ||||
|               (hidden) => { | ||||
|                 userscript.setPref("startHidden", hidden); | ||||
|               } | ||||
|             ) | ||||
|           ); | ||||
|  | ||||
|           const divider = document.createElement("div"); | ||||
|           divider.className = "divider-3nqZNm marginTop40-Q4o1tS"; | ||||
|  | ||||
|           firstDivider.after(section); | ||||
|           section.after(divider); | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|   }, 500); | ||||
| } | ||||
|   | ||||
| @@ -2,6 +2,8 @@ | ||||
| <RCC> | ||||
|   <qresource> | ||||
|     <file>assets/userscript.js</file> | ||||
|     <file>assets/arrpc_bridge_mod.js</file> | ||||
|     <file>assets/arrpc.js</file> | ||||
|     <file>assets/de.shorsh.discord-screenaudio.png</file> | ||||
|   </qresource> | ||||
| </RCC> | ||||
							
								
								
									
										36
									
								
								scripts/build_arrpc.sh
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										36
									
								
								scripts/build_arrpc.sh
									
									
									
									
									
										Executable file
									
								
							| @@ -0,0 +1,36 @@ | ||||
| #!/usr/bin/bash | ||||
| set -e | ||||
|  | ||||
| cd "$(dirname "$0")/../submodules" | ||||
|  | ||||
| echo_status() { | ||||
|   echo | ||||
|   echo | ||||
|   echo "-> $1..." | ||||
| } | ||||
|  | ||||
| if [ ! -d "arrpc" ]; then | ||||
|   echo_status "Cloning arRPC" | ||||
|   git clone https://github.com/OpenAsar/arrpc.git | ||||
|   cd arrpc | ||||
| else | ||||
|   echo_status "Fetching arRPC changes" | ||||
|   cd arrpc | ||||
|   git fetch | ||||
| fi | ||||
|  | ||||
| echo_status "Checking out latest commit" | ||||
| git reset --hard HEAD | ||||
| git checkout main | ||||
|  | ||||
| echo_status "Installing dependencies" | ||||
| pnpm i -D @vercel/ncc | ||||
|  | ||||
| echo_status "Patching arRPC" | ||||
| sed -i 's/"type": "module",//' package.json | ||||
|  | ||||
| echo_status "Building arRPC" | ||||
| pnpm exec ncc build -m src/index.js | ||||
|  | ||||
| echo_status "Copying built file" | ||||
| cp -v ./dist/index.js ../../assets/arrpc.js | ||||
							
								
								
									
										84
									
								
								src/centralwidget.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										84
									
								
								src/centralwidget.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,84 @@ | ||||
| #include "centralwidget.h" | ||||
| #include "discordpage.h" | ||||
| #include "mainwindow.h" | ||||
|  | ||||
| #include <QWebEngineNotification> | ||||
| #include <QWebEngineProfile> | ||||
| #include <QWebEngineScript> | ||||
| #include <QWebEngineScriptCollection> | ||||
| #include <QWebEngineSettings> | ||||
|  | ||||
| CentralWidget::CentralWidget(QWidget *parent) : QWidget(parent) { | ||||
|   setStyleSheet("background-color:#313338;"); | ||||
|   m_layout = new QVBoxLayout(this); | ||||
|   m_layout->setMargin(0); | ||||
|   m_layout->setSpacing(0); | ||||
|   setupWebView(); | ||||
| } | ||||
|  | ||||
| void CentralWidget::setupWebView() { | ||||
|   auto page = new DiscordPage(this); | ||||
|  | ||||
|   m_webView = new QWebEngineView(this); | ||||
|   m_webView->setPage(page); | ||||
|  | ||||
|   bool useNotifySend = MainWindow::instance() | ||||
|                            ->settings() | ||||
|                            ->value("useNotifySend", false) | ||||
|                            .toBool(); | ||||
|   if (m_useKF5Notifications || useNotifySend) | ||||
|     QWebEngineProfile::defaultProfile()->setNotificationPresenter( | ||||
|         [&](std::unique_ptr<QWebEngineNotification> notificationInfo) { | ||||
|           if (useNotifySend) { | ||||
|             auto title = notificationInfo->title(); | ||||
|             auto message = notificationInfo->message(); | ||||
|             auto image_path = | ||||
|                 QString("/tmp/discord-screenaudio-%1.png").arg(title); | ||||
|             notificationInfo->icon().save(image_path); | ||||
|             QProcess::execute("notify-send", | ||||
|                               {"--icon", image_path, "--app-name", | ||||
|                                "discord-screenaudio", title, message}); | ||||
|           } else if (m_useKF5Notifications) { | ||||
| #ifdef KNOTIFICATIONS | ||||
|             KNotification *notification = | ||||
|                 new KNotification("discordNotification"); | ||||
|             notification->setTitle(notificationInfo->title()); | ||||
|             notification->setText(notificationInfo->message()); | ||||
|             notification->setPixmap( | ||||
|                 QPixmap::fromImage(notificationInfo->icon())); | ||||
|             notification->setDefaultAction("View"); | ||||
|             connect(notification, &KNotification::defaultActivated, | ||||
|                     [&, notificationInfo = std::move(notificationInfo)]() { | ||||
|                       notificationInfo->click(); | ||||
|                       show(); | ||||
|                       activateWindow(); | ||||
|                     }); | ||||
|             notification->sendEvent(); | ||||
| #endif | ||||
|           } | ||||
|         }); | ||||
|  | ||||
|   connect(page->userScript(), &UserScript::loadingMessageChanged, this, | ||||
|           &CentralWidget::setLoadingIndicator); | ||||
|  | ||||
|   m_layout->addWidget(m_webView); | ||||
| } | ||||
|  | ||||
| void CentralWidget::setLoadingIndicator(QString text) { | ||||
|   if (text != "") { | ||||
|     if (m_loadingLabel == nullptr) { | ||||
|       m_loadingLabel = new QLabel(this); | ||||
|       m_loadingLabel->setMaximumHeight(20); | ||||
|       m_loadingLabel->setAlignment(Qt::AlignHCenter); | ||||
|       m_loadingLabel->setStyleSheet("color:#dedede;"); | ||||
|       m_layout->addWidget(m_loadingLabel); | ||||
|     } | ||||
|     m_loadingLabel->setText(text.mid(0, 100)); | ||||
|   } else { | ||||
|     if (m_loadingLabel != nullptr) { | ||||
|       m_layout->removeWidget(m_loadingLabel); | ||||
|       m_loadingLabel->deleteLater(); | ||||
|       m_loadingLabel = nullptr; | ||||
|     } | ||||
|   } | ||||
| } | ||||
							
								
								
									
										29
									
								
								src/centralwidget.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								src/centralwidget.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,29 @@ | ||||
| #pragma once | ||||
|  | ||||
| #include <QLabel> | ||||
| #include <QVBoxLayout> | ||||
| #include <QWebEnginePage> | ||||
| #include <QWebEngineProfile> | ||||
| #include <QWebEngineView> | ||||
| #include <QWidget> | ||||
|  | ||||
| class CentralWidget : public QWidget { | ||||
|   Q_OBJECT | ||||
|  | ||||
| public: | ||||
|   CentralWidget(QWidget *parent = nullptr); | ||||
|  | ||||
| private: | ||||
|   void setupWebView(); | ||||
|   QVBoxLayout *m_layout; | ||||
|   QWebEngineView *m_webView; | ||||
| #ifdef KNOTIFICATIONS | ||||
|   bool m_useKF5Notifications = true; | ||||
| #else | ||||
|   bool m_useKF5Notifications = false; | ||||
| #endif | ||||
|   QLabel *m_loadingLabel = nullptr; | ||||
|  | ||||
| public Q_SLOTS: | ||||
|   void setLoadingIndicator(QString text); | ||||
| }; | ||||
| @@ -3,24 +3,13 @@ | ||||
| #include "mainwindow.h" | ||||
| #include "virtmic.h" | ||||
|  | ||||
| #ifdef KXMLGUI | ||||
| #include <KAboutData> | ||||
| #include <KHelpMenu> | ||||
| #include <KShortcutsDialog> | ||||
| #include <KXmlGuiWindow> | ||||
| #include <QAction> | ||||
|  | ||||
| #ifdef KGLOBALACCEL | ||||
| #include <KGlobalAccel> | ||||
| #endif | ||||
|  | ||||
| #endif | ||||
|  | ||||
| #include <QApplication> | ||||
| #include <QDesktopServices> | ||||
| #include <QFile> | ||||
| #include <QFileInfo> | ||||
| #include <QMessageBox> | ||||
| #include <QNetworkReply> | ||||
| #include <QTemporaryFile> | ||||
| #include <QTimer> | ||||
| #include <QWebChannel> | ||||
| #include <QWebEngineScript> | ||||
| @@ -28,17 +17,31 @@ | ||||
| #include <QWebEngineSettings> | ||||
|  | ||||
| DiscordPage::DiscordPage(QWidget *parent) : QWebEnginePage(parent) { | ||||
|   setBackgroundColor(QColor("#202225")); | ||||
|   m_virtmicProcess.setProcessChannelMode(QProcess::ForwardedChannels); | ||||
|   setBackgroundColor(QColor("#313338")); | ||||
|  | ||||
|   connect(this, &QWebEnginePage::featurePermissionRequested, this, | ||||
|           &DiscordPage::featurePermissionRequested); | ||||
|   connect(this, &DiscordPage::fullScreenRequested, MainWindow::instance(), | ||||
|           &MainWindow::fullScreenRequested); | ||||
|  | ||||
|   connect(this, &QWebEnginePage::loadStarted, [=]() { | ||||
|     runJavaScript(QString("window.discordScreenaudioVersion = '%1';") | ||||
|                       .arg(QApplication::applicationVersion())); | ||||
|   }); | ||||
|   setupPermissions(); | ||||
|  | ||||
|   injectFile(&DiscordPage::injectScript, "qwebchannel.js", | ||||
|              ":/qtwebchannel/qwebchannel.js"); | ||||
|  | ||||
|   setUrl(QUrl("https://discord.com/app")); | ||||
|  | ||||
|   setWebChannel(new QWebChannel(this)); | ||||
|   webChannel()->registerObject("userscript", &m_userScript); | ||||
|  | ||||
|   injectFile(&DiscordPage::injectScript, "userscript.js", | ||||
|              ":/assets/userscript.js"); | ||||
|  | ||||
|   setupUserStyles(); | ||||
|   setupArrpc(); | ||||
| } | ||||
|  | ||||
| void DiscordPage::setupPermissions() { | ||||
|   settings()->setAttribute(QWebEngineSettings::ScreenCaptureEnabled, true); | ||||
|   settings()->setAttribute(QWebEngineSettings::JavascriptCanOpenWindows, true); | ||||
|   settings()->setAttribute(QWebEngineSettings::AllowRunningInsecureContent, | ||||
| @@ -50,71 +53,87 @@ DiscordPage::DiscordPage(QWidget *parent) : QWebEnginePage(parent) { | ||||
|                            false); | ||||
|   settings()->setAttribute(QWebEngineSettings::JavascriptCanOpenWindows, false); | ||||
|   settings()->setAttribute(QWebEngineSettings::ScrollAnimatorEnabled, true); | ||||
|  | ||||
|   setUrl(QUrl("https://discord.com/app")); | ||||
|  | ||||
|   injectScriptFile("userscript.js", ":/assets/userscript.js"); | ||||
|  | ||||
|   injectScriptText("version.js", | ||||
|                    QString("window.discordScreenaudioVersion = '%1';") | ||||
|                        .arg(QApplication::applicationVersion())); | ||||
|  | ||||
| #ifdef KXMLGUI | ||||
|   injectScriptText("xmlgui.js", "window.discordScreenaudioKXMLGUI = true;"); | ||||
|  | ||||
|   KAboutData aboutData( | ||||
|       "discord-screenaudio", "discord-screenaudio", | ||||
|       QApplication::applicationVersion(), | ||||
|       "Custom Discord client with the ability to stream audio on Linux", | ||||
|       KAboutLicense::GPL_V3, "Copyright 2022 (C) Malte Jürgens"); | ||||
|   aboutData.setBugAddress("https://github.com/maltejur/discord-screenaudio"); | ||||
|   aboutData.addAuthor("Malte Jürgens", "Author", "maltejur@dismail.de", | ||||
|                       "https://github.com/maltejur"); | ||||
|   aboutData.addCredit("edisionnano", | ||||
|                       "For creating and documenting the approach for streaming " | ||||
|                       "audio in Discord used in this project.", | ||||
|                       QString(), | ||||
|                       "https://github.com/edisionnano/" | ||||
|                       "Screenshare-with-audio-on-Discord-with-Linux"); | ||||
|   aboutData.addCredit( | ||||
|       "Curve", "For creating the Rohrkabel library used in this project.", | ||||
|       QString(), "https://github.com/Curve"); | ||||
|   aboutData.addComponent("Rohrkabel", "A C++ RAII Pipewire-API Wrapper", "1.3", | ||||
|                          "https://github.com/Soundux/rohrkabel"); | ||||
|   m_helpMenu = new KHelpMenu(parent, aboutData); | ||||
|  | ||||
| #ifdef KGLOBALACCEL | ||||
|   injectScriptText("kglobalaccel.js", | ||||
|                    "window.discordScreenaudioKGLOBALACCEL = true;"); | ||||
|  | ||||
|   auto toggleMuteAction = new QAction(this); | ||||
|   toggleMuteAction->setText("Toggle Mute"); | ||||
|   toggleMuteAction->setIcon(QIcon::fromTheme("microphone-sensitivity-muted")); | ||||
|   connect(toggleMuteAction, &QAction::triggered, this, | ||||
|           &DiscordPage::toggleMute); | ||||
|  | ||||
|   auto toggleDeafenAction = new QAction(this); | ||||
|   toggleDeafenAction->setText("Toggle Deafen"); | ||||
|   toggleDeafenAction->setIcon(QIcon::fromTheme("audio-volume-muted")); | ||||
|   connect(toggleDeafenAction, &QAction::triggered, this, | ||||
|           &DiscordPage::toggleDeafen); | ||||
|  | ||||
|   m_actionCollection = new KActionCollection(this); | ||||
|   m_actionCollection->addAction("toggleMute", toggleMuteAction); | ||||
|   KGlobalAccel::setGlobalShortcut(toggleMuteAction, QList<QKeySequence>{}); | ||||
|   m_actionCollection->addAction("toggleDeafen", toggleDeafenAction); | ||||
|   KGlobalAccel::setGlobalShortcut(toggleDeafenAction, QList<QKeySequence>{}); | ||||
|  | ||||
|   m_shortcutsDialog = new KShortcutsDialog(KShortcutsEditor::GlobalAction); | ||||
|   m_shortcutsDialog->addCollection(m_actionCollection); | ||||
| #endif | ||||
| #endif | ||||
|  | ||||
|   connect(&m_streamDialog, &StreamDialog::requestedStreamStart, this, | ||||
|           &DiscordPage::startStream); | ||||
| } | ||||
|  | ||||
| void DiscordPage::injectScriptText(QString name, QString content) { | ||||
| void DiscordPage::setupUserStyles() { | ||||
|   qDebug(userstylesLog).noquote() | ||||
|       << "Looking for userstyles in" << m_configLocation.absolutePath(); | ||||
|   m_userStylesFile = | ||||
|       new QFile(m_configLocation.absoluteFilePath("userstyles.css")); | ||||
|   if (m_userStylesFile->exists()) { | ||||
|     qDebug(userstylesLog).noquote() | ||||
|         << "Found userstyles:" << m_userStylesFile->fileName(); | ||||
|     m_userStylesFile->open(QIODevice::ReadOnly); | ||||
|     m_userStylesContent = m_userStylesFile->readAll(); | ||||
|     m_userStylesFile->close(); | ||||
|     fetchUserStyles(); | ||||
|   } | ||||
|   connect(&m_userScript, &UserScript::shouldInstallUserStyles, this, | ||||
|           &DiscordPage::getUserStyles); | ||||
| } | ||||
|  | ||||
| const QRegularExpression importRegex( | ||||
|     R"r(@import (?:url\(|)['"]{0,1}(?!.*usrbgs?\.css)([^'"]+?)['"]{0,1}(?:|\));)r"); | ||||
| const QRegularExpression urlRegex( | ||||
|     R"r(url\(['"]{0,1}((?!https:\/\/fonts.gstatic.com)(?!data:)(?!.*\.woff2)(?!.*\.ttf)[^'"]+?)['"]{0,1}\))r"); | ||||
|  | ||||
| void DiscordPage::fetchUserStyles() { | ||||
|   m_userScript.setProperty( | ||||
|       "loadingMessage", "Loading userstyles: Fetching additional resources..."); | ||||
|   bool foundImport = true; | ||||
|   auto match = importRegex.match(m_userStylesContent); | ||||
|   if (!match.hasMatch()) { | ||||
|     foundImport = false; | ||||
|     match = urlRegex.match(m_userStylesContent); | ||||
|   } | ||||
|   if (match.hasMatch()) { | ||||
|     auto url = match.captured(1); | ||||
|     qDebug(userstylesLog) << "Fetching" << url; | ||||
|     m_userScript.setProperty( | ||||
|         "loadingMessage", | ||||
|         QString("Loading userstyles: Fetching %1...").arg(url)); | ||||
|     QNetworkRequest request(url); | ||||
|     auto reply = m_networkAccessManager.get(request); | ||||
|     connect(reply, &QNetworkReply::finished, [=]() { | ||||
|       QByteArray content = ""; | ||||
|       if (reply->error() == QNetworkReply::NoError) { | ||||
|         if (!reply->attribute(QNetworkRequest::RedirectionTargetAttribute) | ||||
|                  .isNull()) | ||||
|           content = | ||||
|               reply->attribute(QNetworkRequest::RedirectionTargetAttribute) | ||||
|                   .toByteArray(); | ||||
|         else | ||||
|           content = reply->readAll(); | ||||
|       } else | ||||
|         qDebug(userstylesLog) << reply->errorString().toUtf8().constData(); | ||||
|       reply->deleteLater(); | ||||
|       m_userStylesContent = m_userStylesContent.replace( | ||||
|           match.captured(0), foundImport | ||||
|                                  ? content | ||||
|                                  : "url(data:application/octet-stream;base64," + | ||||
|                                        content.toBase64() + ")"); | ||||
|       fetchUserStyles(); | ||||
|     }); | ||||
|     return; | ||||
|   } | ||||
|   qDebug(userstylesLog) << "Injecting userstyles"; | ||||
|   m_userScript.setProperty("userstyles", m_userStylesContent); | ||||
|   m_userScript.setProperty("loadingMessage", ""); | ||||
|   if (!m_configLocation.exists()) | ||||
|     m_configLocation.mkpath("."); | ||||
|   m_userStylesFile->open(QIODevice::WriteOnly); | ||||
|   m_userStylesFile->write(m_userStylesContent.toUtf8()); | ||||
|   m_userStylesFile->close(); | ||||
| } | ||||
|  | ||||
| void DiscordPage::getUserStyles(QString url) { | ||||
|   m_userStylesContent = url == "" ? "" : QString("@import url(%1);").arg(url); | ||||
|   fetchUserStyles(); | ||||
| } | ||||
|  | ||||
| void DiscordPage::injectScript( | ||||
|     QString name, QString content, | ||||
|     QWebEngineScript::InjectionPoint injectionPoint) { | ||||
|   qDebug(mainLog) << "Injecting " << name; | ||||
|  | ||||
|   QWebEngineScript script; | ||||
| @@ -122,20 +141,36 @@ void DiscordPage::injectScriptText(QString name, QString content) { | ||||
|   script.setSourceCode(content); | ||||
|   script.setName(name); | ||||
|   script.setWorldId(QWebEngineScript::MainWorld); | ||||
|   script.setInjectionPoint(QWebEngineScript::DocumentCreation); | ||||
|   script.setInjectionPoint(injectionPoint); | ||||
|   script.setRunsOnSubFrames(false); | ||||
|  | ||||
|   scripts().insert(script); | ||||
| } | ||||
|  | ||||
| void DiscordPage::injectScriptFile(QString name, QString source) { | ||||
| void DiscordPage::injectScript(QString name, QString content) { | ||||
|   injectScript(name, content, QWebEngineScript::DocumentCreation); | ||||
| } | ||||
|  | ||||
| void DiscordPage::injectStylesheet(QString name, QString content) { | ||||
|   auto script = QString(R"(const stylesheet = document.createElement("style"); | ||||
| stylesheet.id = "%1"; | ||||
| stylesheet.innerText = `%2`; | ||||
| document.head.appendChild(stylesheet); | ||||
| )") | ||||
|                     .arg(name) | ||||
|                     .arg(content); | ||||
|   injectScript(name, script, QWebEngineScript::DocumentReady); | ||||
| } | ||||
|  | ||||
| void DiscordPage::injectFile(void (DiscordPage::*inject)(QString, QString), | ||||
|                              QString name, QString source) { | ||||
|   QFile file(source); | ||||
|  | ||||
|   if (!file.open(QIODevice::ReadOnly)) { | ||||
|     qFatal("Failed to load %s with error: %s", source.toLatin1().constData(), | ||||
|            file.errorString().toLatin1().constData()); | ||||
|   } else { | ||||
|     injectScriptText(name, file.readAll()); | ||||
|     (this->*inject)(name, file.readAll()); | ||||
|   } | ||||
| } | ||||
|  | ||||
| @@ -146,11 +181,10 @@ void DiscordPage::featurePermissionRequested(const QUrl &securityOrigin, | ||||
|                        QWebEnginePage::PermissionGrantedByUser); | ||||
|  | ||||
|   if (feature == QWebEnginePage::Feature::MediaAudioCapture) { | ||||
|     if (m_virtmicProcess.state() == QProcess::NotRunning) { | ||||
|     if (!m_userScript.isVirtmicRunning()) { | ||||
|       qDebug(virtmicLog) << "Starting Virtmic with no target to make sure " | ||||
|                             "Discord can find all the audio devices"; | ||||
|       m_virtmicProcess.start(QApplication::arguments()[0], | ||||
|                              {"--virtmic", "None"}); | ||||
|       m_userScript.startVirtmic("None"); | ||||
|     } | ||||
|   } | ||||
| } | ||||
| @@ -177,78 +211,101 @@ QWebEnginePage *DiscordPage::createWindow(QWebEnginePage::WebWindowType type) { | ||||
|   return new ExternalPage; | ||||
| } | ||||
|  | ||||
| void DiscordPage::stopVirtmic() { | ||||
|   if (m_virtmicProcess.state() == QProcess::Running) { | ||||
|     qDebug(virtmicLog) << "Stopping Virtmic"; | ||||
|     m_virtmicProcess.kill(); | ||||
|     m_virtmicProcess.waitForFinished(); | ||||
|   } | ||||
| } | ||||
|  | ||||
| void DiscordPage::startVirtmic(QString target) { | ||||
|   if (target != "None") { | ||||
|     qDebug(virtmicLog) << "Starting Virtmic with target" << target; | ||||
|     m_virtmicProcess.start(QApplication::arguments()[0], {"--virtmic", target}); | ||||
|   } | ||||
| } | ||||
| const QMap<QString, QString> cssAnsiColorMap = {{"black", "30"}, | ||||
|                                                 {"red", "31"}, | ||||
|                                                 {"green", "32"}, | ||||
|                                                 {"yellow", "33"}, | ||||
|                                                 {"blue", "34"}, | ||||
|                                                 {"magenta", "35"}, | ||||
|                                                 {"cyan", "36"}, | ||||
|                                                 {"white", "37"}, | ||||
|                                                 {"gray", "90"}, | ||||
|                                                 {"bright-red", "91"}, | ||||
|                                                 {"bright-green", "92"}, | ||||
|                                                 {"bright-yellow", "93"}, | ||||
|                                                 {"bright-blue", "94"}, | ||||
|                                                 {"bright-magenta", "95"}, | ||||
|                                                 {"bright-cyan", "96"}, | ||||
|                                                 {"bright-white", "97"}, | ||||
|                                                 {"orange", "38;5;208"}, | ||||
|                                                 {"pink", "38;5;205"}, | ||||
|                                                 {"brown", "38;5;94"}, | ||||
|                                                 {"light-gray", "38;5;251"}, | ||||
|                                                 {"dark-gray", "38;5;239"}, | ||||
|                                                 {"light-red", "38;5;203"}, | ||||
|                                                 {"light-green", "38;5;83"}, | ||||
|                                                 {"light-yellow", "38;5;227"}, | ||||
|                                                 {"light-blue", "38;5;75"}, | ||||
|                                                 {"light-magenta", "38;5;207"}, | ||||
|                                                 {"light-cyan", "38;5;87"}, | ||||
|                                                 {"turquoise", "38;5;80"}, | ||||
|                                                 {"violet", "38;5;92"}, | ||||
|                                                 {"purple", "38;5;127"}, | ||||
|                                                 {"lavender", "38;5;183"}, | ||||
|                                                 {"maroon", "38;5;124"}, | ||||
|                                                 {"beige", "38;5;230"}, | ||||
|                                                 {"olive", "38;5;142"}, | ||||
|                                                 {"indigo", "38;5;54"}, | ||||
|                                                 {"teal", "38;5;30"}, | ||||
|                                                 {"gold", "38;5;220"}, | ||||
|                                                 {"silver", "38;5;7"}, | ||||
|                                                 {"navy", "38;5;17"}, | ||||
|                                                 {"steel", "38;5;188"}, | ||||
|                                                 {"salmon", "38;5;173"}, | ||||
|                                                 {"peach", "38;5;217"}, | ||||
|                                                 {"khaki", "38;5;179"}, | ||||
|                                                 {"coral", "38;5;209"}, | ||||
|                                                 {"crimson", "38;5;160"}}; | ||||
|  | ||||
| void DiscordPage::javaScriptConsoleMessage( | ||||
|     QWebEnginePage::JavaScriptConsoleMessageLevel level, const QString &message, | ||||
|     int lineNumber, const QString &sourceID) { | ||||
|   if (message == "!discord-screenaudio-start-stream") { | ||||
|     if (m_streamDialog.isHidden()) | ||||
|       m_streamDialog.setHidden(false); | ||||
|     else | ||||
|       m_streamDialog.activateWindow(); | ||||
|     m_streamDialog.updateTargets(); | ||||
|   } else if (message == "!discord-screenaudio-stream-stopped") { | ||||
|     stopVirtmic(); | ||||
|   } else if (message == "!discord-screenaudio-about") { | ||||
| #ifdef KXMLGUI | ||||
|     m_helpMenu->aboutApplication(); | ||||
| #endif | ||||
|   } else if (message == "!discord-screenaudio-keybinds") { | ||||
| #ifdef KXMLGUI | ||||
| #ifdef KGLOBALACCEL | ||||
|     m_shortcutsDialog->show(); | ||||
| #else | ||||
|     QMessageBox::information(MainWindow::instance(), "discord-screenaudio", | ||||
|                              "Keybinds are not supported on this platform " | ||||
|                              "(KGlobalAccel is not available).", | ||||
|                              QMessageBox::Ok); | ||||
| #endif | ||||
| #else | ||||
|     QMessageBox::information(MainWindow::instance(), "discord-screenaudio", | ||||
|                              "Keybinds are not supported on this platform " | ||||
|                              "(KXmlGui and KGlobalAccel are not available).", | ||||
|                              QMessageBox::Ok); | ||||
| #endif | ||||
|   } else if (message.startsWith("dsa: ")) { | ||||
|     qDebug(userscriptLog) << message.mid(5).toUtf8().constData(); | ||||
|   } else { | ||||
|     qDebug(discordLog) << message; | ||||
|   auto colorSegments = message.split("%c"); | ||||
|   if (colorSegments[0] != "") { | ||||
|     for (auto line : colorSegments[0].split("\n")) | ||||
|       qDebug(discordLog) << line.toUtf8().constData(); | ||||
|   } | ||||
|   for (auto segment : colorSegments.mid(1)) { | ||||
|     auto lines = segment.split("\n"); | ||||
|     QString ansi; | ||||
|     uint endOfStyles = lines.length(); | ||||
|     for (auto line = 1; line < lines.length(); line++) { | ||||
|       if (!lines[line].endsWith(";")) { | ||||
|         endOfStyles = line; | ||||
|         break; | ||||
|       } | ||||
|       if (lines[line] == "font-weight: bold;") | ||||
|         ansi += "\033[1m"; | ||||
|       else if (lines[line].startsWith("color: ")) { | ||||
|         auto color = lines[line].mid(7).chopped(1); | ||||
|         if (cssAnsiColorMap.find(color) != cssAnsiColorMap.end()) | ||||
|           ansi += "\033[" + cssAnsiColorMap[color] + "m"; | ||||
|       } | ||||
|     } | ||||
|     qDebug(discordLog) << (ansi + lines[0].trimmed() + "\033[0m " + | ||||
|                            lines[endOfStyles].trimmed()) | ||||
|                               .toUtf8() | ||||
|                               .constData(); | ||||
|     for (auto line : lines.mid(endOfStyles + 1)) { | ||||
|       qDebug(discordLog) << line.toUtf8().constData(); | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | ||||
| void DiscordPage::startStream(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)); | ||||
|   }); | ||||
| } | ||||
| UserScript *DiscordPage::userScript() { return &m_userScript; } | ||||
|  | ||||
| void DiscordPage::toggleMute() { | ||||
|   qDebug(shortcutLog) << "Toggling mute"; | ||||
|   runJavaScript("window.discordScreenaudioToggleMute();"); | ||||
| } | ||||
| void DiscordPage::setupArrpc() { | ||||
|   QFile nodejs("/usr/bin/node"); | ||||
|   if (nodejs.exists()) { | ||||
|     auto arrpcSource = QTemporaryFile::createNativeFile(":/assets/arrpc.js"); | ||||
|     qDebug(mainLog).noquote() | ||||
|         << "NodeJS found, starting arRPC located at" << arrpcSource->fileName(); | ||||
|     m_arrpcProcess.setProcessChannelMode(QProcess::ForwardedChannels); | ||||
|     m_arrpcProcess.setProgram(nodejs.fileName()); | ||||
|     m_arrpcProcess.setArguments(QStringList{arrpcSource->fileName()}); | ||||
|     m_arrpcProcess.start(); | ||||
|  | ||||
| void DiscordPage::toggleDeafen() { | ||||
|   qDebug(shortcutLog) << "Toggling deafen"; | ||||
|   runJavaScript("window.discordScreenaudioToggleDeafen();"); | ||||
|     injectFile(&DiscordPage::injectScript, "arrpc_bridge_mod.js", | ||||
|                ":/assets/arrpc_bridge_mod.js"); | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -1,34 +1,35 @@ | ||||
| #pragma once | ||||
|  | ||||
| #include "streamdialog.h" | ||||
| #include "virtmic.h" | ||||
|  | ||||
| #ifdef KXMLGUI | ||||
| #include <KActionCollection> | ||||
| #include <KHelpMenu> | ||||
| #include <KShortcutsDialog> | ||||
| #endif | ||||
| #include "userscript.h" | ||||
|  | ||||
| #include <QDir> | ||||
| #include <QFile> | ||||
| #include <QNetworkAccessManager> | ||||
| #include <QProcess> | ||||
| #include <QStandardPaths> | ||||
| #include <QWebEngineFullScreenRequest> | ||||
| #include <QWebEnginePage> | ||||
| #include <QWebEngineScript> | ||||
|  | ||||
| class DiscordPage : public QWebEnginePage { | ||||
|   Q_OBJECT | ||||
|  | ||||
| public: | ||||
|   explicit DiscordPage(QWidget *parent = nullptr); | ||||
|   UserScript *userScript(); | ||||
|  | ||||
| private: | ||||
|   StreamDialog m_streamDialog; | ||||
|   QProcess m_virtmicProcess; | ||||
| #ifdef KXMLGUI | ||||
|   KHelpMenu *m_helpMenu; | ||||
| #ifdef KGLOBALACCEL | ||||
|   KActionCollection *m_actionCollection; | ||||
|   KShortcutsDialog *m_shortcutsDialog; | ||||
| #endif | ||||
| #endif | ||||
|   UserScript m_userScript; | ||||
|   QFile *m_userStylesFile; | ||||
|   QString m_userStylesContent; | ||||
|   QNetworkAccessManager m_networkAccessManager; | ||||
|   QProcess m_arrpcProcess; | ||||
|   const QDir m_configLocation = | ||||
|       QStandardPaths::writableLocation(QStandardPaths::AppConfigLocation); | ||||
|   void setupPermissions(); | ||||
|   void setupUserStyles(); | ||||
|   void setupArrpc(); | ||||
|   void fetchUserStyles(); | ||||
|   bool acceptNavigationRequest(const QUrl &url, | ||||
|                                QWebEnginePage::NavigationType type, | ||||
|                                bool isMainFrame) override; | ||||
| @@ -37,17 +38,19 @@ private: | ||||
|   javaScriptConsoleMessage(QWebEnginePage::JavaScriptConsoleMessageLevel level, | ||||
|                            const QString &message, int lineNumber, | ||||
|                            const QString &sourceID) override; | ||||
|   void injectScriptText(QString name, QString content); | ||||
|   void injectScriptFile(QString name, QString source); | ||||
|   void stopVirtmic(); | ||||
|   void startVirtmic(QString target); | ||||
|   void toggleMute(); | ||||
|   void toggleDeafen(); | ||||
|   void injectScript(QString name, QString content, | ||||
|                     QWebEngineScript::InjectionPoint injectionPoint); | ||||
|   void injectScript(QString name, QString content); | ||||
|   void injectStylesheet(QString name, QString content); | ||||
|   void injectFile(void (DiscordPage::*inject)(QString, QString), QString name, | ||||
|                   QString source); | ||||
|  | ||||
| private Q_SLOTS: | ||||
|   void featurePermissionRequested(const QUrl &securityOrigin, | ||||
|                                   QWebEnginePage::Feature feature); | ||||
|   void startStream(QString target, uint width, uint height, uint frameRate); | ||||
|  | ||||
| public Q_SLOTS: | ||||
|   void getUserStyles(QString url); | ||||
| }; | ||||
|  | ||||
| // Will immediately get destroyed again but is needed for navigation to | ||||
|   | ||||
| @@ -5,3 +5,4 @@ Q_LOGGING_CATEGORY(discordLog, "discord"); | ||||
| Q_LOGGING_CATEGORY(userscriptLog, "userscript"); | ||||
| Q_LOGGING_CATEGORY(virtmicLog, "virtmic"); | ||||
| Q_LOGGING_CATEGORY(shortcutLog, "shortcut"); | ||||
| Q_LOGGING_CATEGORY(userstylesLog, "userstyles"); | ||||
|   | ||||
| @@ -7,3 +7,4 @@ Q_DECLARE_LOGGING_CATEGORY(discordLog); | ||||
| Q_DECLARE_LOGGING_CATEGORY(userscriptLog); | ||||
| Q_DECLARE_LOGGING_CATEGORY(virtmicLog); | ||||
| Q_DECLARE_LOGGING_CATEGORY(shortcutLog); | ||||
| Q_DECLARE_LOGGING_CATEGORY(userstylesLog); | ||||
|   | ||||
							
								
								
									
										12
									
								
								src/main.cpp
									
									
									
									
									
								
							
							
						
						
									
										12
									
								
								src/main.cpp
									
									
									
									
									
								
							| @@ -24,19 +24,15 @@ int main(int argc, char *argv[]) { | ||||
|       "Custom Discord client with the ability to stream audio on Linux"); | ||||
|   parser.addHelpOption(); | ||||
|   parser.addVersionOption(); | ||||
|   QCommandLineOption virtmicOption("virtmic", "Start the Virtual Microphone", | ||||
|                                    "target"); | ||||
|   parser.addOption(virtmicOption); | ||||
|   QCommandLineOption degubOption("remote-debugging", | ||||
|                                  "Open Chromium Remote Debugging on port 9222"); | ||||
|   parser.addOption(degubOption); | ||||
|   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)); | ||||
|   } | ||||
|  | ||||
|   qputenv("QTWEBENGINE_CHROMIUM_FLAGS", | ||||
|           "--enable-features=WebRTCPipeWireCapturer " + | ||||
|               qgetenv("QTWEBENGINE_CHROMIUM_FLAGS")); | ||||
| @@ -46,7 +42,7 @@ int main(int argc, char *argv[]) { | ||||
|             "--remote-debugging-port=9222 " + | ||||
|                 qgetenv("QTWEBENGINE_CHROMIUM_FLAGS")); | ||||
|  | ||||
|   MainWindow w; | ||||
|   MainWindow w(parser.isSet(notifySendOption)); | ||||
|   w.show(); | ||||
|  | ||||
|   return app.exec(); | ||||
|   | ||||
| @@ -14,50 +14,33 @@ | ||||
| #include <QPushButton> | ||||
| #include <QSpacerItem> | ||||
| #include <QThread> | ||||
| #include <QTimer> | ||||
| #include <QUrl> | ||||
| #include <QWebEngineNotification> | ||||
| #include <QWebEngineProfile> | ||||
| #include <QWebEngineScript> | ||||
| #include <QWebEngineScriptCollection> | ||||
| #include <QWebEngineSettings> | ||||
| #include <QWebEngineFullScreenRequest> | ||||
| #include <QWidget> | ||||
|  | ||||
| MainWindow *MainWindow::m_instance = nullptr; | ||||
|  | ||||
| MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) { | ||||
| MainWindow::MainWindow(bool useNotifySend, QWidget *parent) | ||||
|     : QMainWindow(parent) { | ||||
|   assert(MainWindow::m_instance == nullptr); | ||||
|   MainWindow::m_instance = this; | ||||
|   setupWebView(); | ||||
|   resize(1000, 700); | ||||
|   showMaximized(); | ||||
| } | ||||
|  | ||||
| void MainWindow::setupWebView() { | ||||
|   auto page = new DiscordPage(this); | ||||
|   connect(page, &QWebEnginePage::fullScreenRequested, this, | ||||
|           &MainWindow::fullScreenRequested); | ||||
|  | ||||
|   m_webView = new QWebEngineView(this); | ||||
|   m_webView->setPage(page); | ||||
|  | ||||
| #ifdef KNOTIFICATIONS | ||||
|   QWebEngineProfile::defaultProfile()->setNotificationPresenter( | ||||
|       [&](std::unique_ptr<QWebEngineNotification> notificationInfo) { | ||||
|         KNotification *notification = new KNotification("discordNotification"); | ||||
|         notification->setTitle(notificationInfo->title()); | ||||
|         notification->setText(notificationInfo->message()); | ||||
|         notification->setPixmap(QPixmap::fromImage(notificationInfo->icon())); | ||||
|         notification->setDefaultAction("View"); | ||||
|         connect(notification, &KNotification::defaultActivated, | ||||
|                 [&, notificationInfo = std::move(notificationInfo)]() { | ||||
|                   notificationInfo->click(); | ||||
|                   activateWindow(); | ||||
|                 }); | ||||
|         notification->sendEvent(); | ||||
|       }); | ||||
| #endif | ||||
|  | ||||
|   setCentralWidget(m_webView); | ||||
|   setupSettings(); | ||||
|   m_settings->setValue("useNotifySend", useNotifySend); | ||||
|   m_centralWidget = new CentralWidget(this); | ||||
|   setCentralWidget(m_centralWidget); | ||||
|   setupTrayIcon(); | ||||
|   if (m_settings->contains("geometry")) { | ||||
|     restoreGeometry(m_settings->value("geometry").toByteArray()); | ||||
|   } else { | ||||
|     resize(1000, 700); | ||||
|     showMaximized(); | ||||
|   } | ||||
|   if (m_settings->value("trayIcon", false).toBool() && | ||||
|       m_settings->value("startHidden", false).toBool()) { | ||||
|     hide(); | ||||
|     QTimer::singleShot(0, [=]() { hide(); }); | ||||
|   } | ||||
| } | ||||
|  | ||||
| void MainWindow::fullScreenRequested( | ||||
| @@ -71,6 +54,79 @@ void MainWindow::fullScreenRequested( | ||||
|   } | ||||
| } | ||||
|  | ||||
| void MainWindow::closeEvent(QCloseEvent *event) { QApplication::quit(); } | ||||
| void MainWindow::setupTrayIcon() { | ||||
|   if (m_settings->value("trayIcon", false).toBool() == false || | ||||
|       m_trayIcon != nullptr) | ||||
|     return; | ||||
|  | ||||
|   auto aboutAction = new QAction( | ||||
|       "discord-screenaudio v" + QString(DISCORD_SCEENAUDIO_VERSION_FULL), this); | ||||
|   aboutAction->setIcon(QIcon(":assets/de.shorsh.discord-screenaudio.png")); | ||||
|   aboutAction->setEnabled(false); | ||||
|  | ||||
|   auto exitAction = new QAction("Exit", this); | ||||
|   connect(exitAction, &QAction::triggered, []() { QApplication::quit(); }); | ||||
|  | ||||
|   m_trayIconMenu = new QMenu(this); | ||||
|   m_trayIconMenu->addAction(aboutAction); | ||||
|   m_trayIconMenu->addAction(exitAction); | ||||
|  | ||||
|   m_trayIcon = new QSystemTrayIcon(this); | ||||
|   m_trayIcon->setContextMenu(m_trayIconMenu); | ||||
|   m_trayIcon->setIcon(QIcon(":assets/de.shorsh.discord-screenaudio.png")); | ||||
|   m_trayIcon->show(); | ||||
|  | ||||
|   connect(m_trayIcon, &QSystemTrayIcon::activated, [this](auto reason) { | ||||
|     if (reason == QSystemTrayIcon::Trigger) { | ||||
|       if (isVisible()) { | ||||
|         hide(); | ||||
|       } else { | ||||
|         show(); | ||||
|         activateWindow(); | ||||
|       } | ||||
|     } | ||||
|   }); | ||||
| } | ||||
|  | ||||
| void MainWindow::cleanTrayIcon() { | ||||
|   if (m_trayIcon == nullptr) | ||||
|     return; | ||||
|   m_trayIcon->hide(); | ||||
|   m_trayIconMenu->deleteLater(); | ||||
|   m_trayIcon->deleteLater(); | ||||
|   m_trayIconMenu = nullptr; | ||||
|   m_trayIcon = nullptr; | ||||
| } | ||||
|  | ||||
| void MainWindow::setupSettings() { | ||||
|   m_settings = | ||||
|       new QSettings("discord-screenaudio", "discord-screenaudio", this); | ||||
|   m_settings->beginGroup("settings"); | ||||
|   m_settings->endGroup(); | ||||
| } | ||||
|  | ||||
| QSettings *MainWindow::settings() const { return m_settings; } | ||||
|  | ||||
| void MainWindow::setTrayIcon(bool enabled) { | ||||
|   m_settings->setValue("trayIcon", enabled); | ||||
|   if (enabled) { | ||||
|     setupTrayIcon(); | ||||
|   } else { | ||||
|     cleanTrayIcon(); | ||||
|   } | ||||
| } | ||||
|  | ||||
| void MainWindow::closeEvent(QCloseEvent *event) { | ||||
|   if (m_settings->value("trayIcon", false).toBool()) { | ||||
|     hide(); | ||||
|   } else { | ||||
|     m_settings->setValue("geometry", saveGeometry()); | ||||
|     QApplication::quit(); | ||||
|   } | ||||
| } | ||||
|  | ||||
| MainWindow *MainWindow::instance() { return m_instance; } | ||||
|  | ||||
| CentralWidget *MainWindow::centralWidget() { | ||||
|   return instance()->m_centralWidget; | ||||
| }; | ||||
|   | ||||
| @@ -1,31 +1,40 @@ | ||||
| #pragma once | ||||
|  | ||||
| #include "discordpage.h" | ||||
| #include "centralwidget.h" | ||||
| #include "virtmic.h" | ||||
|  | ||||
| #include <QMainWindow> | ||||
| #include <QMenu> | ||||
| #include <QScopedPointer> | ||||
| #include <QSettings> | ||||
| #include <QString> | ||||
| #include <QSystemTrayIcon> | ||||
| #include <QVBoxLayout> | ||||
| #include <QVector> | ||||
| #include <QWebEnginePage> | ||||
| #include <QWebEngineProfile> | ||||
| #include <QWebEngineView> | ||||
|  | ||||
| class MainWindow : public QMainWindow { | ||||
|   Q_OBJECT | ||||
|  | ||||
| public: | ||||
|   explicit MainWindow(QWidget *parent = nullptr); | ||||
|   explicit MainWindow(bool useNotifySend = false, QWidget *parent = nullptr); | ||||
|   static MainWindow *instance(); | ||||
|   QSettings *settings() const; | ||||
|   static CentralWidget *centralWidget(); | ||||
|  | ||||
| private: | ||||
|   void setupWebView(); | ||||
|   QWebEngineView *m_webView; | ||||
|   void setupTrayIcon(); | ||||
|   void cleanTrayIcon(); | ||||
|   void setupSettings(); | ||||
|   QWebEngineProfile *prepareProfile(); | ||||
|   DiscordPage *m_discordPage; | ||||
|   void closeEvent(QCloseEvent *event) override; | ||||
|   QSystemTrayIcon *m_trayIcon = nullptr; | ||||
|   QMenu *m_trayIconMenu; | ||||
|   QSettings *m_settings; | ||||
|   bool m_wasMaximized; | ||||
|   static MainWindow *m_instance; | ||||
|   CentralWidget *m_centralWidget; | ||||
|  | ||||
| private Q_SLOTS: | ||||
| public Q_SLOTS: | ||||
|   void setTrayIcon(bool enabled); | ||||
|   void fullScreenRequested(QWebEngineFullScreenRequest fullScreenRequest); | ||||
| }; | ||||
|   | ||||
| @@ -1,4 +1,5 @@ | ||||
| #include "streamdialog.h" | ||||
| #include "mainwindow.h" | ||||
| #include "virtmic.h" | ||||
|  | ||||
| #include <QComboBox> | ||||
| @@ -9,73 +10,95 @@ | ||||
| #include <QSizePolicy> | ||||
| #include <QVBoxLayout> | ||||
|  | ||||
| StreamDialog::StreamDialog() : QWidget() { | ||||
| StreamDialog::StreamDialog(QWidget *parent) : QDialog(parent) { | ||||
|   setAttribute(Qt::WA_QuitOnClose, false); | ||||
|  | ||||
|   auto layout = new QVBoxLayout(this); | ||||
|   layout->setSizeConstraint(QLayout::SetFixedSize); | ||||
|   { | ||||
|     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); | ||||
|  | ||||
|   auto targetHBox = new QHBoxLayout(this); | ||||
|   layout->addLayout(targetHBox); | ||||
|     { | ||||
|       auto videoLayout = new QVBoxLayout(m_videoGroupBox); | ||||
|  | ||||
|   m_targetComboBox = new QComboBox(this); | ||||
|   updateTargets(); | ||||
|   targetHBox->addWidget(m_targetComboBox); | ||||
|       auto resolutionLabel = new QLabel(this); | ||||
|       resolutionLabel->setText("Resolution"); | ||||
|       videoLayout->addWidget(resolutionLabel); | ||||
|  | ||||
|   auto refreshTargetsButton = new QPushButton(this); | ||||
|   refreshTargetsButton->setFixedSize(30, 30); | ||||
|   refreshTargetsButton->setIcon(QIcon::fromTheme("view-refresh")); | ||||
|   connect(refreshTargetsButton, &QPushButton::clicked, this, | ||||
|           &StreamDialog::updateTargets); | ||||
|   targetHBox->addWidget(refreshTargetsButton); | ||||
|       m_resolutionComboBox = new QComboBox(this); | ||||
|       m_resolutionComboBox->addItem("2160p", "3840x2160"); | ||||
|       m_resolutionComboBox->addItem("1440p", "2560x1440"); | ||||
|       m_resolutionComboBox->addItem("1080p", "1920x1080"); | ||||
|       m_resolutionComboBox->addItem("720p", "1280x720"); | ||||
|       m_resolutionComboBox->addItem("480p", "854x480"); | ||||
|       m_resolutionComboBox->addItem("360p", "640x360"); | ||||
|       m_resolutionComboBox->addItem("240p", "426x240"); | ||||
|       m_resolutionComboBox->setCurrentText("720p"); | ||||
|       videoLayout->addWidget(m_resolutionComboBox); | ||||
|  | ||||
|   auto qualityLabel = new QLabel(this); | ||||
|   qualityLabel->setText("Stream Quality"); | ||||
|   layout->addWidget(qualityLabel); | ||||
|       auto framerateLabel = new QLabel(this); | ||||
|       framerateLabel->setText("Framerate"); | ||||
|       videoLayout->addWidget(framerateLabel); | ||||
|  | ||||
|   auto qualityHBox = new QHBoxLayout(this); | ||||
|   layout->addLayout(qualityHBox); | ||||
|       m_framerateComboBox = new QComboBox(this); | ||||
|       m_framerateComboBox->addItem("144 FPS", 144); | ||||
|       m_framerateComboBox->addItem("60 FPS", 60); | ||||
|       m_framerateComboBox->addItem("30 FPS", 30); | ||||
|       m_framerateComboBox->addItem("15 FPS", 15); | ||||
|       m_framerateComboBox->addItem("5 FPS", 5); | ||||
|       m_framerateComboBox->setCurrentText("30 FPS"); | ||||
|       videoLayout->addWidget(m_framerateComboBox); | ||||
|     } | ||||
|  | ||||
|   m_qualityResolutionComboBox = new QComboBox(this); | ||||
|   m_qualityResolutionComboBox->addItem("2160p", "3840x2160"); | ||||
|   m_qualityResolutionComboBox->addItem("1440p", "2560x1440"); | ||||
|   m_qualityResolutionComboBox->addItem("1080p", "1920x1080"); | ||||
|   m_qualityResolutionComboBox->addItem("720p", "1280x720"); | ||||
|   m_qualityResolutionComboBox->addItem("480p", "854x480"); | ||||
|   m_qualityResolutionComboBox->addItem("360p", "640x360"); | ||||
|   m_qualityResolutionComboBox->addItem("240p", "426x240"); | ||||
|   m_qualityResolutionComboBox->setCurrentText("720p"); | ||||
|   qualityHBox->addWidget(m_qualityResolutionComboBox); | ||||
|     m_audioGroupBox = new QGroupBox(this); | ||||
|     m_audioGroupBox->setCheckable(true); | ||||
|     m_audioGroupBox->setTitle("Audio"); | ||||
|     layout->addWidget(m_audioGroupBox); | ||||
|  | ||||
|   m_qualityFPSComboBox = new QComboBox(this); | ||||
|   m_qualityFPSComboBox->addItem("144 FPS", 144); | ||||
|   m_qualityFPSComboBox->addItem("60 FPS", 60); | ||||
|   m_qualityFPSComboBox->addItem("30 FPS", 30); | ||||
|   m_qualityFPSComboBox->addItem("15 FPS", 15); | ||||
|   m_qualityFPSComboBox->addItem("5 FPS", 5); | ||||
|   m_qualityFPSComboBox->setCurrentText("30 FPS"); | ||||
|   qualityHBox->addWidget(m_qualityFPSComboBox); | ||||
|     { | ||||
|       auto audioLayout = new QVBoxLayout(m_audioGroupBox); | ||||
|  | ||||
|   auto button = new QPushButton(this); | ||||
|   button->setText("Start Stream"); | ||||
|   connect(button, &QPushButton::clicked, this, &StreamDialog::startStream); | ||||
|   layout->addWidget(button, Qt::AlignRight | Qt::AlignBottom); | ||||
|       auto targetLabel = new QLabel(this); | ||||
|       targetLabel->setText("Audio Source"); | ||||
|       audioLayout->addWidget(targetLabel); | ||||
|  | ||||
|   setLayout(layout); | ||||
|       { | ||||
|         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"); | ||||
| } | ||||
|  | ||||
| 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); | ||||
| } | ||||
|  | ||||
| @@ -83,7 +106,6 @@ 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); | ||||
|   | ||||
| @@ -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(); | ||||
|   | ||||
							
								
								
									
										184
									
								
								src/userscript.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										184
									
								
								src/userscript.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,184 @@ | ||||
| #include "userscript.h" | ||||
| #include "log.h" | ||||
| #include "mainwindow.h" | ||||
|  | ||||
| #include <QApplication> | ||||
| #include <QDebug> | ||||
| #include <QFile> | ||||
| #include <QInputDialog> | ||||
| #include <QMessageBox> | ||||
| #include <QStandardPaths> | ||||
| #include <QTimer> | ||||
|  | ||||
| #ifdef KXMLGUI | ||||
| #include <KActionCollection> | ||||
| #endif | ||||
|  | ||||
| UserScript::UserScript() : QObject() { | ||||
|   setupHelpMenu(); | ||||
|   setupShortcutsDialog(); | ||||
|   setupStreamDialog(); | ||||
|   setupVirtmic(); | ||||
| } | ||||
|  | ||||
| void UserScript::setupHelpMenu() { | ||||
| #ifdef KXMLGUI | ||||
|   m_kxmlgui = true; | ||||
|  | ||||
|   KAboutData aboutData( | ||||
|       "discord-screenaudio", "discord-screenaudio", | ||||
|       QApplication::applicationVersion(), | ||||
|       "Custom Discord client with the ability to stream audio on Linux", | ||||
|       KAboutLicense::GPL_V3, "Copyright 2022 (C) Malte Jürgens"); | ||||
|   aboutData.setBugAddress("https://github.com/maltejur/discord-screenaudio"); | ||||
|   aboutData.addAuthor("Malte Jürgens", "Author", "maltejur@dismail.de", | ||||
|                       "https://github.com/maltejur"); | ||||
|   aboutData.addCredit("edisionnano", | ||||
|                       "For creating and documenting the approach for streaming " | ||||
|                       "audio in Discord used in this project.", | ||||
|                       QString(), | ||||
|                       "https://github.com/edisionnano/" | ||||
|                       "Screenshare-with-audio-on-Discord-with-Linux"); | ||||
|   aboutData.addCredit( | ||||
|       "Curve", "For creating the Rohrkabel library used in this project.", | ||||
|       QString(), "https://github.com/Curve"); | ||||
|   aboutData.addComponent("Rohrkabel", "A C++ RAII Pipewire-API Wrapper", "1.5", | ||||
|                          "https://github.com/Soundux/rohrkabel"); | ||||
|   aboutData.addComponent( | ||||
|       "Soundux/channel ", | ||||
|       "A C++ implementation of Rust's std::sync::mpsc::channel", nullptr, | ||||
|       "https://github.com/Soundux/channel"); | ||||
|   m_helpMenu = new KHelpMenu(MainWindow::instance(), aboutData); | ||||
| #endif | ||||
| } | ||||
|  | ||||
| void UserScript::setupShortcutsDialog() { | ||||
| #ifdef KXMLGUI | ||||
| #ifdef KGLOBALACCEL | ||||
|   m_kglobalaccel = true; | ||||
|  | ||||
|   auto toggleMuteAction = new QAction(this); | ||||
|   toggleMuteAction->setText("Toggle Mute"); | ||||
|   toggleMuteAction->setIcon(QIcon::fromTheme("microphone-sensitivity-muted")); | ||||
|   connect(toggleMuteAction, &QAction::triggered, this, | ||||
|           &UserScript::muteToggled); | ||||
|  | ||||
|   auto toggleDeafenAction = new QAction(this); | ||||
|   toggleDeafenAction->setText("Toggle Deafen"); | ||||
|   toggleDeafenAction->setIcon(QIcon::fromTheme("audio-volume-muted")); | ||||
|   connect(toggleDeafenAction, &QAction::triggered, this, | ||||
|           &UserScript::deafenToggled); | ||||
|  | ||||
|   m_actionCollection = new KActionCollection(this); | ||||
|   m_actionCollection->addAction("toggleMute", toggleMuteAction); | ||||
|   KGlobalAccel::setGlobalShortcut(toggleMuteAction, QList<QKeySequence>{}); | ||||
|   m_actionCollection->addAction("toggleDeafen", toggleDeafenAction); | ||||
|   KGlobalAccel::setGlobalShortcut(toggleDeafenAction, QList<QKeySequence>{}); | ||||
|  | ||||
|   m_shortcutsDialog = new KShortcutsDialog(KShortcutsEditor::GlobalAction); | ||||
|   m_shortcutsDialog->addCollection(m_actionCollection); | ||||
| #endif | ||||
| #endif | ||||
| } | ||||
|  | ||||
| void UserScript::setupStreamDialog() { | ||||
|   m_streamDialog = new StreamDialog(MainWindow::instance()); | ||||
|   connect(m_streamDialog, &StreamDialog::requestedStreamStart, this, | ||||
|           &UserScript::startStream); | ||||
| } | ||||
|  | ||||
| void UserScript::setupVirtmic() { | ||||
|   m_virtmicProcess.setProcessChannelMode(QProcess::ForwardedChannels); | ||||
| } | ||||
|  | ||||
| bool UserScript::isVirtmicRunning() { | ||||
|   return m_virtmicProcess.state() != QProcess::NotRunning; | ||||
| } | ||||
|  | ||||
| QString UserScript::version() { return QApplication::applicationVersion(); } | ||||
|  | ||||
| QVariant UserScript::getPref(QString name, QVariant fallback) { | ||||
|   return MainWindow::instance()->settings()->value(name, fallback); | ||||
| } | ||||
|  | ||||
| bool UserScript::getBoolPref(QString name, bool fallback) { | ||||
|   return getPref(name, fallback).toBool(); | ||||
| } | ||||
|  | ||||
| void UserScript::setPref(QString name, QVariant value) { | ||||
|   return MainWindow::instance()->settings()->setValue(name, value); | ||||
| } | ||||
|  | ||||
| void UserScript::setTrayIcon(bool value) { | ||||
|   setPref("trayIcon", value); | ||||
|   MainWindow::instance()->setTrayIcon(value); | ||||
| } | ||||
|  | ||||
| void UserScript::log(QString message) { | ||||
|   qDebug(userscriptLog) << message.toUtf8().constData(); | ||||
| } | ||||
|  | ||||
| void UserScript::showShortcutsDialog() { | ||||
| #ifdef KXMLGUI | ||||
| #ifdef KGLOBALACCEL | ||||
|   m_shortcutsDialog->show(); | ||||
| #else | ||||
|   QMessageBox::information(MainWindow::instance(), "discord-screenaudio", | ||||
|                            "Keybinds are not supported on this platform " | ||||
|                            "(KGlobalAccel is not available).", | ||||
|                            QMessageBox::Ok); | ||||
| #endif | ||||
| #else | ||||
|   QMessageBox::information(MainWindow::instance(), "discord-screenaudio", | ||||
|                            "Keybinds are not supported on this platform " | ||||
|                            "(KXmlGui and KGlobalAccel are not available).", | ||||
|                            QMessageBox::Ok); | ||||
| #endif | ||||
| } | ||||
|  | ||||
| void UserScript::showHelpMenu() { | ||||
| #ifdef KXMLGUI | ||||
|   m_helpMenu->aboutApplication(); | ||||
| #endif | ||||
| } | ||||
|  | ||||
| void UserScript::stopVirtmic() { | ||||
|   if (m_virtmicProcess.state() == QProcess::Running) { | ||||
|     qDebug(virtmicLog) << "Stopping Virtmic"; | ||||
|     m_virtmicProcess.kill(); | ||||
|     m_virtmicProcess.waitForFinished(); | ||||
|   } | ||||
| } | ||||
|  | ||||
| void UserScript::startVirtmic(QString target) { | ||||
|   qDebug(virtmicLog) << "Starting Virtmic with target" << target; | ||||
|   m_virtmicProcess.start(QApplication::arguments()[0], {"--virtmic", target}); | ||||
| } | ||||
|  | ||||
| void UserScript::startStream(bool video, bool audio, int width, int height, | ||||
|                              int frameRate, QString target) { | ||||
|   stopVirtmic(); | ||||
|   startVirtmic(audio ? target : "[None]"); | ||||
|   // Wait a bit for the virtmic to start | ||||
|   QTimer::singleShot( | ||||
|       200, [=]() { emit streamStarted(video, width, height, frameRate); }); | ||||
| } | ||||
|  | ||||
| void UserScript::showStreamDialog() { | ||||
|   if (m_streamDialog->isHidden()) | ||||
|     m_streamDialog->setHidden(false); | ||||
|   else | ||||
|     m_streamDialog->activateWindow(); | ||||
|   m_streamDialog->updateTargets(); | ||||
| } | ||||
|  | ||||
| void UserScript::showThemeDialog() { | ||||
|   auto url = QInputDialog::getText(MainWindow::instance(), "Theme Installation", | ||||
|                                    "Please enter the URL of the Theme"); | ||||
|   if (url != "") | ||||
|     emit shouldInstallUserStyles(url); | ||||
| } | ||||
|  | ||||
| void UserScript::installUserStyles(QString url) { | ||||
|   emit shouldInstallUserStyles(url); | ||||
| } | ||||
							
								
								
									
										83
									
								
								src/userscript.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										83
									
								
								src/userscript.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,83 @@ | ||||
| #pragma once | ||||
|  | ||||
| #include "streamdialog.h" | ||||
|  | ||||
| #include <QObject> | ||||
| #include <QProcess> | ||||
|  | ||||
| #ifdef KXMLGUI | ||||
| #include <KAboutData> | ||||
| #include <KHelpMenu> | ||||
| #include <KShortcutsDialog> | ||||
| #include <KXmlGuiWindow> | ||||
| #include <QAction> | ||||
|  | ||||
| #ifdef KGLOBALACCEL | ||||
| #include <KGlobalAccel> | ||||
| #endif | ||||
|  | ||||
| #endif | ||||
|  | ||||
| #ifdef KNOTIFICATIONS | ||||
| #include <KNotification> | ||||
| #endif | ||||
|  | ||||
| class UserScript : public QObject { | ||||
|   Q_OBJECT | ||||
|  | ||||
| public: | ||||
|   UserScript(); | ||||
|   bool isVirtmicRunning(); | ||||
|   Q_PROPERTY(QString version READ version CONSTANT); | ||||
|   Q_PROPERTY(bool kxmlgui MEMBER m_kxmlgui CONSTANT); | ||||
|   Q_PROPERTY(bool kglobalaccel MEMBER m_kglobalaccel CONSTANT); | ||||
|   Q_PROPERTY(QString userstyles MEMBER m_userstyles NOTIFY userstylesChanged); | ||||
|   Q_PROPERTY(QString loadingMessage MEMBER m_loadingMessage NOTIFY | ||||
|                  loadingMessageChanged); | ||||
|  | ||||
| private: | ||||
|   QProcess m_virtmicProcess; | ||||
|   StreamDialog *m_streamDialog; | ||||
|   bool m_kxmlgui = false; | ||||
|   bool m_kglobalaccel = false; | ||||
|   QString m_userstyles; | ||||
|   QString m_loadingMessage; | ||||
| #ifdef KXMLGUI | ||||
|   KHelpMenu *m_helpMenu; | ||||
| #ifdef KGLOBALACCEL | ||||
|   KActionCollection *m_actionCollection; | ||||
|   KShortcutsDialog *m_shortcutsDialog; | ||||
| #endif | ||||
| #endif | ||||
|   void setupHelpMenu(); | ||||
|   void setupShortcutsDialog(); | ||||
|   void setupStreamDialog(); | ||||
|   void setupVirtmic(); | ||||
|  | ||||
| Q_SIGNALS: | ||||
|   void muteToggled(); | ||||
|   void deafenToggled(); | ||||
|   void streamStarted(bool video, int width, int height, int frameRate); | ||||
|   void userstylesChanged(); | ||||
|   void loadingMessageChanged(QString message); | ||||
|   void shouldInstallUserStyles(QString url); | ||||
|  | ||||
| public Q_SLOTS: | ||||
|   void log(QString message); | ||||
|   QString version(); | ||||
|   QVariant getPref(QString name, QVariant fallback); | ||||
|   bool getBoolPref(QString name, bool fallback); | ||||
|   void setPref(QString name, QVariant value); | ||||
|   void setTrayIcon(bool value); | ||||
|   void showShortcutsDialog(); | ||||
|   void showHelpMenu(); | ||||
|   void showStreamDialog(); | ||||
|   void stopVirtmic(); | ||||
|   void startVirtmic(QString target); | ||||
|   void showThemeDialog(); | ||||
|   void installUserStyles(QString url); | ||||
|  | ||||
| private Q_SLOTS: | ||||
|   void startStream(bool video, bool audio, int width, int height, int frameRate, | ||||
|                    QString target); | ||||
| }; | ||||
							
								
								
									
										321
									
								
								src/virtmic.cpp
									
									
									
									
									
								
							
							
						
						
									
										321
									
								
								src/virtmic.cpp
									
									
									
									
									
								
							| @@ -1,175 +1,174 @@ | ||||
| #include "virtmic.h" | ||||
| #include "log.h" | ||||
|  | ||||
| #include <rohrkabel/loop/main.hpp> | ||||
| #include <rohrkabel/registry/registry.hpp> | ||||
| QThread virtmicThread; | ||||
| std::unique_ptr<pipewire::sender<Virtmic::set_target, Virtmic::terminate>> | ||||
|     senderr; | ||||
| std::unique_ptr<cr::receiver<Virtmic::new_targets>> receiverr; | ||||
|  | ||||
| namespace Virtmic { | ||||
|  | ||||
| const QStringList EXCLUDE_TARGETS{"Chromium input", "discord-screenaudio"}; | ||||
|  | ||||
| QVector<QString> getTargets() { | ||||
|   auto main_loop = pipewire::main_loop(); | ||||
|   auto context = pipewire::context(main_loop); | ||||
|   auto core = pipewire::core(context); | ||||
|   auto reg = pipewire::registry(core); | ||||
|  | ||||
|   QVector<QString> targets; | ||||
|  | ||||
|   auto reg_listener = reg.listen<pipewire::registry_listener>(); | ||||
|   reg_listener.on<pipewire::registry_event::global>( | ||||
|       [&](const pipewire::global &global) { | ||||
|         if (global.type == pipewire::node::type) { | ||||
|           auto node = reg.bind<pipewire::node>(global.id); | ||||
|           auto info = node.info(); | ||||
|           auto name = QString::fromStdString(info.props["application.name"]); | ||||
|  | ||||
|           if (name != "" && !EXCLUDE_TARGETS.contains(name) && | ||||
|               !targets.contains(name)) { | ||||
|             targets.append(name); | ||||
|           } | ||||
|         } | ||||
|       }); | ||||
|   core.update(); | ||||
|  | ||||
|   return targets; | ||||
| void Virtmic::instance() { | ||||
|   if (!virtmicThread.isRunning()) { | ||||
|     auto [main_sender, main_receiver] = cr::channel<new_targets>(); | ||||
|     auto [pw_sender, pw_receiver] = pipewire::channel<set_target, terminate>(); | ||||
|     Virtmic *virtmic = | ||||
|         new Virtmic(std::move(pw_receiver), std::move(main_sender)); | ||||
|     virtmic->moveToThread(&virtmicThread); | ||||
|     virtmicThread.start(); | ||||
|     QMetaObject::invokeMethod(virtmic, "run"); | ||||
|     receiverr = std::make_unique<cr::receiver<Virtmic::new_targets>>( | ||||
|         std::move(main_receiver)); | ||||
|     senderr = std::make_unique< | ||||
|         pipewire::sender<Virtmic::set_target, Virtmic::terminate>>( | ||||
|         std::move(pw_sender)); | ||||
|   } | ||||
| } | ||||
|  | ||||
| void start(QString _target) { | ||||
|   std::map<std::uint32_t, pipewire::port> ports; | ||||
|   std::unique_ptr<pipewire::port> virt_fl, virt_fr; | ||||
| void Virtmic::setTarget(QString target) { | ||||
|   senderr.get()->send<Virtmic::set_target>({target}); | ||||
| } | ||||
|  | ||||
|   std::map<std::uint32_t, pipewire::node_info> nodes; | ||||
|   std::map<std::uint32_t, pipewire::link_factory> links; | ||||
| void Virtmic::getTargets() { senderr.get()->send<Virtmic::get_targets>(); } | ||||
|  | ||||
|   auto main_loop = pipewire::main_loop(); | ||||
|   auto context = pipewire::context(main_loop); | ||||
|   auto core = pipewire::core(context); | ||||
|   auto reg = pipewire::registry(core); | ||||
|  | ||||
|   auto link = [&](const std::string &target, pipewire::core &core) { | ||||
|     for (const auto &[port_id, port] : ports) { | ||||
|       if (!virt_fl || !virt_fr) | ||||
|         continue; | ||||
|  | ||||
|       if (links.count(port_id)) | ||||
|         continue; | ||||
|  | ||||
|       if (port.info().direction == pipewire::port_direction::input) | ||||
|         continue; | ||||
|  | ||||
|       if (!port.info().props.count("node.id")) | ||||
|         continue; | ||||
|  | ||||
|       auto parent_id = std::stoul(port.info().props["node.id"]); | ||||
|  | ||||
|       if (!nodes.count(parent_id)) | ||||
|         continue; | ||||
|  | ||||
|       auto &parent = nodes.at(parent_id); | ||||
|       auto name = parent.props["application.name"]; | ||||
|  | ||||
|       if (name == target || | ||||
|           (target == "[All Desktop Audio]" && | ||||
|            !EXCLUDE_TARGETS.contains(QString::fromStdString(name)))) { | ||||
|         auto fl = port.info().props["audio.channel"] == "FL"; | ||||
|         links.emplace( | ||||
|             port_id, | ||||
|             core.create<pipewire::link_factory>( | ||||
|                 {fl ? virt_fl->info().id : virt_fr->info().id, port_id})); | ||||
|         qDebug(virtmicLog) << QString("Link: %1:%2 -> %3") | ||||
|                                   .arg(QString::fromStdString( | ||||
|                                       parent.props["application.name"])) | ||||
|                                   .arg(port_id) | ||||
|                                   .arg(fl ? virt_fl->info().id | ||||
|                                           : virt_fr->info().id) | ||||
|                                   .toUtf8() | ||||
|                                   .data(); | ||||
|       } | ||||
|     } | ||||
|   }; | ||||
|  | ||||
|   std::string target = _target.toLatin1().toStdString(); | ||||
|  | ||||
|   auto virtual_mic = core.create("adapter", | ||||
|                                  {{"node.name", "discord-screenaudio-virtmic"}, | ||||
|                                   {"media.class", "Audio/Source/Virtual"}, | ||||
|                                   {"factory.name", "support.null-audio-sink"}, | ||||
|                                   {"audio.channels", "2"}, | ||||
|                                   {"audio.position", "FL,FR"}}, | ||||
|                                  pipewire::node::type, pipewire::node::version, | ||||
|                                  pipewire::update_strategy::none); | ||||
|  | ||||
|   if (target == "[None]") { | ||||
|     while (true) { | ||||
|       main_loop.run(); | ||||
|     } | ||||
|     return; | ||||
|   } | ||||
|  | ||||
|   auto reg_events = reg.listen<pipewire::registry_listener>(); | ||||
|   reg_events.on<pipewire::registry_event::global>( | ||||
|       [&](const pipewire::global &global) { | ||||
|         if (global.type == pipewire::node::type) { | ||||
|           auto node = reg.bind<pipewire::node>(global.id); | ||||
|           if (!node.info().props.count("application.name")) | ||||
|             return; | ||||
|           qDebug(virtmicLog) << QString("Added: %1") | ||||
|                                     .arg(QString::fromStdString( | ||||
|                                         node.info().props["application.name"])) | ||||
|                                     .toUtf8() | ||||
|                                     .data(); | ||||
|  | ||||
|           if (!nodes.count(global.id)) { | ||||
|             nodes.emplace(global.id, node.info()); | ||||
|             link(target, core); | ||||
|           } | ||||
|         } | ||||
|         if (global.type == pipewire::port::type) { | ||||
|           auto port = reg.bind<pipewire::port>(global.id); | ||||
|           auto info = port.info(); | ||||
|  | ||||
|           if (info.props.count("node.id")) { | ||||
|             auto node_id = std::stoul(info.props["node.id"]); | ||||
|  | ||||
|             if (node_id == virtual_mic.id() && | ||||
|                 info.direction == pipewire::port_direction::input) { | ||||
|               if (info.props["audio.channel"] == "FL") { | ||||
|                 virt_fl = std::make_unique<pipewire::port>(std::move(port)); | ||||
|               } else { | ||||
|                 virt_fr = std::make_unique<pipewire::port>(std::move(port)); | ||||
|               } | ||||
|             } else { | ||||
|               ports.emplace(global.id, std::move(port)); | ||||
|             } | ||||
|  | ||||
|             link(target, core); | ||||
|           } | ||||
|         } | ||||
|       }); | ||||
|  | ||||
|   reg_events.on<pipewire::registry_event::global_removed>( | ||||
|       [&](const std::uint32_t id) { | ||||
|         if (nodes.count(id)) { | ||||
|           auto info = nodes.at(id); | ||||
|           qDebug(virtmicLog) << QString("Removed: %1") | ||||
|                                     .arg(QString::fromStdString( | ||||
|                                         info.props["application.name"].data())) | ||||
|                                     .toUtf8() | ||||
|                                     .data(); | ||||
|           nodes.erase(id); | ||||
|         } | ||||
|         if (ports.count(id)) { | ||||
|           ports.erase(id); | ||||
|         } | ||||
|         if (links.count(id)) { | ||||
|           links.erase(id); | ||||
|         } | ||||
|       }); | ||||
| Virtmic::Virtmic(pipewire::receiver<set_target, terminate> receiver, | ||||
|                  cr::sender<new_targets> sender) { | ||||
|   m_receiver = std::make_unique<pipewire::receiver<set_target, terminate>>( | ||||
|       std::move(receiver)); | ||||
|   m_sender = std::make_unique<cr::sender<new_targets>>(std::move(sender)); | ||||
|   virtual_mic = std::make_unique<pipewire::proxy>( | ||||
|       std::move(*core.create("adapter", | ||||
|                              {{"node.name", "discord-screenaudio-virtmic"}, | ||||
|                               {"media.class", "Audio/Source/Virtual"}, | ||||
|                               {"factory.name", "support.null-audio-sink"}, | ||||
|                               {"audio.channels", "2"}, | ||||
|                               {"audio.position", "FL,FR"}}, | ||||
|                              pipewire::node::type, pipewire::node::version, | ||||
|                              pipewire::update_strategy::none) | ||||
|                      .get())); | ||||
|   metadata_listener.on<pipewire::registry_event::global>( | ||||
|       [&](const auto &global) { globalEvent(global); }); | ||||
|   metadata_listener.on<pipewire::registry_event::global_removed>( | ||||
|       [&](const std::uint32_t id) { globalRemovedEvent(id); }); | ||||
| } | ||||
|  | ||||
| void Virtmic::run() { | ||||
|   while (true) { | ||||
|     main_loop.run(); | ||||
|   } | ||||
| } | ||||
|  | ||||
| } // namespace Virtmic | ||||
| void Virtmic::link() { | ||||
|   for (const auto &[port_id, port] : ports) { | ||||
|     auto port_info = port.info(); | ||||
|     if (!virt_fl || !virt_fr) | ||||
|       continue; | ||||
|  | ||||
|     if (links.count(port_id)) | ||||
|       continue; | ||||
|  | ||||
|     if (port_info.direction == pipewire::port_direction::input) | ||||
|       continue; | ||||
|  | ||||
|     if (!port_info.props.count("node.id")) | ||||
|       continue; | ||||
|  | ||||
|     auto parent_id = std::stoul(port_info.props["node.id"]); | ||||
|  | ||||
|     if (!nodes.count(parent_id)) | ||||
|       continue; | ||||
|  | ||||
|     auto &parent = nodes[parent_id]; | ||||
|     QString name; | ||||
|     if (parent.props.count("application.name") && | ||||
|         parent.props["application.name"] != "") | ||||
|       name = QString::fromStdString(parent.props["application.name"]); | ||||
|     else | ||||
|       name = QString::fromStdString(parent.props["application.process.binary"]); | ||||
|  | ||||
|     if (name == target || | ||||
|         (target == "[All Desktop Audio]" && !EXCLUDE_TARGETS.contains(name))) { | ||||
|       auto fl = port_info.props["audio.channel"] == "FL"; | ||||
|       links.emplace(port_id, | ||||
|                     *core.create_simple<pipewire::link>(fl ? virt_fl->info().id | ||||
|                                                            : virt_fr->info().id, | ||||
|                                                         port_id) | ||||
|                          .get()); | ||||
|       qDebug(virtmicLog) << QString("Link: %1:%2 -> %3") | ||||
|                                 .arg(name) | ||||
|                                 .arg(port_id) | ||||
|                                 .arg(fl ? virt_fl->info().id | ||||
|                                         : virt_fr->info().id) | ||||
|                                 .toUtf8() | ||||
|                                 .data(); | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | ||||
| void Virtmic::unlink() { links.clear(); } | ||||
|  | ||||
| void Virtmic::globalEvent(const pipewire::global &global) { | ||||
|   if (global.type == pipewire::node::type) { | ||||
|     auto node = *reg.bind<pipewire::node>(global.id).get(); | ||||
|     auto info = node.info(); | ||||
|     std::string name; | ||||
|     if (info.props.count("application.name") && | ||||
|         info.props["application.name"] != "") | ||||
|       name = info.props["application.name"]; | ||||
|     else if (info.props.count("application.process.binary")) { | ||||
|       name = info.props["application.process.binary"]; | ||||
|     } else | ||||
|       return; | ||||
|     qDebug(virtmicLog) << QString("Added: %1") | ||||
|                               .arg(QString::fromStdString(name)) | ||||
|                               .toUtf8() | ||||
|                               .data(); | ||||
|  | ||||
|     if (!nodes.count(global.id)) { | ||||
|       nodes.emplace(global.id, node.info()); | ||||
|       link(); | ||||
|     } | ||||
|   } | ||||
|   if (global.type == pipewire::port::type) { | ||||
|     auto port = *reg.bind<pipewire::port>(global.id).get(); | ||||
|     auto info = port.info(); | ||||
|  | ||||
|     if (info.props.count("node.id")) { | ||||
|       auto node_id = std::stoul(info.props["node.id"]); | ||||
|  | ||||
|       if (node_id == virtual_mic.get()->id() && | ||||
|           info.direction == pipewire::port_direction::input) { | ||||
|         if (info.props["audio.channel"] == "FL") { | ||||
|           virt_fl = std::make_unique<pipewire::port>(std::move(port)); | ||||
|         } else { | ||||
|           virt_fr = std::make_unique<pipewire::port>(std::move(port)); | ||||
|         } | ||||
|       } else { | ||||
|         ports.emplace(global.id, std::move(port)); | ||||
|       } | ||||
|  | ||||
|       link(); | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | ||||
| void Virtmic::globalRemovedEvent(const std::uint32_t id) { | ||||
|   if (nodes.count(id)) { | ||||
|     auto info = nodes.at(id); | ||||
|     std::string name; | ||||
|     if (info.props.count("application.name") && | ||||
|         info.props["application.name"] != "") | ||||
|       name = info.props["application.name"]; | ||||
|     else | ||||
|       name = info.props["application.process.binary"]; | ||||
|     qDebug(virtmicLog) << QString("Removed: %1") | ||||
|                               .arg(QString::fromStdString(name)) | ||||
|                               .toUtf8() | ||||
|                               .data(); | ||||
|     nodes.erase(id); | ||||
|   } | ||||
|   if (ports.count(id)) { | ||||
|     ports.erase(id); | ||||
|   } | ||||
|   if (links.count(id)) { | ||||
|     links.erase(id); | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -1,12 +1,63 @@ | ||||
| #pragma once | ||||
|  | ||||
| #include <QString> | ||||
| #include <QVector> | ||||
| #include <cr/channel.hpp> | ||||
| #include <iostream> | ||||
| #include <rohrkabel/channel/channel.hpp> | ||||
| #include <rohrkabel/main_loop.hpp> | ||||
| #include <rohrkabel/registry/registry.hpp> | ||||
|  | ||||
| namespace Virtmic { | ||||
| #include <QMap> | ||||
| #include <QScopedPointer> | ||||
| #include <QStringList> | ||||
| #include <QThread> | ||||
|  | ||||
| QVector<QString> getTargets(); | ||||
| void start(QString _target); | ||||
| class Virtmic : public QObject { | ||||
|   Q_OBJECT | ||||
| public: | ||||
|   static void setTarget(QString target); | ||||
|   static void getTargets(); | ||||
|  | ||||
| } // namespace Virtmic | ||||
| public: | ||||
|   struct set_target { | ||||
|     QString name; | ||||
|   }; | ||||
|   struct get_targets {}; | ||||
|   struct terminate {}; | ||||
|   struct new_targets { | ||||
|     QStringList targets; | ||||
|   }; | ||||
|  | ||||
| protected: | ||||
|   static void instance(); | ||||
|  | ||||
| protected: | ||||
|   Virtmic(pipewire::receiver<set_target, terminate> receiver, | ||||
|           cr::sender<new_targets> sender); | ||||
|   void run(); | ||||
|  | ||||
| private: | ||||
|   std::unique_ptr<pipewire::receiver<set_target, terminate>> m_receiver; | ||||
|   std::unique_ptr<cr::sender<new_targets>> m_sender; | ||||
|  | ||||
|   const QStringList EXCLUDE_TARGETS{"Chromium input", "discord-screenaudio"}; | ||||
|   QString target; | ||||
|  | ||||
|   pipewire::main_loop main_loop = pipewire::main_loop(); | ||||
|   pipewire::context context = pipewire::context(main_loop); | ||||
|   pipewire::core core = pipewire::core(context); | ||||
|   pipewire::registry reg = pipewire::registry(core); | ||||
|  | ||||
|   pipewire::registry_listener metadata_listener = | ||||
|       reg.listen<pipewire::registry_listener>(); | ||||
|   std::unique_ptr<pipewire::proxy> virtual_mic; | ||||
|  | ||||
|   std::map<uint32_t, pipewire::port> ports; | ||||
|   std::unique_ptr<pipewire::port> virt_fl, virt_fr; | ||||
|   std::map<uint32_t, pipewire::node_info> nodes; | ||||
|   std::map<uint32_t, pipewire::link> links; | ||||
|  | ||||
|   void link(); | ||||
|   void unlink(); | ||||
|   void globalEvent(const pipewire::global &global); | ||||
|   void globalRemovedEvent(const std::uint32_t id); | ||||
| }; | ||||
|   | ||||
							
								
								
									
										1
									
								
								submodules/channel
									
									
									
									
									
										Submodule
									
								
							
							
								
								
								
								
								
							
						
						
									
										1
									
								
								submodules/channel
									
									
									
									
									
										Submodule
									
								
							 Submodule submodules/channel added at 6977815409
									
								
							 Submodule submodules/rohrkabel updated: 04bfb921c4...8a7705be07
									
								
							
		Reference in New Issue
	
	Block a user