diff --git a/app/app.pro b/app/app.pro index 4306ac92..c9e16093 100644 --- a/app/app.pro +++ b/app/app.pro @@ -52,18 +52,18 @@ win32 { SOURCES += \ main.cpp \ - streaming/audio.c \ - streaming/input.c \ gui/mainwindow.cpp \ gui/popupmanager.cpp \ backend/identitymanager.cpp \ backend/nvhttp.cpp \ backend/nvpairingmanager.cpp \ - streaming/video.c \ - streaming/connection.cpp \ backend/computermanager.cpp \ backend/boxartmanager.cpp \ - settings/streamingpreferences.cpp + settings/streamingpreferences.cpp \ + streaming/input.cpp \ + streaming/session.cpp \ + streaming/audio.cpp \ + streaming/video.cpp HEADERS += \ utils.h \ @@ -72,10 +72,11 @@ HEADERS += \ backend/identitymanager.h \ backend/nvhttp.h \ backend/nvpairingmanager.h \ - streaming/streaming.h \ backend/computermanager.h \ backend/boxartmanager.h \ - settings/streamingpreferences.h + settings/streamingpreferences.h \ + streaming/input.hpp \ + streaming/session.hpp FORMS += \ gui/mainwindow.ui diff --git a/app/backend/computermanager.cpp b/app/backend/computermanager.cpp index ecbbf7fb..1e73cd37 100644 --- a/app/backend/computermanager.cpp +++ b/app/backend/computermanager.cpp @@ -45,6 +45,8 @@ NvComputer::NvComputer(QSettings& settings) this->currentGameId = 0; this->pairState = PS_UNKNOWN; this->state = CS_UNKNOWN; + this->gfeVersion = nullptr; + this->appVersion = nullptr; } void @@ -104,6 +106,8 @@ NvComputer::NvComputer(QString address, QString serverInfo) this->pairState = NvHTTP::getXmlString(serverInfo, "PairStatus") == "1" ? PS_PAIRED : PS_NOT_PAIRED; this->currentGameId = NvHTTP::getCurrentGame(serverInfo); + this->appVersion = NvHTTP::getXmlString(serverInfo, "appversion"); + this->gfeVersion = NvHTTP::getXmlString(serverInfo, "GfeVersion"); this->activeAddress = address; this->state = NvComputer::CS_ONLINE; } @@ -142,6 +146,8 @@ bool NvComputer::update(NvComputer& that) ASSIGN_IF_CHANGED(currentGameId); ASSIGN_IF_CHANGED(activeAddress); ASSIGN_IF_CHANGED(state); + ASSIGN_IF_CHANGED(gfeVersion); + ASSIGN_IF_CHANGED(appVersion); ASSIGN_IF_CHANGED_AND_NONEMPTY(appList); return changed; } diff --git a/app/backend/computermanager.h b/app/backend/computermanager.h index 9acf07b5..60f5f001 100644 --- a/app/backend/computermanager.h +++ b/app/backend/computermanager.h @@ -37,6 +37,8 @@ public: PairState pairState; QString activeAddress; int currentGameId; + QString gfeVersion; + QString appVersion; // Persisted traits QString localAddress; diff --git a/app/gui/mainwindow.cpp b/app/gui/mainwindow.cpp index 87d93652..d336f83d 100644 --- a/app/gui/mainwindow.cpp +++ b/app/gui/mainwindow.cpp @@ -3,13 +3,11 @@ #include "gui/popupmanager.h" #include "backend/identitymanager.h" #include "backend/nvpairingmanager.h" -#include "streaming/streaming.h" +#include "streaming/session.hpp" #include "backend/computermanager.h" #include "backend/boxartmanager.h" #include "settings/streamingpreferences.h" -#include - MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new Ui::MainWindow), @@ -57,92 +55,28 @@ void MainWindow::computerStateChanged(NvComputer* computer) // Stop polling before launching a game m_ComputerManager.stopPollingAsync(); -#if 1 - STREAM_CONFIGURATION streamConfig; - StreamingPreferences prefs; + Session session(computer, computer->appList.last()); + QStringList warnings; + QString errorMessage; - LiInitializeStreamConfiguration(&streamConfig); - // TODO: Validate 4K and 4K60 based on serverinfo - streamConfig.width = prefs.width; - streamConfig.height = prefs.height; - streamConfig.fps = prefs.fps; - streamConfig.bitrate = prefs.bitrateKbps; - streamConfig.packetSize = 1024; - streamConfig.hevcBitratePercentageMultiplier = 75; - for (int i = 0; i < sizeof(streamConfig.remoteInputAesKey); i++) { - streamConfig.remoteInputAesKey[i] = - (char)(QRandomGenerator::global()->generate() % 256); - } - *(int*)streamConfig.remoteInputAesIv = qToBigEndian(QRandomGenerator::global()->generate()); - switch (prefs.audioConfig) - { - case StreamingPreferences::AC_AUTO: - streamConfig.audioConfiguration = SdlDetermineAudioConfiguration(); - break; - case StreamingPreferences::AC_FORCE_STEREO: - streamConfig.audioConfiguration = AUDIO_CONFIGURATION_STEREO; - break; - case StreamingPreferences::AC_FORCE_SURROUND: - streamConfig.audioConfiguration = AUDIO_CONFIGURATION_51_SURROUND; - break; - } - switch (prefs.videoCodecConfig) - { - case StreamingPreferences::VCC_AUTO: - // TODO: Determine if HEVC is better depending on the decoder - streamConfig.supportsHevc = true; - streamConfig.enableHdr = false; - break; - case StreamingPreferences::VCC_FORCE_H264: - streamConfig.supportsHevc = false; - streamConfig.enableHdr = false; - break; - case StreamingPreferences::VCC_FORCE_HEVC: - streamConfig.supportsHevc = true; - streamConfig.enableHdr = false; - break; - case StreamingPreferences::VCC_FORCE_HEVC_HDR: - streamConfig.supportsHevc = true; - streamConfig.enableHdr = true; - break; + // First check for a fatal configuration error + errorMessage = session.checkForFatalValidationError(); + if (!errorMessage.isEmpty()) { + // TODO: display error dialog + goto AfterStreaming; } - // TODO: Validate HEVC support based on decoder caps - // TODO: Validate HDR support based on decoder caps, display, server caps, and app - - // Initialize the gamepad code with our preferences - // Note: must be done before SdlGetAttachedGamepadMask! - SdlInitializeGamepad(prefs.multiController); - - QMessageBox* box = new QMessageBox(nullptr); - box->setAttribute(Qt::WA_DeleteOnClose); //makes sure the msgbox is deleted automatically when closed - box->setStandardButtons(QMessageBox::Cancel); - box->setText("Launching game..."); - box->open(); - - if (computer->currentGameId != 0) { - http.resumeApp(&streamConfig); - } - else { - http.launchApp(999999, &streamConfig, - prefs.enableGameOptimizations, - prefs.playAudioOnHost, - SdlGetAttachedGamepadMask()); + // Check for any informational messages to display + warnings = session.checkForAdvisoryValidationError(); + if (!warnings.isEmpty()) { + // TODO: display toast or something before we start } - SERVER_INFORMATION hostInfo; - QString serverInfo = http.getServerInfo(); + // Run the streaming session until termination + session.exec(); - QByteArray hostnameStr = computer->activeAddress.toLatin1(); - QByteArray siAppVersion = http.getXmlString(serverInfo, "appversion").toLatin1(); - QByteArray siGfeVersion = http.getXmlString(serverInfo, "GfeVersion").toLatin1(); - - hostInfo.address = hostnameStr.data(); - hostInfo.serverInfoAppVersion = siAppVersion.data(); - hostInfo.serverInfoGfeVersion = siGfeVersion.data(); - - StartConnection(&hostInfo, &streamConfig, box); -#endif + AfterStreaming: + m_ComputerManager.startPolling(); } } diff --git a/app/streaming/audio.c b/app/streaming/audio.cpp similarity index 63% rename from app/streaming/audio.c rename to app/streaming/audio.cpp index 133f33fc..da2de610 100644 --- a/app/streaming/audio.c +++ b/app/streaming/audio.cpp @@ -1,16 +1,17 @@ +#include "session.hpp" + #include -#include #include #define MAX_CHANNELS 6 #define SAMPLES_PER_FRAME 240 -static SDL_AudioDeviceID g_AudioDevice; -static OpusMSDecoder* g_OpusDecoder; -static short g_OpusDecodeBuffer[MAX_CHANNELS * SAMPLES_PER_FRAME]; -static OPUS_MULTISTREAM_CONFIGURATION g_OpusConfig; +SDL_AudioDeviceID Session::s_AudioDevice; +OpusMSDecoder* Session::s_OpusDecoder; +short Session::s_OpusDecodeBuffer[MAX_CHANNELS * SAMPLES_PER_FRAME]; +int Session::s_ChannelCount; -int SdlDetermineAudioConfiguration(void) +int Session::sdlDetermineAudioConfiguration() { SDL_AudioSpec want, have; SDL_AudioDeviceID dev; @@ -48,7 +49,9 @@ int SdlDetermineAudioConfiguration(void) } } -int SdlAudioInit(int audioConfiguration, POPUS_MULTISTREAM_CONFIGURATION opusConfig, void* arContext, int arFlags) +int Session::sdlAudioInit(int audioConfiguration, + POPUS_MULTISTREAM_CONFIGURATION opusConfig, + void* arContext, int arFlags) { SDL_AudioSpec want, have; int error; @@ -59,81 +62,72 @@ int SdlAudioInit(int audioConfiguration, POPUS_MULTISTREAM_CONFIGURATION opusCon want.channels = opusConfig->channelCount; want.samples = 1024; - g_AudioDevice = SDL_OpenAudioDevice(NULL, 0, &want, &have, 0); - if (g_AudioDevice == 0) { + s_AudioDevice = SDL_OpenAudioDevice(NULL, 0, &want, &have, 0); + if (s_AudioDevice == 0) { SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Failed to open audio device: %s", SDL_GetError()); return -1; } - g_OpusDecoder = opus_multistream_decoder_create(opusConfig->sampleRate, + s_OpusDecoder = opus_multistream_decoder_create(opusConfig->sampleRate, opusConfig->channelCount, opusConfig->streams, opusConfig->coupledStreams, opusConfig->mapping, &error); - if (g_OpusDecoder == NULL) { + if (s_OpusDecoder == NULL) { SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Failed to create decoder: %d", error); - SDL_CloseAudioDevice(g_AudioDevice); - g_AudioDevice = 0; + SDL_CloseAudioDevice(s_AudioDevice); + s_AudioDevice = 0; return -2; } - SDL_memcpy(&g_OpusConfig, opusConfig, sizeof(g_OpusConfig)); + s_ChannelCount = opusConfig->channelCount; return 0; } -void SdlAudioStart(void) +void Session::sdlAudioStart() { // Unpause the audio device - SDL_PauseAudioDevice(g_AudioDevice, 0); + SDL_PauseAudioDevice(s_AudioDevice, 0); } -void SdlAudioStop(void) +void Session::sdlAudioStop() { // Pause the audio device - SDL_PauseAudioDevice(g_AudioDevice, 1); + SDL_PauseAudioDevice(s_AudioDevice, 1); } -void SdlAudioCleanup(void) +void Session::sdlAudioCleanup() { - SDL_CloseAudioDevice(g_AudioDevice); - g_AudioDevice = 0; + SDL_CloseAudioDevice(s_AudioDevice); + s_AudioDevice = 0; - opus_multistream_decoder_destroy(g_OpusDecoder); - g_OpusDecoder = NULL; + opus_multistream_decoder_destroy(s_OpusDecoder); + s_OpusDecoder = NULL; } -void SdlAudioDecodeAndPlaySample(char* sampleData, int sampleLength) +void Session::sdlAudioDecodeAndPlaySample(char* sampleData, int sampleLength) { int samplesDecoded; - samplesDecoded = opus_multistream_decode(g_OpusDecoder, + samplesDecoded = opus_multistream_decode(s_OpusDecoder, (unsigned char*)sampleData, sampleLength, - g_OpusDecodeBuffer, + s_OpusDecodeBuffer, SAMPLES_PER_FRAME, 0); if (samplesDecoded > 0) { - if (SDL_QueueAudio(g_AudioDevice, - g_OpusDecodeBuffer, - sizeof(short) * samplesDecoded * g_OpusConfig.channelCount) < 0) { + if (SDL_QueueAudio(s_AudioDevice, + s_OpusDecodeBuffer, + sizeof(short) * samplesDecoded * s_ChannelCount) < 0) { SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Failed to queue audio sample: %s", SDL_GetError()); } } } - -AUDIO_RENDERER_CALLBACKS k_AudioCallbacks = { - SdlAudioInit, - SdlAudioStart, - SdlAudioStop, - SdlAudioCleanup, - SdlAudioDecodeAndPlaySample, - CAPABILITY_DIRECT_SUBMIT -}; diff --git a/app/streaming/connection.cpp b/app/streaming/connection.cpp deleted file mode 100644 index 1f642ead..00000000 --- a/app/streaming/connection.cpp +++ /dev/null @@ -1,134 +0,0 @@ -#include "streaming.h" -#include -#include - -#include - -bool g_StreamActive; -QMessageBox* g_ProgressBox; - -static -void -ClStageStarting(int stage) -{ - char buffer[512]; - sprintf(buffer, "Starting %s...", LiGetStageName(stage)); - g_ProgressBox->setText(buffer); -} - -static -void -ClStageFailed(int stage, long errorCode) -{ - char buffer[512]; - sprintf(buffer, "Failed %s with error: %ld", - LiGetStageName(stage), errorCode); - g_ProgressBox->setText(buffer); -} - -static -void -ClConnectionTerminated(long errorCode) -{ - SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, - "Connection terminated: %ld", - errorCode); - - // Push a quit event to the main loop - SDL_Event event; - event.type = SDL_QUIT; - event.quit.timestamp = SDL_GetTicks(); - SDL_PushEvent(&event); -} - -static -void -ClLogMessage(const char* format, ...) -{ - va_list ap; - - va_start(ap, format); - SDL_LogMessageV(SDL_LOG_CATEGORY_APPLICATION, - SDL_LOG_PRIORITY_INFO, - format, - ap); - va_end(ap); -} - -int -StartConnection(PSERVER_INFORMATION serverInfo, PSTREAM_CONFIGURATION streamConfig, QMessageBox* progressBox) -{ - CONNECTION_LISTENER_CALLBACKS listener; - - // Hold onto this for our callbacks - g_ProgressBox = progressBox; - - - LiInitializeConnectionCallbacks(&listener); - listener.stageStarting = ClStageStarting; - listener.stageFailed = ClStageFailed; - listener.connectionTerminated = ClConnectionTerminated; - listener.logMessage = ClLogMessage; - - int err = LiStartConnection(serverInfo, streamConfig, &listener, - &k_VideoCallbacks, &k_AudioCallbacks, - NULL, 0, NULL, 0); - if (err != 0) { - SDL_Quit(); - return err; - } - - // Before we get into our loop, close the message box used to - // display progress - Q_ASSERT(g_ProgressBox == progressBox); - progressBox->close(); - delete progressBox; - g_ProgressBox = nullptr; - - // 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; - while (SDL_WaitEvent(&event)) { - switch (event.type) { - case SDL_QUIT: - SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, - "Quit event received"); - goto Exit; - case SDL_KEYUP: - case SDL_KEYDOWN: - SdlHandleKeyEvent(&event.key); - break; - case SDL_MOUSEBUTTONDOWN: - case SDL_MOUSEBUTTONUP: - SdlHandleMouseButtonEvent(&event.button); - break; - case SDL_MOUSEMOTION: - SdlHandleMouseMotionEvent(&event.motion); - break; - case SDL_MOUSEWHEEL: - SdlHandleMouseWheelEvent(&event.wheel); - break; - case SDL_CONTROLLERAXISMOTION: - SdlHandleControllerAxisEvent(&event.caxis); - break; - case SDL_CONTROLLERBUTTONDOWN: - case SDL_CONTROLLERBUTTONUP: - SdlHandleControllerButtonEvent(&event.cbutton); - break; - case SDL_CONTROLLERDEVICEADDED: - case SDL_CONTROLLERDEVICEREMOVED: - SdlHandleControllerDeviceEvent(&event.cdevice); - break; - } - } - - SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, - "SDL_WaitEvent() failed: %s", - SDL_GetError()); - -Exit: - g_StreamActive = false; - LiStopConnection(); - - return 0; -} diff --git a/app/streaming/input.c b/app/streaming/input.cpp similarity index 83% rename from app/streaming/input.c rename to app/streaming/input.cpp index 862343cc..8cf6ee0f 100644 --- a/app/streaming/input.c +++ b/app/streaming/input.cpp @@ -1,6 +1,6 @@ #include #include -#include +#include "input.hpp" #define VK_0 0x30 #define VK_A 0x41 @@ -12,23 +12,7 @@ #define VK_NUMPAD1 0x61 #endif -typedef struct _GAMEPAD_STATE { - SDL_GameController* controller; - SDL_JoystickID jsId; - short index; - - short buttons; - short lsX, lsY; - short rsX, rsY; - unsigned char lt, rt; -} GAMEPAD_STATE, *PGAMEPAD_STATE; - -#define MAX_GAMEPADS 4 -static GAMEPAD_STATE g_GamepadState[MAX_GAMEPADS]; -static unsigned short g_GamepadMask; -static bool g_MultiController; - -static const short k_ButtonMap[] = { +const int SdlInputHandler::k_ButtonMap[] = { A_FLAG, B_FLAG, X_FLAG, Y_FLAG, BACK_FLAG, SPECIAL_FLAG, PLAY_FLAG, LS_CLK_FLAG, RS_CLK_FLAG, @@ -36,7 +20,16 @@ static const short k_ButtonMap[] = { UP_FLAG, DOWN_FLAG, LEFT_FLAG, RIGHT_FLAG }; -void SdlHandleKeyEvent(SDL_KeyboardEvent* event) +SdlInputHandler::SdlInputHandler(bool multiController) + : m_MultiController(multiController) +{ + if (!m_MultiController) { + // Player 1 is always present in non-MC mode + m_GamepadMask = 0x1; + } +} + +void SdlInputHandler::handleKeyEvent(SDL_KeyboardEvent* event) { short keyCode; char modifiers; @@ -257,7 +250,7 @@ void SdlHandleKeyEvent(SDL_KeyboardEvent* event) modifiers); } -void SdlHandleMouseButtonEvent(SDL_MouseButtonEvent* event) +void SdlInputHandler::handleMouseButtonEvent(SDL_MouseButtonEvent* event) { int button; @@ -289,7 +282,7 @@ void SdlHandleMouseButtonEvent(SDL_MouseButtonEvent* event) button); } -void SdlHandleMouseMotionEvent(SDL_MouseMotionEvent* event) +void SdlInputHandler::handleMouseMotionEvent(SDL_MouseMotionEvent* event) { if (event->xrel != 0 || event->yrel != 0) { LiSendMouseMoveEvent((unsigned short)event->xrel, @@ -297,34 +290,35 @@ void SdlHandleMouseMotionEvent(SDL_MouseMotionEvent* event) } } -void SdlHandleMouseWheelEvent(SDL_MouseWheelEvent* event) +void SdlInputHandler::handleMouseWheelEvent(SDL_MouseWheelEvent* event) { if (event->y != 0) { LiSendScrollEvent((signed char)event->y); } } -static PGAMEPAD_STATE FindStateForGamepad(SDL_JoystickID id) +GamepadState* +SdlInputHandler::findStateForGamepad(SDL_JoystickID id) { int i; for (i = 0; i < MAX_GAMEPADS; i++) { - if (g_GamepadState[i].jsId == id) { - SDL_assert(!g_MultiController || g_GamepadState[i].index == i); - return &g_GamepadState[i]; + if (m_GamepadState[i].jsId == id) { + SDL_assert(!m_MultiController || m_GamepadState[i].index == i); + return &m_GamepadState[i]; } } // This should only happen with > 4 gamepads SDL_assert(SDL_NumJoysticks() > 4); - return NULL; + return nullptr; } -static void SendGamepadState(PGAMEPAD_STATE state) +void SdlInputHandler::sendGamepadState(GamepadState* state) { - SDL_assert(g_GamepadMask == 0x1 || g_MultiController); + SDL_assert(m_GamepadMask == 0x1 || m_MultiController); LiSendMultiControllerEvent(state->index, - g_GamepadMask, + m_GamepadMask, state->buttons, state->lt, state->rt, @@ -334,9 +328,9 @@ static void SendGamepadState(PGAMEPAD_STATE state) state->rsY); } -void SdlHandleControllerAxisEvent(SDL_ControllerAxisEvent* event) +void SdlInputHandler::handleControllerAxisEvent(SDL_ControllerAxisEvent* event) { - PGAMEPAD_STATE state = FindStateForGamepad(event->which); + GamepadState* state = findStateForGamepad(event->which); if (state == NULL) { return; } @@ -368,12 +362,12 @@ void SdlHandleControllerAxisEvent(SDL_ControllerAxisEvent* event) return; } - SendGamepadState(state); + sendGamepadState(state); } -void SdlHandleControllerButtonEvent(SDL_ControllerButtonEvent* event) +void SdlInputHandler::handleControllerButtonEvent(SDL_ControllerButtonEvent* event) { - PGAMEPAD_STATE state = FindStateForGamepad(event->which); + GamepadState* state = findStateForGamepad(event->which); if (state == NULL) { return; } @@ -385,19 +379,19 @@ void SdlHandleControllerButtonEvent(SDL_ControllerButtonEvent* event) state->buttons &= ~k_ButtonMap[event->button]; } - SendGamepadState(state); + sendGamepadState(state); } -void SdlHandleControllerDeviceEvent(SDL_ControllerDeviceEvent* event) +void SdlInputHandler::handleControllerDeviceEvent(SDL_ControllerDeviceEvent* event) { - PGAMEPAD_STATE state; + GamepadState* state; if (event->type == SDL_CONTROLLERDEVICEADDED) { int i; const char* name; for (i = 0; i < MAX_GAMEPADS; i++) { - if (g_GamepadState[i].controller == NULL) { + if (m_GamepadState[i].controller == NULL) { // Found an empty slot break; } @@ -409,8 +403,8 @@ void SdlHandleControllerDeviceEvent(SDL_ControllerDeviceEvent* event) return; } - state = &g_GamepadState[i]; - if (g_MultiController) { + state = &m_GamepadState[i]; + if (m_MultiController) { state->index = i; } else { @@ -435,29 +429,29 @@ void SdlHandleControllerDeviceEvent(SDL_ControllerDeviceEvent* event) name != NULL ? name : ""); // Add this gamepad to the gamepad mask - if (g_MultiController) { - SDL_assert(!(g_GamepadMask & (1 << state->index))); - g_GamepadMask |= (1 << state->index); + if (m_MultiController) { + SDL_assert(!(m_GamepadMask & (1 << state->index))); + m_GamepadMask |= (1 << state->index); } else { - SDL_assert(g_GamepadMask == 0x1); + SDL_assert(m_GamepadMask == 0x1); } // Send an empty event to tell the PC we've arrived - SendGamepadState(state); + sendGamepadState(state); } else if (event->type == SDL_CONTROLLERDEVICEREMOVED) { - state = FindStateForGamepad(event->which); + state = findStateForGamepad(event->which); if (state != NULL) { SDL_GameControllerClose(state->controller); // Remove this from the gamepad mask in MC-mode - if (g_MultiController) { - SDL_assert(g_GamepadMask & (1 << state->index)); - g_GamepadMask &= ~(1 << state->index); + if (m_MultiController) { + SDL_assert(m_GamepadMask & (1 << state->index)); + m_GamepadMask &= ~(1 << state->index); } else { - SDL_assert(g_GamepadMask == 0x1); + SDL_assert(m_GamepadMask == 0x1); } SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, @@ -465,7 +459,7 @@ void SdlHandleControllerDeviceEvent(SDL_ControllerDeviceEvent* event) state->index); // Send a final event to let the PC know this gamepad is gone - LiSendMultiControllerEvent(state->index, g_GamepadMask, + LiSendMultiControllerEvent(state->index, m_GamepadMask, 0, 0, 0, 0, 0, 0, 0); // Clear all remaining state from this slot @@ -474,13 +468,13 @@ void SdlHandleControllerDeviceEvent(SDL_ControllerDeviceEvent* event) } } -int SdlGetAttachedGamepadMask(void) +int SdlInputHandler::getAttachedGamepadMask() { int i; int count; int mask; - if (!g_MultiController) { + if (!m_MultiController) { // Player 1 is always present in non-MC mode return 0x1; } @@ -494,12 +488,3 @@ int SdlGetAttachedGamepadMask(void) return mask; } - -void SdlInitializeGamepad(bool multiController) -{ - g_MultiController = multiController; - if (!g_MultiController) { - // Player 1 is always present in non-MC mode - g_GamepadMask = 0x01; - } -} diff --git a/app/streaming/input.hpp b/app/streaming/input.hpp new file mode 100644 index 00000000..ddb4f2f8 --- /dev/null +++ b/app/streaming/input.hpp @@ -0,0 +1,50 @@ +#pragma once + +#include + +struct GamepadState { + SDL_GameController* controller; + SDL_JoystickID jsId; + short index; + + short buttons; + short lsX, lsY; + short rsX, rsY; + unsigned char lt, rt; +}; + +#define MAX_GAMEPADS 4 + +class SdlInputHandler +{ +public: + explicit SdlInputHandler(bool multiController); + + void handleKeyEvent(SDL_KeyboardEvent* event); + + void handleMouseButtonEvent(SDL_MouseButtonEvent* event); + + void handleMouseMotionEvent(SDL_MouseMotionEvent* event); + + void handleMouseWheelEvent(SDL_MouseWheelEvent* event); + + void handleControllerAxisEvent(SDL_ControllerAxisEvent* event); + + void handleControllerButtonEvent(SDL_ControllerButtonEvent* event); + + void handleControllerDeviceEvent(SDL_ControllerDeviceEvent* event); + + int getAttachedGamepadMask(); + +private: + GamepadState* + findStateForGamepad(SDL_JoystickID id); + + void sendGamepadState(GamepadState* state); + + bool m_MultiController; + int m_GamepadMask; + GamepadState m_GamepadState[MAX_GAMEPADS]; + + static const int k_ButtonMap[]; +}; diff --git a/app/streaming/session.cpp b/app/streaming/session.cpp new file mode 100644 index 00000000..5714fd15 --- /dev/null +++ b/app/streaming/session.cpp @@ -0,0 +1,298 @@ +#include "session.hpp" +#include "settings/streamingpreferences.h" + +#include +#include + +#include +#include +#include +#include + +CONNECTION_LISTENER_CALLBACKS Session::k_ConnCallbacks = { + Session::clStageStarting, + nullptr, + Session::clStageFailed, + nullptr, + Session::clConnectionTerminated, + nullptr, + nullptr, + Session::clLogMessage +}; + +AUDIO_RENDERER_CALLBACKS Session::k_AudioCallbacks = { + Session::sdlAudioInit, + Session::sdlAudioStart, + Session::sdlAudioStop, + Session::sdlAudioCleanup, + Session::sdlAudioDecodeAndPlaySample, + CAPABILITY_DIRECT_SUBMIT +}; + +DECODER_RENDERER_CALLBACKS Session::k_VideoCallbacks; + +Session* Session::s_ActiveSession; + +void Session::clStageStarting(int stage) +{ + char buffer[512]; + sprintf(buffer, "Starting %s...", LiGetStageName(stage)); + + // We know this is called on the same thread as LiStartConnection() + // which happens to be the main thread, so it's cool to interact + // with the GUI in these callbacks. + s_ActiveSession->m_ProgressBox.setText(buffer); + QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents); +} + +void Session::clStageFailed(int stage, long errorCode) +{ + s_ActiveSession->m_ProgressBox.close(); + + // TODO: Open error dialog + char buffer[512]; + sprintf(buffer, "Failed %s with error: %ld", + LiGetStageName(stage), errorCode); + + // We know this is called on the same thread as LiStartConnection() + // which happens to be the main thread, so it's cool to interact + // with the GUI in these callbacks. + QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents); +} + +void Session::clConnectionTerminated(long errorCode) +{ + SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, + "Connection terminated: %ld", + errorCode); + + // Push a quit event to the main loop + SDL_Event event; + event.type = SDL_QUIT; + event.quit.timestamp = SDL_GetTicks(); + SDL_PushEvent(&event); +} + +void Session::clLogMessage(const char* format, ...) +{ + va_list ap; + + va_start(ap, format); + SDL_LogMessageV(SDL_LOG_CATEGORY_APPLICATION, + SDL_LOG_PRIORITY_INFO, + format, + ap); + va_end(ap); +} + +Session::Session(NvComputer* computer, NvApp& app) + : m_Computer(computer), + m_App(app), + m_ProgressBox(nullptr) +{ + StreamingPreferences prefs; + + LiInitializeStreamConfiguration(&m_StreamConfig); + m_StreamConfig.width = prefs.width; + m_StreamConfig.height = prefs.height; + m_StreamConfig.fps = prefs.fps; + m_StreamConfig.bitrate = prefs.bitrateKbps; + m_StreamConfig.packetSize = 1024; + m_StreamConfig.hevcBitratePercentageMultiplier = 75; + for (int i = 0; i < sizeof(m_StreamConfig.remoteInputAesKey); i++) { + m_StreamConfig.remoteInputAesKey[i] = + (char)(QRandomGenerator::global()->generate() % 256); + } + *(int*)m_StreamConfig.remoteInputAesIv = qToBigEndian(QRandomGenerator::global()->generate()); + switch (prefs.audioConfig) + { + case StreamingPreferences::AC_AUTO: + m_StreamConfig.audioConfiguration = sdlDetermineAudioConfiguration(); + break; + case StreamingPreferences::AC_FORCE_STEREO: + m_StreamConfig.audioConfiguration = AUDIO_CONFIGURATION_STEREO; + break; + case StreamingPreferences::AC_FORCE_SURROUND: + m_StreamConfig.audioConfiguration = AUDIO_CONFIGURATION_51_SURROUND; + break; + } + switch (prefs.videoCodecConfig) + { + case StreamingPreferences::VCC_AUTO: + // TODO: Determine if HEVC is better depending on the decoder + m_StreamConfig.supportsHevc = true; + m_StreamConfig.enableHdr = false; + break; + case StreamingPreferences::VCC_FORCE_H264: + m_StreamConfig.supportsHevc = false; + m_StreamConfig.enableHdr = false; + break; + case StreamingPreferences::VCC_FORCE_HEVC: + m_StreamConfig.supportsHevc = true; + m_StreamConfig.enableHdr = false; + break; + case StreamingPreferences::VCC_FORCE_HEVC_HDR: + m_StreamConfig.supportsHevc = true; + m_StreamConfig.enableHdr = true; + break; + } +} + +QString Session::checkForFatalValidationError() +{ + // Nothing here yet + return nullptr; +} + +QStringList Session::checkForAdvisoryValidationError() +{ + NvHTTP http(m_Computer->activeAddress); + QStringList warningList; + + if (m_StreamConfig.enableHdr) { + // Turn HDR back off unless all criteria are met. + m_StreamConfig.enableHdr = false; + + // Check that the app supports HDR + if (!m_App.hdrSupported) { + warningList.append(m_App.name + " doesn't support HDR10."); + } + // Check that the server GPU supports HDR + else if (!(m_Computer->serverCodecModeSupport & 0x200)) { + warningList.append("Your host PC GPU doesn't support HDR streaming. " + "A GeForce GTX 1000-series (Pascal) or later GPU is required for HDR streaming."); + } + else { + // TODO: Also validate client decoder and display capabilites + + // Validation successful so HDR is good to go + m_StreamConfig.enableHdr = true; + } + } + + if (m_StreamConfig.width >= 3840) { + if (m_StreamConfig.fps >= 60) { + // TODO: Validate 4K60 support based on serverinfo + } + else { + // TODO: Validate 4K30 support based on serverinfo + } + } + + if (m_StreamConfig.supportsHevc) { + // TODO: Validate HEVC support based on decoder caps + } + + return warningList; +} + +void Session::exec() +{ + // We're now active + s_ActiveSession = this; + + // Initialize the gamepad code with our preferences + StreamingPreferences prefs; + SdlInputHandler inputHandler(prefs.multiController); + + m_ProgressBox.setStandardButtons(QMessageBox::Cancel); + m_ProgressBox.setText("Launching "+m_App.name+"..."); + m_ProgressBox.open(); + + // Ensure the progress box is immediately visible + QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents); + + NvHTTP http(m_Computer->activeAddress); + + // The UI should have ensured the old game was already quit + // if we decide to stream a different game. + Q_ASSERT(m_Computer->currentGameId == 0 || + m_Computer->currentGameId == m_App.id); + + if (m_Computer->currentGameId != 0) { + http.resumeApp(&m_StreamConfig); + } + else { + http.launchApp(m_App.id, &m_StreamConfig, + prefs.enableGameOptimizations, + prefs.playAudioOnHost, + inputHandler.getAttachedGamepadMask()); + } + + QByteArray hostnameStr = m_Computer->activeAddress.toLatin1(); + QByteArray siAppVersion = m_Computer->appVersion.toLatin1(); + + SERVER_INFORMATION hostInfo; + hostInfo.address = hostnameStr.data(); + hostInfo.serverInfoAppVersion = siAppVersion.data(); + + // Older GFE versions didn't have this field + QByteArray siGfeVersion; + if (!m_Computer->gfeVersion.isNull()) { + siGfeVersion = m_Computer->gfeVersion.toLatin1(); + } + if (!siGfeVersion.isNull()) { + hostInfo.serverInfoGfeVersion = siGfeVersion.data(); + } + + int err = LiStartConnection(&hostInfo, &m_StreamConfig, &k_ConnCallbacks, + &k_VideoCallbacks, &k_AudioCallbacks, + NULL, 0, NULL, 0); + if (err != 0) { + // We already displayed an error dialog in the stage failure + // listener. + return; + } + + // Before we get into our SDL loop, close the message box used to + // display progress + m_ProgressBox.close(); + QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents); + + //SDL_CreateWindow("SDL", 0, 0, 1280, 720, SDL_WINDOW_INPUT_GRABBED); + + // 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; + while (SDL_WaitEvent(&event)) { + switch (event.type) { + case SDL_QUIT: + SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, + "Quit event received"); + goto Exit; + case SDL_KEYUP: + case SDL_KEYDOWN: + inputHandler.handleKeyEvent(&event.key); + break; + case SDL_MOUSEBUTTONDOWN: + case SDL_MOUSEBUTTONUP: + inputHandler.handleMouseButtonEvent(&event.button); + break; + case SDL_MOUSEMOTION: + inputHandler.handleMouseMotionEvent(&event.motion); + break; + case SDL_MOUSEWHEEL: + inputHandler.handleMouseWheelEvent(&event.wheel); + break; + case SDL_CONTROLLERAXISMOTION: + inputHandler.handleControllerAxisEvent(&event.caxis); + break; + case SDL_CONTROLLERBUTTONDOWN: + case SDL_CONTROLLERBUTTONUP: + inputHandler.handleControllerButtonEvent(&event.cbutton); + break; + case SDL_CONTROLLERDEVICEADDED: + case SDL_CONTROLLERDEVICEREMOVED: + inputHandler.handleControllerDeviceEvent(&event.cdevice); + break; + } + } + + SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, + "SDL_WaitEvent() failed: %s", + SDL_GetError()); + +Exit: + s_ActiveSession = nullptr; + LiStopConnection(); +} diff --git a/app/streaming/session.hpp b/app/streaming/session.hpp new file mode 100644 index 00000000..d0ba7d54 --- /dev/null +++ b/app/streaming/session.hpp @@ -0,0 +1,69 @@ +#pragma once + +#include +#include +#include "backend/computermanager.h" +#include "input.hpp" + +#include + + +class Session +{ +public: + explicit Session(NvComputer* computer, NvApp& app); + + QString checkForFatalValidationError(); + + QStringList checkForAdvisoryValidationError(); + + void exec(); + +private: + static + void clStageStarting(int stage); + + static + void clStageFailed(int stage, long errorCode); + + static + void clConnectionTerminated(long errorCode); + + static + void clLogMessage(const char* format, ...); + + static + int sdlDetermineAudioConfiguration(); + + static + int sdlAudioInit(int audioConfiguration, + POPUS_MULTISTREAM_CONFIGURATION opusConfig, + void* arContext, int arFlags); + + static + void sdlAudioStart(); + + static + void sdlAudioStop(); + + static + void sdlAudioCleanup(); + + static + void sdlAudioDecodeAndPlaySample(char* sampleData, int sampleLength); + + STREAM_CONFIGURATION m_StreamConfig; + QMessageBox m_ProgressBox; + NvComputer* m_Computer; + NvApp m_App; + + static SDL_AudioDeviceID s_AudioDevice; + static OpusMSDecoder* s_OpusDecoder; + static short s_OpusDecodeBuffer[]; + static int s_ChannelCount; + + static AUDIO_RENDERER_CALLBACKS k_AudioCallbacks; + static DECODER_RENDERER_CALLBACKS k_VideoCallbacks; + static CONNECTION_LISTENER_CALLBACKS k_ConnCallbacks; + static Session* s_ActiveSession; +}; diff --git a/app/streaming/streaming.h b/app/streaming/streaming.h deleted file mode 100644 index 43305afc..00000000 --- a/app/streaming/streaming.h +++ /dev/null @@ -1,31 +0,0 @@ -#pragma once - -#include -#include -#include - -#ifdef __cplusplus -extern "C" { -#endif - -extern AUDIO_RENDERER_CALLBACKS k_AudioCallbacks; -extern DECODER_RENDERER_CALLBACKS k_VideoCallbacks; - -int SdlDetermineAudioConfiguration(void); - -void SdlInitializeGamepad(bool multiController); -int SdlGetAttachedGamepadMask(void); -void SdlHandleControllerDeviceEvent(SDL_ControllerDeviceEvent* event); -void SdlHandleControllerButtonEvent(SDL_ControllerButtonEvent* event); -void SdlHandleControllerAxisEvent(SDL_ControllerAxisEvent* event); -void SdlHandleMouseWheelEvent(SDL_MouseWheelEvent* event); -void SdlHandleMouseMotionEvent(SDL_MouseMotionEvent* event); -void SdlHandleMouseButtonEvent(SDL_MouseButtonEvent* event); -void SdlHandleKeyEvent(SDL_KeyboardEvent* event); - -#ifdef __cplusplus -} -#endif - -// This function uses C++ linkage -int StartConnection(PSERVER_INFORMATION serverInfo, PSTREAM_CONFIGURATION streamConfig, QMessageBox* progressBox); diff --git a/app/streaming/video.c b/app/streaming/video.c deleted file mode 100644 index 7d8e976e..00000000 --- a/app/streaming/video.c +++ /dev/null @@ -1,3 +0,0 @@ -#include - -DECODER_RENDERER_CALLBACKS k_VideoCallbacks; diff --git a/app/streaming/video.cpp b/app/streaming/video.cpp new file mode 100644 index 00000000..46dd576d --- /dev/null +++ b/app/streaming/video.cpp @@ -0,0 +1,2 @@ +#include +#include "session.hpp"