From db0f3233b273f42293132fc4821e78dfe60c5b7f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Malte=20J=C3=BCrgens?= Date: Thu, 7 Jul 2022 13:11:51 +0200 Subject: [PATCH] Initial commit --- .gitignore | 1 + CMakeLists.txt | 32 ++++++ README | 14 +++ assets/discord-screenaudio.desktop | 6 ++ assets/userscript.js | 62 ++++++++++++ resources.qrc | 6 ++ src/main.cpp | 10 ++ src/mainwindow.cpp | 109 +++++++++++++++++++++ src/mainwindow.h | 27 ++++++ src/virtmic.cpp | 151 +++++++++++++++++++++++++++++ src/virtmic.h | 13 +++ 11 files changed, 431 insertions(+) create mode 100644 .gitignore create mode 100644 CMakeLists.txt create mode 100644 README create mode 100644 assets/discord-screenaudio.desktop create mode 100644 assets/userscript.js create mode 100644 resources.qrc create mode 100644 src/main.cpp create mode 100644 src/mainwindow.cpp create mode 100644 src/mainwindow.h create mode 100644 src/virtmic.cpp create mode 100644 src/virtmic.h diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..796b96d --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/build diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..c367829 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,32 @@ +cmake_minimum_required(VERSION 3.0) +project(discord-screenaudio) + +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_INCLUDE_CURRENT_DIR ON) +set(CMAKE_AUTOMOC ON) +set(CMAKE_AUTORCC ON) +# set(CMAKE_AUTOUIC ON) + +find_package(Qt5 CONFIG REQUIRED COMPONENTS + Widgets + WebEngineWidgets +) + +set(discord-screenaudio_SRC + src/main.cpp + src/mainwindow.cpp + src/virtmic.cpp + resources.qrc +) + +include(FetchContent) + +FetchContent_Declare(rohrkabel GIT_REPOSITORY "https://github.com/Soundux/rohrkabel") +FetchContent_MakeAvailable(rohrkabel) + +add_executable(discord-screenaudio ${discord-screenaudio_SRC}) + +target_link_libraries(discord-screenaudio Qt5::Widgets Qt5::WebEngineWidgets rohrkabel) + +install(TARGETS discord-screenaudio DESTINATION bin) +install(PROGRAMS assets/discord-screenaudio.desktop DESTINATION ${CMAKE_INSTALL_PREFIX}/share/applications) diff --git a/README b/README new file mode 100644 index 0000000..24f6f10 --- /dev/null +++ b/README @@ -0,0 +1,14 @@ +-- Build instructions -- + +mkdir build +cd build +cmake ../ +make + +./discord-screenaudio + + +-- Tutorials and resources -- + +Qt online docs +https://doc.qt.io/ diff --git a/assets/discord-screenaudio.desktop b/assets/discord-screenaudio.desktop new file mode 100644 index 0000000..9308c0c --- /dev/null +++ b/assets/discord-screenaudio.desktop @@ -0,0 +1,6 @@ +[Desktop Entry] +Type=Application +Name=discord-screenaudio +Exec=discord-screenaudio +Icon=discord +Terminal=false diff --git a/assets/userscript.js b/assets/userscript.js new file mode 100644 index 0000000..3bec773 --- /dev/null +++ b/assets/userscript.js @@ -0,0 +1,62 @@ +// ==UserScript== +// @name Screenshare with Audio +// @namespace https://github.com/edisionnano +// @version 0.4 +// @description Screenshare with Audio on Discord +// @author Guest271314 and Samantas5855 +// @match https://*.discord.com/* +// @icon https://www.google.com/s2/favicons?domain=discord.com +// @grant none +// @license MIT +// ==/UserScript== + +/* jshint esversion: 8 */ + +navigator.mediaDevices.chromiumGetDisplayMedia = + navigator.mediaDevices.getDisplayMedia; + +const getAudioDevice = async (nameOfAudioDevice) => { + await navigator.mediaDevices.getUserMedia({ + audio: true, + }); + let devices = await navigator.mediaDevices.enumerateDevices(); + let audioDevice = devices.find(({ label }) => label === nameOfAudioDevice); + return audioDevice; +}; + +const getDisplayMedia = async (...args) => { + var id; + try { + let myDiscordAudioSink = await getAudioDevice( + "discord-screenaudio-virtmic" + ); + id = myDiscordAudioSink.deviceId; + } catch (error) { + id = "default"; + } + 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(...args); + gdm.addTrack(track); + return gdm; +}; +navigator.mediaDevices.getDisplayMedia = getDisplayMedia; diff --git a/resources.qrc b/resources.qrc new file mode 100644 index 0000000..87e5201 --- /dev/null +++ b/resources.qrc @@ -0,0 +1,6 @@ + + + + assets/userscript.js + + \ No newline at end of file diff --git a/src/main.cpp b/src/main.cpp new file mode 100644 index 0000000..ededf0d --- /dev/null +++ b/src/main.cpp @@ -0,0 +1,10 @@ +#include "mainwindow.h" +#include + +int main(int argc, char *argv[]) { + QApplication app(argc, argv); + MainWindow w; + w.show(); + + return app.exec(); +} diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp new file mode 100644 index 0000000..e3e8469 --- /dev/null +++ b/src/mainwindow.cpp @@ -0,0 +1,109 @@ +#include "mainwindow.h" +#include "virtmic.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +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; diff --git a/src/mainwindow.h b/src/mainwindow.h new file mode 100644 index 0000000..e8b8520 --- /dev/null +++ b/src/mainwindow.h @@ -0,0 +1,27 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +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); +}; diff --git a/src/virtmic.cpp b/src/virtmic.cpp new file mode 100644 index 0000000..f1d1881 --- /dev/null +++ b/src/virtmic.cpp @@ -0,0 +1,151 @@ +#include "virtmic.h" + +namespace Virtmic { + +QVector getTargets() { + auto main_loop = pipewire::main_loop(); + auto context = pipewire::context(main_loop); + auto core = pipewire::core(context); + auto reg = pipewire::registry(core); + + QVector targets; + + auto reg_listener = reg.listen(); + reg_listener.on( + [&](const pipewire::global &global) { + if (global.type == pipewire::node::type) { + auto node = reg.bind(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 ports; + std::unique_ptr virt_fl, virt_fr; + + std::map nodes; + std::map 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( + {virt_fl->info().id, port_id})); + std::cout << "[virtmic] " << virt_fl->info().id << std::endl; + } else { + links.emplace(port_id, core.create( + {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(); + reg_events.on( + [&](const pipewire::global &global) { + if (global.type == pipewire::node::type) { + auto node = reg.bind(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(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(std::move(port)); + } else { + virt_fr = std::make_unique(std::move(port)); + } + } else { + ports.emplace(global.id, std::move(port)); + } + + link(target, core); + } + } + }); + + reg_events.on( + [&](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 diff --git a/src/virtmic.h b/src/virtmic.h new file mode 100644 index 0000000..0aeb9d2 --- /dev/null +++ b/src/virtmic.h @@ -0,0 +1,13 @@ +#pragma once + +#include +#include +#include +#include + +namespace Virtmic { + +QVector getTargets(); +void start(QString _target); + +} // namespace Virtmic \ No newline at end of file