Compare commits

...
21 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
40 changed files with 403 additions and 191 deletions
+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
@@ -40,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
+2 -2
View File
@@ -23,9 +23,9 @@
<key>NSSupportsAutomaticGraphicsSwitching</key> <key>NSSupportsAutomaticGraphicsSwitching</key>
<true/> <true/>
<key>CFBundleVersion</key> <key>CFBundleVersion</key>
<string>0.7.0</string> <string>0.8.0</string>
<key>CFBundleShortVersionString</key> <key>CFBundleShortVersionString</key>
<string>0.7.0</string> <string>0.8.0</string>
<key>CFBundleDisplayName</key> <key>CFBundleDisplayName</key>
<string>Moonlight</string> <string>Moonlight</string>
</dict> </dict>
+2 -2
View File
@@ -327,5 +327,5 @@ macx {
QMAKE_RPATHDIR += @executable_path/../Frameworks QMAKE_RPATHDIR += @executable_path/../Frameworks
} }
VERSION = 0.7.0 VERSION = 0.8.0
DEFINES += VERSION_STR=\\\"0.7.0\\\" DEFINES += VERSION_STR=\\\"0.8.0\\\"
+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;
+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
+4
View File
@@ -287,6 +287,7 @@ void StreamCommandLineParser::parse(const QStringList &args, StreamingPreference
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());
@@ -372,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"));
@@ -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>
@@ -33,6 +33,23 @@
</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"> <release version="0.7.0" date="2018-12-05">
<description> <description>
<p>New features:</p> <p>New features:</p>
+23 -2
View File
@@ -300,7 +300,7 @@ Flickable {
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
} }
@@ -322,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 {
@@ -378,7 +383,7 @@ Flickable {
CheckBox { CheckBox {
id: vsyncCheck id: vsyncCheck
hoverEnabled: true hoverEnabled: true
text: "Enable V-Sync" text: "V-Sync"
font.pointSize: 12 font.pointSize: 12
checked: prefs.enableVsync checked: prefs.enableVsync
onCheckedChanged: { onCheckedChanged: {
@@ -390,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"
}
} }
} }
+8
View File
@@ -136,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);
+3
View File
@@ -23,6 +23,7 @@
#define SER_QUITAPPAFTER "quitAppAfter" #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)
@@ -47,6 +48,7 @@ void StreamingPreferences::reload()
quitAppAfter = settings.value(SER_QUITAPPAFTER, false).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,
@@ -76,6 +78,7 @@ void StreamingPreferences::save()
settings.setValue(SER_QUITAPPAFTER, quitAppAfter); 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));
+3
View File
@@ -76,6 +76,7 @@ public:
Q_PROPERTY(bool quitAppAfter MEMBER quitAppAfter NOTIFY quitAppAfterChanged) 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)
@@ -95,6 +96,7 @@ public:
bool quitAppAfter; bool quitAppAfter;
bool mouseAcceleration; bool mouseAcceleration;
bool startWindowed; bool startWindowed;
bool framePacing;
AudioConfig audioConfig; AudioConfig audioConfig;
VideoCodecConfig videoCodecConfig; VideoCodecConfig videoCodecConfig;
VideoDecoderSelection videoDecoderSelection; VideoDecoderSelection videoDecoderSelection;
@@ -116,5 +118,6 @@ signals:
void videoDecoderSelectionChanged(); void videoDecoderSelectionChanged();
void windowModeChanged(); void windowModeChanged();
void startWindowedChanged(); void startWindowedChanged();
void framePacingChanged();
}; };
@@ -4,19 +4,28 @@
#include <QtGlobal> #include <QtGlobal>
// This determines the size of the buffers we'll // GFE sends us packets in 5 ms chunks
// 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. It is also the minimum
// size that we will write when called to fill a buffer.
const double SoundIoAudioRenderer::k_RawSampleLengthSec = 0.005; 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)
{ {
@@ -24,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);
} }
@@ -159,7 +172,7 @@ 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_RawSampleLengthSec; 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;
@@ -220,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");
@@ -240,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;
} }
@@ -330,41 +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
framesLeft = qMin(framesLeft, frameCountMax);
// Ensure we always write at least a buffer, even if it's silence, to avoid // Ensure we always write at least a buffer, even if it's silence, to avoid
// busy looping when no audio data is available while libsoundio tries to keep // busy looping when no audio data is available while libsoundio tries to keep
// us from starving the output device. // us from starving the output device.
frameCountMin = qMax(frameCountMin, (int)(stream->sample_rate * k_RawSampleLengthSec)); frameCountMin = qMax(frameCountMin, (int)(stream->sample_rate * k_MinSampleLengthSec));
frameCountMin = qMin(frameCountMin, frameCountMax); 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, ALSA, and PulseAudio. framesLeft = qMin(framesLeft, frameCountMin);
//
// This bound was set by testing on several Windows machines.
// The highest latency was found on a XPS 9343 running Windows 7
// in Steam Big Picture and the 5.1 audio test clip from Fraunhofer.
if (me->m_SoundIo->current_backend != SoundIoBackendCoreAudio && me->m_SoundIo->current_backend != SoundIoBackendJack) {
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,7 +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_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()) {
+11 -6
View File
@@ -99,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)) {
@@ -217,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;
@@ -257,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;
@@ -593,7 +597,7 @@ private:
// Perform a best-effort app quit // Perform a best-effort app quit
if (shouldQuit) { if (shouldQuit) {
NvHTTP http(m_Session->m_Computer->activeAddress); NvHTTP http(m_Session->m_Computer->activeAddress, m_Session->m_Computer->serverCert);
// Logging is already done inside NvHTTP // Logging is already done inside NvHTTP
try { try {
@@ -812,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);
} }
@@ -1120,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,
+2 -1
View File
@@ -72,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);
+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);