diff --git a/app/gui/SettingsView.qml b/app/gui/SettingsView.qml index 5305e6ed1..d8007e4d5 100644 --- a/app/gui/SettingsView.qml +++ b/app/gui/SettingsView.qml @@ -1279,6 +1279,58 @@ Flickable { ToolTip.visible: hovered ToolTip.text: qsTr("Prevents the screensaver from starting or the display from going to sleep while streaming.") } + + CheckBox { + id: inactivityTimeoutCheck + width: parent.width + text: qsTr("Disconnect after inactivity") + font.pointSize: 12 + checked: StreamingPreferences.inactivityTimeoutEnabled + onCheckedChanged: { + StreamingPreferences.inactivityTimeoutEnabled = checked + } + + ToolTip.delay: 1000 + ToolTip.timeout: 5000 + ToolTip.visible: hovered + ToolTip.text: qsTr("Automatically disconnects if no input is received for the selected duration.") + } + + Column { + width: parent.width + spacing: 4 + opacity: inactivityTimeoutCheck.checked ? 1.0 : 0.6 + + Label { + width: parent.width + text: qsTr("Inactivity timeout duration") + font.pointSize: 11 + wrapMode: Text.Wrap + } + + Row { + width: parent.width + spacing: 8 + + SpinBox { + id: inactivityTimeoutMinutes + enabled: inactivityTimeoutCheck.checked + from: 5 + to: 240 + stepSize: 5 + value: StreamingPreferences.inactivityTimeoutMinutes + onValueModified: { + StreamingPreferences.inactivityTimeoutMinutes = value + } + } + + Label { + text: qsTr("minutes") + font.pointSize: 11 + verticalAlignment: Text.AlignVCenter + } + } + } } } } diff --git a/app/settings/streamingpreferences.cpp b/app/settings/streamingpreferences.cpp index 40c3e782c..da3f2387e 100644 --- a/app/settings/streamingpreferences.cpp +++ b/app/settings/streamingpreferences.cpp @@ -50,6 +50,8 @@ #define SER_SWAPFACEBUTTONS "swapfacebuttons" #define SER_CAPTURESYSKEYS "capturesyskeys" #define SER_KEEPAWAKE "keepawake" +#define SER_INACTIVITY_TIMEOUT_ENABLED "inactivityTimeoutEnabled" +#define SER_INACTIVITY_TIMEOUT_MINUTES "inactivityTimeoutMinutes" #define SER_LANGUAGE "language" #define CURRENT_DEFAULT_VER 2 @@ -150,6 +152,8 @@ void StreamingPreferences::reload() reverseScrollDirection = settings.value(SER_REVERSESCROLL, false).toBool(); swapFaceButtons = settings.value(SER_SWAPFACEBUTTONS, false).toBool(); keepAwake = settings.value(SER_KEEPAWAKE, true).toBool(); + inactivityTimeoutEnabled = settings.value(SER_INACTIVITY_TIMEOUT_ENABLED, false).toBool(); + inactivityTimeoutMinutes = settings.value(SER_INACTIVITY_TIMEOUT_MINUTES, 30).toInt(); enableHdr = settings.value(SER_HDR, false).toBool(); captureSysKeysMode = static_cast(settings.value(SER_CAPTURESYSKEYS, static_cast(CaptureSysKeysMode::CSK_OFF)).toInt()); @@ -358,6 +362,8 @@ void StreamingPreferences::save() settings.setValue(SER_SWAPFACEBUTTONS, swapFaceButtons); settings.setValue(SER_CAPTURESYSKEYS, captureSysKeysMode); settings.setValue(SER_KEEPAWAKE, keepAwake); + settings.setValue(SER_INACTIVITY_TIMEOUT_ENABLED, inactivityTimeoutEnabled); + settings.setValue(SER_INACTIVITY_TIMEOUT_MINUTES, inactivityTimeoutMinutes); } int StreamingPreferences::getDefaultBitrate(int width, int height, int fps, bool yuv444) diff --git a/app/settings/streamingpreferences.h b/app/settings/streamingpreferences.h index 2d2d39b64..447675ce8 100644 --- a/app/settings/streamingpreferences.h +++ b/app/settings/streamingpreferences.h @@ -143,6 +143,8 @@ class StreamingPreferences : public QObject Q_PROPERTY(bool reverseScrollDirection MEMBER reverseScrollDirection NOTIFY reverseScrollDirectionChanged) Q_PROPERTY(bool swapFaceButtons MEMBER swapFaceButtons NOTIFY swapFaceButtonsChanged) Q_PROPERTY(bool keepAwake MEMBER keepAwake NOTIFY keepAwakeChanged) + Q_PROPERTY(bool inactivityTimeoutEnabled MEMBER inactivityTimeoutEnabled NOTIFY inactivityTimeoutEnabledChanged) + Q_PROPERTY(int inactivityTimeoutMinutes MEMBER inactivityTimeoutMinutes NOTIFY inactivityTimeoutMinutesChanged) Q_PROPERTY(CaptureSysKeysMode captureSysKeysMode MEMBER captureSysKeysMode NOTIFY captureSysKeysModeChanged) Q_PROPERTY(Language language MEMBER language NOTIFY languageChanged); @@ -176,6 +178,8 @@ class StreamingPreferences : public QObject bool reverseScrollDirection; bool swapFaceButtons; bool keepAwake; + bool inactivityTimeoutEnabled; + int inactivityTimeoutMinutes; int packetSize; AudioConfig audioConfig; VideoCodecConfig videoCodecConfig; @@ -223,6 +227,8 @@ class StreamingPreferences : public QObject void swapFaceButtonsChanged(); void captureSysKeysModeChanged(); void keepAwakeChanged(); + void inactivityTimeoutEnabledChanged(); + void inactivityTimeoutMinutesChanged(); void languageChanged(); private: diff --git a/app/streaming/session.cpp b/app/streaming/session.cpp index a9dde1746..5811fb292 100644 --- a/app/streaming/session.cpp +++ b/app/streaming/session.cpp @@ -580,7 +580,11 @@ Session::Session(NvComputer* computer, NvApp& app, StreamingPreferences *prefere m_OpusDecoder(nullptr), m_AudioRenderer(nullptr), m_AudioSampleCount(0), - m_DropAudioEndTime(0) + m_DropAudioEndTime(0), + m_LastInputTime(0), + m_InactivityTimeoutMs((m_Preferences->inactivityTimeoutMinutes > 0 ? + m_Preferences->inactivityTimeoutMinutes : 30) * 60 * 1000), + m_InactivityTimeoutEnabled(m_Preferences->inactivityTimeoutEnabled) { } @@ -592,6 +596,31 @@ Session::~Session() SDL_DestroyMutex(m_DecoderLock); } +void Session::resetInactivityTimer() +{ + if (m_InactivityTimeoutEnabled) { + m_LastInputTime = SDL_GetTicks(); + } +} + +void Session::checkInactivityTimeout() +{ + if (m_InactivityTimeoutEnabled) { + Uint32 currentTime = SDL_GetTicks(); + if (currentTime - m_LastInputTime > m_InactivityTimeoutMs) { + SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, + "Disconnecting due to %d minutes of inactivity", + m_InactivityTimeoutMs / 60000); + + // Push a quit event to trigger cleanup + SDL_Event quitEvent; + quitEvent.type = SDL_QUIT; + quitEvent.quit.timestamp = SDL_GetTicks(); + SDL_PushEvent(&quitEvent); + } + } +} + bool Session::initialize(QQuickWindow* qtWindow) { m_QtWindow = qtWindow; @@ -1998,6 +2027,11 @@ void Session::exec() // Switch to async logging mode when we enter the SDL loop StreamUtils::enterAsyncLoggingMode(); + // Initialize inactivity timer (if enabled) + if (m_InactivityTimeoutEnabled) { + resetInactivityTimer(); + } + // Hijack this thread to be the SDL main thread. We have to do this // because we want to suspend all Qt processing until the stream is over. SDL_Event event; @@ -2014,6 +2048,9 @@ void Session::exec() // and other problems. if (!SDL_WaitEventTimeout(&event, 1000)) { presence.runCallbacks(); + if (m_InactivityTimeoutEnabled) { + checkInactivityTimeout(); + } continue; } #else @@ -2030,6 +2067,9 @@ void Session::exec() SDL_Delay(10); #endif presence.runCallbacks(); + if (m_InactivityTimeoutEnabled) { + checkInactivityTimeout(); + } continue; } #endif @@ -2287,25 +2327,43 @@ void Session::exec() case SDL_KEYUP: case SDL_KEYDOWN: presence.runCallbacks(); + if (m_InactivityTimeoutEnabled) { + resetInactivityTimer(); + } m_InputHandler->handleKeyEvent(&event.key); break; case SDL_MOUSEBUTTONDOWN: case SDL_MOUSEBUTTONUP: presence.runCallbacks(); + if (m_InactivityTimeoutEnabled) { + resetInactivityTimer(); + } m_InputHandler->handleMouseButtonEvent(&event.button); break; case SDL_MOUSEMOTION: + if (m_InactivityTimeoutEnabled) { + resetInactivityTimer(); + } m_InputHandler->handleMouseMotionEvent(&event.motion); break; case SDL_MOUSEWHEEL: + if (m_InactivityTimeoutEnabled) { + resetInactivityTimer(); + } m_InputHandler->handleMouseWheelEvent(&event.wheel); break; case SDL_CONTROLLERAXISMOTION: + if (m_InactivityTimeoutEnabled) { + resetInactivityTimer(); + } m_InputHandler->handleControllerAxisEvent(&event.caxis); break; case SDL_CONTROLLERBUTTONDOWN: case SDL_CONTROLLERBUTTONUP: presence.runCallbacks(); + if (m_InactivityTimeoutEnabled) { + resetInactivityTimer(); + } m_InputHandler->handleControllerButtonEvent(&event.cbutton); break; #if SDL_VERSION_ATLEAST(2, 0, 14) @@ -2315,6 +2373,9 @@ void Session::exec() case SDL_CONTROLLERTOUCHPADDOWN: case SDL_CONTROLLERTOUCHPADUP: case SDL_CONTROLLERTOUCHPADMOTION: + if (m_InactivityTimeoutEnabled) { + resetInactivityTimer(); + } m_InputHandler->handleControllerTouchpadEvent(&event.ctouchpad); break; #endif @@ -2333,6 +2394,9 @@ void Session::exec() case SDL_FINGERDOWN: case SDL_FINGERMOTION: case SDL_FINGERUP: + if (m_InactivityTimeoutEnabled) { + resetInactivityTimer(); + } m_InputHandler->handleTouchFingerEvent(&event.tfinger); break; case SDL_DISPLAYEVENT: diff --git a/app/streaming/session.h b/app/streaming/session.h index b4bba36d1..5ee2cb9e1 100644 --- a/app/streaming/session.h +++ b/app/streaming/session.h @@ -281,7 +281,15 @@ class Session : public QObject Overlay::OverlayManager m_OverlayManager; + // Inactivity timeout tracking + Uint32 m_LastInputTime; + Uint32 m_InactivityTimeoutMs; + bool m_InactivityTimeoutEnabled; + static CONNECTION_LISTENER_CALLBACKS k_ConnCallbacks; static Session* s_ActiveSession; static QSemaphore s_ActiveSessionSemaphore; + + void checkInactivityTimeout(); + void resetInactivityTimer(); };