Implement Stream Dialog

This commit is contained in:
Malte Jürgens 2022-07-09 22:07:14 +02:00
parent 81317fd43d
commit 67087e8251
No known key found for this signature in database
GPG key ID: D29FBD5F93C0CFC3
10 changed files with 394 additions and 140 deletions

View file

@ -5,6 +5,7 @@ set(CMAKE_CXX_STANDARD 17)
set(CMAKE_INCLUDE_CURRENT_DIR ON)
set(CMAKE_AUTOMOC ON)
set(CMAKE_AUTORCC ON)
set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -DDEBUG")
# set(CMAKE_AUTOUIC ON)
find_package(Qt5 CONFIG REQUIRED COMPONENTS
@ -16,6 +17,8 @@ set(discord-screenaudio_SRC
src/main.cpp
src/mainwindow.cpp
src/virtmic.cpp
src/discordpage.cpp
src/streamdialog.cpp
resources.qrc
)

View file

@ -1,16 +1,4 @@
// ==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 */
// From v0.4
navigator.mediaDevices.chromiumGetDisplayMedia =
navigator.mediaDevices.getDisplayMedia;
@ -24,39 +12,115 @@ const getAudioDevice = async (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,
function setGetDisplayMedia(overrideArgs = undefined) {
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
},
// 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;
});
let [track] = captureSystemAudioStream.getAudioTracks();
const gdm = await navigator.mediaDevices.chromiumGetDisplayMedia(
...(overrideArgs
? [overrideArgs]
: args || [{ video: true, audio: true }])
);
gdm.addTrack(track);
return gdm;
};
navigator.mediaDevices.getDisplayMedia = getDisplayMedia;
}
setGetDisplayMedia();
const clonedElements = [];
const hiddenElements = [];
let wasStreamActive = false;
setInterval(() => {
const streamActive =
document.getElementsByClassName("panel-2ZFCRb activityPanel-9icbyU")
.length > 0;
if (!streamActive && wasStreamActive)
console.log("!discord-screenaudio-stream-stopped");
wasStreamActive = streamActive;
if (streamActive) {
clonedElements.forEach((el) => {
el.remove();
});
clonedElements.length = 0;
hiddenElements.forEach((el) => {
el.style.display = "block";
});
hiddenElements.length = 0;
} else {
for (const el of document.querySelectorAll(
'[aria-label="Share Your Screen"]'
)) {
elClone = el.cloneNode(true);
elClone.ariaLabel = "Share Your Screen with Audio";
elClone.title = "Share Your Screen with Audio";
elClone.addEventListener("click", () => {
console.log("!discord-screenaudio-start-stream");
});
const initialDisplay = el.style.display;
window.discordScreenaudioStartStream = (width, height, frameRate) => {
setGetDisplayMedia({
audio: true,
video: { width, height, frameRate },
});
el.click();
el.style.display = initialDisplay;
elClone.remove();
};
el.style.display = "none";
el.parentNode.insertBefore(elClone, el);
clonedElements.push(elClone);
hiddenElements.push(el);
}
}
if (
document.getElementsByClassName("dirscordScreenaudioAboutText").length == 0
) {
for (const el of document.getElementsByClassName("info-3pQQBb")) {
const aboutEl = document.createElement("div");
aboutEl.innerText = "discord-screenaudio v1.0.0-alpha";
aboutEl.style.fontSize = "12px";
aboutEl.style.color = "var(--text-muted)";
aboutEl.classList.add("dirscordScreenaudioAboutText");
el.appendChild(aboutEl);
}
}
}, 1000);

119
src/discordpage.cpp Normal file
View file

@ -0,0 +1,119 @@
#include "discordpage.h"
#include "virtmic.h"
#include <QApplication>
#include <QDesktopServices>
#include <QFile>
#include <QTimer>
#include <QWebChannel>
#include <QWebEngineScript>
#include <QWebEngineScriptCollection>
#include <QWebEngineSettings>
DiscordPage::DiscordPage(QWidget *parent) : QWebEnginePage(parent) {
setBackgroundColor(QColor("#202225"));
m_virtmicProcess.setProcessChannelMode(QProcess::ForwardedChannels);
connect(this, &QWebEnginePage::featurePermissionRequested, this,
&DiscordPage::featurePermissionRequested);
settings()->setAttribute(QWebEngineSettings::ScreenCaptureEnabled, true);
settings()->setAttribute(QWebEngineSettings::JavascriptCanOpenWindows, true);
settings()->setAttribute(QWebEngineSettings::AllowRunningInsecureContent,
true);
settings()->setAttribute(
QWebEngineSettings::AllowWindowActivationFromJavaScript, true);
settings()->setAttribute(QWebEngineSettings::FullScreenSupportEnabled, true);
settings()->setAttribute(QWebEngineSettings::PlaybackRequiresUserGesture,
false);
setUrl(QUrl("https://discord.com/app"));
injectScript(":/assets/userscript.js");
connect(&m_streamDialog, &StreamDialog::requestedStreamStart, this,
&DiscordPage::startStream);
}
void DiscordPage::injectScript(QString source) {
qDebug() << "[main ] Injecting " << source;
QFile userscript(source);
if (!userscript.open(QIODevice::ReadOnly)) {
qFatal("Failed to load %s with error: %s", source.toLatin1().constData(),
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);
scripts().insert(script);
}
}
void DiscordPage::featurePermissionRequested(const QUrl &securityOrigin,
QWebEnginePage::Feature feature) {
// Allow every permission asked
setFeaturePermission(securityOrigin, feature,
QWebEnginePage::PermissionGrantedByUser);
}
bool DiscordPage::acceptNavigationRequest(const QUrl &url,
QWebEnginePage::NavigationType type,
bool isMainFrame) {
qDebug() << url;
if (type == QWebEnginePage::NavigationTypeLinkClicked) {
QDesktopServices::openUrl(url);
return false;
}
return true;
};
void DiscordPage::stopVirtmic() {
if (m_virtmicProcess.state() == QProcess::Running) {
qDebug() << "[virtmic] Stopping Virtmic";
m_virtmicProcess.kill();
}
}
void DiscordPage::startVirtmic(QString target) {
if (target != "") {
qDebug() << "[virtmic] Starting Virtmic with target" << target;
m_virtmicProcess.start(QApplication::arguments()[0], {"--virtmic", target});
}
}
void DiscordPage::javaScriptConsoleMessage(
QWebEnginePage::JavaScriptConsoleMessageLevel level, const QString &message,
int lineNumber, const QString &sourceID) {
if (message == "!discord-screenaudio-start-stream") {
if (m_streamDialog.isHidden())
m_streamDialog.setHidden(false);
else
m_streamDialog.activateWindow();
} else if (message == "!discord-screenaudio-stream-stopped") {
stopVirtmic();
} else {
qDebug() << "[discord]" << message;
}
}
void DiscordPage::startStream(QString target, uint width, uint height,
uint frameRate) {
stopVirtmic();
startVirtmic(target);
// Wait a bit for the virtmic to start
QTimer::singleShot(target == "" ? 0 : 200, [=]() {
runJavaScript(QString("window.discordScreenaudioStartStream(%1, %2, %3);")
.arg(width)
.arg(height)
.arg(frameRate));
});
}

33
src/discordpage.h Normal file
View file

@ -0,0 +1,33 @@
#pragma once
#include "streamdialog.h"
#include "virtmic.h"
#include <QProcess>
#include <QWebEnginePage>
class DiscordPage : public QWebEnginePage {
Q_OBJECT
public:
explicit DiscordPage(QWidget *parent = nullptr);
private:
StreamDialog m_streamDialog;
QProcess m_virtmicProcess;
bool acceptNavigationRequest(const QUrl &url,
QWebEnginePage::NavigationType type,
bool isMainFrame) override;
void
javaScriptConsoleMessage(QWebEnginePage::JavaScriptConsoleMessageLevel level,
const QString &message, int lineNumber,
const QString &sourceID) override;
void injectScript(QString source);
void stopVirtmic();
void startVirtmic(QString target);
private Q_SLOTS:
void featurePermissionRequested(const QUrl &securityOrigin,
QWebEnginePage::Feature feature);
void startStream(QString target, uint width, uint height, uint frameRate);
};

View file

@ -1,8 +1,32 @@
#include "mainwindow.h"
#include "virtmic.h"
#include <QApplication>
#include <QCommandLineParser>
int main(int argc, char *argv[]) {
QApplication app(argc, argv);
QApplication::setApplicationName("discord-screenaudio");
QApplication::setApplicationVersion("1.0.0-alpha");
QCommandLineParser parser;
parser.setApplicationDescription(
"Custom Discord client with the ability to stream audio on Linux");
parser.addHelpOption();
parser.addVersionOption();
QCommandLineOption virtmicOption("virtmic", "Start the Virtual Microphone",
"target");
parser.addOption(virtmicOption);
#ifdef DEBUG
parser.addOption(QCommandLineOption(
"remote-debugging-port", "Chromium Remote Debugging Port", "port"));
#endif
parser.process(app);
if (parser.isSet(virtmicOption)) {
Virtmic::start(parser.value(virtmicOption));
}
MainWindow w;
w.show();

View file

@ -1,6 +1,7 @@
#include "mainwindow.h"
#include "virtmic.h"
#include <QApplication>
#include <QColor>
#include <QComboBox>
#include <QFile>
@ -16,96 +17,14 @@
#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);
setupWebView();
resize(1000, 700);
showMaximized();
}
void MainWindow::setupWebView() {
m_webView = new QWebEngineView(this);
m_webView->page()->setBackgroundColor(QColor("#202225"));
// 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);
}
auto page = new DiscordPage;
m_webView->setPage(page);
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;

View file

@ -1,5 +1,7 @@
#pragma once
#include "discordpage.h"
#include <QMainWindow>
#include <QScopedPointer>
#include <QString>
@ -13,15 +15,10 @@ class MainWindow : public QMainWindow {
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);
DiscordPage *m_discordPage;
};

71
src/streamdialog.cpp Normal file
View file

@ -0,0 +1,71 @@
#include "streamdialog.h"
#include "virtmic.h"
#include <QComboBox>
#include <QGridLayout>
#include <QHBoxLayout>
#include <QLabel>
#include <QPushButton>
#include <QSizePolicy>
#include <QVBoxLayout>
StreamDialog::StreamDialog() : QWidget() {
auto layout = new QVBoxLayout;
auto targetLabel = new QLabel;
targetLabel->setText("Which app do you want to stream sound from?");
layout->addWidget(targetLabel);
m_targetComboBox = new QComboBox;
m_targetComboBox->addItem("None");
for (auto target : Virtmic::getTargets()) {
m_targetComboBox->addItem(target);
}
layout->addWidget(m_targetComboBox);
auto qualityLabel = new QLabel;
qualityLabel->setText("Stream Quality");
layout->addWidget(qualityLabel);
auto qualityHBox = new QHBoxLayout;
layout->addLayout(qualityHBox);
m_qualityResolutionComboBox = new QComboBox;
m_qualityResolutionComboBox->addItem("2160p", "3840x2160");
m_qualityResolutionComboBox->addItem("1440p", "2560x1440");
m_qualityResolutionComboBox->addItem("1080p", "1920x1080");
m_qualityResolutionComboBox->addItem("720p", "1280x720");
m_qualityResolutionComboBox->addItem("480p", "854x480");
m_qualityResolutionComboBox->addItem("360p", "640x360");
m_qualityResolutionComboBox->addItem("240p", "426x240");
m_qualityResolutionComboBox->setCurrentText("720p");
qualityHBox->addWidget(m_qualityResolutionComboBox);
m_qualityFPSComboBox = new QComboBox;
m_qualityFPSComboBox->addItem("144 FPS", 144);
m_qualityFPSComboBox->addItem("60 FPS", 60);
m_qualityFPSComboBox->addItem("30 FPS", 30);
m_qualityFPSComboBox->addItem("15 FPS", 15);
m_qualityFPSComboBox->addItem("5 FPS", 5);
m_qualityFPSComboBox->setCurrentText("30 FPS");
qualityHBox->addWidget(m_qualityFPSComboBox);
auto button = new QPushButton;
button->setText("Start Stream");
connect(button, &QPushButton::clicked, this, &StreamDialog::startStream);
layout->addWidget(button, Qt::AlignRight | Qt::AlignBottom);
setLayout(layout);
setWindowTitle("discord-screenaudio Stream Dialog");
setFixedSize(0, 0);
}
void StreamDialog::startStream() {
auto resolution =
m_qualityResolutionComboBox->currentData().toString().split('x');
emit requestedStreamStart(m_targetComboBox->currentText(),
resolution[0].toUInt(), resolution[1].toUInt(),
m_qualityFPSComboBox->currentData().toUInt());
setHidden(true);
}

24
src/streamdialog.h Normal file
View file

@ -0,0 +1,24 @@
#pragma once
#include <QComboBox>
#include <QDialog>
#include <QWidget>
class StreamDialog : public QWidget {
Q_OBJECT
public:
explicit StreamDialog();
private:
QComboBox *m_targetComboBox;
QComboBox *m_qualityResolutionComboBox;
QComboBox *m_qualityFPSComboBox;
Q_SIGNALS:
void requestedStreamStart(QString target, uint width, uint height,
uint frameRate);
private Q_SLOTS:
void startStream();
};