Move connection establishment to a separate thread to keep the UI responsive

This commit is contained in:
Cameron Gutman 2020-08-10 22:21:54 -07:00
parent 28ff15c99e
commit d3949806f5
3 changed files with 81 additions and 79 deletions

View File

@ -64,7 +64,7 @@ Item {
function sessionFinished(portTestResult) function sessionFinished(portTestResult)
{ {
if (portTestResult !== 0 && streamSegueErrorDialog.text) { if (portTestResult !== 0 && portTestResult !== -1 && streamSegueErrorDialog.text) {
streamSegueErrorDialog.text += "\n\nThis PC's Internet connection is blocking Moonlight. Streaming over the Internet may not work while connected to this network." streamSegueErrorDialog.text += "\n\nThis PC's Internet connection is blocking Moonlight. Streaming over the Internet may not work while connected to this network."
} }

View File

@ -33,6 +33,8 @@
#include <QGuiApplication> #include <QGuiApplication>
#include <QCursor> #include <QCursor>
#define CONN_TEST_SERVER "qt.conntest.moonlight-stream.org"
CONNECTION_LISTENER_CALLBACKS Session::k_ConnCallbacks = { CONNECTION_LISTENER_CALLBACKS Session::k_ConnCallbacks = {
Session::clStageStarting, Session::clStageStarting,
nullptr, nullptr,
@ -53,22 +55,19 @@ void Session::clStageStarting(int stage)
// which happens to be the main thread, so it's cool to interact // which happens to be the main thread, so it's cool to interact
// with the GUI in these callbacks. // with the GUI in these callbacks.
emit s_ActiveSession->stageStarting(QString::fromLocal8Bit(LiGetStageName(stage))); emit s_ActiveSession->stageStarting(QString::fromLocal8Bit(LiGetStageName(stage)));
QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents);
} }
void Session::clStageFailed(int stage, int errorCode) void Session::clStageFailed(int stage, int errorCode)
{ {
// We know this is called on the same thread as LiStartConnection() // Perform the port test now, while we're on the async connection thread and not blocking the UI.
// which happens to be the main thread, so it's cool to interact s_ActiveSession->m_PortTestResults = LiTestClientConnectivity(CONN_TEST_SERVER, 443, LiGetPortFlagsFromStage(stage));
// with the GUI in these callbacks.
s_ActiveSession->m_FailedStageId = stage;
emit s_ActiveSession->stageFailed(QString::fromLocal8Bit(LiGetStageName(stage)), errorCode); emit s_ActiveSession->stageFailed(QString::fromLocal8Bit(LiGetStageName(stage)), errorCode);
QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents);
} }
void Session::clConnectionTerminated(int errorCode) void Session::clConnectionTerminated(int errorCode)
{ {
s_ActiveSession->m_TerminationErrorCode = errorCode; s_ActiveSession->m_PortTestResults = LiTestClientConnectivity(CONN_TEST_SERVER, 443, LiGetPortFlagsFromTerminationErrorCode(errorCode));
// Display the termination dialog if this was not intended // Display the termination dialog if this was not intended
switch (errorCode) { switch (errorCode) {
@ -339,8 +338,8 @@ Session::Session(NvComputer* computer, NvApp& app, StreamingPreferences *prefere
m_InputHandler(nullptr), m_InputHandler(nullptr),
m_InputHandlerLock(0), m_InputHandlerLock(0),
m_MouseEmulationRefCount(0), m_MouseEmulationRefCount(0),
m_TerminationErrorCode(ML_ERROR_GRACEFUL_TERMINATION), m_AsyncConnectionSuccess(false),
m_FailedStageId(STAGE_NONE), m_PortTestResults(0),
m_OpusDecoder(nullptr), m_OpusDecoder(nullptr),
m_AudioRenderer(nullptr), m_AudioRenderer(nullptr),
m_AudioSampleCount(0), m_AudioSampleCount(0),
@ -706,36 +705,12 @@ private:
!m_Session->m_UnexpectedTermination && !m_Session->m_UnexpectedTermination &&
m_Session->m_Preferences->quitAppAfter; m_Session->m_Preferences->quitAppAfter;
// If the connection terminated due to an error, we may want
// to perform a connection test to ensure our traffic is not
// being blocked.
int portFlags;
unsigned int portTestResult = 0;
if (m_Session->m_FailedStageId != STAGE_NONE) {
portFlags = LiGetPortFlagsFromStage(m_Session->m_FailedStageId);
}
else if (m_Session->m_TerminationErrorCode != ML_ERROR_GRACEFUL_TERMINATION) {
portFlags = LiGetPortFlagsFromTerminationErrorCode(m_Session->m_TerminationErrorCode);
}
else {
portFlags = 0;
}
if (portFlags != 0 && m_Session->m_Preferences->detectNetworkBlocking) {
portTestResult = LiTestClientConnectivity("qt.conntest.moonlight-stream.org", 443, portFlags);
// Ignore an inconclusive result
if (portTestResult == ML_TEST_RESULT_INCONCLUSIVE) {
portTestResult = 0;
}
}
// Notify the UI // Notify the UI
if (shouldQuit) { if (shouldQuit) {
emit m_Session->quitStarting(); emit m_Session->quitStarting();
} }
else { else {
emit m_Session->sessionFinished(portTestResult); emit m_Session->sessionFinished(m_Session->m_PortTestResults);
} }
// Finish cleanup of the connection state // Finish cleanup of the connection state
@ -753,7 +728,7 @@ private:
} }
// Session is finished now // Session is finished now
emit m_Session->sessionFinished(portTestResult); emit m_Session->sessionFinished(m_Session->m_PortTestResults);
} }
} }
@ -942,39 +917,30 @@ void Session::notifyMouseEmulationMode(bool enabled)
} }
} }
void Session::exec(int displayOriginX, int displayOriginY) class AsyncConnectionStartThread : public QThread
{ {
m_DisplayOriginX = displayOriginX; public:
m_DisplayOriginY = displayOriginY; AsyncConnectionStartThread(Session* session) :
QThread(nullptr),
m_Session(session) {}
// Complete initialization in this deferred context to avoid private:
// calling expensive functions in the constructor (during the void run() override
// process of loading the StreamSegue). {
if (!initialize()) { m_Session->m_AsyncConnectionSuccess = m_Session->startConnectionAsync();
emit sessionFinished(0);
return;
} }
Session* m_Session;
};
// Called in a non-main thread
bool Session::startConnectionAsync()
{
StreamingPreferences prefs;
// Wait 1.5 seconds before connecting to let the user // Wait 1.5 seconds before connecting to let the user
// have time to read any messages present on the segue // have time to read any messages present on the segue
uint32_t start = SDL_GetTicks(); SDL_Delay(1500);
while (!SDL_TICKS_PASSED(SDL_GetTicks(), start + 1500)) {
// Pump the UI loop while we wait
SDL_Delay(5);
QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents);
}
// Wait for any old session to finish cleanup
s_ActiveSessionSemaphore.acquire();
// We're now active
s_ActiveSession = this;
// Initialize the gamepad code with our preferences
StreamingPreferences prefs;
m_InputHandler = new SdlInputHandler(prefs, m_Computer,
m_StreamConfig.width,
m_StreamConfig.height);
// The UI should have ensured the old game was already quit // The UI should have ensured the old game was already quit
// if we decide to stream a different game. // if we decide to stream a different game.
@ -1008,19 +974,11 @@ void Session::exec(int displayOriginX, int displayOriginY)
m_InputHandler->getAttachedGamepadMask()); m_InputHandler->getAttachedGamepadMask());
} }
} catch (const GfeHttpResponseException& e) { } catch (const GfeHttpResponseException& e) {
delete m_InputHandler;
m_InputHandler = nullptr;
SDL_QuitSubSystem(SDL_INIT_VIDEO);
emit displayLaunchError("GeForce Experience returned error: " + e.toQString()); emit displayLaunchError("GeForce Experience returned error: " + e.toQString());
QThreadPool::globalInstance()->start(new DeferredSessionCleanupTask(this)); return false;
return;
} catch (const QtNetworkReplyException& e) { } catch (const QtNetworkReplyException& e) {
delete m_InputHandler;
m_InputHandler = nullptr;
SDL_QuitSubSystem(SDL_INIT_VIDEO);
emit displayLaunchError(e.toQString()); emit displayLaunchError(e.toQString());
QThreadPool::globalInstance()->start(new DeferredSessionCleanupTask(this)); return false;
return;
} }
QByteArray hostnameStr = m_Computer->activeAddress.toLatin1(); QByteArray hostnameStr = m_Computer->activeAddress.toLatin1();
@ -1070,6 +1028,51 @@ void Session::exec(int displayOriginX, int displayOriginY)
if (err != 0) { if (err != 0) {
// We already displayed an error dialog in the stage failure // We already displayed an error dialog in the stage failure
// listener. // listener.
return false;
}
emit connectionStarted();
return true;
}
void Session::exec(int displayOriginX, int displayOriginY)
{
m_DisplayOriginX = displayOriginX;
m_DisplayOriginY = displayOriginY;
// 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);
return;
}
// Wait for any old session to finish cleanup
s_ActiveSessionSemaphore.acquire();
// We're now active
s_ActiveSession = this;
// Initialize the gamepad code with our preferences
// NB: m_InputHandler must be initialize before starting the connection.
StreamingPreferences prefs;
m_InputHandler = new SdlInputHandler(prefs, m_Computer,
m_StreamConfig.width,
m_StreamConfig.height);
// Kick off the async connection thread while we sit here and pump the event loop
AsyncConnectionStartThread asyncConnThread(this);
asyncConnThread.start();
while (!asyncConnThread.wait(10)) {
QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents);
}
// If the connection failed, clean up and abort the connection.
if (!m_AsyncConnectionSuccess) {
delete m_InputHandler; delete m_InputHandler;
m_InputHandler = nullptr; m_InputHandler = nullptr;
SDL_QuitSubSystem(SDL_INIT_VIDEO); SDL_QuitSubSystem(SDL_INIT_VIDEO);
@ -1077,10 +1080,6 @@ void Session::exec(int displayOriginX, int displayOriginY)
return; return;
} }
// Pump the message loop to update the UI
emit connectionStarted();
QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents);
int x, y, width, height; int x, y, width, height;
getWindowDimensions(x, y, width, height); getWindowDimensions(x, y, width, height);

View File

@ -16,6 +16,7 @@ class Session : public QObject
friend class SdlInputHandler; friend class SdlInputHandler;
friend class DeferredSessionCleanupTask; friend class DeferredSessionCleanupTask;
friend class AsyncConnectionStartThread;
public: public:
explicit Session(NvComputer* computer, NvApp& app, StreamingPreferences *preferences = nullptr); explicit Session(NvComputer* computer, NvApp& app, StreamingPreferences *preferences = nullptr);
@ -56,6 +57,8 @@ signals:
private: private:
bool initialize(); bool initialize();
bool startConnectionAsync();
bool validateLaunch(SDL_Window* testWindow); bool validateLaunch(SDL_Window* testWindow);
void emitLaunchWarning(QString text); void emitLaunchWarning(QString text);
@ -147,8 +150,8 @@ private:
SDL_SpinLock m_InputHandlerLock; SDL_SpinLock m_InputHandlerLock;
int m_MouseEmulationRefCount; int m_MouseEmulationRefCount;
int m_TerminationErrorCode; bool m_AsyncConnectionSuccess;
int m_FailedStageId; int m_PortTestResults;
int m_ActiveVideoFormat; int m_ActiveVideoFormat;
int m_ActiveVideoWidth; int m_ActiveVideoWidth;