Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c5d70c48c9 | ||
|
|
82230ef2f5 | ||
|
|
dcfd3d3e48 | ||
|
|
cb1e2ac32e | ||
|
|
ec2c15946a | ||
|
|
8be73142f8 | ||
|
|
d1640e3bb8 | ||
|
|
8612e6726d | ||
|
|
3b4694e9f8 | ||
|
|
14cb2e1999 | ||
|
|
cfabaf334e | ||
|
|
c054536fab | ||
|
|
35aac18b4e | ||
|
|
de3928cb4d | ||
|
|
9bf612b800 | ||
|
|
d9a8d67e3d | ||
|
|
99b0a56a6a | ||
|
|
61d7aa0400 | ||
|
|
1e98594972 | ||
|
|
c6383f042c | ||
|
|
b4d8e0e551 | ||
|
|
d696536486 | ||
|
|
b46a3cf759 | ||
|
|
8a5e3cdb76 | ||
|
|
4fb39226ef | ||
|
|
c40174034c | ||
|
|
5029c855f0 | ||
|
|
2eee839eaa | ||
|
|
f42c7fec1e | ||
|
|
0ab07303c9 | ||
|
|
ad47990a87 | ||
|
|
1968461ce9 | ||
|
|
8aa3b5d3da | ||
|
|
23e77eefb0 | ||
|
|
094443cf26 | ||
|
|
b97758ef46 | ||
|
|
d1fe752028 | ||
|
|
b26df787cd | ||
|
|
4c5b254730 | ||
|
|
9a153b7170 | ||
|
|
065a6caee5 |
@@ -13,6 +13,9 @@ Any special steps that are required for the bug to appear.
|
||||
**Screenshots**
|
||||
If applicable, add screenshots to help explain your problem. If the issue is related to video glitching or poor quality, please include screenshots.
|
||||
|
||||
**Affected games**
|
||||
List the games you've tried that exhibit the issue. To see if the issue is game-specific, try streaming Steam Big Picture with Moonlight and see if the issue persists there.
|
||||
|
||||
**Moonlight settings (please complete the following information)**
|
||||
- Have any settings been adjusted from defaults?
|
||||
- If so, which settings have been changed?
|
||||
@@ -22,10 +25,15 @@ If applicable, add screenshots to help explain your problem. If the issue is rel
|
||||
- OS: [e.g. Windows 10 1803]
|
||||
- Moonlight Version: [e.g. v0.2.0]
|
||||
|
||||
**Logs (please attach)**
|
||||
**Server PC details (please complete the following information)**
|
||||
- OS: [e.g. Windows 10 1803]
|
||||
- GeForce Experience version: [e.g. 3.16.0.140]
|
||||
- Nvidia GPU driver: [e.g. 417.35]
|
||||
|
||||
**Moonlight Logs (please attach)**
|
||||
- On Windows, `Moonlight-###.log` files can be found in `%TEMP%`. Simply type that into the File Explorer path field to navigate there.
|
||||
- On macOS, `Moonlight-###.log` files can be found in `/tmp`. In Finder, press Cmd+Shift+G, then type `/tmp` to navigate there.
|
||||
- On Linux, logs print to the terminal when running via `flatpak run com.moonlight_stream.Moonlight`
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
||||
Anything else you think may be relevant to the issue
|
||||
|
||||
+1
-1
@@ -9,7 +9,7 @@
|
||||
url = https://github.com/gabomdq/SDL_GameControllerDB.git
|
||||
[submodule "soundio/libsoundio"]
|
||||
path = soundio/libsoundio
|
||||
url = https://github.com/cgutman/libsoundio.git
|
||||
url = https://github.com/andrewrk/libsoundio.git
|
||||
[submodule "h264bitstream/h264bitstream"]
|
||||
path = h264bitstream/h264bitstream
|
||||
url = https://github.com/aizvorski/h264bitstream.git
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
# Moonlight PC
|
||||
|
||||
[Moonlight PC](http://moonlight-stream.com) is an open source implementation of NVIDIA's GameStream, as used by the NVIDIA Shield, but built to run on Windows, Mac, and Linux. This client is the successor to [Moonlight Chrome](https://github.com/moonlight-stream/moonlight-chrome) for streaming on PC.
|
||||
[Moonlight PC](https://moonlight-stream.org) is an open source implementation of NVIDIA's GameStream, as used by the NVIDIA Shield, but built to run on Windows, Mac, and Linux. This client is the successor to [Moonlight Chrome](https://github.com/moonlight-stream/moonlight-chrome) for streaming on PC.
|
||||
|
||||
Moonlight also has mobile versions for [Android](https://github.com/moonlight-stream/moonlight-android) and [iOS](https://github.com/moonlight-stream/moonlight-ios).
|
||||
|
||||
See the [releases page](https://github.com/moonlight-stream/moonlight-qt/releases) for download links for Windows and Mac. For Linux, we're on [Flathub](https://flathub.org/apps/details/com.moonlight_stream.Moonlight).
|
||||
|
||||
You can follow development on our [Discord server](https://discord.gg/6ERtzFY).
|
||||
|
||||
[](https://ci.appveyor.com/project/cgutman/moonlight-qt/branch/master)
|
||||
@@ -19,6 +17,10 @@ You can follow development on our [Discord server](https://discord.gg/6ERtzFY).
|
||||
- HEVC support for better image quality at reduced bandwidth
|
||||
- Keyboard and mouse support
|
||||
- Gamepad support with SDL gamepad mappings
|
||||
|
||||
## Downloads
|
||||
- [Windows and macOS](https://github.com/moonlight-stream/moonlight-qt/releases)
|
||||
- [Linux](https://flathub.org/apps/details/com.moonlight_stream.Moonlight)
|
||||
|
||||
## Building
|
||||
### General Build Requirements
|
||||
@@ -38,7 +40,7 @@ You can follow development on our [Discord server](https://discord.gg/6ERtzFY).
|
||||
|
||||
### Linux-specific Requirements
|
||||
* GCC or Clang
|
||||
* Install your distro equivalents of: `openssl-devel qt5-devel SDL2-devel ffmpeg-devel qt5-qtquickcontrols2-devel libva-devel libvdpau-devel opus-devel`
|
||||
* Install your distro equivalents of: `openssl-devel qt5-devel SDL2-devel ffmpeg-devel qt5-qtquickcontrols2-devel libva-devel libvdpau-devel opus-devel pulseaudio-libs-devel alsa-lib-devel`
|
||||
* FFmpeg 4.0 is required to build. If your distro doesn't package FFmpeg 4.0, you can build and install it from source on http://ffmpeg.org/
|
||||
|
||||
### Build Setup Steps
|
||||
@@ -55,4 +57,4 @@ You can follow development on our [Discord server](https://discord.gg/6ERtzFY).
|
||||
2. Write code
|
||||
3. Send Pull Requests
|
||||
|
||||
Check out our [website](http://moonlight-stream.com) for project links and information.
|
||||
Check out our [website](https://moonlight-stream.org) for project links and information.
|
||||
|
||||
+2
-2
@@ -23,9 +23,9 @@
|
||||
<key>NSSupportsAutomaticGraphicsSwitching</key>
|
||||
<true/>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>0.6.4</string>
|
||||
<string>0.8.0</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>0.6.4</string>
|
||||
<string>0.8.0</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>Moonlight</string>
|
||||
</dict>
|
||||
|
||||
+1
-1
Submodule app/SDL_GameControllerDB updated: 19944691bf...04ebce52ee
+7
-3
@@ -54,7 +54,7 @@ macx {
|
||||
}
|
||||
|
||||
unix:!macx {
|
||||
CONFIG += link_pkgconfig
|
||||
CONFIG += link_pkgconfig soundio
|
||||
PKGCONFIG += openssl sdl2 opus
|
||||
|
||||
# For libsoundio
|
||||
@@ -100,6 +100,7 @@ macx {
|
||||
|
||||
SOURCES += \
|
||||
main.cpp \
|
||||
backend/computerseeker.cpp \
|
||||
backend/identitymanager.cpp \
|
||||
backend/nvcomputer.cpp \
|
||||
backend/nvhttp.cpp \
|
||||
@@ -107,6 +108,7 @@ SOURCES += \
|
||||
backend/computermanager.cpp \
|
||||
backend/boxartmanager.cpp \
|
||||
cli/commandlineparser.cpp \
|
||||
cli/quitstream.cpp \
|
||||
cli/startstream.cpp \
|
||||
settings/streamingpreferences.cpp \
|
||||
streaming/input.cpp \
|
||||
@@ -123,6 +125,7 @@ SOURCES += \
|
||||
|
||||
HEADERS += \
|
||||
utils.h \
|
||||
backend/computerseeker.h \
|
||||
backend/identitymanager.h \
|
||||
backend/nvcomputer.h \
|
||||
backend/nvhttp.h \
|
||||
@@ -130,6 +133,7 @@ HEADERS += \
|
||||
backend/computermanager.h \
|
||||
backend/boxartmanager.h \
|
||||
cli/commandlineparser.h \
|
||||
cli/quitstream.h \
|
||||
cli/startstream.h \
|
||||
settings/streamingpreferences.h \
|
||||
streaming/input.h \
|
||||
@@ -323,5 +327,5 @@ macx {
|
||||
QMAKE_RPATHDIR += @executable_path/../Frameworks
|
||||
}
|
||||
|
||||
VERSION = 0.6.4
|
||||
DEFINES += VERSION_STR=\\\"0.6.4\\\"
|
||||
VERSION = 0.8.0
|
||||
DEFINES += VERSION_STR=\\\"0.8.0\\\"
|
||||
|
||||
@@ -12,6 +12,9 @@ AutoUpdateChecker::AutoUpdateChecker(QObject *parent) :
|
||||
// Never communicate over HTTP
|
||||
m_Nam.setStrictTransportSecurityEnabled(true);
|
||||
|
||||
// Allow HTTP redirects
|
||||
m_Nam.setRedirectPolicy(QNetworkRequest::NoLessSafeRedirectPolicy);
|
||||
|
||||
connect(&m_Nam, SIGNAL(finished(QNetworkReply*)),
|
||||
this, SLOT(handleUpdateCheckRequestFinished(QNetworkReply*)));
|
||||
|
||||
@@ -27,7 +30,7 @@ void AutoUpdateChecker::start()
|
||||
{
|
||||
#if defined(Q_OS_WIN32) || defined(Q_OS_DARWIN) // Only run update checker on platforms without auto-update
|
||||
// We'll get a callback when this is finished
|
||||
QUrl url("https://moonlight-stream.com/updates/qt.json");
|
||||
QUrl url("https://moonlight-stream.org/updates/qt.json");
|
||||
m_Nam.get(QNetworkRequest(url));
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -97,7 +97,7 @@ void BoxArtManager::handleBoxArtLoadComplete(NvComputer* computer, NvApp app, QU
|
||||
|
||||
QUrl BoxArtManager::loadBoxArtFromNetwork(NvComputer* computer, int appId)
|
||||
{
|
||||
NvHTTP http(computer->activeAddress);
|
||||
NvHTTP http(computer->activeAddress, computer->serverCert);
|
||||
|
||||
QString cachePath = getFilePathForBoxArt(computer, appId);
|
||||
QImage image;
|
||||
|
||||
@@ -27,7 +27,7 @@ public:
|
||||
private:
|
||||
bool tryPollComputer(QString address, bool& changed)
|
||||
{
|
||||
NvHTTP http(address);
|
||||
NvHTTP http(address, m_Computer->serverCert);
|
||||
|
||||
QString serverInfo;
|
||||
try {
|
||||
@@ -36,7 +36,7 @@ private:
|
||||
return false;
|
||||
}
|
||||
|
||||
NvComputer newState(address, serverInfo);
|
||||
NvComputer newState(address, serverInfo, QSslCertificate());
|
||||
|
||||
// Ensure the machine that responded is the one we intended to contact
|
||||
if (m_Computer->uuid != newState.uuid) {
|
||||
@@ -52,7 +52,7 @@ private:
|
||||
{
|
||||
Q_ASSERT(m_Computer->activeAddress != nullptr);
|
||||
|
||||
NvHTTP http(m_Computer->activeAddress);
|
||||
NvHTTP http(m_Computer->activeAddress, m_Computer->serverCert);
|
||||
|
||||
QVector<NvApp> appList;
|
||||
|
||||
@@ -218,7 +218,7 @@ void ComputerManager::saveHosts()
|
||||
settings.beginWriteArray(SER_HOSTS);
|
||||
for (int i = 0; i < m_KnownHosts.count(); i++) {
|
||||
settings.setArrayIndex(i);
|
||||
m_KnownHosts[m_KnownHosts.keys()[i]]->serialize(settings);
|
||||
m_KnownHosts.value(m_KnownHosts.keys()[i])->serialize(settings);
|
||||
}
|
||||
settings.endArray();
|
||||
}
|
||||
@@ -266,7 +266,7 @@ void ComputerManager::startPollingComputer(NvComputer* computer)
|
||||
}
|
||||
|
||||
if (m_PollThreads.contains(computer->uuid)) {
|
||||
Q_ASSERT(m_PollThreads[computer->uuid]->isRunning());
|
||||
Q_ASSERT(m_PollThreads.value(computer->uuid)->isRunning());
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -324,9 +324,8 @@ public:
|
||||
{
|
||||
QWriteLocker lock(&m_ComputerManager->m_Lock);
|
||||
|
||||
pollingThread = m_ComputerManager->m_PollThreads[m_Computer->uuid];
|
||||
pollingThread = m_ComputerManager->m_PollThreads.take(m_Computer->uuid);
|
||||
|
||||
m_ComputerManager->m_PollThreads.remove(m_Computer->uuid);
|
||||
m_ComputerManager->m_KnownHosts.remove(m_Computer->uuid);
|
||||
}
|
||||
|
||||
@@ -385,7 +384,7 @@ private:
|
||||
NvPairingManager pairingManager(m_Computer->activeAddress);
|
||||
|
||||
try {
|
||||
NvPairingManager::PairState result = pairingManager.pair(m_Computer->appVersion, m_Pin);
|
||||
NvPairingManager::PairState result = pairingManager.pair(m_Computer->appVersion, m_Pin, m_Computer->serverCert);
|
||||
switch (result)
|
||||
{
|
||||
case NvPairingManager::PairState::PIN_WRONG:
|
||||
@@ -438,7 +437,7 @@ signals:
|
||||
private:
|
||||
void run()
|
||||
{
|
||||
NvHTTP http(m_Computer->activeAddress);
|
||||
NvHTTP http(m_Computer->activeAddress, m_Computer->serverCert);
|
||||
|
||||
try {
|
||||
if (m_Computer->currentGameId != 0) {
|
||||
@@ -538,12 +537,8 @@ signals:
|
||||
void computerStateChanged(NvComputer* computer);
|
||||
|
||||
private:
|
||||
void run()
|
||||
QString fetchServerInfo(NvHTTP& http)
|
||||
{
|
||||
NvHTTP http(m_Address);
|
||||
|
||||
qInfo() << "Processing new PC at" << m_Address << "from" << (m_Mdns ? "mDNS" : "user");
|
||||
|
||||
QString serverInfo;
|
||||
try {
|
||||
// There's a race condition between GameStream servers reporting presence over
|
||||
@@ -564,14 +559,51 @@ private:
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
return serverInfo;
|
||||
} catch (...) {
|
||||
if (!m_Mdns) {
|
||||
emit computerAddCompleted(false);
|
||||
}
|
||||
return QString();
|
||||
}
|
||||
}
|
||||
|
||||
void run()
|
||||
{
|
||||
NvHTTP http(m_Address, QSslCertificate());
|
||||
|
||||
qInfo() << "Processing new PC at" << m_Address << "from" << (m_Mdns ? "mDNS" : "user");
|
||||
|
||||
// Perform initial serverinfo fetch over HTTP since we don't know which cert to use
|
||||
QString serverInfo = fetchServerInfo(http);
|
||||
if (serverInfo.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
NvComputer* newComputer = new NvComputer(m_Address, serverInfo);
|
||||
// Create initial newComputer using HTTP serverinfo with no pinned cert
|
||||
NvComputer* newComputer = new NvComputer(m_Address, serverInfo, QSslCertificate());
|
||||
|
||||
// Check if we have a record of this host UUID to pull the pinned cert
|
||||
NvComputer* existingComputer;
|
||||
{
|
||||
QReadLocker lock(&m_ComputerManager->m_Lock);
|
||||
existingComputer = m_ComputerManager->m_KnownHosts.value(newComputer->uuid);
|
||||
if (existingComputer != nullptr) {
|
||||
http.setServerCert(existingComputer->serverCert);
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch serverinfo again over HTTPS with the pinned cert
|
||||
if (existingComputer != nullptr) {
|
||||
serverInfo = fetchServerInfo(http);
|
||||
if (serverInfo.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Update the polled computer with the HTTPS information
|
||||
NvComputer httpsComputer(m_Address, serverInfo, QSslCertificate());
|
||||
newComputer->update(httpsComputer);
|
||||
}
|
||||
|
||||
// Update addresses depending on the context
|
||||
if (m_Mdns) {
|
||||
@@ -591,46 +623,48 @@ private:
|
||||
newComputer->manualAddress = m_Address;
|
||||
}
|
||||
|
||||
// Check if this PC already exists
|
||||
QWriteLocker lock(&m_ComputerManager->m_Lock);
|
||||
NvComputer* existingComputer = m_ComputerManager->m_KnownHosts[newComputer->uuid];
|
||||
if (existingComputer != nullptr) {
|
||||
// Fold it into the existing PC
|
||||
bool changed = existingComputer->update(*newComputer);
|
||||
delete newComputer;
|
||||
{
|
||||
// Check if this PC already exists
|
||||
QWriteLocker lock(&m_ComputerManager->m_Lock);
|
||||
NvComputer* existingComputer = m_ComputerManager->m_KnownHosts.value(newComputer->uuid);
|
||||
if (existingComputer != nullptr) {
|
||||
// Fold it into the existing PC
|
||||
bool changed = existingComputer->update(*newComputer);
|
||||
delete newComputer;
|
||||
|
||||
// Drop the lock before notifying
|
||||
lock.unlock();
|
||||
// Drop the lock before notifying
|
||||
lock.unlock();
|
||||
|
||||
// For non-mDNS clients, let them know it succeeded
|
||||
if (!m_Mdns) {
|
||||
emit computerAddCompleted(true);
|
||||
// For non-mDNS clients, let them know it succeeded
|
||||
if (!m_Mdns) {
|
||||
emit computerAddCompleted(true);
|
||||
}
|
||||
|
||||
// Tell our client if something changed
|
||||
if (changed) {
|
||||
qInfo() << existingComputer->name << "is now at" << existingComputer->activeAddress;
|
||||
emit computerStateChanged(existingComputer);
|
||||
}
|
||||
}
|
||||
else {
|
||||
// Store this in our active sets
|
||||
m_ComputerManager->m_KnownHosts[newComputer->uuid] = newComputer;
|
||||
|
||||
// Tell our client if something changed
|
||||
if (changed) {
|
||||
qInfo() << existingComputer->name << "is now at" << existingComputer->activeAddress;
|
||||
emit computerStateChanged(existingComputer);
|
||||
// Start polling if enabled (write lock required)
|
||||
m_ComputerManager->startPollingComputer(newComputer);
|
||||
|
||||
// Drop the lock before notifying
|
||||
lock.unlock();
|
||||
|
||||
// For non-mDNS clients, let them know it succeeded
|
||||
if (!m_Mdns) {
|
||||
emit computerAddCompleted(true);
|
||||
}
|
||||
|
||||
// Tell our client about this new PC
|
||||
emit computerStateChanged(newComputer);
|
||||
}
|
||||
}
|
||||
else {
|
||||
// Store this in our active sets
|
||||
m_ComputerManager->m_KnownHosts[newComputer->uuid] = newComputer;
|
||||
|
||||
// Start polling if enabled (write lock required)
|
||||
m_ComputerManager->startPollingComputer(newComputer);
|
||||
|
||||
// Drop the lock before notifying
|
||||
lock.unlock();
|
||||
|
||||
// For non-mDNS clients, let them know it succeeded
|
||||
if (!m_Mdns) {
|
||||
emit computerAddCompleted(true);
|
||||
}
|
||||
|
||||
// Tell our client about this new PC
|
||||
emit computerStateChanged(newComputer);
|
||||
}
|
||||
}
|
||||
|
||||
ComputerManager* m_ComputerManager;
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
#include "computerseeker.h"
|
||||
#include "computermanager.h"
|
||||
#include <QTimer>
|
||||
|
||||
ComputerSeeker::ComputerSeeker(ComputerManager *manager, QString computerName, QObject *parent)
|
||||
: QObject(parent), m_ComputerManager(manager), m_ComputerName(computerName),
|
||||
m_TimeoutTimer(new QTimer(this))
|
||||
{
|
||||
m_TimeoutTimer->setSingleShot(true);
|
||||
connect(m_TimeoutTimer, &QTimer::timeout,
|
||||
this, &ComputerSeeker::onTimeout);
|
||||
connect(m_ComputerManager, &ComputerManager::computerStateChanged,
|
||||
this, &ComputerSeeker::onComputerUpdated);
|
||||
}
|
||||
|
||||
void ComputerSeeker::start(int timeout)
|
||||
{
|
||||
m_TimeoutTimer->start(timeout);
|
||||
// Seek desired computer by both connecting to it directly (this may fail
|
||||
// if m_ComputerName is UUID, or the name that doesn't resolve to an IP
|
||||
// address) and by polling it using mDNS, hopefully one of these methods
|
||||
// would find the host
|
||||
m_ComputerManager->addNewHost(m_ComputerName, false);
|
||||
m_ComputerManager->startPolling();
|
||||
}
|
||||
|
||||
void ComputerSeeker::onComputerUpdated(NvComputer *computer)
|
||||
{
|
||||
if (!m_TimeoutTimer->isActive()) {
|
||||
return;
|
||||
}
|
||||
if (matchComputer(computer) && isOnline(computer)) {
|
||||
m_ComputerManager->stopPollingAsync();
|
||||
m_TimeoutTimer->stop();
|
||||
emit computerFound(computer);
|
||||
}
|
||||
}
|
||||
|
||||
bool ComputerSeeker::matchComputer(NvComputer *computer) const
|
||||
{
|
||||
QString value = m_ComputerName.toLower();
|
||||
return computer->name.toLower() == value ||
|
||||
computer->localAddress.toLower() == value ||
|
||||
computer->remoteAddress.toLower() == value ||
|
||||
computer->manualAddress.toLower() == value ||
|
||||
computer->uuid.toLower() == value;
|
||||
}
|
||||
|
||||
bool ComputerSeeker::isOnline(NvComputer *computer) const
|
||||
{
|
||||
return computer->state == NvComputer::CS_ONLINE;
|
||||
}
|
||||
|
||||
void ComputerSeeker::onTimeout()
|
||||
{
|
||||
m_TimeoutTimer->stop();
|
||||
m_ComputerManager->stopPollingAsync();
|
||||
emit errorTimeout();
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
#pragma once
|
||||
|
||||
#include <QObject>
|
||||
|
||||
class ComputerManager;
|
||||
class NvComputer;
|
||||
class QTimer;
|
||||
|
||||
class ComputerSeeker : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit ComputerSeeker(ComputerManager *manager, QString computerName, QObject *parent = nullptr);
|
||||
|
||||
void start(int timeout);
|
||||
|
||||
signals:
|
||||
void computerFound(NvComputer *computer);
|
||||
void errorTimeout();
|
||||
|
||||
private slots:
|
||||
void onComputerUpdated(NvComputer *computer);
|
||||
void onTimeout();
|
||||
|
||||
private:
|
||||
bool matchComputer(NvComputer *computer) const;
|
||||
bool isOnline(NvComputer *computer) const;
|
||||
|
||||
private:
|
||||
ComputerManager *m_ComputerManager;
|
||||
QString m_ComputerName;
|
||||
QTimer *m_TimeoutTimer;
|
||||
};
|
||||
@@ -10,6 +10,7 @@
|
||||
#define SER_REMOTEADDR "remoteaddress"
|
||||
#define SER_MANUALADDR "manualaddress"
|
||||
#define SER_APPLIST "apps"
|
||||
#define SER_SRVCERT "srvcert"
|
||||
|
||||
#define SER_APPNAME "name"
|
||||
#define SER_APPID "id"
|
||||
@@ -23,6 +24,7 @@ NvComputer::NvComputer(QSettings& settings)
|
||||
this->localAddress = settings.value(SER_LOCALADDR).toString();
|
||||
this->remoteAddress = settings.value(SER_REMOTEADDR).toString();
|
||||
this->manualAddress = settings.value(SER_MANUALADDR).toString();
|
||||
this->serverCert = QSslCertificate(settings.value(SER_SRVCERT).toByteArray());
|
||||
|
||||
int appCount = settings.beginReadArray(SER_APPLIST);
|
||||
for (int i = 0; i < appCount; i++) {
|
||||
@@ -61,6 +63,7 @@ void NvComputer::serialize(QSettings& settings)
|
||||
settings.setValue(SER_LOCALADDR, localAddress);
|
||||
settings.setValue(SER_REMOTEADDR, remoteAddress);
|
||||
settings.setValue(SER_MANUALADDR, manualAddress);
|
||||
settings.setValue(SER_SRVCERT, serverCert.toPem());
|
||||
|
||||
// Avoid deleting an existing applist if we couldn't get one
|
||||
if (!appList.isEmpty()) {
|
||||
@@ -84,8 +87,10 @@ void NvComputer::sortAppList()
|
||||
});
|
||||
}
|
||||
|
||||
NvComputer::NvComputer(QString address, QString serverInfo)
|
||||
NvComputer::NvComputer(QString address, QString serverInfo, QSslCertificate serverCert)
|
||||
{
|
||||
this->serverCert = serverCert;
|
||||
|
||||
this->name = NvHTTP::getXmlString(serverInfo, "hostname");
|
||||
if (this->name.isEmpty()) {
|
||||
this->name = "UNKNOWN";
|
||||
@@ -255,6 +260,13 @@ bool NvComputer::update(NvComputer& that)
|
||||
changed = true; \
|
||||
}
|
||||
|
||||
#define ASSIGN_IF_CHANGED_AND_NONNULL(field) \
|
||||
if (!that.field.isNull() && \
|
||||
this->field != that.field) { \
|
||||
this->field = that.field; \
|
||||
changed = true; \
|
||||
}
|
||||
|
||||
ASSIGN_IF_CHANGED(name);
|
||||
ASSIGN_IF_CHANGED_AND_NONEMPTY(macAddress);
|
||||
ASSIGN_IF_CHANGED_AND_NONEMPTY(localAddress);
|
||||
@@ -269,6 +281,7 @@ bool NvComputer::update(NvComputer& that)
|
||||
ASSIGN_IF_CHANGED(appVersion);
|
||||
ASSIGN_IF_CHANGED(maxLumaPixelsHEVC);
|
||||
ASSIGN_IF_CHANGED(gpuModel);
|
||||
ASSIGN_IF_CHANGED_AND_NONNULL(serverCert);
|
||||
ASSIGN_IF_CHANGED_AND_NONEMPTY(appList);
|
||||
ASSIGN_IF_CHANGED_AND_NONEMPTY(displayModes);
|
||||
return changed;
|
||||
|
||||
@@ -19,7 +19,7 @@ private:
|
||||
bool pendingQuit;
|
||||
|
||||
public:
|
||||
explicit NvComputer(QString address, QString serverInfo);
|
||||
explicit NvComputer(QString address, QString serverInfo, QSslCertificate serverCert);
|
||||
|
||||
explicit NvComputer(QSettings& settings);
|
||||
|
||||
@@ -68,6 +68,7 @@ public:
|
||||
QByteArray macAddress;
|
||||
QString name;
|
||||
QString uuid;
|
||||
QSslCertificate serverCert;
|
||||
QVector<NvApp> appList;
|
||||
|
||||
// Synchronization
|
||||
|
||||
+76
-28
@@ -14,8 +14,9 @@
|
||||
|
||||
#define REQUEST_TIMEOUT_MS 5000
|
||||
|
||||
NvHTTP::NvHTTP(QString address) :
|
||||
m_Address(address)
|
||||
NvHTTP::NvHTTP(QString address, QSslCertificate serverCert) :
|
||||
m_Address(address),
|
||||
m_ServerCert(serverCert)
|
||||
{
|
||||
Q_ASSERT(!address.isEmpty());
|
||||
|
||||
@@ -31,6 +32,11 @@ NvHTTP::NvHTTP(QString address) :
|
||||
m_Nam.setProxy(noProxy);
|
||||
}
|
||||
|
||||
void NvHTTP::setServerCert(QSslCertificate serverCert)
|
||||
{
|
||||
m_ServerCert = serverCert;
|
||||
}
|
||||
|
||||
QVector<int>
|
||||
NvHTTP::parseQuad(QString quad)
|
||||
{
|
||||
@@ -73,36 +79,50 @@ NvHTTP::getServerInfo(NvLogLevel logLevel)
|
||||
{
|
||||
QString serverInfo;
|
||||
|
||||
try
|
||||
// Check if we have a pinned cert for this host yet
|
||||
if (!m_ServerCert.isNull())
|
||||
{
|
||||
// Always try HTTPS first, since it properly reports
|
||||
// pairing status (and a few other attributes).
|
||||
serverInfo = openConnectionToString(m_BaseUrlHttps,
|
||||
"serverinfo",
|
||||
nullptr,
|
||||
true,
|
||||
logLevel);
|
||||
// Throws if the request failed
|
||||
verifyResponseStatus(serverInfo);
|
||||
}
|
||||
catch (const GfeHttpResponseException& e)
|
||||
{
|
||||
if (e.getStatusCode() == 401)
|
||||
try
|
||||
{
|
||||
// Certificate validation error, fallback to HTTP
|
||||
serverInfo = openConnectionToString(m_BaseUrlHttp,
|
||||
// Always try HTTPS first, since it properly reports
|
||||
// pairing status (and a few other attributes).
|
||||
serverInfo = openConnectionToString(m_BaseUrlHttps,
|
||||
"serverinfo",
|
||||
nullptr,
|
||||
true,
|
||||
logLevel);
|
||||
// Throws if the request failed
|
||||
verifyResponseStatus(serverInfo);
|
||||
}
|
||||
else
|
||||
catch (const GfeHttpResponseException& e)
|
||||
{
|
||||
// Rethrow real errors
|
||||
throw e;
|
||||
if (e.getStatusCode() == 401)
|
||||
{
|
||||
// Certificate validation error, fallback to HTTP
|
||||
serverInfo = openConnectionToString(m_BaseUrlHttp,
|
||||
"serverinfo",
|
||||
nullptr,
|
||||
true,
|
||||
logLevel);
|
||||
verifyResponseStatus(serverInfo);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Rethrow real errors
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Only use HTTP prior to pairing
|
||||
serverInfo = openConnectionToString(m_BaseUrlHttp,
|
||||
"serverinfo",
|
||||
nullptr,
|
||||
true,
|
||||
logLevel);
|
||||
verifyResponseStatus(serverInfo);
|
||||
}
|
||||
|
||||
return serverInfo;
|
||||
}
|
||||
@@ -379,8 +399,12 @@ NvHTTP::openConnection(QUrl baseUrl,
|
||||
// Build a URL for the request
|
||||
QUrl url(baseUrl);
|
||||
url.setPath("/" + command);
|
||||
url.setQuery("uniqueid=" + IdentityManager::get()->getUniqueId() +
|
||||
"&uuid=" + QUuid::createUuid().toRfc4122().toHex() +
|
||||
|
||||
// Use a common UID for Moonlight clients to allow them to quit
|
||||
// games for each other (otherwise GFE gets screwed up and it requires
|
||||
// manual intervention to solve).
|
||||
url.setQuery("uniqueid=0123456789ABCDEF&uuid=" +
|
||||
QUuid::createUuid().toRfc4122().toHex() +
|
||||
((arguments != nullptr) ? ("&" + arguments) : ""));
|
||||
|
||||
QNetworkRequest request = QNetworkRequest(url);
|
||||
@@ -390,8 +414,22 @@ NvHTTP::openConnection(QUrl baseUrl,
|
||||
|
||||
QNetworkReply* reply = m_Nam.get(request);
|
||||
|
||||
// Ignore self-signed certificate errors (since GFE uses them)
|
||||
reply->ignoreSslErrors();
|
||||
// Assert that we always have a server cert for HTTPS, since
|
||||
// the request will fail anyway if we do not.
|
||||
Q_ASSERT(!m_ServerCert.isNull() || baseUrl == m_BaseUrlHttp);
|
||||
|
||||
if (!m_ServerCert.isNull()) {
|
||||
// Pin the server certificate received during pairing
|
||||
QList<QSslError> expectedSslErrors;
|
||||
expectedSslErrors.append(QSslError(QSslError::HostNameMismatch, m_ServerCert));
|
||||
expectedSslErrors.append(QSslError(QSslError::SelfSignedCertificate, m_ServerCert));
|
||||
|
||||
// The SecureTransport backend for Qt TLS on macOS throws CertificateUntrusted
|
||||
// instead of SelfSignedCertificate, so we will need to allow that error too.
|
||||
expectedSslErrors.append(QSslError(QSslError::CertificateUntrusted, m_ServerCert));
|
||||
|
||||
reply->ignoreSslErrors(expectedSslErrors);
|
||||
}
|
||||
|
||||
// Run the request with a timeout if requested
|
||||
QEventLoop loop;
|
||||
@@ -424,9 +462,19 @@ NvHTTP::openConnection(QUrl baseUrl,
|
||||
if (logLevel >= NvLogLevel::ERROR) {
|
||||
qWarning() << command << " request failed with error " << reply->error();
|
||||
}
|
||||
QtNetworkReplyException exception(reply->error(), reply->errorString());
|
||||
delete reply;
|
||||
throw exception;
|
||||
|
||||
if (reply->error() == QNetworkReply::SslHandshakeFailedError) {
|
||||
// This will trigger falling back to HTTP for the serverinfo query
|
||||
// then pairing again to get the updated certificate.
|
||||
GfeHttpResponseException exception(401, "Server certificate mismatch");
|
||||
delete reply;
|
||||
throw exception;
|
||||
}
|
||||
else {
|
||||
QtNetworkReplyException exception(reply->error(), reply->errorString());
|
||||
delete reply;
|
||||
throw exception;
|
||||
}
|
||||
}
|
||||
|
||||
return reply;
|
||||
|
||||
@@ -122,7 +122,7 @@ public:
|
||||
VERBOSE
|
||||
};
|
||||
|
||||
explicit NvHTTP(QString address);
|
||||
explicit NvHTTP(QString address, QSslCertificate serverCert);
|
||||
|
||||
static
|
||||
int
|
||||
@@ -152,6 +152,8 @@ public:
|
||||
bool enableTimeout,
|
||||
NvLogLevel logLevel = NvLogLevel::VERBOSE);
|
||||
|
||||
void setServerCert(QSslCertificate serverCert);
|
||||
|
||||
static
|
||||
QVector<int>
|
||||
parseQuad(QString quad);
|
||||
@@ -191,4 +193,5 @@ private:
|
||||
|
||||
QString m_Address;
|
||||
QNetworkAccessManager m_Nam;
|
||||
QSslCertificate m_ServerCert;
|
||||
};
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
#include <openssl/evp.h>
|
||||
|
||||
NvPairingManager::NvPairingManager(QString address) :
|
||||
m_Http(address)
|
||||
m_Http(address, QSslCertificate())
|
||||
{
|
||||
QByteArray cert = IdentityManager::get()->getCertificate();
|
||||
BIO *bio = BIO_new_mem_buf(cert.data(), -1);
|
||||
@@ -161,7 +161,7 @@ NvPairingManager::saltPin(QByteArray salt, QString pin)
|
||||
}
|
||||
|
||||
NvPairingManager::PairState
|
||||
NvPairingManager::pair(QString appVersion, QString pin)
|
||||
NvPairingManager::pair(QString appVersion, QString pin, QSslCertificate& serverCert)
|
||||
{
|
||||
int serverMajorVersion = NvHTTP::parseQuad(appVersion).at(0);
|
||||
qInfo() << "Pairing with server generation:" << serverMajorVersion;
|
||||
@@ -200,14 +200,26 @@ NvPairingManager::pair(QString appVersion, QString pin)
|
||||
return PairState::FAILED;
|
||||
}
|
||||
|
||||
QByteArray serverCert = NvHTTP::getXmlStringFromHex(getCert, "plaincert");
|
||||
if (serverCert == nullptr)
|
||||
QByteArray serverCertStr = NvHTTP::getXmlStringFromHex(getCert, "plaincert");
|
||||
if (serverCertStr == nullptr)
|
||||
{
|
||||
qCritical() << "Server likely already pairing";
|
||||
m_Http.openConnectionToString(m_Http.m_BaseUrlHttp, "unpair", nullptr, true);
|
||||
return PairState::ALREADY_IN_PROGRESS;
|
||||
}
|
||||
|
||||
serverCert = QSslCertificate(serverCertStr);
|
||||
if (serverCert.isNull()) {
|
||||
Q_ASSERT(!serverCert.isNull());
|
||||
|
||||
qCritical() << "Failed to parse plaincert";
|
||||
m_Http.openConnectionToString(m_Http.m_BaseUrlHttp, "unpair", nullptr, true);
|
||||
return PairState::FAILED;
|
||||
}
|
||||
|
||||
// Pin this cert for TLS
|
||||
m_Http.setServerCert(serverCert);
|
||||
|
||||
QByteArray randomChallenge = generateRandomBytes(16);
|
||||
QByteArray encryptedChallenge = encrypt(randomChallenge, &encKey);
|
||||
QString challengeXml = m_Http.openConnectionToString(m_Http.m_BaseUrlHttp,
|
||||
@@ -262,7 +274,7 @@ NvPairingManager::pair(QString appVersion, QString pin)
|
||||
|
||||
if (!verifySignature(serverSecret,
|
||||
serverSignature,
|
||||
serverCert))
|
||||
serverCertStr))
|
||||
{
|
||||
qCritical() << "MITM detected";
|
||||
m_Http.openConnectionToString(m_Http.m_BaseUrlHttp, "unpair", nullptr, true);
|
||||
@@ -271,7 +283,7 @@ NvPairingManager::pair(QString appVersion, QString pin)
|
||||
|
||||
QByteArray expectedResponseData;
|
||||
expectedResponseData.append(randomChallenge);
|
||||
expectedResponseData.append(getSignatureFromPemCert(serverCert));
|
||||
expectedResponseData.append(getSignatureFromPemCert(serverCertStr));
|
||||
expectedResponseData.append(serverSecret);
|
||||
if (QCryptographicHash::hash(expectedResponseData, hashAlgo) != serverResponse)
|
||||
{
|
||||
|
||||
@@ -23,7 +23,7 @@ public:
|
||||
~NvPairingManager();
|
||||
|
||||
PairState
|
||||
pair(QString appVersion, QString pin);
|
||||
pair(QString appVersion, QString pin, QSslCertificate& serverCert);
|
||||
|
||||
private:
|
||||
QByteArray
|
||||
|
||||
@@ -164,6 +164,7 @@ GlobalCommandLineParser::ParseResult GlobalCommandLineParser::parse(const QStrin
|
||||
"Starts Moonlight normally if no arguments are given.\n"
|
||||
"\n"
|
||||
"Available actions:\n"
|
||||
" quit Quit the currently running app\n"
|
||||
" stream Start streaming an app\n"
|
||||
"\n"
|
||||
"See 'moonlight <action> --help' for help of specific action."
|
||||
@@ -179,6 +180,8 @@ GlobalCommandLineParser::ParseResult GlobalCommandLineParser::parse(const QStrin
|
||||
parser.handleHelpAndVersionOptions();
|
||||
parser.handleUnknownOptions();
|
||||
return NormalStartRequested;
|
||||
} else if (action == "quit") {
|
||||
return QuitRequested;
|
||||
} else if (action == "stream") {
|
||||
return StreamRequested;
|
||||
} else {
|
||||
@@ -186,6 +189,48 @@ GlobalCommandLineParser::ParseResult GlobalCommandLineParser::parse(const QStrin
|
||||
}
|
||||
}
|
||||
|
||||
QuitCommandLineParser::QuitCommandLineParser()
|
||||
{
|
||||
}
|
||||
|
||||
QuitCommandLineParser::~QuitCommandLineParser()
|
||||
{
|
||||
}
|
||||
|
||||
void QuitCommandLineParser::parse(const QStringList &args)
|
||||
{
|
||||
CommandLineParser parser;
|
||||
parser.setupCommonOptions();
|
||||
parser.setApplicationDescription(
|
||||
"\n"
|
||||
"Quit the currently running app on the given host."
|
||||
);
|
||||
parser.addPositionalArgument("quit", "quit running app");
|
||||
parser.addPositionalArgument("host", "Host computer name, UUID, or IP address", "<host>");
|
||||
|
||||
if (!parser.parse(args)) {
|
||||
parser.showError(parser.errorText());
|
||||
}
|
||||
|
||||
parser.handleUnknownOptions();
|
||||
|
||||
// This method will not return and terminates the process if --version or
|
||||
// --help is specified
|
||||
parser.handleHelpAndVersionOptions();
|
||||
|
||||
// Verify that host has been provided
|
||||
auto posArgs = parser.positionalArguments();
|
||||
if (posArgs.length() < 2) {
|
||||
parser.showError("Host not provided");
|
||||
}
|
||||
m_Host = parser.positionalArguments().at(1);
|
||||
}
|
||||
|
||||
QString QuitCommandLineParser::getHost() const
|
||||
{
|
||||
return m_Host;
|
||||
}
|
||||
|
||||
StreamCommandLineParser::StreamCommandLineParser()
|
||||
{
|
||||
m_WindowModeMap = {
|
||||
@@ -238,9 +283,11 @@ void StreamCommandLineParser::parse(const QStringList &args, StreamingPreference
|
||||
parser.addChoiceOption("display-mode", "display mode", m_WindowModeMap.keys());
|
||||
parser.addChoiceOption("audio-config", "audio config", m_AudioConfigMap.keys());
|
||||
parser.addToggleOption("multi-controller", "multiple controller support");
|
||||
parser.addToggleOption("quit-after", "quit app after session");
|
||||
parser.addToggleOption("mouse-acceleration", "mouse acceleration");
|
||||
parser.addToggleOption("game-optimization", "game optimizations");
|
||||
parser.addToggleOption("audio-on-host", "audio on host PC");
|
||||
parser.addToggleOption("frame-pacing", "frame pacing");
|
||||
parser.addChoiceOption("video-codec", "video codec", m_VideoCodecMap.keys());
|
||||
parser.addChoiceOption("video-decoder", "video decoder", m_VideoDecoderMap.keys());
|
||||
|
||||
@@ -314,6 +361,9 @@ void StreamCommandLineParser::parse(const QStringList &args, StreamingPreference
|
||||
// Resolve --multi-controller and --no-multi-controller options
|
||||
preferences->multiController = parser.getToggleOptionValue("multi-controller", preferences->multiController);
|
||||
|
||||
// Resolve --quit-after and --no-quit-after options
|
||||
preferences->quitAppAfter = parser.getToggleOptionValue("quit-after", preferences->quitAppAfter);
|
||||
|
||||
// Resolve --mouse-acceleration and --no-mouse-acceleration options
|
||||
preferences->mouseAcceleration = parser.getToggleOptionValue("mouse-acceleration", preferences->mouseAcceleration);
|
||||
|
||||
@@ -323,6 +373,9 @@ void StreamCommandLineParser::parse(const QStringList &args, StreamingPreference
|
||||
// Resolve --audio-on-host and --no-audio-on-host options
|
||||
preferences->playAudioOnHost = parser.getToggleOptionValue("audio-on-host", preferences->playAudioOnHost);
|
||||
|
||||
// Resolve --frame-pacing and --no-frame-pacing options
|
||||
preferences->framePacing = parser.getToggleOptionValue("frame-pacing", preferences->framePacing);
|
||||
|
||||
// Resolve --video-codec option
|
||||
if (parser.isSet("video-codec")) {
|
||||
preferences->videoCodecConfig = mapValue(m_VideoCodecMap, parser.getChoiceOptionValue("video-codec"));
|
||||
@@ -359,4 +412,3 @@ QString StreamCommandLineParser::getAppName() const
|
||||
{
|
||||
return m_AppName;
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ public:
|
||||
enum ParseResult {
|
||||
NormalStartRequested,
|
||||
StreamRequested,
|
||||
QuitRequested,
|
||||
};
|
||||
|
||||
GlobalCommandLineParser();
|
||||
@@ -20,6 +21,20 @@ public:
|
||||
|
||||
};
|
||||
|
||||
class QuitCommandLineParser
|
||||
{
|
||||
public:
|
||||
QuitCommandLineParser();
|
||||
virtual ~QuitCommandLineParser();
|
||||
|
||||
void parse(const QStringList &args);
|
||||
|
||||
QString getHost() const;
|
||||
|
||||
private:
|
||||
QString m_Host;
|
||||
};
|
||||
|
||||
class StreamCommandLineParser
|
||||
{
|
||||
public:
|
||||
|
||||
@@ -0,0 +1,171 @@
|
||||
#include "quitstream.h"
|
||||
|
||||
#include "backend/computermanager.h"
|
||||
#include "backend/computerseeker.h"
|
||||
#include "streaming/session.h"
|
||||
|
||||
#include <QCoreApplication>
|
||||
#include <QTimer>
|
||||
|
||||
#define COMPUTER_SEEK_TIMEOUT 10000
|
||||
|
||||
namespace CliQuitStream
|
||||
{
|
||||
|
||||
enum State {
|
||||
StateInit,
|
||||
StateSeekComputer,
|
||||
StateQuitApp,
|
||||
StateFailure,
|
||||
};
|
||||
|
||||
class Event
|
||||
{
|
||||
public:
|
||||
enum Type {
|
||||
AppQuitCompleted,
|
||||
ComputerFound,
|
||||
ComputerSeekTimedout,
|
||||
Executed,
|
||||
};
|
||||
|
||||
Event(Type type)
|
||||
: type(type), computerManager(nullptr), computer(nullptr) {}
|
||||
|
||||
Type type;
|
||||
ComputerManager *computerManager;
|
||||
NvComputer *computer;
|
||||
QString errorMessage;
|
||||
};
|
||||
|
||||
class LauncherPrivate
|
||||
{
|
||||
Q_DECLARE_PUBLIC(Launcher)
|
||||
|
||||
public:
|
||||
LauncherPrivate(Launcher *q) : q_ptr(q) {}
|
||||
|
||||
void handleEvent(Event event)
|
||||
{
|
||||
Q_Q(Launcher);
|
||||
NvApp app;
|
||||
|
||||
switch (event.type) {
|
||||
// Occurs when CliQuitStreamSegue becomes visible and the UI calls launcher's execute()
|
||||
case Event::Executed:
|
||||
if (m_State == StateInit) {
|
||||
m_State = StateSeekComputer;
|
||||
m_ComputerManager = event.computerManager;
|
||||
q->connect(m_ComputerManager, &ComputerManager::quitAppCompleted,
|
||||
q, &Launcher::onQuitAppCompleted);
|
||||
|
||||
m_ComputerSeeker = new ComputerSeeker(m_ComputerManager, m_ComputerName, q);
|
||||
q->connect(m_ComputerSeeker, &ComputerSeeker::computerFound,
|
||||
q, &Launcher::onComputerFound);
|
||||
q->connect(m_ComputerSeeker, &ComputerSeeker::errorTimeout,
|
||||
q, &Launcher::onComputerSeekTimeout);
|
||||
m_ComputerSeeker->start(COMPUTER_SEEK_TIMEOUT);
|
||||
|
||||
emit q->searchingComputer();
|
||||
}
|
||||
break;
|
||||
// Occurs when computer search timed out
|
||||
case Event::ComputerSeekTimedout:
|
||||
if (m_State == StateSeekComputer) {
|
||||
m_State = StateFailure;
|
||||
emit q->failed(QString("Failed to connect to %1").arg(m_ComputerName));
|
||||
}
|
||||
break;
|
||||
// Occurs when searched computer is found
|
||||
case Event::ComputerFound:
|
||||
if (m_State == StateSeekComputer) {
|
||||
if (event.computer->pairState == NvComputer::PS_PAIRED) {
|
||||
m_State = StateQuitApp;
|
||||
emit q->quittingApp();
|
||||
m_ComputerManager->quitRunningApp(event.computer);
|
||||
} else {
|
||||
m_State = StateFailure;
|
||||
QString msg = QString("Computer %1 has not been paired. "
|
||||
"Please open Moonlight to pair before streaming.")
|
||||
.arg(event.computer->name);
|
||||
emit q->failed(msg);
|
||||
}
|
||||
}
|
||||
break;
|
||||
// Occurs when app quit completed (error message is set if failed)
|
||||
case Event::AppQuitCompleted:
|
||||
if (m_State == StateQuitApp) {
|
||||
if (event.errorMessage.isEmpty()) {
|
||||
QCoreApplication::exit(0);
|
||||
} else {
|
||||
m_State = StateFailure;
|
||||
emit q->failed(QString("Quitting app failed, reason: %1").arg(event.errorMessage));
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Launcher *q_ptr;
|
||||
ComputerManager *m_ComputerManager;
|
||||
QString m_ComputerName;
|
||||
ComputerSeeker *m_ComputerSeeker;
|
||||
State m_State;
|
||||
QTimer *m_TimeoutTimer;
|
||||
};
|
||||
|
||||
Launcher::Launcher(QString computer, QObject *parent)
|
||||
: QObject(parent),
|
||||
m_DPtr(new LauncherPrivate(this))
|
||||
{
|
||||
Q_D(Launcher);
|
||||
d->m_ComputerName = computer;
|
||||
d->m_State = StateInit;
|
||||
d->m_TimeoutTimer = new QTimer(this);
|
||||
d->m_TimeoutTimer->setSingleShot(true);
|
||||
connect(d->m_TimeoutTimer, &QTimer::timeout,
|
||||
this, &Launcher::onComputerSeekTimeout);
|
||||
}
|
||||
|
||||
Launcher::~Launcher()
|
||||
{
|
||||
}
|
||||
|
||||
void Launcher::execute(ComputerManager *manager)
|
||||
{
|
||||
Q_D(Launcher);
|
||||
Event event(Event::Executed);
|
||||
event.computerManager = manager;
|
||||
d->handleEvent(event);
|
||||
}
|
||||
|
||||
bool Launcher::isExecuted() const
|
||||
{
|
||||
Q_D(const Launcher);
|
||||
return d->m_State != StateInit;
|
||||
}
|
||||
|
||||
void Launcher::onComputerFound(NvComputer *computer)
|
||||
{
|
||||
Q_D(Launcher);
|
||||
Event event(Event::ComputerFound);
|
||||
event.computer = computer;
|
||||
d->handleEvent(event);
|
||||
}
|
||||
|
||||
void Launcher::onComputerSeekTimeout()
|
||||
{
|
||||
Q_D(Launcher);
|
||||
Event event(Event::ComputerSeekTimedout);
|
||||
d->handleEvent(event);
|
||||
}
|
||||
|
||||
void Launcher::onQuitAppCompleted(QVariant error)
|
||||
{
|
||||
Q_D(Launcher);
|
||||
Event event(Event::AppQuitCompleted);
|
||||
event.errorMessage = error.toString();
|
||||
d->handleEvent(event);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
#pragma once
|
||||
|
||||
#include <QObject>
|
||||
#include <QVariant>
|
||||
|
||||
class ComputerManager;
|
||||
class NvComputer;
|
||||
|
||||
namespace CliQuitStream
|
||||
{
|
||||
|
||||
class LauncherPrivate;
|
||||
|
||||
class Launcher : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
Q_DECLARE_PRIVATE_D(m_DPtr, Launcher)
|
||||
|
||||
public:
|
||||
explicit Launcher(QString computer, QObject *parent = nullptr);
|
||||
~Launcher();
|
||||
|
||||
Q_INVOKABLE void execute(ComputerManager *manager);
|
||||
Q_INVOKABLE bool isExecuted() const;
|
||||
|
||||
signals:
|
||||
void searchingComputer();
|
||||
void quittingApp();
|
||||
void failed(QString text);
|
||||
|
||||
private slots:
|
||||
void onComputerFound(NvComputer *computer);
|
||||
void onComputerSeekTimeout();
|
||||
void onQuitAppCompleted(QVariant error);
|
||||
|
||||
private:
|
||||
QScopedPointer<LauncherPrivate> m_DPtr;
|
||||
};
|
||||
|
||||
}
|
||||
+83
-47
@@ -1,7 +1,9 @@
|
||||
#include "startstream.h"
|
||||
#include "backend/computermanager.h"
|
||||
#include "backend/computerseeker.h"
|
||||
#include "streaming/session.h"
|
||||
|
||||
#include <QCoreApplication>
|
||||
#include <QTimer>
|
||||
|
||||
#define COMPUTER_SEEK_TIMEOUT 10000
|
||||
@@ -22,6 +24,9 @@ class Event
|
||||
{
|
||||
public:
|
||||
enum Type {
|
||||
AppQuitCompleted,
|
||||
AppQuitRequested,
|
||||
ComputerFound,
|
||||
ComputerUpdated,
|
||||
Executed,
|
||||
Timedout,
|
||||
@@ -33,6 +38,7 @@ public:
|
||||
Type type;
|
||||
ComputerManager *computerManager;
|
||||
NvComputer *computer;
|
||||
QString errorMessage;
|
||||
};
|
||||
|
||||
class LauncherPrivate
|
||||
@@ -49,58 +55,78 @@ public:
|
||||
NvApp app;
|
||||
|
||||
switch (event.type) {
|
||||
// Occurs when CliStartStreamSegue becomes visible and the UI calls launcher's execute()
|
||||
case Event::Executed:
|
||||
if (m_State == StateInit) {
|
||||
m_State = StateSeekComputer;
|
||||
m_TimeoutTimer->start(COMPUTER_SEEK_TIMEOUT);
|
||||
m_ComputerManager = event.computerManager;
|
||||
|
||||
m_ComputerSeeker = new ComputerSeeker(m_ComputerManager, m_ComputerName, q);
|
||||
q->connect(m_ComputerSeeker, &ComputerSeeker::computerFound,
|
||||
q, &Launcher::onComputerFound);
|
||||
q->connect(m_ComputerSeeker, &ComputerSeeker::errorTimeout,
|
||||
q, &Launcher::onTimeout);
|
||||
m_ComputerSeeker->start(COMPUTER_SEEK_TIMEOUT);
|
||||
|
||||
q->connect(m_ComputerManager, &ComputerManager::computerStateChanged,
|
||||
q, &Launcher::onComputerUpdated);
|
||||
// Seek desired computer by both connecting to it directly (this may fail
|
||||
// if m_ComputerName is UUID, or the name that doesn't resolve to an IP
|
||||
// address) and by polling it using mDNS, hopefully one of these methods
|
||||
// would find the host
|
||||
m_ComputerManager->addNewHost(m_ComputerName, false);
|
||||
m_ComputerManager->startPolling();
|
||||
q->connect(m_ComputerManager, &ComputerManager::quitAppCompleted,
|
||||
q, &Launcher::onQuitAppCompleted);
|
||||
|
||||
emit q->searchingComputer();
|
||||
}
|
||||
break;
|
||||
case Event::ComputerUpdated:
|
||||
// Occurs when searched computer is found
|
||||
case Event::ComputerFound:
|
||||
if (m_State == StateSeekComputer) {
|
||||
if (matchComputer(event.computer) && isOnline(event.computer)) {
|
||||
if (isPaired(event.computer)) {
|
||||
m_State = StateSeekApp;
|
||||
m_TimeoutTimer->start(APP_SEEK_TIMEOUT);
|
||||
m_Computer = event.computer;
|
||||
m_ComputerManager->stopPollingAsync();
|
||||
emit q->searchingApp();
|
||||
} else {
|
||||
m_State = StateFailure;
|
||||
QString msg = QString("Computer %1 has not been paired. "
|
||||
"Please open Moonlight to pair before streaming.")
|
||||
.arg(event.computer->name);
|
||||
emit q->failed(msg);
|
||||
}
|
||||
if (event.computer->pairState == NvComputer::PS_PAIRED) {
|
||||
m_State = StateSeekApp;
|
||||
m_Computer = event.computer;
|
||||
m_TimeoutTimer->start(APP_SEEK_TIMEOUT);
|
||||
emit q->searchingApp();
|
||||
} else {
|
||||
m_State = StateFailure;
|
||||
QString msg = QString("Computer %1 has not been paired. "
|
||||
"Please open Moonlight to pair before streaming.")
|
||||
.arg(event.computer->name);
|
||||
emit q->failed(msg);
|
||||
}
|
||||
}
|
||||
break;
|
||||
// Occurs when a computer is updated
|
||||
case Event::ComputerUpdated:
|
||||
if (m_State == StateSeekApp) {
|
||||
int index = getAppIndex();
|
||||
if (-1 != index) {
|
||||
app = m_Computer->appList[index];
|
||||
m_TimeoutTimer->stop();
|
||||
if (isNotStreaming() || isStreamingApp(app)) {
|
||||
m_State = StateStartSession;
|
||||
m_TimeoutTimer->stop();
|
||||
session = new Session(m_Computer, app, m_Preferences);
|
||||
emit q->sessionCreated(app.name, session);
|
||||
} else {
|
||||
m_State = StateFailure;
|
||||
QString msg = QString("%1 is already running. Please quit %1 to stream %2.")
|
||||
.arg(getCurrentAppName(), app.name);
|
||||
emit q->failed(msg);
|
||||
emit q->appQuitRequired(getCurrentAppName());
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
// Occurs when there was another app running on computer and user accepted quit
|
||||
// confirmation dialog
|
||||
case Event::AppQuitRequested:
|
||||
if (m_State == StateSeekApp) {
|
||||
m_ComputerManager->quitRunningApp(m_Computer);
|
||||
}
|
||||
break;
|
||||
// Occurs when the previous app quit has been completed, handles quitting errors if any
|
||||
// happended. ComputerUpdated event's handler handles session start when previous app has
|
||||
// quit.
|
||||
case Event::AppQuitCompleted:
|
||||
if (m_State == StateSeekApp && !event.errorMessage.isEmpty()) {
|
||||
m_State = StateFailure;
|
||||
emit q->failed(QString("Quitting app failed, reason: %1").arg(event.errorMessage));
|
||||
}
|
||||
break;
|
||||
// Occurs when computer or app search timed out
|
||||
case Event::Timedout:
|
||||
if (m_State == StateSeekComputer) {
|
||||
m_State = StateFailure;
|
||||
@@ -114,26 +140,6 @@ public:
|
||||
}
|
||||
}
|
||||
|
||||
bool matchComputer(NvComputer *computer) const
|
||||
{
|
||||
QString value = m_ComputerName.toLower();
|
||||
return computer->name.toLower() == value ||
|
||||
computer->localAddress.toLower() == value ||
|
||||
computer->remoteAddress.toLower() == value ||
|
||||
computer->manualAddress.toLower() == value ||
|
||||
computer->uuid.toLower() == value;
|
||||
}
|
||||
|
||||
bool isOnline(NvComputer *computer) const
|
||||
{
|
||||
return computer->state == NvComputer::CS_ONLINE;
|
||||
}
|
||||
|
||||
bool isPaired(NvComputer *computer) const
|
||||
{
|
||||
return computer->pairState == NvComputer::PS_PAIRED;
|
||||
}
|
||||
|
||||
int getAppIndex() const
|
||||
{
|
||||
for (int i = 0; i < m_Computer->appList.length(); i++) {
|
||||
@@ -169,6 +175,7 @@ public:
|
||||
QString m_AppName;
|
||||
StreamingPreferences *m_Preferences;
|
||||
ComputerManager *m_ComputerManager;
|
||||
ComputerSeeker *m_ComputerSeeker;
|
||||
NvComputer *m_Computer;
|
||||
State m_State;
|
||||
QTimer *m_TimeoutTimer;
|
||||
@@ -202,6 +209,27 @@ void Launcher::execute(ComputerManager *manager)
|
||||
d->handleEvent(event);
|
||||
}
|
||||
|
||||
void Launcher::quitRunningApp()
|
||||
{
|
||||
Q_D(Launcher);
|
||||
Event event(Event::AppQuitRequested);
|
||||
d->handleEvent(event);
|
||||
}
|
||||
|
||||
bool Launcher::isExecuted() const
|
||||
{
|
||||
Q_D(const Launcher);
|
||||
return d->m_State != StateInit;
|
||||
}
|
||||
|
||||
void Launcher::onComputerFound(NvComputer *computer)
|
||||
{
|
||||
Q_D(Launcher);
|
||||
Event event(Event::ComputerFound);
|
||||
event.computer = computer;
|
||||
d->handleEvent(event);
|
||||
}
|
||||
|
||||
void Launcher::onComputerUpdated(NvComputer *computer)
|
||||
{
|
||||
Q_D(Launcher);
|
||||
@@ -217,4 +245,12 @@ void Launcher::onTimeout()
|
||||
d->handleEvent(event);
|
||||
}
|
||||
|
||||
void Launcher::onQuitAppCompleted(QVariant error)
|
||||
{
|
||||
Q_D(Launcher);
|
||||
Event event(Event::AppQuitCompleted);
|
||||
event.errorMessage = error.toString();
|
||||
d->handleEvent(event);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include <QObject>
|
||||
#include <QVariant>
|
||||
|
||||
class ComputerManager;
|
||||
class NvComputer;
|
||||
@@ -24,16 +25,21 @@ public:
|
||||
QObject *parent = nullptr);
|
||||
~Launcher();
|
||||
Q_INVOKABLE void execute(ComputerManager *manager);
|
||||
Q_INVOKABLE void quitRunningApp();
|
||||
Q_INVOKABLE bool isExecuted() const;
|
||||
|
||||
signals:
|
||||
void searchingComputer();
|
||||
void searchingApp();
|
||||
void sessionCreated(QString appName, Session *session);
|
||||
void failed(QString text);
|
||||
void appQuitRequired(QString appName);
|
||||
|
||||
private slots:
|
||||
void onComputerFound(NvComputer *computer);
|
||||
void onComputerUpdated(NvComputer *computer);
|
||||
void onTimeout();
|
||||
void onQuitAppCompleted(QVariant error);
|
||||
|
||||
private:
|
||||
QScopedPointer<LauncherPrivate> m_DPtr;
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<metadata_license>CC0-1.0</metadata_license>
|
||||
<project_license>GPL-3.0+</project_license>
|
||||
<name>Moonlight</name>
|
||||
<summary>Stream games from your NVIDIA GameStream-enabled PC</summary>
|
||||
<summary>Play games remotely from your NVIDIA GameStream-enabled PC</summary>
|
||||
|
||||
<description>
|
||||
<p>Moonlight can stream games and other applications from a PC with an NVIDIA GeForce GTX 600-series or higher GPU and GeForce Experience installed.</p>
|
||||
@@ -19,20 +19,54 @@
|
||||
</ul>
|
||||
</description>
|
||||
|
||||
<url type="homepage">https://moonlight-stream.com</url>
|
||||
<url type="homepage">https://moonlight-stream.org</url>
|
||||
<url type="bugtracker">https://github.com/moonlight-stream/moonlight-qt/issues</url>
|
||||
<url type="help">https://github.com/moonlight-stream/moonlight-docs/wiki/Setup-Guide</url>
|
||||
|
||||
<screenshots>
|
||||
<screenshot type="default">
|
||||
<image>https://moonlight-stream.com/appstream_resources/moonlight_screenshot_1.png</image>
|
||||
<image>https://moonlight-stream.org/appstream_resources/moonlight_screenshot_1.png</image>
|
||||
</screenshot>
|
||||
<screenshot>
|
||||
<image>https://moonlight-stream.com/appstream_resources/moonlight_screenshot_2.png</image>
|
||||
<image>https://moonlight-stream.org/appstream_resources/moonlight_screenshot_2.png</image>
|
||||
</screenshot>
|
||||
</screenshots>
|
||||
|
||||
<releases>
|
||||
<release version="0.8.0" date="2018-12-25">
|
||||
<description>
|
||||
<p>New features:</p>
|
||||
<ul>
|
||||
<li>Improved security of pairing - After this update, you must re-pair with your PC!</li>
|
||||
<li>Enabled quitting games started by other Moonlight clients also running this update</li>
|
||||
</ul>
|
||||
<p>Bugfixes:</p>
|
||||
<ul>
|
||||
<li>Fixed the video bitrate slider not working above certain values on GFE 3.16</li>
|
||||
<li>Fixed broken Bluetooth audio with PulseAudio</li>
|
||||
<li>Reduced audio latency</li>
|
||||
<li>Fixed some non-gamepad devices triggering the unmapped gamepad dialog box</li>
|
||||
<li>Minor UI tweaks</li>
|
||||
</ul>
|
||||
</description>
|
||||
</release>
|
||||
<release version="0.7.0" date="2018-12-05">
|
||||
<description>
|
||||
<p>New features:</p>
|
||||
<ul>
|
||||
<li>Added option to quit the app after stopping the stream</li>
|
||||
<li>Added CLI option to quit the running app</li>
|
||||
</ul>
|
||||
<p>Bugfixes:</p>
|
||||
<ul>
|
||||
<li>Switched to libsoundio backend to address audio glitches and reduce latency</li>
|
||||
<li>Fixed CLI version output</li>
|
||||
<li>Fixed streaming error messages not showing up when launching from CLI</li>
|
||||
<li>Updated included SDL gamepad mappings</li>
|
||||
<li>Various minor UI tweaks</li>
|
||||
</ul>
|
||||
</description>
|
||||
</release>
|
||||
<release version="0.6.4" date="2018-11-20">
|
||||
<description>
|
||||
<p>New features:</p>
|
||||
@@ -214,7 +248,7 @@
|
||||
</categories>
|
||||
|
||||
<developer_name>Moonlight Game Streaming Team</developer_name>
|
||||
<update_contact>cameron@moonlight-stream.com</update_contact>
|
||||
<update_contact>cameron@moonlight-stream.org</update_contact>
|
||||
|
||||
<launchable type="desktop-id">com.moonlight_stream.Moonlight.desktop</launchable>
|
||||
|
||||
|
||||
+1
-2
@@ -89,10 +89,9 @@ GridView {
|
||||
}
|
||||
}
|
||||
|
||||
Text {
|
||||
Label {
|
||||
id: appNameText
|
||||
text: model.name
|
||||
color: "white"
|
||||
width: parent.width
|
||||
height: 125
|
||||
anchors.top: appIcon.bottom
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
import QtQuick 2.0
|
||||
import QtQuick.Controls 2.2
|
||||
import QtQuick.Dialogs 1.2
|
||||
|
||||
import ComputerManager 1.0
|
||||
import Session 1.0
|
||||
|
||||
Item {
|
||||
anchors.fill: parent
|
||||
|
||||
function onSearchingComputer() {
|
||||
stageLabel.text = "Establishing connection to PC..."
|
||||
}
|
||||
|
||||
function onQuittingApp() {
|
||||
stageLabel.text = "Quitting app..."
|
||||
}
|
||||
|
||||
function onFailure(message) {
|
||||
errorDialog.text = message
|
||||
errorDialog.open()
|
||||
}
|
||||
|
||||
// The StackView will trigger a visibility change when
|
||||
// we're pushed onto it, causing our onVisibleChanged
|
||||
// routine to run, but only if we start as invisible
|
||||
visible: false
|
||||
|
||||
onVisibleChanged: {
|
||||
if (visible && !launcher.isExecuted()) {
|
||||
toolBar.visible = false
|
||||
launcher.searchingComputer.connect(onSearchingComputer)
|
||||
launcher.quittingApp.connect(onQuittingApp)
|
||||
launcher.failed.connect(onFailure)
|
||||
launcher.execute(ComputerManager)
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
anchors.centerIn: parent
|
||||
spacing: 5
|
||||
|
||||
BusyIndicator {
|
||||
id: stageSpinner
|
||||
}
|
||||
|
||||
Label {
|
||||
id: stageLabel
|
||||
height: stageSpinner.height
|
||||
text: stageText
|
||||
font.pointSize: 20
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
|
||||
wrapMode: Text.Wrap
|
||||
}
|
||||
}
|
||||
|
||||
MessageDialog {
|
||||
id: errorDialog
|
||||
modality:Qt.WindowModal
|
||||
icon: StandardIcon.Critical
|
||||
standardButtons: StandardButton.Ok
|
||||
|
||||
onVisibleChanged: {
|
||||
if (!visible) {
|
||||
Qt.quit()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
import QtQml 2.2
|
||||
import QtQuick 2.0
|
||||
import QtQuick.Controls 2.2
|
||||
import QtQuick.Dialogs 1.2
|
||||
@@ -30,13 +31,19 @@ Item {
|
||||
errorDialog.open()
|
||||
}
|
||||
|
||||
function onAppQuitRequired(appName) {
|
||||
quitAppDialog.appName = appName
|
||||
quitAppDialog.open()
|
||||
}
|
||||
|
||||
onVisibleChanged: {
|
||||
if (visible) {
|
||||
if (visible && !launcher.isExecuted()) {
|
||||
toolBar.visible = false
|
||||
launcher.searchingComputer.connect(onSearchingComputer)
|
||||
launcher.searchingApp.connect(onSearchingApp)
|
||||
launcher.sessionCreated.connect(onSessionCreated)
|
||||
launcher.failed.connect(onLaunchFailed)
|
||||
launcher.appQuitRequired.connect(onAppQuitRequired)
|
||||
launcher.execute(ComputerManager)
|
||||
}
|
||||
}
|
||||
@@ -56,7 +63,6 @@ Item {
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
|
||||
wrapMode: Text.Wrap
|
||||
color: "white"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,4 +81,44 @@ Item {
|
||||
}
|
||||
}
|
||||
|
||||
MessageDialog {
|
||||
id: quitAppDialog
|
||||
modality:Qt.WindowModal
|
||||
text:"Are you sure you want to quit " + appName +"? Any unsaved progress will be lost."
|
||||
standardButtons: StandardButton.Yes | StandardButton.No
|
||||
property string appName : ""
|
||||
|
||||
function quitApp() {
|
||||
var component = Qt.createComponent("QuitSegue.qml")
|
||||
var params = {"appName": appName}
|
||||
stackView.push(component.createObject(stackView, params))
|
||||
// Trigger the quit after pushing the quit segue on screen
|
||||
launcher.quitRunningApp()
|
||||
}
|
||||
|
||||
onYes: quitApp()
|
||||
|
||||
// For keyboard/gamepad navigation
|
||||
onAccepted: quitApp()
|
||||
|
||||
// Exit process if app quit is rejected (reacts also to closing of the
|
||||
// dialog from title bar's close button).
|
||||
// Note: this depends on undocumented behavior of visibleChanged()
|
||||
// signal being emitted before yes() or accepted() has been emitted.
|
||||
onVisibleChanged: {
|
||||
if (!visible) {
|
||||
quitTimer.start()
|
||||
}
|
||||
}
|
||||
Component.onCompleted: {
|
||||
yes.connect(quitTimer.stop)
|
||||
accepted.connect(quitTimer.stop)
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: quitTimer
|
||||
interval: 100
|
||||
onTriggered: Qt.quit()
|
||||
}
|
||||
}
|
||||
|
||||
+1
-2
@@ -124,10 +124,9 @@ GridView {
|
||||
visible: !model.addPc && model.statusUnknown
|
||||
}
|
||||
|
||||
Text {
|
||||
Label {
|
||||
id: pcNameText
|
||||
text: model.name
|
||||
color: "white"
|
||||
|
||||
width: parent.width
|
||||
anchors.top: pcIcon.bottom
|
||||
|
||||
@@ -65,7 +65,6 @@ Item {
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
|
||||
wrapMode: Text.Wrap
|
||||
color: "white"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+53
-24
@@ -72,8 +72,7 @@ Flickable {
|
||||
id: resFPStitle
|
||||
text: qsTr("Resolution and FPS")
|
||||
font.pointSize: 12
|
||||
wrapMode: Text.Wrap
|
||||
color: "white"
|
||||
wrapMode: Text.Wrap
|
||||
}
|
||||
|
||||
Label {
|
||||
@@ -82,7 +81,6 @@ Flickable {
|
||||
text: qsTr("Setting values too high for your PC or network connection may cause lag, stuttering, or errors.")
|
||||
font.pointSize: 9
|
||||
wrapMode: Text.Wrap
|
||||
color: "white"
|
||||
}
|
||||
|
||||
Row {
|
||||
@@ -297,16 +295,14 @@ Flickable {
|
||||
text: qsTr("Video bitrate: ")
|
||||
font.pointSize: 12
|
||||
wrapMode: Text.Wrap
|
||||
color: "white"
|
||||
}
|
||||
|
||||
Label {
|
||||
width: parent.width
|
||||
id: bitrateDesc
|
||||
text: qsTr("Lower bitrate to reduce lag and stuttering. Raise bitrate to increase image quality.")
|
||||
text: qsTr("Lower the bitrate on slower connections. Raise the bitrate to increase image quality.")
|
||||
font.pointSize: 9
|
||||
wrapMode: Text.Wrap
|
||||
color: "white"
|
||||
}
|
||||
|
||||
Slider {
|
||||
@@ -326,6 +322,11 @@ Flickable {
|
||||
bitrateTitle.text = "Video bitrate: " + (value / 1000.0) + " Mbps"
|
||||
prefs.bitrateKbps = value
|
||||
}
|
||||
|
||||
ToolTip.delay: 1000
|
||||
ToolTip.timeout: 5000
|
||||
ToolTip.visible: hovered
|
||||
ToolTip.text: "This slider controls video bitrate only. Audio data will add an additional 1-2 Mbps on top of this."
|
||||
}
|
||||
|
||||
Label {
|
||||
@@ -334,7 +335,6 @@ Flickable {
|
||||
text: qsTr("Display mode")
|
||||
font.pointSize: 12
|
||||
wrapMode: Text.Wrap
|
||||
color: "white"
|
||||
}
|
||||
|
||||
AutoResizingComboBox {
|
||||
@@ -383,7 +383,7 @@ Flickable {
|
||||
CheckBox {
|
||||
id: vsyncCheck
|
||||
hoverEnabled: true
|
||||
text: "<font color=\"white\">Enable V-Sync</font>"
|
||||
text: "V-Sync"
|
||||
font.pointSize: 12
|
||||
checked: prefs.enableVsync
|
||||
onCheckedChanged: {
|
||||
@@ -395,6 +395,22 @@ Flickable {
|
||||
ToolTip.visible: hovered
|
||||
ToolTip.text: "Disabling V-Sync allows sub-frame rendering latency, but it can display visible tearing"
|
||||
}
|
||||
|
||||
CheckBox {
|
||||
id: framePacingCheck
|
||||
hoverEnabled: true
|
||||
text: "Frame pacing"
|
||||
font.pointSize: 12
|
||||
enabled: prefs.enableVsync
|
||||
checked: prefs.enableVsync && prefs.framePacing
|
||||
onCheckedChanged: {
|
||||
prefs.framePacing = checked
|
||||
}
|
||||
ToolTip.delay: 1000
|
||||
ToolTip.timeout: 5000
|
||||
ToolTip.visible: hovered
|
||||
ToolTip.text: "Frame pacing reduces micro-stutter by delaying frames that come in too early"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -416,7 +432,6 @@ Flickable {
|
||||
text: qsTr("Audio configuration")
|
||||
font.pointSize: 12
|
||||
wrapMode: Text.Wrap
|
||||
color: "white"
|
||||
}
|
||||
|
||||
AutoResizingComboBox {
|
||||
@@ -468,12 +483,12 @@ Flickable {
|
||||
spacing: 5
|
||||
|
||||
CheckBox {
|
||||
id: startWindowedCheck
|
||||
text: "<font color=\"white\">Start Moonlight in windowed mode</font>"
|
||||
id: startMaximizedCheck
|
||||
text: "Maximize Moonlight window on startup"
|
||||
font.pointSize: 12
|
||||
checked: prefs.startWindowed
|
||||
checked: !prefs.startWindowed
|
||||
onCheckedChanged: {
|
||||
prefs.startWindowed = checked
|
||||
prefs.startWindowed = !checked
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -498,19 +513,25 @@ Flickable {
|
||||
spacing: 5
|
||||
|
||||
CheckBox {
|
||||
id: multiControllerCheck
|
||||
text: "<font color=\"white\">Multiple controller support</font>"
|
||||
id: singleControllerCheck
|
||||
text: "Force gamepad #1 always present"
|
||||
font.pointSize: 12
|
||||
checked: prefs.multiController
|
||||
checked: !prefs.multiController
|
||||
onCheckedChanged: {
|
||||
prefs.multiController = checked
|
||||
prefs.multiController = !checked
|
||||
}
|
||||
|
||||
ToolTip.delay: 1000
|
||||
ToolTip.timeout: 5000
|
||||
ToolTip.visible: hovered
|
||||
ToolTip.text: "Forces a single gamepad to always stay connected to the host, even if no gamepads are actually connected to this PC.\n" +
|
||||
"Only enable this option when streaming a game that doesn't support gamepads being connected after startup."
|
||||
}
|
||||
|
||||
CheckBox {
|
||||
id: rawInputCheck
|
||||
hoverEnabled: true
|
||||
text: "<font color=\"white\">Raw mouse input</font>"
|
||||
text: "Raw mouse input"
|
||||
font.pointSize: 12
|
||||
checked: !prefs.mouseAcceleration
|
||||
onCheckedChanged: {
|
||||
@@ -538,7 +559,7 @@ Flickable {
|
||||
|
||||
CheckBox {
|
||||
id: optimizeGameSettingsCheck
|
||||
text: "<font color=\"white\">Optimize game settings</font>"
|
||||
text: "Optimize game settings"
|
||||
font.pointSize: 12
|
||||
checked: prefs.gameOptimizations
|
||||
onCheckedChanged: {
|
||||
@@ -548,7 +569,7 @@ Flickable {
|
||||
|
||||
CheckBox {
|
||||
id: audioPcCheck
|
||||
text: "<font color=\"white\">Play audio on host PC</font>"
|
||||
text: "Play audio on host PC"
|
||||
font.pointSize: 12
|
||||
checked: prefs.playAudioOnHost
|
||||
onCheckedChanged: {
|
||||
@@ -575,7 +596,6 @@ Flickable {
|
||||
text: qsTr("Video decoder")
|
||||
font.pointSize: 12
|
||||
wrapMode: Text.Wrap
|
||||
color: "white"
|
||||
}
|
||||
|
||||
AutoResizingComboBox {
|
||||
@@ -622,7 +642,6 @@ Flickable {
|
||||
text: qsTr("Video codec")
|
||||
font.pointSize: 12
|
||||
wrapMode: Text.Wrap
|
||||
color: "white"
|
||||
}
|
||||
|
||||
AutoResizingComboBox {
|
||||
@@ -672,7 +691,7 @@ Flickable {
|
||||
|
||||
CheckBox {
|
||||
id: unlockUnsupportedFps
|
||||
text: "<font color=\"white\">Unlock unsupported FPS options</font>"
|
||||
text: "Unlock unsupported FPS options"
|
||||
font.pointSize: 12
|
||||
checked: prefs.unsupportedFps
|
||||
onCheckedChanged: {
|
||||
@@ -686,7 +705,7 @@ Flickable {
|
||||
|
||||
CheckBox {
|
||||
id: enableMdns
|
||||
text: "<font color=\"white\">Automatically find PCs on the local network (Recommended)</font>"
|
||||
text: "Automatically find PCs on the local network (Recommended)"
|
||||
font.pointSize: 12
|
||||
checked: prefs.enableMdns
|
||||
onCheckedChanged: {
|
||||
@@ -703,6 +722,16 @@ Flickable {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CheckBox {
|
||||
id: quitAppAfter
|
||||
text: "Quit app after quitting session"
|
||||
font.pointSize: 12
|
||||
checked: prefs.quitAppAfter
|
||||
onCheckedChanged: {
|
||||
prefs.quitAppAfter = checked
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+53
-21
@@ -3,6 +3,7 @@ import QtQuick.Controls 2.2
|
||||
import QtQuick.Dialogs 1.2
|
||||
import QtQuick.Window 2.2
|
||||
|
||||
import ComputerManager 1.0
|
||||
import SdlGamepadKeyNavigation 1.0
|
||||
import Session 1.0
|
||||
|
||||
@@ -59,6 +60,44 @@ Item {
|
||||
toast.visible = true
|
||||
}
|
||||
|
||||
function quitStarting()
|
||||
{
|
||||
// Avoid the push transition animation
|
||||
var component = Qt.createComponent("QuitSegue.qml")
|
||||
stackView.replace(stackView.currentItem, component.createObject(stackView, {"appName": appName}), StackView.Immediate)
|
||||
|
||||
// Show the Qt window again to show quit segue
|
||||
window.visible = true
|
||||
}
|
||||
|
||||
function sessionFinished()
|
||||
{
|
||||
if (quitAfter) {
|
||||
if (errorDialog.text) {
|
||||
// Quit when the error dialog is acknowledged
|
||||
errorDialog.open()
|
||||
}
|
||||
else {
|
||||
// Quit immediately
|
||||
Qt.quit()
|
||||
}
|
||||
} else {
|
||||
// Exit this view
|
||||
stackView.pop()
|
||||
|
||||
// Show the Qt window again after streaming
|
||||
window.visible = true
|
||||
|
||||
// Display any launch errors. We do this after
|
||||
// the Qt UI is visible again to prevent losing
|
||||
// focus on the dialog which would impact gamepad
|
||||
// users.
|
||||
if (errorDialog.text) {
|
||||
errorDialog.open()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// It's important that we don't call enable() here
|
||||
// or it may interfere with the Session instance
|
||||
// getting notified of initial connected gamepads.
|
||||
@@ -85,29 +124,13 @@ Item {
|
||||
session.connectionStarted.connect(connectionStarted)
|
||||
session.displayLaunchError.connect(displayLaunchError)
|
||||
session.displayLaunchWarning.connect(displayLaunchWarning)
|
||||
session.quitStarting.connect(quitStarting)
|
||||
session.sessionFinished.connect(sessionFinished)
|
||||
|
||||
// Run the streaming session to completion
|
||||
session.exec(Screen.virtualX, Screen.virtualY)
|
||||
|
||||
if (quitAfter) {
|
||||
Qt.quit()
|
||||
} else {
|
||||
// Show the Qt window again after streaming
|
||||
window.visible = true
|
||||
|
||||
// Exit this view
|
||||
stackView.pop()
|
||||
|
||||
// Display any launch errors. We do this after
|
||||
// the Qt UI is visible again to prevent losing
|
||||
// focus on the dialog which would impact gamepad
|
||||
// users.
|
||||
if (errorDialog.text) {
|
||||
errorDialog.open()
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
else if (!quitAfter) {
|
||||
// Show the toolbar again when we become hidden
|
||||
toolBar.visible = true
|
||||
}
|
||||
@@ -129,7 +152,6 @@ Item {
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
|
||||
wrapMode: Text.Wrap
|
||||
color: "white"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -142,7 +164,6 @@ Item {
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
|
||||
wrapMode: Text.Wrap
|
||||
color: "white"
|
||||
}
|
||||
|
||||
MessageDialog {
|
||||
@@ -150,8 +171,19 @@ Item {
|
||||
modality:Qt.WindowModal
|
||||
icon: StandardIcon.Critical
|
||||
standardButtons: StandardButton.Ok | StandardButton.Help
|
||||
|
||||
onAccepted: {
|
||||
if (quitAfter) {
|
||||
Qt.quit()
|
||||
}
|
||||
}
|
||||
|
||||
onHelp: {
|
||||
Qt.openUrlExternally("https://github.com/moonlight-stream/moonlight-docs/wiki/Troubleshooting");
|
||||
|
||||
if (quitAfter) {
|
||||
Qt.quit()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
#include "antihookingprotection.h"
|
||||
#endif
|
||||
|
||||
#include "cli/quitstream.h"
|
||||
#include "cli/startstream.h"
|
||||
#include "cli/commandlineparser.h"
|
||||
#include "path.h"
|
||||
@@ -135,6 +136,14 @@ void qtLogToDiskHandler(QtMsgType type, const QMessageLogContext&, const QString
|
||||
break;
|
||||
}
|
||||
|
||||
// HACK: Avoid printing this internal Qt warning which seems to print every time we
|
||||
// start our event loop for the QNetworkReply. I have found this warning in many
|
||||
// unrelated bug reports, but none actually tackle this warning itself. It seems to be
|
||||
// new in Qt 5.12.
|
||||
if (msg.startsWith("QNetworkReplyHttpImplPrivate::_q_startOperation was called more than once")) {
|
||||
return;
|
||||
}
|
||||
|
||||
QTime logTime = QTime::fromMSecsSinceStartOfDay(s_LoggerTime.elapsed());
|
||||
QString txt = QString("%1 - Qt %2: %3\n").arg(logTime.toString()).arg(typeTxt).arg(msg);
|
||||
|
||||
@@ -222,6 +231,9 @@ LONG WINAPI UnhandledExceptionHandler(struct _EXCEPTION_POINTERS *ExceptionInfo)
|
||||
|
||||
int main(int argc, char *argv[])
|
||||
{
|
||||
// Set the app version for the QCommandLineParser's showVersion() command
|
||||
QCoreApplication::setApplicationVersion(VERSION_STR);
|
||||
|
||||
// Set these here to allow us to use the default QSettings constructor.
|
||||
// These also ensure that our cache directory is named correctly. As such,
|
||||
// it is critical that these be called before Path::initialize().
|
||||
@@ -342,6 +354,15 @@ int main(int argc, char *argv[])
|
||||
engine.rootContext()->setContextProperty("launcher", launcher);
|
||||
break;
|
||||
}
|
||||
case GlobalCommandLineParser::QuitRequested:
|
||||
{
|
||||
initialView = "CliQuitStreamSegue.qml";
|
||||
QuitCommandLineParser quitParser;
|
||||
quitParser.parse(app.arguments());
|
||||
auto launcher = new CliQuitStream::Launcher(quitParser.getHost(), &app);
|
||||
engine.rootContext()->setContextProperty("launcher", launcher);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
engine.rootContext()->setContextProperty("initialView", initialView);
|
||||
@@ -369,5 +390,9 @@ int main(int argc, char *argv[])
|
||||
|
||||
int err = app.exec();
|
||||
|
||||
// Give worker tasks time to properly exit. Fixes PendingQuitTask
|
||||
// sometimes freezing and blocking process exit.
|
||||
QThreadPool::globalInstance()->waitForDone(30000);
|
||||
|
||||
return err;
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
<file>gui/NavigableToolButton.qml</file>
|
||||
<file>gui/NavigableItemDelegate.qml</file>
|
||||
<file>gui/NavigableMenuItem.qml</file>
|
||||
<file>gui/CliQuitStreamSegue.qml</file>
|
||||
<file>gui/CliStartStreamSegue.qml</file>
|
||||
<file>gui/AutoResizingComboBox.qml</file>
|
||||
</qresource>
|
||||
|
||||
+1
-1
@@ -1,3 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="24" height="24" viewBox="0 0 24 24" fill="#0BF200">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="24" height="24" viewBox="0 0 24 24" fill="#FF0000">
|
||||
<path d="M21,10.12H14.22L16.96,7.3C14.23,4.6 9.81,4.5 7.08,7.2C4.35,9.91 4.35,14.28 7.08,17C9.81,19.7 14.23,19.7 16.96,17C18.32,15.65 19,14.08 19,12.1H21C21,14.08 20.12,16.65 18.36,18.39C14.85,21.87 9.15,21.87 5.64,18.39C2.14,14.92 2.11,9.28 5.62,5.81C9.13,2.34 14.76,2.34 18.27,5.81L21,3V10.12M12.5,8V12.25L16,14.33L15.28,15.54L11,13V8H12.5Z" />
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 499 B After Width: | Height: | Size: 499 B |
@@ -20,8 +20,10 @@
|
||||
#define SER_WINDOWMODE "windowmode"
|
||||
#define SER_UNSUPPORTEDFPS "unsupportedfps"
|
||||
#define SER_MDNS "mdns"
|
||||
#define SER_QUITAPPAFTER "quitAppAfter"
|
||||
#define SER_MOUSEACCELERATION "mouseacceleration"
|
||||
#define SER_STARTWINDOWED "startwindowed"
|
||||
#define SER_FRAMEPACING "framepacing"
|
||||
|
||||
StreamingPreferences::StreamingPreferences(QObject *parent)
|
||||
: QObject(parent)
|
||||
@@ -43,8 +45,10 @@ void StreamingPreferences::reload()
|
||||
multiController = settings.value(SER_MULTICONT, true).toBool();
|
||||
unsupportedFps = settings.value(SER_UNSUPPORTEDFPS, false).toBool();
|
||||
enableMdns = settings.value(SER_MDNS, true).toBool();
|
||||
quitAppAfter = settings.value(SER_QUITAPPAFTER, false).toBool();
|
||||
mouseAcceleration = settings.value(SER_MOUSEACCELERATION, false).toBool();
|
||||
startWindowed = settings.value(SER_STARTWINDOWED, false).toBool();
|
||||
framePacing = settings.value(SER_FRAMEPACING, false).toBool();
|
||||
audioConfig = static_cast<AudioConfig>(settings.value(SER_AUDIOCFG,
|
||||
static_cast<int>(AudioConfig::AC_STEREO)).toInt());
|
||||
videoCodecConfig = static_cast<VideoCodecConfig>(settings.value(SER_VIDEOCFG,
|
||||
@@ -71,8 +75,10 @@ void StreamingPreferences::save()
|
||||
settings.setValue(SER_MULTICONT, multiController);
|
||||
settings.setValue(SER_UNSUPPORTEDFPS, unsupportedFps);
|
||||
settings.setValue(SER_MDNS, enableMdns);
|
||||
settings.setValue(SER_QUITAPPAFTER, quitAppAfter);
|
||||
settings.setValue(SER_MOUSEACCELERATION, mouseAcceleration);
|
||||
settings.setValue(SER_STARTWINDOWED, startWindowed);
|
||||
settings.setValue(SER_FRAMEPACING, framePacing);
|
||||
settings.setValue(SER_AUDIOCFG, static_cast<int>(audioConfig));
|
||||
settings.setValue(SER_VIDEOCFG, static_cast<int>(videoCodecConfig));
|
||||
settings.setValue(SER_VIDEODEC, static_cast<int>(videoDecoderSelection));
|
||||
|
||||
@@ -73,8 +73,10 @@ public:
|
||||
Q_PROPERTY(bool multiController MEMBER multiController NOTIFY multiControllerChanged)
|
||||
Q_PROPERTY(bool unsupportedFps MEMBER unsupportedFps NOTIFY unsupportedFpsChanged)
|
||||
Q_PROPERTY(bool enableMdns MEMBER enableMdns NOTIFY enableMdnsChanged)
|
||||
Q_PROPERTY(bool quitAppAfter MEMBER quitAppAfter NOTIFY quitAppAfterChanged)
|
||||
Q_PROPERTY(bool mouseAcceleration MEMBER mouseAcceleration NOTIFY mouseAccelerationChanged)
|
||||
Q_PROPERTY(bool startWindowed MEMBER startWindowed NOTIFY startWindowedChanged)
|
||||
Q_PROPERTY(bool framePacing MEMBER framePacing NOTIFY framePacingChanged)
|
||||
Q_PROPERTY(AudioConfig audioConfig MEMBER audioConfig NOTIFY audioConfigChanged)
|
||||
Q_PROPERTY(VideoCodecConfig videoCodecConfig MEMBER videoCodecConfig NOTIFY videoCodecConfigChanged)
|
||||
Q_PROPERTY(VideoDecoderSelection videoDecoderSelection MEMBER videoDecoderSelection NOTIFY videoDecoderSelectionChanged)
|
||||
@@ -91,8 +93,10 @@ public:
|
||||
bool multiController;
|
||||
bool unsupportedFps;
|
||||
bool enableMdns;
|
||||
bool quitAppAfter;
|
||||
bool mouseAcceleration;
|
||||
bool startWindowed;
|
||||
bool framePacing;
|
||||
AudioConfig audioConfig;
|
||||
VideoCodecConfig videoCodecConfig;
|
||||
VideoDecoderSelection videoDecoderSelection;
|
||||
@@ -107,11 +111,13 @@ signals:
|
||||
void multiControllerChanged();
|
||||
void unsupportedFpsChanged();
|
||||
void enableMdnsChanged();
|
||||
void quitAppAfterChanged();
|
||||
void mouseAccelerationChanged();
|
||||
void audioConfigChanged();
|
||||
void videoCodecConfigChanged();
|
||||
void videoDecoderSelectionChanged();
|
||||
void windowModeChanged();
|
||||
void startWindowedChanged();
|
||||
void framePacingChanged();
|
||||
};
|
||||
|
||||
|
||||
@@ -4,12 +4,28 @@
|
||||
|
||||
#include <QtGlobal>
|
||||
|
||||
// GFE sends us packets in 5 ms chunks
|
||||
const double SoundIoAudioRenderer::k_RawSampleLengthSec = 0.005;
|
||||
|
||||
#ifdef Q_OS_LINUX
|
||||
// PulseAudio and ALSA require more than just 5 ms samples
|
||||
// for some reason, so write a minimum of 20 ms each time to
|
||||
// prevent underruns on Bluetooth.
|
||||
const double SoundIoAudioRenderer::k_MinSampleLengthSec = 0.020;
|
||||
#else
|
||||
// This determines the size of the buffers we'll
|
||||
// get from CoreAudio. It is also the minimum
|
||||
// size that we will write when called to fill a buffer.
|
||||
const double SoundIoAudioRenderer::k_MinSampleLengthSec = k_RawSampleLengthSec;
|
||||
#endif
|
||||
|
||||
SoundIoAudioRenderer::SoundIoAudioRenderer()
|
||||
: m_OpusChannelCount(0),
|
||||
m_SoundIo(nullptr),
|
||||
m_Device(nullptr),
|
||||
m_OutputStream(nullptr),
|
||||
m_RingBuffer(nullptr),
|
||||
m_Latency(0),
|
||||
m_Errored(false)
|
||||
{
|
||||
|
||||
@@ -17,6 +33,10 @@ SoundIoAudioRenderer::SoundIoAudioRenderer()
|
||||
|
||||
SoundIoAudioRenderer::~SoundIoAudioRenderer()
|
||||
{
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Audio latency: %f",
|
||||
m_Latency);
|
||||
|
||||
if (m_OutputStream != nullptr) {
|
||||
soundio_outstream_destroy(m_OutputStream);
|
||||
}
|
||||
@@ -152,17 +172,12 @@ bool SoundIoAudioRenderer::prepareForPlayback(const OPUS_MULTISTREAM_CONFIGURATI
|
||||
|
||||
m_OutputStream->format = SoundIoFormatS16NE;
|
||||
m_OutputStream->sample_rate = opusConfig->sampleRate;
|
||||
m_OutputStream->software_latency = k_MinSampleLengthSec;
|
||||
m_OutputStream->name = "Moonlight";
|
||||
m_OutputStream->userdata = this;
|
||||
m_OutputStream->error_callback = sioErrorCallback;
|
||||
m_OutputStream->write_callback = sioWriteCallback;
|
||||
|
||||
// This determines the size of the buffers we'll
|
||||
// get from CoreAudio. Since GFE sends us packets
|
||||
// in 5 ms chunks, we'll give them to the OS in
|
||||
// buffers of the same size.
|
||||
m_OutputStream->software_latency = 0.005;
|
||||
|
||||
SoundIoChannelLayout bestLayout = m_Device->current_layout;
|
||||
for (int i = 0; i < m_Device->layout_count; i++) {
|
||||
if (scoreChannelLayout(&bestLayout, opusConfig) <
|
||||
@@ -218,12 +233,33 @@ bool SoundIoAudioRenderer::prepareForPlayback(const OPUS_MULTISTREAM_CONFIGURATI
|
||||
}
|
||||
}
|
||||
|
||||
// Buffer up to 6 packets of audio (30 ms) to smooth
|
||||
// out network packet delivery jitter
|
||||
int packetsToBuffer;
|
||||
|
||||
#ifdef Q_OS_LINUX
|
||||
// PulseAudio and ALSA need the large buffer (see comment on k_MinSampleLengthSec),
|
||||
// so we need a buffer at least double that size to allow packets to arrive
|
||||
// while we're writing to the sink.
|
||||
packetsToBuffer = (int)(k_MinSampleLengthSec / k_RawSampleLengthSec) * 2;
|
||||
#else
|
||||
if (m_SoundIo->current_backend == SoundIoBackendWasapi) {
|
||||
// 15 ms buffer seems to be fine for WASAPI
|
||||
packetsToBuffer = 3;
|
||||
}
|
||||
else {
|
||||
// 30 ms buffer on CoreAudio to avoid glitching on macOS
|
||||
packetsToBuffer = 6;
|
||||
}
|
||||
#endif
|
||||
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Audio buffer size: %d packets",
|
||||
packetsToBuffer);
|
||||
|
||||
m_RingBuffer = soundio_ring_buffer_create(m_SoundIo,
|
||||
m_OutputStream->bytes_per_sample *
|
||||
m_OpusChannelCount *
|
||||
SAMPLES_PER_FRAME * 6);
|
||||
SAMPLES_PER_FRAME *
|
||||
packetsToBuffer);
|
||||
if (m_RingBuffer == nullptr) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"soundio_ring_buffer_create() failed");
|
||||
@@ -238,6 +274,11 @@ bool SoundIoAudioRenderer::prepareForPlayback(const OPUS_MULTISTREAM_CONFIGURATI
|
||||
return false;
|
||||
}
|
||||
|
||||
// HACK: For some reason, a constant latency hangs around in the audio pipeline
|
||||
// unless we wait for the audio stream to drain before actually submitting any samples.
|
||||
// This is a gross hack, but it works remarkably well.
|
||||
SDL_Delay(500);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -328,34 +369,19 @@ void SoundIoAudioRenderer::sioWriteCallback(SoundIoOutStream* stream, int frameC
|
||||
(me->m_OpusChannelCount * stream->bytes_per_sample);
|
||||
int bytesRead = 0;
|
||||
|
||||
// Clamp framesLeft to frameCountMax
|
||||
framesLeft = qMin(framesLeft, frameCountMax);
|
||||
// Ensure we always write at least a buffer, even if it's silence, to avoid
|
||||
// busy looping when no audio data is available while libsoundio tries to keep
|
||||
// us from starving the output device.
|
||||
frameCountMin = qMax(frameCountMin, (int)(stream->sample_rate * k_MinSampleLengthSec));
|
||||
frameCountMin = qMin(frameCountMin, frameCountMax);
|
||||
|
||||
// Place an upper-bound on audio stream latency to
|
||||
// avoid accumulating packets in queue-based backends
|
||||
// like WASAPI. This bound was set by testing on several
|
||||
// Windows machines. The highest latency was found on
|
||||
// a XPS 9343 running Windows 7 in Steam Big Picture
|
||||
// and the 5.1 audio test clip from Fraunhofer.
|
||||
if (me->m_SoundIo->current_backend == SoundIoBackendWasapi) {
|
||||
double latency;
|
||||
if (soundio_outstream_get_latency(stream, &latency) == SoundIoErrorNone) {
|
||||
if (latency > 0.050) {
|
||||
// If our latency is higher than desired, drop these samples to gracefully lower
|
||||
// the latency without glitching too much. Dropping the whole buffer causes
|
||||
// a much more noticeable glitch. This approach also ensures that we don't
|
||||
// accidentally underflow if the driver/kernel side is delayed and isn't
|
||||
// consuming data fast enough. Dropping a frame at a time and re-evaluating
|
||||
// each time ensures that we'll stop dropping if latency returns to normal.
|
||||
readPtr += framesLeft * stream->bytes_per_sample * me->m_OpusChannelCount;
|
||||
bytesRead += framesLeft * stream->bytes_per_sample * me->m_OpusChannelCount;
|
||||
framesLeft = 0;
|
||||
// Clamp framesLeft to frameCountMin to ensure that we never write more than one sample.
|
||||
// This makes sure that we never increase our latency beyond what the sink is consuming.
|
||||
framesLeft = qMin(framesLeft, frameCountMin);
|
||||
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Latency exceeded drop cap: %f",
|
||||
latency);
|
||||
}
|
||||
}
|
||||
// Track latency on queueing-based backends
|
||||
if (me->m_SoundIo->current_backend != SoundIoBackendCoreAudio && me->m_SoundIo->current_backend != SoundIoBackendJack) {
|
||||
soundio_outstream_get_latency(stream, &me->m_Latency);
|
||||
}
|
||||
|
||||
for (;;) {
|
||||
|
||||
@@ -32,5 +32,9 @@ private:
|
||||
struct SoundIoOutStream* m_OutputStream;
|
||||
struct SoundIoRingBuffer* m_RingBuffer;
|
||||
struct SoundIoChannelLayout m_EffectiveLayout;
|
||||
double m_Latency;
|
||||
bool m_Errored;
|
||||
|
||||
static const double k_RawSampleLengthSec;
|
||||
static const double k_MinSampleLengthSec;
|
||||
};
|
||||
|
||||
@@ -971,7 +971,7 @@ QString SdlInputHandler::getUnmappedGamepads()
|
||||
"Number of axes: %d | Number of buttons: %d | Number of hats: %d",
|
||||
numAxes, numButtons, numHats);
|
||||
|
||||
if (numAxes >= 4 && numButtons >= 8 && numHats <= 1) {
|
||||
if ((numAxes >= 4 && numAxes <= 8) && numButtons >= 8 && numHats <= 1) {
|
||||
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Joystick likely to be an unmapped game controller");
|
||||
if (!ret.isEmpty()) {
|
||||
|
||||
+76
-29
@@ -73,6 +73,7 @@ void Session::clStageFailed(int stage, long errorCode)
|
||||
|
||||
void Session::clConnectionTerminated(long errorCode)
|
||||
{
|
||||
s_ActiveSession->m_UnexpectedTermination = true;
|
||||
emit s_ActiveSession->displayLaunchError("Connection terminated");
|
||||
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
@@ -98,12 +99,16 @@ void Session::clLogMessage(const char* format, ...)
|
||||
va_end(ap);
|
||||
}
|
||||
|
||||
#define CALL_INITIALIZE(dec) (dec)->initialize(vds, window, videoFormat, width, height, frameRate, enableVsync)
|
||||
#define CALL_INITIALIZE(dec) (dec)->initialize(vds, window, videoFormat, width, height, frameRate, enableVsync, enableFramePacing)
|
||||
|
||||
bool Session::chooseDecoder(StreamingPreferences::VideoDecoderSelection vds,
|
||||
SDL_Window* window, int videoFormat, int width, int height,
|
||||
int frameRate, bool enableVsync, IVideoDecoder*& chosenDecoder)
|
||||
int frameRate, bool enableVsync, bool enableFramePacing, IVideoDecoder*& chosenDecoder)
|
||||
{
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"V-sync %s",
|
||||
enableVsync ? "enabled" : "disabled");
|
||||
|
||||
#ifdef HAVE_SLVIDEO
|
||||
chosenDecoder = new SLVideoDecoder();
|
||||
if (CALL_INITIALIZE(chosenDecoder)) {
|
||||
@@ -216,7 +221,7 @@ bool Session::isHardwareDecodeAvailable(StreamingPreferences::VideoDecoderSelect
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!chooseDecoder(vds, window, videoFormat, width, height, frameRate, true, decoder)) {
|
||||
if (!chooseDecoder(vds, window, videoFormat, width, height, frameRate, true, false, decoder)) {
|
||||
SDL_DestroyWindow(window);
|
||||
SDL_QuitSubSystem(SDL_INIT_VIDEO);
|
||||
return false;
|
||||
@@ -256,7 +261,7 @@ int Session::getDecoderCapabilities(StreamingPreferences::VideoDecoderSelection
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!chooseDecoder(vds, window, videoFormat, width, height, frameRate, true, decoder)) {
|
||||
if (!chooseDecoder(vds, window, videoFormat, width, height, frameRate, true, false, decoder)) {
|
||||
SDL_DestroyWindow(window);
|
||||
SDL_QuitSubSystem(SDL_INIT_VIDEO);
|
||||
return false;
|
||||
@@ -287,12 +292,21 @@ Session::Session(NvComputer* computer, NvApp& app, StreamingPreferences *prefere
|
||||
m_DisplayOriginX(0),
|
||||
m_DisplayOriginY(0),
|
||||
m_PendingWindowedTransition(false),
|
||||
m_UnexpectedTermination(true), // Failure prior to streaming is unexpected
|
||||
m_OpusDecoder(nullptr),
|
||||
m_AudioRenderer(nullptr),
|
||||
m_AudioSampleCount(0)
|
||||
{
|
||||
}
|
||||
|
||||
Session::~Session()
|
||||
{
|
||||
// Acquire session semaphore to ensure all cleanup is done before the destructor returns
|
||||
// and the object is deallocated.
|
||||
s_ActiveSessionSemaphore.acquire();
|
||||
s_ActiveSessionSemaphore.release();
|
||||
}
|
||||
|
||||
void Session::initialize()
|
||||
{
|
||||
qInfo() << "Server GPU:" << m_Computer->gpuModel;
|
||||
@@ -315,6 +329,8 @@ void Session::initialize()
|
||||
m_StreamConfig.fps = m_Preferences->fps;
|
||||
m_StreamConfig.bitrate = m_Preferences->bitrateKbps;
|
||||
m_StreamConfig.hevcBitratePercentageMultiplier = 75;
|
||||
m_StreamConfig.streamingRemotely = STREAM_CFG_AUTO;
|
||||
m_StreamConfig.packetSize = 1392;
|
||||
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Video bitrate: %d kbps",
|
||||
@@ -382,20 +398,6 @@ void Session::initialize()
|
||||
break;
|
||||
}
|
||||
|
||||
if (m_Computer->activeAddress == m_Computer->remoteAddress) {
|
||||
m_StreamConfig.streamingRemotely = 1;
|
||||
}
|
||||
else {
|
||||
m_StreamConfig.streamingRemotely = 0;
|
||||
}
|
||||
|
||||
if (m_Computer->activeAddress == m_Computer->localAddress) {
|
||||
m_StreamConfig.packetSize = 1392;
|
||||
}
|
||||
else {
|
||||
m_StreamConfig.packetSize = 1024;
|
||||
}
|
||||
|
||||
switch (m_Preferences->windowMode)
|
||||
{
|
||||
case StreamingPreferences::WM_FULLSCREEN_DESKTOP:
|
||||
@@ -563,15 +565,53 @@ bool Session::validateLaunch()
|
||||
|
||||
class DeferredSessionCleanupTask : public QRunnable
|
||||
{
|
||||
void run() override
|
||||
{
|
||||
// Finish cleanup of the connection state
|
||||
LiStopConnection();
|
||||
public:
|
||||
DeferredSessionCleanupTask(Session* session) :
|
||||
m_Session(session) {}
|
||||
|
||||
private:
|
||||
virtual ~DeferredSessionCleanupTask() override
|
||||
{
|
||||
// Allow another session to start now that we're cleaned up
|
||||
Session::s_ActiveSession = nullptr;
|
||||
Session::s_ActiveSessionSemaphore.release();
|
||||
}
|
||||
|
||||
void run() override
|
||||
{
|
||||
// Only quit the running app if our session terminated gracefully
|
||||
bool shouldQuit =
|
||||
!m_Session->m_UnexpectedTermination &&
|
||||
m_Session->m_Preferences->quitAppAfter;
|
||||
|
||||
// Notify the UI
|
||||
if (shouldQuit) {
|
||||
emit m_Session->quitStarting();
|
||||
}
|
||||
else {
|
||||
emit m_Session->sessionFinished();
|
||||
}
|
||||
|
||||
// Finish cleanup of the connection state
|
||||
LiStopConnection();
|
||||
|
||||
// Perform a best-effort app quit
|
||||
if (shouldQuit) {
|
||||
NvHTTP http(m_Session->m_Computer->activeAddress, m_Session->m_Computer->serverCert);
|
||||
|
||||
// Logging is already done inside NvHTTP
|
||||
try {
|
||||
http.quitApp();
|
||||
} catch (const GfeHttpResponseException&) {
|
||||
} catch (const QtNetworkReplyException&) {
|
||||
}
|
||||
|
||||
// Session is finished now
|
||||
emit m_Session->sessionFinished();
|
||||
}
|
||||
}
|
||||
|
||||
Session* m_Session;
|
||||
};
|
||||
|
||||
void Session::getWindowDimensions(int& x, int& y,
|
||||
@@ -745,6 +785,7 @@ void Session::exec(int displayOriginX, int displayOriginY)
|
||||
// Check for validation errors/warnings and emit
|
||||
// signals for them, if appropriate
|
||||
if (!validateLaunch()) {
|
||||
emit sessionFinished();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -775,7 +816,7 @@ void Session::exec(int displayOriginX, int displayOriginY)
|
||||
m_Computer->currentGameId == m_App.id);
|
||||
|
||||
try {
|
||||
NvHTTP http(m_Computer->activeAddress);
|
||||
NvHTTP http(m_Computer->activeAddress, m_Computer->serverCert);
|
||||
if (m_Computer->currentGameId != 0) {
|
||||
http.resumeApp(&m_StreamConfig);
|
||||
}
|
||||
@@ -787,11 +828,11 @@ void Session::exec(int displayOriginX, int displayOriginY)
|
||||
}
|
||||
} catch (const GfeHttpResponseException& e) {
|
||||
emit displayLaunchError(e.toQString());
|
||||
s_ActiveSessionSemaphore.release();
|
||||
QThreadPool::globalInstance()->start(new DeferredSessionCleanupTask(this));
|
||||
return;
|
||||
} catch (const QtNetworkReplyException& e) {
|
||||
emit displayLaunchError(e.toQString());
|
||||
s_ActiveSessionSemaphore.release();
|
||||
QThreadPool::globalInstance()->start(new DeferredSessionCleanupTask(this));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -801,7 +842,7 @@ void Session::exec(int displayOriginX, int displayOriginY)
|
||||
"SDL_InitSubSystem(SDL_INIT_VIDEO) failed: %s",
|
||||
SDL_GetError());
|
||||
emit displayLaunchError(QString::fromLocal8Bit(SDL_GetError()));
|
||||
s_ActiveSessionSemaphore.release();
|
||||
QThreadPool::globalInstance()->start(new DeferredSessionCleanupTask(this));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -829,7 +870,7 @@ void Session::exec(int displayOriginX, int displayOriginY)
|
||||
// We already displayed an error dialog in the stage failure
|
||||
// listener.
|
||||
SDL_QuitSubSystem(SDL_INIT_VIDEO);
|
||||
s_ActiveSessionSemaphore.release();
|
||||
QThreadPool::globalInstance()->start(new DeferredSessionCleanupTask(this));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -851,7 +892,7 @@ void Session::exec(int displayOriginX, int displayOriginY)
|
||||
"SDL_CreateWindow() failed: %s",
|
||||
SDL_GetError());
|
||||
SDL_QuitSubSystem(SDL_INIT_VIDEO);
|
||||
s_ActiveSessionSemaphore.release();
|
||||
QThreadPool::globalInstance()->start(new DeferredSessionCleanupTask(this));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -931,6 +972,11 @@ void Session::exec(int displayOriginX, int displayOriginY)
|
||||
|
||||
int currentDisplayIndex = SDL_GetWindowDisplayIndex(m_Window);
|
||||
|
||||
// Now that we're about to stream, any SDL_QUIT event is expected
|
||||
// unless it comes from the connection termination callback where
|
||||
// (m_UnexpectedTermination is set back to true).
|
||||
m_UnexpectedTermination = false;
|
||||
|
||||
// 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;
|
||||
@@ -1078,6 +1124,7 @@ void Session::exec(int displayOriginX, int displayOriginY)
|
||||
m_Window, m_ActiveVideoFormat, m_ActiveVideoWidth,
|
||||
m_ActiveVideoHeight, m_ActiveVideoFrameRate,
|
||||
enableVsync,
|
||||
enableVsync && m_Preferences->framePacing,
|
||||
s_ActiveSession->m_VideoDecoder)) {
|
||||
SDL_AtomicUnlock(&m_DecoderLock);
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
@@ -1164,6 +1211,6 @@ DispatchDeferredCleanup:
|
||||
// Cleanup can take a while, so dispatch it to a worker thread.
|
||||
// When it is complete, it will release our s_ActiveSessionSemaphore
|
||||
// reference.
|
||||
QThreadPool::globalInstance()->start(new DeferredSessionCleanupTask());
|
||||
QThreadPool::globalInstance()->start(new DeferredSessionCleanupTask(this));
|
||||
}
|
||||
|
||||
|
||||
@@ -20,6 +20,8 @@ class Session : public QObject
|
||||
public:
|
||||
explicit Session(NvComputer* computer, NvApp& app, StreamingPreferences *preferences = nullptr);
|
||||
|
||||
virtual ~Session();
|
||||
|
||||
Q_INVOKABLE void exec(int displayOriginX, int displayOriginY);
|
||||
|
||||
static
|
||||
@@ -41,6 +43,10 @@ signals:
|
||||
|
||||
void displayLaunchWarning(QString text);
|
||||
|
||||
void quitStarting();
|
||||
|
||||
void sessionFinished();
|
||||
|
||||
private:
|
||||
void initialize();
|
||||
|
||||
@@ -66,7 +72,8 @@ private:
|
||||
static
|
||||
bool chooseDecoder(StreamingPreferences::VideoDecoderSelection vds,
|
||||
SDL_Window* window, int videoFormat, int width, int height,
|
||||
int frameRate, bool enableVsync, IVideoDecoder*& chosenDecoder);
|
||||
int frameRate, bool enableVsync, bool enableFramePacing,
|
||||
IVideoDecoder*& chosenDecoder);
|
||||
|
||||
static
|
||||
void clStageStarting(int stage);
|
||||
@@ -114,6 +121,7 @@ private:
|
||||
int m_DisplayOriginX;
|
||||
int m_DisplayOriginY;
|
||||
bool m_PendingWindowedTransition;
|
||||
bool m_UnexpectedTermination;
|
||||
|
||||
int m_ActiveVideoFormat;
|
||||
int m_ActiveVideoWidth;
|
||||
|
||||
@@ -16,6 +16,7 @@ typedef struct _VIDEO_STATS {
|
||||
uint32_t pacerDroppedFrames;
|
||||
uint32_t totalReassemblyTime;
|
||||
uint32_t totalDecodeTime;
|
||||
uint32_t totalPacerTime;
|
||||
uint32_t totalRenderTime;
|
||||
float receivedFps;
|
||||
float decodedFps;
|
||||
@@ -32,7 +33,8 @@ public:
|
||||
int width,
|
||||
int height,
|
||||
int frameRate,
|
||||
bool enableVsync) = 0;
|
||||
bool enableVsync,
|
||||
bool enableFramePacing) = 0;
|
||||
virtual bool isHardwareAccelerated() = 0;
|
||||
virtual int getDecoderCapabilities() = 0;
|
||||
virtual int submitDecodeUnit(PDECODE_UNIT du) = 0;
|
||||
|
||||
@@ -644,9 +644,9 @@ int DXVA2Renderer::getDecoderCapabilities()
|
||||
return 0;
|
||||
}
|
||||
|
||||
IFFmpegRenderer::VSyncConstraint DXVA2Renderer::getVsyncConstraint()
|
||||
IFFmpegRenderer::FramePacingConstraint DXVA2Renderer::getFramePacingConstraint()
|
||||
{
|
||||
return VSYNC_ANY;
|
||||
return PACING_ANY;
|
||||
}
|
||||
|
||||
void DXVA2Renderer::renderFrameAtVsync(AVFrame *frame)
|
||||
|
||||
@@ -25,7 +25,7 @@ public:
|
||||
virtual void renderFrameAtVsync(AVFrame* frame);
|
||||
virtual bool needsTestFrame();
|
||||
virtual int getDecoderCapabilities();
|
||||
virtual VSyncConstraint getVsyncConstraint();
|
||||
virtual FramePacingConstraint getFramePacingConstraint();
|
||||
|
||||
private:
|
||||
bool initializeDecoder();
|
||||
|
||||
@@ -87,7 +87,6 @@ void Pacer::vsyncCallback(int timeUntilNextVsyncMillis)
|
||||
av_frame_free(&frame);
|
||||
}
|
||||
|
||||
|
||||
if (m_FrameQueue.isEmpty()) {
|
||||
SDL_AtomicUnlock(&m_FrameQueueLock);
|
||||
|
||||
@@ -112,8 +111,11 @@ RenderNextFrame:
|
||||
AVFrame* frame = m_FrameQueue.dequeue();
|
||||
SDL_AtomicUnlock(&m_FrameQueueLock);
|
||||
|
||||
// Render it
|
||||
// Count time spent in Pacer's queues
|
||||
Uint32 beforeRender = SDL_GetTicks();
|
||||
m_VideoStats->totalPacerTime += beforeRender - frame->pts;
|
||||
|
||||
// Render it
|
||||
m_VsyncRenderer->renderFrameAtVsync(frame);
|
||||
m_VideoStats->totalRenderTime += SDL_GetTicks() - beforeRender;
|
||||
m_VideoStats->renderedFrames++;
|
||||
@@ -122,15 +124,14 @@ RenderNextFrame:
|
||||
av_frame_free(&frame);
|
||||
}
|
||||
|
||||
bool Pacer::initialize(SDL_Window* window, int maxVideoFps, bool enableVsync)
|
||||
bool Pacer::initialize(SDL_Window* window, int maxVideoFps, bool enablePacing)
|
||||
{
|
||||
m_MaxVideoFps = maxVideoFps;
|
||||
m_EnableVsync = enableVsync;
|
||||
m_DisplayFps = StreamUtils::getDisplayRefreshRate(window);
|
||||
|
||||
if (m_EnableVsync) {
|
||||
if (enablePacing) {
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Frame pacing in tear-free mode: target %d Hz with %d FPS stream",
|
||||
"Frame pacing active: target %d Hz with %d FPS stream",
|
||||
m_DisplayFps, m_MaxVideoFps);
|
||||
|
||||
#if defined(Q_OS_DARWIN)
|
||||
@@ -148,7 +149,7 @@ bool Pacer::initialize(SDL_Window* window, int maxVideoFps, bool enableVsync)
|
||||
}
|
||||
else {
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Minimal latency tearing mode: target %d Hz with %d FPS stream",
|
||||
"Frame pacing disabled: target %d Hz with %d FPS stream",
|
||||
m_DisplayFps, m_MaxVideoFps);
|
||||
}
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ public:
|
||||
|
||||
void submitFrame(AVFrame* frame);
|
||||
|
||||
bool initialize(SDL_Window* window, int maxVideoFps, bool enableVsync);
|
||||
bool initialize(SDL_Window* window, int maxVideoFps, bool enablePacing);
|
||||
|
||||
void vsyncCallback(int timeUntilNextVsyncMillis);
|
||||
|
||||
@@ -35,6 +35,5 @@ private:
|
||||
IFFmpegRenderer* m_VsyncRenderer;
|
||||
int m_MaxVideoFps;
|
||||
int m_DisplayFps;
|
||||
bool m_EnableVsync;
|
||||
PVIDEO_STATS m_VideoStats;
|
||||
};
|
||||
|
||||
@@ -8,10 +8,10 @@ extern "C" {
|
||||
|
||||
class IFFmpegRenderer {
|
||||
public:
|
||||
enum VSyncConstraint {
|
||||
VSYNC_FORCE_OFF,
|
||||
VSYNC_FORCE_ON,
|
||||
VSYNC_ANY
|
||||
enum FramePacingConstraint {
|
||||
PACING_FORCE_OFF,
|
||||
PACING_FORCE_ON,
|
||||
PACING_ANY
|
||||
};
|
||||
|
||||
virtual ~IFFmpegRenderer() {}
|
||||
@@ -25,7 +25,7 @@ public:
|
||||
virtual void renderFrameAtVsync(AVFrame* frame) = 0;
|
||||
virtual bool needsTestFrame() = 0;
|
||||
virtual int getDecoderCapabilities() = 0;
|
||||
virtual VSyncConstraint getVsyncConstraint() = 0;
|
||||
virtual FramePacingConstraint getFramePacingConstraint() = 0;
|
||||
};
|
||||
|
||||
class SdlRenderer : public IFFmpegRenderer {
|
||||
@@ -42,7 +42,7 @@ public:
|
||||
virtual void renderFrameAtVsync(AVFrame* frame);
|
||||
virtual bool needsTestFrame();
|
||||
virtual int getDecoderCapabilities();
|
||||
virtual VSyncConstraint getVsyncConstraint();
|
||||
virtual FramePacingConstraint getFramePacingConstraint();
|
||||
|
||||
private:
|
||||
SDL_Renderer* m_Renderer;
|
||||
|
||||
@@ -43,9 +43,9 @@ int SdlRenderer::getDecoderCapabilities()
|
||||
return CAPABILITY_REFERENCE_FRAME_INVALIDATION_AVC;
|
||||
}
|
||||
|
||||
IFFmpegRenderer::VSyncConstraint SdlRenderer::getVsyncConstraint()
|
||||
IFFmpegRenderer::FramePacingConstraint SdlRenderer::getFramePacingConstraint()
|
||||
{
|
||||
return VSYNC_ANY;
|
||||
return PACING_ANY;
|
||||
}
|
||||
|
||||
bool SdlRenderer::initialize(SDL_Window* window,
|
||||
|
||||
@@ -177,9 +177,9 @@ VAAPIRenderer::getDecoderCapabilities()
|
||||
return 0;
|
||||
}
|
||||
|
||||
IFFmpegRenderer::VSyncConstraint VAAPIRenderer::getVsyncConstraint()
|
||||
IFFmpegRenderer::FramePacingConstraint VAAPIRenderer::getFramePacingConstraint()
|
||||
{
|
||||
return VSYNC_ANY;
|
||||
return PACING_ANY;
|
||||
}
|
||||
|
||||
void
|
||||
|
||||
@@ -40,7 +40,7 @@ public:
|
||||
virtual void renderFrameAtVsync(AVFrame* frame);
|
||||
virtual bool needsTestFrame();
|
||||
virtual int getDecoderCapabilities();
|
||||
virtual VSyncConstraint getVsyncConstraint();
|
||||
virtual FramePacingConstraint getFramePacingConstraint();
|
||||
|
||||
private:
|
||||
int m_WindowSystem;
|
||||
|
||||
@@ -246,9 +246,9 @@ int VDPAURenderer::getDecoderCapabilities()
|
||||
return 0;
|
||||
}
|
||||
|
||||
IFFmpegRenderer::VSyncConstraint VDPAURenderer::getVsyncConstraint()
|
||||
IFFmpegRenderer::FramePacingConstraint VDPAURenderer::getFramePacingConstraint()
|
||||
{
|
||||
return VSYNC_ANY;
|
||||
return PACING_ANY;
|
||||
}
|
||||
|
||||
void VDPAURenderer::renderFrameAtVsync(AVFrame* frame)
|
||||
|
||||
@@ -23,7 +23,7 @@ public:
|
||||
virtual void renderFrameAtVsync(AVFrame* frame);
|
||||
virtual bool needsTestFrame();
|
||||
virtual int getDecoderCapabilities();
|
||||
virtual VSyncConstraint getVsyncConstraint();
|
||||
virtual FramePacingConstraint getFramePacingConstraint();
|
||||
|
||||
private:
|
||||
uint32_t m_VideoWidth, m_VideoHeight;
|
||||
|
||||
@@ -211,12 +211,12 @@ public:
|
||||
return 0;
|
||||
}
|
||||
|
||||
virtual IFFmpegRenderer::VSyncConstraint getVsyncConstraint() override
|
||||
virtual IFFmpegRenderer::FramePacingConstraint getFramePacingConstraint() override
|
||||
{
|
||||
// This renderer is inherently tied to V-sync due how we're
|
||||
// rendering with AVSampleBufferDisplay layer. Running without
|
||||
// the V-Sync source leads to massive stuttering.
|
||||
return VSYNC_FORCE_ON;
|
||||
return PACING_FORCE_ON;
|
||||
}
|
||||
|
||||
private:
|
||||
|
||||
@@ -131,27 +131,27 @@ void FFmpegVideoDecoder::reset()
|
||||
|
||||
bool FFmpegVideoDecoder::completeInitialization(AVCodec* decoder, SDL_Window* window,
|
||||
int videoFormat, int width, int height,
|
||||
int maxFps, bool enableVsync, bool testOnly)
|
||||
int maxFps, bool enableFramePacing, bool testOnly)
|
||||
{
|
||||
auto vsyncConstraint = m_Renderer->getVsyncConstraint();
|
||||
if (vsyncConstraint == IFFmpegRenderer::VSYNC_FORCE_OFF && enableVsync) {
|
||||
auto vsyncConstraint = m_Renderer->getFramePacingConstraint();
|
||||
if (vsyncConstraint == IFFmpegRenderer::PACING_FORCE_OFF && enableFramePacing) {
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"V-sync is forcefully disabled by the active renderer");
|
||||
enableVsync = false;
|
||||
"Frame pacing is forcefully disabled by the active renderer");
|
||||
enableFramePacing = false;
|
||||
}
|
||||
else if (vsyncConstraint == IFFmpegRenderer::VSYNC_FORCE_ON && !enableVsync) {
|
||||
else if (vsyncConstraint == IFFmpegRenderer::PACING_FORCE_ON && !enableFramePacing) {
|
||||
// FIXME: This duplicates logic in Session.cpp
|
||||
int displayHz = StreamUtils::getDisplayRefreshRate(window);
|
||||
if (displayHz + 5 >= maxFps) {
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"V-sync is forcefully enabled by the active renderer");
|
||||
enableVsync = true;
|
||||
"Frame pacing is forcefully enabled by the active renderer");
|
||||
enableFramePacing = true;
|
||||
}
|
||||
}
|
||||
|
||||
m_StreamFps = maxFps;
|
||||
m_Pacer = new Pacer(m_Renderer, &m_ActiveWndVideoStats);
|
||||
if (!m_Pacer->initialize(window, maxFps, enableVsync)) {
|
||||
if (!m_Pacer->initialize(window, maxFps, enableFramePacing)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -258,6 +258,7 @@ void FFmpegVideoDecoder::addVideoStats(VIDEO_STATS& src, VIDEO_STATS& dst)
|
||||
dst.pacerDroppedFrames += src.pacerDroppedFrames;
|
||||
dst.totalReassemblyTime += src.totalReassemblyTime;
|
||||
dst.totalDecodeTime += src.totalDecodeTime;
|
||||
dst.totalPacerTime += src.totalPacerTime;
|
||||
dst.totalRenderTime += src.totalRenderTime;
|
||||
|
||||
Uint32 now = SDL_GetTicks();
|
||||
@@ -311,6 +312,9 @@ void FFmpegVideoDecoder::logVideoStats(VIDEO_STATS& stats, const char* title)
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Average decode time: %.2f ms",
|
||||
(float)stats.totalDecodeTime / stats.decodedFrames);
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Average frame pacing delay: %.2f ms",
|
||||
(float)stats.totalPacerTime / stats.renderedFrames);
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Average render time: %.2f ms",
|
||||
(float)stats.totalRenderTime / stats.renderedFrames);
|
||||
@@ -359,7 +363,8 @@ bool FFmpegVideoDecoder::initialize(
|
||||
int width,
|
||||
int height,
|
||||
int maxFps,
|
||||
bool enableVsync)
|
||||
bool enableVsync,
|
||||
bool enableFramePacing)
|
||||
{
|
||||
AVCodec* decoder;
|
||||
|
||||
@@ -395,7 +400,7 @@ bool FFmpegVideoDecoder::initialize(
|
||||
m_Renderer = new SdlRenderer();
|
||||
if (vds != StreamingPreferences::VDS_FORCE_HARDWARE &&
|
||||
m_Renderer->initialize(window, videoFormat, width, height, maxFps, enableVsync) &&
|
||||
completeInitialization(decoder, window, videoFormat, width, height, maxFps, enableVsync, false)) {
|
||||
completeInitialization(decoder, window, videoFormat, width, height, maxFps, enableFramePacing, false)) {
|
||||
return true;
|
||||
}
|
||||
else {
|
||||
@@ -412,13 +417,13 @@ bool FFmpegVideoDecoder::initialize(
|
||||
m_HwDecodeCfg = config;
|
||||
// Initialize the hardware codec and submit a test frame if the renderer needs it
|
||||
if (m_Renderer->initialize(window, videoFormat, width, height, maxFps, enableVsync) &&
|
||||
completeInitialization(decoder, window, videoFormat, width, height, maxFps, enableVsync, m_Renderer->needsTestFrame())) {
|
||||
completeInitialization(decoder, window, videoFormat, width, height, maxFps, enableFramePacing, m_Renderer->needsTestFrame())) {
|
||||
if (m_Renderer->needsTestFrame()) {
|
||||
// The test worked, so now let's initialize it for real
|
||||
reset();
|
||||
if ((m_Renderer = createAcceleratedRenderer(config)) != nullptr &&
|
||||
m_Renderer->initialize(window, videoFormat, width, height, maxFps, enableVsync) &&
|
||||
completeInitialization(decoder, window, videoFormat, width, height, maxFps, enableVsync, false)) {
|
||||
completeInitialization(decoder, window, videoFormat, width, height, maxFps, enableFramePacing, false)) {
|
||||
return true;
|
||||
}
|
||||
else {
|
||||
@@ -577,6 +582,9 @@ int FFmpegVideoDecoder::submitDecodeUnit(PDECODE_UNIT du)
|
||||
// Reset failed decodes count if we reached this far
|
||||
m_ConsecutiveFailedDecodes = 0;
|
||||
|
||||
// Capture a frame timestamp to measuring pacing delay
|
||||
frame->pts = SDL_GetTicks();
|
||||
|
||||
// Queue the frame for rendering from the main thread
|
||||
SDL_AtomicIncRef(&m_QueuedFrames);
|
||||
queueFrame(frame);
|
||||
|
||||
@@ -18,7 +18,8 @@ public:
|
||||
int width,
|
||||
int height,
|
||||
int maxFps,
|
||||
bool enableVsync) override;
|
||||
bool enableVsync,
|
||||
bool enableFramePacing) override;
|
||||
virtual bool isHardwareAccelerated() override;
|
||||
virtual int getDecoderCapabilities() override;
|
||||
virtual int submitDecodeUnit(PDECODE_UNIT du) override;
|
||||
@@ -30,7 +31,7 @@ public:
|
||||
private:
|
||||
bool completeInitialization(AVCodec* decoder, SDL_Window* window,
|
||||
int videoFormat, int width, int height,
|
||||
int maxFps, bool enableVsync, bool testOnly);
|
||||
int maxFps, bool enableFramePacing, bool testOnly);
|
||||
|
||||
void logVideoStats(VIDEO_STATS& stats, const char* title);
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ SLVideoDecoder::getDecoderCapabilities()
|
||||
bool
|
||||
SLVideoDecoder::initialize(StreamingPreferences::VideoDecoderSelection vds,
|
||||
SDL_Window*,
|
||||
int videoFormat, int, int, int frameRate, bool)
|
||||
int videoFormat, int, int, int frameRate, bool, bool)
|
||||
{
|
||||
// SLVideo only supports hardware decoding
|
||||
if (vds == StreamingPreferences::VDS_FORCE_SOFTWARE) {
|
||||
|
||||
@@ -15,7 +15,8 @@ public:
|
||||
int width,
|
||||
int height,
|
||||
int frameRate,
|
||||
bool enableVsync);
|
||||
bool enableVsync,
|
||||
bool enableFramePacing);
|
||||
virtual bool isHardwareAccelerated();
|
||||
virtual int getDecoderCapabilities();
|
||||
virtual int submitDecodeUnit(PDECODE_UNIT du);
|
||||
|
||||
Submodule moonlight-common-c/moonlight-common-c updated: 396b02a94d...dd49e5bbfd
+1
-1
Submodule soundio/libsoundio updated: 5d6a7210fa...fb23636c95
Reference in New Issue
Block a user