Implement Stream Dialog
This commit is contained in:
parent
81317fd43d
commit
67087e8251
10 changed files with 394 additions and 140 deletions
|
@ -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
|
||||
)
|
||||
|
||||
|
|
|
@ -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,6 +12,7 @@ const getAudioDevice = async (nameOfAudioDevice) => {
|
|||
return audioDevice;
|
||||
};
|
||||
|
||||
function setGetDisplayMedia(overrideArgs = undefined) {
|
||||
const getDisplayMedia = async (...args) => {
|
||||
var id;
|
||||
try {
|
||||
|
@ -55,8 +44,83 @@ const getDisplayMedia = async (...args) => {
|
|||
},
|
||||
});
|
||||
let [track] = captureSystemAudioStream.getAudioTracks();
|
||||
const gdm = await navigator.mediaDevices.chromiumGetDisplayMedia(...args);
|
||||
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
119
src/discordpage.cpp
Normal 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
33
src/discordpage.h
Normal 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);
|
||||
};
|
24
src/main.cpp
24
src/main.cpp
|
@ -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();
|
||||
|
||||
|
|
|
@ -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);
|
||||
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;
|
||||
|
|
|
@ -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
71
src/streamdialog.cpp
Normal 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
24
src/streamdialog.h
Normal 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();
|
||||
};
|
Loading…
Reference in a new issue