Initial commit
This commit is contained in:
10
src/main.cpp
Normal file
10
src/main.cpp
Normal file
@@ -0,0 +1,10 @@
|
||||
#include "mainwindow.h"
|
||||
#include <QApplication>
|
||||
|
||||
int main(int argc, char *argv[]) {
|
||||
QApplication app(argc, argv);
|
||||
MainWindow w;
|
||||
w.show();
|
||||
|
||||
return app.exec();
|
||||
}
|
||||
109
src/mainwindow.cpp
Normal file
109
src/mainwindow.cpp
Normal file
@@ -0,0 +1,109 @@
|
||||
#include "mainwindow.h"
|
||||
#include "virtmic.h"
|
||||
|
||||
#include <QComboBox>
|
||||
#include <QFile>
|
||||
#include <QGridLayout>
|
||||
#include <QLabel>
|
||||
#include <QPushButton>
|
||||
#include <QSpacerItem>
|
||||
#include <QThread>
|
||||
#include <QUrl>
|
||||
#include <QWebEngineScript>
|
||||
#include <QWebEngineScriptCollection>
|
||||
#include <QWebEngineSettings>
|
||||
#include <QWidget>
|
||||
|
||||
MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) {
|
||||
auto centralWidget = new QWidget;
|
||||
|
||||
auto layout = new QGridLayout;
|
||||
layout->setAlignment(Qt::AlignCenter);
|
||||
|
||||
auto label = new QLabel;
|
||||
label->setText("Which app do you want to stream sound from?");
|
||||
|
||||
auto comboBox = new QComboBox;
|
||||
for (auto target : Virtmic::getTargets()) {
|
||||
comboBox->addItem(target);
|
||||
}
|
||||
|
||||
auto button = new QPushButton;
|
||||
button->setText("Confirm");
|
||||
connect(button, &QPushButton::clicked, [=]() {
|
||||
auto target = comboBox->currentText();
|
||||
auto thread = QThread::create([=]() { Virtmic::start(target); });
|
||||
thread->start();
|
||||
setupWebView();
|
||||
});
|
||||
|
||||
layout->addWidget(label, 0, 0);
|
||||
layout->addWidget(comboBox, 1, 0);
|
||||
layout->addWidget(button, 2, 0, Qt::AlignRight);
|
||||
centralWidget->setLayout(layout);
|
||||
setCentralWidget(centralWidget);
|
||||
resize(1000, 700);
|
||||
showMaximized();
|
||||
}
|
||||
|
||||
void MainWindow::setupWebView() {
|
||||
m_webView = new QWebEngineView(this);
|
||||
|
||||
// TODO: Custom QWebEnginePage that implements acceptNavigationRequest
|
||||
connect(m_webView->page(), &QWebEnginePage::featurePermissionRequested, this,
|
||||
&MainWindow::featurePermissionRequested);
|
||||
m_webView->settings()->setAttribute(QWebEngineSettings::ScreenCaptureEnabled,
|
||||
true);
|
||||
m_webView->settings()->setAttribute(
|
||||
QWebEngineSettings::JavascriptCanOpenWindows, true);
|
||||
m_webView->settings()->setAttribute(
|
||||
QWebEngineSettings::AllowRunningInsecureContent, true);
|
||||
m_webView->settings()->setAttribute(
|
||||
QWebEngineSettings::AllowWindowActivationFromJavaScript, true);
|
||||
m_webView->settings()->setAttribute(
|
||||
QWebEngineSettings::FullScreenSupportEnabled, true);
|
||||
m_webView->settings()->setAttribute(
|
||||
QWebEngineSettings::PlaybackRequiresUserGesture, false);
|
||||
|
||||
m_webView->setUrl(QUrl("https://discord.com/app"));
|
||||
|
||||
const char *userscriptSrc = ":/assets/userscript.js";
|
||||
QFile userscript(userscriptSrc);
|
||||
|
||||
if (!userscript.open(QIODevice::ReadOnly)) {
|
||||
qFatal("Failed to load %s with error: %s", userscriptSrc,
|
||||
userscript.errorString().toLatin1().constData());
|
||||
} else {
|
||||
QByteArray userscriptJs = userscript.readAll();
|
||||
|
||||
QWebEngineScript script;
|
||||
|
||||
script.setSourceCode(userscriptJs);
|
||||
script.setName("userscript.js");
|
||||
script.setWorldId(QWebEngineScript::MainWorld);
|
||||
script.setInjectionPoint(QWebEngineScript::DocumentCreation);
|
||||
script.setRunsOnSubFrames(false);
|
||||
|
||||
m_webView->page()->scripts().insert(script);
|
||||
}
|
||||
|
||||
setCentralWidget(m_webView);
|
||||
}
|
||||
|
||||
void MainWindow::featurePermissionRequested(const QUrl &securityOrigin,
|
||||
QWebEnginePage::Feature feature) {
|
||||
// if (feature == QWebEnginePage::MediaAudioCapture ||
|
||||
// feature == QWebEnginePage::MediaVideoCapture ||
|
||||
// feature == QWebEnginePage::MediaAudioVideoCapture ||
|
||||
// feature == QWebEnginePage::DesktopVideoCapture ||
|
||||
// feature == QWebEnginePage::DesktopAudioVideoCapture)
|
||||
// m_webView->page()->setFeaturePermission(
|
||||
// securityOrigin, feature, QWebEnginePage::PermissionGrantedByUser);
|
||||
// else
|
||||
// m_webView->page()->setFeaturePermission(
|
||||
// securityOrigin, feature, QWebEnginePage::PermissionDeniedByUser);
|
||||
m_webView->page()->setFeaturePermission(
|
||||
securityOrigin, feature, QWebEnginePage::PermissionGrantedByUser);
|
||||
}
|
||||
|
||||
MainWindow::~MainWindow() = default;
|
||||
27
src/mainwindow.h
Normal file
27
src/mainwindow.h
Normal file
@@ -0,0 +1,27 @@
|
||||
#pragma once
|
||||
|
||||
#include <QMainWindow>
|
||||
#include <QScopedPointer>
|
||||
#include <QString>
|
||||
#include <QVector>
|
||||
#include <QWebEnginePage>
|
||||
#include <QWebEngineProfile>
|
||||
#include <QWebEngineView>
|
||||
|
||||
class MainWindow : public QMainWindow {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit MainWindow(QWidget *parent = nullptr);
|
||||
~MainWindow() override;
|
||||
|
||||
private:
|
||||
void setupWebView();
|
||||
QWebEngineView *m_webView;
|
||||
QWebEngineProfile *prepareProfile();
|
||||
QThread *m_virtmicThread;
|
||||
|
||||
private Q_SLOTS:
|
||||
void featurePermissionRequested(const QUrl &securityOrigin,
|
||||
QWebEnginePage::Feature feature);
|
||||
};
|
||||
151
src/virtmic.cpp
Normal file
151
src/virtmic.cpp
Normal file
@@ -0,0 +1,151 @@
|
||||
#include "virtmic.h"
|
||||
|
||||
namespace Virtmic {
|
||||
|
||||
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();
|
||||
|
||||
if (info.props.count("node.name")) {
|
||||
auto name = QString::fromStdString(info.props["node.name"]);
|
||||
if (!targets.contains(name))
|
||||
targets.append(name);
|
||||
}
|
||||
}
|
||||
});
|
||||
core.sync();
|
||||
|
||||
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> 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);
|
||||
|
||||
if (parent.info().props["node.name"].find(target) != std::string::npos) {
|
||||
std::cout << "[virtmic] "
|
||||
<< "Link : " << target << ":" << port_id << " -> ";
|
||||
|
||||
if (port.info().props["audio.channel"] == "FL") {
|
||||
links.emplace(port_id, core.create<pipewire::link_factory>(
|
||||
{virt_fl->info().id, port_id}));
|
||||
std::cout << "[virtmic] " << virt_fl->info().id << std::endl;
|
||||
} else {
|
||||
links.emplace(port_id, core.create<pipewire::link_factory>(
|
||||
{virt_fr->info().id, port_id}));
|
||||
std::cout << "[virtmic] " << virt_fr->info().id << std::endl;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
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, false);
|
||||
|
||||
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);
|
||||
std::cout << "[virtmic] "
|
||||
<< "Added : " << node.info().props["node.name"]
|
||||
<< std::endl;
|
||||
|
||||
if (!nodes.count(global.id)) {
|
||||
nodes.emplace(global.id, std::move(node));
|
||||
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).info();
|
||||
std::cout << "[virtmic] "
|
||||
<< "Removed: " << info.props["node.name"] << std::endl;
|
||||
nodes.erase(id);
|
||||
}
|
||||
if (ports.count(id)) {
|
||||
ports.erase(id);
|
||||
}
|
||||
if (links.count(id)) {
|
||||
links.erase(id);
|
||||
}
|
||||
});
|
||||
|
||||
while (true) {
|
||||
main_loop.run();
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace Virtmic
|
||||
13
src/virtmic.h
Normal file
13
src/virtmic.h
Normal file
@@ -0,0 +1,13 @@
|
||||
#pragma once
|
||||
|
||||
#include <QString>
|
||||
#include <QVector>
|
||||
#include <iostream>
|
||||
#include <rohrkabel/registry/registry.hpp>
|
||||
|
||||
namespace Virtmic {
|
||||
|
||||
QVector<QString> getTargets();
|
||||
void start(QString _target);
|
||||
|
||||
} // namespace Virtmic
|
||||
Reference in New Issue
Block a user