Initial commit
This commit is contained in:
commit
db0f3233b2
|
@ -0,0 +1 @@
|
||||||
|
/build
|
|
@ -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)
|
|
@ -0,0 +1,14 @@
|
||||||
|
-- Build instructions --
|
||||||
|
|
||||||
|
mkdir build
|
||||||
|
cd build
|
||||||
|
cmake ../
|
||||||
|
make
|
||||||
|
|
||||||
|
./discord-screenaudio
|
||||||
|
|
||||||
|
|
||||||
|
-- Tutorials and resources --
|
||||||
|
|
||||||
|
Qt online docs
|
||||||
|
https://doc.qt.io/
|
|
@ -0,0 +1,6 @@
|
||||||
|
[Desktop Entry]
|
||||||
|
Type=Application
|
||||||
|
Name=discord-screenaudio
|
||||||
|
Exec=discord-screenaudio
|
||||||
|
Icon=discord
|
||||||
|
Terminal=false
|
|
@ -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;
|
|
@ -0,0 +1,6 @@
|
||||||
|
<!DOCTYPE RCC>
|
||||||
|
<RCC>
|
||||||
|
<qresource>
|
||||||
|
<file>assets/userscript.js</file>
|
||||||
|
</qresource>
|
||||||
|
</RCC>
|
|
@ -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();
|
||||||
|
}
|
|
@ -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;
|
|
@ -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);
|
||||||
|
};
|
|
@ -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
|
|
@ -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
|
Loading…
Reference in New Issue