Compare commits
10 Commits
v1.7.1
...
virtmic-re
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e6449ad807 | ||
|
|
e919e3ea25 | ||
|
|
10fff86ca4 | ||
|
|
87e84dec5c | ||
|
|
4b782133ae | ||
|
|
423884ae0c | ||
|
|
7ea3d0aab1 | ||
|
|
18b15f5cf4 | ||
|
|
3540774c82 | ||
|
|
f31901073e |
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
|
||||
|
||||
@@ -63,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)
|
||||
|
||||
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);
|
||||
@@ -1,65 +0,0 @@
|
||||
setTimeout(e => {
|
||||
let Dispatcher, lookupAsset, lookupApp, apps = {};
|
||||
|
||||
const ws = new WebSocket('ws://127.0.0.1:1337'); // connect to arRPC bridge websocket
|
||||
ws.onmessage = async x => {
|
||||
msg = JSON.parse(x.data);
|
||||
console.log(msg);
|
||||
|
||||
if (!Dispatcher) {
|
||||
const wpRequire = window.webpackChunkdiscord_app.push([[Symbol()], {}, x => x]);
|
||||
const cache = wpRequire.c;
|
||||
window.webpackChunkdiscord_app.pop();
|
||||
|
||||
for (const id in cache) {
|
||||
let mod = cache[id].exports;
|
||||
mod = mod && (mod.Z ?? mod.ZP);
|
||||
|
||||
if (mod && mod.register && mod.wait) {
|
||||
Dispatcher = mod;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const factories = wpRequire.m;
|
||||
for (const id in factories) {
|
||||
if (factories[id].toString().includes('getAssetImage: size must === [number, number] for Twitch')) {
|
||||
const mod = wpRequire(id);
|
||||
|
||||
const _lookupAsset = Object.values(mod).find(e => typeof e === "function" && e.toString().includes("apply("));
|
||||
lookupAsset = async (appId, name) => (await _lookupAsset(appId, [name, undefined]))[0];
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
for (const id in factories) {
|
||||
if (factories[id].toString().includes(`e.application={`)) {
|
||||
const mod = wpRequire(id);
|
||||
|
||||
const _lookupApp = Object.values(mod).find(e => typeof e === "function" && e.toString().includes(`e.application={`));
|
||||
lookupApp = async appId => {
|
||||
let socket = {};
|
||||
await _lookupApp(socket, appId);
|
||||
return socket.application;
|
||||
};
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (msg.activity?.assets?.large_image) msg.activity.assets.large_image = await lookupAsset(msg.activity.application_id, msg.activity.assets.large_image);
|
||||
if (msg.activity?.assets?.small_image) msg.activity.assets.small_image = await lookupAsset(msg.activity.application_id, msg.activity.assets.small_image);
|
||||
|
||||
if (msg.activity) {
|
||||
const appId = msg.activity.application_id;
|
||||
if (!apps[appId]) apps[appId] = await lookupApp(appId);
|
||||
|
||||
const app = apps[appId];
|
||||
if (!msg.activity.name) msg.activity.name = app.name;
|
||||
}
|
||||
|
||||
Dispatcher.dispatch({ type: "LOCAL_ACTIVITY_UPDATE", ...msg }); // set RPC status
|
||||
};
|
||||
}, 10000)
|
||||
@@ -2,7 +2,8 @@
|
||||
<RCC>
|
||||
<qresource>
|
||||
<file>assets/userscript.js</file>
|
||||
<file>assets/bridge_mod.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
|
||||
@@ -9,6 +9,7 @@
|
||||
#include <QFileInfo>
|
||||
#include <QMessageBox>
|
||||
#include <QNetworkReply>
|
||||
#include <QTemporaryFile>
|
||||
#include <QTimer>
|
||||
#include <QWebChannel>
|
||||
#include <QWebEngineScript>
|
||||
@@ -35,11 +36,9 @@ DiscordPage::DiscordPage(QWidget *parent) : QWebEnginePage(parent) {
|
||||
|
||||
injectFile(&DiscordPage::injectScript, "userscript.js",
|
||||
":/assets/userscript.js");
|
||||
|
||||
injectFile(&DiscordPage::injectScript, "bridge_mod.js",
|
||||
":/assets/bridge_mod.js");
|
||||
|
||||
setupUserStyles();
|
||||
setupArrpc();
|
||||
}
|
||||
|
||||
void DiscordPage::setupPermissions() {
|
||||
@@ -294,3 +293,19 @@ void DiscordPage::javaScriptConsoleMessage(
|
||||
}
|
||||
|
||||
UserScript *DiscordPage::userScript() { return &m_userScript; }
|
||||
|
||||
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();
|
||||
|
||||
injectFile(&DiscordPage::injectScript, "arrpc_bridge_mod.js",
|
||||
":/assets/arrpc_bridge_mod.js");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#include <QDir>
|
||||
#include <QFile>
|
||||
#include <QNetworkAccessManager>
|
||||
#include <QProcess>
|
||||
#include <QStandardPaths>
|
||||
#include <QWebEngineFullScreenRequest>
|
||||
#include <QWebEnginePage>
|
||||
@@ -22,10 +23,12 @@ private:
|
||||
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,
|
||||
|
||||
@@ -24,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);
|
||||
@@ -36,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"));
|
||||
|
||||
@@ -99,7 +99,8 @@ void MainWindow::cleanTrayIcon() {
|
||||
}
|
||||
|
||||
void MainWindow::setupSettings() {
|
||||
m_settings = new QSettings("maltejur", "discord-screenaudio", this);
|
||||
m_settings =
|
||||
new QSettings("discord-screenaudio", "discord-screenaudio", this);
|
||||
m_settings->beginGroup("settings");
|
||||
m_settings->endGroup();
|
||||
}
|
||||
|
||||
@@ -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,8 +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(
|
||||
"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
|
||||
}
|
||||
|
||||
342
src/virtmic.cpp
342
src/virtmic.cpp
@@ -1,196 +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();
|
||||
QString name;
|
||||
if (info.props.count("application.name") &&
|
||||
info.props["application.name"] != "")
|
||||
name = QString::fromStdString(info.props["application.name"]);
|
||||
else
|
||||
name = QString::fromStdString(
|
||||
info.props["application.process.binary"]);
|
||||
|
||||
if (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);
|
||||
std::string name;
|
||||
if (parent.props.count("application.name") &&
|
||||
parent.props["application.name"] != "")
|
||||
name = parent.props["application.name"];
|
||||
else
|
||||
name = parent.props["application.process.binary"];
|
||||
|
||||
if (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.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);
|
||||
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(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);
|
||||
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);
|
||||
}
|
||||
});
|
||||
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