Rewrite streaming code to C++ and forklift most of the setup out of the GUI code

This commit is contained in:
Cameron Gutman 2018-06-28 01:44:43 -07:00
parent cad95f12f6
commit 0821c66d8c
13 changed files with 532 additions and 359 deletions

View File

@ -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

View File

@ -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;
}

View File

@ -37,6 +37,8 @@ public:
PairState pairState;
QString activeAddress;
int currentGameId;
QString gfeVersion;
QString appVersion;
// Persisted traits
QString localAddress;

View File

@ -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 <QRandomGenerator>
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();
}
}

View File

@ -1,16 +1,17 @@
#include "session.hpp"
#include <Limelight.h>
#include <opus_multistream.h>
#include <SDL.h>
#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
};

View File

@ -1,134 +0,0 @@
#include "streaming.h"
#include <Limelight.h>
#include <SDL.h>
#include <QMessageBox>
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;
}

View File

@ -1,6 +1,6 @@
#include <Limelight.h>
#include <SDL.h>
#include <stdbool.h>
#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 : "<null>");
// 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;
}
}

50
app/streaming/input.hpp Normal file
View File

@ -0,0 +1,50 @@
#pragma once
#include <SDL.h>
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[];
};

298
app/streaming/session.cpp Normal file
View File

@ -0,0 +1,298 @@
#include "session.hpp"
#include "settings/streamingpreferences.h"
#include <Limelight.h>
#include <SDL.h>
#include <QRandomGenerator>
#include <QMessageBox>
#include <QtEndian>
#include <QCoreApplication>
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();
}

69
app/streaming/session.hpp Normal file
View File

@ -0,0 +1,69 @@
#pragma once
#include <Limelight.h>
#include <opus_multistream.h>
#include "backend/computermanager.h"
#include "input.hpp"
#include <QMessageBox>
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;
};

View File

@ -1,31 +0,0 @@
#pragma once
#include <Limelight.h>
#include <SDL.h>
#include <QMessageBox>
#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);

View File

@ -1,3 +0,0 @@
#include <Limelight.h>
DECODER_RENDERER_CALLBACKS k_VideoCallbacks;

2
app/streaming/video.cpp Normal file
View File

@ -0,0 +1,2 @@
#include <Limelight.h>
#include "session.hpp"