Compare commits

...
41 Commits
Author SHA1 Message Date
Cameron Gutman c5d70c48c9 Fix QMap usage where insertion on read of missing value was not intended 2018-12-26 00:59:30 -08:00
Cameron Gutman 82230ef2f5 Version 0.8.0 2018-12-25 22:29:27 -08:00
Cameron Gutman dcfd3d3e48 Add a debug print for V-sync 2018-12-25 22:19:23 -08:00
Cameron Gutman cb1e2ac32e Avoid filling our client logs with QNetworkReplyHttpImplPrivate warnings 2018-12-25 21:54:20 -08:00
Cameron Gutman ec2c15946a Update audio buffering parameters based on local testing 2018-12-25 21:45:15 -08:00
Cameron Gutman 8be73142f8 Fix cert pinning on macOS 2018-12-25 21:15:59 -08:00
Cameron Gutman d1640e3bb8 Audio latency tweaks and fix for PulseAudio with A2DP 2018-12-25 17:54:18 -08:00
Cameron Gutman 8612e6726d Remove accidentally enabled debug prints 2018-12-25 14:05:55 -08:00
Cameron Gutman 3b4694e9f8 Add libraries required by libsoundio 2018-12-25 14:05:35 -08:00
Cameron Gutman 14cb2e1999 Remove latency drop logic and replace by single frame writes 2018-12-25 14:05:17 -08:00
Cameron Gutman cfabaf334e Add frame pacing toggle 2018-12-25 12:57:00 -08:00
Cameron Gutman c054536fab Count Pacer delay in frame stats 2018-12-25 12:09:45 -08:00
Cameron Gutman 35aac18b4e Improve bitrate slider label and add tooltip 2018-12-25 11:48:34 -08:00
Cameron Gutman de3928cb4d Reduce FPs on the unmapped gamepad dialog 2018-12-25 11:41:48 -08:00
Cameron Gutman 9bf612b800 Update common-c with bitrate fix 2018-12-24 20:10:37 -08:00
Cameron Gutman d9a8d67e3d Use a shared UID for all Moonlight clients 2018-12-22 21:04:44 -08:00
Cameron Gutman 99b0a56a6a Add additional cert pinning assert 2018-12-22 20:05:45 -08:00
Cameron Gutman 61d7aa0400 Require cert pinning for HTTPS 2018-12-22 19:55:28 -08:00
Cameron Gutman 1e98594972 Handle server cert change by pairing again 2018-12-21 19:17:32 -08:00
Cameron Gutman c6383f042c Pin server cert to host during pairing 2018-12-21 18:08:07 -08:00
Cameron Gutman b4d8e0e551 Update bug_report.md 2018-12-19 23:11:15 +04:00
Cameron Gutman d696536486 Version 0.7.0 2018-12-05 22:49:42 -08:00
Cameron Gutman b46a3cf759 Show stream error dialog when launching from the CLI 2018-12-05 22:22:30 -08:00
Cameron Gutman 8a5e3cdb76 Fix returning to app page when start fails 2018-12-05 22:17:26 -08:00
Cameron Gutman 4fb39226ef Fix 'moonlight -v' output on Linux 2018-12-05 22:01:22 -08:00
Cameron Gutman c40174034c Don't kill the app on the host when the session terminates unexpectedly 2018-12-05 20:17:00 -08:00
Cameron Gutman 5029c855f0 Simplify app quitting implementation a bit 2018-12-05 19:49:06 -08:00
Cameron Gutman 2eee839eaa Update moonlight-common-c 2018-12-05 18:47:56 -08:00
Cameron Gutman f42c7fec1e Update SDL GC DB 2018-12-05 18:47:44 -08:00
Janne Hakonen 0ab07303c9 Add quit cli command and app quit option after stream session. Fixes #92 (#138)
* Add quit cli command and app quit option after stream session. Fixes #92

* Code review fixes.
2018-12-05 18:45:28 -08:00
Cameron Gutman ad47990a87 Make update icon more obnoxious so people actually update 2018-12-05 18:40:21 -08:00
Cameron Gutman 1968461ce9 Allow HTTP redirects when checking for updates 2018-12-03 23:29:49 -08:00
Cameron Gutman 8aa3b5d3da Switch to new hostname 2018-12-03 21:48:49 -08:00
Cameron Gutman 23e77eefb0 Rename multi-controller checkbox to be more descriptive 2018-11-29 20:10:47 -08:00
Cameron Gutman 094443cf26 Rename start in windowed mode option to be more accurate 2018-11-28 21:46:14 -08:00
Cameron Gutman b97758ef46 Switch to libsoundio on Linux now that PA works reliably after d1fe752028 2018-11-28 00:13:04 -08:00
Cameron Gutman d1fe752028 Fix busy looping in libsoundio audio renderer when no audio is playing 2018-11-27 23:43:31 -08:00
Cameron Gutman b26df787cd Update to upstream libsoundio 2018-11-27 23:26:15 -08:00
Cameron Gutman 4c5b254730 Add larger downloads section 2018-11-23 02:09:54 -08:00
Cameron Gutman 9a153b7170 Remove unneccessary hardcoded text color for better style support 2018-11-22 02:35:25 -08:00
Cameron Gutman 065a6caee5 Use automatic remote streaming detection 2018-11-21 20:55:25 -08:00
59 changed files with 1205 additions and 322 deletions

No files matched your search

+10 -2
View File
@@ -13,6 +13,9 @@ Any special steps that are required for the bug to appear.
**Screenshots** **Screenshots**
If applicable, add screenshots to help explain your problem. If the issue is related to video glitching or poor quality, please include 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)** **Moonlight settings (please complete the following information)**
- Have any settings been adjusted from defaults? - Have any settings been adjusted from defaults?
- If so, which settings have been changed? - 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] - OS: [e.g. Windows 10 1803]
- Moonlight Version: [e.g. v0.2.0] - 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 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 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` - On Linux, logs print to the terminal when running via `flatpak run com.moonlight_stream.Moonlight`
**Additional context** **Additional context**
Add any other context about the problem here. Anything else you think may be relevant to the issue
+1 -1
View File
@@ -9,7 +9,7 @@
url = https://github.com/gabomdq/SDL_GameControllerDB.git url = https://github.com/gabomdq/SDL_GameControllerDB.git
[submodule "soundio/libsoundio"] [submodule "soundio/libsoundio"]
path = soundio/libsoundio path = soundio/libsoundio
url = https://github.com/cgutman/libsoundio.git url = https://github.com/andrewrk/libsoundio.git
[submodule "h264bitstream/h264bitstream"] [submodule "h264bitstream/h264bitstream"]
path = h264bitstream/h264bitstream path = h264bitstream/h264bitstream
url = https://github.com/aizvorski/h264bitstream.git url = https://github.com/aizvorski/h264bitstream.git
+7 -5
View File
@@ -1,11 +1,9 @@
# Moonlight PC # 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). 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). You can follow development on our [Discord server](https://discord.gg/6ERtzFY).
[![Windows AppVeyor Status](https://ci.appveyor.com/api/projects/status/glj5cxqwy2w3bglv/branch/master?svg=true)](https://ci.appveyor.com/project/cgutman/moonlight-qt/branch/master) [![Windows AppVeyor Status](https://ci.appveyor.com/api/projects/status/glj5cxqwy2w3bglv/branch/master?svg=true)](https://ci.appveyor.com/project/cgutman/moonlight-qt/branch/master)
@@ -20,6 +18,10 @@ You can follow development on our [Discord server](https://discord.gg/6ERtzFY).
- Keyboard and mouse support - Keyboard and mouse support
- Gamepad support with SDL gamepad mappings - 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 ## Building
### General Build Requirements ### General Build Requirements
* Qt 5.9 SDK or later * Qt 5.9 SDK or later
@@ -38,7 +40,7 @@ You can follow development on our [Discord server](https://discord.gg/6ERtzFY).
### Linux-specific Requirements ### Linux-specific Requirements
* GCC or Clang * 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/ * 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 ### Build Setup Steps
@@ -55,4 +57,4 @@ You can follow development on our [Discord server](https://discord.gg/6ERtzFY).
2. Write code 2. Write code
3. Send Pull Requests 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
View File
@@ -23,9 +23,9 @@
<key>NSSupportsAutomaticGraphicsSwitching</key> <key>NSSupportsAutomaticGraphicsSwitching</key>
<true/> <true/>
<key>CFBundleVersion</key> <key>CFBundleVersion</key>
<string>0.6.4</string> <string>0.8.0</string>
<key>CFBundleShortVersionString</key> <key>CFBundleShortVersionString</key>
<string>0.6.4</string> <string>0.8.0</string>
<key>CFBundleDisplayName</key> <key>CFBundleDisplayName</key>
<string>Moonlight</string> <string>Moonlight</string>
</dict> </dict>
+7 -3
View File
@@ -54,7 +54,7 @@ macx {
} }
unix:!macx { unix:!macx {
CONFIG += link_pkgconfig CONFIG += link_pkgconfig soundio
PKGCONFIG += openssl sdl2 opus PKGCONFIG += openssl sdl2 opus
# For libsoundio # For libsoundio
@@ -100,6 +100,7 @@ macx {
SOURCES += \ SOURCES += \
main.cpp \ main.cpp \
backend/computerseeker.cpp \
backend/identitymanager.cpp \ backend/identitymanager.cpp \
backend/nvcomputer.cpp \ backend/nvcomputer.cpp \
backend/nvhttp.cpp \ backend/nvhttp.cpp \
@@ -107,6 +108,7 @@ SOURCES += \
backend/computermanager.cpp \ backend/computermanager.cpp \
backend/boxartmanager.cpp \ backend/boxartmanager.cpp \
cli/commandlineparser.cpp \ cli/commandlineparser.cpp \
cli/quitstream.cpp \
cli/startstream.cpp \ cli/startstream.cpp \
settings/streamingpreferences.cpp \ settings/streamingpreferences.cpp \
streaming/input.cpp \ streaming/input.cpp \
@@ -123,6 +125,7 @@ SOURCES += \
HEADERS += \ HEADERS += \
utils.h \ utils.h \
backend/computerseeker.h \
backend/identitymanager.h \ backend/identitymanager.h \
backend/nvcomputer.h \ backend/nvcomputer.h \
backend/nvhttp.h \ backend/nvhttp.h \
@@ -130,6 +133,7 @@ HEADERS += \
backend/computermanager.h \ backend/computermanager.h \
backend/boxartmanager.h \ backend/boxartmanager.h \
cli/commandlineparser.h \ cli/commandlineparser.h \
cli/quitstream.h \
cli/startstream.h \ cli/startstream.h \
settings/streamingpreferences.h \ settings/streamingpreferences.h \
streaming/input.h \ streaming/input.h \
@@ -323,5 +327,5 @@ macx {
QMAKE_RPATHDIR += @executable_path/../Frameworks QMAKE_RPATHDIR += @executable_path/../Frameworks
} }
VERSION = 0.6.4 VERSION = 0.8.0
DEFINES += VERSION_STR=\\\"0.6.4\\\" DEFINES += VERSION_STR=\\\"0.8.0\\\"
+4 -1
View File
@@ -12,6 +12,9 @@ AutoUpdateChecker::AutoUpdateChecker(QObject *parent) :
// Never communicate over HTTP // Never communicate over HTTP
m_Nam.setStrictTransportSecurityEnabled(true); m_Nam.setStrictTransportSecurityEnabled(true);
// Allow HTTP redirects
m_Nam.setRedirectPolicy(QNetworkRequest::NoLessSafeRedirectPolicy);
connect(&m_Nam, SIGNAL(finished(QNetworkReply*)), connect(&m_Nam, SIGNAL(finished(QNetworkReply*)),
this, SLOT(handleUpdateCheckRequestFinished(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 #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 // 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)); m_Nam.get(QNetworkRequest(url));
#endif #endif
} }
+1 -1
View File
@@ -97,7 +97,7 @@ void BoxArtManager::handleBoxArtLoadComplete(NvComputer* computer, NvApp app, QU
QUrl BoxArtManager::loadBoxArtFromNetwork(NvComputer* computer, int appId) QUrl BoxArtManager::loadBoxArtFromNetwork(NvComputer* computer, int appId)
{ {
NvHTTP http(computer->activeAddress); NvHTTP http(computer->activeAddress, computer->serverCert);
QString cachePath = getFilePathForBoxArt(computer, appId); QString cachePath = getFilePathForBoxArt(computer, appId);
QImage image; QImage image;
+83 -49
View File
@@ -27,7 +27,7 @@ public:
private: private:
bool tryPollComputer(QString address, bool& changed) bool tryPollComputer(QString address, bool& changed)
{ {
NvHTTP http(address); NvHTTP http(address, m_Computer->serverCert);
QString serverInfo; QString serverInfo;
try { try {
@@ -36,7 +36,7 @@ private:
return false; return false;
} }
NvComputer newState(address, serverInfo); NvComputer newState(address, serverInfo, QSslCertificate());
// Ensure the machine that responded is the one we intended to contact // Ensure the machine that responded is the one we intended to contact
if (m_Computer->uuid != newState.uuid) { if (m_Computer->uuid != newState.uuid) {
@@ -52,7 +52,7 @@ private:
{ {
Q_ASSERT(m_Computer->activeAddress != nullptr); Q_ASSERT(m_Computer->activeAddress != nullptr);
NvHTTP http(m_Computer->activeAddress); NvHTTP http(m_Computer->activeAddress, m_Computer->serverCert);
QVector<NvApp> appList; QVector<NvApp> appList;
@@ -218,7 +218,7 @@ void ComputerManager::saveHosts()
settings.beginWriteArray(SER_HOSTS); settings.beginWriteArray(SER_HOSTS);
for (int i = 0; i < m_KnownHosts.count(); i++) { for (int i = 0; i < m_KnownHosts.count(); i++) {
settings.setArrayIndex(i); settings.setArrayIndex(i);
m_KnownHosts[m_KnownHosts.keys()[i]]->serialize(settings); m_KnownHosts.value(m_KnownHosts.keys()[i])->serialize(settings);
} }
settings.endArray(); settings.endArray();
} }
@@ -266,7 +266,7 @@ void ComputerManager::startPollingComputer(NvComputer* computer)
} }
if (m_PollThreads.contains(computer->uuid)) { if (m_PollThreads.contains(computer->uuid)) {
Q_ASSERT(m_PollThreads[computer->uuid]->isRunning()); Q_ASSERT(m_PollThreads.value(computer->uuid)->isRunning());
return; return;
} }
@@ -324,9 +324,8 @@ public:
{ {
QWriteLocker lock(&m_ComputerManager->m_Lock); 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); m_ComputerManager->m_KnownHosts.remove(m_Computer->uuid);
} }
@@ -385,7 +384,7 @@ private:
NvPairingManager pairingManager(m_Computer->activeAddress); NvPairingManager pairingManager(m_Computer->activeAddress);
try { 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) switch (result)
{ {
case NvPairingManager::PairState::PIN_WRONG: case NvPairingManager::PairState::PIN_WRONG:
@@ -438,7 +437,7 @@ signals:
private: private:
void run() void run()
{ {
NvHTTP http(m_Computer->activeAddress); NvHTTP http(m_Computer->activeAddress, m_Computer->serverCert);
try { try {
if (m_Computer->currentGameId != 0) { if (m_Computer->currentGameId != 0) {
@@ -538,12 +537,8 @@ signals:
void computerStateChanged(NvComputer* computer); void computerStateChanged(NvComputer* computer);
private: 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; QString serverInfo;
try { try {
// There's a race condition between GameStream servers reporting presence over // There's a race condition between GameStream servers reporting presence over
@@ -564,14 +559,51 @@ private:
throw e; throw e;
} }
} }
return serverInfo;
} catch (...) { } catch (...) {
if (!m_Mdns) { if (!m_Mdns) {
emit computerAddCompleted(false); 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; 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 // Update addresses depending on the context
if (m_Mdns) { if (m_Mdns) {
@@ -591,46 +623,48 @@ private:
newComputer->manualAddress = m_Address; newComputer->manualAddress = m_Address;
} }
// Check if this PC already exists {
QWriteLocker lock(&m_ComputerManager->m_Lock); // Check if this PC already exists
NvComputer* existingComputer = m_ComputerManager->m_KnownHosts[newComputer->uuid]; QWriteLocker lock(&m_ComputerManager->m_Lock);
if (existingComputer != nullptr) { NvComputer* existingComputer = m_ComputerManager->m_KnownHosts.value(newComputer->uuid);
// Fold it into the existing PC if (existingComputer != nullptr) {
bool changed = existingComputer->update(*newComputer); // Fold it into the existing PC
delete newComputer; bool changed = existingComputer->update(*newComputer);
delete newComputer;
// Drop the lock before notifying // Drop the lock before notifying
lock.unlock(); lock.unlock();
// For non-mDNS clients, let them know it succeeded // For non-mDNS clients, let them know it succeeded
if (!m_Mdns) { if (!m_Mdns) {
emit computerAddCompleted(true); 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 // Start polling if enabled (write lock required)
if (changed) { m_ComputerManager->startPollingComputer(newComputer);
qInfo() << existingComputer->name << "is now at" << existingComputer->activeAddress;
emit computerStateChanged(existingComputer); // 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; ComputerManager* m_ComputerManager;
+59
View File
@@ -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();
}
+33
View File
@@ -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;
};
+14 -1
View File
@@ -10,6 +10,7 @@
#define SER_REMOTEADDR "remoteaddress" #define SER_REMOTEADDR "remoteaddress"
#define SER_MANUALADDR "manualaddress" #define SER_MANUALADDR "manualaddress"
#define SER_APPLIST "apps" #define SER_APPLIST "apps"
#define SER_SRVCERT "srvcert"
#define SER_APPNAME "name" #define SER_APPNAME "name"
#define SER_APPID "id" #define SER_APPID "id"
@@ -23,6 +24,7 @@ NvComputer::NvComputer(QSettings& settings)
this->localAddress = settings.value(SER_LOCALADDR).toString(); this->localAddress = settings.value(SER_LOCALADDR).toString();
this->remoteAddress = settings.value(SER_REMOTEADDR).toString(); this->remoteAddress = settings.value(SER_REMOTEADDR).toString();
this->manualAddress = settings.value(SER_MANUALADDR).toString(); this->manualAddress = settings.value(SER_MANUALADDR).toString();
this->serverCert = QSslCertificate(settings.value(SER_SRVCERT).toByteArray());
int appCount = settings.beginReadArray(SER_APPLIST); int appCount = settings.beginReadArray(SER_APPLIST);
for (int i = 0; i < appCount; i++) { for (int i = 0; i < appCount; i++) {
@@ -61,6 +63,7 @@ void NvComputer::serialize(QSettings& settings)
settings.setValue(SER_LOCALADDR, localAddress); settings.setValue(SER_LOCALADDR, localAddress);
settings.setValue(SER_REMOTEADDR, remoteAddress); settings.setValue(SER_REMOTEADDR, remoteAddress);
settings.setValue(SER_MANUALADDR, manualAddress); settings.setValue(SER_MANUALADDR, manualAddress);
settings.setValue(SER_SRVCERT, serverCert.toPem());
// Avoid deleting an existing applist if we couldn't get one // Avoid deleting an existing applist if we couldn't get one
if (!appList.isEmpty()) { 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"); this->name = NvHTTP::getXmlString(serverInfo, "hostname");
if (this->name.isEmpty()) { if (this->name.isEmpty()) {
this->name = "UNKNOWN"; this->name = "UNKNOWN";
@@ -255,6 +260,13 @@ bool NvComputer::update(NvComputer& that)
changed = true; \ 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(name);
ASSIGN_IF_CHANGED_AND_NONEMPTY(macAddress); ASSIGN_IF_CHANGED_AND_NONEMPTY(macAddress);
ASSIGN_IF_CHANGED_AND_NONEMPTY(localAddress); ASSIGN_IF_CHANGED_AND_NONEMPTY(localAddress);
@@ -269,6 +281,7 @@ bool NvComputer::update(NvComputer& that)
ASSIGN_IF_CHANGED(appVersion); ASSIGN_IF_CHANGED(appVersion);
ASSIGN_IF_CHANGED(maxLumaPixelsHEVC); ASSIGN_IF_CHANGED(maxLumaPixelsHEVC);
ASSIGN_IF_CHANGED(gpuModel); ASSIGN_IF_CHANGED(gpuModel);
ASSIGN_IF_CHANGED_AND_NONNULL(serverCert);
ASSIGN_IF_CHANGED_AND_NONEMPTY(appList); ASSIGN_IF_CHANGED_AND_NONEMPTY(appList);
ASSIGN_IF_CHANGED_AND_NONEMPTY(displayModes); ASSIGN_IF_CHANGED_AND_NONEMPTY(displayModes);
return changed; return changed;
+2 -1
View File
@@ -19,7 +19,7 @@ private:
bool pendingQuit; bool pendingQuit;
public: public:
explicit NvComputer(QString address, QString serverInfo); explicit NvComputer(QString address, QString serverInfo, QSslCertificate serverCert);
explicit NvComputer(QSettings& settings); explicit NvComputer(QSettings& settings);
@@ -68,6 +68,7 @@ public:
QByteArray macAddress; QByteArray macAddress;
QString name; QString name;
QString uuid; QString uuid;
QSslCertificate serverCert;
QVector<NvApp> appList; QVector<NvApp> appList;
// Synchronization // Synchronization
+76 -28
View File
@@ -14,8 +14,9 @@
#define REQUEST_TIMEOUT_MS 5000 #define REQUEST_TIMEOUT_MS 5000
NvHTTP::NvHTTP(QString address) : NvHTTP::NvHTTP(QString address, QSslCertificate serverCert) :
m_Address(address) m_Address(address),
m_ServerCert(serverCert)
{ {
Q_ASSERT(!address.isEmpty()); Q_ASSERT(!address.isEmpty());
@@ -31,6 +32,11 @@ NvHTTP::NvHTTP(QString address) :
m_Nam.setProxy(noProxy); m_Nam.setProxy(noProxy);
} }
void NvHTTP::setServerCert(QSslCertificate serverCert)
{
m_ServerCert = serverCert;
}
QVector<int> QVector<int>
NvHTTP::parseQuad(QString quad) NvHTTP::parseQuad(QString quad)
{ {
@@ -73,36 +79,50 @@ NvHTTP::getServerInfo(NvLogLevel logLevel)
{ {
QString serverInfo; 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 try
// 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)
{ {
// Certificate validation error, fallback to HTTP // Always try HTTPS first, since it properly reports
serverInfo = openConnectionToString(m_BaseUrlHttp, // pairing status (and a few other attributes).
serverInfo = openConnectionToString(m_BaseUrlHttps,
"serverinfo", "serverinfo",
nullptr, nullptr,
true, true,
logLevel); logLevel);
// Throws if the request failed
verifyResponseStatus(serverInfo); verifyResponseStatus(serverInfo);
} }
else catch (const GfeHttpResponseException& e)
{ {
// Rethrow real errors if (e.getStatusCode() == 401)
throw e; {
// 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; return serverInfo;
} }
@@ -379,8 +399,12 @@ NvHTTP::openConnection(QUrl baseUrl,
// Build a URL for the request // Build a URL for the request
QUrl url(baseUrl); QUrl url(baseUrl);
url.setPath("/" + command); 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) : "")); ((arguments != nullptr) ? ("&" + arguments) : ""));
QNetworkRequest request = QNetworkRequest(url); QNetworkRequest request = QNetworkRequest(url);
@@ -390,8 +414,22 @@ NvHTTP::openConnection(QUrl baseUrl,
QNetworkReply* reply = m_Nam.get(request); QNetworkReply* reply = m_Nam.get(request);
// Ignore self-signed certificate errors (since GFE uses them) // Assert that we always have a server cert for HTTPS, since
reply->ignoreSslErrors(); // 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 // Run the request with a timeout if requested
QEventLoop loop; QEventLoop loop;
@@ -424,9 +462,19 @@ NvHTTP::openConnection(QUrl baseUrl,
if (logLevel >= NvLogLevel::ERROR) { if (logLevel >= NvLogLevel::ERROR) {
qWarning() << command << " request failed with error " << reply->error(); qWarning() << command << " request failed with error " << reply->error();
} }
QtNetworkReplyException exception(reply->error(), reply->errorString());
delete reply; if (reply->error() == QNetworkReply::SslHandshakeFailedError) {
throw exception; // 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; return reply;
+4 -1
View File
@@ -122,7 +122,7 @@ public:
VERBOSE VERBOSE
}; };
explicit NvHTTP(QString address); explicit NvHTTP(QString address, QSslCertificate serverCert);
static static
int int
@@ -152,6 +152,8 @@ public:
bool enableTimeout, bool enableTimeout,
NvLogLevel logLevel = NvLogLevel::VERBOSE); NvLogLevel logLevel = NvLogLevel::VERBOSE);
void setServerCert(QSslCertificate serverCert);
static static
QVector<int> QVector<int>
parseQuad(QString quad); parseQuad(QString quad);
@@ -191,4 +193,5 @@ private:
QString m_Address; QString m_Address;
QNetworkAccessManager m_Nam; QNetworkAccessManager m_Nam;
QSslCertificate m_ServerCert;
}; };
+18 -6
View File
@@ -9,7 +9,7 @@
#include <openssl/evp.h> #include <openssl/evp.h>
NvPairingManager::NvPairingManager(QString address) : NvPairingManager::NvPairingManager(QString address) :
m_Http(address) m_Http(address, QSslCertificate())
{ {
QByteArray cert = IdentityManager::get()->getCertificate(); QByteArray cert = IdentityManager::get()->getCertificate();
BIO *bio = BIO_new_mem_buf(cert.data(), -1); BIO *bio = BIO_new_mem_buf(cert.data(), -1);
@@ -161,7 +161,7 @@ NvPairingManager::saltPin(QByteArray salt, QString pin)
} }
NvPairingManager::PairState NvPairingManager::PairState
NvPairingManager::pair(QString appVersion, QString pin) NvPairingManager::pair(QString appVersion, QString pin, QSslCertificate& serverCert)
{ {
int serverMajorVersion = NvHTTP::parseQuad(appVersion).at(0); int serverMajorVersion = NvHTTP::parseQuad(appVersion).at(0);
qInfo() << "Pairing with server generation:" << serverMajorVersion; qInfo() << "Pairing with server generation:" << serverMajorVersion;
@@ -200,14 +200,26 @@ NvPairingManager::pair(QString appVersion, QString pin)
return PairState::FAILED; return PairState::FAILED;
} }
QByteArray serverCert = NvHTTP::getXmlStringFromHex(getCert, "plaincert"); QByteArray serverCertStr = NvHTTP::getXmlStringFromHex(getCert, "plaincert");
if (serverCert == nullptr) if (serverCertStr == nullptr)
{ {
qCritical() << "Server likely already pairing"; qCritical() << "Server likely already pairing";
m_Http.openConnectionToString(m_Http.m_BaseUrlHttp, "unpair", nullptr, true); m_Http.openConnectionToString(m_Http.m_BaseUrlHttp, "unpair", nullptr, true);
return PairState::ALREADY_IN_PROGRESS; 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 randomChallenge = generateRandomBytes(16);
QByteArray encryptedChallenge = encrypt(randomChallenge, &encKey); QByteArray encryptedChallenge = encrypt(randomChallenge, &encKey);
QString challengeXml = m_Http.openConnectionToString(m_Http.m_BaseUrlHttp, QString challengeXml = m_Http.openConnectionToString(m_Http.m_BaseUrlHttp,
@@ -262,7 +274,7 @@ NvPairingManager::pair(QString appVersion, QString pin)
if (!verifySignature(serverSecret, if (!verifySignature(serverSecret,
serverSignature, serverSignature,
serverCert)) serverCertStr))
{ {
qCritical() << "MITM detected"; qCritical() << "MITM detected";
m_Http.openConnectionToString(m_Http.m_BaseUrlHttp, "unpair", nullptr, true); m_Http.openConnectionToString(m_Http.m_BaseUrlHttp, "unpair", nullptr, true);
@@ -271,7 +283,7 @@ NvPairingManager::pair(QString appVersion, QString pin)
QByteArray expectedResponseData; QByteArray expectedResponseData;
expectedResponseData.append(randomChallenge); expectedResponseData.append(randomChallenge);
expectedResponseData.append(getSignatureFromPemCert(serverCert)); expectedResponseData.append(getSignatureFromPemCert(serverCertStr));
expectedResponseData.append(serverSecret); expectedResponseData.append(serverSecret);
if (QCryptographicHash::hash(expectedResponseData, hashAlgo) != serverResponse) if (QCryptographicHash::hash(expectedResponseData, hashAlgo) != serverResponse)
{ {
+1 -1
View File
@@ -23,7 +23,7 @@ public:
~NvPairingManager(); ~NvPairingManager();
PairState PairState
pair(QString appVersion, QString pin); pair(QString appVersion, QString pin, QSslCertificate& serverCert);
private: private:
QByteArray QByteArray
+53 -1
View File
@@ -164,6 +164,7 @@ GlobalCommandLineParser::ParseResult GlobalCommandLineParser::parse(const QStrin
"Starts Moonlight normally if no arguments are given.\n" "Starts Moonlight normally if no arguments are given.\n"
"\n" "\n"
"Available actions:\n" "Available actions:\n"
" quit Quit the currently running app\n"
" stream Start streaming an app\n" " stream Start streaming an app\n"
"\n" "\n"
"See 'moonlight <action> --help' for help of specific action." "See 'moonlight <action> --help' for help of specific action."
@@ -179,6 +180,8 @@ GlobalCommandLineParser::ParseResult GlobalCommandLineParser::parse(const QStrin
parser.handleHelpAndVersionOptions(); parser.handleHelpAndVersionOptions();
parser.handleUnknownOptions(); parser.handleUnknownOptions();
return NormalStartRequested; return NormalStartRequested;
} else if (action == "quit") {
return QuitRequested;
} else if (action == "stream") { } else if (action == "stream") {
return StreamRequested; return StreamRequested;
} else { } 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() StreamCommandLineParser::StreamCommandLineParser()
{ {
m_WindowModeMap = { m_WindowModeMap = {
@@ -238,9 +283,11 @@ void StreamCommandLineParser::parse(const QStringList &args, StreamingPreference
parser.addChoiceOption("display-mode", "display mode", m_WindowModeMap.keys()); parser.addChoiceOption("display-mode", "display mode", m_WindowModeMap.keys());
parser.addChoiceOption("audio-config", "audio config", m_AudioConfigMap.keys()); parser.addChoiceOption("audio-config", "audio config", m_AudioConfigMap.keys());
parser.addToggleOption("multi-controller", "multiple controller support"); parser.addToggleOption("multi-controller", "multiple controller support");
parser.addToggleOption("quit-after", "quit app after session");
parser.addToggleOption("mouse-acceleration", "mouse acceleration"); parser.addToggleOption("mouse-acceleration", "mouse acceleration");
parser.addToggleOption("game-optimization", "game optimizations"); parser.addToggleOption("game-optimization", "game optimizations");
parser.addToggleOption("audio-on-host", "audio on host PC"); 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-codec", "video codec", m_VideoCodecMap.keys());
parser.addChoiceOption("video-decoder", "video decoder", m_VideoDecoderMap.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 // Resolve --multi-controller and --no-multi-controller options
preferences->multiController = parser.getToggleOptionValue("multi-controller", preferences->multiController); 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 // Resolve --mouse-acceleration and --no-mouse-acceleration options
preferences->mouseAcceleration = parser.getToggleOptionValue("mouse-acceleration", preferences->mouseAcceleration); 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 // Resolve --audio-on-host and --no-audio-on-host options
preferences->playAudioOnHost = parser.getToggleOptionValue("audio-on-host", preferences->playAudioOnHost); 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 // Resolve --video-codec option
if (parser.isSet("video-codec")) { if (parser.isSet("video-codec")) {
preferences->videoCodecConfig = mapValue(m_VideoCodecMap, parser.getChoiceOptionValue("video-codec")); preferences->videoCodecConfig = mapValue(m_VideoCodecMap, parser.getChoiceOptionValue("video-codec"));
@@ -359,4 +412,3 @@ QString StreamCommandLineParser::getAppName() const
{ {
return m_AppName; return m_AppName;
} }
+15
View File
@@ -11,6 +11,7 @@ public:
enum ParseResult { enum ParseResult {
NormalStartRequested, NormalStartRequested,
StreamRequested, StreamRequested,
QuitRequested,
}; };
GlobalCommandLineParser(); 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 class StreamCommandLineParser
{ {
public: public:
+171
View File
@@ -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);
}
}
+40
View File
@@ -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
View File
@@ -1,7 +1,9 @@
#include "startstream.h" #include "startstream.h"
#include "backend/computermanager.h" #include "backend/computermanager.h"
#include "backend/computerseeker.h"
#include "streaming/session.h" #include "streaming/session.h"
#include <QCoreApplication>
#include <QTimer> #include <QTimer>
#define COMPUTER_SEEK_TIMEOUT 10000 #define COMPUTER_SEEK_TIMEOUT 10000
@@ -22,6 +24,9 @@ class Event
{ {
public: public:
enum Type { enum Type {
AppQuitCompleted,
AppQuitRequested,
ComputerFound,
ComputerUpdated, ComputerUpdated,
Executed, Executed,
Timedout, Timedout,
@@ -33,6 +38,7 @@ public:
Type type; Type type;
ComputerManager *computerManager; ComputerManager *computerManager;
NvComputer *computer; NvComputer *computer;
QString errorMessage;
}; };
class LauncherPrivate class LauncherPrivate
@@ -49,58 +55,78 @@ public:
NvApp app; NvApp app;
switch (event.type) { switch (event.type) {
// Occurs when CliStartStreamSegue becomes visible and the UI calls launcher's execute()
case Event::Executed: case Event::Executed:
if (m_State == StateInit) { if (m_State == StateInit) {
m_State = StateSeekComputer; m_State = StateSeekComputer;
m_TimeoutTimer->start(COMPUTER_SEEK_TIMEOUT);
m_ComputerManager = event.computerManager; 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->connect(m_ComputerManager, &ComputerManager::computerStateChanged,
q, &Launcher::onComputerUpdated); q, &Launcher::onComputerUpdated);
// Seek desired computer by both connecting to it directly (this may fail q->connect(m_ComputerManager, &ComputerManager::quitAppCompleted,
// if m_ComputerName is UUID, or the name that doesn't resolve to an IP q, &Launcher::onQuitAppCompleted);
// 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();
emit q->searchingComputer(); emit q->searchingComputer();
} }
break; break;
case Event::ComputerUpdated: // Occurs when searched computer is found
case Event::ComputerFound:
if (m_State == StateSeekComputer) { if (m_State == StateSeekComputer) {
if (matchComputer(event.computer) && isOnline(event.computer)) { if (event.computer->pairState == NvComputer::PS_PAIRED) {
if (isPaired(event.computer)) { m_State = StateSeekApp;
m_State = StateSeekApp; m_Computer = event.computer;
m_TimeoutTimer->start(APP_SEEK_TIMEOUT); m_TimeoutTimer->start(APP_SEEK_TIMEOUT);
m_Computer = event.computer; emit q->searchingApp();
m_ComputerManager->stopPollingAsync(); } else {
emit q->searchingApp(); m_State = StateFailure;
} else { QString msg = QString("Computer %1 has not been paired. "
m_State = StateFailure; "Please open Moonlight to pair before streaming.")
QString msg = QString("Computer %1 has not been paired. " .arg(event.computer->name);
"Please open Moonlight to pair before streaming.") emit q->failed(msg);
.arg(event.computer->name);
emit q->failed(msg);
}
} }
} }
break;
// Occurs when a computer is updated
case Event::ComputerUpdated:
if (m_State == StateSeekApp) { if (m_State == StateSeekApp) {
int index = getAppIndex(); int index = getAppIndex();
if (-1 != index) { if (-1 != index) {
app = m_Computer->appList[index]; app = m_Computer->appList[index];
m_TimeoutTimer->stop();
if (isNotStreaming() || isStreamingApp(app)) { if (isNotStreaming() || isStreamingApp(app)) {
m_State = StateStartSession; m_State = StateStartSession;
m_TimeoutTimer->stop();
session = new Session(m_Computer, app, m_Preferences); session = new Session(m_Computer, app, m_Preferences);
emit q->sessionCreated(app.name, session); emit q->sessionCreated(app.name, session);
} else { } else {
m_State = StateFailure; emit q->appQuitRequired(getCurrentAppName());
QString msg = QString("%1 is already running. Please quit %1 to stream %2.")
.arg(getCurrentAppName(), app.name);
emit q->failed(msg);
} }
} }
} }
break; 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: case Event::Timedout:
if (m_State == StateSeekComputer) { if (m_State == StateSeekComputer) {
m_State = StateFailure; 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 int getAppIndex() const
{ {
for (int i = 0; i < m_Computer->appList.length(); i++) { for (int i = 0; i < m_Computer->appList.length(); i++) {
@@ -169,6 +175,7 @@ public:
QString m_AppName; QString m_AppName;
StreamingPreferences *m_Preferences; StreamingPreferences *m_Preferences;
ComputerManager *m_ComputerManager; ComputerManager *m_ComputerManager;
ComputerSeeker *m_ComputerSeeker;
NvComputer *m_Computer; NvComputer *m_Computer;
State m_State; State m_State;
QTimer *m_TimeoutTimer; QTimer *m_TimeoutTimer;
@@ -202,6 +209,27 @@ void Launcher::execute(ComputerManager *manager)
d->handleEvent(event); 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) void Launcher::onComputerUpdated(NvComputer *computer)
{ {
Q_D(Launcher); Q_D(Launcher);
@@ -217,4 +245,12 @@ void Launcher::onTimeout()
d->handleEvent(event); d->handleEvent(event);
} }
void Launcher::onQuitAppCompleted(QVariant error)
{
Q_D(Launcher);
Event event(Event::AppQuitCompleted);
event.errorMessage = error.toString();
d->handleEvent(event);
}
} }
+6
View File
@@ -1,6 +1,7 @@
#pragma once #pragma once
#include <QObject> #include <QObject>
#include <QVariant>
class ComputerManager; class ComputerManager;
class NvComputer; class NvComputer;
@@ -24,16 +25,21 @@ public:
QObject *parent = nullptr); QObject *parent = nullptr);
~Launcher(); ~Launcher();
Q_INVOKABLE void execute(ComputerManager *manager); Q_INVOKABLE void execute(ComputerManager *manager);
Q_INVOKABLE void quitRunningApp();
Q_INVOKABLE bool isExecuted() const;
signals: signals:
void searchingComputer(); void searchingComputer();
void searchingApp(); void searchingApp();
void sessionCreated(QString appName, Session *session); void sessionCreated(QString appName, Session *session);
void failed(QString text); void failed(QString text);
void appQuitRequired(QString appName);
private slots: private slots:
void onComputerFound(NvComputer *computer);
void onComputerUpdated(NvComputer *computer); void onComputerUpdated(NvComputer *computer);
void onTimeout(); void onTimeout();
void onQuitAppCompleted(QVariant error);
private: private:
QScopedPointer<LauncherPrivate> m_DPtr; QScopedPointer<LauncherPrivate> m_DPtr;
@@ -4,7 +4,7 @@
<metadata_license>CC0-1.0</metadata_license> <metadata_license>CC0-1.0</metadata_license>
<project_license>GPL-3.0+</project_license> <project_license>GPL-3.0+</project_license>
<name>Moonlight</name> <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> <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> <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> </ul>
</description> </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="bugtracker">https://github.com/moonlight-stream/moonlight-qt/issues</url>
<url type="help">https://github.com/moonlight-stream/moonlight-docs/wiki/Setup-Guide</url> <url type="help">https://github.com/moonlight-stream/moonlight-docs/wiki/Setup-Guide</url>
<screenshots> <screenshots>
<screenshot type="default"> <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>
<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> </screenshot>
</screenshots> </screenshots>
<releases> <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"> <release version="0.6.4" date="2018-11-20">
<description> <description>
<p>New features:</p> <p>New features:</p>
@@ -214,7 +248,7 @@
</categories> </categories>
<developer_name>Moonlight Game Streaming Team</developer_name> <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> <launchable type="desktop-id">com.moonlight_stream.Moonlight.desktop</launchable>
+1 -2
View File
@@ -89,10 +89,9 @@ GridView {
} }
} }
Text { Label {
id: appNameText id: appNameText
text: model.name text: model.name
color: "white"
width: parent.width width: parent.width
height: 125 height: 125
anchors.top: appIcon.bottom anchors.top: appIcon.bottom
+70
View File
@@ -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()
}
}
}
}
+48 -2
View File
@@ -1,3 +1,4 @@
import QtQml 2.2
import QtQuick 2.0 import QtQuick 2.0
import QtQuick.Controls 2.2 import QtQuick.Controls 2.2
import QtQuick.Dialogs 1.2 import QtQuick.Dialogs 1.2
@@ -30,13 +31,19 @@ Item {
errorDialog.open() errorDialog.open()
} }
function onAppQuitRequired(appName) {
quitAppDialog.appName = appName
quitAppDialog.open()
}
onVisibleChanged: { onVisibleChanged: {
if (visible) { if (visible && !launcher.isExecuted()) {
toolBar.visible = false toolBar.visible = false
launcher.searchingComputer.connect(onSearchingComputer) launcher.searchingComputer.connect(onSearchingComputer)
launcher.searchingApp.connect(onSearchingApp) launcher.searchingApp.connect(onSearchingApp)
launcher.sessionCreated.connect(onSessionCreated) launcher.sessionCreated.connect(onSessionCreated)
launcher.failed.connect(onLaunchFailed) launcher.failed.connect(onLaunchFailed)
launcher.appQuitRequired.connect(onAppQuitRequired)
launcher.execute(ComputerManager) launcher.execute(ComputerManager)
} }
} }
@@ -56,7 +63,6 @@ Item {
verticalAlignment: Text.AlignVCenter verticalAlignment: Text.AlignVCenter
wrapMode: Text.Wrap 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
View File
@@ -124,10 +124,9 @@ GridView {
visible: !model.addPc && model.statusUnknown visible: !model.addPc && model.statusUnknown
} }
Text { Label {
id: pcNameText id: pcNameText
text: model.name text: model.name
color: "white"
width: parent.width width: parent.width
anchors.top: pcIcon.bottom anchors.top: pcIcon.bottom
-1
View File
@@ -65,7 +65,6 @@ Item {
verticalAlignment: Text.AlignVCenter verticalAlignment: Text.AlignVCenter
wrapMode: Text.Wrap wrapMode: Text.Wrap
color: "white"
} }
} }
+52 -23
View File
@@ -73,7 +73,6 @@ Flickable {
text: qsTr("Resolution and FPS") text: qsTr("Resolution and FPS")
font.pointSize: 12 font.pointSize: 12
wrapMode: Text.Wrap wrapMode: Text.Wrap
color: "white"
} }
Label { Label {
@@ -82,7 +81,6 @@ Flickable {
text: qsTr("Setting values too high for your PC or network connection may cause lag, stuttering, or errors.") text: qsTr("Setting values too high for your PC or network connection may cause lag, stuttering, or errors.")
font.pointSize: 9 font.pointSize: 9
wrapMode: Text.Wrap wrapMode: Text.Wrap
color: "white"
} }
Row { Row {
@@ -297,16 +295,14 @@ Flickable {
text: qsTr("Video bitrate: ") text: qsTr("Video bitrate: ")
font.pointSize: 12 font.pointSize: 12
wrapMode: Text.Wrap wrapMode: Text.Wrap
color: "white"
} }
Label { Label {
width: parent.width width: parent.width
id: bitrateDesc 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 font.pointSize: 9
wrapMode: Text.Wrap wrapMode: Text.Wrap
color: "white"
} }
Slider { Slider {
@@ -326,6 +322,11 @@ Flickable {
bitrateTitle.text = "Video bitrate: " + (value / 1000.0) + " Mbps" bitrateTitle.text = "Video bitrate: " + (value / 1000.0) + " Mbps"
prefs.bitrateKbps = value 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 { Label {
@@ -334,7 +335,6 @@ Flickable {
text: qsTr("Display mode") text: qsTr("Display mode")
font.pointSize: 12 font.pointSize: 12
wrapMode: Text.Wrap wrapMode: Text.Wrap
color: "white"
} }
AutoResizingComboBox { AutoResizingComboBox {
@@ -383,7 +383,7 @@ Flickable {
CheckBox { CheckBox {
id: vsyncCheck id: vsyncCheck
hoverEnabled: true hoverEnabled: true
text: "<font color=\"white\">Enable V-Sync</font>" text: "V-Sync"
font.pointSize: 12 font.pointSize: 12
checked: prefs.enableVsync checked: prefs.enableVsync
onCheckedChanged: { onCheckedChanged: {
@@ -395,6 +395,22 @@ Flickable {
ToolTip.visible: hovered ToolTip.visible: hovered
ToolTip.text: "Disabling V-Sync allows sub-frame rendering latency, but it can display visible tearing" 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") text: qsTr("Audio configuration")
font.pointSize: 12 font.pointSize: 12
wrapMode: Text.Wrap wrapMode: Text.Wrap
color: "white"
} }
AutoResizingComboBox { AutoResizingComboBox {
@@ -468,12 +483,12 @@ Flickable {
spacing: 5 spacing: 5
CheckBox { CheckBox {
id: startWindowedCheck id: startMaximizedCheck
text: "<font color=\"white\">Start Moonlight in windowed mode</font>" text: "Maximize Moonlight window on startup"
font.pointSize: 12 font.pointSize: 12
checked: prefs.startWindowed checked: !prefs.startWindowed
onCheckedChanged: { onCheckedChanged: {
prefs.startWindowed = checked prefs.startWindowed = !checked
} }
} }
} }
@@ -498,19 +513,25 @@ Flickable {
spacing: 5 spacing: 5
CheckBox { CheckBox {
id: multiControllerCheck id: singleControllerCheck
text: "<font color=\"white\">Multiple controller support</font>" text: "Force gamepad #1 always present"
font.pointSize: 12 font.pointSize: 12
checked: prefs.multiController checked: !prefs.multiController
onCheckedChanged: { 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 { CheckBox {
id: rawInputCheck id: rawInputCheck
hoverEnabled: true hoverEnabled: true
text: "<font color=\"white\">Raw mouse input</font>" text: "Raw mouse input"
font.pointSize: 12 font.pointSize: 12
checked: !prefs.mouseAcceleration checked: !prefs.mouseAcceleration
onCheckedChanged: { onCheckedChanged: {
@@ -538,7 +559,7 @@ Flickable {
CheckBox { CheckBox {
id: optimizeGameSettingsCheck id: optimizeGameSettingsCheck
text: "<font color=\"white\">Optimize game settings</font>" text: "Optimize game settings"
font.pointSize: 12 font.pointSize: 12
checked: prefs.gameOptimizations checked: prefs.gameOptimizations
onCheckedChanged: { onCheckedChanged: {
@@ -548,7 +569,7 @@ Flickable {
CheckBox { CheckBox {
id: audioPcCheck id: audioPcCheck
text: "<font color=\"white\">Play audio on host PC</font>" text: "Play audio on host PC"
font.pointSize: 12 font.pointSize: 12
checked: prefs.playAudioOnHost checked: prefs.playAudioOnHost
onCheckedChanged: { onCheckedChanged: {
@@ -575,7 +596,6 @@ Flickable {
text: qsTr("Video decoder") text: qsTr("Video decoder")
font.pointSize: 12 font.pointSize: 12
wrapMode: Text.Wrap wrapMode: Text.Wrap
color: "white"
} }
AutoResizingComboBox { AutoResizingComboBox {
@@ -622,7 +642,6 @@ Flickable {
text: qsTr("Video codec") text: qsTr("Video codec")
font.pointSize: 12 font.pointSize: 12
wrapMode: Text.Wrap wrapMode: Text.Wrap
color: "white"
} }
AutoResizingComboBox { AutoResizingComboBox {
@@ -672,7 +691,7 @@ Flickable {
CheckBox { CheckBox {
id: unlockUnsupportedFps id: unlockUnsupportedFps
text: "<font color=\"white\">Unlock unsupported FPS options</font>" text: "Unlock unsupported FPS options"
font.pointSize: 12 font.pointSize: 12
checked: prefs.unsupportedFps checked: prefs.unsupportedFps
onCheckedChanged: { onCheckedChanged: {
@@ -686,7 +705,7 @@ Flickable {
CheckBox { CheckBox {
id: enableMdns 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 font.pointSize: 12
checked: prefs.enableMdns checked: prefs.enableMdns
onCheckedChanged: { 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
View File
@@ -3,6 +3,7 @@ import QtQuick.Controls 2.2
import QtQuick.Dialogs 1.2 import QtQuick.Dialogs 1.2
import QtQuick.Window 2.2 import QtQuick.Window 2.2
import ComputerManager 1.0
import SdlGamepadKeyNavigation 1.0 import SdlGamepadKeyNavigation 1.0
import Session 1.0 import Session 1.0
@@ -59,6 +60,44 @@ Item {
toast.visible = true 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 // It's important that we don't call enable() here
// or it may interfere with the Session instance // or it may interfere with the Session instance
// getting notified of initial connected gamepads. // getting notified of initial connected gamepads.
@@ -85,29 +124,13 @@ Item {
session.connectionStarted.connect(connectionStarted) session.connectionStarted.connect(connectionStarted)
session.displayLaunchError.connect(displayLaunchError) session.displayLaunchError.connect(displayLaunchError)
session.displayLaunchWarning.connect(displayLaunchWarning) session.displayLaunchWarning.connect(displayLaunchWarning)
session.quitStarting.connect(quitStarting)
session.sessionFinished.connect(sessionFinished)
// Run the streaming session to completion // Run the streaming session to completion
session.exec(Screen.virtualX, Screen.virtualY) 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 // Show the toolbar again when we become hidden
toolBar.visible = true toolBar.visible = true
} }
@@ -129,7 +152,6 @@ Item {
verticalAlignment: Text.AlignVCenter verticalAlignment: Text.AlignVCenter
wrapMode: Text.Wrap wrapMode: Text.Wrap
color: "white"
} }
} }
@@ -142,7 +164,6 @@ Item {
verticalAlignment: Text.AlignVCenter verticalAlignment: Text.AlignVCenter
wrapMode: Text.Wrap wrapMode: Text.Wrap
color: "white"
} }
MessageDialog { MessageDialog {
@@ -150,8 +171,19 @@ Item {
modality:Qt.WindowModal modality:Qt.WindowModal
icon: StandardIcon.Critical icon: StandardIcon.Critical
standardButtons: StandardButton.Ok | StandardButton.Help standardButtons: StandardButton.Ok | StandardButton.Help
onAccepted: {
if (quitAfter) {
Qt.quit()
}
}
onHelp: { onHelp: {
Qt.openUrlExternally("https://github.com/moonlight-stream/moonlight-docs/wiki/Troubleshooting"); Qt.openUrlExternally("https://github.com/moonlight-stream/moonlight-docs/wiki/Troubleshooting");
if (quitAfter) {
Qt.quit()
}
} }
} }
} }
+25
View File
@@ -21,6 +21,7 @@
#include "antihookingprotection.h" #include "antihookingprotection.h"
#endif #endif
#include "cli/quitstream.h"
#include "cli/startstream.h" #include "cli/startstream.h"
#include "cli/commandlineparser.h" #include "cli/commandlineparser.h"
#include "path.h" #include "path.h"
@@ -135,6 +136,14 @@ void qtLogToDiskHandler(QtMsgType type, const QMessageLogContext&, const QString
break; 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()); QTime logTime = QTime::fromMSecsSinceStartOfDay(s_LoggerTime.elapsed());
QString txt = QString("%1 - Qt %2: %3\n").arg(logTime.toString()).arg(typeTxt).arg(msg); 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[]) 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. // Set these here to allow us to use the default QSettings constructor.
// These also ensure that our cache directory is named correctly. As such, // These also ensure that our cache directory is named correctly. As such,
// it is critical that these be called before Path::initialize(). // 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); engine.rootContext()->setContextProperty("launcher", launcher);
break; 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); engine.rootContext()->setContextProperty("initialView", initialView);
@@ -369,5 +390,9 @@ int main(int argc, char *argv[])
int err = app.exec(); 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; return err;
} }
+1
View File
@@ -10,6 +10,7 @@
<file>gui/NavigableToolButton.qml</file> <file>gui/NavigableToolButton.qml</file>
<file>gui/NavigableItemDelegate.qml</file> <file>gui/NavigableItemDelegate.qml</file>
<file>gui/NavigableMenuItem.qml</file> <file>gui/NavigableMenuItem.qml</file>
<file>gui/CliQuitStreamSegue.qml</file>
<file>gui/CliStartStreamSegue.qml</file> <file>gui/CliStartStreamSegue.qml</file>
<file>gui/AutoResizingComboBox.qml</file> <file>gui/AutoResizingComboBox.qml</file>
</qresource> </qresource>
+1 -1
View File
@@ -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" /> <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> </svg>

Before

Width:  |  Height:  |  Size: 499 B

After

Width:  |  Height:  |  Size: 499 B

+6
View File
@@ -20,8 +20,10 @@
#define SER_WINDOWMODE "windowmode" #define SER_WINDOWMODE "windowmode"
#define SER_UNSUPPORTEDFPS "unsupportedfps" #define SER_UNSUPPORTEDFPS "unsupportedfps"
#define SER_MDNS "mdns" #define SER_MDNS "mdns"
#define SER_QUITAPPAFTER "quitAppAfter"
#define SER_MOUSEACCELERATION "mouseacceleration" #define SER_MOUSEACCELERATION "mouseacceleration"
#define SER_STARTWINDOWED "startwindowed" #define SER_STARTWINDOWED "startwindowed"
#define SER_FRAMEPACING "framepacing"
StreamingPreferences::StreamingPreferences(QObject *parent) StreamingPreferences::StreamingPreferences(QObject *parent)
: QObject(parent) : QObject(parent)
@@ -43,8 +45,10 @@ void StreamingPreferences::reload()
multiController = settings.value(SER_MULTICONT, true).toBool(); multiController = settings.value(SER_MULTICONT, true).toBool();
unsupportedFps = settings.value(SER_UNSUPPORTEDFPS, false).toBool(); unsupportedFps = settings.value(SER_UNSUPPORTEDFPS, false).toBool();
enableMdns = settings.value(SER_MDNS, true).toBool(); enableMdns = settings.value(SER_MDNS, true).toBool();
quitAppAfter = settings.value(SER_QUITAPPAFTER, false).toBool();
mouseAcceleration = settings.value(SER_MOUSEACCELERATION, false).toBool(); mouseAcceleration = settings.value(SER_MOUSEACCELERATION, false).toBool();
startWindowed = settings.value(SER_STARTWINDOWED, false).toBool(); startWindowed = settings.value(SER_STARTWINDOWED, false).toBool();
framePacing = settings.value(SER_FRAMEPACING, false).toBool();
audioConfig = static_cast<AudioConfig>(settings.value(SER_AUDIOCFG, audioConfig = static_cast<AudioConfig>(settings.value(SER_AUDIOCFG,
static_cast<int>(AudioConfig::AC_STEREO)).toInt()); static_cast<int>(AudioConfig::AC_STEREO)).toInt());
videoCodecConfig = static_cast<VideoCodecConfig>(settings.value(SER_VIDEOCFG, videoCodecConfig = static_cast<VideoCodecConfig>(settings.value(SER_VIDEOCFG,
@@ -71,8 +75,10 @@ void StreamingPreferences::save()
settings.setValue(SER_MULTICONT, multiController); settings.setValue(SER_MULTICONT, multiController);
settings.setValue(SER_UNSUPPORTEDFPS, unsupportedFps); settings.setValue(SER_UNSUPPORTEDFPS, unsupportedFps);
settings.setValue(SER_MDNS, enableMdns); settings.setValue(SER_MDNS, enableMdns);
settings.setValue(SER_QUITAPPAFTER, quitAppAfter);
settings.setValue(SER_MOUSEACCELERATION, mouseAcceleration); settings.setValue(SER_MOUSEACCELERATION, mouseAcceleration);
settings.setValue(SER_STARTWINDOWED, startWindowed); settings.setValue(SER_STARTWINDOWED, startWindowed);
settings.setValue(SER_FRAMEPACING, framePacing);
settings.setValue(SER_AUDIOCFG, static_cast<int>(audioConfig)); settings.setValue(SER_AUDIOCFG, static_cast<int>(audioConfig));
settings.setValue(SER_VIDEOCFG, static_cast<int>(videoCodecConfig)); settings.setValue(SER_VIDEOCFG, static_cast<int>(videoCodecConfig));
settings.setValue(SER_VIDEODEC, static_cast<int>(videoDecoderSelection)); settings.setValue(SER_VIDEODEC, static_cast<int>(videoDecoderSelection));
+6
View File
@@ -73,8 +73,10 @@ public:
Q_PROPERTY(bool multiController MEMBER multiController NOTIFY multiControllerChanged) Q_PROPERTY(bool multiController MEMBER multiController NOTIFY multiControllerChanged)
Q_PROPERTY(bool unsupportedFps MEMBER unsupportedFps NOTIFY unsupportedFpsChanged) Q_PROPERTY(bool unsupportedFps MEMBER unsupportedFps NOTIFY unsupportedFpsChanged)
Q_PROPERTY(bool enableMdns MEMBER enableMdns NOTIFY enableMdnsChanged) 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 mouseAcceleration MEMBER mouseAcceleration NOTIFY mouseAccelerationChanged)
Q_PROPERTY(bool startWindowed MEMBER startWindowed NOTIFY startWindowedChanged) 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(AudioConfig audioConfig MEMBER audioConfig NOTIFY audioConfigChanged)
Q_PROPERTY(VideoCodecConfig videoCodecConfig MEMBER videoCodecConfig NOTIFY videoCodecConfigChanged) Q_PROPERTY(VideoCodecConfig videoCodecConfig MEMBER videoCodecConfig NOTIFY videoCodecConfigChanged)
Q_PROPERTY(VideoDecoderSelection videoDecoderSelection MEMBER videoDecoderSelection NOTIFY videoDecoderSelectionChanged) Q_PROPERTY(VideoDecoderSelection videoDecoderSelection MEMBER videoDecoderSelection NOTIFY videoDecoderSelectionChanged)
@@ -91,8 +93,10 @@ public:
bool multiController; bool multiController;
bool unsupportedFps; bool unsupportedFps;
bool enableMdns; bool enableMdns;
bool quitAppAfter;
bool mouseAcceleration; bool mouseAcceleration;
bool startWindowed; bool startWindowed;
bool framePacing;
AudioConfig audioConfig; AudioConfig audioConfig;
VideoCodecConfig videoCodecConfig; VideoCodecConfig videoCodecConfig;
VideoDecoderSelection videoDecoderSelection; VideoDecoderSelection videoDecoderSelection;
@@ -107,11 +111,13 @@ signals:
void multiControllerChanged(); void multiControllerChanged();
void unsupportedFpsChanged(); void unsupportedFpsChanged();
void enableMdnsChanged(); void enableMdnsChanged();
void quitAppAfterChanged();
void mouseAccelerationChanged(); void mouseAccelerationChanged();
void audioConfigChanged(); void audioConfigChanged();
void videoCodecConfigChanged(); void videoCodecConfigChanged();
void videoDecoderSelectionChanged(); void videoDecoderSelectionChanged();
void windowModeChanged(); void windowModeChanged();
void startWindowedChanged(); void startWindowedChanged();
void framePacingChanged();
}; };
@@ -4,12 +4,28 @@
#include <QtGlobal> #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() SoundIoAudioRenderer::SoundIoAudioRenderer()
: m_OpusChannelCount(0), : m_OpusChannelCount(0),
m_SoundIo(nullptr), m_SoundIo(nullptr),
m_Device(nullptr), m_Device(nullptr),
m_OutputStream(nullptr), m_OutputStream(nullptr),
m_RingBuffer(nullptr), m_RingBuffer(nullptr),
m_Latency(0),
m_Errored(false) m_Errored(false)
{ {
@@ -17,6 +33,10 @@ SoundIoAudioRenderer::SoundIoAudioRenderer()
SoundIoAudioRenderer::~SoundIoAudioRenderer() SoundIoAudioRenderer::~SoundIoAudioRenderer()
{ {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Audio latency: %f",
m_Latency);
if (m_OutputStream != nullptr) { if (m_OutputStream != nullptr) {
soundio_outstream_destroy(m_OutputStream); soundio_outstream_destroy(m_OutputStream);
} }
@@ -152,17 +172,12 @@ bool SoundIoAudioRenderer::prepareForPlayback(const OPUS_MULTISTREAM_CONFIGURATI
m_OutputStream->format = SoundIoFormatS16NE; m_OutputStream->format = SoundIoFormatS16NE;
m_OutputStream->sample_rate = opusConfig->sampleRate; m_OutputStream->sample_rate = opusConfig->sampleRate;
m_OutputStream->software_latency = k_MinSampleLengthSec;
m_OutputStream->name = "Moonlight"; m_OutputStream->name = "Moonlight";
m_OutputStream->userdata = this; m_OutputStream->userdata = this;
m_OutputStream->error_callback = sioErrorCallback; m_OutputStream->error_callback = sioErrorCallback;
m_OutputStream->write_callback = sioWriteCallback; 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; SoundIoChannelLayout bestLayout = m_Device->current_layout;
for (int i = 0; i < m_Device->layout_count; i++) { for (int i = 0; i < m_Device->layout_count; i++) {
if (scoreChannelLayout(&bestLayout, opusConfig) < 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 int packetsToBuffer;
// out network packet delivery jitter
#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_RingBuffer = soundio_ring_buffer_create(m_SoundIo,
m_OutputStream->bytes_per_sample * m_OutputStream->bytes_per_sample *
m_OpusChannelCount * m_OpusChannelCount *
SAMPLES_PER_FRAME * 6); SAMPLES_PER_FRAME *
packetsToBuffer);
if (m_RingBuffer == nullptr) { if (m_RingBuffer == nullptr) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"soundio_ring_buffer_create() failed"); "soundio_ring_buffer_create() failed");
@@ -238,6 +274,11 @@ bool SoundIoAudioRenderer::prepareForPlayback(const OPUS_MULTISTREAM_CONFIGURATI
return false; 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; return true;
} }
@@ -328,34 +369,19 @@ void SoundIoAudioRenderer::sioWriteCallback(SoundIoOutStream* stream, int frameC
(me->m_OpusChannelCount * stream->bytes_per_sample); (me->m_OpusChannelCount * stream->bytes_per_sample);
int bytesRead = 0; int bytesRead = 0;
// Clamp framesLeft to frameCountMax // Ensure we always write at least a buffer, even if it's silence, to avoid
framesLeft = qMin(framesLeft, frameCountMax); // 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 // Clamp framesLeft to frameCountMin to ensure that we never write more than one sample.
// avoid accumulating packets in queue-based backends // This makes sure that we never increase our latency beyond what the sink is consuming.
// like WASAPI. This bound was set by testing on several framesLeft = qMin(framesLeft, frameCountMin);
// 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;
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, // Track latency on queueing-based backends
"Latency exceeded drop cap: %f", if (me->m_SoundIo->current_backend != SoundIoBackendCoreAudio && me->m_SoundIo->current_backend != SoundIoBackendJack) {
latency); soundio_outstream_get_latency(stream, &me->m_Latency);
}
}
} }
for (;;) { for (;;) {
@@ -32,5 +32,9 @@ private:
struct SoundIoOutStream* m_OutputStream; struct SoundIoOutStream* m_OutputStream;
struct SoundIoRingBuffer* m_RingBuffer; struct SoundIoRingBuffer* m_RingBuffer;
struct SoundIoChannelLayout m_EffectiveLayout; struct SoundIoChannelLayout m_EffectiveLayout;
double m_Latency;
bool m_Errored; bool m_Errored;
static const double k_RawSampleLengthSec;
static const double k_MinSampleLengthSec;
}; };
+1 -1
View File
@@ -971,7 +971,7 @@ QString SdlInputHandler::getUnmappedGamepads()
"Number of axes: %d | Number of buttons: %d | Number of hats: %d", "Number of axes: %d | Number of buttons: %d | Number of hats: %d",
numAxes, numButtons, numHats); 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, SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"Joystick likely to be an unmapped game controller"); "Joystick likely to be an unmapped game controller");
if (!ret.isEmpty()) { if (!ret.isEmpty()) {
+76 -29
View File
@@ -73,6 +73,7 @@ void Session::clStageFailed(int stage, long errorCode)
void Session::clConnectionTerminated(long errorCode) void Session::clConnectionTerminated(long errorCode)
{ {
s_ActiveSession->m_UnexpectedTermination = true;
emit s_ActiveSession->displayLaunchError("Connection terminated"); emit s_ActiveSession->displayLaunchError("Connection terminated");
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
@@ -98,12 +99,16 @@ void Session::clLogMessage(const char* format, ...)
va_end(ap); 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, bool Session::chooseDecoder(StreamingPreferences::VideoDecoderSelection vds,
SDL_Window* window, int videoFormat, int width, int height, 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 #ifdef HAVE_SLVIDEO
chosenDecoder = new SLVideoDecoder(); chosenDecoder = new SLVideoDecoder();
if (CALL_INITIALIZE(chosenDecoder)) { if (CALL_INITIALIZE(chosenDecoder)) {
@@ -216,7 +221,7 @@ bool Session::isHardwareDecodeAvailable(StreamingPreferences::VideoDecoderSelect
return false; 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_DestroyWindow(window);
SDL_QuitSubSystem(SDL_INIT_VIDEO); SDL_QuitSubSystem(SDL_INIT_VIDEO);
return false; return false;
@@ -256,7 +261,7 @@ int Session::getDecoderCapabilities(StreamingPreferences::VideoDecoderSelection
return false; 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_DestroyWindow(window);
SDL_QuitSubSystem(SDL_INIT_VIDEO); SDL_QuitSubSystem(SDL_INIT_VIDEO);
return false; return false;
@@ -287,12 +292,21 @@ Session::Session(NvComputer* computer, NvApp& app, StreamingPreferences *prefere
m_DisplayOriginX(0), m_DisplayOriginX(0),
m_DisplayOriginY(0), m_DisplayOriginY(0),
m_PendingWindowedTransition(false), m_PendingWindowedTransition(false),
m_UnexpectedTermination(true), // Failure prior to streaming is unexpected
m_OpusDecoder(nullptr), m_OpusDecoder(nullptr),
m_AudioRenderer(nullptr), m_AudioRenderer(nullptr),
m_AudioSampleCount(0) 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() void Session::initialize()
{ {
qInfo() << "Server GPU:" << m_Computer->gpuModel; qInfo() << "Server GPU:" << m_Computer->gpuModel;
@@ -315,6 +329,8 @@ void Session::initialize()
m_StreamConfig.fps = m_Preferences->fps; m_StreamConfig.fps = m_Preferences->fps;
m_StreamConfig.bitrate = m_Preferences->bitrateKbps; m_StreamConfig.bitrate = m_Preferences->bitrateKbps;
m_StreamConfig.hevcBitratePercentageMultiplier = 75; m_StreamConfig.hevcBitratePercentageMultiplier = 75;
m_StreamConfig.streamingRemotely = STREAM_CFG_AUTO;
m_StreamConfig.packetSize = 1392;
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Video bitrate: %d kbps", "Video bitrate: %d kbps",
@@ -382,20 +398,6 @@ void Session::initialize()
break; 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) switch (m_Preferences->windowMode)
{ {
case StreamingPreferences::WM_FULLSCREEN_DESKTOP: case StreamingPreferences::WM_FULLSCREEN_DESKTOP:
@@ -563,15 +565,53 @@ bool Session::validateLaunch()
class DeferredSessionCleanupTask : public QRunnable class DeferredSessionCleanupTask : public QRunnable
{ {
void run() override public:
{ DeferredSessionCleanupTask(Session* session) :
// Finish cleanup of the connection state m_Session(session) {}
LiStopConnection();
private:
virtual ~DeferredSessionCleanupTask() override
{
// Allow another session to start now that we're cleaned up // Allow another session to start now that we're cleaned up
Session::s_ActiveSession = nullptr; Session::s_ActiveSession = nullptr;
Session::s_ActiveSessionSemaphore.release(); 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, 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 // Check for validation errors/warnings and emit
// signals for them, if appropriate // signals for them, if appropriate
if (!validateLaunch()) { if (!validateLaunch()) {
emit sessionFinished();
return; return;
} }
@@ -775,7 +816,7 @@ void Session::exec(int displayOriginX, int displayOriginY)
m_Computer->currentGameId == m_App.id); m_Computer->currentGameId == m_App.id);
try { try {
NvHTTP http(m_Computer->activeAddress); NvHTTP http(m_Computer->activeAddress, m_Computer->serverCert);
if (m_Computer->currentGameId != 0) { if (m_Computer->currentGameId != 0) {
http.resumeApp(&m_StreamConfig); http.resumeApp(&m_StreamConfig);
} }
@@ -787,11 +828,11 @@ void Session::exec(int displayOriginX, int displayOriginY)
} }
} catch (const GfeHttpResponseException& e) { } catch (const GfeHttpResponseException& e) {
emit displayLaunchError(e.toQString()); emit displayLaunchError(e.toQString());
s_ActiveSessionSemaphore.release(); QThreadPool::globalInstance()->start(new DeferredSessionCleanupTask(this));
return; return;
} catch (const QtNetworkReplyException& e) { } catch (const QtNetworkReplyException& e) {
emit displayLaunchError(e.toQString()); emit displayLaunchError(e.toQString());
s_ActiveSessionSemaphore.release(); QThreadPool::globalInstance()->start(new DeferredSessionCleanupTask(this));
return; return;
} }
@@ -801,7 +842,7 @@ void Session::exec(int displayOriginX, int displayOriginY)
"SDL_InitSubSystem(SDL_INIT_VIDEO) failed: %s", "SDL_InitSubSystem(SDL_INIT_VIDEO) failed: %s",
SDL_GetError()); SDL_GetError());
emit displayLaunchError(QString::fromLocal8Bit(SDL_GetError())); emit displayLaunchError(QString::fromLocal8Bit(SDL_GetError()));
s_ActiveSessionSemaphore.release(); QThreadPool::globalInstance()->start(new DeferredSessionCleanupTask(this));
return; return;
} }
@@ -829,7 +870,7 @@ void Session::exec(int displayOriginX, int displayOriginY)
// We already displayed an error dialog in the stage failure // We already displayed an error dialog in the stage failure
// listener. // listener.
SDL_QuitSubSystem(SDL_INIT_VIDEO); SDL_QuitSubSystem(SDL_INIT_VIDEO);
s_ActiveSessionSemaphore.release(); QThreadPool::globalInstance()->start(new DeferredSessionCleanupTask(this));
return; return;
} }
@@ -851,7 +892,7 @@ void Session::exec(int displayOriginX, int displayOriginY)
"SDL_CreateWindow() failed: %s", "SDL_CreateWindow() failed: %s",
SDL_GetError()); SDL_GetError());
SDL_QuitSubSystem(SDL_INIT_VIDEO); SDL_QuitSubSystem(SDL_INIT_VIDEO);
s_ActiveSessionSemaphore.release(); QThreadPool::globalInstance()->start(new DeferredSessionCleanupTask(this));
return; return;
} }
@@ -931,6 +972,11 @@ void Session::exec(int displayOriginX, int displayOriginY)
int currentDisplayIndex = SDL_GetWindowDisplayIndex(m_Window); 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 // 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. // because we want to suspend all Qt processing until the stream is over.
SDL_Event event; SDL_Event event;
@@ -1078,6 +1124,7 @@ void Session::exec(int displayOriginX, int displayOriginY)
m_Window, m_ActiveVideoFormat, m_ActiveVideoWidth, m_Window, m_ActiveVideoFormat, m_ActiveVideoWidth,
m_ActiveVideoHeight, m_ActiveVideoFrameRate, m_ActiveVideoHeight, m_ActiveVideoFrameRate,
enableVsync, enableVsync,
enableVsync && m_Preferences->framePacing,
s_ActiveSession->m_VideoDecoder)) { s_ActiveSession->m_VideoDecoder)) {
SDL_AtomicUnlock(&m_DecoderLock); SDL_AtomicUnlock(&m_DecoderLock);
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
@@ -1164,6 +1211,6 @@ DispatchDeferredCleanup:
// Cleanup can take a while, so dispatch it to a worker thread. // Cleanup can take a while, so dispatch it to a worker thread.
// When it is complete, it will release our s_ActiveSessionSemaphore // When it is complete, it will release our s_ActiveSessionSemaphore
// reference. // reference.
QThreadPool::globalInstance()->start(new DeferredSessionCleanupTask()); QThreadPool::globalInstance()->start(new DeferredSessionCleanupTask(this));
} }
+9 -1
View File
@@ -20,6 +20,8 @@ class Session : public QObject
public: public:
explicit Session(NvComputer* computer, NvApp& app, StreamingPreferences *preferences = nullptr); explicit Session(NvComputer* computer, NvApp& app, StreamingPreferences *preferences = nullptr);
virtual ~Session();
Q_INVOKABLE void exec(int displayOriginX, int displayOriginY); Q_INVOKABLE void exec(int displayOriginX, int displayOriginY);
static static
@@ -41,6 +43,10 @@ signals:
void displayLaunchWarning(QString text); void displayLaunchWarning(QString text);
void quitStarting();
void sessionFinished();
private: private:
void initialize(); void initialize();
@@ -66,7 +72,8 @@ private:
static static
bool chooseDecoder(StreamingPreferences::VideoDecoderSelection vds, bool chooseDecoder(StreamingPreferences::VideoDecoderSelection vds,
SDL_Window* window, int videoFormat, int width, int height, SDL_Window* window, int videoFormat, int width, int height,
int frameRate, bool enableVsync, IVideoDecoder*& chosenDecoder); int frameRate, bool enableVsync, bool enableFramePacing,
IVideoDecoder*& chosenDecoder);
static static
void clStageStarting(int stage); void clStageStarting(int stage);
@@ -114,6 +121,7 @@ private:
int m_DisplayOriginX; int m_DisplayOriginX;
int m_DisplayOriginY; int m_DisplayOriginY;
bool m_PendingWindowedTransition; bool m_PendingWindowedTransition;
bool m_UnexpectedTermination;
int m_ActiveVideoFormat; int m_ActiveVideoFormat;
int m_ActiveVideoWidth; int m_ActiveVideoWidth;
+3 -1
View File
@@ -16,6 +16,7 @@ typedef struct _VIDEO_STATS {
uint32_t pacerDroppedFrames; uint32_t pacerDroppedFrames;
uint32_t totalReassemblyTime; uint32_t totalReassemblyTime;
uint32_t totalDecodeTime; uint32_t totalDecodeTime;
uint32_t totalPacerTime;
uint32_t totalRenderTime; uint32_t totalRenderTime;
float receivedFps; float receivedFps;
float decodedFps; float decodedFps;
@@ -32,7 +33,8 @@ public:
int width, int width,
int height, int height,
int frameRate, int frameRate,
bool enableVsync) = 0; bool enableVsync,
bool enableFramePacing) = 0;
virtual bool isHardwareAccelerated() = 0; virtual bool isHardwareAccelerated() = 0;
virtual int getDecoderCapabilities() = 0; virtual int getDecoderCapabilities() = 0;
virtual int submitDecodeUnit(PDECODE_UNIT du) = 0; virtual int submitDecodeUnit(PDECODE_UNIT du) = 0;
@@ -644,9 +644,9 @@ int DXVA2Renderer::getDecoderCapabilities()
return 0; return 0;
} }
IFFmpegRenderer::VSyncConstraint DXVA2Renderer::getVsyncConstraint() IFFmpegRenderer::FramePacingConstraint DXVA2Renderer::getFramePacingConstraint()
{ {
return VSYNC_ANY; return PACING_ANY;
} }
void DXVA2Renderer::renderFrameAtVsync(AVFrame *frame) void DXVA2Renderer::renderFrameAtVsync(AVFrame *frame)
+1 -1
View File
@@ -25,7 +25,7 @@ public:
virtual void renderFrameAtVsync(AVFrame* frame); virtual void renderFrameAtVsync(AVFrame* frame);
virtual bool needsTestFrame(); virtual bool needsTestFrame();
virtual int getDecoderCapabilities(); virtual int getDecoderCapabilities();
virtual VSyncConstraint getVsyncConstraint(); virtual FramePacingConstraint getFramePacingConstraint();
private: private:
bool initializeDecoder(); bool initializeDecoder();
@@ -87,7 +87,6 @@ void Pacer::vsyncCallback(int timeUntilNextVsyncMillis)
av_frame_free(&frame); av_frame_free(&frame);
} }
if (m_FrameQueue.isEmpty()) { if (m_FrameQueue.isEmpty()) {
SDL_AtomicUnlock(&m_FrameQueueLock); SDL_AtomicUnlock(&m_FrameQueueLock);
@@ -112,8 +111,11 @@ RenderNextFrame:
AVFrame* frame = m_FrameQueue.dequeue(); AVFrame* frame = m_FrameQueue.dequeue();
SDL_AtomicUnlock(&m_FrameQueueLock); SDL_AtomicUnlock(&m_FrameQueueLock);
// Render it // Count time spent in Pacer's queues
Uint32 beforeRender = SDL_GetTicks(); Uint32 beforeRender = SDL_GetTicks();
m_VideoStats->totalPacerTime += beforeRender - frame->pts;
// Render it
m_VsyncRenderer->renderFrameAtVsync(frame); m_VsyncRenderer->renderFrameAtVsync(frame);
m_VideoStats->totalRenderTime += SDL_GetTicks() - beforeRender; m_VideoStats->totalRenderTime += SDL_GetTicks() - beforeRender;
m_VideoStats->renderedFrames++; m_VideoStats->renderedFrames++;
@@ -122,15 +124,14 @@ RenderNextFrame:
av_frame_free(&frame); 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_MaxVideoFps = maxVideoFps;
m_EnableVsync = enableVsync;
m_DisplayFps = StreamUtils::getDisplayRefreshRate(window); m_DisplayFps = StreamUtils::getDisplayRefreshRate(window);
if (m_EnableVsync) { if (enablePacing) {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, 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); m_DisplayFps, m_MaxVideoFps);
#if defined(Q_OS_DARWIN) #if defined(Q_OS_DARWIN)
@@ -148,7 +149,7 @@ bool Pacer::initialize(SDL_Window* window, int maxVideoFps, bool enableVsync)
} }
else { else {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, 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); m_DisplayFps, m_MaxVideoFps);
} }
@@ -20,7 +20,7 @@ public:
void submitFrame(AVFrame* frame); 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); void vsyncCallback(int timeUntilNextVsyncMillis);
@@ -35,6 +35,5 @@ private:
IFFmpegRenderer* m_VsyncRenderer; IFFmpegRenderer* m_VsyncRenderer;
int m_MaxVideoFps; int m_MaxVideoFps;
int m_DisplayFps; int m_DisplayFps;
bool m_EnableVsync;
PVIDEO_STATS m_VideoStats; PVIDEO_STATS m_VideoStats;
}; };
@@ -8,10 +8,10 @@ extern "C" {
class IFFmpegRenderer { class IFFmpegRenderer {
public: public:
enum VSyncConstraint { enum FramePacingConstraint {
VSYNC_FORCE_OFF, PACING_FORCE_OFF,
VSYNC_FORCE_ON, PACING_FORCE_ON,
VSYNC_ANY PACING_ANY
}; };
virtual ~IFFmpegRenderer() {} virtual ~IFFmpegRenderer() {}
@@ -25,7 +25,7 @@ public:
virtual void renderFrameAtVsync(AVFrame* frame) = 0; virtual void renderFrameAtVsync(AVFrame* frame) = 0;
virtual bool needsTestFrame() = 0; virtual bool needsTestFrame() = 0;
virtual int getDecoderCapabilities() = 0; virtual int getDecoderCapabilities() = 0;
virtual VSyncConstraint getVsyncConstraint() = 0; virtual FramePacingConstraint getFramePacingConstraint() = 0;
}; };
class SdlRenderer : public IFFmpegRenderer { class SdlRenderer : public IFFmpegRenderer {
@@ -42,7 +42,7 @@ public:
virtual void renderFrameAtVsync(AVFrame* frame); virtual void renderFrameAtVsync(AVFrame* frame);
virtual bool needsTestFrame(); virtual bool needsTestFrame();
virtual int getDecoderCapabilities(); virtual int getDecoderCapabilities();
virtual VSyncConstraint getVsyncConstraint(); virtual FramePacingConstraint getFramePacingConstraint();
private: private:
SDL_Renderer* m_Renderer; SDL_Renderer* m_Renderer;
@@ -43,9 +43,9 @@ int SdlRenderer::getDecoderCapabilities()
return CAPABILITY_REFERENCE_FRAME_INVALIDATION_AVC; 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, bool SdlRenderer::initialize(SDL_Window* window,
@@ -177,9 +177,9 @@ VAAPIRenderer::getDecoderCapabilities()
return 0; return 0;
} }
IFFmpegRenderer::VSyncConstraint VAAPIRenderer::getVsyncConstraint() IFFmpegRenderer::FramePacingConstraint VAAPIRenderer::getFramePacingConstraint()
{ {
return VSYNC_ANY; return PACING_ANY;
} }
void void
+1 -1
View File
@@ -40,7 +40,7 @@ public:
virtual void renderFrameAtVsync(AVFrame* frame); virtual void renderFrameAtVsync(AVFrame* frame);
virtual bool needsTestFrame(); virtual bool needsTestFrame();
virtual int getDecoderCapabilities(); virtual int getDecoderCapabilities();
virtual VSyncConstraint getVsyncConstraint(); virtual FramePacingConstraint getFramePacingConstraint();
private: private:
int m_WindowSystem; int m_WindowSystem;
@@ -246,9 +246,9 @@ int VDPAURenderer::getDecoderCapabilities()
return 0; return 0;
} }
IFFmpegRenderer::VSyncConstraint VDPAURenderer::getVsyncConstraint() IFFmpegRenderer::FramePacingConstraint VDPAURenderer::getFramePacingConstraint()
{ {
return VSYNC_ANY; return PACING_ANY;
} }
void VDPAURenderer::renderFrameAtVsync(AVFrame* frame) void VDPAURenderer::renderFrameAtVsync(AVFrame* frame)
+1 -1
View File
@@ -23,7 +23,7 @@ public:
virtual void renderFrameAtVsync(AVFrame* frame); virtual void renderFrameAtVsync(AVFrame* frame);
virtual bool needsTestFrame(); virtual bool needsTestFrame();
virtual int getDecoderCapabilities(); virtual int getDecoderCapabilities();
virtual VSyncConstraint getVsyncConstraint(); virtual FramePacingConstraint getFramePacingConstraint();
private: private:
uint32_t m_VideoWidth, m_VideoHeight; uint32_t m_VideoWidth, m_VideoHeight;
+2 -2
View File
@@ -211,12 +211,12 @@ public:
return 0; return 0;
} }
virtual IFFmpegRenderer::VSyncConstraint getVsyncConstraint() override virtual IFFmpegRenderer::FramePacingConstraint getFramePacingConstraint() override
{ {
// This renderer is inherently tied to V-sync due how we're // This renderer is inherently tied to V-sync due how we're
// rendering with AVSampleBufferDisplay layer. Running without // rendering with AVSampleBufferDisplay layer. Running without
// the V-Sync source leads to massive stuttering. // the V-Sync source leads to massive stuttering.
return VSYNC_FORCE_ON; return PACING_FORCE_ON;
} }
private: private:
+21 -13
View File
@@ -131,27 +131,27 @@ void FFmpegVideoDecoder::reset()
bool FFmpegVideoDecoder::completeInitialization(AVCodec* decoder, SDL_Window* window, bool FFmpegVideoDecoder::completeInitialization(AVCodec* decoder, SDL_Window* window,
int videoFormat, int width, int height, int videoFormat, int width, int height,
int maxFps, bool enableVsync, bool testOnly) int maxFps, bool enableFramePacing, bool testOnly)
{ {
auto vsyncConstraint = m_Renderer->getVsyncConstraint(); auto vsyncConstraint = m_Renderer->getFramePacingConstraint();
if (vsyncConstraint == IFFmpegRenderer::VSYNC_FORCE_OFF && enableVsync) { if (vsyncConstraint == IFFmpegRenderer::PACING_FORCE_OFF && enableFramePacing) {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"V-sync is forcefully disabled by the active renderer"); "Frame pacing is forcefully disabled by the active renderer");
enableVsync = false; 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 // FIXME: This duplicates logic in Session.cpp
int displayHz = StreamUtils::getDisplayRefreshRate(window); int displayHz = StreamUtils::getDisplayRefreshRate(window);
if (displayHz + 5 >= maxFps) { if (displayHz + 5 >= maxFps) {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"V-sync is forcefully enabled by the active renderer"); "Frame pacing is forcefully enabled by the active renderer");
enableVsync = true; enableFramePacing = true;
} }
} }
m_StreamFps = maxFps; m_StreamFps = maxFps;
m_Pacer = new Pacer(m_Renderer, &m_ActiveWndVideoStats); m_Pacer = new Pacer(m_Renderer, &m_ActiveWndVideoStats);
if (!m_Pacer->initialize(window, maxFps, enableVsync)) { if (!m_Pacer->initialize(window, maxFps, enableFramePacing)) {
return false; return false;
} }
@@ -258,6 +258,7 @@ void FFmpegVideoDecoder::addVideoStats(VIDEO_STATS& src, VIDEO_STATS& dst)
dst.pacerDroppedFrames += src.pacerDroppedFrames; dst.pacerDroppedFrames += src.pacerDroppedFrames;
dst.totalReassemblyTime += src.totalReassemblyTime; dst.totalReassemblyTime += src.totalReassemblyTime;
dst.totalDecodeTime += src.totalDecodeTime; dst.totalDecodeTime += src.totalDecodeTime;
dst.totalPacerTime += src.totalPacerTime;
dst.totalRenderTime += src.totalRenderTime; dst.totalRenderTime += src.totalRenderTime;
Uint32 now = SDL_GetTicks(); Uint32 now = SDL_GetTicks();
@@ -311,6 +312,9 @@ void FFmpegVideoDecoder::logVideoStats(VIDEO_STATS& stats, const char* title)
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Average decode time: %.2f ms", "Average decode time: %.2f ms",
(float)stats.totalDecodeTime / stats.decodedFrames); (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, SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Average render time: %.2f ms", "Average render time: %.2f ms",
(float)stats.totalRenderTime / stats.renderedFrames); (float)stats.totalRenderTime / stats.renderedFrames);
@@ -359,7 +363,8 @@ bool FFmpegVideoDecoder::initialize(
int width, int width,
int height, int height,
int maxFps, int maxFps,
bool enableVsync) bool enableVsync,
bool enableFramePacing)
{ {
AVCodec* decoder; AVCodec* decoder;
@@ -395,7 +400,7 @@ bool FFmpegVideoDecoder::initialize(
m_Renderer = new SdlRenderer(); m_Renderer = new SdlRenderer();
if (vds != StreamingPreferences::VDS_FORCE_HARDWARE && if (vds != StreamingPreferences::VDS_FORCE_HARDWARE &&
m_Renderer->initialize(window, videoFormat, width, height, maxFps, enableVsync) && 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; return true;
} }
else { else {
@@ -412,13 +417,13 @@ bool FFmpegVideoDecoder::initialize(
m_HwDecodeCfg = config; m_HwDecodeCfg = config;
// Initialize the hardware codec and submit a test frame if the renderer needs it // Initialize the hardware codec and submit a test frame if the renderer needs it
if (m_Renderer->initialize(window, videoFormat, width, height, maxFps, enableVsync) && 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()) { if (m_Renderer->needsTestFrame()) {
// The test worked, so now let's initialize it for real // The test worked, so now let's initialize it for real
reset(); reset();
if ((m_Renderer = createAcceleratedRenderer(config)) != nullptr && if ((m_Renderer = createAcceleratedRenderer(config)) != nullptr &&
m_Renderer->initialize(window, videoFormat, width, height, maxFps, enableVsync) && 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; return true;
} }
else { else {
@@ -577,6 +582,9 @@ int FFmpegVideoDecoder::submitDecodeUnit(PDECODE_UNIT du)
// Reset failed decodes count if we reached this far // Reset failed decodes count if we reached this far
m_ConsecutiveFailedDecodes = 0; m_ConsecutiveFailedDecodes = 0;
// Capture a frame timestamp to measuring pacing delay
frame->pts = SDL_GetTicks();
// Queue the frame for rendering from the main thread // Queue the frame for rendering from the main thread
SDL_AtomicIncRef(&m_QueuedFrames); SDL_AtomicIncRef(&m_QueuedFrames);
queueFrame(frame); queueFrame(frame);
+3 -2
View File
@@ -18,7 +18,8 @@ public:
int width, int width,
int height, int height,
int maxFps, int maxFps,
bool enableVsync) override; bool enableVsync,
bool enableFramePacing) override;
virtual bool isHardwareAccelerated() override; virtual bool isHardwareAccelerated() override;
virtual int getDecoderCapabilities() override; virtual int getDecoderCapabilities() override;
virtual int submitDecodeUnit(PDECODE_UNIT du) override; virtual int submitDecodeUnit(PDECODE_UNIT du) override;
@@ -30,7 +31,7 @@ public:
private: private:
bool completeInitialization(AVCodec* decoder, SDL_Window* window, bool completeInitialization(AVCodec* decoder, SDL_Window* window,
int videoFormat, int width, int height, 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); void logVideoStats(VIDEO_STATS& stats, const char* title);
+1 -1
View File
@@ -34,7 +34,7 @@ SLVideoDecoder::getDecoderCapabilities()
bool bool
SLVideoDecoder::initialize(StreamingPreferences::VideoDecoderSelection vds, SLVideoDecoder::initialize(StreamingPreferences::VideoDecoderSelection vds,
SDL_Window*, SDL_Window*,
int videoFormat, int, int, int frameRate, bool) int videoFormat, int, int, int frameRate, bool, bool)
{ {
// SLVideo only supports hardware decoding // SLVideo only supports hardware decoding
if (vds == StreamingPreferences::VDS_FORCE_SOFTWARE) { if (vds == StreamingPreferences::VDS_FORCE_SOFTWARE) {
+2 -1
View File
@@ -15,7 +15,8 @@ public:
int width, int width,
int height, int height,
int frameRate, int frameRate,
bool enableVsync); bool enableVsync,
bool enableFramePacing);
virtual bool isHardwareAccelerated(); virtual bool isHardwareAccelerated();
virtual int getDecoderCapabilities(); virtual int getDecoderCapabilities();
virtual int submitDecodeUnit(PDECODE_UNIT du); virtual int submitDecodeUnit(PDECODE_UNIT du);