Compare commits

..
Author SHA1 Message Date
Cameron Gutman 57137412b5 Experimental AVFoundation renderer 2018-07-17 22:57:15 -07:00
240 changed files with 16341 additions and 10825 deletions
-4
View File
@@ -1,5 +1 @@
*pro.user
**/.vs/
build/
+3
View File
@@ -1,6 +1,9 @@
[submodule "moonlight-common-c/moonlight-common-c"]
path = moonlight-common-c/moonlight-common-c
url = https://github.com/moonlight-stream/moonlight-common-c.git
[submodule "opus/opus"]
path = opus/opus
url = https://github.com/xiph/opus.git
[submodule "qmdnsengine/qmdnsengine"]
path = qmdnsengine/qmdnsengine
url = https://github.com/cgutman/qmdnsengine.git
-46
View File
@@ -1,46 +0,0 @@
language: cpp
git:
depth: 1
matrix:
include:
- os: osx
compiler: clang
- os: linux
sudo: required
dist: xenial
compiler: gcc
addons:
apt:
sources:
- sourceline: 'ppa:beineri/opt-qt596-xenial'
- sourceline: 'ppa:jonathonf/ffmpeg-4'
packages:
- qt59base
- qt59quickcontrols2
- qt59svg
- libesd0-dev
- libgl-dev
- libopus-dev
- libavcodec-dev
- libavutil-dev
- libva-dev
- libvdpau-dev
install:
- '[ "$TRAVIS_OS_NAME" != osx ] || brew install qt5'
- '[ "$TRAVIS_OS_NAME" != osx ] || brew link --force qt5'
- if [[ "$TRAVIS_OS_NAME" = "linux" ]]; then export SDL2_VER=2.0.8 && wget https://www.libsdl.org/release/SDL2-$SDL2_VER.tar.gz && tar -xvf SDL2-$SDL2_VER.tar.gz && cd SDL2-$SDL2_VER && ./configure && make && sudo make install && cd ..; fi
before_script:
- '[ "$TRAVIS_OS_NAME" != "linux" ] || source /opt/qt59/bin/qt59-env.sh'
- qmake -v
- '[[ "$TRAVIS_OS_NAME" != linux || "$CXX" != clang++ ]] || export QMAKESPEC=linux-clang'
- '[[ "$TRAVIS_OS_NAME" != linux || "$CXX" != g++ ]] || export QMAKESPEC=linux-g++'
- '[ "$TRAVIS_OS_NAME" != osx ] || export QMAKESPEC=macx-clang'
script:
- qmake moonlight-qt.pro -spec $QMAKESPEC
- make debug
- make release
+8 -19
View File
@@ -1,31 +1,20 @@
# Moonlight PC
[Moonlight PC](http://moonlight-stream.com) is an open source implementation of NVIDIA's GameStream, as used by the NVIDIA Shield, but built to run on Windows, Mac, and Linux. This client is the successor to [Moonlight Chrome](https://github.com/moonlight-stream/moonlight-chrome) for streaming on PC.
[Moonlight PC](http://moonlight-stream.com) is an open source implementation of NVIDIA's GameStream, as used by the NVIDIA Shield, but built to run on Windows, Mac, and Linux.
Moonlight also has mobile versions for [Android](https://github.com/moonlight-stream/moonlight-android) and [iOS](https://github.com/moonlight-stream/moonlight-ios).
See the [releases page](https://github.com/moonlight-stream/moonlight-qt/releases) for download links for Windows and Mac. For Linux, we're on [Flathub](https://flathub.org/apps/details/com.moonlight_stream.Moonlight).
You can follow development on our [Discord server](https://discord.gg/6ERtzFY).
[![Windows AppVeyor Status](https://ci.appveyor.com/api/projects/status/glj5cxqwy2w3bglv/branch/master?svg=true)](https://ci.appveyor.com/project/cgutman/moonlight-qt/branch/master)
[![Mac and Linux Travis CI Status](https://travis-ci.org/moonlight-stream/moonlight-qt.svg?branch=master)](https://travis-ci.org/moonlight-stream/moonlight-qt)
## Features
- Hardware accelerated video decoding on Windows, Mac, and Linux
- Supports streaming at up to 120 FPS on high refresh rate monitors
- Supports streaming at 720p, 1080p, 1440p, or 4K
- 5.1 surround sound audio
- HEVC support for better image quality at reduced bandwidth
- Keyboard and mouse support
- Gamepad support with SDL gamepad mappings
This is a work-in-progress port and is only suitable for development use at this point.
## Building
1. Install the latest Qt SDK (and optionally, the Qt Creator IDE) from https://www.qt.io/download (select MSVC toolchain on Windows)
2. Run `git submodule update --init --recursive` from within `moonlight-qt/`
3. Open the project in Qt Creator or build from qmake on the command line
1. Install the latest Qt SDK (and optionally, the Qt Creator IDE) from https://www.qt.io/download
3. Run `git submodule update --init --recursive` from within `moonlight-qt/`
4. Open the project in Qt Creator or build from qmake on the command line
## Contribute
This project is being actively developed at [XDA Developers](http://forum.xda-developers.com/showthread.php?t=2505510)
1. Fork us
2. Write code
3. Send Pull Requests
+6 -10
View File
@@ -3,28 +3,24 @@
<plist version="1.0">
<dict>
<key>CFBundleExecutable</key>
<string>Moonlight</string>
<string>moonlight-qt</string>
<key>CFBundleGetInfoString</key>
<string>Stream games from your NVIDIA GameStream-enabled PC</string>
<string>Created by Qt/QMake</string>
<key>CFBundleIconFile</key>
<string>moonlight</string>
<string>macos</string>
<key>CFBundleIdentifier</key>
<string>com.moonlight-stream.Moonlight</string>
<key>NSHighResolutionCapable</key>
<true/>
<string>com.yourcompany.moonlight-qt</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>LSMinimumSystemVersion</key>
<string>10.10</string>
<key>NOTE</key>
<string>This file was generated by Qt/QMake.</string>
<key>NSPrincipalClass</key>
<string>NSApplication</string>
<key>NSSupportsAutomaticGraphicsSwitching</key>
<true/>
<key>CFBundleVersion</key>
<string>0.1.0</string>
<key>CFBundleDisplayName</key>
<string>Moonlight</string>
</dict>
</plist>
+34 -105
View File
@@ -1,16 +1,7 @@
QT += core quick network quickcontrols2 svg
QT += core quick network quickcontrols2
CONFIG += c++11
unix:!macx {
TARGET = moonlight
} else {
# On macOS, this is the name displayed in the global menu bar
TARGET = Moonlight
}
# Support debug and release builds from command line for CI
CONFIG += debug_and_release
TARGET = moonlight-qt
TEMPLATE = app
# The following define makes your compiler emit warnings if you use
@@ -37,36 +28,20 @@ win32 {
LIBS += ws2_32.lib winmm.lib dxva2.lib ole32.lib
}
macx {
INCLUDEPATH += $$PWD/../libs/mac/include $$PWD/../libs/mac/Frameworks/SDL2.framework/Versions/A/Headers
LIBS += -L$$PWD/../libs/mac/lib -F$$PWD/../libs/mac/Frameworks
INCLUDEPATH += $$PWD/../libs/mac/include
LIBS += -L$$PWD/../libs/mac/lib
}
unix:!macx {
CONFIG += link_pkgconfig
PKGCONFIG += openssl sdl2 opus
LIBS += -ldl
packagesExist(libavcodec) {
PKGCONFIG += libavcodec libavutil
CONFIG += ffmpeg
packagesExist(libva) {
CONFIG += libva
}
packagesExist(vdpau) {
CONFIG += libvdpau
}
}
PKGCONFIG += openssl sdl2 libavcodec libavdevice libavformat libavutil
}
win32 {
LIBS += -llibssl -llibcrypto -lSDL2 -lavcodec -lavutil -lopus
CONFIG += ffmpeg
LIBS += -llibssl -llibcrypto -lSDL2 -lavcodec -lavdevice -lavformat -lavutil
}
macx {
LIBS += -lssl -lcrypto -lavcodec.58 -lavutil.56 -lopus -framework SDL2
LIBS += -lobjc -framework VideoToolbox -framework AVFoundation -framework CoreVideo -framework CoreGraphics -framework CoreMedia -framework AppKit
CONFIG += ffmpeg
LIBS += -lssl -lcrypto -lSDL2 -lavcodec.58 -lavdevice.58 -lavformat.58 -lavutil.56
LIBS += -lobjc -framework VideoToolbox -framework AVFoundation -framework CoreVideo -framework CoreMedia -framework AppKit
}
SOURCES += \
@@ -80,9 +55,17 @@ SOURCES += \
streaming/input.cpp \
streaming/session.cpp \
streaming/audio.cpp \
streaming/video/ffmpeg.cpp \
gui/computermodel.cpp \
gui/appmodel.cpp \
streaming/streamutils.cpp
streaming/video/ffmpeg-renderers/sdl.cpp
win32 {
SOURCES += streaming/video/ffmpeg-renderers/dxva2.cpp
}
macx {
SOURCES += streaming/video/avfoundation.mm
}
HEADERS += \
utils.h \
@@ -97,53 +80,14 @@ HEADERS += \
gui/computermodel.h \
gui/appmodel.h \
streaming/video/decoder.h \
streaming/streamutils.h
streaming/video/ffmpeg.h \
streaming/video/ffmpeg-renderers/renderer.h
# Platform-specific renderers and decoders
ffmpeg {
message(FFmpeg decoder selected)
DEFINES += HAVE_FFMPEG
SOURCES += \
streaming/video/ffmpeg.cpp \
streaming/video/ffmpeg-renderers/sdl.cpp
HEADERS += \
streaming/video/ffmpeg.h \
streaming/video/ffmpeg-renderers/renderer.h
}
libva {
message(VAAPI renderer selected)
DEFINES += HAVE_LIBVA
SOURCES += streaming/video/ffmpeg-renderers/vaapi.cpp
HEADERS += streaming/video/ffmpeg-renderers/vaapi.h
}
libvdpau {
message(VDPAU renderer selected)
DEFINES += HAVE_LIBVDPAU
SOURCES += streaming/video/ffmpeg-renderers/vdpau.cpp
HEADERS += streaming/video/ffmpeg-renderers/vdpau.h
}
config_SLVideo {
message(SLVideo decoder/renderer selected)
DEFINES += HAVE_SLVIDEO
LIBS += -lSLVideo
SOURCES += streaming/video/sl.cpp
HEADERS += streaming/video/sl.h
}
win32 {
message(DXVA2 renderer selected)
SOURCES += streaming/video/ffmpeg-renderers/dxva2.cpp
HEADERS += streaming/video/ffmpeg-renderers/dxva2.h
}
macx {
message(VideoToolbox renderer selected)
SOURCES += streaming/video/ffmpeg-renderers/vt.mm
HEADERS += streaming/video/ffmpeg-renderers/vt.h
HEADERS += streaming/video/avfoundation.h
}
RESOURCES += \
@@ -163,6 +107,13 @@ else:unix: LIBS += -L$$OUT_PWD/../moonlight-common-c/ -lmoonlight-common-c
INCLUDEPATH += $$PWD/../moonlight-common-c/moonlight-common-c/src
DEPENDPATH += $$PWD/../moonlight-common-c/moonlight-common-c/src
win32:CONFIG(release, debug|release): LIBS += -L$$OUT_PWD/../opus/release/ -lopus
else:win32:CONFIG(debug, debug|release): LIBS += -L$$OUT_PWD/../opus/debug/ -lopus
else:unix: LIBS += -L$$OUT_PWD/../opus/ -lopus
INCLUDEPATH += $$PWD/../opus/opus/include
DEPENDPATH += $$PWD/../opus/opus/include
win32:CONFIG(release, debug|release): LIBS += -L$$OUT_PWD/../qmdnsengine/release/ -lqmdnsengine
else:win32:CONFIG(debug, debug|release): LIBS += -L$$OUT_PWD/../qmdnsengine/debug/ -lqmdnsengine
else:unix: LIBS += -L$$OUT_PWD/../qmdnsengine/ -lqmdnsengine
@@ -170,36 +121,14 @@ else:unix: LIBS += -L$$OUT_PWD/../qmdnsengine/ -lqmdnsengine
INCLUDEPATH += $$PWD/../qmdnsengine/qmdnsengine/src/include $$PWD/../qmdnsengine
DEPENDPATH += $$PWD/../qmdnsengine/qmdnsengine/src/include $$PWD/../qmdnsengine
unix:!macx: {
isEmpty(PREFIX) {
PREFIX = /usr/local
}
isEmpty(BINDIR) {
BINDIR = bin
}
# Default rules for deployment.
qnx: target.path = /tmp/$${TARGET}/bin
else: unix:!android: target.path = /opt/$${TARGET}/bin
!isEmpty(target.path): INSTALLS += target
target.path = $$PREFIX/$$BINDIR/
desktop.files = deploy/linux/com.moonlight_stream.Moonlight.desktop
desktop.path = $$PREFIX/share/applications/
icons.files = res/moonlight.svg
icons.path = $$PREFIX/share/icons/hicolor/scalable/apps/
appdata.files = deploy/linux/com.moonlight_stream.Moonlight.appdata.xml
appdata.path = $$PREFIX/share/metainfo/
INSTALLS += target desktop icons appdata
}
win32 {
RC_ICONS = moonlight.ico
}
macx {
QMAKE_INFO_PLIST = $$PWD/Info.plist
APP_BUNDLE_RESOURCES.files = moonlight.icns
APP_BUNDLE_RESOURCES.path = Contents/Resources
QMAKE_BUNDLE_DATA += APP_BUNDLE_RESOURCES
APP_QML_FILES.files = res/macos.icns
APP_QML_FILES.path = Contents/Resources
QMAKE_BUNDLE_DATA += APP_QML_FILES
}
VERSION = 0.1.0
+5 -74
View File
@@ -51,8 +51,6 @@ NvComputer::NvComputer(QSettings& settings)
this->appVersion = nullptr;
this->maxLumaPixelsHEVC = 0;
this->serverCodecModeSupport = 0;
this->pendingQuit = false;
this->gpuModel = nullptr;
}
void
@@ -135,21 +133,19 @@ NvComputer::NvComputer(QString address, QString serverInfo)
this->currentGameId = NvHTTP::getCurrentGame(serverInfo);
this->appVersion = NvHTTP::getXmlString(serverInfo, "appversion");
this->gfeVersion = NvHTTP::getXmlString(serverInfo, "GfeVersion");
this->gpuModel = NvHTTP::getXmlString(serverInfo, "gputype");
this->activeAddress = address;
this->state = NvComputer::CS_ONLINE;
this->pendingQuit = false;
}
bool NvComputer::wake()
{
if (state == NvComputer::CS_ONLINE) {
qWarning() << name << "is already online";
qWarning() << name << " is already online";
return true;
}
if (macAddress.isNull()) {
qWarning() << name << "has no MAC address stored";
qWarning() << name << " has no MAC address stored";
return false;
}
@@ -188,7 +184,7 @@ bool NvComputer::wake()
// Send to all ports
for (quint16 port : WOL_PORTS) {
if (sock.writeDatagram(wolPayload, address, port)) {
qInfo().nospace().noquote() << "Send WoL packet to " << name << " via " << address.toString() << ":" << port;
qDebug() << "Send WoL packet to " << name << " via " << address.toString() << ":" << port;
success = true;
}
}
@@ -268,7 +264,6 @@ bool NvComputer::update(NvComputer& that)
ASSIGN_IF_CHANGED(gfeVersion);
ASSIGN_IF_CHANGED(appVersion);
ASSIGN_IF_CHANGED(maxLumaPixelsHEVC);
ASSIGN_IF_CHANGED(gpuModel);
ASSIGN_IF_CHANGED_AND_NONEMPTY(appList);
ASSIGN_IF_CHANGED_AND_NONEMPTY(displayModes);
return changed;
@@ -291,56 +286,6 @@ ComputerManager::ComputerManager(QObject *parent)
settings.endArray();
}
ComputerManager::~ComputerManager()
{
QWriteLocker lock(&m_Lock);
// Delete machines that haven't been resolved yet
while (!m_PendingResolution.isEmpty()) {
MdnsPendingComputer* computer = m_PendingResolution.first();
delete computer;
m_PendingResolution.removeFirst();
}
// Delete the browser to stop discovery
delete m_MdnsBrowser;
m_MdnsBrowser = nullptr;
{
QMutableMapIterator<QString, QThread*> i(m_PollThreads);
// Interrupt all polling threads
while (i.hasNext()) {
i.next();
i.value()->requestInterruption();
}
// Wait for all threads to terminate before destroying
// the NvComputer objects.
i.toFront();
while (i.hasNext()) {
i.next();
QThread* thread = i.value();
thread->wait();
delete thread;
i.remove();
}
}
{
QMutableMapIterator<QString, NvComputer*> i(m_KnownHosts);
// Destroy all NvComputer objects
while (i.hasNext()) {
i.next();
delete i.value();
i.remove();
}
}
}
void ComputerManager::saveHosts()
{
QSettings settings;
@@ -367,7 +312,7 @@ void ComputerManager::startPolling()
m_MdnsBrowser = new QMdnsEngine::Browser(&m_MdnsServer, "_nvstream._tcp.local.", &m_MdnsCache);
connect(m_MdnsBrowser, &QMdnsEngine::Browser::serviceAdded,
this, [this](const QMdnsEngine::Service& service) {
qInfo() << "Discovered mDNS host:" << service.hostname();
qDebug() << "Discovered mDNS host: " << service.hostname();
MdnsPendingComputer* pendingComputer = new MdnsPendingComputer(&m_MdnsServer, &m_MdnsCache, service);
connect(pendingComputer, SIGNAL(resolvedv4(MdnsPendingComputer*,QHostAddress)),
@@ -386,7 +331,7 @@ void ComputerManager::startPolling()
void ComputerManager::handleMdnsServiceResolved(MdnsPendingComputer* computer,
const QHostAddress& address)
{
qInfo() << "Resolved" << computer->hostname() << "to" << address.toString();
qDebug() << "Resolved " << computer->hostname() << " to " << address.toString();
addNewHost(address.toString(), true);
@@ -451,15 +396,6 @@ void ComputerManager::pairHost(NvComputer* computer, QString pin)
QThreadPool::globalInstance()->start(pairing);
}
void ComputerManager::quitRunningApp(NvComputer* computer)
{
QWriteLocker lock(&computer->lock);
computer->pendingQuit = true;
PendingQuitTask* quit = new PendingQuitTask(this, computer);
QThreadPool::globalInstance()->start(quit);
}
void ComputerManager::stopPollingAsync()
{
QWriteLocker lock(&m_Lock);
@@ -511,11 +447,6 @@ ComputerManager::handleComputerStateChanged(NvComputer* computer)
{
emit computerStateChanged(computer);
if (computer->pendingQuit && computer->currentGameId == 0) {
computer->pendingQuit = false;
emit quitAppCompleted(QVariant());
}
// Save updated hosts to QSettings
saveHosts();
}
+11 -60
View File
@@ -16,14 +16,10 @@
class NvComputer
{
friend class PcMonitorThread;
friend class ComputerManager;
friend class PendingQuitTask;
private:
void sortAppList();
bool pendingQuit;
public:
explicit NvComputer(QString address, QString serverInfo);
@@ -65,7 +61,6 @@ public:
QVector<NvDisplayMode> displayModes;
int maxLumaPixelsHEVC;
int serverCodecModeSupport;
QString gpuModel;
// Persisted traits
QString localAddress;
@@ -153,26 +148,31 @@ private:
int pollsSinceLastAppListFetch = POLLS_PER_APPLIST_FETCH;
while (!isInterruptionRequested()) {
bool stateChanged = false;
bool online = false;
for (int i = 0; i < TRIES_BEFORE_OFFLINING && !online; i++) {
for (int i = 0; i < TRIES_BEFORE_OFFLINING; i++) {
for (auto& address : m_Computer->uniqueAddresses()) {
if (isInterruptionRequested()) {
return;
}
if (tryPollComputer(address, stateChanged)) {
online = true;
break;
}
}
// No need to continue retrying if we're online
if (m_Computer->state == NvComputer::CS_ONLINE) {
break;
}
}
// Check if we failed after all retry attempts
// Note: we don't need to acquire the read lock here,
// because we're on the writing thread.
if (!online && m_Computer->state != NvComputer::CS_OFFLINE) {
m_Computer->state = NvComputer::CS_OFFLINE;
stateChanged = true;
if (m_Computer->state != NvComputer::CS_ONLINE) {
if (m_Computer->state != NvComputer::CS_OFFLINE) {
m_Computer->state = NvComputer::CS_OFFLINE;
stateChanged = true;
}
}
// Grab the applist if it's empty or it's been long enough that we need to refresh
@@ -249,8 +249,6 @@ class ComputerManager : public QObject
public:
explicit ComputerManager(QObject *parent = nullptr);
virtual ~ComputerManager();
Q_INVOKABLE void startPolling();
Q_INVOKABLE void stopPollingAsync();
@@ -259,8 +257,6 @@ public:
void pairHost(NvComputer* computer, QString pin);
void quitRunningApp(NvComputer* computer);
QVector<NvComputer*> getComputers();
// computer is deleted inside this call
@@ -273,8 +269,6 @@ signals:
void computerAddCompleted(QVariant success);
void quitAppCompleted(QVariant error);
private slots:
void handleComputerStateChanged(NvComputer* computer);
@@ -342,49 +336,6 @@ private:
QString m_Pin;
};
class PendingQuitTask : public QObject, public QRunnable
{
Q_OBJECT
public:
PendingQuitTask(ComputerManager* computerManager, NvComputer* computer)
: m_Computer(computer)
{
connect(this, &PendingQuitTask::quitAppFailed,
computerManager, &ComputerManager::quitAppCompleted);
}
signals:
void quitAppFailed(QString error);
private:
void run()
{
NvHTTP http(m_Computer->activeAddress);
try {
if (m_Computer->currentGameId != 0) {
http.quitApp();
}
} catch (const GfeHttpResponseException& e) {
{
QWriteLocker lock(&m_Computer->lock);
m_Computer->pendingQuit = false;
}
if (e.getStatusCode() == 599) {
// 599 is a special code we make a custom message for
emit quitAppFailed("The running game wasn't started by this PC. "
"You must quit the game on the host PC manually or use the device that originally started the game.");
}
else {
emit quitAppFailed(e.toQString());
}
}
}
NvComputer* m_Computer;
};
class PendingAddTask : public QObject, public QRunnable
{
Q_OBJECT
+9 -14
View File
@@ -2,12 +2,12 @@
#include "utils.h"
#include <QDebug>
#include <QRandomGenerator64>
#include <openssl/pem.h>
#include <openssl/rsa.h>
#include <openssl/bn.h>
#include <openssl/x509.h>
#include <openssl/rand.h>
#define SER_UNIQUEID "uniqueid"
#define SER_CERT "certificate"
@@ -101,7 +101,7 @@ void IdentityManager::createCredentials(QSettings& settings)
settings.setValue(SER_CERT, m_CachedPemCert);
settings.setValue(SER_KEY, m_CachedPrivateKey);
qInfo() << "Wrote new identity credentials to settings";
qDebug() << "Wrote new identity credentials to settings";
}
IdentityManager::IdentityManager()
@@ -112,7 +112,7 @@ IdentityManager::IdentityManager()
m_CachedPrivateKey = settings.value(SER_KEY).toByteArray();
if (m_CachedPemCert.isEmpty() || m_CachedPrivateKey.isEmpty()) {
qInfo() << "No existing credentials found";
qDebug() << "No existing credentials found";
createCredentials(settings);
}
else if (getSslCertificate().isNull()) {
@@ -157,14 +157,10 @@ IdentityManager::getSslKey()
// We must write out our PEM in the old PKCS1 format for SecureTransport
// on macOS/iOS to be able to read it.
#ifdef Q_OS_DARWIN
PEM_write_bio_PrivateKey_traditional(bio, pk, nullptr, nullptr, 0, nullptr, 0);
#else
PEM_write_bio_PrivateKey(bio, pk, nullptr, nullptr, 0, nullptr, 0);
#endif
BUF_MEM* mem;
BIO_get_mem_ptr(bio, &mem);
PEM_write_bio_PrivateKey_traditional(bio, pk, nullptr, nullptr, 0, nullptr, 0);
m_CachedSslKey = QSslKey(QByteArray::fromRawData(mem->data, (int)mem->length), QSsl::Rsa);
BIO_free(bio);
@@ -191,15 +187,14 @@ IdentityManager::getUniqueId()
// Load the unique ID from settings
m_CachedUniqueId = settings.value(SER_UNIQUEID).toString();
if (!m_CachedUniqueId.isEmpty() && !m_CachedUniqueId.isNull()) {
qInfo() << "Loaded unique ID from settings:" << m_CachedUniqueId;
qDebug() << "Loaded unique ID from settings: " << m_CachedUniqueId;
}
else {
// Generate a new unique ID in base 16
uint64_t uid;
RAND_bytes(reinterpret_cast<unsigned char*>(&uid), sizeof(uid));
m_CachedUniqueId = QString::number(uid, 16);
m_CachedUniqueId = QString::number(
QRandomGenerator64::securelySeeded().generate64(), 16);
qInfo() << "Generated new unique ID:" << m_CachedUniqueId;
qDebug() << "Generated new unique ID: " << m_CachedUniqueId;
settings.setValue(SER_UNIQUEID, m_CachedUniqueId);
}
+10 -36
View File
@@ -67,7 +67,6 @@ NvHTTP::getServerInfo()
serverInfo = openConnectionToString(m_BaseUrlHttps,
"serverinfo",
nullptr,
true,
true);
// Throws if the request failed
verifyResponseStatus(serverInfo);
@@ -80,7 +79,6 @@ NvHTTP::getServerInfo()
serverInfo = openConnectionToString(m_BaseUrlHttp,
"serverinfo",
nullptr,
true,
true);
verifyResponseStatus(serverInfo);
}
@@ -125,11 +123,6 @@ NvHTTP::launchApp(int appId,
bool localAudio,
int gamepadMask)
{
int riKeyId;
memcpy(&riKeyId, streamConfig->remoteInputAesIv, sizeof(riKeyId));
riKeyId = qFromBigEndian(riKeyId);
QString response =
openConnectionToString(m_BaseUrlHttps,
"launch",
@@ -139,7 +132,7 @@ NvHTTP::launchApp(int appId,
QString::number(streamConfig->fps)+
"&additionalStates=1&sops="+QString::number(sops ? 1 : 0)+
"&rikey="+QByteArray(streamConfig->remoteInputAesKey, sizeof(streamConfig->remoteInputAesKey)).toHex()+
"&rikeyid="+QString::number(riKeyId)+
"&rikeyid="+QString::number(qFromBigEndian(*(int*)streamConfig->remoteInputAesIv))+
(streamConfig->enableHdr ?
"&hdrMode=1&clientHdrCapVersion=0&clientHdrCapSupportedFlagsInUint32=0&clientHdrCapMetaDataId=NV_STATIC_METADATA_TYPE_1&clientHdrCapDisplayData=0x0x0x0x0x0x0x0x0x0x0" :
"")+
@@ -156,16 +149,11 @@ NvHTTP::launchApp(int appId,
void
NvHTTP::resumeApp(PSTREAM_CONFIGURATION streamConfig)
{
int riKeyId;
memcpy(&riKeyId, streamConfig->remoteInputAesIv, sizeof(riKeyId));
riKeyId = qFromBigEndian(riKeyId);
QString response =
openConnectionToString(m_BaseUrlHttps,
"resume",
"rikey="+QString(QByteArray(streamConfig->remoteInputAesKey, sizeof(streamConfig->remoteInputAesKey)).toHex())+
"&rikeyid="+QString::number(riKeyId)+
"&rikeyid="+QString::number(qFromBigEndian(*(int*)streamConfig->remoteInputAesIv))+
"&surroundAudioInfo="+getSurroundAudioInfoString(streamConfig->audioConfiguration),
false);
@@ -227,7 +215,6 @@ NvHTTP::getAppList()
QString appxml = openConnectionToString(m_BaseUrlHttps,
"applist",
nullptr,
true,
true);
verifyResponseStatus(appxml);
@@ -278,10 +265,7 @@ NvHTTP::verifyResponseStatus(QString xml)
else
{
QString statusMessage = xmlReader.attributes().value("status_message").toString();
if (statusCode != 401) {
// 401 is expected for unpaired PCs when we fetch serverinfo over HTTPS
qWarning() << "Request failed:" << statusCode << statusMessage;
}
qDebug() << "Request failed: " << statusCode << " " << statusMessage;
throw GfeHttpResponseException(statusCode, statusMessage);
}
}
@@ -341,15 +325,12 @@ QString
NvHTTP::openConnectionToString(QUrl baseUrl,
QString command,
QString arguments,
bool enableTimeout,
bool suppressLogging)
bool enableTimeout)
{
QNetworkReply* reply = openConnection(baseUrl, command, arguments, enableTimeout, suppressLogging);
QNetworkReply* reply = openConnection(baseUrl, command, arguments, enableTimeout);
QString ret;
QTextStream stream(reply);
stream.setCodec("UTF-8");
ret = stream.readAll();
ret = QTextStream(reply).readAll();
delete reply;
return ret;
@@ -359,8 +340,7 @@ QNetworkReply*
NvHTTP::openConnection(QUrl baseUrl,
QString command,
QString arguments,
bool enableTimeout,
bool suppressLogging)
bool enableTimeout)
{
// Build a URL for the request
QUrl url(baseUrl);
@@ -386,17 +366,13 @@ NvHTTP::openConnection(QUrl baseUrl,
{
QTimer::singleShot(REQUEST_TIMEOUT_MS, &loop, SLOT(quit()));
}
if (!suppressLogging) {
qInfo() << "Executing request:" << url.toString();
}
qDebug() << "Executing request: " << url.toString();
loop.exec(QEventLoop::ExcludeUserInputEvents);
// Abort the request if it timed out
if (!reply->isFinished())
{
if (!suppressLogging) {
qWarning() << "Aborting timed out request for" << url.toString();
}
qDebug() << "Aborting timed out request for " << url.toString();
reply->abort();
}
@@ -407,9 +383,7 @@ NvHTTP::openConnection(QUrl baseUrl,
// Handle error
if (reply->error() != QNetworkReply::NoError)
{
if (!suppressLogging) {
qWarning() << command << " request failed with error " << reply->error();
}
qDebug() << command << " request failed with error " << reply->error();
GfeHttpResponseException exception(reply->error(), reply->errorString());
delete reply;
throw exception;
+2 -4
View File
@@ -107,8 +107,7 @@ public:
openConnectionToString(QUrl baseUrl,
QString command,
QString arguments,
bool enableTimeout,
bool suppressLogging = false);
bool enableTimeout);
static
QVector<int>
@@ -144,8 +143,7 @@ private:
openConnection(QUrl baseUrl,
QString command,
QString arguments,
bool enableTimeout,
bool suppressLogging = false);
bool enableTimeout);
QString m_Address;
QNetworkAccessManager m_Nam;
+22 -24
View File
@@ -1,6 +1,8 @@
#include "nvpairingmanager.h"
#include "utils.h"
#include <QRandomGenerator>
#include <openssl/bio.h>
#include <openssl/aes.h>
#include <openssl/rand.h>
@@ -19,7 +21,7 @@ NvPairingManager::NvPairingManager(QString address) :
BIO_free_all(bio);
if (m_Cert == nullptr)
{
throw std::runtime_error("Unable to load certificate");
throw new std::runtime_error("Unable to load certificate");
}
QByteArray pk = IdentityManager::get()->getPrivateKey();
@@ -30,7 +32,7 @@ NvPairingManager::NvPairingManager(QString address) :
BIO_free_all(bio);
if (m_PrivateKey == nullptr)
{
throw std::runtime_error("Unable to load private key");
throw new std::runtime_error("Unable to load private key");
}
}
@@ -87,16 +89,14 @@ NvPairingManager::getSignatureFromPemCert(QByteArray certificate)
X509* cert = PEM_read_bio_X509(bio, nullptr, nullptr, nullptr);
BIO_free_all(bio);
#if (OPENSSL_VERSION_NUMBER < 0x10002000L)
ASN1_BIT_STRING *asnSignature = cert->signature;
#elif (OPENSSL_VERSION_NUMBER < 0x10100000L)
ASN1_BIT_STRING *asnSignature;
X509_get0_signature(&asnSignature, NULL, cert);
#if (OPENSSL_VERSION_NUMBER < 0x10100000L)
ASN1_BIT_STRING *asnSignature;
#else
const ASN1_BIT_STRING *asnSignature;
X509_get0_signature(&asnSignature, NULL, cert);
const ASN1_BIT_STRING *asnSignature;
#endif
X509_get0_signature(&asnSignature, NULL, cert);
QByteArray signature(reinterpret_cast<char*>(asnSignature->data), asnSignature->length);
X509_free(cert);
@@ -164,7 +164,7 @@ NvPairingManager::PairState
NvPairingManager::pair(QString appVersion, QString pin)
{
int serverMajorVersion = NvHTTP::parseQuad(appVersion).at(0);
qInfo() << "Pairing with server generation:" << serverMajorVersion;
qDebug() << "Pairing with server generation: " << serverMajorVersion;
QCryptographicHash::Algorithm hashAlgo;
int hashLength;
@@ -196,14 +196,14 @@ NvPairingManager::pair(QString appVersion, QString pin)
NvHTTP::verifyResponseStatus(getCert);
if (NvHTTP::getXmlString(getCert, "paired") != "1")
{
qCritical() << "Failed pairing at stage #1";
qDebug() << "Failed pairing at stage #1";
return PairState::FAILED;
}
QByteArray serverCert = NvHTTP::getXmlStringFromHex(getCert, "plaincert");
if (serverCert == nullptr)
{
qCritical() << "Server likely already pairing";
qDebug() << "Server likely already pairing";
m_Http.openConnectionToString(m_Http.m_BaseUrlHttp, "unpair", nullptr, true);
return PairState::ALREADY_IN_PROGRESS;
}
@@ -218,7 +218,7 @@ NvPairingManager::pair(QString appVersion, QString pin)
NvHTTP::verifyResponseStatus(challengeXml);
if (NvHTTP::getXmlString(challengeXml, "paired") != "1")
{
qCritical() << "Failed pairing at stage #2";
qDebug() << "Failed pairing at stage #2";
m_Http.openConnectionToString(m_Http.m_BaseUrlHttp, "unpair", nullptr, true);
return PairState::FAILED;
}
@@ -228,15 +228,13 @@ NvPairingManager::pair(QString appVersion, QString pin)
QByteArray challengeResponse;
QByteArray serverResponse(challengeResponseData.data(), hashLength);
#if (OPENSSL_VERSION_NUMBER < 0x10002000L)
ASN1_BIT_STRING *asnSignature = m_Cert->signature;
#elif (OPENSSL_VERSION_NUMBER < 0x10100000L)
#if (OPENSSL_VERSION_NUMBER < 0x10100000L)
ASN1_BIT_STRING *asnSignature;
X509_get0_signature(&asnSignature, NULL, m_Cert);
#else
#else
const ASN1_BIT_STRING *asnSignature;
#endif
X509_get0_signature(&asnSignature, NULL, m_Cert);
#endif
challengeResponse.append(challengeResponseData.data() + hashLength, 16);
challengeResponse.append(reinterpret_cast<char*>(asnSignature->data), asnSignature->length);
@@ -251,7 +249,7 @@ NvPairingManager::pair(QString appVersion, QString pin)
NvHTTP::verifyResponseStatus(respXml);
if (NvHTTP::getXmlString(respXml, "paired") != "1")
{
qCritical() << "Failed pairing at stage #3";
qDebug() << "Failed pairing at stage #3";
m_Http.openConnectionToString(m_Http.m_BaseUrlHttp, "unpair", nullptr, true);
return PairState::FAILED;
}
@@ -264,7 +262,7 @@ NvPairingManager::pair(QString appVersion, QString pin)
serverSignature,
serverCert))
{
qCritical() << "MITM detected";
qDebug() << "MITM detected";
m_Http.openConnectionToString(m_Http.m_BaseUrlHttp, "unpair", nullptr, true);
return PairState::FAILED;
}
@@ -275,7 +273,7 @@ NvPairingManager::pair(QString appVersion, QString pin)
expectedResponseData.append(serverSecret);
if (QCryptographicHash::hash(expectedResponseData, hashAlgo) != serverResponse)
{
qCritical() << "Incorrect PIN";
qDebug() << "Incorrect PIN";
m_Http.openConnectionToString(m_Http.m_BaseUrlHttp, "unpair", nullptr, true);
return PairState::PIN_WRONG;
}
@@ -292,7 +290,7 @@ NvPairingManager::pair(QString appVersion, QString pin)
NvHTTP::verifyResponseStatus(secretRespXml);
if (NvHTTP::getXmlString(secretRespXml, "paired") != "1")
{
qCritical() << "Failed pairing at stage #4";
qDebug() << "Failed pairing at stage #4";
m_Http.openConnectionToString(m_Http.m_BaseUrlHttp, "unpair", nullptr, true);
return PairState::FAILED;
}
@@ -304,7 +302,7 @@ NvPairingManager::pair(QString appVersion, QString pin)
NvHTTP::verifyResponseStatus(pairChallengeXml);
if (NvHTTP::getXmlString(pairChallengeXml, "paired") != "1")
{
qCritical() << "Failed pairing at stage #5";
qDebug() << "Failed pairing at stage #5";
m_Http.openConnectionToString(m_Http.m_BaseUrlHttp, "unpair", nullptr, true);
return PairState::FAILED;
}
@@ -1,88 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<component type="desktop">
<id>com.moonlight_stream.Moonlight</id>
<metadata_license>CC0-1.0</metadata_license>
<project_license>GPL-3.0+</project_license>
<name>Moonlight</name>
<summary>Stream games from your NVIDIA GameStream-enabled PC</summary>
<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>Features include:</p>
<ul>
<li>Streaming at up to 4K resolution</li>
<li>Support for up to 120 FPS streaming on high refresh rate monitors</li>
<li>Hardware accelerated video decoding with VAAPI and VDPAU support</li>
<li>5.1 surround sound audio support</li>
<li>HEVC support for better video compression efficiency</li>
<li>Gamepad support with SDL mapping compatibility</li>
</ul>
</description>
<url type="homepage">https://moonlight-stream.com</url>
<url type="bugtracker">https://github.com/moonlight-stream/moonlight-qt/issues</url>
<url type="help">https://github.com/moonlight-stream/moonlight-docs/wiki/Setup-Guide</url>
<screenshots>
<screenshot type="default">
<image>https://moonlight-stream.com/appstream_resources/moonlight_screenshot_1.png</image>
</screenshot>
<screenshot>
<image>https://moonlight-stream.com/appstream_resources/moonlight_screenshot_2.png</image>
</screenshot>
</screenshots>
<releases>
<release version="0.1.0" date="2018-08-05">
<description>
<p>New features:</p>
<ul>
<li>Allow streaming at the client's native resolution</li>
<li>VDPAU hardware decoding support for NVIDIA GPUs</li>
<li>Updated UI to replace status text with icons</li>
<li>Stream window is now resizable</li>
<li>Display warnings during game launch for configuration issues</li>
</ul>
<p>Bugfixes:</p>
<ul>
<li>Gamepad input now works even if the app doesn't have focus</li>
<li>Fixed streaming black screen if the host supported HEVC but the client didn't</li>
<li>Fixed broken video scaling causing extra stretching and off-center video</li>
<li>Fixed gamepad input issue causing some Y axis inputs to be reversed</li>
</ul>
</description>
</release>
<release version="0.0.5" date="2018-08-01">
<description>
<p>Changes from 0.0.4:</p>
<ul>
<li>Fixed a bug causing increased frame drops and input lag</li>
<li>Added support for quitting running games</li>
<li>Added support for waking sleeping PCs with Wake-on-LAN</li>
<li>Increased the FPS limit to 120 FPS</li>
</ul>
</description>
</release>
<release version="0.0.4a" date="2018-07-30">
<description>
<p>Initial pre-alpha release for Linux. Please file bugs!</p>
</description>
</release>
</releases>
<categories>
<category>Game</category>
<category>Qt</category>
</categories>
<developer_name>Moonlight Game Streaming Team</developer_name>
<update_contact>cameron@moonlight-stream.com</update_contact>
<launchable type="desktop-id">com.moonlight_stream.Moonlight.desktop</launchable>
<provides>
<binary>moonlight</binary>
</provides>
<content_rating type="oars-1.0" />
</component>
@@ -1,9 +0,0 @@
[Desktop Entry]
Name=Moonlight
Comment=Stream games from your NVIDIA GameStream-enabled PC
Exec=moonlight
Icon=moonlight
Terminal=false
Type=Application
Categories=Qt;Game;
Keywords=nvidia;gamestream;stream;
+16 -92
View File
@@ -1,5 +1,4 @@
import QtQuick 2.9
import QtQuick.Dialogs 1.3
import QtQuick.Controls 2.2
import AppModel 1.0
@@ -13,7 +12,7 @@ GridView {
id: appGrid
anchors.fill: parent
anchors.leftMargin: (parent.width % (cellWidth + anchors.rightMargin)) / 2
anchors.topMargin: 20
anchors.topMargin: 5
anchors.rightMargin: 5
anchors.bottomMargin: 5
cellWidth: 225; cellHeight: 350;
@@ -51,6 +50,12 @@ GridView {
delegate: Item {
width: 200; height: 300;
Component.onCompleted: {
if (model.running) {
appNameText.text = "<font color=\"green\">Running</font><font color=\"white\"> - </font>" + appNameText.text
}
}
Image {
id: appIcon
anchors.horizontalCenter: parent.horizontalCenter;
@@ -61,110 +66,29 @@ GridView {
}
}
Image {
id: runningIcon
anchors.centerIn: appIcon
visible: model.running
source: "qrc:/res/baseline-play_circle_filled_white-48px.svg"
sourceSize {
width: 75
height: 75
}
}
Text {
id: appNameText
text: model.name
color: "white"
width: parent.width
height: 125
height: 100
anchors.top: appIcon.bottom
font.pointSize: 22
font.pointSize: 26
horizontalAlignment: Text.AlignHCenter
wrapMode: Text.Wrap
elide: Text.ElideRight
}
function launchOrResumeSelectedApp()
{
var runningIndex = appModel.getRunningAppIndex()
if (runningIndex >= 0 && runningIndex !== index) {
quitAppDialog.appName = appModel.getRunningAppName()
quitAppDialog.segueToStream = true
quitAppDialog.open()
return
}
var component = Qt.createComponent("StreamSegue.qml")
var segue = component.createObject(stackView)
segue.appName = model.name
segue.session = appModel.createSessionForApp(index)
stackView.push(segue)
}
MessageDialog {
id: quitAppDialog
modality:Qt.WindowModal
property string appName : "";
property bool segueToStream : false
text:"Are you sure you want to quit " + appName +"? Any unsaved progress will be lost."
standardButtons: StandardButton.Yes | StandardButton.No
onYes: {
var component = Qt.createComponent("QuitSegue.qml")
var segue = component.createObject(stackView)
segue.appName = appName
if (segueToStream) {
// Store the session and app name if we're going to stream after
// successfully quitting the old app.
segue.nextAppName = model.name
segue.nextSession = appModel.createSessionForApp(index)
}
else {
segue.nextAppName = null
segue.nextSession = null
}
stackView.push(segue)
// Trigger the quit after pushing the quit segue on screen
appModel.quitRunningApp()
}
}
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.LeftButton | Qt.RightButton
onClicked: {
if (mouse.button === Qt.LeftButton) {
// Nothing is running or this app is running
launchOrResumeSelectedApp()
}
else {
// Right click
appContextMenu.open()
}
}
}
// TODO: Check if a different game is running
Menu {
id: appContextMenu
MenuItem {
text: model.running ? "Resume Game" : "Launch Game"
onTriggered: {
appContextMenu.close()
launchOrResumeSelectedApp()
}
height: visible ? implicitHeight : 0
}
MenuItem {
text: "Quit Game"
onTriggered: {
quitAppDialog.appName = appModel.getRunningAppName()
quitAppDialog.segueToStream = false
quitAppDialog.open()
}
visible: model.running
height: visible ? implicitHeight : 0
var component = Qt.createComponent("StreamSegue.qml")
var segue = component.createObject(stackView)
segue.appname = model.name
segue.session = appModel.createSessionForApp(index)
stackView.push(segue)
}
}
}
+60 -113
View File
@@ -6,7 +6,6 @@ import QtQuick.Layouts 1.3
import ComputerModel 1.0
import ComputerManager 1.0
import StreamingPreferences 1.0
GridView {
property ComputerModel computerModel : createModel()
@@ -14,7 +13,7 @@ GridView {
id: pcGrid
anchors.fill: parent
anchors.leftMargin: (parent.width % (cellWidth + anchors.rightMargin)) / 2
anchors.topMargin: 20
anchors.topMargin: 5
anchors.rightMargin: 5
anchors.bottomMargin: 5
cellWidth: 350; cellHeight: 350;
@@ -26,17 +25,9 @@ GridView {
// routine to run, but only if we start as invisible
visible: false
StreamingPreferences {
id: prefs
}
Component.onCompleted: {
// Setup signals on CM
ComputerManager.computerAddCompleted.connect(addComplete)
if (!prefs.hasAnyHardwareAcceleration()) {
noHwDecoderDialog.open()
}
}
onVisibleChanged: {
@@ -81,14 +72,14 @@ GridView {
return model
}
model: computerModel
model: createModel()
delegate: Item {
width: 300; height: 300;
Image {
id: pcIcon
anchors.horizontalCenter: parent.horizontalCenter
anchors.horizontalCenter: parent.horizontalCenter;
source: {
model.addPc ? "qrc:/res/ic_add_to_queue_white_48px.svg" : "qrc:/res/ic_tv_white_48px.svg"
}
@@ -98,30 +89,6 @@ GridView {
}
}
Image {
// TODO: Tooltip
id: stateIcon
anchors.horizontalCenter: pcIcon.horizontalCenter
anchors.verticalCenter: pcIcon.verticalCenter
anchors.verticalCenterOffset: -10
visible: !model.addPc && !model.statusUnknown && (!model.online || !model.paired)
source: !model.online ? "qrc:/res/baseline-warning-24px.svg" : "qrc:/res/baseline-lock-24px.svg"
sourceSize {
width: 75
height: 75
}
}
BusyIndicator {
id: statusUnknownSpinner
anchors.horizontalCenter: pcIcon.horizontalCenter
anchors.verticalCenter: pcIcon.verticalCenter
anchors.verticalCenterOffset: -10
width: 75
height: 75
visible: !model.addPc && model.statusUnknown
}
Text {
id: pcNameText
text: model.name
@@ -134,70 +101,76 @@ GridView {
wrapMode: Text.Wrap
}
Menu {
id: pcContextMenu
MenuItem {
text: "Wake PC"
onTriggered: computerModel.wakeComputer(index)
visible: !model.addPc && !model.online && model.wakeable
height: visible ? implicitHeight : 0
}
MenuItem {
text: "Delete PC"
onTriggered: {
deletePcDialog.pcIndex = index
// get confirmation first, actual closing is called from the dialog
deletePcDialog.open()
Text {
function getStatusText(model)
{
if (model.online) {
var text = "<font color=\"green\">Online</font>"
text += "<font color=\"white\"> - </font>"
if (model.paired) {
text += "<font color=\"skyblue\">Paired</font>"
}
else if (model.busy) {
text += "<font color=\"red\">Busy</font>"
}
else {
text += "<font color=\"red\">Not Paired</font>"
}
return text
}
else {
return "<font color=\"red\">Offline</font>";
}
}
id: pcPairedText
text: getStatusText(model)
visible: !model.addPc
width: parent.width
anchors.top: pcNameText.bottom
font.pointSize: 24
horizontalAlignment: Text.AlignHCenter
}
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.LeftButton | Qt.RightButton;
onClicked: {
if(mouse.button === Qt.LeftButton) {
if (model.addPc) {
addPcDialog.open()
if (model.addPc) {
addPcDialog.open()
}
else if (model.online) {
if (model.paired) {
// go to game view
var component = Qt.createComponent("AppView.qml")
var appView = component.createObject(stackView)
appView.computerIndex = index
appView.objectName = model.name
stackView.push(appView)
}
else if (model.online) {
if (model.paired) {
// go to game view
var component = Qt.createComponent("AppView.qml")
var appView = component.createObject(stackView)
appView.computerIndex = index
appView.objectName = model.name
stackView.push(appView)
else {
if (!model.busy) {
var pin = ("0000" + Math.floor(Math.random() * 10000)).slice(-4)
// Stop polling, since pairing may make GFE unresponsive
ComputerManager.stopPollingAsync()
// Kick off pairing in the background
computerModel.pairComputer(index, pin)
// Display the pairing dialog
pairDialog.pin = pin
pairDialog.open()
}
else {
if (!model.busy) {
var pin = ("0000" + Math.floor(Math.random() * 10000)).slice(-4)
// Stop polling, since pairing may make GFE unresponsive
ComputerManager.stopPollingAsync()
// Kick off pairing in the background
computerModel.pairComputer(index, pin)
// Display the pairing dialog
pairDialog.pin = pin
pairDialog.open()
}
else {
// cannot pair while something is streaming or attempting to pair
errorDialog.text = "This PC is currently busy. Make sure to quit any running games and try again."
errorDialog.open()
}
// cannot pair while something is streaming or attempting to pair
errorDialog.text = "This PC is currently busy. Make sure to quit any running games and try again."
errorDialog.open()
}
} else if(!model.online) {
pcContextMenu.open()
}
}
else { // right click
if(!model.addPc) { // but only for actual PCs, not the add-pc option
pcContextMenu.open()
}
else {
// TODO: Wake on LAN and delete PC options
}
}
}
@@ -211,19 +184,6 @@ GridView {
standardButtons: StandardButton.Ok
}
MessageDialog {
id: noHwDecoderDialog
modality:Qt.WindowModal
icon: StandardIcon.Warning
standardButtons: StandardButton.Ok | StandardButton.Help
text: "No functioning hardware accelerated H.264 video decoder was detected by Moonlight. " +
"Your streaming performance may be severely degraded in this configuration. " +
"Click the Help button for more information on solving this problem."
onHelp: {
Qt.openUrlExternally("https://github.com/moonlight-stream/moonlight-docs/wiki/Fixing-Hardware-Decoding-Problems");
}
}
MessageDialog {
id: pairDialog
// don't allow edits to the rest of the window while open
@@ -236,19 +196,6 @@ GridView {
}
}
MessageDialog {
id: deletePcDialog
// don't allow edits to the rest of the window while open
modality:Qt.WindowModal
property int pcIndex : -1;
text:"Are you sure you want to remove this PC?"
standardButtons: StandardButton.Yes | StandardButton.No
onYes: {
console.log("deleting PC pairing for PC at index: " + pcIndex)
computerModel.deleteComputer(pcIndex);
}
}
Dialog {
id: addPcDialog
property string label: "Enter the IP address of your GameStream PC"
-86
View File
@@ -1,86 +0,0 @@
import QtQuick 2.0
import QtQuick.Controls 2.2
import QtQuick.Dialogs 1.3
import ComputerManager 1.0
import Session 1.0
Item {
property string appName
property Session nextSession : null
property string nextAppName : ""
property string stageText : "Quitting " + appName + "..."
anchors.fill: parent
// The StackView will trigger a visibility change when
// we're pushed onto it, causing our onVisibleChanged
// routine to run, but only if we start as invisible
visible: false
function quitAppCompleted(error)
{
// Display a failed dialog if we got an error
if (error !== undefined) {
errorDialog.text = error
errorDialog.open()
}
// Exit this view
stackView.pop()
// If we're supposed to launch another game after this, do so now
if (error === undefined && nextSession !== null) {
var component = Qt.createComponent("StreamSegue.qml")
var segue = component.createObject(stackView)
segue.appName = nextAppName
segue.session = nextSession
stackView.push(segue)
}
}
onVisibleChanged: {
if (visible) {
// Connect the quit completion signal
ComputerManager.quitAppCompleted.connect(quitAppCompleted)
// We must be polling or we won't get the quitAppCompleted() callback
ComputerManager.startPolling()
}
else {
// Disconnect the signal
ComputerManager.quitAppCompleted.disconnect(quitAppCompleted)
// Stop polling when this view is not on top
ComputerManager.stopPollingAsync()
}
}
Row {
anchors.centerIn: parent
spacing: 5
BusyIndicator {
id: stageSpinner
}
Label {
id: stageLabel
height: stageSpinner.height
text: stageText
font.pointSize: 20
verticalAlignment: Text.AlignVCenter
wrapMode: Text.Wrap
color: "white"
}
}
MessageDialog {
id: errorDialog
modality:Qt.WindowModal
icon: StandardIcon.Critical
standardButtons: StandardButton.Ok
}
}
+116 -293
View File
@@ -16,13 +16,14 @@ ScrollView {
}
Column {
padding: 10
id: settingsColumn1
width: settingsPage.width / 2 - padding
x: 10
y: 10
width: settingsPage.width
height: 400
GroupBox {
id: basicSettingsGroupBox
width: (parent.width - 2 * parent.padding)
width: (parent.width - 20)
padding: 12
title: "<font color=\"skyblue\">Basic Settings</font>"
font.pointSize: 12
@@ -34,7 +35,7 @@ ScrollView {
Label {
width: parent.width
id: resFPStitle
text: qsTr("Resolution and FPS")
text: qsTr("Resolution and FPS target")
font.pointSize: 12
wrapMode: Text.Wrap
color: "white"
@@ -43,153 +44,84 @@ ScrollView {
Label {
width: parent.width
id: resFPSdesc
text: qsTr("Setting values too high for your PC may cause lag, stuttering, or errors.")
text: qsTr("Setting values too high for your device may cause lag or crashes")
font.pointSize: 9
wrapMode: Text.Wrap
color: "white"
}
Row {
spacing: 5
ComboBox {
// ignore setting the index at first, and actually set it when the component is loaded
Component.onCompleted: {
// Add native resolutions for all attached displays
for (var displayIndex = 0;; displayIndex++) {
var screenRect = prefs.getDisplayResolution(displayIndex)
if (screenRect.width === 0) {
// Exceeded max count of displays
break
}
var indexToAdd = 0
for (var j = 0; j < resolutionComboBox.count; j++) {
var existing_width = parseInt(resolutionListModel.get(j).video_width);
var existing_height = parseInt(resolutionListModel.get(j).video_height);
if (screenRect.width === existing_width && screenRect.height === existing_height) {
// Duplicate entry, skip
indexToAdd = -1
break
}
else if (screenRect.width * screenRect.height > existing_width * existing_height) {
// Candidate entrypoint after this entry
indexToAdd = j + 1
}
}
// Insert this display's resolution if it's not a duplicate
if (indexToAdd >= 0) {
resolutionListModel.insert(indexToAdd,
{
"text": "Native ("+screenRect.width+"x"+screenRect.height+")",
"video_width": ""+screenRect.width,
"video_height": ""+screenRect.height
})
}
}
// load the saved width/height, and iterate through the ComboBox until a match is found
// and set it to that index.
var saved_width = prefs.width
var saved_height = prefs.height
currentIndex = 0
for (var i = 0; i < resolutionComboBox.count; i++) {
var el_width = parseInt(resolutionListModel.get(i).video_width);
var el_height = parseInt(resolutionListModel.get(i).video_height);
// Pick the highest value lesser or equal to the saved resolution
if (saved_width * saved_height >= el_width * el_height) {
currentIndex = i
}
}
}
id: resolutionComboBox
width: 200
textRole: "text"
model: ListModel {
id: resolutionListModel
// Other elements may be added at runtime
// based on attached display resolution
ListElement {
text: "720p"
video_width: "1280"
video_height: "720"
}
ListElement {
text: "1080p"
video_width: "1920"
video_height: "1080"
}
ListElement {
text: "1440p"
video_width: "2560"
video_height: "1440"
}
ListElement {
text: "4K"
video_width: "3840"
video_height: "2160"
}
}
// ::onActivated must be used, as it only listens for when the index is changed by a human
onActivated : {
prefs.width = parseInt(resolutionListModel.get(currentIndex).video_width)
prefs.height = parseInt(resolutionListModel.get(currentIndex).video_height)
prefs.bitrateKbps = prefs.getDefaultBitrate(prefs.width, prefs.height, prefs.fps);
slider.value = prefs.bitrateKbps
ComboBox {
// ignore setting the index at first, and actually set it when the component is loaded
Component.onCompleted: {
// load the saved width/height/fps, and iterate through the ComboBox until a match is found
// set it to that index.
var saved_width = prefs.width
var saved_height = prefs.height
var saved_fps = prefs.fps
currentIndex = 0
for(var i = 0; i < resolutionComboBox.count; i++) {
var el_width = parseInt(resolutionListModel.get(i).video_width);
var el_height = parseInt(resolutionListModel.get(i).video_height);
var el_fps = parseInt(resolutionListModel.get(i).video_fps);
if(saved_width === el_width &&
saved_height === el_height &&
saved_fps === el_fps) {
currentIndex = i
}
}
}
ComboBox {
// ignore setting the index at first, and actually set it when the component is loaded
Component.onCompleted: {
// Get the max supported FPS on this system
var max_fps = prefs.getMaximumStreamingFrameRate();
// Use 64 as the cutoff for adding a separate option to
// handle wonky displays that report just over 60 Hz.
if (max_fps > 64) {
fpsListModel.append({"text": max_fps+" FPS", "video_fps": ""+max_fps})
}
var saved_fps = prefs.fps
currentIndex = 0
for (var i = 0; i < fpsComboBox.count; i++) {
var el_fps = parseInt(fpsListModel.get(i).video_fps);
// Pick the highest value lesser or equal to the saved FPS
if (saved_fps >= el_fps) {
currentIndex = i
}
}
id: resolutionComboBox
width: Math.min(bitrateDesc.implicitWidth, parent.width)
font.pointSize: 9
textRole: "text"
model: ListModel {
id: resolutionListModel
ListElement {
text: "720p 30 FPS"
video_width: "1280"
video_height: "720"
video_fps: "30"
}
id: fpsComboBox
textRole: "text"
model: ListModel {
id: fpsListModel
ListElement {
text: "30 FPS"
video_fps: "30"
}
ListElement {
text: "60 FPS"
video_fps: "60"
}
// A higher value may be added at runtime
// based on the attached display refresh rate
ListElement {
text: "720p 60 FPS"
video_width: "1280"
video_height: "720"
video_fps: "60"
}
// ::onActivated must be used, as it only listens for when the index is changed by a human
onActivated : {
prefs.fps = parseInt(fpsListModel.get(currentIndex).video_fps)
prefs.bitrateKbps = prefs.getDefaultBitrate(prefs.width, prefs.height, prefs.fps);
slider.value = prefs.bitrateKbps
ListElement {
text: "1080p 30 FPS"
video_width: "1920"
video_height: "1080"
video_fps: "30"
}
ListElement {
text: "1080p 60 FPS"
video_width: "1920"
video_height: "1080"
video_fps: "60"
}
ListElement {
text: "4K 30 FPS"
video_width: "3840"
video_height: "2160"
video_fps: "30"
}
ListElement {
text: "4K 60 FPS"
video_width: "3840"
video_height: "2160"
video_fps: "60"
}
}
// ::onActivated must be used, as it only listens for when the index is changed by a human
onActivated : {
prefs.width = parseInt(resolutionListModel.get(currentIndex).video_width)
prefs.height = parseInt(resolutionListModel.get(currentIndex).video_height)
prefs.fps = parseInt(resolutionListModel.get(currentIndex).video_fps)
prefs.bitrateKbps = prefs.getDefaultBitrate(prefs.width, prefs.height, prefs.fps);
slider.value = prefs.bitrateKbps
}
}
@@ -219,7 +151,7 @@ ScrollView {
stepSize: 500
from : 500
to: 150000
to: 100000
snapMode: "SnapOnRelease"
width: Math.min(bitrateDesc.implicitWidth, parent.width)
@@ -245,7 +177,7 @@ ScrollView {
GroupBox {
id: audioSettingsGroupBox
width: (parent.width - 2 * parent.padding)
width: (parent.width - 20)
padding: 12
title: "<font color=\"skyblue\">Audio Settings</font>"
font.pointSize: 12
@@ -254,65 +186,24 @@ ScrollView {
anchors.fill: parent
spacing: 5
Label {
width: parent.width
id: resAudioTitle
text: qsTr("Audio configuration")
font.pointSize: 12
wrapMode: Text.Wrap
color: "white"
}
CheckBox {
id: surroundSoundCheck
text: "<font color=\"white\">Enable 5.1 surround sound</font>"
font.pointSize: 12
ComboBox {
// ignore setting the index at first, and actually set it when the component is loaded
Component.onCompleted: {
var saved_audio = prefs.audioConfig
currentIndex = 0
for(var i = 0; i < audioListModel.count; i++) {
var el_audio= audioListModel.get(i).val;
if(saved_audio === el_audio) {
currentIndex = i
}
}
}
// the backend actually supports auto/stereo/5.1. We'll expose stereo/5.1
checked: prefs.audioConfig === StreamingPreferences.AC_FORCE_SURROUND
id: audioComboBox
width: Math.min(bitrateDesc.implicitWidth, parent.width)
textRole: "text"
model: ListModel {
id: audioListModel
ListElement {
text: "Autodetect"
val: StreamingPreferences.AC_AUTO
}
ListElement {
text: "Stereo"
val: StreamingPreferences.AC_FORCE_STEREO
}
ListElement {
text: "5.1 surround sound"
val: StreamingPreferences.AC_FORCE_SURROUND
}
}
// ::onActivated must be used, as it only listens for when the index is changed by a human
onActivated : {
prefs.audioConfig = audioListModel.get(currentIndex).val
onCheckedChanged: {
prefs.audioConfig = checked ? StreamingPreferences.AC_FORCE_SURROUND : StreamingPreferences.AC_FORCE_STEREO
}
}
}
}
}
Column {
padding: 10
anchors.left: settingsColumn1.right
id: settingsColumn2
width: settingsPage.width / 2 - padding
GroupBox {
id: gamepadSettingsGroupBox
width: (parent.width - parent.padding)
width: (parent.width - 20)
padding: 12
title: "<font color=\"skyblue\">Gamepad Settings</font>"
font.pointSize: 12
@@ -330,12 +221,38 @@ ScrollView {
prefs.multiController = checked
}
}
CheckBox {
id: mouseEmulationCheck
text: "<font color=\"white\">Mouse emulation via gamepad</font>"
font.pointSize: 12
// TODO: make this actually do anything
}
}
}
GroupBox {
id: onScreenControlsGroupBox
width: (parent.width - 20)
padding: 12
title: "<font color=\"skyblue\">On-screen Controls Settings</font>"
font.pointSize: 12
Column {
anchors.fill: parent
spacing: 5
CheckBox {
id: onScreenControlsCheck
text: "<font color=\"white\">Show on-screen controls</font>"
font.pointSize: 12
// TODO: make this actually do anything
}
}
}
GroupBox {
id: hostSettingsGroupBox
width: (parent.width - parent.padding)
width: (parent.width - 20)
padding: 12
title: "<font color=\"skyblue\">Host Settings</font>"
font.pointSize: 12
@@ -368,7 +285,7 @@ ScrollView {
GroupBox {
id: advancedSettingsGroupBox
width: (parent.width - parent.padding)
width: (parent.width - 20)
padding: 12
title: "<font color=\"skyblue\">Advanced Settings</font>"
font.pointSize: 12
@@ -377,105 +294,11 @@ ScrollView {
anchors.fill: parent
spacing: 5
Label {
width: parent.width
id: resVDSTitle
text: qsTr("Video decoder")
font.pointSize: 12
wrapMode: Text.Wrap
color: "white"
CheckBox {
id: neverDropFramesCheck
text: "<font color=\"white\">Never drop frames</font>"
font.pointSize: 12
}
ComboBox {
// ignore setting the index at first, and actually set it when the component is loaded
Component.onCompleted: {
var saved_vds = prefs.videoDecoderSelection
currentIndex = 0
for(var i = 0; i < decoderListModel.count; i++) {
var el_vds = decoderListModel.get(i).val;
if(saved_vds === el_vds) {
currentIndex = i
}
}
}
id: decoderComboBox
width: Math.min(bitrateDesc.implicitWidth, parent.width)
textRole: "text"
model: ListModel {
id: decoderListModel
ListElement {
text: "Automatic (Recommended)"
val: StreamingPreferences.VDS_AUTO
}
ListElement {
text: "Force software decoding"
val: StreamingPreferences.VDS_FORCE_SOFTWARE
}
ListElement {
text: "Force hardware decoding"
val: StreamingPreferences.VDS_FORCE_HARDWARE
}
}
// ::onActivated must be used, as it only listens for when the index is changed by a human
onActivated : {
prefs.videoDecoderSelection = decoderListModel.get(currentIndex).val
}
}
Label {
width: parent.width
id: resVCCTitle
text: qsTr("Video codec")
font.pointSize: 12
wrapMode: Text.Wrap
color: "white"
}
ComboBox {
// ignore setting the index at first, and actually set it when the component is loaded
Component.onCompleted: {
var saved_vcc = prefs.videoCodecConfig
currentIndex = 0
for(var i = 0; i < codecListModel.count; i++) {
var el_vcc = codecListModel.get(i).val;
if(saved_vcc === el_vcc) {
currentIndex = i
}
}
}
id: codecComboBox
width: Math.min(bitrateDesc.implicitWidth, parent.width)
textRole: "text"
model: ListModel {
id: codecListModel
ListElement {
text: "Automatic (Recommended)"
val: StreamingPreferences.VCC_AUTO
}
ListElement {
text: "Force H.264"
val: StreamingPreferences.VCC_FORCE_H264
}
ListElement {
text: "Force HEVC"
val: StreamingPreferences.VCC_FORCE_HEVC
}
// HDR seems to be broken in GFE 3.14.1, and even when that's fixed
// we'll probably need to gate this feature on OS support in our
// renderers.
/* ListElement {
text: "Force HEVC HDR"
val: StreamingPreferences.VCC_FORCE_HEVC_HDR
} */
}
// ::onActivated must be used, as it only listens for when the index is changed by a human
onActivated : {
prefs.videoCodecConfig = codecListModel.get(currentIndex).val
}
}
}
}
}
+3 -9
View File
@@ -6,8 +6,8 @@ import Session 1.0
Item {
property Session session
property string appName
property string stageText : "Starting " + appName + "..."
property string appname
property string stageText : "Starting " + appname + "..."
anchors.fill: parent
@@ -47,13 +47,7 @@ Item {
function displayLaunchWarning(text)
{
// This toast appears for 3 seconds, just shorter than how long
// Session will wait for it to be displayed. This gives it time
// to transition to invisible before continuing.
var toast = Qt.createQmlObject('import QtQuick.Controls 2.3; ToolTip {}', parent, '')
toast.text = text
toast.timeout = 3000
toast.visible = true
// TODO: toast
}
onVisibleChanged: {
+5 -41
View File
@@ -19,32 +19,6 @@ void AppModel::initialize(ComputerManager* computerManager, int computerIndex)
m_CurrentGameId = m_Computer->currentGameId;
}
int AppModel::getRunningAppIndex()
{
if (m_CurrentGameId != 0) {
for (int i = 0; i < m_Apps.count(); i++) {
if (m_Apps[i].id == m_CurrentGameId) {
return i;
}
}
}
return -1;
}
QString AppModel::getRunningAppName()
{
if (m_CurrentGameId != 0) {
for (int i = 0; i < m_Apps.count(); i++) {
if (m_Apps[i].id == m_CurrentGameId) {
return m_Apps[i].name;
}
}
}
return nullptr;
}
Session* AppModel::createSessionForApp(int appIndex)
{
Q_ASSERT(appIndex < m_Apps.count());
@@ -96,11 +70,6 @@ QHash<int, QByteArray> AppModel::roleNames() const
return names;
}
void AppModel::quitRunningApp()
{
m_ComputerManager->quitRunningApp(m_Computer);
}
void AppModel::handleComputerStateChanged(NvComputer* computer)
{
// Ignore updates for computers that aren't ours
@@ -154,15 +123,10 @@ void AppModel::handleBoxArtLoaded(NvComputer* computer, NvApp app, QUrl /* image
Q_ASSERT(computer == m_Computer);
int index = m_Apps.indexOf(app);
Q_ASSERT(index >= 0);
// Make sure we're not delivering a callback to an app that's already been removed
if (index >= 0) {
// Let our view know the box art data has changed for this app
emit dataChanged(createIndex(index, 0),
createIndex(index, 0),
QVector<int>() << BoxArtRole);
}
else {
qWarning() << "App not found for box art callback:" << app.name;
}
// Let our view know the box art data has changed for this app
emit dataChanged(createIndex(index, 0),
createIndex(index, 0),
QVector<int>() << BoxArtRole);
}
-6
View File
@@ -25,12 +25,6 @@ public:
Q_INVOKABLE Session* createSessionForApp(int appIndex);
Q_INVOKABLE int getRunningAppIndex();
Q_INVOKABLE QString getRunningAppName();
Q_INVOKABLE void quitRunningApp();
QVariant data(const QModelIndex &index, int role) const override;
int rowCount(const QModelIndex &parent) const override;
-6
View File
@@ -46,12 +46,8 @@ QVariant ComputerModel::data(const QModelIndex& index, int role) const
return computer->pairState == NvComputer::PS_PAIRED;
case BusyRole:
return computer->currentGameId != 0;
case WakeableRole:
return !computer->macAddress.isEmpty();
case AddPcRole:
return false;
case StatusUnknownRole:
return computer->state == NvComputer::CS_UNKNOWN;
default:
return QVariant();
}
@@ -78,8 +74,6 @@ QHash<int, QByteArray> ComputerModel::roleNames() const
names[PairedRole] = "paired";
names[BusyRole] = "busy";
names[AddPcRole] = "addPc";
names[WakeableRole] = "wakeable";
names[StatusUnknownRole] = "statusUnknown";
return names;
}
-2
View File
@@ -13,8 +13,6 @@ class ComputerModel : public QAbstractListModel
OnlineRole,
PairedRole,
BusyRole,
WakeableRole,
StatusUnknownRole,
AddPcRole
};
-3
View File
@@ -80,9 +80,6 @@ ApplicationWindow {
}
ToolButton {
// TODO: Implement gamepad mapping then unhide this button
visible: false
icon.source: "qrc:/res/ic_videogame_asset_white_48px.svg"
onClicked: navigateTo("qrc:/gui/GamepadMapper.qml", "Gamepad Mapping")
+5 -129
View File
@@ -2,8 +2,6 @@
#include <QQmlApplicationEngine>
#include <QIcon>
#include <QQuickStyle>
#include <QMutex>
#include <QtDebug>
// Don't let SDL hook our main function, since Qt is already
// doing the same thing. This needs to be before any headers
@@ -16,134 +14,8 @@
#include "streaming/session.hpp"
#include "settings/streamingpreferences.h"
#if !defined(QT_DEBUG) && defined(Q_OS_WIN32)
// Log to file for release Windows builds
#define USE_CUSTOM_LOGGER
#define LOG_TO_FILE
#elif defined(Q_OS_UNIX) && !defined(Q_OS_DARWIN)
// Use stdout logger on all Linux/BSD builds
#define USE_CUSTOM_LOGGER
#elif !defined(QT_DEBUG) && defined(Q_OS_DARWIN)
// Log to file for release Mac builds
#define USE_CUSTOM_LOGGER
#define LOG_TO_FILE
#else
// For debug Windows and Mac builds, use default logger
#endif
#ifdef USE_CUSTOM_LOGGER
static QTime s_LoggerTime;
static QTextStream s_LoggerStream(stdout);
static QMutex s_LoggerLock;
#ifdef LOG_TO_FILE
#define MAX_LOG_LINES 10000
static int s_LogLinesWritten = 0;
static bool s_LogLimitReached = false;
static QFile* s_LoggerFile;
#endif
void logToLoggerStream(QString& message)
{
QMutexLocker lock(&s_LoggerLock);
#ifdef LOG_TO_FILE
if (s_LogLimitReached) {
return;
}
else if (s_LogLinesWritten == MAX_LOG_LINES) {
s_LoggerStream << "Log size limit reached!" << endl;
s_LogLimitReached = true;
return;
}
else {
s_LogLinesWritten++;
}
#endif
s_LoggerStream << message << endl;
}
void sdlLogToDiskHandler(void*, int category, SDL_LogPriority priority, const char* message)
{
QString priorityTxt;
switch (priority) {
case SDL_LOG_PRIORITY_VERBOSE:
priorityTxt = "Verbose";
break;
case SDL_LOG_PRIORITY_DEBUG:
priorityTxt = "Debug";
break;
case SDL_LOG_PRIORITY_INFO:
priorityTxt = "Info";
break;
case SDL_LOG_PRIORITY_WARN:
priorityTxt = "Warn";
break;
case SDL_LOG_PRIORITY_ERROR:
priorityTxt = "Error";
break;
case SDL_LOG_PRIORITY_CRITICAL:
priorityTxt = "Critical";
break;
default:
priorityTxt = "Unknown";
break;
}
QTime logTime = QTime::fromMSecsSinceStartOfDay(s_LoggerTime.elapsed());
QString txt = QString("%1 - SDL %2 (%3): %4").arg(logTime.toString()).arg(priorityTxt).arg(category).arg(message);
logToLoggerStream(txt);
}
void qtLogToDiskHandler(QtMsgType type, const QMessageLogContext&, const QString& msg)
{
QString typeTxt;
switch (type) {
case QtDebugMsg:
typeTxt = "Debug";
break;
case QtInfoMsg:
typeTxt = "Info";
break;
case QtWarningMsg:
typeTxt = "Warning";
break;
case QtCriticalMsg:
typeTxt = "Critical";
break;
case QtFatalMsg:
typeTxt = "Fatal";
break;
}
QTime logTime = QTime::fromMSecsSinceStartOfDay(s_LoggerTime.elapsed());
QString txt = QString("%1 - Qt %2: %3").arg(logTime.toString()).arg(typeTxt).arg(msg);
logToLoggerStream(txt);
}
#endif
int main(int argc, char *argv[])
{
#ifdef USE_CUSTOM_LOGGER
#ifdef LOG_TO_FILE
QDir tempDir(QDir::tempPath());
s_LoggerFile = new QFile(tempDir.filePath(QString("Moonlight-%1.log").arg(QDateTime::currentSecsSinceEpoch())));
if (s_LoggerFile->open(QIODevice::WriteOnly)) {
qInfo() << "Redirecting log output to " << s_LoggerFile->fileName();
s_LoggerStream.setDevice(s_LoggerFile);
}
#endif
s_LoggerTime.start();
qInstallMessageHandler(qtLogToDiskHandler);
SDL_LogSetOutputFunction(sdlLogToDiskHandler, nullptr);
#endif
QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
// This avoids using the default keychain for SSL, which may cause
@@ -181,8 +53,12 @@ int main(int argc, char *argv[])
if (engine.rootObjects().isEmpty())
return -1;
// Ensure that SDL is always initialized since we may need to use it
// for non-streaming purposes (like checking on audio devices)
SDL_SetMainReady();
if (SDL_Init(0) != 0) {
if (SDL_Init(SDL_INIT_VIDEO |
SDL_INIT_AUDIO |
SDL_INIT_GAMECONTROLLER) != 0) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"SDL_Init() failed: %s",
SDL_GetError());
Binary file not shown.
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 345 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.4 KiB

-1
View File
@@ -6,6 +6,5 @@
<file>gui/SettingsView.qml</file>
<file>gui/StreamSegue.qml</file>
<file>gui/GamepadMapper.qml</file>
<file>gui/QuitSegue.qml</file>
</qresource>
</RCC>
-4
View File
@@ -1,4 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="#FFFFFF" width="24" height="24" viewBox="0 0 24 24">
<path d="M0 0h24v24H0z" fill="none"/>
<path d="M18 8h-1V6c0-2.76-2.24-5-5-5S7 3.24 7 6v2H6c-1.1 0-2 .9-2 2v10c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V10c0-1.1-.9-2-2-2zm-6 9c-1.1 0-2-.9-2-2s.9-2 2-2 2 .9 2 2-.9 2-2 2zm3.1-9H8.9V6c0-1.71 1.39-3.1 3.1-3.1 1.71 0 3.1 1.39 3.1 3.1v2z"/>
</svg>

Before

Width:  |  Height:  |  Size: 390 B

@@ -1,4 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48" fill="#fff">
<path d="M0 0h48v48H0z" fill="none"/>
<path d="M24 4C12.95 4 4 12.95 4 24s8.95 20 20 20 20-8.95 20-20S35.05 4 24 4zm-4 29V15l12 9-12 9z"/>
</svg>

Before

Width:  |  Height:  |  Size: 250 B

-4
View File
@@ -1,4 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="#FFFFFF" width="24" height="24" viewBox="0 0 24 24">
<path d="M0 0h24v24H0z" fill="none"/>
<path d="M1 21h22L12 2 1 21zm12-3h-2v-2h2v2zm0-4h-2v-4h2v4z"/>
</svg>

Before

Width:  |  Height:  |  Size: 215 B

Binary file not shown.
+22 -68
View File
@@ -1,69 +1,23 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
id="svg3761"
viewBox="0 0 680 680"
height="680"
width="680"
version="1.0">
<metadata
id="metadata3767">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<defs
id="defs3765" />
<g
id="g3759"
transform="translate(-135,491) scale(0.100000,-0.100000)">
<circle
id="circle3741"
fill="#565C64"
r="3400"
cy="1505"
cx="4755" />
<path
id="path3743"
style="fill:#ffffff"
d="M4550 4230 c-582 -48 -1123 -271 -1568 -646 l-84 -70 869 -869 c477 -477 871 -868 876 -868 4 0 6 552 5 1228 l-3 1227 -25 1 c-14 0 -45 -1 -70 -3z" />
<path
id="path3745"
style="fill:#ffffff"
d="M4867 4230 c-10 -9 -9 -2453 1 -2453 4 0 398 391 875 868 l869 869 -84 70 c-344 290 -749 492 -1178 587 -166 36 -469 74 -483 59z" />
<path
id="path3747"
style="fill:#ffffff"
d="M2673 3275 c-241 -285 -412 -595 -526 -953 -63 -196 -111 -458 -119 -640 l-3 -70 1228 -3 c675 -1 1227 1 1227 5 0 5 -391 399 -868 876 l-869 869 -70 -84z" />
<path
id="path3749"
style="fill:#ffffff"
d="M5898 2490 c-478 -477 -868 -871 -868 -876 0 -4 552 -6 1228 -5 l1227 3 -3 70 c-14 326 -124 729 -287 1055 -91 182 -259 430 -387 573 l-43 48 -867 -868z" />
<path
id="path3751"
style="fill:#ffffff"
d="M2026 1388 c-7 -11 10 -198 30 -321 76 -486 291 -952 617 -1338 l70 -84 869 869 c477 477 868 871 868 875 0 11 -2448 10 -2454 -1z" />
<path
id="path3753"
style="fill:#ffffff"
d="M5030 1390 c0 -5 391 -399 868 -876 l869 -869 70 84 c241 285 412 595 526 953 63 196 111 458 119 640 l3 70 -1227 3 c-676 1 -1228 -1 -1228 -5z" />
<path
id="path3755"
style="fill:#ffffff"
d="M3767 359 l-869 -869 84 -70 c285 -241 595 -412 953 -526 196 -63 458 -111 640 -119 l70 -3 3 1228 c1 675 -1 1227 -5 1227 -5 0 -399 -391 -876 -868z" />
<path
id="path3757"
style="fill:#ffffff"
d="M4862 0 l3 -1228 70 3 c182 8 444 56 640 119 358 114 668 285 953 526 l84 70 -869 869 c-477 477 -871 868 -876 868 -4 0 -6 -552 -5 -1227z" />
<svg xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:cc="http://creativecommons.org/ns#" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" version="1.0">
<defs>
<filter id="f1" y="-40%" width="120%" height="220%">
<feOffset xmlns="http://www.w3.org/2000/svg" result="offOut" in="SourceAlpha" dy="-100"/>
<feGaussianBlur result="blurOut" in="offOut" stdDeviation="200"/>
<feBlend in="SourceGraphic" in2="blurOut" mode="normal"/>
</filter>
</defs>
<!-- -105, 521 are within 0.5 pixel-perfect -->
<!-- offsetting more to show shadow -->
<g transform="translate(-95,530) scale(0.100000,-0.100000)" filter="url(#f1)">
<circle cx="4755" cy="1505" r="3700" fill="#565C64">
</circle>
<path d="M4550 4230 c-582 -48 -1123 -271 -1568 -646 l-84 -70 869 -869 c477 -477 871 -868 876 -868 4 0 6 552 5 1228 l-3 1227 -25 1 c-14 0 -45 -1 -70 -3z" style="fill:#ffffff"/>
<path xmlns="http://www.w3.org/2000/svg" d="M4867 4230 c-10 -9 -9 -2453 1 -2453 4 0 398 391 875 868 l869 869 -84 70 c-344 290 -749 492 -1178 587 -166 36 -469 74 -483 59z" style="fill:#ffffff"/>
<path d="M2673 3275 c-241 -285 -412 -595 -526 -953 -63 -196 -111 -458 -119 -640 l-3 -70 1228 -3 c675 -1 1227 1 1227 5 0 5 -391 399 -868 876 l-869 869 -70 -84z" style="fill:#ffffff"/>
<path d="M5898 2490 c-478 -477 -868 -871 -868 -876 0 -4 552 -6 1228 -5 l1227 3 -3 70 c-14 326 -124 729 -287 1055 -91 182 -259 430 -387 573 l-43 48 -867 -868z" style="fill:#ffffff"/>
<path d="M2026 1388 c-7 -11 10 -198 30 -321 76 -486 291 -952 617 -1338 l70 -84 869 869 c477 477 868 871 868 875 0 11 -2448 10 -2454 -1z" style="fill:#ffffff"/>
<path d="M5030 1390 c0 -5 391 -399 868 -876 l869 -869 70 84 c241 285 412 595 526 953 63 196 111 458 119 640 l3 70 -1227 3 c-676 1 -1228 -1 -1228 -5z" style="fill:#ffffff"/>
<path d="M3767 359 l-869 -869 84 -70 c285 -241 595 -412 953 -526 196 -63 458 -111 640 -119 l70 -3 3 1228 c1 675 -1 1227 -5 1227 -5 0 -399 -391 -876 -868z" style="fill:#ffffff"/>
<path d="M4862 0 l3 -1228 70 3 c182 8 444 56 640 119 358 114 668 285 953 526 l84 70 -869 869 c-477 477 -871 868 -876 868 -4 0 -6 -552 -5 -1227z" style="fill:#ffffff"/>
</g>
</svg>
</svg>

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

-3
View File
@@ -5,9 +5,6 @@
<file>res/ic_videogame_asset_white_48px.svg</file>
<file>res/ic_tv_white_48px.svg</file>
<file>res/ic_add_to_queue_white_48px.svg</file>
<file>res/baseline-lock-24px.svg</file>
<file>res/baseline-play_circle_filled_white-48px.svg</file>
<file>res/baseline-warning-24px.svg</file>
<file>res/no_app_image.png</file>
<file>res/settings.svg</file>
<file>res/arrow_left.svg</file>
+14 -79
View File
@@ -1,5 +1,4 @@
#include "streamingpreferences.h"
#include "streaming/session.hpp"
#include <QSettings>
@@ -58,88 +57,24 @@ void StreamingPreferences::save()
settings.setValue(SER_VIDEODEC, static_cast<int>(videoDecoderSelection));
}
bool StreamingPreferences::hasAnyHardwareAcceleration()
{
// Always use VDS_AUTO to avoid spamming the user with warnings
// if they've forced software decoding.
return Session::isHardwareDecodeAvailable(VDS_AUTO,
VIDEO_FORMAT_H264,
1920, 1080, 60);
}
int StreamingPreferences::getMaximumStreamingFrameRate()
{
// Never let the maximum drop below 60 FPS
int maxFrameRate = 60;
if (SDL_InitSubSystem(SDL_INIT_VIDEO) != 0) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"SDL_InitSubSystem(SDL_INIT_VIDEO) failed: %s",
SDL_GetError());
return maxFrameRate;
}
for (int i = 0; i < SDL_GetNumVideoDisplays(); i++) {
SDL_DisplayMode mode;
if (SDL_GetCurrentDisplayMode(i, &mode) == 0) {
// Cap the frame rate at 120 FPS. Past this, the encoders start
// to max out and drop frames.
maxFrameRate = qMax(maxFrameRate, qMin(120, mode.refresh_rate));
}
}
SDL_QuitSubSystem(SDL_INIT_VIDEO);
return maxFrameRate;
}
QRect StreamingPreferences::getDisplayResolution(int displayIndex)
{
if (SDL_InitSubSystem(SDL_INIT_VIDEO) != 0) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"SDL_InitSubSystem(SDL_INIT_VIDEO) failed: %s",
SDL_GetError());
return QRect();
}
if (displayIndex >= SDL_GetNumVideoDisplays()) {
SDL_QuitSubSystem(SDL_INIT_VIDEO);
return QRect();
}
SDL_DisplayMode mode;
int err = SDL_GetCurrentDisplayMode(displayIndex, &mode);
SDL_QuitSubSystem(SDL_INIT_VIDEO);
if (err == 0) {
return QRect(0, 0, mode.w, mode.h);
}
else {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"SDL_GetCurrentDisplayMode() failed: %s",
SDL_GetError());
return QRect();
}
}
int StreamingPreferences::getDefaultBitrate(int width, int height, int fps)
{
// This table prefers 16:10 resolutions because they are
// only slightly more pixels than the 16:9 equivalents, so
// we don't want to bump those 16:10 resolutions up to the
// next 16:9 slot.
// This covers 1280x720 and 1280x800 too
if (width * height <= 1366 * 768) {
return static_cast<int>(5000 * (fps / 30.0));
if (width * height * fps <= 1280 * 720 * 30) {
return 5000;
}
else if (width * height <= 1920 * 1200) {
return static_cast<int>(10000 * (fps / 30.0));
else if (width * height * fps <= 1280 * 720 * 60) {
return 10000;
}
else if (width * height <= 2560 * 1600) {
return static_cast<int>(20000 * (fps / 30.0));
else if (width * height * fps <= 1920 * 1080 * 30) {
return 10000;
}
else /* if (width * height <= 3840 * 2160) */ {
return static_cast<int>(40000 * (fps / 30.0));
else if (width * height * fps <= 1920 * 1080 * 60) {
return 20000;
}
else if (width * height * fps <= 3840 * 2160 * 30) {
return 40000;
}
else /* if (width * height * fps <= 3840 * 2160 * 60) */ {
return 80000;
}
}
-7
View File
@@ -1,7 +1,6 @@
#pragma once
#include <QObject>
#include <QRect>
class StreamingPreferences : public QObject
{
@@ -15,12 +14,6 @@ public:
Q_INVOKABLE void save();
Q_INVOKABLE static bool hasAnyHardwareAcceleration();
Q_INVOKABLE static int getMaximumStreamingFrameRate();
Q_INVOKABLE QRect getDisplayResolution(int displayIndex);
void reload();
enum AudioConfig
+5 -35
View File
@@ -23,14 +23,6 @@ int Session::sdlDetermineAudioConfiguration()
{
SDL_AudioSpec want, have;
SDL_AudioDeviceID dev;
int ret;
if (SDL_InitSubSystem(SDL_INIT_AUDIO) != 0) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"SDL_InitSubSystem(SDL_INIT_AUDIO) failed: %s",
SDL_GetError());
return AUDIO_CONFIGURATION_STEREO;
}
SDL_zero(want);
want.freq = 48000;
@@ -46,8 +38,7 @@ int Session::sdlDetermineAudioConfiguration()
"Failed to open audio device");
// We'll probably have issues during audio stream init, but we'll
// try anyway
ret = AUDIO_CONFIGURATION_STEREO;
goto Exit;
return AUDIO_CONFIGURATION_STEREO;
}
SDL_CloseAudioDevice(dev);
@@ -59,15 +50,11 @@ int Session::sdlDetermineAudioConfiguration()
// We don't support quadraphonic or 7.1 surround, but SDL
// should be able to downmix or upmix better from 5.1 than
// from stereo, so use 5.1 in non-stereo cases.
ret = AUDIO_CONFIGURATION_51_SURROUND;
return AUDIO_CONFIGURATION_51_SURROUND;
}
else {
ret = AUDIO_CONFIGURATION_STEREO;
return AUDIO_CONFIGURATION_STEREO;
}
Exit:
SDL_QuitSubSystem(SDL_INIT_AUDIO);
return ret;
}
int Session::sdlAudioInit(int /* audioConfiguration */,
@@ -77,14 +64,6 @@ int Session::sdlAudioInit(int /* audioConfiguration */,
SDL_AudioSpec want, have;
int error;
SDL_assert(!SDL_WasInit(SDL_INIT_AUDIO));
if (SDL_InitSubSystem(SDL_INIT_AUDIO) != 0) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"SDL_InitSubSystem(SDL_INIT_AUDIO) failed: %s",
SDL_GetError());
return AUDIO_CONFIGURATION_STEREO;
}
SDL_zero(want);
want.freq = opusConfig->sampleRate;
want.format = AUDIO_S16;
@@ -101,7 +80,6 @@ int Session::sdlAudioInit(int /* audioConfiguration */,
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Failed to open audio device: %s",
SDL_GetError());
SDL_QuitSubSystem(SDL_INIT_AUDIO);
return -1;
}
@@ -117,7 +95,6 @@ int Session::sdlAudioInit(int /* audioConfiguration */,
error);
SDL_CloseAudioDevice(s_AudioDevice);
s_AudioDevice = 0;
SDL_QuitSubSystem(SDL_INIT_AUDIO);
return -2;
}
@@ -127,7 +104,7 @@ int Session::sdlAudioInit(int /* audioConfiguration */,
// pending data count. Get a baseline so we
// can exclude that data.
s_BaselinePendingData = 0;
#ifdef Q_OS_WIN32
#ifdef _WIN32
for (int i = 0; i < 100; i++) {
s_BaselinePendingData = qMax(s_BaselinePendingData, SDL_GetQueuedAudioSize(s_AudioDevice));
SDL_Delay(10);
@@ -142,10 +119,6 @@ int Session::sdlAudioInit(int /* audioConfiguration */,
s_SampleIndex = 0;
s_PendingDrops = s_PendingHardDrops = 0;
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Audio stream has %d channels",
opusConfig->channelCount);
return 0;
}
@@ -167,10 +140,7 @@ void Session::sdlAudioCleanup()
s_AudioDevice = 0;
opus_multistream_decoder_destroy(s_OpusDecoder);
s_OpusDecoder = nullptr;
SDL_QuitSubSystem(SDL_INIT_AUDIO);
SDL_assert(!SDL_WasInit(SDL_INIT_AUDIO));
s_OpusDecoder = NULL;
}
void Session::sdlAudioDecodeAndPlaySample(char* sampleData, int sampleLength)
+94 -115
View File
@@ -2,8 +2,6 @@
#include <SDL.h>
#include "streaming/session.hpp"
#include <QtGlobal>
#define VK_0 0x30
#define VK_A 0x41
@@ -11,7 +9,7 @@
#ifndef VK_F1
#define VK_F1 0x70
#define VK_F13 0x7C
#define VK_NUMPAD0 0x60
#define VK_NUMPAD1 0x61
#endif
const int SdlInputHandler::k_ButtonMap[] = {
@@ -25,18 +23,6 @@ const int SdlInputHandler::k_ButtonMap[] = {
SdlInputHandler::SdlInputHandler(bool multiController)
: m_MultiController(multiController)
{
// Allow gamepad input when the app doesn't have focus
SDL_SetHint(SDL_HINT_JOYSTICK_ALLOW_BACKGROUND_EVENTS, "1");
// We need to reinit this each time, since you only get
// an initial set of gamepad arrival events once per init.
SDL_assert(!SDL_WasInit(SDL_INIT_GAMECONTROLLER));
if (SDL_InitSubSystem(SDL_INIT_GAMECONTROLLER) != 0) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"SDL_InitSubSystem(SDL_INIT_GAMECONTROLLER) failed: %s",
SDL_GetError());
}
if (!m_MultiController) {
// Player 1 is always present in non-MC mode
m_GamepadMask = 0x1;
@@ -52,9 +38,6 @@ SdlInputHandler::~SdlInputHandler()
SDL_GameControllerClose(m_GamepadState[i].controller);
}
}
SDL_QuitSubSystem(SDL_INIT_GAMECONTROLLER);
SDL_assert(!SDL_WasInit(SDL_INIT_GAMECONTROLLER));
}
void SdlInputHandler::handleKeyEvent(SDL_KeyboardEvent* event)
@@ -68,26 +51,26 @@ void SdlInputHandler::handleKeyEvent(SDL_KeyboardEvent* event)
(event->keysym.mod & KMOD_ALT) &&
(event->keysym.mod & KMOD_SHIFT)) {
// Force raise all keys in the combo to avoid
// leaving them down after disconnecting
LiSendKeyboardEvent(0xA0, KEY_ACTION_UP, 0);
LiSendKeyboardEvent(0xA1, KEY_ACTION_UP, 0);
LiSendKeyboardEvent(0xA2, KEY_ACTION_UP, 0);
LiSendKeyboardEvent(0xA3, KEY_ACTION_UP, 0);
LiSendKeyboardEvent(0xA4, KEY_ACTION_UP, 0);
LiSendKeyboardEvent(0xA5, KEY_ACTION_UP, 0);
// Check for quit combo (Ctrl+Alt+Shift+Q)
if (event->keysym.sym == SDLK_q) {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Detected quit key combo");
// Uncapture the mouse to avoid processing any
// further keyboard input
SDL_SetRelativeMouseMode(SDL_FALSE);
// Force raise all keys in the combo to avoid
// leaving them down after disconnecting
LiSendKeyboardEvent(0xA0, KEY_ACTION_UP, 0);
LiSendKeyboardEvent(0xA1, KEY_ACTION_UP, 0);
LiSendKeyboardEvent(0xA2, KEY_ACTION_UP, 0);
LiSendKeyboardEvent(0xA3, KEY_ACTION_UP, 0);
LiSendKeyboardEvent(0xA4, KEY_ACTION_UP, 0);
LiSendKeyboardEvent(0xA5, KEY_ACTION_UP, 0);
SDL_Event event;
// Drain the event queue of any additional input
// that might be processed before our quit message.
while (SDL_PollEvent(&event));
// Push a quit event to the main loop
event.type = SDL_QUIT;
event.quit.timestamp = SDL_GetTicks();
@@ -105,7 +88,12 @@ void SdlInputHandler::handleKeyEvent(SDL_KeyboardEvent* event)
else if (event->keysym.sym == SDLK_x) {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Detected full-screen toggle combo");
Session::s_ActiveSession->toggleFullscreen();
if (SDL_GetWindowFlags(Session::s_ActiveSession->m_Window) & SDL_WINDOW_FULLSCREEN) {
SDL_SetWindowFullscreen(Session::s_ActiveSession->m_Window, 0);
}
else {
SDL_SetWindowFullscreen(Session::s_ActiveSession->m_Window, SDL_WINDOW_FULLSCREEN);
}
return;
}
}
@@ -127,199 +115,195 @@ void SdlInputHandler::handleKeyEvent(SDL_KeyboardEvent* event)
modifiers |= MODIFIER_SHIFT;
}
// Set keycode. We explicitly use scancode here because GFE will try to correct
// for AZERTY layouts on the host but it depends on receiving VK_ values matching
// a QWERTY layout to work.
if (event->keysym.scancode >= SDL_SCANCODE_1 && event->keysym.scancode <= SDL_SCANCODE_9) {
// SDL defines SDL_SCANCODE_0 > SDL_SCANCODE_9, so we need to handle that manually
keyCode = (event->keysym.scancode - SDL_SCANCODE_1) + VK_0 + 1;
// Set keycode
if (event->keysym.sym >= SDLK_0 && event->keysym.sym <= SDLK_9) {
keyCode = (event->keysym.sym - SDLK_0) + VK_0;
}
else if (event->keysym.scancode >= SDL_SCANCODE_A && event->keysym.scancode <= SDL_SCANCODE_Z) {
keyCode = (event->keysym.scancode - SDL_SCANCODE_A) + VK_A;
else if (event->keysym.sym >= SDLK_a && event->keysym.sym <= SDLK_z) {
keyCode = (event->keysym.sym - SDLK_a) + VK_A;
}
else if (event->keysym.scancode >= SDL_SCANCODE_F1 && event->keysym.scancode <= SDL_SCANCODE_F12) {
keyCode = (event->keysym.scancode - SDL_SCANCODE_F1) + VK_F1;
else if (event->keysym.sym >= SDLK_F1 && event->keysym.sym <= SDLK_F12) {
keyCode = (event->keysym.sym - SDLK_F1) + VK_F1;
}
else if (event->keysym.scancode >= SDL_SCANCODE_F13 && event->keysym.scancode <= SDL_SCANCODE_F24) {
keyCode = (event->keysym.scancode - SDL_SCANCODE_F13) + VK_F13;
else if (event->keysym.sym >= SDLK_F13 && event->keysym.sym <= SDLK_F24) {
keyCode = (event->keysym.sym - SDLK_F13) + VK_F13;
}
else if (event->keysym.scancode >= SDL_SCANCODE_KP_1 && event->keysym.scancode <= SDL_SCANCODE_KP_9) {
// SDL defines SDL_SCANCODE_KP_0 > SDL_SCANCODE_KP_9, so we need to handle that manually
keyCode = (event->keysym.scancode - SDL_SCANCODE_KP_1) + VK_NUMPAD0 + 1;
else if (event->keysym.sym >= SDLK_KP_1 && event->keysym.sym <= SDLK_KP_9) {
// SDL defines SDLK_KP_0 > SDLK_KP_9, so we need to handle that manually
keyCode = (event->keysym.sym - SDLK_KP_1) + VK_NUMPAD1;
}
else {
switch (event->keysym.scancode) {
case SDL_SCANCODE_BACKSPACE:
switch (event->keysym.sym) {
case SDLK_BACKSPACE:
keyCode = 0x08;
break;
case SDL_SCANCODE_TAB:
case SDLK_TAB:
keyCode = 0x09;
break;
case SDL_SCANCODE_CLEAR:
case SDLK_CLEAR:
keyCode = 0x0C;
break;
case SDL_SCANCODE_RETURN:
case SDLK_RETURN:
keyCode = 0x0D;
break;
case SDL_SCANCODE_PAUSE:
case SDLK_PAUSE:
keyCode = 0x13;
break;
case SDL_SCANCODE_CAPSLOCK:
case SDLK_CAPSLOCK:
keyCode = 0x14;
break;
case SDL_SCANCODE_ESCAPE:
case SDLK_ESCAPE:
keyCode = 0x1B;
break;
case SDL_SCANCODE_SPACE:
case SDLK_SPACE:
keyCode = 0x20;
break;
case SDL_SCANCODE_PAGEUP:
case SDLK_PAGEUP:
keyCode = 0x21;
break;
case SDL_SCANCODE_PAGEDOWN:
case SDLK_PAGEDOWN:
keyCode = 0x22;
break;
case SDL_SCANCODE_END:
case SDLK_END:
keyCode = 0x23;
break;
case SDL_SCANCODE_HOME:
case SDLK_HOME:
keyCode = 0x24;
break;
case SDL_SCANCODE_LEFT:
case SDLK_LEFT:
keyCode = 0x25;
break;
case SDL_SCANCODE_UP:
case SDLK_UP:
keyCode = 0x26;
break;
case SDL_SCANCODE_RIGHT:
case SDLK_RIGHT:
keyCode = 0x27;
break;
case SDL_SCANCODE_DOWN:
case SDLK_DOWN:
keyCode = 0x28;
break;
case SDL_SCANCODE_SELECT:
case SDLK_SELECT:
keyCode = 0x29;
break;
case SDL_SCANCODE_EXECUTE:
case SDLK_EXECUTE:
keyCode = 0x2B;
break;
case SDL_SCANCODE_PRINTSCREEN:
case SDLK_PRINTSCREEN:
keyCode = 0x2C;
break;
case SDL_SCANCODE_INSERT:
case SDLK_INSERT:
keyCode = 0x2D;
break;
case SDL_SCANCODE_DELETE:
case SDLK_DELETE:
keyCode = 0x2E;
break;
case SDL_SCANCODE_HELP:
case SDLK_HELP:
keyCode = 0x2F;
break;
case SDL_SCANCODE_KP_0:
// See comment above about why we only handle SDL_SCANCODE_KP_0 here
keyCode = VK_NUMPAD0;
case SDLK_KP_0:
// See comment above about why we only handle KP_0 here
keyCode = 0x60;
break;
case SDL_SCANCODE_0:
// See comment above about why we only handle SDL_SCANCODE_0 here
keyCode = VK_0;
break;
case SDL_SCANCODE_KP_MULTIPLY:
case SDLK_KP_MULTIPLY:
keyCode = 0x6A;
break;
case SDL_SCANCODE_KP_PLUS:
case SDLK_KP_PLUS:
keyCode = 0x6B;
break;
case SDL_SCANCODE_KP_COMMA:
case SDLK_KP_COMMA:
keyCode = 0x6C;
break;
case SDL_SCANCODE_KP_MINUS:
case SDLK_KP_MINUS:
keyCode = 0x6D;
break;
case SDL_SCANCODE_KP_DECIMAL:
case SDLK_KP_DECIMAL:
keyCode = 0x6E;
break;
case SDL_SCANCODE_KP_DIVIDE:
case SDLK_KP_DIVIDE:
keyCode = 0x6F;
break;
case SDL_SCANCODE_NUMLOCKCLEAR:
case SDLK_NUMLOCKCLEAR:
keyCode = 0x90;
break;
case SDL_SCANCODE_SCROLLLOCK:
case SDLK_SCROLLLOCK:
keyCode = 0x91;
break;
case SDL_SCANCODE_LSHIFT:
case SDLK_LSHIFT:
keyCode = 0xA0;
break;
case SDL_SCANCODE_RSHIFT:
case SDLK_RSHIFT:
keyCode = 0xA1;
break;
case SDL_SCANCODE_LCTRL:
case SDLK_LCTRL:
keyCode = 0xA2;
break;
case SDL_SCANCODE_RCTRL:
case SDLK_RCTRL:
keyCode = 0xA3;
break;
case SDL_SCANCODE_LALT:
case SDLK_LALT:
keyCode = 0xA4;
break;
case SDL_SCANCODE_RALT:
case SDLK_RALT:
keyCode = 0xA5;
break;
case SDL_SCANCODE_AC_BACK:
case SDLK_AC_BACK:
keyCode = 0xA6;
break;
case SDL_SCANCODE_AC_FORWARD:
case SDLK_AC_FORWARD:
keyCode = 0xA7;
break;
case SDL_SCANCODE_AC_REFRESH:
case SDLK_AC_REFRESH:
keyCode = 0xA8;
break;
case SDL_SCANCODE_AC_STOP:
case SDLK_AC_STOP:
keyCode = 0xA9;
break;
case SDL_SCANCODE_AC_SEARCH:
case SDLK_AC_SEARCH:
keyCode = 0xAA;
break;
case SDL_SCANCODE_AC_BOOKMARKS:
case SDLK_AC_BOOKMARKS:
keyCode = 0xAB;
break;
case SDL_SCANCODE_AC_HOME:
case SDLK_AC_HOME:
keyCode = 0xAC;
break;
case SDL_SCANCODE_SEMICOLON:
case SDLK_SEMICOLON:
keyCode = 0xBA;
break;
case SDL_SCANCODE_COMMA:
case SDLK_PLUS:
keyCode = 0xBB;
break;
case SDLK_COMMA:
keyCode = 0xBC;
break;
case SDL_SCANCODE_MINUS:
case SDLK_MINUS:
keyCode = 0xBD;
break;
case SDL_SCANCODE_PERIOD:
case SDLK_PERIOD:
keyCode = 0xBE;
break;
case SDL_SCANCODE_SLASH:
case SDLK_SLASH:
keyCode = 0xBF;
break;
case SDL_SCANCODE_GRAVE:
case SDLK_BACKQUOTE:
keyCode = 0xC0;
break;
case SDL_SCANCODE_LEFTBRACKET:
case SDLK_LEFTBRACKET:
keyCode = 0xDB;
break;
case SDL_SCANCODE_BACKSLASH:
case SDLK_BACKSLASH:
keyCode = 0xDC;
break;
case SDL_SCANCODE_RIGHTBRACKET:
case SDLK_RIGHTBRACKET:
keyCode = 0xDD;
break;
case SDL_SCANCODE_APOSTROPHE:
case SDLK_QUOTE:
keyCode = 0xDE;
break;
default:
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Unhandled button event: %d",
event->keysym.scancode);
event->keysym.sym);
return;
}
}
@@ -443,18 +427,13 @@ void SdlInputHandler::handleControllerAxisEvent(SDL_ControllerAxisEvent* event)
state->lsX = event->value;
break;
case SDL_CONTROLLER_AXIS_LEFTY:
// Signed values have one more negative value than
// positive value, so inverting the sign on -32768
// could actually cause the value to overflow and
// wrap around to be negative again. Avoid that by
// capping the value at 32767.
state->lsY = -qMax(event->value, (short)-32767);
state->lsY = -event->value;
break;
case SDL_CONTROLLER_AXIS_RIGHTX:
state->rsX = event->value;
break;
case SDL_CONTROLLER_AXIS_RIGHTY:
state->rsY = -qMax(event->value, (short)-32767);
state->rsY = -event->value;
break;
case SDL_CONTROLLER_AXIS_TRIGGERLEFT:
state->lt = (unsigned char)(event->value * 255UL / 32767);
+111 -398
View File
@@ -5,38 +5,16 @@
#include <SDL.h>
#include "utils.h"
#ifdef HAVE_FFMPEG
#ifdef __APPLE__
#include "video/avfoundation.h"
#endif
#include "video/ffmpeg.h"
#endif
#ifdef HAVE_SLVIDEO
#include "video/sl.h"
#endif
#if defined(Q_OS_WIN32) || defined(Q_OS_DARWIN)
// Using full-screen desktop allows us to avoid needing to enable V-sync on Windows
// and it also avoids some strange flickering issues on my Win7 test machine
// with Intel HD 5500 graphics. It also allows Spaces to work on macOS.
#define SDL_OS_FULLSCREEN_FLAG SDL_WINDOW_FULLSCREEN_DESKTOP
#else
#define SDL_OS_FULLSCREEN_FLAG SDL_WINDOW_FULLSCREEN
#endif
#ifdef Q_OS_WIN32
// Scaling the icon down on Win32 looks dreadful, so render at lower res
#define ICON_SIZE 32
#else
#define ICON_SIZE 64
#endif
#include <openssl/rand.h>
#include <QRandomGenerator>
#include <QtEndian>
#include <QCoreApplication>
#include <QThreadPool>
#include <QSvgRenderer>
#include <QPainter>
#include <QImage>
CONNECTION_LISTENER_CALLBACKS Session::k_ConnCallbacks = {
Session::clStageStarting,
@@ -108,22 +86,21 @@ bool Session::chooseDecoder(StreamingPreferences::VideoDecoderSelection vds,
SDL_Window* window, int videoFormat, int width, int height,
int frameRate, IVideoDecoder*& chosenDecoder)
{
#ifdef HAVE_SLVIDEO
chosenDecoder = new SLVideoDecoder();
#ifdef __APPLE__
chosenDecoder = AVFoundationDecoderFactory::createDecoder();
if (chosenDecoder->initialize(vds, window, videoFormat, width, height, frameRate)) {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"SLVideo video decoder chosen");
"AVFoundation-based video decoder chosen");
return true;
}
else {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Unable to load SLVideo decoder");
"Unable to load AVFoundation decoder");
delete chosenDecoder;
chosenDecoder = nullptr;
}
#endif
#ifdef HAVE_FFMPEG
chosenDecoder = new FFmpegVideoDecoder();
if (chosenDecoder->initialize(vds, window, videoFormat, width, height, frameRate)) {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
@@ -136,11 +113,6 @@ bool Session::chooseDecoder(StreamingPreferences::VideoDecoderSelection vds,
delete chosenDecoder;
chosenDecoder = nullptr;
}
#endif
#if !defined(HAVE_FFMPEG) && !defined(HAVE_SLVIDEO)
#error No video decoding libraries available!
#endif
// If we reach this, we didn't initialize any decoders successfully
return false;
@@ -148,17 +120,12 @@ bool Session::chooseDecoder(StreamingPreferences::VideoDecoderSelection vds,
int Session::drSetup(int videoFormat, int width, int height, int frameRate, void *, int)
{
s_ActiveSession->m_ActiveVideoFormat = videoFormat;
s_ActiveSession->m_ActiveVideoWidth = width;
s_ActiveSession->m_ActiveVideoHeight = height;
s_ActiveSession->m_ActiveVideoFrameRate = frameRate;
// Defer decoder setup until we've started streaming so we
// don't have to hide and show the SDL window (which seems to
// cause pointer hiding to break on Windows).
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Video stream is %dx%dx%d (format 0x%x)",
width, height, frameRate, videoFormat);
if (!chooseDecoder(s_ActiveSession->m_Preferences.videoDecoderSelection,
s_ActiveSession->m_Window,
videoFormat, width, height, frameRate,
s_ActiveSession->m_VideoDecoder)) {
return -1;
}
return 0;
}
@@ -169,16 +136,7 @@ int Session::drSubmitDecodeUnit(PDECODE_UNIT du)
// from underneath the session when we initiate destruction.
// We need to destroy the decoder on the main thread to satisfy
// some API constraints (like DXVA2).
SDL_AtomicLock(&s_ActiveSession->m_DecoderLock);
if (s_ActiveSession->m_NeedsIdr) {
// If we reset our decoder, we'll need to request an IDR frame
s_ActiveSession->m_NeedsIdr = false;
SDL_AtomicUnlock(&s_ActiveSession->m_DecoderLock);
return DR_NEED_IDR;
}
IVideoDecoder* decoder = s_ActiveSession->m_VideoDecoder;
if (decoder != nullptr) {
int ret = decoder->submitDecodeUnit(du);
@@ -196,35 +154,23 @@ bool Session::isHardwareDecodeAvailable(StreamingPreferences::VideoDecoderSelect
{
IVideoDecoder* decoder;
if (SDL_InitSubSystem(SDL_INIT_VIDEO) != 0) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"SDL_InitSubSystem(SDL_INIT_VIDEO) failed: %s",
SDL_GetError());
return false;
}
SDL_Window* window = SDL_CreateWindow("", 0, 0, width, height, SDL_WINDOW_HIDDEN);
if (!window) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Failed to create window for hardware decode test: %s",
SDL_GetError());
SDL_QuitSubSystem(SDL_INIT_VIDEO);
return false;
}
if (!chooseDecoder(vds, window, videoFormat, width, height, frameRate, decoder)) {
SDL_DestroyWindow(window);
SDL_QuitSubSystem(SDL_INIT_VIDEO);
return false;
}
SDL_DestroyWindow(window);
bool ret = decoder->isHardwareAccelerated();
delete decoder;
SDL_QuitSubSystem(SDL_INIT_VIDEO);
return ret;
}
@@ -233,11 +179,8 @@ Session::Session(NvComputer* computer, NvApp& app)
m_App(app),
m_Window(nullptr),
m_VideoDecoder(nullptr),
m_DecoderLock(0),
m_NeedsIdr(false)
m_DecoderLock(0)
{
qDebug() << "Server GPU:" << m_Computer->gpuModel;
LiInitializeVideoCallbacks(&m_VideoCallbacks);
m_VideoCallbacks.setup = drSetup;
m_VideoCallbacks.submitDecodeUnit = drSubmitDecodeUnit;
@@ -246,11 +189,7 @@ Session::Session(NvComputer* computer, NvApp& app)
m_VideoCallbacks.capabilities |= CAPABILITY_DIRECT_SUBMIT;
// Slice up to 4 times for parallel decode, once slice per core
int slices = qMin(MAX_SLICES, SDL_GetCPUCount());
m_VideoCallbacks.capabilities |= CAPABILITY_SLICES_PER_FRAME(slices);
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Encoder configured for %d slices per frame",
slices);
m_VideoCallbacks.capabilities |= CAPABILITY_SLICES_PER_FRAME(qMin(MAX_SLICES, SDL_GetCPUCount()));
LiInitializeStreamConfiguration(&m_StreamConfig);
m_StreamConfig.width = m_Preferences.width;
@@ -258,21 +197,14 @@ Session::Session(NvComputer* computer, NvApp& app)
m_StreamConfig.fps = m_Preferences.fps;
m_StreamConfig.bitrate = m_Preferences.bitrateKbps;
m_StreamConfig.hevcBitratePercentageMultiplier = 75;
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Video bitrate: %d kbps",
m_StreamConfig.bitrate);
RAND_bytes(reinterpret_cast<unsigned char*>(m_StreamConfig.remoteInputAesKey),
sizeof(m_StreamConfig.remoteInputAesKey));
// Only the first 4 bytes are populated in the RI key IV
RAND_bytes(reinterpret_cast<unsigned char*>(m_StreamConfig.remoteInputAesIv), 4);
for (unsigned int i = 0; i < sizeof(m_StreamConfig.remoteInputAesKey); i++) {
m_StreamConfig.remoteInputAesKey[i] =
(char)(QRandomGenerator::global()->generate() % 256);
}
*(int*)m_StreamConfig.remoteInputAesIv = qToBigEndian(QRandomGenerator::global()->generate());
switch (m_Preferences.audioConfig)
{
case StreamingPreferences::AC_AUTO:
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Autodetecting audio configuration");
m_StreamConfig.audioConfiguration = sdlDetermineAudioConfiguration();
break;
case StreamingPreferences::AC_FORCE_STEREO:
@@ -283,10 +215,6 @@ Session::Session(NvComputer* computer, NvApp& app)
break;
}
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Audio configuration: %d",
m_StreamConfig.audioConfiguration);
switch (m_Preferences.videoCodecConfig)
{
case StreamingPreferences::VCC_AUTO:
@@ -328,54 +256,28 @@ Session::Session(NvComputer* computer, NvApp& app)
}
}
void Session::emitLaunchWarning(QString text)
{
// Emit the warning to the UI
emit displayLaunchWarning(text);
// Wait a little bit so the user can actually read what we just said.
// This wait is a little longer than the actual toast timeout (3 seconds)
// to allow it to transition off the screen before continuing.
uint32_t start = SDL_GetTicks();
while (!SDL_TICKS_PASSED(SDL_GetTicks(), start + 3500)) {
// Pump the UI loop while we wait
SDL_Delay(5);
QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents);
}
}
bool Session::validateLaunch()
{
QStringList warningList;
if (m_Preferences.videoDecoderSelection == StreamingPreferences::VDS_FORCE_SOFTWARE) {
emitLaunchWarning("Your settings selection to force software decoding may cause poor streaming performance.");
}
if (m_StreamConfig.supportsHevc) {
bool hevcForced = m_Preferences.videoCodecConfig == StreamingPreferences::VCC_FORCE_HEVC ||
m_Preferences.videoCodecConfig == StreamingPreferences::VCC_FORCE_HEVC_HDR;
if (!isHardwareDecodeAvailable(m_Preferences.videoDecoderSelection,
VIDEO_FORMAT_H265,
m_StreamConfig.width,
m_StreamConfig.height,
m_StreamConfig.fps) &&
m_Preferences.videoDecoderSelection != StreamingPreferences::VDS_FORCE_SOFTWARE) {
if (hevcForced) {
emitLaunchWarning("Using software decoding due to your selection to force HEVC. This may cause poor streaming performance.");
}
else {
emitLaunchWarning("This PC's GPU doesn't support HEVC decoding.");
m_StreamConfig.supportsHevc = false;
if (m_Preferences.videoCodecConfig == StreamingPreferences::VCC_FORCE_HEVC ||
m_Preferences.videoCodecConfig == StreamingPreferences::VCC_FORCE_HEVC_HDR) {
if (m_Computer->maxLumaPixelsHEVC == 0) {
emit displayLaunchWarning("Your host PC GPU doesn't support HEVC. "
"A GeForce GTX 900-series (Maxwell) or later GPU is required for HEVC streaming.");
}
}
if (hevcForced) {
if (m_Computer->maxLumaPixelsHEVC == 0) {
emitLaunchWarning("Your host PC GPU doesn't support HEVC. "
"A GeForce GTX 900-series (Maxwell) or later GPU is required for HEVC streaming.");
}
else if (!isHardwareDecodeAvailable(m_Preferences.videoDecoderSelection,
VIDEO_FORMAT_H265,
m_StreamConfig.width,
m_StreamConfig.height,
m_StreamConfig.fps)) {
// NOTE: HEVC currently uses only 1 slice regardless of what
// we provide in CAPABILITY_SLICES_PER_FRAME(), so we should
// never use it for software decoding (unless common-c starts
// respecting it for HEVC).
m_StreamConfig.supportsHevc = false;
}
}
@@ -385,19 +287,19 @@ bool Session::validateLaunch()
// Check that the app supports HDR
if (!m_App.hdrSupported) {
emitLaunchWarning(m_App.name + " doesn't support HDR10.");
emit displayLaunchWarning(m_App.name + " doesn't support HDR10.");
}
// Check that the server GPU supports HDR
else if (!(m_Computer->serverCodecModeSupport & 0x200)) {
emitLaunchWarning("Your host PC GPU doesn't support HDR streaming. "
"A GeForce GTX 1000-series (Pascal) or later GPU is required for HDR streaming.");
emit displayLaunchWarning("Your host PC GPU doesn't support HDR streaming. "
"A GeForce GTX 1000-series (Pascal) or later GPU is required for HDR streaming.");
}
else if (!isHardwareDecodeAvailable(m_Preferences.videoDecoderSelection,
VIDEO_FORMAT_H265_MAIN10,
m_StreamConfig.width,
m_StreamConfig.height,
m_StreamConfig.fps)) {
emitLaunchWarning("This PC's GPU doesn't support HEVC Main10 decoding for HDR streaming.");
emit displayLaunchWarning("Your client PC GPU doesn't support HEVC Main10 decoding for HDR streaming.");
}
else {
// TODO: Also validate display capabilites
@@ -410,7 +312,7 @@ bool Session::validateLaunch()
if (m_StreamConfig.width >= 3840) {
// Only allow 4K on GFE 3.x+
if (m_Computer->gfeVersion.isNull() || m_Computer->gfeVersion.startsWith("2.")) {
emitLaunchWarning("GeForce Experience 3.0 or higher is required for 4K streaming.");
emit displayLaunchWarning("GeForce Experience 3.0 or higher is required for 4K streaming.");
m_StreamConfig.width = 1920;
m_StreamConfig.height = 1080;
@@ -418,8 +320,8 @@ bool Session::validateLaunch()
// This list is sorted from least to greatest
else if (m_Computer->displayModes.last().width < 3840 ||
(m_Computer->displayModes.last().refreshRate < 60 && m_StreamConfig.fps >= 60)) {
emitLaunchWarning("Your host PC GPU doesn't support 4K streaming. "
"A GeForce GTX 900-series (Maxwell) or later GPU is required for 4K streaming.");
emit displayLaunchWarning("Your host PC GPU doesn't support 4K streaming. "
"A GeForce GTX 900-series (Maxwell) or later GPU is required for 4K streaming.");
m_StreamConfig.width = 1920;
m_StreamConfig.height = 1080;
@@ -437,6 +339,9 @@ class DeferredSessionCleanupTask : public QRunnable
{
// Finish cleanup of the connection state
LiStopConnection();
if (Session::s_ActiveSession->m_Window != nullptr) {
SDL_DestroyWindow(Session::s_ActiveSession->m_Window);
}
// Allow another session to start now that we're cleaned up
Session::s_ActiveSession = nullptr;
@@ -444,130 +349,6 @@ class DeferredSessionCleanupTask : public QRunnable
}
};
void Session::getWindowDimensions(bool fullScreen,
int& x, int& y,
int& width, int& height)
{
int displayIndex = 0;
// If there's a display matching this exact resolution, pick that
// one (for native full-screen streaming). Otherwise, assume
// display 0 for now. TODO: Default to the screen that the Qt window is on
if (fullScreen) {
for (int i = 0; i < SDL_GetNumVideoDisplays(); i++) {
SDL_DisplayMode mode;
if (SDL_GetCurrentDisplayMode(i, &mode) == 0 &&
m_ActiveVideoWidth == mode.w &&
m_ActiveVideoHeight == mode.h) {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Found exact resolution match on display: %d",
i);
displayIndex = i;
break;
}
}
}
if (m_Window != nullptr) {
displayIndex = SDL_GetWindowDisplayIndex(m_Window);
if (displayIndex < 0) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"SDL_GetWindowDisplayIndex() failed: %s",
SDL_GetError());
// Assume display 0
displayIndex = 0;
}
}
if (fullScreen) {
SDL_DisplayMode currentMode;
if (SDL_GetCurrentDisplayMode(displayIndex, &currentMode) == 0) {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Using current display mode: %dx%dx%d",
currentMode.w, currentMode.h, currentMode.refresh_rate);
width = currentMode.w;
height = currentMode.h;
}
else {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Unable to get current display mode: %s",
SDL_GetError());
width = m_StreamConfig.width;
height = m_StreamConfig.height;
}
// Full-screen modes always start at the origin
x = y = SDL_WINDOWPOS_UNDEFINED_DISPLAY(displayIndex);
}
else {
SDL_Rect usableBounds;
if (SDL_GetDisplayUsableBounds(displayIndex, &usableBounds) == 0) {
width = usableBounds.w;
height = usableBounds.h;
x = usableBounds.x;
y = usableBounds.y;
if (m_Window != nullptr) {
int top, left, bottom, right;
if (SDL_GetWindowBordersSize(m_Window, &top, &left, &bottom, &right) < 0) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"Unable to get window border size");
return;
}
x += left;
y += top;
width -= left + right;
height -= top + bottom;
// If the stream window can fit within the usable drawing area with 1:1
// scaling, do that rather than filling the screen.
if (m_StreamConfig.width < width && m_StreamConfig.height < height) {
width = m_StreamConfig.width;
height = m_StreamConfig.height;
}
}
}
else {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"SDL_GetDisplayUsableBounds() failed: %s",
SDL_GetError());
width = m_StreamConfig.width;
height = m_StreamConfig.height;
x = y = SDL_WINDOWPOS_UNDEFINED_DISPLAY(displayIndex);
}
}
}
void Session::toggleFullscreen()
{
bool fullScreen = !(SDL_GetWindowFlags(m_Window) & SDL_OS_FULLSCREEN_FLAG);
int x, y, width, height;
// If we're leaving full screen, toggle out before setting our size and position
// that way we have accurate display size metrics (not the size our stream changed it to).
if (!fullScreen) {
SDL_SetWindowFullscreen(m_Window, 0);
SDL_SetWindowResizable(m_Window, SDL_TRUE);
}
getWindowDimensions(fullScreen, x, y, width, height);
SDL_SetWindowPosition(m_Window, x, y);
SDL_SetWindowSize(m_Window, width, height);
if (fullScreen) {
SDL_SetWindowResizable(m_Window, SDL_FALSE);
SDL_SetWindowFullscreen(m_Window, SDL_OS_FULLSCREEN_FLAG);
}
}
void Session::exec()
{
// Check for validation errors/warnings and emit
@@ -594,18 +375,6 @@ void Session::exec()
Q_ASSERT(m_Computer->currentGameId == 0 ||
m_Computer->currentGameId == m_App.id);
bool enableGameOptimizations = false;
for (const NvDisplayMode &mode : m_Computer->displayModes) {
if (mode.width == m_StreamConfig.width &&
mode.height == m_StreamConfig.height) {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Found host supported resolution: %dx%d",
mode.width, mode.height);
enableGameOptimizations = prefs.gameOptimizations;
break;
}
}
try {
NvHTTP http(m_Computer->activeAddress);
if (m_Computer->currentGameId != 0) {
@@ -613,7 +382,7 @@ void Session::exec()
}
else {
http.launchApp(m_App.id, &m_StreamConfig,
enableGameOptimizations,
prefs.gameOptimizations,
prefs.playAudioOnHost,
inputHandler.getAttachedGamepadMask());
}
@@ -623,12 +392,64 @@ void Session::exec()
return;
}
SDL_assert(!SDL_WasInit(SDL_INIT_VIDEO));
if (SDL_InitSubSystem(SDL_INIT_VIDEO) != 0) {
int flags = SDL_WINDOW_HIDDEN;
int width, height;
if (m_Preferences.fullScreen) {
SDL_DisplayMode desired, closest;
SDL_zero(desired);
desired.w = m_StreamConfig.width;
desired.h = m_StreamConfig.height;
desired.refresh_rate = m_StreamConfig.fps;
if (SDL_GetClosestDisplayMode(0, &desired, &closest)) {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Closest match for %dx%dx%d is %dx%dx%d",
desired.w, desired.h, desired.refresh_rate,
closest.w, closest.h, closest.refresh_rate);
width = closest.w;
height = closest.h;
}
else if (SDL_GetCurrentDisplayMode(0, &closest) == 0) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"Using current display mode: %dx%dx%d",
closest.w, closest.h, closest.refresh_rate);
width = closest.w;
height = closest.h;
}
else {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"Unable to get current or closest display mode");
width = m_StreamConfig.width;
height = m_StreamConfig.height;
}
flags |= SDL_WINDOW_FULLSCREEN;
}
else {
SDL_DisplayMode current;
width = m_StreamConfig.width;
height = m_StreamConfig.height;
if (SDL_GetCurrentDisplayMode(0, &current) == 0) {
if (current.w <= m_StreamConfig.width || current.h <= m_StreamConfig.height) {
// If we match or exceed the dimensions of the display, maximize the window
flags |= SDL_WINDOW_MAXIMIZED;
}
}
}
m_Window = SDL_CreateWindow("Moonlight",
SDL_WINDOWPOS_UNDEFINED,
SDL_WINDOWPOS_UNDEFINED,
width,
height,
flags);
if (!m_Window) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"SDL_InitSubSystem(SDL_INIT_VIDEO) failed: %s",
"SDL_CreateWindow() failed: %s",
SDL_GetError());
emit displayLaunchError(QString::fromLocal8Bit(SDL_GetError()));
s_ActiveSessionSemaphore.release();
return;
}
@@ -656,7 +477,6 @@ void Session::exec()
// We already displayed an error dialog in the stage failure
// listener.
s_ActiveSessionSemaphore.release();
SDL_QuitSubSystem(SDL_INIT_VIDEO);
return;
}
@@ -664,71 +484,11 @@ void Session::exec()
emit connectionStarted();
QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents);
int x, y, width, height;
getWindowDimensions(m_Preferences.fullScreen,
x, y, width, height);
// Show the streaming window
SDL_ShowWindow(m_Window);
m_Window = SDL_CreateWindow("Moonlight",
x,
y,
width,
height,
m_Preferences.fullScreen ?
SDL_OS_FULLSCREEN_FLAG : 0);
if (!m_Window) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"SDL_CreateWindow() failed: %s",
SDL_GetError());
s_ActiveSessionSemaphore.release();
SDL_QuitSubSystem(SDL_INIT_VIDEO);
return;
}
// For non-full screen windows, call getWindowDimensions()
// again after creating a window to allow it to account
// for window chrome size.
if (!m_Preferences.fullScreen) {
getWindowDimensions(false, x, y, width, height);
SDL_SetWindowPosition(m_Window, x, y);
SDL_SetWindowSize(m_Window, width, height);
// Passing SDL_WINDOW_RESIZABLE to set this during window
// creation causes our window to be full screen for some reason
SDL_SetWindowResizable(m_Window, SDL_TRUE);
}
QSvgRenderer svgIconRenderer(QString(":/res/moonlight.svg"));
QImage svgImage(ICON_SIZE, ICON_SIZE, QImage::Format_RGBA8888);
svgImage.fill(0);
QPainter svgPainter(&svgImage);
svgIconRenderer.render(&svgPainter);
SDL_Surface* iconSurface = SDL_CreateRGBSurfaceWithFormatFrom((void*)svgImage.constBits(),
svgImage.width(),
svgImage.height(),
32,
4 * svgImage.width(),
SDL_PIXELFORMAT_RGBA32);
#ifndef Q_OS_DARWIN
// Other platforms seem to preserve our Qt icon when creating a new window
if (iconSurface != nullptr) {
SDL_SetWindowIcon(m_Window, iconSurface);
}
#endif
#ifndef QT_DEBUG
// Capture the mouse by default on release builds only.
// This prevents the mouse from becoming trapped inside
// Moonlight when it's halted at a debug break.
// Capture the mouse
SDL_SetRelativeMouseMode(SDL_TRUE);
#endif
// Stop text input. SDL enables it by default
// when we initialize the video subsystem, but this
// causes an IME popup when certain keys are held down
// on macOS.
SDL_StopTextInput();
// Disable the screen saver
SDL_DisableScreenSaver();
@@ -744,15 +504,7 @@ void Session::exec()
// Hijack this thread to be the SDL main thread. We have to do this
// because we want to suspend all Qt processing until the stream is over.
SDL_Event event;
for (;;) {
// We explicitly use SDL_PollEvent() and SDL_Delay() because
// SDL_WaitEvent() has an internal SDL_Delay(10) inside which
// blocks this thread too long for high polling rate mice and high
// refresh rate displays.
if (!SDL_PollEvent(&event)) {
SDL_Delay(1);
continue;
}
while (SDL_WaitEvent(&event)) {
switch (event.type) {
case SDL_QUIT:
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
@@ -777,41 +529,6 @@ void Session::exec()
m_VideoDecoder->renderFrame(&event.user);
break;
}
case SDL_WINDOWEVENT:
// We want to recreate the decoder for resizes (full-screen toggles) and the initial shown event.
// We use SDL_WINDOWEVENT_SIZE_CHANGED rather than SDL_WINDOWEVENT_RESIZED because the latter doesn't
// seem to fire when switching from windowed to full-screen on X11.
if (event.window.event != SDL_WINDOWEVENT_SIZE_CHANGED && event.window.event != SDL_WINDOWEVENT_SHOWN) {
break;
}
// Fall through
case SDL_RENDER_DEVICE_RESET:
case SDL_RENDER_TARGETS_RESET:
SDL_AtomicLock(&m_DecoderLock);
// Destroy the old decoder
delete m_VideoDecoder;
// Chose a new decoder (hopefully the same one, but possibly
// not if a GPU was removed or something).
if (!chooseDecoder(m_Preferences.videoDecoderSelection,
m_Window, m_ActiveVideoFormat, m_ActiveVideoWidth,
m_ActiveVideoHeight, m_ActiveVideoFrameRate,
s_ActiveSession->m_VideoDecoder)) {
SDL_AtomicUnlock(&m_DecoderLock);
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Failed to recreate decoder after reset");
goto DispatchDeferredCleanup;
}
// Request an IDR frame to complete the reset
m_NeedsIdr = true;
SDL_AtomicUnlock(&m_DecoderLock);
break;
case SDL_KEYUP:
case SDL_KEYDOWN:
inputHandler.handleKeyEvent(&event.key);
@@ -849,6 +566,9 @@ DispatchDeferredCleanup:
// so we can return to the Qt GUI ASAP.
SDL_SetRelativeMouseMode(SDL_FALSE);
SDL_EnableScreenSaver();
if (m_Window != nullptr) {
SDL_HideWindow(m_Window);
}
// Destroy the decoder, since this must be done on the main thread
SDL_AtomicLock(&m_DecoderLock);
@@ -856,13 +576,6 @@ DispatchDeferredCleanup:
m_VideoDecoder = nullptr;
SDL_AtomicUnlock(&m_DecoderLock);
SDL_DestroyWindow(m_Window);
if (iconSurface != nullptr) {
SDL_FreeSurface(iconSurface);
}
SDL_QuitSubSystem(SDL_INIT_VIDEO);
SDL_assert(!SDL_WasInit(SDL_INIT_VIDEO));
// Cleanup can take a while, so dispatch it to a worker thread.
// When it is complete, it will release our s_ActiveSessionSemaphore
// reference.
+3 -17
View File
@@ -21,10 +21,6 @@ public:
Q_INVOKABLE void exec();
static
bool isHardwareDecodeAvailable(StreamingPreferences::VideoDecoderSelection vds,
int videoFormat, int width, int height, int frameRate);
signals:
void stageStarting(QString stage);
@@ -39,17 +35,13 @@ signals:
private:
bool validateLaunch();
void emitLaunchWarning(QString text);
int getDecoderCapabilities();
int sdlDetermineAudioConfiguration();
void getWindowDimensions(bool fullScreen,
int& x, int& y,
int& width, int& height);
void toggleFullscreen();
static
bool isHardwareDecodeAvailable(StreamingPreferences::VideoDecoderSelection vds,
int videoFormat, int width, int height, int frameRate);
static
bool chooseDecoder(StreamingPreferences::VideoDecoderSelection vds,
@@ -102,12 +94,6 @@ private:
SDL_Window* m_Window;
IVideoDecoder* m_VideoDecoder;
SDL_SpinLock m_DecoderLock;
bool m_NeedsIdr;
int m_ActiveVideoFormat;
int m_ActiveVideoWidth;
int m_ActiveVideoHeight;
int m_ActiveVideoFrameRate;
static SDL_AudioDeviceID s_AudioDevice;
static OpusMSDecoder* s_OpusDecoder;
-20
View File
@@ -1,20 +0,0 @@
#include "streamutils.h"
void StreamUtils::scaleSourceToDestinationSurface(SDL_Rect* src, SDL_Rect* dst)
{
int dstH = dst->w * src->h / src->w;
int dstW = dst->h * src->w / src->h;
if (dstH > dst->h) {
dst->y = 0;
dst->x = (dst->w - dstW) / 2;
dst->w = dstW;
SDL_assert(dst->w * src->h / src->w <= dst->h);
}
else {
dst->x = 0;
dst->y = (dst->h - dstH) / 2;
dst->h = dstH;
SDL_assert(dst->h * src->w / src->h <= dst->w);
}
}
-10
View File
@@ -1,10 +0,0 @@
#pragma once
#include <SDL.h>
class StreamUtils
{
public:
static
void scaleSourceToDestinationSurface(SDL_Rect* src, SDL_Rect* dst);
};
+11
View File
@@ -0,0 +1,11 @@
#pragma once
#include "decoder.h"
// A factory is required to avoid pulling in
// incompatible Objective-C headers.
class AVFoundationDecoderFactory {
public:
static
IVideoDecoder* createDecoder();
};
+521
View File
@@ -0,0 +1,521 @@
#include "avfoundation.h"
#include <SDL_syswm.h>
#import <Cocoa/Cocoa.h>
#import <VideoToolbox/VideoToolbox.h>
#import <AVFoundation/AVFoundation.h>
#import <CoreVideo/CoreVideo.h>
#define FRAME_START_PREFIX_SIZE 4
#define NALU_START_PREFIX_SIZE 3
#define NAL_LENGTH_PREFIX_SIZE 4
#define FUTURE_FRAMES_IN_VSYNC 3
class AVFoundationDecoder : public IVideoDecoder
{
public:
AVFoundationDecoder()
: m_DisplayLayer(nullptr),
m_FormatDesc(nullptr),
m_SpsData(nullptr),
m_PpsData(nullptr),
m_VpsData(nullptr),
m_View(nullptr),
m_NeedsIdr(false),
m_DisplayLink(nullptr),
m_VsyncTime(0),
m_LastVsyncTime(0),
m_VsyncStatsLock(0)
{
SDL_zero(m_FrameCountOnVsync);
}
virtual ~AVFoundationDecoder()
{
if (m_FormatDesc != nullptr) {
CFRelease(m_FormatDesc);
}
if (m_DisplayLink != nullptr) {
CVDisplayLinkStop(m_DisplayLink);
CVDisplayLinkRelease(m_DisplayLink);
}
delete m_SpsData;
delete m_PpsData;
delete m_VpsData;
}
static
CVReturn
displayLinkOutputCallback(
CVDisplayLinkRef,
const CVTimeStamp*,
const CVTimeStamp* vsyncTime,
CVOptionFlags,
CVOptionFlags*,
void *displayLinkContext)
{
AVFoundationDecoder* me = reinterpret_cast<AVFoundationDecoder*>(displayLinkContext);
SDL_AtomicLock(&me->m_VsyncStatsLock);
me->m_LastVsyncTime = me->m_VsyncTime;
me->m_VsyncTime = vsyncTime->hostTime;
int framesThisVsync = *(volatile int*)&me->m_FrameCountOnVsync[0];
memmove(&me->m_FrameCountOnVsync[0],
&me->m_FrameCountOnVsync[1],
sizeof(me->m_FrameCountOnVsync) - sizeof(me->m_FrameCountOnVsync[0]));
me->m_FrameCountOnVsync[FUTURE_FRAMES_IN_VSYNC - 1] = 0;
SDL_AtomicUnlock(&me->m_VsyncStatsLock);
if (framesThisVsync != 1)
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"V-Sync interval frames: %d",
framesThisVsync);
return kCVReturnSuccess;
}
virtual bool initialize(StreamingPreferences::VideoDecoderSelection vds,
SDL_Window* window,
int videoFormat,
int, int, int) override
{
m_VideoFormat = videoFormat;
// Fail if the user wants software decode only
if (vds == StreamingPreferences::VDS_FORCE_SOFTWARE) {
return false;
}
if (videoFormat & VIDEO_FORMAT_MASK_H264) {
// Prior to 10.13, we'll just assume everything has
// H.264 support and fail open to allow VT decode.
#if __MAC_OS_X_VERSION_MAX_ALLOWED >= 101300
if (__builtin_available(macOS 10.13, *)) {
if (!VTIsHardwareDecodeSupported(kCMVideoCodecType_H264)) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"No HW accelerated H.264 decode via VT");
return false;
}
}
else
#endif
{
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"Assuming H.264 HW decode on < macOS 10.13");
}
}
else if (videoFormat & VIDEO_FORMAT_MASK_H265) {
#if __MAC_OS_X_VERSION_MAX_ALLOWED >= 101300
if (__builtin_available(macOS 10.13, *)) {
if (!VTIsHardwareDecodeSupported(kCMVideoCodecType_HEVC)) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"No HW accelerated HEVC decode via VT");
return false;
}
}
else
#endif
{
// Fail closed for HEVC if we're not on 10.13+
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"No HEVC support on < macOS 10.13");
return false;
}
}
SDL_SysWMinfo info;
SDL_VERSION(&info.version);
if (!SDL_GetWindowWMInfo(window, &info)) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"SDL_GetWindowWMInfo() failed: %s",
SDL_GetError());
return false;
}
SDL_assert(info.subsystem == SDL_SYSWM_COCOA);
// SDL adds its own content view to listen for events.
// We need to add a subview for our display layer.
NSView* contentView = info.info.cocoa.window.contentView;
m_View = [[NSView alloc] initWithFrame:contentView.bounds];
m_View.wantsLayer = YES;
[contentView addSubview: m_View];
setupDisplayLayer();
CVDisplayLinkCreateWithActiveCGDisplays(&m_DisplayLink);
CVDisplayLinkSetOutputCallback(m_DisplayLink, displayLinkOutputCallback, (void*)this);
CVDisplayLinkStart(m_DisplayLink);
return true;
}
virtual bool isHardwareAccelerated() override
{
// We only use AVFoundation for HW acceleration
return true;
}
virtual int submitDecodeUnit(PDECODE_UNIT du) override
{
OSStatus status;
unsigned char* data = (unsigned char*)malloc(du->fullLength);
if (!data) {
return DR_NEED_IDR;
}
PLENTRY entry = du->bufferList;
int pictureDataLength = 0;
while (entry != nullptr) {
if (entry->bufferType == BUFFER_TYPE_VPS) {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Got VPS");
delete m_VpsData;
m_VpsData = new QByteArray(&entry->data[FRAME_START_PREFIX_SIZE], entry->length - FRAME_START_PREFIX_SIZE);
// We got a new VPS so wait for a new SPS to match it
delete m_SpsData;
delete m_PpsData;
m_SpsData = m_PpsData = nullptr;
}
else if (entry->bufferType == BUFFER_TYPE_SPS) {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Got SPS");
delete m_SpsData;
m_SpsData = new QByteArray(&entry->data[FRAME_START_PREFIX_SIZE], entry->length - FRAME_START_PREFIX_SIZE);
// We got a new SPS so wait for a new PPS to match it
delete m_PpsData;
m_PpsData = nullptr;
}
else if (entry->bufferType == BUFFER_TYPE_PPS) {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Got PPS");
delete m_PpsData;
m_PpsData = new QByteArray(&entry->data[FRAME_START_PREFIX_SIZE], entry->length - FRAME_START_PREFIX_SIZE);
}
else {
// Picture data
SDL_assert(entry->bufferType == BUFFER_TYPE_PICDATA);
memcpy(&data[pictureDataLength],
entry->data,
entry->length);
pictureDataLength += entry->length;
}
entry = entry->next;
}
SDL_assert(pictureDataLength <= du->fullLength);
if (du->frameType == FRAME_TYPE_IDR) {
m_NeedsIdr = false;
if (m_FormatDesc != nullptr) {
CFRelease(m_FormatDesc);
}
if (m_VideoFormat & VIDEO_FORMAT_MASK_H264) {
const uint8_t* const parameterSetPointers[] = { (uint8_t*)m_SpsData->data(),
(uint8_t*)m_PpsData->data() };
const size_t parameterSetSizes[] = { (size_t)m_SpsData->length(),
(size_t)m_PpsData->length() };
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Constructing new H264 format description");
status = CMVideoFormatDescriptionCreateFromH264ParameterSets(kCFAllocatorDefault,
2, /* count of parameter sets */
parameterSetPointers,
parameterSetSizes,
NAL_LENGTH_PREFIX_SIZE,
&m_FormatDesc);
if (status != noErr) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Failed to create H264 format description: %d",
(int)status);
m_FormatDesc = NULL;
}
}
else {
#if __MAC_OS_X_VERSION_MAX_ALLOWED >= 101300
const uint8_t* const parameterSetPointers[] = { (uint8_t*)m_VpsData->data(),
(uint8_t*)m_SpsData->data(),
(uint8_t*)m_PpsData->data() };
const size_t parameterSetSizes[] = { (size_t)m_VpsData->length(),
(size_t)m_SpsData->length(),
(size_t)m_PpsData->length() };
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Constructing new HEVC format description");
if (__builtin_available(macOS 10.13, *)) {
status = CMVideoFormatDescriptionCreateFromHEVCParameterSets(kCFAllocatorDefault,
3, /* count of parameter sets */
parameterSetPointers,
parameterSetSizes,
NAL_LENGTH_PREFIX_SIZE,
nil,
&m_FormatDesc);
if (status != noErr) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Failed to create HEVC format description: %d",
(int)status);
m_FormatDesc = NULL;
}
}
else
#endif
{
// This means Moonlight-common-c decided to give us an HEVC stream
// even though we said we couldn't support it. All we can do is abort().
abort();
}
}
}
if (m_FormatDesc == nullptr || m_NeedsIdr) {
// Can't decode if we haven't gotten our parameter sets yet
free(data);
return DR_NEED_IDR;
}
// Now we're decoding actual frame data here
CMBlockBufferRef blockBuffer;
status = CMBlockBufferCreateEmpty(NULL, 0, 0, &blockBuffer);
if (status != noErr) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"CMBlockBufferCreateEmpty failed: %d",
(int)status);
free(data);
return DR_NEED_IDR;
}
int lastOffset = -1;
for (int i = 0; i < pictureDataLength - FRAME_START_PREFIX_SIZE; i++) {
// Search for a NALU
if (data[i] == 0 && data[i+1] == 0 && data[i+2] == 1) {
// It's the start of a new NALU
if (lastOffset != -1) {
// We've seen a start before this so enqueue that NALU
updateBufferForRange(blockBuffer, data, lastOffset, i - lastOffset);
}
lastOffset = i;
}
}
if (lastOffset != -1) {
// Enqueue the remaining data
updateBufferForRange(blockBuffer, data, lastOffset, pictureDataLength - lastOffset);
}
// From now on, CMBlockBuffer owns the data pointer and will free it when it's released
CMSampleBufferRef sampleBuffer;
CMSampleTimingInfo time = {
.decodeTimeStamp = kCMTimeInvalid,
.duration = kCMTimeInvalid
};
SDL_AtomicLock(&m_VsyncStatsLock);
// Find a slot to put this frame
for (int i = 0; i < FUTURE_FRAMES_IN_VSYNC; i++) {
if (m_FrameCountOnVsync[i] == 0) {
time.presentationTimeStamp =
CMTimeMake(m_VsyncTime + (i + 1) * (m_VsyncTime - m_LastVsyncTime),
1000 * 1000 * 1000);
m_FrameCountOnVsync[i]++;
}
}
SDL_AtomicUnlock(&m_VsyncStatsLock);
status = CMSampleBufferCreate(kCFAllocatorDefault,
blockBuffer,
true, NULL,
NULL, m_FormatDesc, 1, 1,
&time, 0, NULL,
&sampleBuffer);
if (status != noErr) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"CMSampleBufferCreate failed: %d",
(int)status);
CFRelease(blockBuffer);
return DR_NEED_IDR;
}
// Submit for callback on the main thread
queueFrame(sampleBuffer, (void*)(long)du->frameType);
return DR_OK;
}
// This is called on the main thread, so it's safe to interact with
// the AVSampleBufferDisplayLayer here.
void submitBuffer(CMSampleBufferRef sampleBuffer, int frameType)
{
if (m_DisplayLayer.status == AVQueuedSampleBufferRenderingStatusFailed) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"Resetting failed AVSampleBufferDisplay layer");
setupDisplayLayer();
CFRelease(sampleBuffer);
return;
}
CFArrayRef attachments = CMSampleBufferGetSampleAttachmentsArray(sampleBuffer, YES);
CFMutableDictionaryRef dict = (CFMutableDictionaryRef)CFArrayGetValueAtIndex(attachments, 0);
CFDictionarySetValue(dict, kCMSampleAttachmentKey_IsDependedOnByOthers, kCFBooleanTrue);
if (frameType == FRAME_TYPE_PFRAME) {
// P-frame
CFDictionarySetValue(dict, kCMSampleAttachmentKey_NotSync, kCFBooleanTrue);
CFDictionarySetValue(dict, kCMSampleAttachmentKey_DependsOnOthers, kCFBooleanTrue);
} else {
// I-frame
CFDictionarySetValue(dict, kCMSampleAttachmentKey_NotSync, kCFBooleanFalse);
CFDictionarySetValue(dict, kCMSampleAttachmentKey_DependsOnOthers, kCFBooleanFalse);
}
[m_DisplayLayer enqueueSampleBuffer:sampleBuffer];
CFRelease(sampleBuffer);
}
virtual void renderFrame(SDL_UserEvent* event) override
{
submitBuffer((CMSampleBufferRef)event->data1, (int)(long)event->data2);
}
virtual void dropFrame(SDL_UserEvent* event) override
{
// Submit anyway and let the drop happen in the render pipeline
submitBuffer((CMSampleBufferRef)event->data1, (int)(long)event->data2);
}
private:
void setupDisplayLayer()
{
CALayer* oldLayer = m_DisplayLayer;
m_DisplayLayer = [[AVSampleBufferDisplayLayer alloc] init];
m_DisplayLayer.bounds = m_View.bounds;
m_DisplayLayer.position = CGPointMake(CGRectGetMidX(m_View.bounds), CGRectGetMidY(m_View.bounds));
m_DisplayLayer.videoGravity = AVLayerVideoGravityResizeAspect;
CALayer* viewLayer = m_View.layer;
if (oldLayer != nil) {
[viewLayer replaceSublayer:oldLayer with:m_DisplayLayer];
}
else {
[viewLayer addSublayer:m_DisplayLayer];
}
// Wait for another IDR frame
m_NeedsIdr = true;
delete m_SpsData;
delete m_PpsData;
delete m_VpsData;
m_SpsData = m_PpsData = m_VpsData = nullptr;
}
void updateBufferForRange(CMBlockBufferRef existingBuffer, unsigned char* data, int offset, int nalLength)
{
OSStatus status;
size_t oldOffset = CMBlockBufferGetDataLength(existingBuffer);
// If we're at index 1 (first NALU in frame), enqueue this buffer to the memory block
// so it can handle freeing it when the block buffer is destroyed
if (offset == 1) {
int dataLength = nalLength - NALU_START_PREFIX_SIZE;
// Pass the real buffer pointer directly (no offset)
// This will give it to the block buffer to free when it's released.
// All further calls to CMBlockBufferAppendMemoryBlock will do so
// at an offset and will not be asking the buffer to be freed.
status = CMBlockBufferAppendMemoryBlock(existingBuffer, data,
nalLength + 1, // Add 1 for the offset we decremented
kCFAllocatorDefault,
NULL, 0, nalLength + 1, 0);
if (status != noErr) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"CMBlockBufferReplaceDataBytes failed: %d",
(int)status);
return;
}
// Write the length prefix to existing buffer
const uint8_t lengthBytes[] = {(uint8_t)(dataLength >> 24), (uint8_t)(dataLength >> 16),
(uint8_t)(dataLength >> 8), (uint8_t)dataLength};
status = CMBlockBufferReplaceDataBytes(lengthBytes, existingBuffer,
oldOffset, NAL_LENGTH_PREFIX_SIZE);
if (status != noErr) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"CMBlockBufferReplaceDataBytes failed: %d",
(int)status);
return;
}
} else {
// Append a 4 byte buffer to this block for the length prefix
status = CMBlockBufferAppendMemoryBlock(existingBuffer, NULL,
NAL_LENGTH_PREFIX_SIZE,
kCFAllocatorDefault, NULL, 0,
NAL_LENGTH_PREFIX_SIZE, 0);
if (status != noErr) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"CMBlockBufferAppendMemoryBlock failed: %d",
(int)status);
return;
}
// Write the length prefix to the new buffer
int dataLength = nalLength - NALU_START_PREFIX_SIZE;
const uint8_t lengthBytes[] = {(uint8_t)(dataLength >> 24), (uint8_t)(dataLength >> 16),
(uint8_t)(dataLength >> 8), (uint8_t)dataLength};
status = CMBlockBufferReplaceDataBytes(lengthBytes, existingBuffer,
oldOffset, NAL_LENGTH_PREFIX_SIZE);
if (status != noErr) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"CMBlockBufferReplaceDataBytes failed: %d",
(int)status);
return;
}
// Attach the buffer by reference to the block buffer
status = CMBlockBufferAppendMemoryBlock(existingBuffer, &data[offset+NALU_START_PREFIX_SIZE],
dataLength,
kCFAllocatorNull, // Don't deallocate data on free
NULL, 0, dataLength, 0);
if (status != noErr) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"CMBlockBufferReplaceDataBytes failed: %d",
(int)status);
return;
}
}
}
AVSampleBufferDisplayLayer* m_DisplayLayer;
CMVideoFormatDescriptionRef m_FormatDesc;
int m_VideoFormat;
QByteArray* m_SpsData;
QByteArray* m_PpsData;
QByteArray* m_VpsData;
NSView* m_View;
bool m_NeedsIdr;
CVDisplayLinkRef m_DisplayLink;
uint64_t m_VsyncTime, m_LastVsyncTime;
int m_FrameCountOnVsync[FUTURE_FRAMES_IN_VSYNC];
SDL_SpinLock m_VsyncStatsLock;
};
IVideoDecoder* AVFoundationDecoderFactory::createDecoder() {
return new AVFoundationDecoder();
}
+78 -286
View File
@@ -1,7 +1,5 @@
#include <initguid.h>
#include <Initguid.h>
#include "dxva2.h"
#include "../ffmpeg.h"
#include <streaming/streamutils.h>
#include <Limelight.h>
@@ -10,16 +8,16 @@ DEFINE_GUID(DXVADDI_Intel_ModeH264_E, 0x604F8E68,0x4951,0x4C54,0x88,0xFE,0xAB,0x
#define SAFE_COM_RELEASE(x) if (x) { (x)->Release(); }
DXVA2Renderer::DXVA2Renderer() :
m_FrameIndex(0),
m_SurfacesUsed(0),
m_SdlRenderer(nullptr),
m_DecService(nullptr),
m_Decoder(nullptr),
m_SurfacesUsed(0),
m_Pool(nullptr),
m_Device(nullptr),
m_RenderTarget(nullptr),
m_ProcService(nullptr),
m_Processor(nullptr),
m_FrameIndex(0)
m_Processor(nullptr)
{
RtlZeroMemory(m_DecSurfaces, sizeof(m_DecSurfaces));
RtlZeroMemory(&m_DXVAContext, sizeof(m_DXVAContext));
@@ -77,8 +75,11 @@ bool DXVA2Renderer::prepareDecoderContext(AVCodecContext* context)
context->hwaccel_context = &m_DXVAContext;
context->get_format = ffGetFormat;
context->get_buffer2 = ffGetBuffer2;
context->opaque = this;
m_Pool = av_buffer_pool_init2(ARRAYSIZE(m_DecSurfaces), this, ffPoolAlloc, nullptr);
if (!m_Pool) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
@@ -86,15 +87,12 @@ bool DXVA2Renderer::prepareDecoderContext(AVCodecContext* context)
return false;
}
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Using DXVA2 accelerated renderer");
return true;
}
int DXVA2Renderer::ffGetBuffer2(AVCodecContext* context, AVFrame* frame, int)
{
DXVA2Renderer* me = (DXVA2Renderer*)((FFmpegVideoDecoder*)context->opaque)->getRenderer();
DXVA2Renderer* me = reinterpret_cast<DXVA2Renderer*>(context->opaque);
frame->buf[0] = av_buffer_pool_get(me->m_Pool);
if (!frame->buf[0]) {
@@ -103,20 +101,31 @@ int DXVA2Renderer::ffGetBuffer2(AVCodecContext* context, AVFrame* frame, int)
frame->data[3] = frame->buf[0]->data;
frame->format = AV_PIX_FMT_DXVA2_VLD;
frame->width = me->m_VideoWidth;
frame->height = me->m_VideoHeight;
frame->width = me->m_Width;
frame->height = me->m_Height;
return 0;
}
enum AVPixelFormat DXVA2Renderer::ffGetFormat(AVCodecContext*,
const enum AVPixelFormat* pixFmts)
{
const enum AVPixelFormat *p;
for (p = pixFmts; *p != -1; p++) {
if (*p == AV_PIX_FMT_DXVA2_VLD)
return *p;
}
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Failed to find DXVA2 HW surface format");
return AV_PIX_FMT_NONE;
}
bool DXVA2Renderer::initializeDecoder()
{
HRESULT hr;
if (isDecoderBlacklisted()) {
return false;
}
hr = DXVA2CreateVideoService(m_Device, IID_IDirectXVideoDecoderService,
reinterpret_cast<void**>(&m_DecService));
if (FAILED(hr)) {
@@ -199,11 +208,18 @@ bool DXVA2Renderer::initializeDecoder()
return false;
}
// Alignment was already taken care of
SDL_assert(m_Desc.SampleWidth % 16 == 0);
SDL_assert(m_Desc.SampleHeight % 16 == 0);
hr = m_DecService->CreateSurface(m_Desc.SampleWidth,
m_Desc.SampleHeight,
int alignment;
// HEVC using DXVA requires 128B alignment
if (m_VideoFormat & VIDEO_FORMAT_MASK_H265) {
alignment = 128;
}
else {
alignment = 16;
}
hr = m_DecService->CreateSurface(FFALIGN(m_Width, alignment),
FFALIGN(m_Height, alignment),
ARRAYSIZE(m_DecSurfaces) - 1,
m_Desc.Format,
D3DPOOL_DEFAULT,
@@ -246,9 +262,6 @@ bool DXVA2Renderer::initializeRenderer()
D3DSURFACE_DESC renderTargetDesc;
m_RenderTarget->GetDesc(&renderTargetDesc);
m_DisplayWidth = renderTargetDesc.Width;
m_DisplayHeight = renderTargetDesc.Height;
hr = DXVA2CreateVideoService(m_Device, IID_IDirectXVideoProcessorService,
reinterpret_cast<void**>(&m_ProcService));
@@ -289,14 +302,6 @@ bool DXVA2Renderer::initializeRenderer()
caps.VideoProcessorOperations);
continue;
}
else if (!(caps.VideoProcessorOperations & DXVA2_VideoProcess_StretchX) ||
!(caps.VideoProcessorOperations & DXVA2_VideoProcess_StretchY)) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"Device %d can't stretch video: %x",
i,
caps.VideoProcessorOperations);
continue;
}
m_ProcService->GetProcAmpRange(guids[i], &m_Desc, renderTargetDesc.Format, DXVA2_ProcAmp_Brightness, &m_BrightnessRange);
m_ProcService->GetProcAmpRange(guids[i], &m_Desc, renderTargetDesc.Format, DXVA2_ProcAmp_Contrast, &m_ContrastRange);
@@ -333,117 +338,24 @@ bool DXVA2Renderer::initializeRenderer()
return true;
}
bool DXVA2Renderer::isDecoderBlacklisted()
{
IDirect3D9* d3d9;
HRESULT hr;
bool result = false;
// TODO: Update for HEVC Main10
SDL_assert(m_VideoFormat != VIDEO_FORMAT_H265_MAIN10);
hr = m_Device->GetDirect3D(&d3d9);
if (SUCCEEDED(hr)) {
D3DCAPS9 caps;
hr = m_Device->GetDeviceCaps(&caps);
if (SUCCEEDED(hr)) {
D3DADAPTER_IDENTIFIER9 id;
hr = d3d9->GetAdapterIdentifier(caps.AdapterOrdinal, 0, &id);
if (SUCCEEDED(hr)) {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Detected GPU: %s (%x:%x)",
id.Description,
id.VendorId,
id.DeviceId);
if (id.VendorId == 0x8086) {
// Intel seems to encode the series in the high byte of
// the device ID. We want to avoid the "Partial" acceleration
// support explicitly. Those will claim to have HW acceleration
// but perform badly.
// https://en.wikipedia.org/wiki/Intel_Graphics_Technology#Capabilities_(GPU_video_acceleration)
// https://raw.githubusercontent.com/GameTechDev/gpudetect/master/IntelGfx.cfg
switch (id.DeviceId & 0xFF00) {
case 0x0400: // Haswell
case 0x0A00: // Haswell
case 0x0D00: // Haswell
case 0x1600: // Broadwell
case 0x2200: // Cherry Trail and Braswell
// Blacklist these for HEVC to avoid hybrid decode
result = (m_VideoFormat & VIDEO_FORMAT_MASK_H265) != 0;
break;
default:
// Everything else is fine with whatever it says it supports
result = false;
break;
}
}
else if (id.VendorId == 0x10DE) {
// For NVIDIA, we wait to avoid those GPUs with Feature Set E
// for HEVC decoding, since that's hybrid.
// https://en.wikipedia.org/wiki/Nvidia_PureVideo
// http://envytools.readthedocs.io/en/latest/hw/pciid.html (missing GM200)
if ((id.DeviceId >= 0x1340 && id.DeviceId <= 0x137F) || // GM108
(id.DeviceId >= 0x1380 && id.DeviceId <= 0x13BF) || // GM107
(id.DeviceId >= 0x13C0 && id.DeviceId <= 0x13FF) || // GM204
(id.DeviceId >= 0x1617 && id.DeviceId <= 0x161A) || // GM204
(id.DeviceId == 0x1667) || // GM204
(id.DeviceId >= 0x17C0 && id.DeviceId <= 0x17FF)) { // GM200
// Avoid HEVC on Feature Set E GPUs
result = (m_VideoFormat & VIDEO_FORMAT_MASK_H265) != 0;
}
else {
result = false;
}
}
else if (id.VendorId == 0x1002) {
// AMD doesn't seem to do hybrid acceleration?
result = false;
}
else {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"Unrecognized vendor ID: %x",
id.VendorId);
}
}
}
else {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"GetDeviceCaps() failed: %x", hr);
}
d3d9->Release();
}
else {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"GetDirect3D() failed: %x", hr);
}
if (result) {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"GPU blacklisted for format %x",
m_VideoFormat);
}
return result;
}
bool DXVA2Renderer::initialize(SDL_Window* window, int videoFormat, int width, int height)
{
m_VideoFormat = videoFormat;
m_VideoWidth = width;
m_VideoHeight = height;
m_Width = width;
m_Height = height;
// FFmpeg will be decoding on different threads than the main thread that we're
// currently running on right now. We must set this hint so SDL will pass
// D3DCREATE_MULTITHREADED to IDirect3D9::CreateDevice().
SDL_SetHint(SDL_HINT_RENDER_DIRECT3D_THREADSAFE, "1");
// We require full-screen desktop mode to avoid having to enable V-sync
// to synchronize frame delivery (which has a much higher latency penalty).
m_SdlRenderer = SDL_CreateRenderer(window, -1, SDL_RENDERER_ACCELERATED);
// We want VSYNC when running in full-screen. DWM will give us
// tear-free video when running composited (windowed) without any
// extra latency waiting for VSYNC.
m_SdlRenderer = SDL_CreateRenderer(window, -1,
SDL_RENDERER_ACCELERATED |
((SDL_GetWindowFlags(window) & SDL_WINDOW_FULLSCREEN) ?
SDL_RENDERER_PRESENTVSYNC : 0));
if (!m_SdlRenderer) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"SDL_CreateRenderer() failed: %s",
@@ -453,31 +365,15 @@ bool DXVA2Renderer::initialize(SDL_Window* window, int videoFormat, int width, i
m_Device = SDL_RenderGetD3D9Device(m_SdlRenderer);
// Draw a black frame until the video stream starts rendering
SDL_SetRenderDrawColor(m_SdlRenderer, 0, 0, 0, SDL_ALPHA_OPAQUE);
SDL_RenderClear(m_SdlRenderer);
SDL_RenderPresent(m_SdlRenderer);
RtlZeroMemory(&m_Desc, sizeof(m_Desc));
int alignment;
// HEVC using DXVA requires 128B alignment
if (m_VideoFormat & VIDEO_FORMAT_MASK_H265) {
alignment = 128;
}
else {
alignment = 16;
}
m_Desc.SampleWidth = FFALIGN(m_VideoWidth, alignment);
m_Desc.SampleHeight = FFALIGN(m_VideoHeight, alignment);
m_Desc.SampleFormat.VideoChromaSubsampling = DXVA2_VideoChromaSubsampling_Unknown;
m_Desc.SampleFormat.NominalRange = DXVA2_NominalRange_Unknown;
m_Desc.SampleFormat.VideoTransferMatrix = DXVA2_VideoTransferMatrix_Unknown;
m_Desc.SampleFormat.VideoLighting = DXVA2_VideoLighting_Unknown;
m_Desc.SampleFormat.VideoPrimaries = DXVA2_VideoPrimaries_Unknown;
m_Desc.SampleFormat.VideoTransferFunction = DXVA2_VideoTransFunc_Unknown;
m_Desc.SampleWidth = m_Width;
m_Desc.SampleHeight = m_Height;
m_Desc.SampleFormat.VideoChromaSubsampling = DXVA2_VideoChromaSubsampling_ProgressiveChroma;
m_Desc.SampleFormat.NominalRange = DXVA2_NominalRange_0_255;
m_Desc.SampleFormat.VideoTransferMatrix = DXVA2_VideoTransferMatrix_BT709;
m_Desc.SampleFormat.VideoLighting = DXVA2_VideoLighting_dim;
m_Desc.SampleFormat.VideoPrimaries = DXVA2_VideoPrimaries_BT709;
m_Desc.SampleFormat.VideoTransferFunction = DXVA2_VideoTransFunc_709;
m_Desc.SampleFormat.SampleFormat = DXVA2_SampleProgressiveFrame;
m_Desc.Format = (D3DFORMAT)MAKEFOURCC('N','V','1','2');
@@ -497,126 +393,43 @@ void DXVA2Renderer::renderFrame(AVFrame* frame)
IDirect3DSurface9* surface = reinterpret_cast<IDirect3DSurface9*>(frame->data[3]);
HRESULT hr;
switch (frame->color_range) {
case AVCOL_RANGE_JPEG:
m_Desc.SampleFormat.NominalRange = DXVA2_NominalRange_0_255;
break;
case AVCOL_RANGE_MPEG:
m_Desc.SampleFormat.NominalRange = DXVA2_NominalRange_16_235;
hr = m_Device->TestCooperativeLevel();
switch (hr) {
case D3D_OK:
break;
case D3DERR_DEVICELOST:
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"D3D device lost!");
return;
case D3DERR_DEVICENOTRESET:
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"D3D device not reset!");
return;
default:
m_Desc.SampleFormat.NominalRange = DXVA2_NominalRange_Unknown;
break;
}
switch (frame->color_primaries) {
case AVCOL_PRI_BT709:
m_Desc.SampleFormat.VideoPrimaries = DXVA2_VideoPrimaries_BT709;
break;
case AVCOL_PRI_BT470M:
m_Desc.SampleFormat.VideoPrimaries = DXVA2_VideoPrimaries_BT470_2_SysM;
break;
case AVCOL_PRI_BT470BG:
m_Desc.SampleFormat.VideoPrimaries = DXVA2_VideoPrimaries_BT470_2_SysBG;
break;
case AVCOL_PRI_SMPTE170M:
m_Desc.SampleFormat.VideoPrimaries = DXVA2_VideoPrimaries_SMPTE170M;
break;
case AVCOL_PRI_SMPTE240M:
m_Desc.SampleFormat.VideoPrimaries = DXVA2_VideoPrimaries_SMPTE240M;
break;
default:
m_Desc.SampleFormat.VideoPrimaries = DXVA2_VideoPrimaries_Unknown;
break;
}
switch (frame->color_trc) {
case AVCOL_TRC_SMPTE170M:
case AVCOL_TRC_BT709:
m_Desc.SampleFormat.VideoTransferFunction = DXVA2_VideoTransFunc_709;
break;
case AVCOL_TRC_LINEAR:
m_Desc.SampleFormat.VideoTransferFunction = DXVA2_VideoTransFunc_10;
break;
case AVCOL_TRC_GAMMA22:
m_Desc.SampleFormat.VideoTransferFunction = DXVA2_VideoTransFunc_22;
break;
case AVCOL_TRC_GAMMA28:
m_Desc.SampleFormat.VideoTransferFunction = DXVA2_VideoTransFunc_28;
break;
case AVCOL_TRC_SMPTE240M:
m_Desc.SampleFormat.VideoTransferFunction = DXVA2_VideoTransFunc_240M;
break;
case AVCOL_TRC_IEC61966_2_1:
m_Desc.SampleFormat.VideoTransferFunction = DXVA2_VideoTransFunc_sRGB;
break;
default:
m_Desc.SampleFormat.VideoTransferFunction = DXVA2_VideoTransFunc_Unknown;
break;
}
switch (frame->colorspace) {
case AVCOL_SPC_BT709:
m_Desc.SampleFormat.VideoTransferMatrix = DXVA2_VideoTransferMatrix_BT709;
break;
case AVCOL_SPC_BT470BG:
case AVCOL_SPC_SMPTE170M:
m_Desc.SampleFormat.VideoTransferMatrix = DXVA2_VideoTransferMatrix_BT601;
break;
case AVCOL_SPC_SMPTE240M:
m_Desc.SampleFormat.VideoTransferMatrix = DXVA2_VideoTransferMatrix_SMPTE240M;
break;
default:
m_Desc.SampleFormat.VideoTransferMatrix = DXVA2_VideoTransferMatrix_Unknown;
break;
}
switch (frame->chroma_location) {
case AVCHROMA_LOC_LEFT:
m_Desc.SampleFormat.VideoChromaSubsampling = DXVA2_VideoChromaSubsampling_Horizontally_Cosited |
DXVA2_VideoChromaSubsampling_Vertically_AlignedChromaPlanes;
break;
case AVCHROMA_LOC_CENTER:
m_Desc.SampleFormat.VideoChromaSubsampling = DXVA2_VideoChromaSubsampling_Vertically_AlignedChromaPlanes;
break;
case AVCHROMA_LOC_TOPLEFT:
m_Desc.SampleFormat.VideoChromaSubsampling = DXVA2_VideoChromaSubsampling_Horizontally_Cosited |
DXVA2_VideoChromaSubsampling_Vertically_Cosited;
break;
default:
m_Desc.SampleFormat.VideoChromaSubsampling = DXVA2_VideoChromaSubsampling_Unknown;
break;
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Unknown D3D error: %x",
hr);
return;
}
DXVA2_VideoSample sample = {};
sample.Start = m_FrameIndex;
sample.End = m_FrameIndex + 1;
sample.SrcSurface = surface;
sample.SrcRect.right = m_VideoWidth;
sample.SrcRect.bottom = m_VideoHeight;
sample.SrcRect.right = m_Desc.SampleWidth;
sample.SrcRect.bottom = m_Desc.SampleHeight;
sample.DstRect = sample.SrcRect;
sample.SampleFormat = m_Desc.SampleFormat;
sample.PlanarAlpha = DXVA2_Fixed32OpaqueAlpha();
// Center in frame and preserve aspect ratio
SDL_Rect src, dst;
src.x = src.y = 0;
src.w = m_VideoWidth;
src.h = m_VideoHeight;
dst.x = dst.y = 0;
dst.w = m_DisplayWidth;
dst.h = m_DisplayHeight;
StreamUtils::scaleSourceToDestinationSurface(&src, &dst);
sample.DstRect.left = dst.x;
sample.DstRect.right = dst.x + dst.w;
sample.DstRect.top = dst.y;
sample.DstRect.bottom = dst.y + dst.h;
DXVA2_VideoProcessBltParams bltParams = {};
bltParams.TargetFrame = m_FrameIndex++;
bltParams.TargetRect = sample.DstRect;
bltParams.TargetRect.right = m_Desc.SampleWidth;
bltParams.TargetRect.bottom = m_Desc.SampleHeight;
bltParams.BackgroundColor.Y = 0x1000;
bltParams.BackgroundColor.Cb = 0x8000;
bltParams.BackgroundColor.Cr = 0x8000;
bltParams.BackgroundColor.Alpha = 0xFFFF;
bltParams.DestFormat.SampleFormat = DXVA2_SampleProgressiveFrame;
@@ -628,33 +441,12 @@ void DXVA2Renderer::renderFrame(AVFrame* frame)
bltParams.Alpha = DXVA2_Fixed32OpaqueAlpha();
if (SDL_RenderClear(m_SdlRenderer) != 0) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"SDL_RenderClear() failed: %s",
SDL_GetError());
av_frame_free(&frame);
// We're going to cheat a little bit here. It seems SDL's
// renderer may flake out in scenarios like moving the window
// between monitors, so generate a synthetic reset event for
// the main loop to consume.
SDL_Event event;
event.type = SDL_RENDER_TARGETS_RESET;
SDL_PushEvent(&event);
return;
}
hr = m_Processor->VideoProcessBlt(m_RenderTarget, &bltParams, &sample, 1, nullptr);
av_frame_free(&frame);
if (FAILED(hr)) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"VideoProcessBlt() failed: %x",
hr);
}
// We must try to present to trigger SDL's logic to recover the render target,
// even if VideoProcessBlt() fails.
SDL_RenderPresent(m_SdlRenderer);
m_Device->Present(nullptr, nullptr, nullptr, nullptr);
}
+6 -6
View File
@@ -24,7 +24,6 @@ public:
private:
bool initializeDecoder();
bool initializeRenderer();
bool isDecoderBlacklisted();
static
AVBufferRef* ffPoolAlloc(void* opaque, int size);
@@ -35,12 +34,13 @@ private:
static
int ffGetBuffer2(AVCodecContext* context, AVFrame* frame, int flags);
int m_VideoFormat;
int m_VideoWidth;
int m_VideoHeight;
static
enum AVPixelFormat ffGetFormat(AVCodecContext*,
const enum AVPixelFormat* pixFmts);
int m_DisplayWidth;
int m_DisplayHeight;
int m_VideoFormat;
int m_Width;
int m_Height;
SDL_Renderer* m_SdlRenderer;
@@ -1,7 +1,5 @@
#include "renderer.h"
#include <Limelight.h>
SdlRenderer::SdlRenderer()
: m_Renderer(nullptr),
m_Texture(nullptr)
@@ -23,10 +21,6 @@ SdlRenderer::~SdlRenderer()
bool SdlRenderer::prepareDecoderContext(AVCodecContext*)
{
/* Nothing to do */
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Using SDL software renderer");
return true;
}
@@ -47,11 +41,6 @@ bool SdlRenderer::initialize(SDL_Window* window,
// logical rendering surface size is equal to the stream size
SDL_RenderSetLogicalSize(m_Renderer, width, height);
// Draw a black frame until the video stream starts rendering
SDL_SetRenderDrawColor(m_Renderer, 0, 0, 0, SDL_ALPHA_OPAQUE);
SDL_RenderClear(m_Renderer);
SDL_RenderPresent(m_Renderer);
m_Texture = SDL_CreateTexture(m_Renderer,
SDL_PIXELFORMAT_YV12,
SDL_TEXTUREACCESS_STREAMING,
@@ -76,10 +65,6 @@ void SdlRenderer::renderFrame(AVFrame* frame)
frame->linesize[1],
frame->data[2],
frame->linesize[2]);
// Done with the frame now
av_frame_free(&frame);
SDL_RenderClear(m_Renderer);
SDL_RenderCopy(m_Renderer, m_Texture, nullptr, nullptr);
SDL_RenderPresent(m_Renderer);
@@ -1,133 +0,0 @@
#include "vaapi.h"
#include <streaming/streamutils.h>
#include <dlfcn.h>
#include <SDL_syswm.h>
VAAPIRenderer::VAAPIRenderer()
: m_HwContext(nullptr),
m_X11VaLibHandle(nullptr),
m_vaPutSurface(nullptr)
{
}
VAAPIRenderer::~VAAPIRenderer()
{
if (m_HwContext != nullptr) {
av_buffer_unref(&m_HwContext);
}
if (m_X11VaLibHandle != nullptr) {
dlclose(m_X11VaLibHandle);
}
}
bool
VAAPIRenderer::initialize(SDL_Window* window, int, int width, int height)
{
int err;
SDL_SysWMinfo info;
m_VideoWidth = width;
m_VideoHeight = height;
SDL_GetWindowSize(window, &m_DisplayWidth, &m_DisplayHeight);
SDL_VERSION(&info.version);
if (!SDL_GetWindowWMInfo(window, &info)) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"SDL_GetWindowWMInfo() failed: %s",
SDL_GetError());
return false;
}
SDL_assert(info.subsystem == SDL_SYSWM_X11);
if (info.subsystem == SDL_SYSWM_X11) {
m_XWindow = info.info.x11.window;
m_X11VaLibHandle = dlopen("libva-x11.so", RTLD_LAZY);
if (!m_X11VaLibHandle) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"dlopen(libva.so) failed: %s",
dlerror());
return false;
}
m_vaPutSurface = (vaPutSurface_t)dlsym(m_X11VaLibHandle, "vaPutSurface");
}
else if (info.subsystem == SDL_SYSWM_WAYLAND) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"VAAPI backend does not currently support Wayland");
return false;
}
else {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Unsupported VAAPI rendering subsystem: %d",
info.subsystem);
return false;
}
err = av_hwdevice_ctx_create(&m_HwContext,
AV_HWDEVICE_TYPE_VAAPI,
nullptr, nullptr, 0);
if (err < 0) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Failed to create VAAPI context: %d",
err);
return false;
}
// This quirk is set for the VDPAU wrapper which doesn't work with our VAAPI renderer
if (((AVVAAPIDeviceContext*)((AVHWDeviceContext*)(m_HwContext->data))->hwctx)->driver_quirks & AV_VAAPI_DRIVER_QUIRK_SURFACE_ATTRIBUTES) {
// Fail and let our VDPAU renderer pick this up
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"Avoiding VDPAU wrapper for VAAPI decoding");
return false;
}
return true;
}
bool
VAAPIRenderer::prepareDecoderContext(AVCodecContext* context)
{
context->hw_device_ctx = av_buffer_ref(m_HwContext);
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Using VAAPI accelerated renderer");
return true;
}
void
VAAPIRenderer::renderFrame(AVFrame* frame)
{
VASurfaceID surface = (VASurfaceID)(uintptr_t)frame->data[3];
AVHWDeviceContext* deviceContext = (AVHWDeviceContext*)m_HwContext->data;
AVVAAPIDeviceContext* vaDeviceContext = (AVVAAPIDeviceContext*)deviceContext->hwctx;
SDL_Rect src, dst;
src.x = src.y = 0;
src.w = m_VideoWidth;
src.h = m_VideoHeight;
dst.x = dst.y = 0;
dst.w = m_DisplayWidth;
dst.h = m_DisplayHeight;
StreamUtils::scaleSourceToDestinationSurface(&src, &dst);
m_vaPutSurface(vaDeviceContext->display,
surface,
m_XWindow,
0, 0,
m_VideoWidth, m_VideoHeight,
dst.x, dst.y,
dst.w, dst.h,
NULL, 0, 0);
av_frame_free(&frame);
}
@@ -1,49 +0,0 @@
#pragma once
#include "renderer.h"
extern "C" {
#include <va/va.h>
#include <va/va_x11.h>
#include <libavutil/hwcontext_vaapi.h>
}
class VAAPIRenderer : public IFFmpegRenderer
{
typedef VAStatus (*vaPutSurface_t)(
VADisplay dpy,
VASurfaceID surface,
Drawable draw, /* X Drawable */
short srcx,
short srcy,
unsigned short srcw,
unsigned short srch,
short destx,
short desty,
unsigned short destw,
unsigned short desth,
VARectangle *cliprects,
unsigned int number_cliprects,
unsigned int flags
);
public:
VAAPIRenderer();
virtual ~VAAPIRenderer();
virtual bool initialize(SDL_Window* window,
int videoFormat,
int width,
int height);
virtual bool prepareDecoderContext(AVCodecContext* context);
virtual void renderFrame(AVFrame* frame);
private:
Window m_XWindow;
AVBufferRef* m_HwContext;
void* m_X11VaLibHandle;
vaPutSurface_t m_vaPutSurface;
int m_VideoWidth;
int m_VideoHeight;
int m_DisplayWidth;
int m_DisplayHeight;
};
@@ -1,332 +0,0 @@
#include "vdpau.h"
#include <streaming/streamutils.h>
#include <SDL_syswm.h>
#define BAIL_ON_FAIL(status, something) if ((status) != VDP_STATUS_OK) { \
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, \
#something " failed: %d", (status)); \
return false; \
}
#define GET_PROC_ADDRESS(id, func) status = vdpauCtx->get_proc_address(m_Device, id, (void**)func); \
BAIL_ON_FAIL(status, id)
const VdpRGBAFormat VDPAURenderer::k_OutputFormats[] = {
VDP_RGBA_FORMAT_B8G8R8A8,
VDP_RGBA_FORMAT_R8G8B8A8
};
VDPAURenderer::VDPAURenderer()
: m_HwContext(nullptr),
m_PresentationQueueTarget(0),
m_PresentationQueue(0),
m_VideoMixer(0),
m_NextSurfaceIndex(0)
{
SDL_zero(m_OutputSurface);
}
VDPAURenderer::~VDPAURenderer()
{
if (m_PresentationQueue != 0) {
m_VdpPresentationQueueDestroy(m_PresentationQueue);
}
if (m_VideoMixer != 0) {
m_VdpVideoMixerDestroy(m_VideoMixer);
}
if (m_PresentationQueueTarget != 0) {
m_VdpPresentationQueueTargetDestroy(m_PresentationQueueTarget);
}
for (int i = 0; i < OUTPUT_SURFACE_COUNT; i++) {
if (m_OutputSurface[i] != 0) {
m_VdpOutputSurfaceDestroy(m_OutputSurface[i]);
}
}
// This must be done last as it frees VDPAU context required to call
// the functions above.
if (m_HwContext != nullptr) {
av_buffer_unref(&m_HwContext);
}
}
bool VDPAURenderer::initialize(SDL_Window* window, int, int width, int height)
{
int err;
VdpStatus status;
SDL_SysWMinfo info;
m_VideoWidth = width;
m_VideoHeight = height;
err = av_hwdevice_ctx_create(&m_HwContext,
AV_HWDEVICE_TYPE_VDPAU,
nullptr, nullptr, 0);
if (err < 0) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Failed to create VDPAU context: %d",
err);
return false;
}
AVHWDeviceContext* devCtx = (AVHWDeviceContext*)m_HwContext->data;
AVVDPAUDeviceContext* vdpauCtx = (AVVDPAUDeviceContext*)devCtx->hwctx;
m_Device = vdpauCtx->device;
GET_PROC_ADDRESS(VDP_FUNC_ID_GET_ERROR_STRING, &m_VdpGetErrorString);
GET_PROC_ADDRESS(VDP_FUNC_ID_PRESENTATION_QUEUE_TARGET_DESTROY, &m_VdpPresentationQueueTargetDestroy);
GET_PROC_ADDRESS(VDP_FUNC_ID_VIDEO_MIXER_CREATE, &m_VdpVideoMixerCreate);
GET_PROC_ADDRESS(VDP_FUNC_ID_VIDEO_MIXER_DESTROY, &m_VdpVideoMixerDestroy);
GET_PROC_ADDRESS(VDP_FUNC_ID_VIDEO_MIXER_RENDER, &m_VdpVideoMixerRender);
GET_PROC_ADDRESS(VDP_FUNC_ID_PRESENTATION_QUEUE_CREATE, &m_VdpPresentationQueueCreate);
GET_PROC_ADDRESS(VDP_FUNC_ID_PRESENTATION_QUEUE_DESTROY, &m_VdpPresentationQueueDestroy);
GET_PROC_ADDRESS(VDP_FUNC_ID_PRESENTATION_QUEUE_DISPLAY, &m_VdpPresentationQueueDisplay);
GET_PROC_ADDRESS(VDP_FUNC_ID_PRESENTATION_QUEUE_SET_BACKGROUND_COLOR, &m_VdpPresentationQueueSetBackgroundColor);
GET_PROC_ADDRESS(VDP_FUNC_ID_PRESENTATION_QUEUE_BLOCK_UNTIL_SURFACE_IDLE, &m_VdpPresentationQueueBlockUntilSurfaceIdle);
GET_PROC_ADDRESS(VDP_FUNC_ID_OUTPUT_SURFACE_CREATE, &m_VdpOutputSurfaceCreate);
GET_PROC_ADDRESS(VDP_FUNC_ID_OUTPUT_SURFACE_DESTROY, &m_VdpOutputSurfaceDestroy);
GET_PROC_ADDRESS(VDP_FUNC_ID_OUTPUT_SURFACE_QUERY_CAPABILITIES, &m_VdpOutputSurfaceQueryCapabilities);
GET_PROC_ADDRESS(VDP_FUNC_ID_VIDEO_SURFACE_GET_PARAMETERS, &m_VdpVideoSurfaceGetParameters);
SDL_GetWindowSize(window, (int*)&m_DisplayWidth, (int*)&m_DisplayHeight);
SDL_VERSION(&info.version);
if (!SDL_GetWindowWMInfo(window, &info)) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"SDL_GetWindowWMInfo() failed: %s",
SDL_GetError());
return false;
}
SDL_assert(info.subsystem == SDL_SYSWM_X11);
if (info.subsystem == SDL_SYSWM_X11) {
GET_PROC_ADDRESS(VDP_FUNC_ID_PRESENTATION_QUEUE_TARGET_CREATE_X11,
&m_VdpPresentationQueueTargetCreateX11);
status = m_VdpPresentationQueueTargetCreateX11(m_Device,
info.info.x11.window,
&m_PresentationQueueTarget);
if (status != VDP_STATUS_OK) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"VdpPresentationQueueTargetCreateX11() failed: %s",
m_VdpGetErrorString(status));
return false;
}
}
else if (info.subsystem == SDL_SYSWM_WAYLAND) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"VDPAU backend does not currently support Wayland");
return false;
}
else {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Unsupported VDPAU rendering subsystem: %d",
info.subsystem);
return false;
}
// Try our available output formats to find something the GPU supports
bool foundFormat = false;
for (int i = 0; i < OUTPUT_SURFACE_FORMAT_COUNT; i++) {
VdpBool supported;
uint32_t maxWidth, maxHeight;
status = m_VdpOutputSurfaceQueryCapabilities(m_Device, k_OutputFormats[i],
&supported, &maxWidth, &maxHeight);
if (status != VDP_STATUS_OK) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"VdpOutputSurfaceQueryCapabilities() failed: %s",
m_VdpGetErrorString(status));
return false;
}
if (supported) {
if (m_DisplayWidth <= maxWidth && m_DisplayHeight <= maxHeight) {
m_OutputSurfaceFormat = k_OutputFormats[i];
foundFormat = true;
break;
}
else {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"Display size not within capabilities %dx%d vs %dx%d",
m_DisplayWidth, m_DisplayWidth,
maxWidth, maxHeight);
}
}
}
if (!foundFormat) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"No compatible output surface format found!");
return false;
}
// Create the output surfaces
for (int i = 0; i < OUTPUT_SURFACE_COUNT; i++) {
// It seems there's some lazy freeing going on or something in VDPAU
// because we can get VDP_STATUS_RESOURCES, then wait a bit and it'll
// complete without a problem.
int tries = 1;
do {
status = m_VdpOutputSurfaceCreate(m_Device, m_OutputSurfaceFormat,
m_DisplayWidth, m_DisplayHeight,
&m_OutputSurface[i]);
if (status != VDP_STATUS_OK) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"VdpOutputSurfaceCreate() try #%d: %s",
tries,
m_VdpGetErrorString(status));
SDL_Delay(250);
}
} while (status == VDP_STATUS_RESOURCES && ++tries <= 10);
if (status != VDP_STATUS_OK) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"VdpOutputSurfaceCreate() failed: %s",
m_VdpGetErrorString(status));
return false;
}
}
status = m_VdpPresentationQueueCreate(m_Device, m_PresentationQueueTarget,
&m_PresentationQueue);
if (status != VDP_STATUS_OK) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"VdpPresentationQueueCreate() failed: %s",
m_VdpGetErrorString(status));
return false;
}
// Set the background to opaque black
VdpColor color = {0.0, 0.0, 0.0, 1.0};
m_VdpPresentationQueueSetBackgroundColor(m_PresentationQueue, &color);
return true;
}
bool VDPAURenderer::prepareDecoderContext(AVCodecContext* context)
{
context->hw_device_ctx = av_buffer_ref(m_HwContext);
// Allow HEVC usage on VDPAU. This was disabled by FFmpeg due to
// GL interop issues, but we use VDPAU for rendering so it's no issue.
// https://github.com/FFmpeg/FFmpeg/commit/64ecb78b7179cab2dbdf835463104679dbb7c895
context->hwaccel_flags |= AV_HWACCEL_FLAG_ALLOW_PROFILE_MISMATCH;
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Using VDPAU accelerated renderer");
return true;
}
void VDPAURenderer::renderFrame(AVFrame* frame)
{
VdpStatus status;
VdpVideoSurface videoSurface = (VdpVideoSurface)(uintptr_t)frame->data[3];
// This is safe without locking because this is always called on the main thread
VdpOutputSurface chosenSurface = m_OutputSurface[m_NextSurfaceIndex];
m_NextSurfaceIndex = (m_NextSurfaceIndex + 1) % OUTPUT_SURFACE_COUNT;
// We need to create the mixer on the fly, because we don't know the dimensions
// of our video surfaces in advance of decoding
if (m_VideoMixer == 0) {
VdpChromaType chroma;
uint32_t videoSurfaceWidth, videoSurfaceHeight;
status = m_VdpVideoSurfaceGetParameters(videoSurface, &chroma, &videoSurfaceWidth, &videoSurfaceHeight);
if (status != VDP_STATUS_OK) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"VdpVideoSurfaceGetParameters() failed: %s",
m_VdpGetErrorString(status));
av_frame_free(&frame);
return;
}
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"VDPAU surface size: %dx%d",
videoSurfaceWidth, videoSurfaceHeight);
#define PARAM_COUNT 2
const VdpVideoMixerParameter params[PARAM_COUNT] = {
VDP_VIDEO_MIXER_PARAMETER_VIDEO_SURFACE_WIDTH,
VDP_VIDEO_MIXER_PARAMETER_VIDEO_SURFACE_HEIGHT,
};
const void* const paramValues[PARAM_COUNT] = {
&videoSurfaceWidth,
&videoSurfaceHeight,
};
status = m_VdpVideoMixerCreate(m_Device, 0, nullptr,
PARAM_COUNT, params, paramValues,
&m_VideoMixer);
if (status != VDP_STATUS_OK) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"VdpVideoMixerCreate() failed: %s",
m_VdpGetErrorString(status));
av_frame_free(&frame);
return;
}
}
// Wait for this frame to be off the screen
VdpTime pts;
m_VdpPresentationQueueBlockUntilSurfaceIdle(m_PresentationQueue, chosenSurface, &pts);
VdpRect sourceRect, outputRect;
SDL_Rect src, dst;
src.x = src.y = 0;
src.w = m_VideoWidth;
src.h = m_VideoHeight;
dst.x = dst.y = 0;
dst.w = m_DisplayWidth;
dst.h = m_DisplayHeight;
StreamUtils::scaleSourceToDestinationSurface(&src, &dst);
outputRect.x0 = dst.x;
outputRect.x1 = dst.x + dst.w;
outputRect.y0 = dst.y;
outputRect.y1 = dst.y + dst.h;
sourceRect.x0 = sourceRect.y0 = 0;
sourceRect.x1 = m_VideoWidth;
sourceRect.y1 = m_VideoHeight;
// Render the next frame into the output surface
status = m_VdpVideoMixerRender(m_VideoMixer,
VDP_INVALID_HANDLE, nullptr,
VDP_VIDEO_MIXER_PICTURE_STRUCTURE_FRAME,
0, nullptr,
videoSurface,
0, nullptr,
&sourceRect,
chosenSurface,
&outputRect,
nullptr,
0,
nullptr);
// The decoder can have this surface back now
av_frame_free(&frame);
if (status != VDP_STATUS_OK) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"VdpVideoMixerRender() failed: %s",
m_VdpGetErrorString(status));
return;
}
// Queue the frame for display immediately
status = m_VdpPresentationQueueDisplay(m_PresentationQueue, chosenSurface, 0, 0, 0);
if (status != VDP_STATUS_OK) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"VdpPresentationQueueDisplay() failed: %s",
m_VdpGetErrorString(status));
return;
}
}
@@ -1,59 +0,0 @@
#pragma once
#include "renderer.h"
extern "C" {
#include <vdpau/vdpau.h>
#include <vdpau/vdpau_x11.h>
#include <libavutil/hwcontext_vdpau.h>
}
class VDPAURenderer : public IFFmpegRenderer
{
public:
VDPAURenderer();
virtual ~VDPAURenderer();
virtual bool initialize(SDL_Window* window,
int videoFormat,
int width,
int height);
virtual bool prepareDecoderContext(AVCodecContext* context);
virtual void renderFrame(AVFrame* frame);
private:
uint32_t m_VideoWidth, m_VideoHeight;
uint32_t m_DisplayWidth, m_DisplayHeight;
AVBufferRef* m_HwContext;
VdpPresentationQueueTarget m_PresentationQueueTarget;
VdpPresentationQueue m_PresentationQueue;
VdpVideoMixer m_VideoMixer;
VdpRGBAFormat m_OutputSurfaceFormat;
VdpDevice m_Device;
#define OUTPUT_SURFACE_COUNT 3
VdpOutputSurface m_OutputSurface[OUTPUT_SURFACE_COUNT];
int m_NextSurfaceIndex;
#define OUTPUT_SURFACE_FORMAT_COUNT 2
static const VdpRGBAFormat k_OutputFormats[OUTPUT_SURFACE_FORMAT_COUNT];
VdpGetErrorString* m_VdpGetErrorString;
VdpPresentationQueueTargetDestroy* m_VdpPresentationQueueTargetDestroy;
VdpVideoMixerCreate* m_VdpVideoMixerCreate;
VdpVideoMixerDestroy* m_VdpVideoMixerDestroy;
VdpVideoMixerRender* m_VdpVideoMixerRender;
VdpPresentationQueueCreate* m_VdpPresentationQueueCreate;
VdpPresentationQueueDestroy* m_VdpPresentationQueueDestroy;
VdpPresentationQueueDisplay* m_VdpPresentationQueueDisplay;
VdpPresentationQueueSetBackgroundColor* m_VdpPresentationQueueSetBackgroundColor;
VdpPresentationQueueBlockUntilSurfaceIdle* m_VdpPresentationQueueBlockUntilSurfaceIdle;
VdpOutputSurfaceCreate* m_VdpOutputSurfaceCreate;
VdpOutputSurfaceDestroy* m_VdpOutputSurfaceDestroy;
VdpOutputSurfaceQueryCapabilities* m_VdpOutputSurfaceQueryCapabilities;
VdpVideoSurfaceGetParameters* m_VdpVideoSurfaceGetParameters;
// X11 stuff
VdpPresentationQueueTargetCreateX11* m_VdpPresentationQueueTargetCreateX11;
};
+39 -147
View File
@@ -7,14 +7,9 @@
#include <SDL_syswm.h>
#include <Limelight.h>
#include <QQueue>
#import <Cocoa/Cocoa.h>
#import <VideoToolbox/VideoToolbox.h>
#import <AVFoundation/AVFoundation.h>
#import <CoreVideo/CoreVideo.h>
#define FRAME_HISTORY_ENTRIES 8
class VTRenderer : public IFFmpegRenderer
{
@@ -23,10 +18,9 @@ public:
: m_HwContext(nullptr),
m_DisplayLayer(nullptr),
m_FormatDesc(nullptr),
m_View(nullptr),
m_DisplayLink(nullptr),
m_FrameQueueLock(0)
m_View(nullptr)
{
}
virtual ~VTRenderer()
@@ -38,132 +32,8 @@ public:
if (m_FormatDesc != nullptr) {
CFRelease(m_FormatDesc);
}
if (m_DisplayLink != nullptr) {
CVDisplayLinkStop(m_DisplayLink);
CVDisplayLinkRelease(m_DisplayLink);
}
while (!m_FrameQueue.isEmpty()) {
AVFrame* frame = m_FrameQueue.dequeue();
av_frame_free(&frame);
}
if (m_View != nullptr) {
[m_View removeFromSuperview];
}
}
void drawFrame(uint64_t vsyncTime)
{
OSStatus status;
SDL_AtomicLock(&m_FrameQueueLock);
int frameDropTarget;
// If the queue length history entries are large, be strict
// about dropping excess frames.
frameDropTarget = 1;
for (int i = 0; i < m_FrameQueueHistory.count(); i++) {
if (m_FrameQueueHistory[i] <= 1) {
// Be lenient as long as the queue length
// resolves before the end of frame history
frameDropTarget = 3;
}
}
if (m_FrameQueueHistory.count() == FRAME_HISTORY_ENTRIES) {
m_FrameQueueHistory.dequeue();
}
m_FrameQueueHistory.enqueue(m_FrameQueue.count());
// Catch up if we're several frames ahead
while (m_FrameQueue.count() > frameDropTarget) {
AVFrame* frame = m_FrameQueue.dequeue();
av_frame_free(&frame);
}
if (m_FrameQueue.isEmpty()) {
SDL_AtomicUnlock(&m_FrameQueueLock);
return;
}
// Grab the first frame
AVFrame* frame = m_FrameQueue.dequeue();
SDL_AtomicUnlock(&m_FrameQueueLock);
CVPixelBufferRef pixBuf = reinterpret_cast<CVPixelBufferRef>(frame->data[3]);
// If the format has changed or doesn't exist yet, construct it with the
// pixel buffer data
if (!m_FormatDesc || !CMVideoFormatDescriptionMatchesImageBuffer(m_FormatDesc, pixBuf)) {
if (m_FormatDesc != nullptr) {
CFRelease(m_FormatDesc);
}
status = CMVideoFormatDescriptionCreateForImageBuffer(kCFAllocatorDefault,
pixBuf, &m_FormatDesc);
if (status != noErr) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"CMVideoFormatDescriptionCreateForImageBuffer() failed: %d",
status);
av_frame_free(&frame);
return;
}
}
// Queue this sample for the next v-sync
CMSampleTimingInfo timingInfo = {
.duration = kCMTimeInvalid,
.decodeTimeStamp = kCMTimeInvalid,
.presentationTimeStamp = CMTimeMake(vsyncTime, 1000 * 1000 * 1000)
};
CMSampleBufferRef sampleBuffer;
status = CMSampleBufferCreateReadyWithImageBuffer(kCFAllocatorDefault,
pixBuf,
m_FormatDesc,
&timingInfo,
&sampleBuffer);
if (status != noErr) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"CMSampleBufferCreateReadyWithImageBuffer() failed: %d",
status);
av_frame_free(&frame);
return;
}
[m_DisplayLayer enqueueSampleBuffer:sampleBuffer];
CFRelease(sampleBuffer);
av_frame_free(&frame);
}
static
CVReturn
displayLinkOutputCallback(
CVDisplayLinkRef,
const CVTimeStamp* now,
const CVTimeStamp* /* vsyncTime */,
CVOptionFlags,
CVOptionFlags*,
void *displayLinkContext)
{
VTRenderer* me = reinterpret_cast<VTRenderer*>(displayLinkContext);
// In my testing on macOS 10.13, this callback is invoked about 24 ms
// prior to the specified v-sync time (now - vsyncTime). Since this is
// greater than the standard v-sync interval (16 ms = 60 FPS), we will
// draw using the current host time, rather than the actual v-sync target
// time. Because the CVDisplayLink is in sync with the actual v-sync
// interval, even if many ms prior, we can safely use the current host time
// and get a consistent callback for each v-sync. This reduces video latency
// by at least 1 frame vs. rendering with the actual vsyncTime.
me->drawFrame(now->hostTime);
return kCVReturnSuccess;
}
virtual bool initialize(SDL_Window* window,
int videoFormat,
@@ -243,34 +113,60 @@ public:
return false;
}
CVDisplayLinkCreateWithActiveCGDisplays(&m_DisplayLink);
CVDisplayLinkSetOutputCallback(m_DisplayLink, displayLinkOutputCallback, this);
CVDisplayLinkStart(m_DisplayLink);
return true;
}
virtual bool prepareDecoderContext(AVCodecContext* context) override
{
context->hw_device_ctx = av_buffer_ref(m_HwContext);
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Using VideoToolbox accelerated renderer");
return true;
}
virtual void renderFrame(AVFrame* frame) override
{
CVPixelBufferRef pixBuf = reinterpret_cast<CVPixelBufferRef>(frame->data[3]);
OSStatus status;
if (m_DisplayLayer.status == AVQueuedSampleBufferRenderingStatusFailed) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"Resetting failed AVSampleBufferDisplay layer");
setupDisplayLayer();
}
SDL_AtomicLock(&m_FrameQueueLock);
m_FrameQueue.enqueue(frame);
SDL_AtomicUnlock(&m_FrameQueueLock);
// If the format has changed or doesn't exist yet, construct it with the
// pixel buffer data
if (!m_FormatDesc || !CMVideoFormatDescriptionMatchesImageBuffer(m_FormatDesc, pixBuf)) {
status = CMVideoFormatDescriptionCreateForImageBuffer(kCFAllocatorDefault,
pixBuf, &m_FormatDesc);
if (status != noErr) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"CMVideoFormatDescriptionCreateForImageBuffer() failed: %d",
status);
return;
}
}
CMSampleBufferRef sampleBuffer;
status = CMSampleBufferCreateReadyWithImageBuffer(kCFAllocatorDefault,
pixBuf,
m_FormatDesc,
&kCMTimingInfoInvalid,
&sampleBuffer);
if (status != noErr) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"CMSampleBufferCreateReadyWithImageBuffer() failed: %d",
status);
return;
}
CFArrayRef attachments = CMSampleBufferGetSampleAttachmentsArray(sampleBuffer, true);
CFMutableDictionaryRef dict = (CFMutableDictionaryRef)CFArrayGetValueAtIndex(attachments, 0);
CFDictionarySetValue(dict, kCMSampleAttachmentKey_DisplayImmediately, kCFBooleanTrue);
[m_DisplayLayer enqueueSampleBuffer:sampleBuffer];
CFRelease(sampleBuffer);
}
private:
@@ -296,10 +192,6 @@ private:
AVSampleBufferDisplayLayer* m_DisplayLayer;
CMVideoFormatDescriptionRef m_FormatDesc;
NSView* m_View;
CVDisplayLinkRef m_DisplayLink;
QQueue<AVFrame*> m_FrameQueue;
QQueue<int> m_FrameQueueHistory;
SDL_SpinLock m_FrameQueueLock;
};
IFFmpegRenderer* VTRendererFactory::createRenderer() {
+99 -219
View File
@@ -1,51 +1,87 @@
#include <Limelight.h>
#include "ffmpeg.h"
#ifdef Q_OS_WIN32
#ifdef _WIN32
#include "ffmpeg-renderers/dxva2.h"
#endif
#ifdef Q_OS_DARWIN
#include "ffmpeg-renderers/vt.h"
#endif
bool FFmpegVideoDecoder::chooseDecoder(
StreamingPreferences::VideoDecoderSelection vds,
SDL_Window* window,
int videoFormat,
int width, int height,
AVCodec*& chosenDecoder,
const AVCodecHWConfig*& chosenHwConfig,
IFFmpegRenderer*& newRenderer)
{
if (videoFormat & VIDEO_FORMAT_MASK_H264) {
chosenDecoder = avcodec_find_decoder(AV_CODEC_ID_H264);
}
else if (videoFormat & VIDEO_FORMAT_MASK_H265) {
chosenDecoder = avcodec_find_decoder(AV_CODEC_ID_HEVC);
}
else {
Q_ASSERT(false);
chosenDecoder = nullptr;
}
#ifdef HAVE_LIBVA
#include "ffmpeg-renderers/vaapi.h"
#endif
if (!chosenDecoder) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Unable to find decoder for format: %x",
videoFormat);
return false;
}
#ifdef HAVE_LIBVDPAU
#include "ffmpeg-renderers/vdpau.h"
#endif
for (int i = 0;; i++) {
const AVCodecHWConfig *config = avcodec_get_hw_config(chosenDecoder, i);
if (!config || vds == StreamingPreferences::VDS_FORCE_SOFTWARE) {
// No matching hardware acceleration support.
// This is not an error.
chosenHwConfig = nullptr;
newRenderer = new SdlRenderer();
if (vds != StreamingPreferences::VDS_FORCE_HARDWARE &&
newRenderer->initialize(window, videoFormat, width, height)) {
return true;
}
else {
delete newRenderer;
newRenderer = nullptr;
return false;
}
}
// This is gross but it allows us to use sizeof()
#include "ffmpeg_videosamples.cpp"
if (!(config->methods & AV_CODEC_HW_CONFIG_METHOD_HW_DEVICE_CTX)) {
continue;
}
// Look for acceleration types we support
switch (config->device_type) {
#ifdef _WIN32
case AV_HWDEVICE_TYPE_DXVA2:
newRenderer = new DXVA2Renderer();
break;
#endif
default:
continue;
}
if (newRenderer->initialize(window, videoFormat, width, height)) {
chosenHwConfig = config;
return true;
}
else {
// Failed to initialize
delete newRenderer;
newRenderer = nullptr;
}
}
}
bool FFmpegVideoDecoder::isHardwareAccelerated()
{
return m_HwDecodeCfg != nullptr;
}
enum AVPixelFormat FFmpegVideoDecoder::ffGetFormat(AVCodecContext* context,
const enum AVPixelFormat* pixFmts)
{
FFmpegVideoDecoder* decoder = (FFmpegVideoDecoder*)context->opaque;
const enum AVPixelFormat *p;
for (p = pixFmts; *p != -1; p++) {
// Only match our hardware decoding codec or SW pixel
// format (if not using hardware decoding). It's crucial
// to override the default get_format() which will try
// to gracefully fall back to software decode and break us.
if (*p == (decoder->m_HwDecodeCfg ?
decoder->m_HwDecodeCfg->pix_fmt :
context->pix_fmt)) {
return *p;
}
}
return AV_PIX_FMT_NONE;
}
FFmpegVideoDecoder::FFmpegVideoDecoder()
: m_VideoDecoderCtx(nullptr),
m_DecodeBuffer(1024 * 1024, 0),
@@ -53,7 +89,6 @@ FFmpegVideoDecoder::FFmpegVideoDecoder()
m_Renderer(nullptr)
{
av_init_packet(&m_Pkt);
SDL_AtomicSet(&m_QueuedFrames, 0);
// Use linear filtering when renderer scaling is required
SDL_SetHint(SDL_HINT_RENDER_SCALE_QUALITY, "1");
@@ -61,46 +96,32 @@ FFmpegVideoDecoder::FFmpegVideoDecoder()
FFmpegVideoDecoder::~FFmpegVideoDecoder()
{
reset();
}
avcodec_close(m_VideoDecoderCtx);
av_free(m_VideoDecoderCtx);
m_VideoDecoderCtx = nullptr;
IFFmpegRenderer* FFmpegVideoDecoder::getRenderer()
{
return m_Renderer;
}
void FFmpegVideoDecoder::reset()
{
// Drop any frames still queued to ensure
// they are properly freed.
SDL_Event event;
while (SDL_AtomicGet(&m_QueuedFrames) > 0) {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Waiting for %d frames to return",
SDL_AtomicGet(&m_QueuedFrames));
if (SDL_PeepEvents(&event,
1,
SDL_GETEVENT,
SDL_USEREVENT,
SDL_USEREVENT) == 1) {
dropFrame(&event.user);
}
else {
SDL_Delay(100);
SDL_PumpEvents();
}
}
avcodec_free_context(&m_VideoDecoderCtx);
m_HwDecodeCfg = nullptr;
delete m_Renderer;
m_Renderer = nullptr;
}
bool FFmpegVideoDecoder::completeInitialization(AVCodec* decoder, int videoFormat, int width, int height, bool testOnly)
bool FFmpegVideoDecoder::initialize(
StreamingPreferences::VideoDecoderSelection vds,
SDL_Window* window,
int videoFormat,
int width,
int height,
int)
{
AVCodec* decoder;
if (!chooseDecoder(vds, window, videoFormat, width, height,
decoder, m_HwDecodeCfg, m_Renderer)) {
// Error logged in chooseDecoder()
return false;
}
m_VideoDecoderCtx = avcodec_alloc_context3(decoder);
if (!m_VideoDecoderCtx) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
@@ -125,20 +146,12 @@ bool FFmpegVideoDecoder::completeInitialization(AVCodec* decoder, int videoForma
m_VideoDecoderCtx->width = width;
m_VideoDecoderCtx->height = height;
m_VideoDecoderCtx->pix_fmt = AV_PIX_FMT_YUV420P; // FIXME: HDR
m_VideoDecoderCtx->get_format = ffGetFormat;
// Allow the renderer to attach data to this decoder
if (!m_Renderer->prepareDecoderContext(m_VideoDecoderCtx)) {
return false;
}
// Nobody must override our ffGetFormat
SDL_assert(m_VideoDecoderCtx->get_format == ffGetFormat);
// Stash a pointer to this object in the context
SDL_assert(m_VideoDecoderCtx->opaque == nullptr);
m_VideoDecoderCtx->opaque = this;
int err = avcodec_open2(m_VideoDecoderCtx, decoder, nullptr);
if (err < 0) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
@@ -147,148 +160,9 @@ bool FFmpegVideoDecoder::completeInitialization(AVCodec* decoder, int videoForma
return false;
}
// FFMpeg doesn't completely initialize the codec until the codec
// config data comes in. This would be too late for us to change
// our minds on the selected video codec, so we'll do a trial run
// now to see if things will actually work when the video stream
// comes in.
if (testOnly) {
if (videoFormat & VIDEO_FORMAT_MASK_H264) {
m_Pkt.data = (uint8_t*)k_H264TestFrame;
m_Pkt.size = sizeof(k_H264TestFrame);
}
else {
m_Pkt.data = (uint8_t*)k_HEVCTestFrame;
m_Pkt.size = sizeof(k_HEVCTestFrame);
}
err = avcodec_send_packet(m_VideoDecoderCtx, &m_Pkt);
if (err < 0) {
char errorstring[512];
av_strerror(err, errorstring, sizeof(errorstring));
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"Test decode failed: %s", errorstring);
return false;
}
}
#ifdef QT_DEBUG
// Restore default log level before streaming
av_log_set_level(AV_LOG_INFO);
#endif
return true;
}
IFFmpegRenderer* FFmpegVideoDecoder::createAcceleratedRenderer(const AVCodecHWConfig* hwDecodeCfg)
{
if (!(hwDecodeCfg->methods & AV_CODEC_HW_CONFIG_METHOD_HW_DEVICE_CTX)) {
return nullptr;
}
// Look for acceleration types we support
switch (hwDecodeCfg->device_type) {
#ifdef Q_OS_WIN32
case AV_HWDEVICE_TYPE_DXVA2:
return new DXVA2Renderer();
#endif
#ifdef Q_OS_DARWIN
case AV_HWDEVICE_TYPE_VIDEOTOOLBOX:
return VTRendererFactory::createRenderer();
#endif
#ifdef HAVE_LIBVA
case AV_HWDEVICE_TYPE_VAAPI:
return new VAAPIRenderer();
#endif
#ifdef HAVE_LIBVDPAU
case AV_HWDEVICE_TYPE_VDPAU:
return new VDPAURenderer();
#endif
default:
return nullptr;
}
}
bool FFmpegVideoDecoder::initialize(
StreamingPreferences::VideoDecoderSelection vds,
SDL_Window* window,
int videoFormat,
int width,
int height,
int)
{
AVCodec* decoder;
#ifdef QT_DEBUG
// Increase log level during initialization
av_log_set_level(AV_LOG_DEBUG);
#endif
if (videoFormat & VIDEO_FORMAT_MASK_H264) {
decoder = avcodec_find_decoder(AV_CODEC_ID_H264);
}
else if (videoFormat & VIDEO_FORMAT_MASK_H265) {
decoder = avcodec_find_decoder(AV_CODEC_ID_HEVC);
}
else {
Q_ASSERT(false);
decoder = nullptr;
}
if (!decoder) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Unable to find decoder for format: %x",
videoFormat);
return false;
}
for (int i = 0;; i++) {
const AVCodecHWConfig *config = avcodec_get_hw_config(decoder, i);
if (!config || vds == StreamingPreferences::VDS_FORCE_SOFTWARE) {
// No matching hardware acceleration support.
// This is not an error.
m_HwDecodeCfg = nullptr;
m_Renderer = new SdlRenderer();
if (vds != StreamingPreferences::VDS_FORCE_HARDWARE &&
m_Renderer->initialize(window, videoFormat, width, height) &&
completeInitialization(decoder, videoFormat, width, height, false)) {
return true;
}
else {
reset();
return false;
}
}
m_Renderer = createAcceleratedRenderer(config);
if (!m_Renderer) {
continue;
}
m_HwDecodeCfg = config;
// Submit test frame to ensure this codec really works
if (m_Renderer->initialize(window, videoFormat, width, height) &&
completeInitialization(decoder, videoFormat, width, height, true)) {
// OK, it worked, so now let's initialize it for real
reset();
if ((m_Renderer = createAcceleratedRenderer(config)) != nullptr &&
m_Renderer->initialize(window, videoFormat, width, height) &&
completeInitialization(decoder, videoFormat, width, height, false)) {
return true;
}
else {
SDL_LogCritical(SDL_LOG_CATEGORY_APPLICATION,
"Decoder failed to initialize after successful test");
reset();
}
}
else {
// Failed to initialize or test frame failed, so keep looking
reset();
}
}
}
int FFmpegVideoDecoder::submitDecodeUnit(PDECODE_UNIT du)
{
PLENTRY entry = du->bufferList;
@@ -314,6 +188,8 @@ int FFmpegVideoDecoder::submitDecodeUnit(PDECODE_UNIT du)
err = avcodec_send_packet(m_VideoDecoderCtx, &m_Pkt);
if (err < 0) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"Decoding failed: %d", err);
char errorstring[512];
av_strerror(err, errorstring, sizeof(errorstring));
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
@@ -332,9 +208,14 @@ int FFmpegVideoDecoder::submitDecodeUnit(PDECODE_UNIT du)
err = avcodec_receive_frame(m_VideoDecoderCtx, frame);
if (err == 0) {
// Queue the frame for rendering from the main thread
SDL_AtomicIncRef(&m_QueuedFrames);
queueFrame(frame);
SDL_Event event;
event.type = SDL_USEREVENT;
event.user.code = SDL_CODE_FRAME_READY;
event.user.data1 = frame;
// The main thread will handle freeing this
SDL_PushEvent(&event);
}
else {
av_frame_free(&frame);
@@ -348,7 +229,7 @@ void FFmpegVideoDecoder::renderFrame(SDL_UserEvent* event)
{
AVFrame* frame = reinterpret_cast<AVFrame*>(event->data1);
m_Renderer->renderFrame(frame);
SDL_AtomicDecRef(&m_QueuedFrames);
av_frame_free(&frame);
}
// Called on main thread
@@ -356,5 +237,4 @@ void FFmpegVideoDecoder::dropFrame(SDL_UserEvent* event)
{
AVFrame* frame = reinterpret_cast<AVFrame*>(event->data1);
av_frame_free(&frame);
SDL_AtomicDecRef(&m_QueuedFrames);
}
+9 -16
View File
@@ -22,27 +22,20 @@ public:
virtual void renderFrame(SDL_UserEvent* event) override;
virtual void dropFrame(SDL_UserEvent* event) override;
virtual IFFmpegRenderer* getRenderer();
private:
bool completeInitialization(AVCodec* decoder, int videoFormat,
int width, int height, bool testOnly);
IFFmpegRenderer* createAcceleratedRenderer(const AVCodecHWConfig* hwDecodeCfg);
void reset();
static
enum AVPixelFormat ffGetFormat(AVCodecContext* context,
const enum AVPixelFormat* pixFmts);
bool
chooseDecoder(
StreamingPreferences::VideoDecoderSelection vds,
SDL_Window* window,
int videoFormat,
int width, int height,
AVCodec*& chosenDecoder,
const AVCodecHWConfig*& chosenHwConfig,
IFFmpegRenderer*& newRenderer);
AVPacket m_Pkt;
AVCodecContext* m_VideoDecoderCtx;
QByteArray m_DecodeBuffer;
const AVCodecHWConfig* m_HwDecodeCfg;
IFFmpegRenderer* m_Renderer;
SDL_atomic_t m_QueuedFrames;
static const uint8_t k_H264TestFrame[];
static const uint8_t k_HEVCTestFrame[];
};
@@ -1,19 +0,0 @@
#include "ffmpeg.h"
// 720p 60 FPS H.264 with 1 reference frame
const uint8_t FFmpegVideoDecoder::k_H264TestFrame[] = {
0x00, 0x00, 0x00, 0x01, 0x67, 0x64, 0x00, 0x20, 0xac, 0x2b, 0x40, 0x28, 0x02, 0xdd, 0x80, 0xb5, 0x06, 0x06, 0x06, 0xa5, 0x00, 0x00, 0x03, 0x03, 0xe8, 0x00, 0x01, 0xd4, 0xc0, 0x8f, 0x4a, 0xa0,
0x00, 0x00, 0x00, 0x01, 0x68, 0xee, 0x3c, 0xb0,
0x00, 0x00, 0x00, 0x01, 0x65, 0xb8, 0x02, 0x01, 0x67, 0x25, 0x1b, 0xf4, 0xfa, 0x7d, 0x40, 0x1a, 0x78, 0xe5, 0x10, 0x52, 0xc2, 0xee, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0xc6, 0xef, 0xbb, 0x81, 0x85, 0x2d, 0x47, 0xda, 0xca, 0x4c, 0x00, 0x00, 0x03, 0x00, 0x02, 0x7b, 0xcf, 0x80, 0x00, 0x45, 0x40, 0x01, 0x8d, 0xa6, 0x00, 0x01, 0x64, 0x00, 0x0e, 0x03, 0xc8, 0x00, 0x0e, 0x10, 0x00, 0xbd, 0xc5, 0x00, 0x01, 0x11, 0x00, 0x0e, 0xa3, 0x80, 0x00, 0x38, 0xa0,
0x00, 0x00, 0x01, 0x65, 0x00, 0x6e, 0x2e, 0x00, 0x83, 0x7f, 0xb4, 0x8e, 0x79, 0xa5, 0xff, 0x84, 0x3f, 0x7f, 0x34, 0x3f, 0x97, 0x1b, 0xaf, 0x31, 0x5f, 0x6e, 0xaa, 0xb6, 0xac, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x78, 0x36, 0x9d, 0xa4, 0x65, 0xf9, 0x1e, 0x5b, 0x3a, 0xb0, 0x40, 0x00, 0x00, 0x03, 0x00, 0x00, 0x41, 0x80, 0x00, 0xc5, 0xc8, 0x00, 0x00, 0xfa, 0x00, 0x03, 0x24, 0x00, 0x0e, 0x20, 0x00, 0x3f, 0x80, 0x01, 0x32, 0x00, 0x08, 0x68, 0x00, 0x3e, 0xc0, 0x03, 0x8a,
0x00, 0x00, 0x01, 0x65, 0x00, 0x37, 0x0b, 0x80, 0x21, 0x7f, 0xe3, 0x85, 0x1c, 0xd9, 0xff, 0xe1, 0x1b, 0x01, 0xfa, 0xc0, 0x3e, 0x11, 0x7e, 0xfe, 0x45, 0x5c, 0x43, 0x69, 0x31, 0x4b, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x02, 0x24, 0xae, 0x1a, 0xbb, 0xae, 0x75, 0x9e, 0x35, 0xae, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x03, 0x64, 0x00, 0x09, 0x98, 0x00, 0x1e, 0xc0, 0x00, 0x64, 0x80, 0x01, 0xc4, 0x00, 0x07, 0xf0, 0x00, 0x42, 0xf0, 0x00, 0x00, 0xe1, 0x98, 0x00, 0x05, 0x4b, 0x28, 0x00, 0x06, 0x04,
0x00, 0x00, 0x01, 0x65, 0x00, 0x14, 0xa2, 0xe0, 0x08, 0x5f, 0xe3, 0x85, 0x1c, 0xd9, 0xff, 0xe1, 0x1b, 0x01, 0xfa, 0xc0, 0x3e, 0x11, 0x7e, 0xfe, 0x45, 0x5c, 0x43, 0x69, 0x31, 0x4b, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x02, 0x24, 0xb9, 0x7d, 0xb4, 0x70, 0x4d, 0x15, 0xc5, 0x0a, 0x4e, 0x60, 0x00, 0x00, 0x03, 0x00, 0x00, 0x26, 0xa8, 0xb0, 0x00, 0x13, 0xcd, 0xcc, 0x00, 0x07, 0x48, 0x88, 0x00, 0x06, 0x29, 0x69, 0x00, 0x01, 0x16, 0xac, 0x80, 0x00, 0x9e, 0x4e, 0x80, 0x00, 0x3f, 0x84, 0x20, 0x00, 0x6f, 0x41, 0xa0, 0x00, 0x20, 0x00, 0x02, 0x16, 0xb8, 0x00, 0x08, 0x08
};
// 720p 60 FPS HEVC with 1 reference frame
const uint8_t FFmpegVideoDecoder::k_HEVCTestFrame[] = {
0x00, 0x00, 0x00, 0x01, 0x40, 0x01, 0x0c, 0x01, 0xff, 0xff, 0x01, 0x40, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x78, 0xac, 0x09,
0x00, 0x00, 0x00, 0x01, 0x42, 0x01, 0x01, 0x01, 0x40, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x78, 0xa0, 0x02, 0x80, 0x80, 0x2e, 0x1f, 0x13, 0x96, 0xb4, 0xa4, 0x25, 0x92, 0xe3, 0x01, 0x6a, 0x0c, 0x0c, 0x0d, 0x48, 0x20, 0x00, 0x00, 0x03, 0x00, 0x20, 0x00, 0x00, 0x07, 0x85, 0xf1, 0xa2, 0xd0,
0x00, 0x00, 0x00, 0x01, 0x44, 0x01, 0xc0, 0xf7, 0xc0, 0xcc, 0x90,
0x00, 0x00, 0x00, 0x01, 0x2a, 0x01, 0xac, 0x0c, 0xf0, 0xe6, 0xf6, 0x5e, 0xff, 0xd9, 0x97, 0xeb, 0x44, 0x7d, 0x1f, 0xa4, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x03, 0x52, 0x7c, 0x4e, 0x5b, 0xfe, 0xbe, 0xc2, 0x87, 0x20, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x2b, 0x60,
};
-106
View File
@@ -1,106 +0,0 @@
#include "sl.h"
SLVideoDecoder::SLVideoDecoder()
: m_VideoContext(nullptr),
m_VideoStream(nullptr)
{
}
SLVideoDecoder::~SLVideoDecoder()
{
if (m_VideoStream != nullptr) {
SLVideo_FreeStream(m_VideoStream);
}
if (m_VideoContext != nullptr) {
SLVideo_FreeContext(m_VideoContext);
}
}
bool
SLVideoDecoder::isHardwareAccelerated()
{
// SLVideo is always hardware accelerated
return true;
}
bool
SLVideoDecoder::initialize(StreamingPreferences::VideoDecoderSelection vds,
SDL_Window*,
int videoFormat, int, int, int frameRate)
{
// SLVideo only supports hardware decoding
if (vds == StreamingPreferences::VDS_FORCE_SOFTWARE) {
return false;
}
// SLVideo only supports H.264
if (videoFormat != VIDEO_FORMAT_H264) {
return false;
}
m_VideoContext = SLVideo_CreateContext();
if (!m_VideoContext) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"SLVideo_CreateContext() failed");
return false;
}
// Create a low latency H.264 stream
m_VideoStream = SLVideo_CreateStream(m_VideoContext, k_ESLVideoFormatH264, 1);
if (!m_VideoStream) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"SLVideo_CreateStream() failed");
return false;
}
SLVideo_SetStreamTargetFramerate(m_VideoStream, frameRate, 1);
return true;
}
int
SLVideoDecoder::submitDecodeUnit(PDECODE_UNIT du)
{
int err;
err = SLVideo_BeginFrame(m_VideoStream, du->fullLength);
if (err < 0) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"SLVideo_BeginFrame() failed: %d",
err);
// Need an IDR frame to resync
return DR_NEED_IDR;
}
PLENTRY entry = du->bufferList;
while (entry != nullptr) {
err = SLVideo_WriteFrameData(m_VideoStream,
entry->data,
entry->length);
if (err < 0) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"SLVideo_WriteFrameData() failed: %d",
err);
// Need an IDR frame to resync
return DR_NEED_IDR;
}
entry = entry->next;
}
err = SLVideo_SubmitFrame(m_VideoStream);
if (err < 0) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"SLVideo_SubmitFrame() failed: %d",
err);
// Need an IDR frame to resync
return DR_NEED_IDR;
}
return DR_OK;
}
-28
View File
@@ -1,28 +0,0 @@
#pragma once
#include "decoder.h"
#include <SLVideo.h>
class SLVideoDecoder : public IVideoDecoder
{
public:
SLVideoDecoder();
virtual ~SLVideoDecoder();
virtual bool initialize(StreamingPreferences::VideoDecoderSelection vds,
SDL_Window* window,
int videoFormat,
int width,
int height,
int frameRate);
virtual bool isHardwareAccelerated();
virtual int submitDecodeUnit(PDECODE_UNIT du);
// Unused since rendering is done directly from the decode thread
virtual void renderFrame(SDL_UserEvent*) {}
virtual void dropFrame(SDL_UserEvent*) {}
private:
CSLVideoContext* m_VideoContext;
CSLVideoStream* m_VideoStream;
};
+1 -1
View File
@@ -1,5 +1,5 @@
#pragma once
#define THROW_BAD_ALLOC_IF_NULL(x) \
if ((x) == nullptr) throw std::bad_alloc()
if ((x) == nullptr) throw new std::bad_alloc()
-3
View File
@@ -1,3 +0,0 @@
SOURCES = main.cpp
LIBS += -lSLVideo
-7
View File
@@ -1,7 +0,0 @@
#include <SLVideo.h>
int main()
{
SLVideo_CreateContext();
return 0;
}
@@ -1 +0,0 @@
Versions/Current/Headers
@@ -1 +0,0 @@
Versions/Current/Resources
-1
View File
@@ -1 +0,0 @@
Versions/Current/SDL2
@@ -1,46 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>BuildMachineOSBuild</key>
<string>17C88</string>
<key>CFBundleDevelopmentRegion</key>
<string>English</string>
<key>CFBundleExecutable</key>
<string>SDL2</string>
<key>CFBundleGetInfoString</key>
<string>http://www.libsdl.org</string>
<key>CFBundleIdentifier</key>
<string>org.libsdl.SDL2</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>Simple DirectMedia Layer</string>
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CFBundleShortVersionString</key>
<string>2.0.8</string>
<key>CFBundleSignature</key>
<string>SDLX</string>
<key>CFBundleSupportedPlatforms</key>
<array>
<string>MacOSX</string>
</array>
<key>CFBundleVersion</key>
<string>2.0.8</string>
<key>DTCompiler</key>
<string>com.apple.compilers.llvm.clang.1_0</string>
<key>DTPlatformBuild</key>
<string>9C40b</string>
<key>DTPlatformVersion</key>
<string>GM</string>
<key>DTSDKBuild</key>
<string>17C76</string>
<key>DTSDKName</key>
<string>macosx10.13</string>
<key>DTXcode</key>
<string>0920</string>
<key>DTXcodeBuild</key>
<string>9C40b</string>
</dict>
</plist>
@@ -1,792 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>files</key>
<dict>
<key>Resources/Info.plist</key>
<data>
dts8ape5zBg3+8u8BQ5kWqp9cC8=
</data>
</dict>
<key>files2</key>
<dict>
<key>Headers/SDL.h</key>
<dict>
<key>hash</key>
<data>
wiqwnVgtbgjGPAOFejxz8yw7geU=
</data>
<key>hash2</key>
<data>
LLwzMAHn2OsCr2SVR7M71KeQf+TiJpHk/KKojzQdRlI=
</data>
</dict>
<key>Headers/SDL_assert.h</key>
<dict>
<key>hash</key>
<data>
zWoE6/P7ZWAYr4J+EVgDAK+kot8=
</data>
<key>hash2</key>
<data>
tuS18C7A11akqH9MWT0WWCb6q7kcF97Ui5OFwNtSF/k=
</data>
</dict>
<key>Headers/SDL_atomic.h</key>
<dict>
<key>hash</key>
<data>
/q15zvR2djPgTRkySENYqzisn64=
</data>
<key>hash2</key>
<data>
n9rGA8zXFxasUnN0ytgLLWmW2CDCtKBeGJZcfB+nfRc=
</data>
</dict>
<key>Headers/SDL_audio.h</key>
<dict>
<key>hash</key>
<data>
9uJv81fH9Op/f3sF7/GVuNYQNOc=
</data>
<key>hash2</key>
<data>
KDokTSV3kZuHCQFFrRum3VoTP1QSW2xAghjceoIArXY=
</data>
</dict>
<key>Headers/SDL_bits.h</key>
<dict>
<key>hash</key>
<data>
yp8Jj93ZE224v1+u++cOexlp9RY=
</data>
<key>hash2</key>
<data>
kQTBxFYnOjKSlti4H7pAYZWdkH0BpQ9Eq54E/r/nlVY=
</data>
</dict>
<key>Headers/SDL_blendmode.h</key>
<dict>
<key>hash</key>
<data>
kgTnzOrp0kqeI8lzDgQyT/gCGiM=
</data>
<key>hash2</key>
<data>
d/8B0iLh4aFeoaPEFqQ4C6x4ISlDJCKrybaKuHCJHL4=
</data>
</dict>
<key>Headers/SDL_clipboard.h</key>
<dict>
<key>hash</key>
<data>
qq1r82AS7fHG8G94+jvL41iKEyA=
</data>
<key>hash2</key>
<data>
+r0LOXHGRf2Q9+FkqRU6i++1zEzsE5sWXxxMGrsSyKk=
</data>
</dict>
<key>Headers/SDL_config.h</key>
<dict>
<key>hash</key>
<data>
nJnfliCMDJUfOG8t4QYt5U9tFBY=
</data>
<key>hash2</key>
<data>
qkVvxnI+XRMTIdbzQ9tV+ERRc75r/EzWu0qRTgt5K5c=
</data>
</dict>
<key>Headers/SDL_config_macosx.h</key>
<dict>
<key>hash</key>
<data>
D0LXp0FibaaLSE6877hbyqf2Jb8=
</data>
<key>hash2</key>
<data>
pCQLXwJ6HaGZiG5vr9njJtgNcIV9p0mxcSJtYoj147A=
</data>
</dict>
<key>Headers/SDL_copying.h</key>
<dict>
<key>hash</key>
<data>
0DKOkPvRlLo00X7iE2fL4i8XBww=
</data>
<key>hash2</key>
<data>
nHJ8cj/FeSfLoRm5gRiDk73eyYlKn7u1BfbqkB2ofLg=
</data>
</dict>
<key>Headers/SDL_cpuinfo.h</key>
<dict>
<key>hash</key>
<data>
2wleDfJFmCBrEssrE9rcuaiqOLQ=
</data>
<key>hash2</key>
<data>
MEpF0mFQFLhuTV+5uSB++q8tT2z5heESxyP1HuzuKxc=
</data>
</dict>
<key>Headers/SDL_endian.h</key>
<dict>
<key>hash</key>
<data>
WmJrxu+b1MkjtE6ttCymwH4EVC0=
</data>
<key>hash2</key>
<data>
77hv0QIqlra/8gcQF81aAwP21/ZAANKiIaJRV1QK8mk=
</data>
</dict>
<key>Headers/SDL_error.h</key>
<dict>
<key>hash</key>
<data>
HMprigJ3u4PqQ7B+gD1UUbTSrJ8=
</data>
<key>hash2</key>
<data>
errosoVYCD45UvQVzL4Lmfq4eCQg92a6xUZ062DGEL4=
</data>
</dict>
<key>Headers/SDL_events.h</key>
<dict>
<key>hash</key>
<data>
ptgRA0IBsCg5za/lwhTOPGNmekY=
</data>
<key>hash2</key>
<data>
lwISPqzee6nV4HCHaMzZCVJJAtFZmoi8cYlJkY41wKA=
</data>
</dict>
<key>Headers/SDL_filesystem.h</key>
<dict>
<key>hash</key>
<data>
9rIKoCtsAONdgfT/4QKm3a56hV0=
</data>
<key>hash2</key>
<data>
l5nBvMglAIFEWf357KMyQ+E7cr4GqO9XNwevA04+VmQ=
</data>
</dict>
<key>Headers/SDL_gamecontroller.h</key>
<dict>
<key>hash</key>
<data>
sUaSBwOprX7A696W6u2oOtIne+Y=
</data>
<key>hash2</key>
<data>
VZDstHk/V+tZik3q3xnDCAG3AfA+BoHk9lB406mKtxU=
</data>
</dict>
<key>Headers/SDL_gesture.h</key>
<dict>
<key>hash</key>
<data>
d80fR6I92r+G26xckkQzTRj/tj4=
</data>
<key>hash2</key>
<data>
/5+tOQWQ9raopqBCExlH9ClAA/fVAfEBIrd/WlpaqIE=
</data>
</dict>
<key>Headers/SDL_haptic.h</key>
<dict>
<key>hash</key>
<data>
95rNclZzbsuV6pyeG8cbIETgS84=
</data>
<key>hash2</key>
<data>
O4RUmC9HMqsjAf22Ru6z5j9HaYTsc7SxOnzOXPR3kz0=
</data>
</dict>
<key>Headers/SDL_hints.h</key>
<dict>
<key>hash</key>
<data>
UjEgXfZiLUmQTh9G+Bkx8Y1Ov6k=
</data>
<key>hash2</key>
<data>
DqFPmBXNg9BovdIAlipnBKZo5mcQJCrLmhyIM/O+tvU=
</data>
</dict>
<key>Headers/SDL_joystick.h</key>
<dict>
<key>hash</key>
<data>
97SJSCx+xjRHUQ6ilndhP8TZ110=
</data>
<key>hash2</key>
<data>
xYWL+BS5f9NGr7eeP+vL0kD5K6O99PXgOWLT2Bx2dRQ=
</data>
</dict>
<key>Headers/SDL_keyboard.h</key>
<dict>
<key>hash</key>
<data>
RsaE9DMLq9lFsYokvdaQ8nhIkfY=
</data>
<key>hash2</key>
<data>
tpZGEHU3TKCozcYf4MA9gA5bXNPSxx9rqqBaPttgr+4=
</data>
</dict>
<key>Headers/SDL_keycode.h</key>
<dict>
<key>hash</key>
<data>
9nIqgM67nCJUdjV3iiiviT8DFhk=
</data>
<key>hash2</key>
<data>
SpMclG+epmPQibi3fNsQe1lBUxZPJOOu4DfQjpTZYgw=
</data>
</dict>
<key>Headers/SDL_loadso.h</key>
<dict>
<key>hash</key>
<data>
m+OEcfHbykTuEo6P3/y/kuV1xjk=
</data>
<key>hash2</key>
<data>
Yg3OrZpF17uC69TwprPyrpn3+su3YAaaA1hTNXAHxLw=
</data>
</dict>
<key>Headers/SDL_log.h</key>
<dict>
<key>hash</key>
<data>
HcUUwymYqVSfgz2LdtuUqZSwjUY=
</data>
<key>hash2</key>
<data>
j4F34jvgNTEcHQQoPbBBugi4RKQLhGM+x4ndXdtDc0E=
</data>
</dict>
<key>Headers/SDL_main.h</key>
<dict>
<key>hash</key>
<data>
7I0GPDiLKlavuXuoepYsl2mS6CI=
</data>
<key>hash2</key>
<data>
qwXROmpXppMPodetUGE0s/7HHmwfu3taLUeiiu+APk0=
</data>
</dict>
<key>Headers/SDL_messagebox.h</key>
<dict>
<key>hash</key>
<data>
MEB/oCw7dyinbrBmQQqh46/uRVk=
</data>
<key>hash2</key>
<data>
DSuDS/nAzfEi2kO06DEpxRLj5V1++p2DYYA9Oou8ewQ=
</data>
</dict>
<key>Headers/SDL_mouse.h</key>
<dict>
<key>hash</key>
<data>
I2UksqoB5v6RX64Ddwea9dW0sO0=
</data>
<key>hash2</key>
<data>
gFMH6bBzvJCff8F7FMWlP6o6TydFmiXEu0daWp166wk=
</data>
</dict>
<key>Headers/SDL_mutex.h</key>
<dict>
<key>hash</key>
<data>
H6Q1bWPDKu2RsALoLzTBzPTrPkk=
</data>
<key>hash2</key>
<data>
WKU8g7bVD3KpQEgvXuUGrPcG5hgg+LwQBgzLQCeSVTg=
</data>
</dict>
<key>Headers/SDL_name.h</key>
<dict>
<key>hash</key>
<data>
doLASBj4oiSiP6MW9wQblnyPN1E=
</data>
<key>hash2</key>
<data>
M0b8pslMMsWZp1KDVUdqX7ml3DKUVcwiTcfwGbhopvA=
</data>
</dict>
<key>Headers/SDL_opengl.h</key>
<dict>
<key>hash</key>
<data>
ZN9vVzyxx542ZCZ4nmASJKJ7qFM=
</data>
<key>hash2</key>
<data>
dazK6AC8BOZXW9m9761qsuoqe3eJozPIbSc1YEY3UVE=
</data>
</dict>
<key>Headers/SDL_opengl_glext.h</key>
<dict>
<key>hash</key>
<data>
nqPX1ObCVYyVzW3VbD3C/1vaTRE=
</data>
<key>hash2</key>
<data>
IULBL/m+rBc3UQPqu68w6FDM/Mda58oybFnlyn3OkjE=
</data>
</dict>
<key>Headers/SDL_opengles.h</key>
<dict>
<key>hash</key>
<data>
aVEa5N2CHg93ZxdDuKfDtFnCbU8=
</data>
<key>hash2</key>
<data>
3CmcA5B4ydtkVin3NvNmYVHfgnhJJAVYNVw1YKM0Mko=
</data>
</dict>
<key>Headers/SDL_opengles2.h</key>
<dict>
<key>hash</key>
<data>
yQ5nSugeN94hMSNhsIOmlAstJhY=
</data>
<key>hash2</key>
<data>
8H2lt37zXZs68UfBlqDGqSJTQ2+Slu7qywggJ2mIosI=
</data>
</dict>
<key>Headers/SDL_opengles2_gl2.h</key>
<dict>
<key>hash</key>
<data>
g4y04zPFy1H/qujSNGRd0vB7ClQ=
</data>
<key>hash2</key>
<data>
9GAwK0cg8YPrZHWVqMGpyVwKFz3Ay9VHiyz70jzWltA=
</data>
</dict>
<key>Headers/SDL_opengles2_gl2ext.h</key>
<dict>
<key>hash</key>
<data>
bTlnL+42kbi+n/gH//X2p0pqeuM=
</data>
<key>hash2</key>
<data>
5404JOHXH4pGO60gR/uLhz9zW6fKy9jvdMw5i1WTcpo=
</data>
</dict>
<key>Headers/SDL_opengles2_gl2platform.h</key>
<dict>
<key>hash</key>
<data>
ByFMXJFtjcRglS/e0+DujyjC3dM=
</data>
<key>hash2</key>
<data>
wlKRTsmTMrnniYqSvQklJfb9VF07J0MDpqzYOcfiDDY=
</data>
</dict>
<key>Headers/SDL_opengles2_khrplatform.h</key>
<dict>
<key>hash</key>
<data>
NLpJq9uBqjOpWhXISOQHPnTp/XQ=
</data>
<key>hash2</key>
<data>
djHeDyzmR0e/2JNIbqpBqgi56MXlxlAbgcBFxuvwSyo=
</data>
</dict>
<key>Headers/SDL_pixels.h</key>
<dict>
<key>hash</key>
<data>
q2Uas2PGwUNo30Wiqomvrb03mp4=
</data>
<key>hash2</key>
<data>
fpczDM4z7VLJW6FMvRTygnypORPdk2q89S1kCbS2fpM=
</data>
</dict>
<key>Headers/SDL_platform.h</key>
<dict>
<key>hash</key>
<data>
aM0ptLXVxYx4yXLsNYOssy/jx9Y=
</data>
<key>hash2</key>
<data>
R6MDdJ2ZUd9FGKReGdJE6YyHwqczb99jK9/IDW59AmI=
</data>
</dict>
<key>Headers/SDL_power.h</key>
<dict>
<key>hash</key>
<data>
NqkQCWzmodWGdUFUZmzR9B/TApc=
</data>
<key>hash2</key>
<data>
VkJkbp+5Wd4WYDemJiu8R/emdH4PZSNPweoPmzRkfxM=
</data>
</dict>
<key>Headers/SDL_quit.h</key>
<dict>
<key>hash</key>
<data>
eOEJzybg3UBfe83rKNfky9XAVGM=
</data>
<key>hash2</key>
<data>
mN/T7PjHVtVce+/m7tIWIH8p8lylLWPuJAkCsbEvjhg=
</data>
</dict>
<key>Headers/SDL_rect.h</key>
<dict>
<key>hash</key>
<data>
B7oWq3VOxrWbVn0YV8xpvXG6dic=
</data>
<key>hash2</key>
<data>
dCsZcEfdmcr1RuOycWAtHnnQs5BWk8lw0Ubnv/aUwG4=
</data>
</dict>
<key>Headers/SDL_render.h</key>
<dict>
<key>hash</key>
<data>
ATenWUmKg6AEbv2SbCy9JfT8jCw=
</data>
<key>hash2</key>
<data>
EYiKPX5wRIcWkCyROlT59wuiDupZ6yp0+YsDWOrhtnQ=
</data>
</dict>
<key>Headers/SDL_revision.h</key>
<dict>
<key>hash</key>
<data>
XBn2NI8Qm53MeZGaXVB3fQrs0uc=
</data>
<key>hash2</key>
<data>
FHG6Lqi1wMmjow+JjxKYy/LAoqP+iDH8QYuJS1zr6T4=
</data>
</dict>
<key>Headers/SDL_rwops.h</key>
<dict>
<key>hash</key>
<data>
ULSy/1QoNQhBu3kk75qXo30Wcvs=
</data>
<key>hash2</key>
<data>
KewlZ14FJ6CfLjd7EDc1KlVSiUhIt2P9d3p2VEJuUUA=
</data>
</dict>
<key>Headers/SDL_scancode.h</key>
<dict>
<key>hash</key>
<data>
FAcbPIsuPOYbsldZL4m0AFUeoW4=
</data>
<key>hash2</key>
<data>
5VaZC9IfMuNgz3h6OJn9Q/IGB/+cpymxIrjkluCWzOo=
</data>
</dict>
<key>Headers/SDL_shape.h</key>
<dict>
<key>hash</key>
<data>
S5Ty+gKktVyifkoYT0K7E48JqdI=
</data>
<key>hash2</key>
<data>
LVwSnfZBuKL1bW2zy4rt0vRPmH2/ZxfmP0xB9pLVEOE=
</data>
</dict>
<key>Headers/SDL_stdinc.h</key>
<dict>
<key>hash</key>
<data>
qBTB0Yqh+34vD3n3dN9ry9GwqJM=
</data>
<key>hash2</key>
<data>
TDMriI0PPYF73lvh6suExzpMldCmX1SkLgg+wkBqt7A=
</data>
</dict>
<key>Headers/SDL_surface.h</key>
<dict>
<key>hash</key>
<data>
bssTKTIF4Yw/3a6fyF7GhaVrdU0=
</data>
<key>hash2</key>
<data>
zRF/b8bCqjClZnbPorqjhlWw9aujM9RwnVemHyiC2yM=
</data>
</dict>
<key>Headers/SDL_system.h</key>
<dict>
<key>hash</key>
<data>
n2WodCV+WpbfpFSKUUKpB/i5ZPw=
</data>
<key>hash2</key>
<data>
3W0Z/Fdwbc7dTyR+v4g6eVE58XcEYLTzDGO+RMwcmJc=
</data>
</dict>
<key>Headers/SDL_syswm.h</key>
<dict>
<key>hash</key>
<data>
QoebSqgb1XslKPBwMiUOM6daVKo=
</data>
<key>hash2</key>
<data>
rCqmdvdibhT5MBOnvKCT0gK7Howb5uLewC321yLYr5U=
</data>
</dict>
<key>Headers/SDL_thread.h</key>
<dict>
<key>hash</key>
<data>
fXCWQD4MIniqvdB6pWVwlxNS9jk=
</data>
<key>hash2</key>
<data>
18iJ1yFzOTeiM30JwK3GMlbgUqzNDIQiw3ZLLr3M+XQ=
</data>
</dict>
<key>Headers/SDL_timer.h</key>
<dict>
<key>hash</key>
<data>
qfzyWhIh36SK6BAcdd50D/MfdGo=
</data>
<key>hash2</key>
<data>
5Qe92nVkUnHbxgbGwxkyglvVKajhfgVhxPei6dhr4c0=
</data>
</dict>
<key>Headers/SDL_touch.h</key>
<dict>
<key>hash</key>
<data>
yyJt6piMAcoFWhFqsHk9mo3MtaI=
</data>
<key>hash2</key>
<data>
9mQ2V937DAO6OjcQFMboFrnJEiqdk+2uHBzdvMYUnd0=
</data>
</dict>
<key>Headers/SDL_types.h</key>
<dict>
<key>hash</key>
<data>
oVg7CzABdDKtxCWINHaUvBRbRZ0=
</data>
<key>hash2</key>
<data>
8uZym5ly46CZ4VQZCz/Y/cH4THoDS8kGqrvB+Ux/254=
</data>
</dict>
<key>Headers/SDL_version.h</key>
<dict>
<key>hash</key>
<data>
nKLee2vSNZZK9p94Up9o87OoiDE=
</data>
<key>hash2</key>
<data>
F3dpo9XlvebIS2NLJ1ovH+W7fG3bzb01F8OKUeqd+U0=
</data>
</dict>
<key>Headers/SDL_video.h</key>
<dict>
<key>hash</key>
<data>
UFZPU1U3CDog3RBKdd5wu9LFQZk=
</data>
<key>hash2</key>
<data>
6/OCq92BIQnGHBLmlGX4nUNnfLM7LzPoMp+lPlYY4ew=
</data>
</dict>
<key>Headers/SDL_vulkan.h</key>
<dict>
<key>hash</key>
<data>
eLdIezOR2pV1ai5rLBrtiR3NAok=
</data>
<key>hash2</key>
<data>
iCB8avj2P0+ZkjVGfTchu5BAd7BJ+LeC1fJ9BU+80IE=
</data>
</dict>
<key>Headers/begin_code.h</key>
<dict>
<key>hash</key>
<data>
6RX1yuMtg6N9G2iCtCjjKzPCkuU=
</data>
<key>hash2</key>
<data>
JQ71zTwAp3EzI22aBP3w80YYEcxNBRF8/0DkqO39oDo=
</data>
</dict>
<key>Headers/close_code.h</key>
<dict>
<key>hash</key>
<data>
IDKa63p9RAIkSpPXA0mU/dzgBWQ=
</data>
<key>hash2</key>
<data>
oF0I0dcQDvLcuEIgqx9wEQiuqHR5KLoq1zhL/AK5geg=
</data>
</dict>
<key>Resources/Info.plist</key>
<dict>
<key>hash</key>
<data>
dts8ape5zBg3+8u8BQ5kWqp9cC8=
</data>
<key>hash2</key>
<data>
ZCgx1szeVwObsPiq6VoZh8Q9ym7l+7WXzv0HTSoFT0U=
</data>
</dict>
</dict>
<key>rules</key>
<dict>
<key>^Resources/</key>
<true/>
<key>^Resources/.*\.lproj/</key>
<dict>
<key>optional</key>
<true/>
<key>weight</key>
<real>1000</real>
</dict>
<key>^Resources/.*\.lproj/locversion.plist$</key>
<dict>
<key>omit</key>
<true/>
<key>weight</key>
<real>1100</real>
</dict>
<key>^Resources/Base\.lproj/</key>
<dict>
<key>weight</key>
<real>1010</real>
</dict>
<key>^version.plist$</key>
<true/>
</dict>
<key>rules2</key>
<dict>
<key>.*\.dSYM($|/)</key>
<dict>
<key>weight</key>
<real>11</real>
</dict>
<key>^(.*/)?\.DS_Store$</key>
<dict>
<key>omit</key>
<true/>
<key>weight</key>
<real>2000</real>
</dict>
<key>^(Frameworks|SharedFrameworks|PlugIns|Plug-ins|XPCServices|Helpers|MacOS|Library/(Automator|Spotlight|LoginItems))/</key>
<dict>
<key>nested</key>
<true/>
<key>weight</key>
<real>10</real>
</dict>
<key>^.*</key>
<true/>
<key>^Info\.plist$</key>
<dict>
<key>omit</key>
<true/>
<key>weight</key>
<real>20</real>
</dict>
<key>^PkgInfo$</key>
<dict>
<key>omit</key>
<true/>
<key>weight</key>
<real>20</real>
</dict>
<key>^Resources/</key>
<dict>
<key>weight</key>
<real>20</real>
</dict>
<key>^Resources/.*\.lproj/</key>
<dict>
<key>optional</key>
<true/>
<key>weight</key>
<real>1000</real>
</dict>
<key>^Resources/.*\.lproj/locversion.plist$</key>
<dict>
<key>omit</key>
<true/>
<key>weight</key>
<real>1100</real>
</dict>
<key>^Resources/Base\.lproj/</key>
<dict>
<key>weight</key>
<real>1010</real>
</dict>
<key>^[^/]+$</key>
<dict>
<key>nested</key>
<true/>
<key>weight</key>
<real>10</real>
</dict>
<key>^embedded\.provisionprofile$</key>
<dict>
<key>weight</key>
<real>20</real>
</dict>
<key>^version\.plist$</key>
<dict>
<key>weight</key>
<real>20</real>
</dict>
</dict>
</dict>
</plist>
@@ -1 +0,0 @@
A

Some files were not shown because too many files have changed in this diff Show More