Compare commits
	
		
			3 Commits
		
	
	
		
			autogain
			...
			virtmic-re
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | e6449ad807 | ||
|  | e919e3ea25 | ||
|  | f31901073e | 
							
								
								
									
										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 | ||||
|   | ||||
| @@ -45,7 +45,6 @@ set(discord-screenaudio_SRC | ||||
|   src/log.cpp | ||||
|   src/userscript.cpp | ||||
|   src/centralwidget.cpp | ||||
|   src/localserver.cpp | ||||
|   resources.qrc | ||||
| ) | ||||
|  | ||||
| @@ -64,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 Qt::Widgets Qt::WebEngineWidgets rohrkabel) | ||||
| target_link_libraries(discord-screenaudio Qt::Widgets Qt::WebEngineWidgets rohrkabel channel) | ||||
|  | ||||
| if(KF5Notifications_FOUND) | ||||
|   target_link_libraries(discord-screenaudio KF5::Notifications) | ||||
|   | ||||
| @@ -62,9 +62,6 @@ You have multiple options: | ||||
| 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` | ||||
|  | ||||
| With dnf: | ||||
| `dnf install @development-tools cmake qt5-qtbase-devel qt5-qtwebengine-devel kf5-knotifications-devel kf5-kxmlgui-devel kf5-kglobalaccel-devel pkgconfig pipewire-devel git` | ||||
|  | ||||
| ### Building | ||||
|  | ||||
| First, clone the repository: | ||||
|   | ||||
| @@ -1,14 +1,12 @@ | ||||
| navigator.mediaDevices.chromiumGetDisplayMedia = | ||||
|   navigator.mediaDevices.getDisplayMedia; | ||||
| navigator.mediaDevices.chromiumGetUserMedia = | ||||
|   navigator.mediaDevices.getUserMedia; | ||||
|  | ||||
| function sleep(ms) { | ||||
|   return new Promise((resolve) => setTimeout(resolve, ms)); | ||||
| } | ||||
|  | ||||
| const getAudioDevice = async (nameOfAudioDevice) => { | ||||
|   await navigator.mediaDevices.chromiumGetUserMedia({ | ||||
|   await navigator.mediaDevices.getUserMedia({ | ||||
|     audio: true, | ||||
|   }); | ||||
|   let audioDevice; | ||||
| @@ -25,16 +23,6 @@ const getAudioDevice = async (nameOfAudioDevice) => { | ||||
|   return audioDevice; | ||||
| }; | ||||
|  | ||||
| function setGetUserMedia() { | ||||
|   const getUserMedia = async (constraints) => { | ||||
|     return await navigator.mediaDevices.chromiumGetUserMedia({ | ||||
|       video: constraints?.video || false, | ||||
|       audio: { ...constraints?.audio, autoGainControl }, | ||||
|     }); | ||||
|   }; | ||||
|   navigator.mediaDevices.getUserMedia = getUserMedia; | ||||
| } | ||||
|  | ||||
| function setGetDisplayMedia(video = true, overrideArgs = undefined) { | ||||
|   const getDisplayMedia = async (...args) => { | ||||
|     var id; | ||||
| @@ -46,27 +34,26 @@ function setGetDisplayMedia(video = true, overrideArgs = undefined) { | ||||
|     } catch (error) { | ||||
|       id = "default"; | ||||
|     } | ||||
|     let captureSystemAudioStream = | ||||
|       await navigator.mediaDevices.chromiumGetUserMedia({ | ||||
|         audio: { | ||||
|           // We add our audio constraints here, to get a list of supported constraints use navigator.mediaDevices.getSupportedConstraints(); | ||||
|           // We must capture a microphone, we use default since its the only deviceId that is the same for every Chromium user | ||||
|           deviceId: { | ||||
|             exact: id, | ||||
|           }, | ||||
|           // We want auto gain control, noise cancellation and noise suppression disabled so that our stream won't sound bad | ||||
|           autoGainControl: false, | ||||
|           echoCancellation: false, | ||||
|           noiseSuppression: false, | ||||
|           // By default Chromium sets channel count for audio devices to 1, we want it to be stereo in case we find a way for Discord to accept stereo screenshare too | ||||
|           channelCount: 2, | ||||
|           // You can set more audio constraints here, bellow are some examples | ||||
|           //latency: 0, | ||||
|           //sampleRate: 48000, | ||||
|           //sampleSize: 16, | ||||
|           //volume: 1.0 | ||||
|     let captureSystemAudioStream = await navigator.mediaDevices.getUserMedia({ | ||||
|       audio: { | ||||
|         // We add our audio constraints here, to get a list of supported constraints use navigator.mediaDevices.getSupportedConstraints(); | ||||
|         // We must capture a microphone, we use default since its the only deviceId that is the same for every Chromium user | ||||
|         deviceId: { | ||||
|           exact: id, | ||||
|         }, | ||||
|       }); | ||||
|         // We want auto gain control, noise cancellation and noise suppression disabled so that our stream won't sound bad | ||||
|         autoGainControl: false, | ||||
|         echoCancellation: false, | ||||
|         noiseSuppression: false, | ||||
|         // By default Chromium sets channel count for audio devices to 1, we want it to be stereo in case we find a way for Discord to accept stereo screenshare too | ||||
|         channelCount: 2, | ||||
|         // You can set more audio constraints here, bellow are some examples | ||||
|         //latency: 0, | ||||
|         //sampleRate: 48000, | ||||
|         //sampleSize: 16, | ||||
|         //volume: 1.0 | ||||
|       }, | ||||
|     }); | ||||
|     let [track] = captureSystemAudioStream.getAudioTracks(); | ||||
|     const gdm = await navigator.mediaDevices.chromiumGetDisplayMedia( | ||||
|       ...(overrideArgs | ||||
| @@ -81,7 +68,6 @@ function setGetDisplayMedia(video = true, overrideArgs = undefined) { | ||||
| } | ||||
|  | ||||
| setGetDisplayMedia(); | ||||
| setGetUserMedia(); | ||||
|  | ||||
| let userscript; | ||||
| let muteBtn; | ||||
| @@ -90,7 +76,6 @@ let streamStartBtn; | ||||
| let streamStartBtnInitialDisplay; | ||||
| let streamStartBtnClone; | ||||
| let resolutionString; | ||||
| let autoGainControl = true; | ||||
| const clonedElements = []; | ||||
| const hiddenElements = []; | ||||
| let wasStreamActive = false; | ||||
| @@ -174,10 +159,6 @@ function main() { | ||||
|     streamStartBtnClone.remove(); | ||||
|   }); | ||||
|  | ||||
|   userscript.getPref("disableAutomaticGain", false).then((disabled) => { | ||||
|     autoGainControl = !disabled; | ||||
|   }); | ||||
|  | ||||
|   function updateUserstyles() { | ||||
|     userscript.log("Loading userstyles..."); | ||||
|     userscript.loadingMessage = "Loading userstyles..."; | ||||
| @@ -387,29 +368,5 @@ function main() { | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     for (const el of document.getElementsByClassName("sensitivity-3A7Gs9")) { | ||||
|       if ( | ||||
|         el.getElementsByTagName("div").length > 0 && | ||||
|         !document.getElementById("discord-screenaudio-gaintoggle") | ||||
|       ) { | ||||
|         const toggle = createSwitch( | ||||
|           "Disable automatic gain", | ||||
|           await userscript.getPref("disableAutomaticGain", false), | ||||
|           async (disabled) => { | ||||
|             await userscript.setPref("disableAutomaticGain", disabled); | ||||
|             autoGainControl = !disabled; | ||||
|             setGetUserMedia(); | ||||
|             if (disabled) | ||||
|               userscript.showInformation( | ||||
|                 "discord-screenaudio", | ||||
|                 "If you are currently in a call, this setting may only take effect after you rejoin the call or restart discord-screenaudio." | ||||
|               ); | ||||
|           } | ||||
|         ); | ||||
|         toggle.id = "discord-screenaudio-gaintoggle"; | ||||
|         el.getElementsByTagName("div")[0].appendChild(toggle); | ||||
|       } | ||||
|     } | ||||
|   }, 500); | ||||
| } | ||||
|   | ||||
| @@ -282,11 +282,10 @@ void DiscordPage::javaScriptConsoleMessage( | ||||
|           ansi += "\033[" + cssAnsiColorMap[color] + "m"; | ||||
|       } | ||||
|     } | ||||
|     if (endOfStyles < lines.length()) | ||||
|       qDebug(discordLog) << (ansi + lines[0].trimmed() + "\033[0m " + | ||||
|                              lines[endOfStyles].trimmed()) | ||||
|                                 .toUtf8() | ||||
|                                 .constData(); | ||||
|     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(); | ||||
|     } | ||||
|   | ||||
| @@ -1,22 +0,0 @@ | ||||
| #include "localserver.h" | ||||
|  | ||||
| bool isProgramRunning(const QString &program_name) { | ||||
|   QLocalSocket socket; | ||||
|   socket.connectToServer(program_name); | ||||
|   if (socket.waitForConnected()) { | ||||
|     return true; // program is already running | ||||
|   } | ||||
|   return false; | ||||
| } | ||||
|  | ||||
| void showErrorMessage(const char *text) { | ||||
|   QMessageBox msgBox; | ||||
|  | ||||
|   msgBox.setIcon(QMessageBox::Critical); | ||||
|   msgBox.setText(text); | ||||
|   msgBox.setStandardButtons(QMessageBox::Ok); | ||||
|   msgBox.setDefaultButton(QMessageBox::Ok); | ||||
|   msgBox.setWindowIcon(QIcon(":assets/de.shorsh.discord-screenaudio.png")); | ||||
|  | ||||
|   msgBox.exec(); | ||||
| } | ||||
| @@ -1,9 +0,0 @@ | ||||
| #pragma once | ||||
| #include "mainwindow.h" | ||||
|  | ||||
| #include <QLocalServer> | ||||
| #include <QLocalSocket> | ||||
| #include <QMessageBox> | ||||
|  | ||||
| bool isProgramRunning(const QString &program_name); | ||||
| void showErrorMessage(const char *text); | ||||
							
								
								
									
										26
									
								
								src/main.cpp
									
									
									
									
									
								
							
							
						
						
									
										26
									
								
								src/main.cpp
									
									
									
									
									
								
							| @@ -1,4 +1,3 @@ | ||||
| #include "localserver.h" | ||||
| #include "mainwindow.h" | ||||
| #include "virtmic.h" | ||||
|  | ||||
| @@ -8,14 +7,10 @@ | ||||
|  | ||||
| #include <QApplication> | ||||
| #include <QCommandLineParser> | ||||
| #include <QLocalServer> | ||||
| #include <QLocalSocket> | ||||
| #include <QLoggingCategory> | ||||
| #include <QMessageBox> | ||||
|  | ||||
| int main(int argc, char *argv[]) { | ||||
|   QApplication app(argc, argv); | ||||
|  | ||||
|   QApplication::setApplicationName("discord-screenaudio"); | ||||
|   QApplication::setWindowIcon( | ||||
|       QIcon(":assets/de.shorsh.discord-screenaudio.png")); | ||||
| @@ -29,9 +24,6 @@ int main(int argc, char *argv[]) { | ||||
|       "Custom Discord client with the ability to stream audio on Linux"); | ||||
|   parser.addHelpOption(); | ||||
|   parser.addVersionOption(); | ||||
|   QCommandLineOption virtmicOption("virtmic", "Start the Virtual Microphone", | ||||
|                                    "target"); | ||||
|   parser.addOption(virtmicOption); | ||||
|   QCommandLineOption degubOption("remote-debugging", | ||||
|                                  "Open Chromium Remote Debugging on port 9222"); | ||||
|   parser.addOption(degubOption); | ||||
| @@ -41,10 +33,6 @@ int main(int argc, char *argv[]) { | ||||
|  | ||||
|   parser.process(app); | ||||
|  | ||||
|   if (parser.isSet(virtmicOption)) { | ||||
|     Virtmic::start(parser.value(virtmicOption)); | ||||
|   } | ||||
|  | ||||
|   qputenv("QTWEBENGINE_CHROMIUM_FLAGS", | ||||
|           "--enable-features=WebRTCPipeWireCapturer " + | ||||
|               qgetenv("QTWEBENGINE_CHROMIUM_FLAGS")); | ||||
| @@ -55,20 +43,6 @@ int main(int argc, char *argv[]) { | ||||
|                 qgetenv("QTWEBENGINE_CHROMIUM_FLAGS")); | ||||
|  | ||||
|   MainWindow w(parser.isSet(notifySendOption)); | ||||
|  | ||||
|   // Check if discord is already running | ||||
|   QString program_name = "discord-screenaudio"; | ||||
|   if (isProgramRunning(program_name)) { | ||||
|     // if running show error message | ||||
|     showErrorMessage("discord-screenaudio is already running"); | ||||
|     return 1; | ||||
|   } | ||||
|  | ||||
|   // open server so we can check if discord is running | ||||
|   QLocalServer server; | ||||
|   server.listen(program_name); | ||||
|   QObject::connect(&server, &QLocalServer::newConnection, []() {}); | ||||
|  | ||||
|   w.show(); | ||||
|  | ||||
|   return app.exec(); | ||||
|   | ||||
| @@ -30,7 +30,6 @@ MainWindow::MainWindow(bool useNotifySend, QWidget *parent) | ||||
|   m_centralWidget = new CentralWidget(this); | ||||
|   setCentralWidget(m_centralWidget); | ||||
|   setupTrayIcon(); | ||||
|   setMinimumSize(800, 300); | ||||
|   if (m_settings->contains("geometry")) { | ||||
|     restoreGeometry(m_settings->value("geometry").toByteArray()); | ||||
|   } else { | ||||
|   | ||||
| @@ -1,6 +1,7 @@ | ||||
| #pragma once | ||||
|  | ||||
| #include "centralwidget.h" | ||||
| #include "virtmic.h" | ||||
|  | ||||
| #include <QMainWindow> | ||||
| #include <QMenu> | ||||
|   | ||||
| @@ -1,4 +1,5 @@ | ||||
| #include "streamdialog.h" | ||||
| #include "mainwindow.h" | ||||
| #include "virtmic.h" | ||||
|  | ||||
| #include <QComboBox> | ||||
|   | ||||
| @@ -42,12 +42,12 @@ void UserScript::setupHelpMenu() { | ||||
|   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", | ||||
|   aboutData.addComponent("Rohrkabel", "A C++ RAII Pipewire-API Wrapper", "1.5", | ||||
|                          "https://github.com/Soundux/rohrkabel"); | ||||
|   aboutData.addComponent("arRPC", | ||||
|                          "An open implementation of Discord's local RPC " | ||||
|                          "servers<br>Copyright (c) 2022 OpenAsar", | ||||
|                          nullptr, "https://github.com/OpenAsar/arrpc"); | ||||
|   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 | ||||
| } | ||||
| @@ -182,7 +182,3 @@ void UserScript::showThemeDialog() { | ||||
| void UserScript::installUserStyles(QString url) { | ||||
|   emit shouldInstallUserStyles(url); | ||||
| } | ||||
|  | ||||
| void UserScript::showInformation(QString title, QString message) { | ||||
|   QMessageBox::information(MainWindow::instance(), title, message); | ||||
| } | ||||
|   | ||||
| @@ -72,7 +72,6 @@ public Q_SLOTS: | ||||
|   void showShortcutsDialog(); | ||||
|   void showHelpMenu(); | ||||
|   void showStreamDialog(); | ||||
|   void showInformation(QString title, QString message); | ||||
|   void stopVirtmic(); | ||||
|   void startVirtmic(QString target); | ||||
|   void showThemeDialog(); | ||||
|   | ||||
							
								
								
									
										340
									
								
								src/virtmic.cpp
									
									
									
									
									
								
							
							
						
						
									
										340
									
								
								src/virtmic.cpp
									
									
									
									
									
								
							| @@ -1,194 +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"}; | ||||
|  | ||||
| const std::string nullstr = ""; | ||||
| const std::string &getTarget(const pipewire::spa::dict &props) { | ||||
|   if (props.count("media.class") && | ||||
|       props.at("media.class") == "Stream/Output/Audio") { | ||||
|     if (props.count("application.name") && props.at("application.name") != "") | ||||
|       return props.at("application.name"); | ||||
|     else if (props.count("application.process.binary") && | ||||
|              props.at("application.process.binary") != "") | ||||
|       return props.at("application.process.binary"); | ||||
|     else | ||||
|       return props.at("node.name"); | ||||
|   } else | ||||
|     return nullstr; | ||||
| } | ||||
|  | ||||
| QString qGetTarget(const pipewire::spa::dict &props) { | ||||
|   return QString::fromStdString(getTarget(props)); | ||||
| } | ||||
|  | ||||
| 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(); | ||||
|           QString name = qGetTarget(info.props); | ||||
|           if (name != "" && !EXCLUDE_TARGETS.contains(name) && | ||||
|               !targets.contains(name)) { | ||||
|             targets.append(name); | ||||
|           } | ||||
|         } | ||||
|       }); | ||||
|   core.update(); | ||||
|  | ||||
|   return targets; | ||||
| } | ||||
|  | ||||
| void start(QString _target) { | ||||
|   std::map<std::uint32_t, pipewire::port> ports; | ||||
|   std::unique_ptr<pipewire::port> virt_fl, virt_fr; | ||||
|  | ||||
|   std::map<std::uint32_t, pipewire::node_info> nodes; | ||||
|   std::map<std::uint32_t, pipewire::link_factory> links; | ||||
|  | ||||
|   auto main_loop = pipewire::main_loop(); | ||||
|   auto context = pipewire::context(main_loop); | ||||
|   auto core = pipewire::core(context); | ||||
|   auto reg = pipewire::registry(core); | ||||
|  | ||||
|   auto link = [&](const std::string &target, pipewire::core &core) { | ||||
|     for (const auto &[port_id, port] : ports) { | ||||
|       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); | ||||
|       std::string name = getTarget(parent.props); | ||||
|       if (name == target || | ||||
|           (target == "[All Desktop Audio]" && | ||||
|            !EXCLUDE_TARGETS.contains(QString::fromStdString(name)))) { | ||||
|         auto fl = port.info().props["audio.channel"] == "FL"; | ||||
|         links.emplace( | ||||
|             port_id, | ||||
|             core.create<pipewire::link_factory>( | ||||
|                 {fl ? virt_fl->info().id : virt_fr->info().id, port_id})); | ||||
|         qDebug(virtmicLog) << QString("Link: %1:%2 -> %3") | ||||
|                                   .arg(QString::fromStdString(name)) | ||||
|                                   .arg(port_id) | ||||
|                                   .arg(fl ? virt_fl->info().id | ||||
|                                           : virt_fr->info().id) | ||||
|                                   .toUtf8() | ||||
|                                   .data(); | ||||
|       } | ||||
|     } | ||||
|   }; | ||||
|  | ||||
|   std::string target = _target.toUtf8().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; | ||||
| 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)); | ||||
|   } | ||||
| } | ||||
|  | ||||
|   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); | ||||
|           auto info = node.info(); | ||||
|           std::string name = getTarget(info.props); | ||||
|           if (name == nullstr) | ||||
|             return; | ||||
|           qDebug(virtmicLog) << QString("Added: %1") | ||||
|                                     .arg(QString::fromStdString(name)) | ||||
|                                     .toUtf8() | ||||
|                                     .data(); | ||||
| void Virtmic::setTarget(QString target) { | ||||
|   senderr.get()->send<Virtmic::set_target>({target}); | ||||
| } | ||||
|  | ||||
|           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(); | ||||
| void Virtmic::getTargets() { senderr.get()->send<Virtmic::get_targets>(); } | ||||
|  | ||||
|           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); | ||||
|           std::string name = getTarget(info.props); | ||||
|           if (name == nullstr) | ||||
|             return; | ||||
|           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); | ||||
|         } | ||||
|       }); | ||||
| 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