Initial commit
This commit is contained in:
commit
db0f3233b2
11 changed files with 431 additions and 0 deletions
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
/build
|
32
CMakeLists.txt
Normal file
32
CMakeLists.txt
Normal 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
14
README
Normal 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/
|
6
assets/discord-screenaudio.desktop
Normal file
6
assets/discord-screenaudio.desktop
Normal 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
62
assets/userscript.js
Normal 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
6
resources.qrc
Normal file
|
@ -0,0 +1,6 @@
|
|||
<!DOCTYPE RCC>
|
||||
<RCC>
|
||||
<qresource>
|
||||
<file>assets/userscript.js</file>
|
||||
</qresource>
|
||||
</RCC>
|
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
|
Loading…
Reference in a new issue