Initial commit

This commit is contained in:
Malte Jürgens 2022-07-07 13:11:51 +02:00
commit db0f3233b2
No known key found for this signature in database
GPG key ID: D29FBD5F93C0CFC3
11 changed files with 431 additions and 0 deletions

1
.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/build

32
CMakeLists.txt Normal file
View file

@ -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)

14
README Normal file
View file

@ -0,0 +1,14 @@
-- Build instructions --
mkdir build
cd build
cmake ../
make
./discord-screenaudio
-- Tutorials and resources --
Qt online docs
https://doc.qt.io/

View file

@ -0,0 +1,6 @@
[Desktop Entry]
Type=Application
Name=discord-screenaudio
Exec=discord-screenaudio
Icon=discord
Terminal=false

62
assets/userscript.js Normal file
View file

@ -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;

6
resources.qrc Normal file
View file

@ -0,0 +1,6 @@
<!DOCTYPE RCC>
<RCC>
<qresource>
<file>assets/userscript.js</file>
</qresource>
</RCC>

10
src/main.cpp Normal file
View 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
View 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
View 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
View 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
View 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