mirror of
https://github.com/moonlight-stream/moonlight-qt.git
synced 2026-06-18 14:40:56 +00:00
Rework session initialization to return to the Qt event loop while connecting
We also display multiple launch warnings at the same time
This commit is contained in:
+47
-19
@@ -48,18 +48,6 @@ Item {
|
|||||||
console.error(text)
|
console.error(text)
|
||||||
}
|
}
|
||||||
|
|
||||||
function displayLaunchWarning(text)
|
|
||||||
{
|
|
||||||
// This toast appears for 3 seconds, just shorter than how long
|
|
||||||
// Session will wait for it to be displayed. This gives it time
|
|
||||||
// to transition to invisible before continuing.
|
|
||||||
var toast = Qt.createQmlObject('import QtQuick.Controls 2.2; ToolTip {}', parent, '')
|
|
||||||
toast.text = text
|
|
||||||
toast.timeout = 3000
|
|
||||||
toast.visible = true
|
|
||||||
console.warn(text)
|
|
||||||
}
|
|
||||||
|
|
||||||
function quitStarting()
|
function quitStarting()
|
||||||
{
|
{
|
||||||
// Avoid the push transition animation
|
// Avoid the push transition animation
|
||||||
@@ -132,7 +120,6 @@ Item {
|
|||||||
session.stageFailed.connect(stageFailed)
|
session.stageFailed.connect(stageFailed)
|
||||||
session.connectionStarted.connect(connectionStarted)
|
session.connectionStarted.connect(connectionStarted)
|
||||||
session.displayLaunchError.connect(displayLaunchError)
|
session.displayLaunchError.connect(displayLaunchError)
|
||||||
session.displayLaunchWarning.connect(displayLaunchWarning)
|
|
||||||
session.quitStarting.connect(quitStarting)
|
session.quitStarting.connect(quitStarting)
|
||||||
session.sessionFinished.connect(sessionFinished)
|
session.sessionFinished.connect(sessionFinished)
|
||||||
session.readyForDeletion.connect(sessionReadyForDeletion)
|
session.readyForDeletion.connect(sessionReadyForDeletion)
|
||||||
@@ -154,6 +141,19 @@ Item {
|
|||||||
onTriggered: stageSpinner.visible = true
|
onTriggered: stageSpinner.visible = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Timer {
|
||||||
|
id: startSessionTimer
|
||||||
|
onTriggered: {
|
||||||
|
// Garbage collect QML stuff before we start streaming,
|
||||||
|
// since we'll probably be streaming for a while and we
|
||||||
|
// won't be able to GC during the stream.
|
||||||
|
gc()
|
||||||
|
|
||||||
|
// Run the streaming session to completion
|
||||||
|
session.start()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Loader {
|
Loader {
|
||||||
id: streamLoader
|
id: streamLoader
|
||||||
active: false
|
active: false
|
||||||
@@ -170,13 +170,41 @@ Item {
|
|||||||
// Stop GUI gamepad usage now
|
// Stop GUI gamepad usage now
|
||||||
SdlGamepadKeyNavigation.disable()
|
SdlGamepadKeyNavigation.disable()
|
||||||
|
|
||||||
// Garbage collect QML stuff before we start streaming,
|
// Initialize the session and probe for host/client capabilities
|
||||||
// since we'll probably be streaming for a while and we
|
if (!session.initialize(window)) {
|
||||||
// won't be able to GC during the stream.
|
sessionFinished(0);
|
||||||
gc()
|
sessionReadyForDeletion();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Run the streaming session to completion
|
// Don't wait unless we have toasts to display
|
||||||
session.exec(window)
|
startSessionTimer.interval = 0
|
||||||
|
|
||||||
|
// Display the toasts together in a vertical centered arrangement
|
||||||
|
var yOffset = 0
|
||||||
|
for (var i = 0; i < session.launchWarnings.length; i++) {
|
||||||
|
var text = session.launchWarnings[i]
|
||||||
|
console.warn(text)
|
||||||
|
|
||||||
|
// Show the tooltip
|
||||||
|
var toast = Qt.createQmlObject('import QtQuick.Controls 2.2; ToolTip {}', parent, '')
|
||||||
|
toast.text = text
|
||||||
|
toast.y += yOffset
|
||||||
|
toast.visible = true
|
||||||
|
|
||||||
|
// Hide the tooltip when the connection starts or fails
|
||||||
|
session.connectionStarted.connect(toast.hide)
|
||||||
|
session.sessionFinished.connect(toast.hide)
|
||||||
|
|
||||||
|
// Offset the next toast below the previous one
|
||||||
|
yOffset = toast.y + toast.padding + toast.height
|
||||||
|
|
||||||
|
// Allow the user 3.5 seconds to read the tooltip(s)
|
||||||
|
startSessionTimer.interval = 3500;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start the timer to wait for toasts (or start the session immediately)
|
||||||
|
startSessionTimer.start()
|
||||||
}
|
}
|
||||||
|
|
||||||
sourceComponent: Item {}
|
sourceComponent: Item {}
|
||||||
|
|||||||
+26
-111
@@ -52,9 +52,12 @@
|
|||||||
#include <QImage>
|
#include <QImage>
|
||||||
#include <QGuiApplication>
|
#include <QGuiApplication>
|
||||||
#include <QCursor>
|
#include <QCursor>
|
||||||
#include <QWindow>
|
|
||||||
#include <QScreen>
|
#include <QScreen>
|
||||||
|
|
||||||
|
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
|
||||||
|
#include <QQuickOpenGLUtils>
|
||||||
|
#endif
|
||||||
|
|
||||||
#define CONN_TEST_SERVER "qt.conntest.moonlight-stream.org"
|
#define CONN_TEST_SERVER "qt.conntest.moonlight-stream.org"
|
||||||
|
|
||||||
CONNECTION_LISTENER_CALLBACKS Session::k_ConnCallbacks = {
|
CONNECTION_LISTENER_CALLBACKS Session::k_ConnCallbacks = {
|
||||||
@@ -595,8 +598,10 @@ Session::~Session()
|
|||||||
SDL_DestroyMutex(m_DecoderLock);
|
SDL_DestroyMutex(m_DecoderLock);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool Session::initialize()
|
bool Session::initialize(QQuickWindow* qtWindow)
|
||||||
{
|
{
|
||||||
|
m_QtWindow = qtWindow;
|
||||||
|
|
||||||
#ifdef Q_OS_DARWIN
|
#ifdef Q_OS_DARWIN
|
||||||
if (qEnvironmentVariableIntValue("I_WANT_BUGGY_FULLSCREEN") == 0) {
|
if (qEnvironmentVariableIntValue("I_WANT_BUGGY_FULLSCREEN") == 0) {
|
||||||
// If we have a notch and the user specified one of the two native display modes
|
// If we have a notch and the user specified one of the two native display modes
|
||||||
@@ -937,37 +942,16 @@ bool Session::initialize()
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (m_Preferences->configurationWarnings) {
|
|
||||||
// Display launch warnings in Qt only after destroying SDL's window.
|
|
||||||
// This avoids conflicts between the windows on display subsystems
|
|
||||||
// such as KMSDRM that only support a single window.
|
|
||||||
for (const auto &text : m_LaunchWarnings) {
|
|
||||||
// Emit the warning to the UI
|
|
||||||
emit displayLaunchWarning(text);
|
|
||||||
|
|
||||||
// Wait a little bit so the user can actually read what we just said.
|
|
||||||
// This wait is a little longer than the actual toast timeout (3 seconds)
|
|
||||||
// to allow it to transition off the screen before continuing.
|
|
||||||
uint32_t start = SDL_GetTicks();
|
|
||||||
while (!SDL_TICKS_PASSED(SDL_GetTicks(), start + 3500)) {
|
|
||||||
SDL_Delay(5);
|
|
||||||
|
|
||||||
if (!m_ThreadedExec) {
|
|
||||||
// Pump the UI loop while we wait if we're on the main thread
|
|
||||||
QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents);
|
|
||||||
QCoreApplication::sendPostedEvents();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
void Session::emitLaunchWarning(QString text)
|
void Session::emitLaunchWarning(QString text)
|
||||||
{
|
{
|
||||||
|
if (m_Preferences->configurationWarnings) {
|
||||||
// Queue this launch warning to be displayed after validation
|
// Queue this launch warning to be displayed after validation
|
||||||
m_LaunchWarnings.append(text);
|
m_LaunchWarnings.append(text);
|
||||||
|
emit launchWarningsChanged();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bool Session::validateLaunch(SDL_Window* testWindow)
|
bool Session::validateLaunch(SDL_Window* testWindow)
|
||||||
@@ -1717,72 +1701,8 @@ void Session::setShouldExitAfterQuit()
|
|||||||
m_ShouldExitAfterQuit = true;
|
m_ShouldExitAfterQuit = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
class ExecThread : public QThread
|
void Session::start()
|
||||||
{
|
{
|
||||||
public:
|
|
||||||
ExecThread(Session* session) :
|
|
||||||
QThread(nullptr),
|
|
||||||
m_Session(session)
|
|
||||||
{
|
|
||||||
setObjectName("Session Exec");
|
|
||||||
}
|
|
||||||
|
|
||||||
void run() override
|
|
||||||
{
|
|
||||||
m_Session->execInternal();
|
|
||||||
}
|
|
||||||
|
|
||||||
Session* m_Session;
|
|
||||||
};
|
|
||||||
|
|
||||||
void Session::exec(QWindow* qtWindow)
|
|
||||||
{
|
|
||||||
m_QtWindow = qtWindow;
|
|
||||||
|
|
||||||
// Use a separate thread for the streaming session on X11 or Wayland
|
|
||||||
// to ensure we don't stomp on Qt's GL context. This breaks when using
|
|
||||||
// the Qt EGLFS backend, so we will restrict this to X11
|
|
||||||
m_ThreadedExec = WMUtils::isRunningX11() || WMUtils::isRunningWayland();
|
|
||||||
|
|
||||||
if (m_ThreadedExec) {
|
|
||||||
// Run the streaming session on a separate thread for Linux/BSD
|
|
||||||
ExecThread execThread(this);
|
|
||||||
execThread.start();
|
|
||||||
|
|
||||||
// Until the SDL streaming window is created, we should continue
|
|
||||||
// to update the Qt UI to allow warning messages to display and
|
|
||||||
// make sure that the Qt window can hide itself.
|
|
||||||
while (!execThread.wait(10) && m_Window == nullptr) {
|
|
||||||
QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents);
|
|
||||||
QCoreApplication::sendPostedEvents();
|
|
||||||
}
|
|
||||||
QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents);
|
|
||||||
QCoreApplication::sendPostedEvents();
|
|
||||||
|
|
||||||
// SDL is in charge now. Wait until the streaming thread exits
|
|
||||||
// to further update the Qt window.
|
|
||||||
execThread.wait();
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
// Run the streaming session on the main thread for Windows and macOS
|
|
||||||
execInternal();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void Session::execInternal()
|
|
||||||
{
|
|
||||||
// Complete initialization in this deferred context to avoid
|
|
||||||
// calling expensive functions in the constructor (during the
|
|
||||||
// process of loading the StreamSegue).
|
|
||||||
//
|
|
||||||
// NB: This initializes the SDL video subsystem, so it must be
|
|
||||||
// called on the main thread.
|
|
||||||
if (!initialize()) {
|
|
||||||
emit sessionFinished(0);
|
|
||||||
emit readyForDeletion();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wait for any old session to finish cleanup
|
// Wait for any old session to finish cleanup
|
||||||
s_ActiveSessionSemaphore.acquire();
|
s_ActiveSessionSemaphore.acquire();
|
||||||
|
|
||||||
@@ -1793,27 +1713,15 @@ void Session::execInternal()
|
|||||||
// NB: m_InputHandler must be initialize before starting the connection.
|
// NB: m_InputHandler must be initialize before starting the connection.
|
||||||
m_InputHandler = new SdlInputHandler(*m_Preferences, m_StreamConfig.width, m_StreamConfig.height);
|
m_InputHandler = new SdlInputHandler(*m_Preferences, m_StreamConfig.width, m_StreamConfig.height);
|
||||||
|
|
||||||
AsyncConnectionStartThread asyncConnThread(this);
|
// Kick off the async connection thread then return to the caller to pump the event loop
|
||||||
if (!m_ThreadedExec) {
|
auto thread = new AsyncConnectionStartThread(this);
|
||||||
// Kick off the async connection thread while we sit here and pump the event loop
|
QObject::connect(thread, &QThread::finished, this, &Session::exec);
|
||||||
asyncConnThread.start();
|
QObject::connect(thread, &QThread::finished, thread, &QThread::deleteLater);
|
||||||
while (!asyncConnThread.wait(10)) {
|
thread->start();
|
||||||
QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents);
|
}
|
||||||
QCoreApplication::sendPostedEvents();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Pump the event loop one last time to ensure we pick up any events from
|
|
||||||
// the thread that happened while it was in the final successful QThread::wait().
|
|
||||||
QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents);
|
|
||||||
QCoreApplication::sendPostedEvents();
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
// We're already in a separate thread so run the connection operations
|
|
||||||
// synchronously and don't pump the event loop. The main thread is already
|
|
||||||
// pumping the event loop for us.
|
|
||||||
asyncConnThread.run();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
void Session::exec()
|
||||||
|
{
|
||||||
// If the connection failed, clean up and abort the connection.
|
// If the connection failed, clean up and abort the connection.
|
||||||
if (!m_AsyncConnectionSuccess) {
|
if (!m_AsyncConnectionSuccess) {
|
||||||
delete m_InputHandler;
|
delete m_InputHandler;
|
||||||
@@ -2434,6 +2342,13 @@ DispatchDeferredCleanup:
|
|||||||
|
|
||||||
SDL_QuitSubSystem(SDL_INIT_VIDEO);
|
SDL_QuitSubSystem(SDL_INIT_VIDEO);
|
||||||
|
|
||||||
|
// Reset this thread's OpenGL state back to what Qt expects
|
||||||
|
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
|
||||||
|
QQuickOpenGLUtils::resetOpenGLState();
|
||||||
|
#else
|
||||||
|
m_QtWindow->resetOpenGLState();
|
||||||
|
#endif
|
||||||
|
|
||||||
// Cleanup can take a while, so dispatch it to a worker thread.
|
// Cleanup can take a while, so dispatch it to a worker thread.
|
||||||
// When it is complete, it will release our s_ActiveSessionSemaphore
|
// When it is complete, it will release our s_ActiveSessionSemaphore
|
||||||
// reference.
|
// reference.
|
||||||
|
|||||||
+9
-11
@@ -1,7 +1,7 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <QSemaphore>
|
#include <QSemaphore>
|
||||||
#include <QWindow>
|
#include <QQuickWindow>
|
||||||
|
|
||||||
#include <Limelight.h>
|
#include <Limelight.h>
|
||||||
#include <opus_multistream.h>
|
#include <opus_multistream.h>
|
||||||
@@ -96,13 +96,14 @@ class Session : public QObject
|
|||||||
friend class SdlInputHandler;
|
friend class SdlInputHandler;
|
||||||
friend class DeferredSessionCleanupTask;
|
friend class DeferredSessionCleanupTask;
|
||||||
friend class AsyncConnectionStartThread;
|
friend class AsyncConnectionStartThread;
|
||||||
friend class ExecThread;
|
|
||||||
|
|
||||||
public:
|
public:
|
||||||
explicit Session(NvComputer* computer, NvApp& app, StreamingPreferences *preferences = nullptr);
|
explicit Session(NvComputer* computer, NvApp& app, StreamingPreferences *preferences = nullptr);
|
||||||
virtual ~Session();
|
virtual ~Session();
|
||||||
|
|
||||||
Q_INVOKABLE void exec(QWindow* qtWindow);
|
Q_INVOKABLE bool initialize(QQuickWindow* qtWindow);
|
||||||
|
Q_INVOKABLE void start();
|
||||||
|
Q_PROPERTY(QStringList launchWarnings MEMBER m_LaunchWarnings NOTIFY launchWarningsChanged);
|
||||||
|
|
||||||
static
|
static
|
||||||
void getDecoderInfo(SDL_Window* window,
|
void getDecoderInfo(SDL_Window* window,
|
||||||
@@ -132,8 +133,6 @@ signals:
|
|||||||
|
|
||||||
void displayLaunchError(QString text);
|
void displayLaunchError(QString text);
|
||||||
|
|
||||||
void displayLaunchWarning(QString text);
|
|
||||||
|
|
||||||
void quitStarting();
|
void quitStarting();
|
||||||
|
|
||||||
void sessionFinished(int portTestResult);
|
void sessionFinished(int portTestResult);
|
||||||
@@ -141,10 +140,10 @@ signals:
|
|||||||
// Emitted after sessionFinished() when the session is ready to be destroyed
|
// Emitted after sessionFinished() when the session is ready to be destroyed
|
||||||
void readyForDeletion();
|
void readyForDeletion();
|
||||||
|
|
||||||
private:
|
void launchWarningsChanged();
|
||||||
void execInternal();
|
|
||||||
|
|
||||||
bool initialize();
|
private:
|
||||||
|
void exec();
|
||||||
|
|
||||||
bool startConnectionAsync();
|
bool startConnectionAsync();
|
||||||
|
|
||||||
@@ -256,13 +255,12 @@ private:
|
|||||||
bool m_AudioDisabled;
|
bool m_AudioDisabled;
|
||||||
bool m_AudioMuted;
|
bool m_AudioMuted;
|
||||||
Uint32 m_FullScreenFlag;
|
Uint32 m_FullScreenFlag;
|
||||||
QWindow* m_QtWindow;
|
QQuickWindow* m_QtWindow;
|
||||||
bool m_ThreadedExec;
|
|
||||||
bool m_UnexpectedTermination;
|
bool m_UnexpectedTermination;
|
||||||
SdlInputHandler* m_InputHandler;
|
SdlInputHandler* m_InputHandler;
|
||||||
int m_MouseEmulationRefCount;
|
int m_MouseEmulationRefCount;
|
||||||
int m_FlushingWindowEventsRef;
|
int m_FlushingWindowEventsRef;
|
||||||
QList<QString> m_LaunchWarnings;
|
QStringList m_LaunchWarnings;
|
||||||
bool m_ShouldExitAfterQuit;
|
bool m_ShouldExitAfterQuit;
|
||||||
|
|
||||||
bool m_AsyncConnectionSuccess;
|
bool m_AsyncConnectionSuccess;
|
||||||
|
|||||||
Reference in New Issue
Block a user