Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c5d70c48c9 | ||
|
|
82230ef2f5 | ||
|
|
dcfd3d3e48 | ||
|
|
cb1e2ac32e | ||
|
|
ec2c15946a | ||
|
|
8be73142f8 | ||
|
|
d1640e3bb8 | ||
|
|
8612e6726d | ||
|
|
3b4694e9f8 | ||
|
|
14cb2e1999 | ||
|
|
cfabaf334e | ||
|
|
c054536fab | ||
|
|
35aac18b4e | ||
|
|
de3928cb4d | ||
|
|
9bf612b800 | ||
|
|
d9a8d67e3d | ||
|
|
99b0a56a6a | ||
|
|
61d7aa0400 | ||
|
|
1e98594972 | ||
|
|
c6383f042c | ||
|
|
b4d8e0e551 |
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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\\\"
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|
||||||
|
|||||||
@@ -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));
|
||||||
|
|||||||
@@ -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;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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()) {
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
Submodule moonlight-common-c/moonlight-common-c updated: 80eba25bb9...dd49e5bbfd
Reference in New Issue
Block a user