From 67087e825178b116f8d2ad1edad9e82f5dddee7f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Malte=20J=C3=BCrgens?= Date: Sat, 9 Jul 2022 22:07:14 +0200 Subject: [PATCH] Implement Stream Dialog --- CMakeLists.txt | 3 + assets/userscript.js | 160 ++++++++++++++++++++++++++++++------------- src/discordpage.cpp | 119 ++++++++++++++++++++++++++++++++ src/discordpage.h | 33 +++++++++ src/main.cpp | 24 +++++++ src/mainwindow.cpp | 89 ++---------------------- src/mainwindow.h | 9 +-- src/streamdialog.cpp | 71 +++++++++++++++++++ src/streamdialog.h | 24 +++++++ src/virtmic.h | 2 +- 10 files changed, 394 insertions(+), 140 deletions(-) create mode 100644 src/discordpage.cpp create mode 100644 src/discordpage.h create mode 100644 src/streamdialog.cpp create mode 100644 src/streamdialog.h diff --git a/CMakeLists.txt b/CMakeLists.txt index c367829..3961517 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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 ) diff --git a/assets/userscript.js b/assets/userscript.js index 3bec773..671f18c 100644 --- a/assets/userscript.js +++ b/assets/userscript.js @@ -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); diff --git a/src/discordpage.cpp b/src/discordpage.cpp new file mode 100644 index 0000000..1946de3 --- /dev/null +++ b/src/discordpage.cpp @@ -0,0 +1,119 @@ +#include "discordpage.h" +#include "virtmic.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +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)); + }); +} diff --git a/src/discordpage.h b/src/discordpage.h new file mode 100644 index 0000000..82b95f3 --- /dev/null +++ b/src/discordpage.h @@ -0,0 +1,33 @@ +#pragma once + +#include "streamdialog.h" +#include "virtmic.h" + +#include +#include + +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); +}; diff --git a/src/main.cpp b/src/main.cpp index ededf0d..9f1e217 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,8 +1,32 @@ #include "mainwindow.h" +#include "virtmic.h" + #include +#include 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(); diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index 24b983e..1a263ed 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -1,6 +1,7 @@ #include "mainwindow.h" #include "virtmic.h" +#include #include #include #include @@ -16,96 +17,14 @@ #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); + 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; diff --git a/src/mainwindow.h b/src/mainwindow.h index e8b8520..d8ece10 100644 --- a/src/mainwindow.h +++ b/src/mainwindow.h @@ -1,5 +1,7 @@ #pragma once +#include "discordpage.h" + #include #include #include @@ -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; }; diff --git a/src/streamdialog.cpp b/src/streamdialog.cpp new file mode 100644 index 0000000..731f0f7 --- /dev/null +++ b/src/streamdialog.cpp @@ -0,0 +1,71 @@ +#include "streamdialog.h" +#include "virtmic.h" + +#include +#include +#include +#include +#include +#include +#include + +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); +} diff --git a/src/streamdialog.h b/src/streamdialog.h new file mode 100644 index 0000000..6818d8f --- /dev/null +++ b/src/streamdialog.h @@ -0,0 +1,24 @@ +#pragma once + +#include +#include +#include + +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(); +}; diff --git a/src/virtmic.h b/src/virtmic.h index 0aeb9d2..7543942 100644 --- a/src/virtmic.h +++ b/src/virtmic.h @@ -10,4 +10,4 @@ namespace Virtmic { QVector getTargets(); void start(QString _target); -} // namespace Virtmic \ No newline at end of file +} // namespace Virtmic