mirror of
https://github.com/moonlight-stream/moonlight-qt.git
synced 2026-10-25 13:31:41 +00:00
Compare commits
113
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
979ba2dab7 | ||
|
|
56277af5d1 | ||
|
|
608db6a3e5 | ||
|
|
be52272e5c | ||
|
|
32f305347f | ||
|
|
18dd6b09bb | ||
|
|
3ed5f9edf7 | ||
|
|
017362a5d1 | ||
|
|
e250e08242 | ||
|
|
48d5a7fb51 | ||
|
|
141ee11e2f | ||
|
|
4e82ea298e | ||
|
|
22be912788 | ||
|
|
02242b9b34 | ||
|
|
e9c1f883da | ||
|
|
141f6669e8 | ||
|
|
6300d242cc | ||
|
|
186579266c | ||
|
|
13490af90c | ||
|
|
23a9c2cd9e | ||
|
|
88631e4a63 | ||
|
|
9e744ce169 | ||
|
|
77a68db8c1 | ||
|
|
27247f253c | ||
|
|
ada7f92c75 | ||
|
|
87db4e88a5 | ||
|
|
277594139b | ||
|
|
79d5e35306 | ||
|
|
cda80b319e | ||
|
|
ab4b024df4 | ||
|
|
7f9cbac232 | ||
|
|
8470ffcb0d | ||
|
|
92c897762b | ||
|
|
36014cfcfd | ||
|
|
df26f731eb | ||
|
|
e39b5a6a72 | ||
|
|
c7138fe36c | ||
|
|
b5deab772d | ||
|
|
4bfeccec74 | ||
|
|
42ed39a67f | ||
|
|
68612db86d | ||
|
|
3ad5f1478c | ||
|
|
a81b501d7d | ||
|
|
871988b2cb | ||
|
|
59d087adf5 | ||
|
|
d9ed3f9694 | ||
|
|
e652dbcc79 | ||
|
|
b5965d763c | ||
|
|
717c3903d1 | ||
|
|
5854ce2000 | ||
|
|
eb1316ce32 | ||
|
|
c31e7262b2 | ||
|
|
9e6aadffa1 | ||
|
|
0bde932550 | ||
|
|
f5499be215 | ||
|
|
42988e0dbf | ||
|
|
b16aac0c06 | ||
|
|
7c74cdc50b | ||
|
|
26ee8564f8 | ||
|
|
718a575478 | ||
|
|
1f314b839e | ||
|
|
34494e4c89 | ||
|
|
03dc563b32 | ||
|
|
5e4182f0a0 | ||
|
|
010475e8a2 | ||
|
|
bd170c4bd6 | ||
|
|
386e463942 | ||
|
|
9d840afdfb | ||
|
|
83b81aad4b | ||
|
|
4caa7ec749 | ||
|
|
cf3cbd627d | ||
|
|
3bb62a82e1 | ||
|
|
a06a625d48 | ||
|
|
13d686b6f8 | ||
|
|
75c7b2828f | ||
|
|
18c5e19ac9 | ||
|
|
06635d9f9a | ||
|
|
ad57a55d12 | ||
|
|
a97e4babf9 | ||
|
|
1117c49867 | ||
|
|
dc723a8da1 | ||
|
|
27239ff320 | ||
|
|
b073c90f0f | ||
|
|
519626a6d5 | ||
|
|
71c11cacd9 | ||
|
|
b7b452875d | ||
|
|
eb536c2273 | ||
|
|
c816a6198c | ||
|
|
399154f5f5 | ||
|
|
64a839c2f9 | ||
|
|
20288210be | ||
|
|
7b235743cb | ||
|
|
4c507f1179 | ||
|
|
e516861366 | ||
|
|
48c2a8c5b0 | ||
|
|
b41e8c07d2 | ||
|
|
10b5245919 | ||
|
|
86874a2e1c | ||
|
|
f951a01a68 | ||
|
|
d12fed3e7f | ||
|
|
4566139c75 | ||
|
|
38df63b962 | ||
|
|
faaf9dfa07 | ||
|
|
9c2befc386 | ||
|
|
e85ca2a6c9 | ||
|
|
e32aace443 | ||
|
|
bd8e4a4bce | ||
|
|
5ee1358712 | ||
|
|
06501bd4b7 | ||
|
|
a4ad58d9ec | ||
|
|
76d99cd60e | ||
|
|
1b85dcc829 | ||
|
|
c97e298b80 |
No files matched your search
@@ -1 +1,5 @@
|
|||||||
*pro.user
|
*pro.user
|
||||||
|
|
||||||
|
|
||||||
|
**/.vs/
|
||||||
|
build/
|
||||||
@@ -1,9 +1,6 @@
|
|||||||
[submodule "moonlight-common-c/moonlight-common-c"]
|
[submodule "moonlight-common-c/moonlight-common-c"]
|
||||||
path = moonlight-common-c/moonlight-common-c
|
path = moonlight-common-c/moonlight-common-c
|
||||||
url = https://github.com/moonlight-stream/moonlight-common-c.git
|
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"]
|
[submodule "qmdnsengine/qmdnsengine"]
|
||||||
path = qmdnsengine/qmdnsengine
|
path = qmdnsengine/qmdnsengine
|
||||||
url = https://github.com/cgutman/qmdnsengine.git
|
url = https://github.com/cgutman/qmdnsengine.git
|
||||||
+41
@@ -0,0 +1,41 @@
|
|||||||
|
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'
|
||||||
|
packages:
|
||||||
|
- qt59base
|
||||||
|
- qt59quickcontrols2
|
||||||
|
- qt59svg
|
||||||
|
- libesd0-dev
|
||||||
|
- libgl-dev
|
||||||
|
- libopus-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
|
||||||
@@ -1,20 +1,31 @@
|
|||||||
# Moonlight PC
|
# Moonlight PC
|
||||||
|
|
||||||
[Moonlight PC](http://moonlight-stream.com) is an open source implementation of NVIDIA's GameStream, as used by the NVIDIA Shield, but built to run on Windows, Mac, and Linux.
|
[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 also has mobile versions for [Android](https://github.com/moonlight-stream/moonlight-android) and [iOS](https://github.com/moonlight-stream/moonlight-ios).
|
Moonlight also has mobile versions for [Android](https://github.com/moonlight-stream/moonlight-android) and [iOS](https://github.com/moonlight-stream/moonlight-ios).
|
||||||
|
|
||||||
This is a work-in-progress port and is only suitable for development use at this point.
|
This client is currently still in development (pre-alpha), but the streaming performance is already much better than the older Moonlight Chrome client on many machines. Test releases may be available for your OS on the [Releases page](https://github.com/moonlight-stream/moonlight-qt/releases).
|
||||||
|
|
||||||
|
You can follow development on our [Discord server](https://discord.gg/6ERtzFY).
|
||||||
|
|
||||||
|
[](https://ci.appveyor.com/project/cgutman/moonlight-qt/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
|
||||||
|
|
||||||
## Building
|
## Building
|
||||||
1. Install the latest Qt SDK (and optionally, the Qt Creator IDE) from https://www.qt.io/download
|
1. Install the latest Qt SDK (and optionally, the Qt Creator IDE) from https://www.qt.io/download (select MSVC toolchain on Windows)
|
||||||
3. Run `git submodule update --init --recursive` from within `moonlight-qt/`
|
2. 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
|
3. Open the project in Qt Creator or build from qmake on the command line
|
||||||
|
|
||||||
## Contribute
|
## Contribute
|
||||||
|
|
||||||
This project is being actively developed at [XDA Developers](http://forum.xda-developers.com/showthread.php?t=2505510)
|
|
||||||
|
|
||||||
1. Fork us
|
1. Fork us
|
||||||
2. Write code
|
2. Write code
|
||||||
3. Send Pull Requests
|
3. Send Pull Requests
|
||||||
|
|||||||
+10
-6
@@ -3,24 +3,28 @@
|
|||||||
<plist version="1.0">
|
<plist version="1.0">
|
||||||
<dict>
|
<dict>
|
||||||
<key>CFBundleExecutable</key>
|
<key>CFBundleExecutable</key>
|
||||||
<string>moonlight-qt</string>
|
<string>Moonlight</string>
|
||||||
<key>CFBundleGetInfoString</key>
|
<key>CFBundleGetInfoString</key>
|
||||||
<string>Created by Qt/QMake</string>
|
<string>Stream games from your NVIDIA GameStream-enabled PC</string>
|
||||||
<key>CFBundleIconFile</key>
|
<key>CFBundleIconFile</key>
|
||||||
<string>macos</string>
|
<string>moonlight</string>
|
||||||
<key>CFBundleIdentifier</key>
|
<key>CFBundleIdentifier</key>
|
||||||
<string>com.yourcompany.moonlight-qt</string>
|
<string>com.moonlight-stream.Moonlight</string>
|
||||||
|
<key>NSHighResolutionCapable</key>
|
||||||
|
<true/>
|
||||||
<key>CFBundlePackageType</key>
|
<key>CFBundlePackageType</key>
|
||||||
<string>APPL</string>
|
<string>APPL</string>
|
||||||
<key>CFBundleSignature</key>
|
<key>CFBundleSignature</key>
|
||||||
<string>????</string>
|
<string>????</string>
|
||||||
<key>LSMinimumSystemVersion</key>
|
<key>LSMinimumSystemVersion</key>
|
||||||
<string>10.10</string>
|
<string>10.10</string>
|
||||||
<key>NOTE</key>
|
|
||||||
<string>This file was generated by Qt/QMake.</string>
|
|
||||||
<key>NSPrincipalClass</key>
|
<key>NSPrincipalClass</key>
|
||||||
<string>NSApplication</string>
|
<string>NSApplication</string>
|
||||||
<key>NSSupportsAutomaticGraphicsSwitching</key>
|
<key>NSSupportsAutomaticGraphicsSwitching</key>
|
||||||
<true/>
|
<true/>
|
||||||
|
<key>CFBundleVersion</key>
|
||||||
|
<string>0.0.5</string>
|
||||||
|
<key>CFBundleDisplayName</key>
|
||||||
|
<string>Moonlight</string>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
+94
-36
@@ -1,7 +1,16 @@
|
|||||||
QT += core quick network quickcontrols2
|
QT += core quick network quickcontrols2 svg
|
||||||
CONFIG += c++11
|
CONFIG += c++11
|
||||||
|
|
||||||
TARGET = moonlight-qt
|
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
|
||||||
|
|
||||||
TEMPLATE = app
|
TEMPLATE = app
|
||||||
|
|
||||||
# The following define makes your compiler emit warnings if you use
|
# The following define makes your compiler emit warnings if you use
|
||||||
@@ -28,20 +37,32 @@ win32 {
|
|||||||
LIBS += ws2_32.lib winmm.lib dxva2.lib ole32.lib
|
LIBS += ws2_32.lib winmm.lib dxva2.lib ole32.lib
|
||||||
}
|
}
|
||||||
macx {
|
macx {
|
||||||
INCLUDEPATH += $$PWD/../libs/mac/include
|
INCLUDEPATH += $$PWD/../libs/mac/include $$PWD/../libs/mac/Frameworks/SDL2.framework/Versions/A/Headers
|
||||||
LIBS += -L$$PWD/../libs/mac/lib
|
LIBS += -L$$PWD/../libs/mac/lib -F$$PWD/../libs/mac/Frameworks
|
||||||
}
|
}
|
||||||
|
|
||||||
unix:!macx {
|
unix:!macx {
|
||||||
CONFIG += link_pkgconfig
|
CONFIG += link_pkgconfig
|
||||||
PKGCONFIG += openssl sdl2 libavcodec libavdevice libavformat libavutil
|
PKGCONFIG += openssl sdl2 opus
|
||||||
|
LIBS += -ldl
|
||||||
|
|
||||||
|
packagesExist(libavcodec) {
|
||||||
|
PKGCONFIG += libavcodec libavutil
|
||||||
|
CONFIG += ffmpeg
|
||||||
|
|
||||||
|
packagesExist(libva) {
|
||||||
|
CONFIG += libva
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
win32 {
|
win32 {
|
||||||
LIBS += -llibssl -llibcrypto -lSDL2 -lavcodec -lavdevice -lavformat -lavutil
|
LIBS += -llibssl -llibcrypto -lSDL2 -lavcodec -lavutil -lopus
|
||||||
|
CONFIG += ffmpeg
|
||||||
}
|
}
|
||||||
macx {
|
macx {
|
||||||
LIBS += -lssl -lcrypto -lSDL2 -lavcodec.58 -lavdevice.58 -lavformat.58 -lavutil.56
|
LIBS += -lssl -lcrypto -lavcodec.58 -lavutil.56 -lopus -framework SDL2
|
||||||
LIBS += -lobjc -framework VideoToolbox -framework AVFoundation -framework CoreVideo -framework CoreMedia -framework AppKit
|
LIBS += -lobjc -framework VideoToolbox -framework AVFoundation -framework CoreVideo -framework CoreGraphics -framework CoreMedia -framework AppKit
|
||||||
|
CONFIG += ffmpeg
|
||||||
}
|
}
|
||||||
|
|
||||||
SOURCES += \
|
SOURCES += \
|
||||||
@@ -55,17 +76,8 @@ SOURCES += \
|
|||||||
streaming/input.cpp \
|
streaming/input.cpp \
|
||||||
streaming/session.cpp \
|
streaming/session.cpp \
|
||||||
streaming/audio.cpp \
|
streaming/audio.cpp \
|
||||||
streaming/video/ffmpeg.cpp \
|
|
||||||
gui/computermodel.cpp \
|
gui/computermodel.cpp \
|
||||||
gui/appmodel.cpp \
|
gui/appmodel.cpp
|
||||||
streaming/video/ffmpeg-renderers/sdl.cpp
|
|
||||||
|
|
||||||
win32 {
|
|
||||||
SOURCES += streaming/video/ffmpeg-renderers/dxva2.cpp
|
|
||||||
}
|
|
||||||
macx {
|
|
||||||
SOURCES += streaming/video/avfoundation.mm
|
|
||||||
}
|
|
||||||
|
|
||||||
HEADERS += \
|
HEADERS += \
|
||||||
utils.h \
|
utils.h \
|
||||||
@@ -79,15 +91,46 @@ HEADERS += \
|
|||||||
streaming/session.hpp \
|
streaming/session.hpp \
|
||||||
gui/computermodel.h \
|
gui/computermodel.h \
|
||||||
gui/appmodel.h \
|
gui/appmodel.h \
|
||||||
streaming/video/decoder.h \
|
streaming/video/decoder.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
|
||||||
|
}
|
||||||
|
config_SLVideo {
|
||||||
|
message(SLVideo decoder/renderer selected)
|
||||||
|
|
||||||
|
DEFINES += HAVE_SLVIDEO
|
||||||
|
LIBS += -lSLVideo
|
||||||
|
SOURCES += streaming/video/sl.cpp
|
||||||
|
HEADERS += streaming/video/sl.h
|
||||||
|
}
|
||||||
win32 {
|
win32 {
|
||||||
|
message(DXVA2 renderer selected)
|
||||||
|
|
||||||
|
SOURCES += streaming/video/ffmpeg-renderers/dxva2.cpp
|
||||||
HEADERS += streaming/video/ffmpeg-renderers/dxva2.h
|
HEADERS += streaming/video/ffmpeg-renderers/dxva2.h
|
||||||
}
|
}
|
||||||
macx {
|
macx {
|
||||||
HEADERS += streaming/video/avfoundation.h
|
message(VideoToolbox renderer selected)
|
||||||
|
|
||||||
|
SOURCES += streaming/video/ffmpeg-renderers/vt.mm
|
||||||
|
HEADERS += streaming/video/ffmpeg-renderers/vt.h
|
||||||
}
|
}
|
||||||
|
|
||||||
RESOURCES += \
|
RESOURCES += \
|
||||||
@@ -107,13 +150,6 @@ else:unix: LIBS += -L$$OUT_PWD/../moonlight-common-c/ -lmoonlight-common-c
|
|||||||
INCLUDEPATH += $$PWD/../moonlight-common-c/moonlight-common-c/src
|
INCLUDEPATH += $$PWD/../moonlight-common-c/moonlight-common-c/src
|
||||||
DEPENDPATH += $$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
|
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:win32:CONFIG(debug, debug|release): LIBS += -L$$OUT_PWD/../qmdnsengine/debug/ -lqmdnsengine
|
||||||
else:unix: LIBS += -L$$OUT_PWD/../qmdnsengine/ -lqmdnsengine
|
else:unix: LIBS += -L$$OUT_PWD/../qmdnsengine/ -lqmdnsengine
|
||||||
@@ -121,14 +157,36 @@ else:unix: LIBS += -L$$OUT_PWD/../qmdnsengine/ -lqmdnsengine
|
|||||||
INCLUDEPATH += $$PWD/../qmdnsengine/qmdnsengine/src/include $$PWD/../qmdnsengine
|
INCLUDEPATH += $$PWD/../qmdnsengine/qmdnsengine/src/include $$PWD/../qmdnsengine
|
||||||
DEPENDPATH += $$PWD/../qmdnsengine/qmdnsengine/src/include $$PWD/../qmdnsengine
|
DEPENDPATH += $$PWD/../qmdnsengine/qmdnsengine/src/include $$PWD/../qmdnsengine
|
||||||
|
|
||||||
# Default rules for deployment.
|
unix:!macx: {
|
||||||
qnx: target.path = /tmp/$${TARGET}/bin
|
isEmpty(PREFIX) {
|
||||||
else: unix:!android: target.path = /opt/$${TARGET}/bin
|
PREFIX = /usr/local
|
||||||
!isEmpty(target.path): INSTALLS += target
|
}
|
||||||
|
isEmpty(BINDIR) {
|
||||||
|
BINDIR = bin
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
macx {
|
||||||
QMAKE_INFO_PLIST = $$PWD/Info.plist
|
QMAKE_INFO_PLIST = $$PWD/Info.plist
|
||||||
APP_QML_FILES.files = res/macos.icns
|
|
||||||
APP_QML_FILES.path = Contents/Resources
|
APP_BUNDLE_RESOURCES.files = moonlight.icns
|
||||||
QMAKE_BUNDLE_DATA += APP_QML_FILES
|
APP_BUNDLE_RESOURCES.path = Contents/Resources
|
||||||
|
QMAKE_BUNDLE_DATA += APP_BUNDLE_RESOURCES
|
||||||
}
|
}
|
||||||
|
|
||||||
|
VERSION = 0.0.5
|
||||||
@@ -51,6 +51,7 @@ NvComputer::NvComputer(QSettings& settings)
|
|||||||
this->appVersion = nullptr;
|
this->appVersion = nullptr;
|
||||||
this->maxLumaPixelsHEVC = 0;
|
this->maxLumaPixelsHEVC = 0;
|
||||||
this->serverCodecModeSupport = 0;
|
this->serverCodecModeSupport = 0;
|
||||||
|
this->pendingQuit = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
@@ -135,17 +136,18 @@ NvComputer::NvComputer(QString address, QString serverInfo)
|
|||||||
this->gfeVersion = NvHTTP::getXmlString(serverInfo, "GfeVersion");
|
this->gfeVersion = NvHTTP::getXmlString(serverInfo, "GfeVersion");
|
||||||
this->activeAddress = address;
|
this->activeAddress = address;
|
||||||
this->state = NvComputer::CS_ONLINE;
|
this->state = NvComputer::CS_ONLINE;
|
||||||
|
this->pendingQuit = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool NvComputer::wake()
|
bool NvComputer::wake()
|
||||||
{
|
{
|
||||||
if (state == NvComputer::CS_ONLINE) {
|
if (state == NvComputer::CS_ONLINE) {
|
||||||
qWarning() << name << " is already online";
|
qWarning() << name << "is already online";
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (macAddress.isNull()) {
|
if (macAddress.isNull()) {
|
||||||
qWarning() << name << " has no MAC address stored";
|
qWarning() << name << "has no MAC address stored";
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -184,7 +186,7 @@ bool NvComputer::wake()
|
|||||||
// Send to all ports
|
// Send to all ports
|
||||||
for (quint16 port : WOL_PORTS) {
|
for (quint16 port : WOL_PORTS) {
|
||||||
if (sock.writeDatagram(wolPayload, address, port)) {
|
if (sock.writeDatagram(wolPayload, address, port)) {
|
||||||
qDebug() << "Send WoL packet to " << name << " via " << address.toString() << ":" << port;
|
qInfo().nospace().noquote() << "Send WoL packet to " << name << " via " << address.toString() << ":" << port;
|
||||||
success = true;
|
success = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -286,6 +288,56 @@ ComputerManager::ComputerManager(QObject *parent)
|
|||||||
settings.endArray();
|
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()
|
void ComputerManager::saveHosts()
|
||||||
{
|
{
|
||||||
QSettings settings;
|
QSettings settings;
|
||||||
@@ -312,7 +364,7 @@ void ComputerManager::startPolling()
|
|||||||
m_MdnsBrowser = new QMdnsEngine::Browser(&m_MdnsServer, "_nvstream._tcp.local.", &m_MdnsCache);
|
m_MdnsBrowser = new QMdnsEngine::Browser(&m_MdnsServer, "_nvstream._tcp.local.", &m_MdnsCache);
|
||||||
connect(m_MdnsBrowser, &QMdnsEngine::Browser::serviceAdded,
|
connect(m_MdnsBrowser, &QMdnsEngine::Browser::serviceAdded,
|
||||||
this, [this](const QMdnsEngine::Service& service) {
|
this, [this](const QMdnsEngine::Service& service) {
|
||||||
qDebug() << "Discovered mDNS host: " << service.hostname();
|
qInfo() << "Discovered mDNS host:" << service.hostname();
|
||||||
|
|
||||||
MdnsPendingComputer* pendingComputer = new MdnsPendingComputer(&m_MdnsServer, &m_MdnsCache, service);
|
MdnsPendingComputer* pendingComputer = new MdnsPendingComputer(&m_MdnsServer, &m_MdnsCache, service);
|
||||||
connect(pendingComputer, SIGNAL(resolvedv4(MdnsPendingComputer*,QHostAddress)),
|
connect(pendingComputer, SIGNAL(resolvedv4(MdnsPendingComputer*,QHostAddress)),
|
||||||
@@ -331,7 +383,7 @@ void ComputerManager::startPolling()
|
|||||||
void ComputerManager::handleMdnsServiceResolved(MdnsPendingComputer* computer,
|
void ComputerManager::handleMdnsServiceResolved(MdnsPendingComputer* computer,
|
||||||
const QHostAddress& address)
|
const QHostAddress& address)
|
||||||
{
|
{
|
||||||
qDebug() << "Resolved " << computer->hostname() << " to " << address.toString();
|
qInfo() << "Resolved" << computer->hostname() << "to" << address.toString();
|
||||||
|
|
||||||
addNewHost(address.toString(), true);
|
addNewHost(address.toString(), true);
|
||||||
|
|
||||||
@@ -396,6 +448,15 @@ void ComputerManager::pairHost(NvComputer* computer, QString pin)
|
|||||||
QThreadPool::globalInstance()->start(pairing);
|
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()
|
void ComputerManager::stopPollingAsync()
|
||||||
{
|
{
|
||||||
QWriteLocker lock(&m_Lock);
|
QWriteLocker lock(&m_Lock);
|
||||||
@@ -447,6 +508,11 @@ ComputerManager::handleComputerStateChanged(NvComputer* computer)
|
|||||||
{
|
{
|
||||||
emit computerStateChanged(computer);
|
emit computerStateChanged(computer);
|
||||||
|
|
||||||
|
if (computer->pendingQuit && computer->currentGameId == 0) {
|
||||||
|
computer->pendingQuit = false;
|
||||||
|
emit quitAppCompleted(QVariant());
|
||||||
|
}
|
||||||
|
|
||||||
// Save updated hosts to QSettings
|
// Save updated hosts to QSettings
|
||||||
saveHosts();
|
saveHosts();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,10 +16,14 @@
|
|||||||
class NvComputer
|
class NvComputer
|
||||||
{
|
{
|
||||||
friend class PcMonitorThread;
|
friend class PcMonitorThread;
|
||||||
|
friend class ComputerManager;
|
||||||
|
friend class PendingQuitTask;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void sortAppList();
|
void sortAppList();
|
||||||
|
|
||||||
|
bool pendingQuit;
|
||||||
|
|
||||||
public:
|
public:
|
||||||
explicit NvComputer(QString address, QString serverInfo);
|
explicit NvComputer(QString address, QString serverInfo);
|
||||||
|
|
||||||
@@ -148,31 +152,26 @@ private:
|
|||||||
int pollsSinceLastAppListFetch = POLLS_PER_APPLIST_FETCH;
|
int pollsSinceLastAppListFetch = POLLS_PER_APPLIST_FETCH;
|
||||||
while (!isInterruptionRequested()) {
|
while (!isInterruptionRequested()) {
|
||||||
bool stateChanged = false;
|
bool stateChanged = false;
|
||||||
for (int i = 0; i < TRIES_BEFORE_OFFLINING; i++) {
|
bool online = false;
|
||||||
|
for (int i = 0; i < TRIES_BEFORE_OFFLINING && !online; i++) {
|
||||||
for (auto& address : m_Computer->uniqueAddresses()) {
|
for (auto& address : m_Computer->uniqueAddresses()) {
|
||||||
if (isInterruptionRequested()) {
|
if (isInterruptionRequested()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (tryPollComputer(address, stateChanged)) {
|
if (tryPollComputer(address, stateChanged)) {
|
||||||
|
online = true;
|
||||||
break;
|
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
|
// Check if we failed after all retry attempts
|
||||||
// Note: we don't need to acquire the read lock here,
|
// Note: we don't need to acquire the read lock here,
|
||||||
// because we're on the writing thread.
|
// because we're on the writing thread.
|
||||||
if (m_Computer->state != NvComputer::CS_ONLINE) {
|
if (!online && m_Computer->state != NvComputer::CS_OFFLINE) {
|
||||||
if (m_Computer->state != NvComputer::CS_OFFLINE) {
|
m_Computer->state = NvComputer::CS_OFFLINE;
|
||||||
m_Computer->state = NvComputer::CS_OFFLINE;
|
stateChanged = true;
|
||||||
stateChanged = true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Grab the applist if it's empty or it's been long enough that we need to refresh
|
// Grab the applist if it's empty or it's been long enough that we need to refresh
|
||||||
@@ -249,6 +248,8 @@ class ComputerManager : public QObject
|
|||||||
public:
|
public:
|
||||||
explicit ComputerManager(QObject *parent = nullptr);
|
explicit ComputerManager(QObject *parent = nullptr);
|
||||||
|
|
||||||
|
virtual ~ComputerManager();
|
||||||
|
|
||||||
Q_INVOKABLE void startPolling();
|
Q_INVOKABLE void startPolling();
|
||||||
|
|
||||||
Q_INVOKABLE void stopPollingAsync();
|
Q_INVOKABLE void stopPollingAsync();
|
||||||
@@ -257,6 +258,8 @@ public:
|
|||||||
|
|
||||||
void pairHost(NvComputer* computer, QString pin);
|
void pairHost(NvComputer* computer, QString pin);
|
||||||
|
|
||||||
|
void quitRunningApp(NvComputer* computer);
|
||||||
|
|
||||||
QVector<NvComputer*> getComputers();
|
QVector<NvComputer*> getComputers();
|
||||||
|
|
||||||
// computer is deleted inside this call
|
// computer is deleted inside this call
|
||||||
@@ -269,6 +272,8 @@ signals:
|
|||||||
|
|
||||||
void computerAddCompleted(QVariant success);
|
void computerAddCompleted(QVariant success);
|
||||||
|
|
||||||
|
void quitAppCompleted(QVariant error);
|
||||||
|
|
||||||
private slots:
|
private slots:
|
||||||
void handleComputerStateChanged(NvComputer* computer);
|
void handleComputerStateChanged(NvComputer* computer);
|
||||||
|
|
||||||
@@ -336,6 +341,49 @@ private:
|
|||||||
QString m_Pin;
|
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
|
class PendingAddTask : public QObject, public QRunnable
|
||||||
{
|
{
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
|
|||||||
@@ -2,12 +2,12 @@
|
|||||||
#include "utils.h"
|
#include "utils.h"
|
||||||
|
|
||||||
#include <QDebug>
|
#include <QDebug>
|
||||||
#include <QRandomGenerator64>
|
|
||||||
|
|
||||||
#include <openssl/pem.h>
|
#include <openssl/pem.h>
|
||||||
#include <openssl/rsa.h>
|
#include <openssl/rsa.h>
|
||||||
#include <openssl/bn.h>
|
#include <openssl/bn.h>
|
||||||
#include <openssl/x509.h>
|
#include <openssl/x509.h>
|
||||||
|
#include <openssl/rand.h>
|
||||||
|
|
||||||
#define SER_UNIQUEID "uniqueid"
|
#define SER_UNIQUEID "uniqueid"
|
||||||
#define SER_CERT "certificate"
|
#define SER_CERT "certificate"
|
||||||
@@ -101,7 +101,7 @@ void IdentityManager::createCredentials(QSettings& settings)
|
|||||||
settings.setValue(SER_CERT, m_CachedPemCert);
|
settings.setValue(SER_CERT, m_CachedPemCert);
|
||||||
settings.setValue(SER_KEY, m_CachedPrivateKey);
|
settings.setValue(SER_KEY, m_CachedPrivateKey);
|
||||||
|
|
||||||
qDebug() << "Wrote new identity credentials to settings";
|
qInfo() << "Wrote new identity credentials to settings";
|
||||||
}
|
}
|
||||||
|
|
||||||
IdentityManager::IdentityManager()
|
IdentityManager::IdentityManager()
|
||||||
@@ -112,7 +112,7 @@ IdentityManager::IdentityManager()
|
|||||||
m_CachedPrivateKey = settings.value(SER_KEY).toByteArray();
|
m_CachedPrivateKey = settings.value(SER_KEY).toByteArray();
|
||||||
|
|
||||||
if (m_CachedPemCert.isEmpty() || m_CachedPrivateKey.isEmpty()) {
|
if (m_CachedPemCert.isEmpty() || m_CachedPrivateKey.isEmpty()) {
|
||||||
qDebug() << "No existing credentials found";
|
qInfo() << "No existing credentials found";
|
||||||
createCredentials(settings);
|
createCredentials(settings);
|
||||||
}
|
}
|
||||||
else if (getSslCertificate().isNull()) {
|
else if (getSslCertificate().isNull()) {
|
||||||
@@ -157,10 +157,14 @@ IdentityManager::getSslKey()
|
|||||||
|
|
||||||
// We must write out our PEM in the old PKCS1 format for SecureTransport
|
// We must write out our PEM in the old PKCS1 format for SecureTransport
|
||||||
// on macOS/iOS to be able to read it.
|
// 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;
|
BUF_MEM* mem;
|
||||||
BIO_get_mem_ptr(bio, &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);
|
m_CachedSslKey = QSslKey(QByteArray::fromRawData(mem->data, (int)mem->length), QSsl::Rsa);
|
||||||
|
|
||||||
BIO_free(bio);
|
BIO_free(bio);
|
||||||
@@ -187,14 +191,15 @@ IdentityManager::getUniqueId()
|
|||||||
// Load the unique ID from settings
|
// Load the unique ID from settings
|
||||||
m_CachedUniqueId = settings.value(SER_UNIQUEID).toString();
|
m_CachedUniqueId = settings.value(SER_UNIQUEID).toString();
|
||||||
if (!m_CachedUniqueId.isEmpty() && !m_CachedUniqueId.isNull()) {
|
if (!m_CachedUniqueId.isEmpty() && !m_CachedUniqueId.isNull()) {
|
||||||
qDebug() << "Loaded unique ID from settings: " << m_CachedUniqueId;
|
qInfo() << "Loaded unique ID from settings:" << m_CachedUniqueId;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
// Generate a new unique ID in base 16
|
// Generate a new unique ID in base 16
|
||||||
m_CachedUniqueId = QString::number(
|
uint64_t uid;
|
||||||
QRandomGenerator64::securelySeeded().generate64(), 16);
|
RAND_bytes(reinterpret_cast<unsigned char*>(&uid), sizeof(uid));
|
||||||
|
m_CachedUniqueId = QString::number(uid, 16);
|
||||||
|
|
||||||
qDebug() << "Generated new unique ID: " << m_CachedUniqueId;
|
qInfo() << "Generated new unique ID:" << m_CachedUniqueId;
|
||||||
|
|
||||||
settings.setValue(SER_UNIQUEID, m_CachedUniqueId);
|
settings.setValue(SER_UNIQUEID, m_CachedUniqueId);
|
||||||
}
|
}
|
||||||
|
|||||||
+22
-7
@@ -123,6 +123,11 @@ NvHTTP::launchApp(int appId,
|
|||||||
bool localAudio,
|
bool localAudio,
|
||||||
int gamepadMask)
|
int gamepadMask)
|
||||||
{
|
{
|
||||||
|
int riKeyId;
|
||||||
|
|
||||||
|
memcpy(&riKeyId, streamConfig->remoteInputAesIv, sizeof(riKeyId));
|
||||||
|
riKeyId = qFromBigEndian(riKeyId);
|
||||||
|
|
||||||
QString response =
|
QString response =
|
||||||
openConnectionToString(m_BaseUrlHttps,
|
openConnectionToString(m_BaseUrlHttps,
|
||||||
"launch",
|
"launch",
|
||||||
@@ -132,7 +137,7 @@ NvHTTP::launchApp(int appId,
|
|||||||
QString::number(streamConfig->fps)+
|
QString::number(streamConfig->fps)+
|
||||||
"&additionalStates=1&sops="+QString::number(sops ? 1 : 0)+
|
"&additionalStates=1&sops="+QString::number(sops ? 1 : 0)+
|
||||||
"&rikey="+QByteArray(streamConfig->remoteInputAesKey, sizeof(streamConfig->remoteInputAesKey)).toHex()+
|
"&rikey="+QByteArray(streamConfig->remoteInputAesKey, sizeof(streamConfig->remoteInputAesKey)).toHex()+
|
||||||
"&rikeyid="+QString::number(qFromBigEndian(*(int*)streamConfig->remoteInputAesIv))+
|
"&rikeyid="+QString::number(riKeyId)+
|
||||||
(streamConfig->enableHdr ?
|
(streamConfig->enableHdr ?
|
||||||
"&hdrMode=1&clientHdrCapVersion=0&clientHdrCapSupportedFlagsInUint32=0&clientHdrCapMetaDataId=NV_STATIC_METADATA_TYPE_1&clientHdrCapDisplayData=0x0x0x0x0x0x0x0x0x0x0" :
|
"&hdrMode=1&clientHdrCapVersion=0&clientHdrCapSupportedFlagsInUint32=0&clientHdrCapMetaDataId=NV_STATIC_METADATA_TYPE_1&clientHdrCapDisplayData=0x0x0x0x0x0x0x0x0x0x0" :
|
||||||
"")+
|
"")+
|
||||||
@@ -149,11 +154,16 @@ NvHTTP::launchApp(int appId,
|
|||||||
void
|
void
|
||||||
NvHTTP::resumeApp(PSTREAM_CONFIGURATION streamConfig)
|
NvHTTP::resumeApp(PSTREAM_CONFIGURATION streamConfig)
|
||||||
{
|
{
|
||||||
|
int riKeyId;
|
||||||
|
|
||||||
|
memcpy(&riKeyId, streamConfig->remoteInputAesIv, sizeof(riKeyId));
|
||||||
|
riKeyId = qFromBigEndian(riKeyId);
|
||||||
|
|
||||||
QString response =
|
QString response =
|
||||||
openConnectionToString(m_BaseUrlHttps,
|
openConnectionToString(m_BaseUrlHttps,
|
||||||
"resume",
|
"resume",
|
||||||
"rikey="+QString(QByteArray(streamConfig->remoteInputAesKey, sizeof(streamConfig->remoteInputAesKey)).toHex())+
|
"rikey="+QString(QByteArray(streamConfig->remoteInputAesKey, sizeof(streamConfig->remoteInputAesKey)).toHex())+
|
||||||
"&rikeyid="+QString::number(qFromBigEndian(*(int*)streamConfig->remoteInputAesIv))+
|
"&rikeyid="+QString::number(riKeyId)+
|
||||||
"&surroundAudioInfo="+getSurroundAudioInfoString(streamConfig->audioConfiguration),
|
"&surroundAudioInfo="+getSurroundAudioInfoString(streamConfig->audioConfiguration),
|
||||||
false);
|
false);
|
||||||
|
|
||||||
@@ -265,7 +275,10 @@ NvHTTP::verifyResponseStatus(QString xml)
|
|||||||
else
|
else
|
||||||
{
|
{
|
||||||
QString statusMessage = xmlReader.attributes().value("status_message").toString();
|
QString statusMessage = xmlReader.attributes().value("status_message").toString();
|
||||||
qDebug() << "Request failed: " << statusCode << " " << statusMessage;
|
if (statusCode != 401) {
|
||||||
|
// 401 is expected for unpaired PCs when we fetch serverinfo over HTTPS
|
||||||
|
qWarning() << "Request failed:" << statusCode << statusMessage;
|
||||||
|
}
|
||||||
throw GfeHttpResponseException(statusCode, statusMessage);
|
throw GfeHttpResponseException(statusCode, statusMessage);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -330,7 +343,9 @@ NvHTTP::openConnectionToString(QUrl baseUrl,
|
|||||||
QNetworkReply* reply = openConnection(baseUrl, command, arguments, enableTimeout);
|
QNetworkReply* reply = openConnection(baseUrl, command, arguments, enableTimeout);
|
||||||
QString ret;
|
QString ret;
|
||||||
|
|
||||||
ret = QTextStream(reply).readAll();
|
QTextStream stream(reply);
|
||||||
|
stream.setCodec("UTF-8");
|
||||||
|
ret = stream.readAll();
|
||||||
delete reply;
|
delete reply;
|
||||||
|
|
||||||
return ret;
|
return ret;
|
||||||
@@ -366,13 +381,13 @@ NvHTTP::openConnection(QUrl baseUrl,
|
|||||||
{
|
{
|
||||||
QTimer::singleShot(REQUEST_TIMEOUT_MS, &loop, SLOT(quit()));
|
QTimer::singleShot(REQUEST_TIMEOUT_MS, &loop, SLOT(quit()));
|
||||||
}
|
}
|
||||||
qDebug() << "Executing request: " << url.toString();
|
qInfo() << "Executing request:" << url.toString();
|
||||||
loop.exec(QEventLoop::ExcludeUserInputEvents);
|
loop.exec(QEventLoop::ExcludeUserInputEvents);
|
||||||
|
|
||||||
// Abort the request if it timed out
|
// Abort the request if it timed out
|
||||||
if (!reply->isFinished())
|
if (!reply->isFinished())
|
||||||
{
|
{
|
||||||
qDebug() << "Aborting timed out request for " << url.toString();
|
qWarning() << "Aborting timed out request for" << url.toString();
|
||||||
reply->abort();
|
reply->abort();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -383,7 +398,7 @@ NvHTTP::openConnection(QUrl baseUrl,
|
|||||||
// Handle error
|
// Handle error
|
||||||
if (reply->error() != QNetworkReply::NoError)
|
if (reply->error() != QNetworkReply::NoError)
|
||||||
{
|
{
|
||||||
qDebug() << command << " request failed with error " << reply->error();
|
qWarning() << command << " request failed with error " << reply->error();
|
||||||
GfeHttpResponseException exception(reply->error(), reply->errorString());
|
GfeHttpResponseException exception(reply->error(), reply->errorString());
|
||||||
delete reply;
|
delete reply;
|
||||||
throw exception;
|
throw exception;
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
#include "nvpairingmanager.h"
|
#include "nvpairingmanager.h"
|
||||||
#include "utils.h"
|
#include "utils.h"
|
||||||
|
|
||||||
#include <QRandomGenerator>
|
|
||||||
|
|
||||||
#include <openssl/bio.h>
|
#include <openssl/bio.h>
|
||||||
#include <openssl/aes.h>
|
#include <openssl/aes.h>
|
||||||
#include <openssl/rand.h>
|
#include <openssl/rand.h>
|
||||||
@@ -21,7 +19,7 @@ NvPairingManager::NvPairingManager(QString address) :
|
|||||||
BIO_free_all(bio);
|
BIO_free_all(bio);
|
||||||
if (m_Cert == nullptr)
|
if (m_Cert == nullptr)
|
||||||
{
|
{
|
||||||
throw new std::runtime_error("Unable to load certificate");
|
throw std::runtime_error("Unable to load certificate");
|
||||||
}
|
}
|
||||||
|
|
||||||
QByteArray pk = IdentityManager::get()->getPrivateKey();
|
QByteArray pk = IdentityManager::get()->getPrivateKey();
|
||||||
@@ -32,7 +30,7 @@ NvPairingManager::NvPairingManager(QString address) :
|
|||||||
BIO_free_all(bio);
|
BIO_free_all(bio);
|
||||||
if (m_PrivateKey == nullptr)
|
if (m_PrivateKey == nullptr)
|
||||||
{
|
{
|
||||||
throw new std::runtime_error("Unable to load private key");
|
throw std::runtime_error("Unable to load private key");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -89,13 +87,15 @@ NvPairingManager::getSignatureFromPemCert(QByteArray certificate)
|
|||||||
X509* cert = PEM_read_bio_X509(bio, nullptr, nullptr, nullptr);
|
X509* cert = PEM_read_bio_X509(bio, nullptr, nullptr, nullptr);
|
||||||
BIO_free_all(bio);
|
BIO_free_all(bio);
|
||||||
|
|
||||||
#if (OPENSSL_VERSION_NUMBER < 0x10100000L)
|
#if (OPENSSL_VERSION_NUMBER < 0x10002000L)
|
||||||
ASN1_BIT_STRING *asnSignature;
|
ASN1_BIT_STRING *asnSignature = cert->signature;
|
||||||
#else
|
#elif (OPENSSL_VERSION_NUMBER < 0x10100000L)
|
||||||
const ASN1_BIT_STRING *asnSignature;
|
ASN1_BIT_STRING *asnSignature;
|
||||||
#endif
|
|
||||||
|
|
||||||
X509_get0_signature(&asnSignature, NULL, cert);
|
X509_get0_signature(&asnSignature, NULL, cert);
|
||||||
|
#else
|
||||||
|
const ASN1_BIT_STRING *asnSignature;
|
||||||
|
X509_get0_signature(&asnSignature, NULL, cert);
|
||||||
|
#endif
|
||||||
|
|
||||||
QByteArray signature(reinterpret_cast<char*>(asnSignature->data), asnSignature->length);
|
QByteArray signature(reinterpret_cast<char*>(asnSignature->data), asnSignature->length);
|
||||||
|
|
||||||
@@ -164,7 +164,7 @@ NvPairingManager::PairState
|
|||||||
NvPairingManager::pair(QString appVersion, QString pin)
|
NvPairingManager::pair(QString appVersion, QString pin)
|
||||||
{
|
{
|
||||||
int serverMajorVersion = NvHTTP::parseQuad(appVersion).at(0);
|
int serverMajorVersion = NvHTTP::parseQuad(appVersion).at(0);
|
||||||
qDebug() << "Pairing with server generation: " << serverMajorVersion;
|
qInfo() << "Pairing with server generation:" << serverMajorVersion;
|
||||||
|
|
||||||
QCryptographicHash::Algorithm hashAlgo;
|
QCryptographicHash::Algorithm hashAlgo;
|
||||||
int hashLength;
|
int hashLength;
|
||||||
@@ -196,14 +196,14 @@ NvPairingManager::pair(QString appVersion, QString pin)
|
|||||||
NvHTTP::verifyResponseStatus(getCert);
|
NvHTTP::verifyResponseStatus(getCert);
|
||||||
if (NvHTTP::getXmlString(getCert, "paired") != "1")
|
if (NvHTTP::getXmlString(getCert, "paired") != "1")
|
||||||
{
|
{
|
||||||
qDebug() << "Failed pairing at stage #1";
|
qCritical() << "Failed pairing at stage #1";
|
||||||
return PairState::FAILED;
|
return PairState::FAILED;
|
||||||
}
|
}
|
||||||
|
|
||||||
QByteArray serverCert = NvHTTP::getXmlStringFromHex(getCert, "plaincert");
|
QByteArray serverCert = NvHTTP::getXmlStringFromHex(getCert, "plaincert");
|
||||||
if (serverCert == nullptr)
|
if (serverCert == nullptr)
|
||||||
{
|
{
|
||||||
qDebug() << "Server likely already pairing";
|
qCritical() << "Server likely already pairing";
|
||||||
m_Http.openConnectionToString(m_Http.m_BaseUrlHttp, "unpair", nullptr, true);
|
m_Http.openConnectionToString(m_Http.m_BaseUrlHttp, "unpair", nullptr, true);
|
||||||
return PairState::ALREADY_IN_PROGRESS;
|
return PairState::ALREADY_IN_PROGRESS;
|
||||||
}
|
}
|
||||||
@@ -218,7 +218,7 @@ NvPairingManager::pair(QString appVersion, QString pin)
|
|||||||
NvHTTP::verifyResponseStatus(challengeXml);
|
NvHTTP::verifyResponseStatus(challengeXml);
|
||||||
if (NvHTTP::getXmlString(challengeXml, "paired") != "1")
|
if (NvHTTP::getXmlString(challengeXml, "paired") != "1")
|
||||||
{
|
{
|
||||||
qDebug() << "Failed pairing at stage #2";
|
qCritical() << "Failed pairing at stage #2";
|
||||||
m_Http.openConnectionToString(m_Http.m_BaseUrlHttp, "unpair", nullptr, true);
|
m_Http.openConnectionToString(m_Http.m_BaseUrlHttp, "unpair", nullptr, true);
|
||||||
return PairState::FAILED;
|
return PairState::FAILED;
|
||||||
}
|
}
|
||||||
@@ -228,13 +228,15 @@ NvPairingManager::pair(QString appVersion, QString pin)
|
|||||||
QByteArray challengeResponse;
|
QByteArray challengeResponse;
|
||||||
QByteArray serverResponse(challengeResponseData.data(), hashLength);
|
QByteArray serverResponse(challengeResponseData.data(), hashLength);
|
||||||
|
|
||||||
#if (OPENSSL_VERSION_NUMBER < 0x10100000L)
|
#if (OPENSSL_VERSION_NUMBER < 0x10002000L)
|
||||||
|
ASN1_BIT_STRING *asnSignature = m_Cert->signature;
|
||||||
|
#elif (OPENSSL_VERSION_NUMBER < 0x10100000L)
|
||||||
ASN1_BIT_STRING *asnSignature;
|
ASN1_BIT_STRING *asnSignature;
|
||||||
#else
|
|
||||||
const ASN1_BIT_STRING *asnSignature;
|
|
||||||
#endif
|
|
||||||
|
|
||||||
X509_get0_signature(&asnSignature, NULL, m_Cert);
|
X509_get0_signature(&asnSignature, NULL, m_Cert);
|
||||||
|
#else
|
||||||
|
const ASN1_BIT_STRING *asnSignature;
|
||||||
|
X509_get0_signature(&asnSignature, NULL, m_Cert);
|
||||||
|
#endif
|
||||||
|
|
||||||
challengeResponse.append(challengeResponseData.data() + hashLength, 16);
|
challengeResponse.append(challengeResponseData.data() + hashLength, 16);
|
||||||
challengeResponse.append(reinterpret_cast<char*>(asnSignature->data), asnSignature->length);
|
challengeResponse.append(reinterpret_cast<char*>(asnSignature->data), asnSignature->length);
|
||||||
@@ -249,7 +251,7 @@ NvPairingManager::pair(QString appVersion, QString pin)
|
|||||||
NvHTTP::verifyResponseStatus(respXml);
|
NvHTTP::verifyResponseStatus(respXml);
|
||||||
if (NvHTTP::getXmlString(respXml, "paired") != "1")
|
if (NvHTTP::getXmlString(respXml, "paired") != "1")
|
||||||
{
|
{
|
||||||
qDebug() << "Failed pairing at stage #3";
|
qCritical() << "Failed pairing at stage #3";
|
||||||
m_Http.openConnectionToString(m_Http.m_BaseUrlHttp, "unpair", nullptr, true);
|
m_Http.openConnectionToString(m_Http.m_BaseUrlHttp, "unpair", nullptr, true);
|
||||||
return PairState::FAILED;
|
return PairState::FAILED;
|
||||||
}
|
}
|
||||||
@@ -262,7 +264,7 @@ NvPairingManager::pair(QString appVersion, QString pin)
|
|||||||
serverSignature,
|
serverSignature,
|
||||||
serverCert))
|
serverCert))
|
||||||
{
|
{
|
||||||
qDebug() << "MITM detected";
|
qCritical() << "MITM detected";
|
||||||
m_Http.openConnectionToString(m_Http.m_BaseUrlHttp, "unpair", nullptr, true);
|
m_Http.openConnectionToString(m_Http.m_BaseUrlHttp, "unpair", nullptr, true);
|
||||||
return PairState::FAILED;
|
return PairState::FAILED;
|
||||||
}
|
}
|
||||||
@@ -273,7 +275,7 @@ NvPairingManager::pair(QString appVersion, QString pin)
|
|||||||
expectedResponseData.append(serverSecret);
|
expectedResponseData.append(serverSecret);
|
||||||
if (QCryptographicHash::hash(expectedResponseData, hashAlgo) != serverResponse)
|
if (QCryptographicHash::hash(expectedResponseData, hashAlgo) != serverResponse)
|
||||||
{
|
{
|
||||||
qDebug() << "Incorrect PIN";
|
qCritical() << "Incorrect PIN";
|
||||||
m_Http.openConnectionToString(m_Http.m_BaseUrlHttp, "unpair", nullptr, true);
|
m_Http.openConnectionToString(m_Http.m_BaseUrlHttp, "unpair", nullptr, true);
|
||||||
return PairState::PIN_WRONG;
|
return PairState::PIN_WRONG;
|
||||||
}
|
}
|
||||||
@@ -290,7 +292,7 @@ NvPairingManager::pair(QString appVersion, QString pin)
|
|||||||
NvHTTP::verifyResponseStatus(secretRespXml);
|
NvHTTP::verifyResponseStatus(secretRespXml);
|
||||||
if (NvHTTP::getXmlString(secretRespXml, "paired") != "1")
|
if (NvHTTP::getXmlString(secretRespXml, "paired") != "1")
|
||||||
{
|
{
|
||||||
qDebug() << "Failed pairing at stage #4";
|
qCritical() << "Failed pairing at stage #4";
|
||||||
m_Http.openConnectionToString(m_Http.m_BaseUrlHttp, "unpair", nullptr, true);
|
m_Http.openConnectionToString(m_Http.m_BaseUrlHttp, "unpair", nullptr, true);
|
||||||
return PairState::FAILED;
|
return PairState::FAILED;
|
||||||
}
|
}
|
||||||
@@ -302,7 +304,7 @@ NvPairingManager::pair(QString appVersion, QString pin)
|
|||||||
NvHTTP::verifyResponseStatus(pairChallengeXml);
|
NvHTTP::verifyResponseStatus(pairChallengeXml);
|
||||||
if (NvHTTP::getXmlString(pairChallengeXml, "paired") != "1")
|
if (NvHTTP::getXmlString(pairChallengeXml, "paired") != "1")
|
||||||
{
|
{
|
||||||
qDebug() << "Failed pairing at stage #5";
|
qCritical() << "Failed pairing at stage #5";
|
||||||
m_Http.openConnectionToString(m_Http.m_BaseUrlHttp, "unpair", nullptr, true);
|
m_Http.openConnectionToString(m_Http.m_BaseUrlHttp, "unpair", nullptr, true);
|
||||||
return PairState::FAILED;
|
return PairState::FAILED;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,61 @@
|
|||||||
|
<?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 or other applications from a PC with an NVIDIA GeForce GTX 600-series or higher GPU and GeForce Experience installed.</p>
|
||||||
|
<p>GameStream utilizes the NVENC hardware encoder present on NVIDIA GPUs for low-latency streaming at up 4K 60 FPS. Moonlight can utilize VAAPI hardware acceleration for high-performance H.264 and HEVC video decoding with low CPU usage.</p>
|
||||||
|
</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.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>
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
[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;
|
||||||
+90
-16
@@ -1,4 +1,5 @@
|
|||||||
import QtQuick 2.9
|
import QtQuick 2.9
|
||||||
|
import QtQuick.Dialogs 1.3
|
||||||
import QtQuick.Controls 2.2
|
import QtQuick.Controls 2.2
|
||||||
|
|
||||||
import AppModel 1.0
|
import AppModel 1.0
|
||||||
@@ -12,7 +13,7 @@ GridView {
|
|||||||
id: appGrid
|
id: appGrid
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
anchors.leftMargin: (parent.width % (cellWidth + anchors.rightMargin)) / 2
|
anchors.leftMargin: (parent.width % (cellWidth + anchors.rightMargin)) / 2
|
||||||
anchors.topMargin: 5
|
anchors.topMargin: 20
|
||||||
anchors.rightMargin: 5
|
anchors.rightMargin: 5
|
||||||
anchors.bottomMargin: 5
|
anchors.bottomMargin: 5
|
||||||
cellWidth: 225; cellHeight: 350;
|
cellWidth: 225; cellHeight: 350;
|
||||||
@@ -50,12 +51,6 @@ GridView {
|
|||||||
delegate: Item {
|
delegate: Item {
|
||||||
width: 200; height: 300;
|
width: 200; height: 300;
|
||||||
|
|
||||||
Component.onCompleted: {
|
|
||||||
if (model.running) {
|
|
||||||
appNameText.text = "<font color=\"green\">Running</font><font color=\"white\"> - </font>" + appNameText.text
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Image {
|
Image {
|
||||||
id: appIcon
|
id: appIcon
|
||||||
anchors.horizontalCenter: parent.horizontalCenter;
|
anchors.horizontalCenter: parent.horizontalCenter;
|
||||||
@@ -68,27 +63,106 @@ GridView {
|
|||||||
|
|
||||||
Text {
|
Text {
|
||||||
id: appNameText
|
id: appNameText
|
||||||
text: model.name
|
text: {
|
||||||
|
if (model.running) {
|
||||||
|
return "<font color=\"green\">Running</font><font color=\"white\"> - </font>" + model.name
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return model.name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
color: "white"
|
color: "white"
|
||||||
|
|
||||||
width: parent.width
|
width: parent.width
|
||||||
height: 100
|
height: 125
|
||||||
anchors.top: appIcon.bottom
|
anchors.top: appIcon.bottom
|
||||||
font.pointSize: 26
|
font.pointSize: 22
|
||||||
horizontalAlignment: Text.AlignHCenter
|
horizontalAlignment: Text.AlignHCenter
|
||||||
wrapMode: Text.Wrap
|
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 {
|
MouseArea {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
|
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
||||||
onClicked: {
|
onClicked: {
|
||||||
// TODO: Check if a different game is running
|
if (mouse.button === Qt.LeftButton) {
|
||||||
|
// Nothing is running or this app is running
|
||||||
|
launchOrResumeSelectedApp()
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Right click
|
||||||
|
appContextMenu.open()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var component = Qt.createComponent("StreamSegue.qml")
|
Menu {
|
||||||
var segue = component.createObject(stackView)
|
id: appContextMenu
|
||||||
segue.appname = model.name
|
MenuItem {
|
||||||
segue.session = appModel.createSessionForApp(index)
|
text: model.running ? "Resume Game" : "Launch Game"
|
||||||
stackView.push(segue)
|
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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+92
-31
@@ -6,6 +6,7 @@ import QtQuick.Layouts 1.3
|
|||||||
import ComputerModel 1.0
|
import ComputerModel 1.0
|
||||||
|
|
||||||
import ComputerManager 1.0
|
import ComputerManager 1.0
|
||||||
|
import StreamingPreferences 1.0
|
||||||
|
|
||||||
GridView {
|
GridView {
|
||||||
property ComputerModel computerModel : createModel()
|
property ComputerModel computerModel : createModel()
|
||||||
@@ -13,7 +14,7 @@ GridView {
|
|||||||
id: pcGrid
|
id: pcGrid
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
anchors.leftMargin: (parent.width % (cellWidth + anchors.rightMargin)) / 2
|
anchors.leftMargin: (parent.width % (cellWidth + anchors.rightMargin)) / 2
|
||||||
anchors.topMargin: 5
|
anchors.topMargin: 20
|
||||||
anchors.rightMargin: 5
|
anchors.rightMargin: 5
|
||||||
anchors.bottomMargin: 5
|
anchors.bottomMargin: 5
|
||||||
cellWidth: 350; cellHeight: 350;
|
cellWidth: 350; cellHeight: 350;
|
||||||
@@ -25,9 +26,17 @@ GridView {
|
|||||||
// routine to run, but only if we start as invisible
|
// routine to run, but only if we start as invisible
|
||||||
visible: false
|
visible: false
|
||||||
|
|
||||||
|
StreamingPreferences {
|
||||||
|
id: prefs
|
||||||
|
}
|
||||||
|
|
||||||
Component.onCompleted: {
|
Component.onCompleted: {
|
||||||
// Setup signals on CM
|
// Setup signals on CM
|
||||||
ComputerManager.computerAddCompleted.connect(addComplete)
|
ComputerManager.computerAddCompleted.connect(addComplete)
|
||||||
|
|
||||||
|
if (!prefs.hasAnyHardwareAcceleration()) {
|
||||||
|
noHwDecoderDialog.open()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onVisibleChanged: {
|
onVisibleChanged: {
|
||||||
@@ -72,7 +81,7 @@ GridView {
|
|||||||
return model
|
return model
|
||||||
}
|
}
|
||||||
|
|
||||||
model: createModel()
|
model: computerModel
|
||||||
|
|
||||||
delegate: Item {
|
delegate: Item {
|
||||||
width: 300; height: 300;
|
width: 300; height: 300;
|
||||||
@@ -133,44 +142,70 @@ GridView {
|
|||||||
horizontalAlignment: Text.AlignHCenter
|
horizontalAlignment: Text.AlignHCenter
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Menu {
|
||||||
|
id: pcContextMenu
|
||||||
|
MenuItem {
|
||||||
|
text: "Wake PC"
|
||||||
|
onTriggered: computerModel.wakeComputer(index)
|
||||||
|
visible: !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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
MouseArea {
|
MouseArea {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
|
acceptedButtons: Qt.LeftButton | Qt.RightButton;
|
||||||
onClicked: {
|
onClicked: {
|
||||||
if (model.addPc) {
|
if(mouse.button === Qt.LeftButton) {
|
||||||
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 {
|
else if (model.online) {
|
||||||
if (!model.busy) {
|
if (model.paired) {
|
||||||
var pin = ("0000" + Math.floor(Math.random() * 10000)).slice(-4)
|
// go to game view
|
||||||
|
var component = Qt.createComponent("AppView.qml")
|
||||||
// Stop polling, since pairing may make GFE unresponsive
|
var appView = component.createObject(stackView)
|
||||||
ComputerManager.stopPollingAsync()
|
appView.computerIndex = index
|
||||||
|
appView.objectName = model.name
|
||||||
// Kick off pairing in the background
|
stackView.push(appView)
|
||||||
computerModel.pairComputer(index, pin)
|
|
||||||
|
|
||||||
// Display the pairing dialog
|
|
||||||
pairDialog.pin = pin
|
|
||||||
pairDialog.open()
|
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
// cannot pair while something is streaming or attempting to pair
|
if (!model.busy) {
|
||||||
errorDialog.text = "This PC is currently busy. Make sure to quit any running games and try again."
|
var pin = ("0000" + Math.floor(Math.random() * 10000)).slice(-4)
|
||||||
errorDialog.open()
|
|
||||||
|
// 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()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
} else if(!model.online) {
|
||||||
|
pcContextMenu.open()
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
else {
|
else { // right click
|
||||||
// TODO: Wake on LAN and delete PC options
|
if(!model.addPc) { // but only for actual PCs, not the add-pc option
|
||||||
|
pcContextMenu.open()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -184,6 +219,19 @@ GridView {
|
|||||||
standardButtons: StandardButton.Ok
|
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 {
|
MessageDialog {
|
||||||
id: pairDialog
|
id: pairDialog
|
||||||
// don't allow edits to the rest of the window while open
|
// don't allow edits to the rest of the window while open
|
||||||
@@ -196,6 +244,19 @@ 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 {
|
Dialog {
|
||||||
id: addPcDialog
|
id: addPcDialog
|
||||||
property string label: "Enter the IP address of your GameStream PC"
|
property string label: "Enter the IP address of your GameStream PC"
|
||||||
|
|||||||
@@ -0,0 +1,86 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
+258
-118
@@ -16,14 +16,13 @@ ScrollView {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Column {
|
Column {
|
||||||
x: 10
|
padding: 10
|
||||||
y: 10
|
id: settingsColumn1
|
||||||
width: settingsPage.width
|
width: settingsPage.width / 2 - padding
|
||||||
height: 400
|
|
||||||
|
|
||||||
GroupBox {
|
GroupBox {
|
||||||
id: basicSettingsGroupBox
|
id: basicSettingsGroupBox
|
||||||
width: (parent.width - 20)
|
width: (parent.width - 2 * parent.padding)
|
||||||
padding: 12
|
padding: 12
|
||||||
title: "<font color=\"skyblue\">Basic Settings</font>"
|
title: "<font color=\"skyblue\">Basic Settings</font>"
|
||||||
font.pointSize: 12
|
font.pointSize: 12
|
||||||
@@ -35,7 +34,7 @@ ScrollView {
|
|||||||
Label {
|
Label {
|
||||||
width: parent.width
|
width: parent.width
|
||||||
id: resFPStitle
|
id: resFPStitle
|
||||||
text: qsTr("Resolution and FPS target")
|
text: qsTr("Resolution and FPS")
|
||||||
font.pointSize: 12
|
font.pointSize: 12
|
||||||
wrapMode: Text.Wrap
|
wrapMode: Text.Wrap
|
||||||
color: "white"
|
color: "white"
|
||||||
@@ -44,84 +43,113 @@ ScrollView {
|
|||||||
Label {
|
Label {
|
||||||
width: parent.width
|
width: parent.width
|
||||||
id: resFPSdesc
|
id: resFPSdesc
|
||||||
text: qsTr("Setting values too high for your device may cause lag or crashes")
|
text: qsTr("Setting values too high for your PC may cause lag, stuttering, or errors")
|
||||||
font.pointSize: 9
|
font.pointSize: 9
|
||||||
wrapMode: Text.Wrap
|
wrapMode: Text.Wrap
|
||||||
color: "white"
|
color: "white"
|
||||||
}
|
}
|
||||||
|
|
||||||
ComboBox {
|
Row {
|
||||||
// ignore setting the index at first, and actually set it when the component is loaded
|
spacing: 5
|
||||||
Component.onCompleted: {
|
|
||||||
// load the saved width/height/fps, and iterate through the ComboBox until a match is found
|
ComboBox {
|
||||||
// set it to that index.
|
// ignore setting the index at first, and actually set it when the component is loaded
|
||||||
var saved_width = prefs.width
|
Component.onCompleted: {
|
||||||
var saved_height = prefs.height
|
// load the saved width/height, and iterate through the ComboBox until a match is found
|
||||||
var saved_fps = prefs.fps
|
// and set it to that index.
|
||||||
currentIndex = 0
|
var saved_width = prefs.width
|
||||||
for(var i = 0; i < resolutionComboBox.count; i++) {
|
var saved_height = prefs.height
|
||||||
var el_width = parseInt(resolutionListModel.get(i).video_width);
|
currentIndex = 0
|
||||||
var el_height = parseInt(resolutionListModel.get(i).video_height);
|
for (var i = 0; i < resolutionComboBox.count; i++) {
|
||||||
var el_fps = parseInt(resolutionListModel.get(i).video_fps);
|
var el_width = parseInt(resolutionListModel.get(i).video_width);
|
||||||
if(saved_width === el_width &&
|
var el_height = parseInt(resolutionListModel.get(i).video_height);
|
||||||
saved_height === el_height &&
|
if (saved_width === el_width && saved_height === el_height) {
|
||||||
saved_fps === el_fps) {
|
currentIndex = i
|
||||||
currentIndex = i
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
id: resolutionComboBox
|
||||||
|
font.pointSize: 9
|
||||||
|
textRole: "text"
|
||||||
|
model: ListModel {
|
||||||
|
id: resolutionListModel
|
||||||
|
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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
id: resolutionComboBox
|
ComboBox {
|
||||||
width: Math.min(bitrateDesc.implicitWidth, parent.width)
|
// ignore setting the index at first, and actually set it when the component is loaded
|
||||||
font.pointSize: 9
|
Component.onCompleted: {
|
||||||
textRole: "text"
|
// Get the max supported FPS on this system
|
||||||
model: ListModel {
|
var max_fps = prefs.getMaximumStreamingFrameRate();
|
||||||
id: resolutionListModel
|
|
||||||
ListElement {
|
|
||||||
text: "720p 30 FPS"
|
|
||||||
video_width: "1280"
|
|
||||||
video_height: "720"
|
|
||||||
video_fps: "30"
|
|
||||||
}
|
|
||||||
ListElement {
|
|
||||||
text: "720p 60 FPS"
|
|
||||||
video_width: "1280"
|
|
||||||
video_height: "720"
|
|
||||||
video_fps: "60"
|
|
||||||
}
|
|
||||||
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);
|
// Use 64 as the cutoff for adding a separate option to
|
||||||
slider.value = prefs.bitrateKbps
|
// 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);
|
||||||
|
if (el_fps === saved_fps) {
|
||||||
|
currentIndex = i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
id: fpsComboBox
|
||||||
|
font.pointSize: 9
|
||||||
|
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
|
||||||
|
}
|
||||||
|
// ::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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -151,7 +179,7 @@ ScrollView {
|
|||||||
|
|
||||||
stepSize: 500
|
stepSize: 500
|
||||||
from : 500
|
from : 500
|
||||||
to: 100000
|
to: 150000
|
||||||
|
|
||||||
snapMode: "SnapOnRelease"
|
snapMode: "SnapOnRelease"
|
||||||
width: Math.min(bitrateDesc.implicitWidth, parent.width)
|
width: Math.min(bitrateDesc.implicitWidth, parent.width)
|
||||||
@@ -177,7 +205,7 @@ ScrollView {
|
|||||||
GroupBox {
|
GroupBox {
|
||||||
|
|
||||||
id: audioSettingsGroupBox
|
id: audioSettingsGroupBox
|
||||||
width: (parent.width - 20)
|
width: (parent.width - 2 * parent.padding)
|
||||||
padding: 12
|
padding: 12
|
||||||
title: "<font color=\"skyblue\">Audio Settings</font>"
|
title: "<font color=\"skyblue\">Audio Settings</font>"
|
||||||
font.pointSize: 12
|
font.pointSize: 12
|
||||||
@@ -186,24 +214,66 @@ ScrollView {
|
|||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
spacing: 5
|
spacing: 5
|
||||||
|
|
||||||
CheckBox {
|
Label {
|
||||||
id: surroundSoundCheck
|
width: parent.width
|
||||||
text: "<font color=\"white\">Enable 5.1 surround sound</font>"
|
id: resAudioTitle
|
||||||
font.pointSize: 12
|
text: qsTr("Audio configuration")
|
||||||
|
font.pointSize: 12
|
||||||
|
wrapMode: Text.Wrap
|
||||||
|
color: "white"
|
||||||
|
}
|
||||||
|
|
||||||
// the backend actually supports auto/stereo/5.1. We'll expose stereo/5.1
|
ComboBox {
|
||||||
checked: prefs.audioConfig === StreamingPreferences.AC_FORCE_SURROUND
|
// 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
onCheckedChanged: {
|
id: audioComboBox
|
||||||
prefs.audioConfig = checked ? StreamingPreferences.AC_FORCE_SURROUND : StreamingPreferences.AC_FORCE_STEREO
|
width: Math.min(bitrateDesc.implicitWidth, parent.width)
|
||||||
|
font.pointSize: 9
|
||||||
|
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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
padding: 10
|
||||||
|
anchors.left: settingsColumn1.right
|
||||||
|
id: settingsColumn2
|
||||||
|
width: settingsPage.width / 2 - padding
|
||||||
|
|
||||||
GroupBox {
|
GroupBox {
|
||||||
id: gamepadSettingsGroupBox
|
id: gamepadSettingsGroupBox
|
||||||
width: (parent.width - 20)
|
width: (parent.width - parent.padding)
|
||||||
padding: 12
|
padding: 12
|
||||||
title: "<font color=\"skyblue\">Gamepad Settings</font>"
|
title: "<font color=\"skyblue\">Gamepad Settings</font>"
|
||||||
font.pointSize: 12
|
font.pointSize: 12
|
||||||
@@ -221,38 +291,12 @@ ScrollView {
|
|||||||
prefs.multiController = checked
|
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 {
|
GroupBox {
|
||||||
id: hostSettingsGroupBox
|
id: hostSettingsGroupBox
|
||||||
width: (parent.width - 20)
|
width: (parent.width - parent.padding)
|
||||||
padding: 12
|
padding: 12
|
||||||
title: "<font color=\"skyblue\">Host Settings</font>"
|
title: "<font color=\"skyblue\">Host Settings</font>"
|
||||||
font.pointSize: 12
|
font.pointSize: 12
|
||||||
@@ -285,7 +329,7 @@ ScrollView {
|
|||||||
|
|
||||||
GroupBox {
|
GroupBox {
|
||||||
id: advancedSettingsGroupBox
|
id: advancedSettingsGroupBox
|
||||||
width: (parent.width - 20)
|
width: (parent.width - parent.padding)
|
||||||
padding: 12
|
padding: 12
|
||||||
title: "<font color=\"skyblue\">Advanced Settings</font>"
|
title: "<font color=\"skyblue\">Advanced Settings</font>"
|
||||||
font.pointSize: 12
|
font.pointSize: 12
|
||||||
@@ -294,11 +338,107 @@ ScrollView {
|
|||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
spacing: 5
|
spacing: 5
|
||||||
|
|
||||||
CheckBox {
|
Label {
|
||||||
id: neverDropFramesCheck
|
width: parent.width
|
||||||
text: "<font color=\"white\">Never drop frames</font>"
|
id: resVDSTitle
|
||||||
font.pointSize: 12
|
text: qsTr("Video decoder")
|
||||||
|
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_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)
|
||||||
|
font.pointSize: 9
|
||||||
|
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)
|
||||||
|
font.pointSize: 9
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ import Session 1.0
|
|||||||
|
|
||||||
Item {
|
Item {
|
||||||
property Session session
|
property Session session
|
||||||
property string appname
|
property string appName
|
||||||
property string stageText : "Starting " + appname + "..."
|
property string stageText : "Starting " + appName + "..."
|
||||||
|
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
|
|
||||||
|
|||||||
+41
-5
@@ -19,6 +19,32 @@ void AppModel::initialize(ComputerManager* computerManager, int computerIndex)
|
|||||||
m_CurrentGameId = m_Computer->currentGameId;
|
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)
|
Session* AppModel::createSessionForApp(int appIndex)
|
||||||
{
|
{
|
||||||
Q_ASSERT(appIndex < m_Apps.count());
|
Q_ASSERT(appIndex < m_Apps.count());
|
||||||
@@ -70,6 +96,11 @@ QHash<int, QByteArray> AppModel::roleNames() const
|
|||||||
return names;
|
return names;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void AppModel::quitRunningApp()
|
||||||
|
{
|
||||||
|
m_ComputerManager->quitRunningApp(m_Computer);
|
||||||
|
}
|
||||||
|
|
||||||
void AppModel::handleComputerStateChanged(NvComputer* computer)
|
void AppModel::handleComputerStateChanged(NvComputer* computer)
|
||||||
{
|
{
|
||||||
// Ignore updates for computers that aren't ours
|
// Ignore updates for computers that aren't ours
|
||||||
@@ -123,10 +154,15 @@ void AppModel::handleBoxArtLoaded(NvComputer* computer, NvApp app, QUrl /* image
|
|||||||
Q_ASSERT(computer == m_Computer);
|
Q_ASSERT(computer == m_Computer);
|
||||||
|
|
||||||
int index = m_Apps.indexOf(app);
|
int index = m_Apps.indexOf(app);
|
||||||
Q_ASSERT(index >= 0);
|
|
||||||
|
|
||||||
// Let our view know the box art data has changed for this app
|
// Make sure we're not delivering a callback to an app that's already been removed
|
||||||
emit dataChanged(createIndex(index, 0),
|
if (index >= 0) {
|
||||||
createIndex(index, 0),
|
// Let our view know the box art data has changed for this app
|
||||||
QVector<int>() << BoxArtRole);
|
emit dataChanged(createIndex(index, 0),
|
||||||
|
createIndex(index, 0),
|
||||||
|
QVector<int>() << BoxArtRole);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
qWarning() << "App not found for box art callback:" << app.name;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -25,6 +25,12 @@ public:
|
|||||||
|
|
||||||
Q_INVOKABLE Session* createSessionForApp(int appIndex);
|
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;
|
QVariant data(const QModelIndex &index, int role) const override;
|
||||||
|
|
||||||
int rowCount(const QModelIndex &parent) const override;
|
int rowCount(const QModelIndex &parent) const override;
|
||||||
|
|||||||
@@ -46,6 +46,8 @@ QVariant ComputerModel::data(const QModelIndex& index, int role) const
|
|||||||
return computer->pairState == NvComputer::PS_PAIRED;
|
return computer->pairState == NvComputer::PS_PAIRED;
|
||||||
case BusyRole:
|
case BusyRole:
|
||||||
return computer->currentGameId != 0;
|
return computer->currentGameId != 0;
|
||||||
|
case WakeableRole:
|
||||||
|
return !computer->macAddress.isEmpty();
|
||||||
case AddPcRole:
|
case AddPcRole:
|
||||||
return false;
|
return false;
|
||||||
default:
|
default:
|
||||||
@@ -74,6 +76,7 @@ QHash<int, QByteArray> ComputerModel::roleNames() const
|
|||||||
names[PairedRole] = "paired";
|
names[PairedRole] = "paired";
|
||||||
names[BusyRole] = "busy";
|
names[BusyRole] = "busy";
|
||||||
names[AddPcRole] = "addPc";
|
names[AddPcRole] = "addPc";
|
||||||
|
names[WakeableRole] = "wakeable";
|
||||||
|
|
||||||
return names;
|
return names;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ class ComputerModel : public QAbstractListModel
|
|||||||
OnlineRole,
|
OnlineRole,
|
||||||
PairedRole,
|
PairedRole,
|
||||||
BusyRole,
|
BusyRole,
|
||||||
|
WakeableRole,
|
||||||
AddPcRole
|
AddPcRole
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -80,6 +80,9 @@ ApplicationWindow {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ToolButton {
|
ToolButton {
|
||||||
|
// TODO: Implement gamepad mapping then unhide this button
|
||||||
|
visible: false
|
||||||
|
|
||||||
icon.source: "qrc:/res/ic_videogame_asset_white_48px.svg"
|
icon.source: "qrc:/res/ic_videogame_asset_white_48px.svg"
|
||||||
onClicked: navigateTo("qrc:/gui/GamepadMapper.qml", "Gamepad Mapping")
|
onClicked: navigateTo("qrc:/gui/GamepadMapper.qml", "Gamepad Mapping")
|
||||||
|
|
||||||
|
|||||||
+129
-5
@@ -2,6 +2,8 @@
|
|||||||
#include <QQmlApplicationEngine>
|
#include <QQmlApplicationEngine>
|
||||||
#include <QIcon>
|
#include <QIcon>
|
||||||
#include <QQuickStyle>
|
#include <QQuickStyle>
|
||||||
|
#include <QMutex>
|
||||||
|
#include <QtDebug>
|
||||||
|
|
||||||
// Don't let SDL hook our main function, since Qt is already
|
// Don't let SDL hook our main function, since Qt is already
|
||||||
// doing the same thing. This needs to be before any headers
|
// doing the same thing. This needs to be before any headers
|
||||||
@@ -14,8 +16,134 @@
|
|||||||
#include "streaming/session.hpp"
|
#include "streaming/session.hpp"
|
||||||
#include "settings/streamingpreferences.h"
|
#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[])
|
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);
|
QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
|
||||||
|
|
||||||
// This avoids using the default keychain for SSL, which may cause
|
// This avoids using the default keychain for SSL, which may cause
|
||||||
@@ -53,12 +181,8 @@ int main(int argc, char *argv[])
|
|||||||
if (engine.rootObjects().isEmpty())
|
if (engine.rootObjects().isEmpty())
|
||||||
return -1;
|
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();
|
SDL_SetMainReady();
|
||||||
if (SDL_Init(SDL_INIT_VIDEO |
|
if (SDL_Init(0) != 0) {
|
||||||
SDL_INIT_AUDIO |
|
|
||||||
SDL_INIT_GAMECONTROLLER) != 0) {
|
|
||||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
"SDL_Init() failed: %s",
|
"SDL_Init() failed: %s",
|
||||||
SDL_GetError());
|
SDL_GetError());
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 345 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 5.4 KiB |
@@ -6,5 +6,6 @@
|
|||||||
<file>gui/SettingsView.qml</file>
|
<file>gui/SettingsView.qml</file>
|
||||||
<file>gui/StreamSegue.qml</file>
|
<file>gui/StreamSegue.qml</file>
|
||||||
<file>gui/GamepadMapper.qml</file>
|
<file>gui/GamepadMapper.qml</file>
|
||||||
|
<file>gui/QuitSegue.qml</file>
|
||||||
</qresource>
|
</qresource>
|
||||||
</RCC>
|
</RCC>
|
||||||
Binary file not shown.
+68
-22
@@ -1,23 +1,69 @@
|
|||||||
<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">
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
<defs>
|
<svg
|
||||||
<filter id="f1" y="-40%" width="120%" height="220%">
|
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||||
<feOffset xmlns="http://www.w3.org/2000/svg" result="offOut" in="SourceAlpha" dy="-100"/>
|
xmlns:cc="http://creativecommons.org/ns#"
|
||||||
<feGaussianBlur result="blurOut" in="offOut" stdDeviation="200"/>
|
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||||
<feBlend in="SourceGraphic" in2="blurOut" mode="normal"/>
|
xmlns:svg="http://www.w3.org/2000/svg"
|
||||||
</filter>
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
</defs>
|
id="svg3761"
|
||||||
<!-- -105, 521 are within 0.5 pixel-perfect -->
|
viewBox="0 0 680 680"
|
||||||
<!-- offsetting more to show shadow -->
|
height="680"
|
||||||
<g transform="translate(-95,530) scale(0.100000,-0.100000)" filter="url(#f1)">
|
width="680"
|
||||||
<circle cx="4755" cy="1505" r="3700" fill="#565C64">
|
version="1.0">
|
||||||
</circle>
|
<metadata
|
||||||
<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"/>
|
id="metadata3767">
|
||||||
<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"/>
|
<rdf:RDF>
|
||||||
<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"/>
|
<cc:Work
|
||||||
<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"/>
|
rdf:about="">
|
||||||
<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"/>
|
<dc:format>image/svg+xml</dc:format>
|
||||||
<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"/>
|
<dc:type
|
||||||
<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"/>
|
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||||
<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"/>
|
<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" />
|
||||||
</g>
|
</g>
|
||||||
</svg>
|
</svg>
|
||||||
|
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 2.6 KiB |
@@ -1,4 +1,5 @@
|
|||||||
#include "streamingpreferences.h"
|
#include "streamingpreferences.h"
|
||||||
|
#include "streaming/session.hpp"
|
||||||
|
|
||||||
#include <QSettings>
|
#include <QSettings>
|
||||||
|
|
||||||
@@ -57,24 +58,53 @@ void StreamingPreferences::save()
|
|||||||
settings.setValue(SER_VIDEODEC, static_cast<int>(videoDecoderSelection));
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
int StreamingPreferences::getDefaultBitrate(int width, int height, int fps)
|
int StreamingPreferences::getDefaultBitrate(int width, int height, int fps)
|
||||||
{
|
{
|
||||||
if (width * height * fps <= 1280 * 720 * 30) {
|
if (width * height <= 1280 * 720) {
|
||||||
return 5000;
|
return static_cast<int>(5000 * (fps / 30.0));
|
||||||
}
|
}
|
||||||
else if (width * height * fps <= 1280 * 720 * 60) {
|
else if (width * height <= 1920 * 1080) {
|
||||||
return 10000;
|
return static_cast<int>(10000 * (fps / 30.0));
|
||||||
}
|
}
|
||||||
else if (width * height * fps <= 1920 * 1080 * 30) {
|
else if (width * height <= 2560 * 1440) {
|
||||||
return 10000;
|
return static_cast<int>(20000 * (fps / 30.0));
|
||||||
}
|
}
|
||||||
else if (width * height * fps <= 1920 * 1080 * 60) {
|
else /* if (width * height <= 3840 * 2160) */ {
|
||||||
return 20000;
|
return static_cast<int>(40000 * (fps / 30.0));
|
||||||
}
|
|
||||||
else if (width * height * fps <= 3840 * 2160 * 30) {
|
|
||||||
return 40000;
|
|
||||||
}
|
|
||||||
else /* if (width * height * fps <= 3840 * 2160 * 60) */ {
|
|
||||||
return 80000;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -14,6 +14,10 @@ public:
|
|||||||
|
|
||||||
Q_INVOKABLE void save();
|
Q_INVOKABLE void save();
|
||||||
|
|
||||||
|
Q_INVOKABLE static bool hasAnyHardwareAcceleration();
|
||||||
|
|
||||||
|
Q_INVOKABLE static int getMaximumStreamingFrameRate();
|
||||||
|
|
||||||
void reload();
|
void reload();
|
||||||
|
|
||||||
enum AudioConfig
|
enum AudioConfig
|
||||||
|
|||||||
+35
-5
@@ -23,6 +23,14 @@ int Session::sdlDetermineAudioConfiguration()
|
|||||||
{
|
{
|
||||||
SDL_AudioSpec want, have;
|
SDL_AudioSpec want, have;
|
||||||
SDL_AudioDeviceID dev;
|
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);
|
SDL_zero(want);
|
||||||
want.freq = 48000;
|
want.freq = 48000;
|
||||||
@@ -38,7 +46,8 @@ int Session::sdlDetermineAudioConfiguration()
|
|||||||
"Failed to open audio device");
|
"Failed to open audio device");
|
||||||
// We'll probably have issues during audio stream init, but we'll
|
// We'll probably have issues during audio stream init, but we'll
|
||||||
// try anyway
|
// try anyway
|
||||||
return AUDIO_CONFIGURATION_STEREO;
|
ret = AUDIO_CONFIGURATION_STEREO;
|
||||||
|
goto Exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
SDL_CloseAudioDevice(dev);
|
SDL_CloseAudioDevice(dev);
|
||||||
@@ -50,11 +59,15 @@ int Session::sdlDetermineAudioConfiguration()
|
|||||||
// We don't support quadraphonic or 7.1 surround, but SDL
|
// We don't support quadraphonic or 7.1 surround, but SDL
|
||||||
// should be able to downmix or upmix better from 5.1 than
|
// should be able to downmix or upmix better from 5.1 than
|
||||||
// from stereo, so use 5.1 in non-stereo cases.
|
// from stereo, so use 5.1 in non-stereo cases.
|
||||||
return AUDIO_CONFIGURATION_51_SURROUND;
|
ret = AUDIO_CONFIGURATION_51_SURROUND;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
return AUDIO_CONFIGURATION_STEREO;
|
ret = AUDIO_CONFIGURATION_STEREO;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Exit:
|
||||||
|
SDL_QuitSubSystem(SDL_INIT_AUDIO);
|
||||||
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
int Session::sdlAudioInit(int /* audioConfiguration */,
|
int Session::sdlAudioInit(int /* audioConfiguration */,
|
||||||
@@ -64,6 +77,14 @@ int Session::sdlAudioInit(int /* audioConfiguration */,
|
|||||||
SDL_AudioSpec want, have;
|
SDL_AudioSpec want, have;
|
||||||
int error;
|
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);
|
SDL_zero(want);
|
||||||
want.freq = opusConfig->sampleRate;
|
want.freq = opusConfig->sampleRate;
|
||||||
want.format = AUDIO_S16;
|
want.format = AUDIO_S16;
|
||||||
@@ -80,6 +101,7 @@ int Session::sdlAudioInit(int /* audioConfiguration */,
|
|||||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
"Failed to open audio device: %s",
|
"Failed to open audio device: %s",
|
||||||
SDL_GetError());
|
SDL_GetError());
|
||||||
|
SDL_QuitSubSystem(SDL_INIT_AUDIO);
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -95,6 +117,7 @@ int Session::sdlAudioInit(int /* audioConfiguration */,
|
|||||||
error);
|
error);
|
||||||
SDL_CloseAudioDevice(s_AudioDevice);
|
SDL_CloseAudioDevice(s_AudioDevice);
|
||||||
s_AudioDevice = 0;
|
s_AudioDevice = 0;
|
||||||
|
SDL_QuitSubSystem(SDL_INIT_AUDIO);
|
||||||
return -2;
|
return -2;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -104,7 +127,7 @@ int Session::sdlAudioInit(int /* audioConfiguration */,
|
|||||||
// pending data count. Get a baseline so we
|
// pending data count. Get a baseline so we
|
||||||
// can exclude that data.
|
// can exclude that data.
|
||||||
s_BaselinePendingData = 0;
|
s_BaselinePendingData = 0;
|
||||||
#ifdef _WIN32
|
#ifdef Q_OS_WIN32
|
||||||
for (int i = 0; i < 100; i++) {
|
for (int i = 0; i < 100; i++) {
|
||||||
s_BaselinePendingData = qMax(s_BaselinePendingData, SDL_GetQueuedAudioSize(s_AudioDevice));
|
s_BaselinePendingData = qMax(s_BaselinePendingData, SDL_GetQueuedAudioSize(s_AudioDevice));
|
||||||
SDL_Delay(10);
|
SDL_Delay(10);
|
||||||
@@ -119,6 +142,10 @@ int Session::sdlAudioInit(int /* audioConfiguration */,
|
|||||||
s_SampleIndex = 0;
|
s_SampleIndex = 0;
|
||||||
s_PendingDrops = s_PendingHardDrops = 0;
|
s_PendingDrops = s_PendingHardDrops = 0;
|
||||||
|
|
||||||
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Audio stream has %d channels",
|
||||||
|
opusConfig->channelCount);
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -140,7 +167,10 @@ void Session::sdlAudioCleanup()
|
|||||||
s_AudioDevice = 0;
|
s_AudioDevice = 0;
|
||||||
|
|
||||||
opus_multistream_decoder_destroy(s_OpusDecoder);
|
opus_multistream_decoder_destroy(s_OpusDecoder);
|
||||||
s_OpusDecoder = NULL;
|
s_OpusDecoder = nullptr;
|
||||||
|
|
||||||
|
SDL_QuitSubSystem(SDL_INIT_AUDIO);
|
||||||
|
SDL_assert(!SDL_WasInit(SDL_INIT_AUDIO));
|
||||||
}
|
}
|
||||||
|
|
||||||
void Session::sdlAudioDecodeAndPlaySample(char* sampleData, int sampleLength)
|
void Session::sdlAudioDecodeAndPlaySample(char* sampleData, int sampleLength)
|
||||||
|
|||||||
+103
-92
@@ -9,7 +9,7 @@
|
|||||||
#ifndef VK_F1
|
#ifndef VK_F1
|
||||||
#define VK_F1 0x70
|
#define VK_F1 0x70
|
||||||
#define VK_F13 0x7C
|
#define VK_F13 0x7C
|
||||||
#define VK_NUMPAD1 0x61
|
#define VK_NUMPAD0 0x60
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
const int SdlInputHandler::k_ButtonMap[] = {
|
const int SdlInputHandler::k_ButtonMap[] = {
|
||||||
@@ -23,6 +23,15 @@ const int SdlInputHandler::k_ButtonMap[] = {
|
|||||||
SdlInputHandler::SdlInputHandler(bool multiController)
|
SdlInputHandler::SdlInputHandler(bool multiController)
|
||||||
: m_MultiController(multiController)
|
: m_MultiController(multiController)
|
||||||
{
|
{
|
||||||
|
// 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) {
|
if (!m_MultiController) {
|
||||||
// Player 1 is always present in non-MC mode
|
// Player 1 is always present in non-MC mode
|
||||||
m_GamepadMask = 0x1;
|
m_GamepadMask = 0x1;
|
||||||
@@ -38,6 +47,9 @@ SdlInputHandler::~SdlInputHandler()
|
|||||||
SDL_GameControllerClose(m_GamepadState[i].controller);
|
SDL_GameControllerClose(m_GamepadState[i].controller);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SDL_QuitSubSystem(SDL_INIT_GAMECONTROLLER);
|
||||||
|
SDL_assert(!SDL_WasInit(SDL_INIT_GAMECONTROLLER));
|
||||||
}
|
}
|
||||||
|
|
||||||
void SdlInputHandler::handleKeyEvent(SDL_KeyboardEvent* event)
|
void SdlInputHandler::handleKeyEvent(SDL_KeyboardEvent* event)
|
||||||
@@ -51,26 +63,26 @@ void SdlInputHandler::handleKeyEvent(SDL_KeyboardEvent* event)
|
|||||||
(event->keysym.mod & KMOD_ALT) &&
|
(event->keysym.mod & KMOD_ALT) &&
|
||||||
(event->keysym.mod & KMOD_SHIFT)) {
|
(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)
|
// Check for quit combo (Ctrl+Alt+Shift+Q)
|
||||||
if (event->keysym.sym == SDLK_q) {
|
if (event->keysym.sym == SDLK_q) {
|
||||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
"Detected quit key combo");
|
"Detected quit key combo");
|
||||||
|
|
||||||
// Force raise all keys in the combo to avoid
|
// Uncapture the mouse to avoid processing any
|
||||||
// leaving them down after disconnecting
|
// further keyboard input
|
||||||
LiSendKeyboardEvent(0xA0, KEY_ACTION_UP, 0);
|
SDL_SetRelativeMouseMode(SDL_FALSE);
|
||||||
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;
|
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
|
// Push a quit event to the main loop
|
||||||
event.type = SDL_QUIT;
|
event.type = SDL_QUIT;
|
||||||
event.quit.timestamp = SDL_GetTicks();
|
event.quit.timestamp = SDL_GetTicks();
|
||||||
@@ -88,12 +100,7 @@ void SdlInputHandler::handleKeyEvent(SDL_KeyboardEvent* event)
|
|||||||
else if (event->keysym.sym == SDLK_x) {
|
else if (event->keysym.sym == SDLK_x) {
|
||||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
"Detected full-screen toggle combo");
|
"Detected full-screen toggle combo");
|
||||||
if (SDL_GetWindowFlags(Session::s_ActiveSession->m_Window) & SDL_WINDOW_FULLSCREEN) {
|
Session::s_ActiveSession->toggleFullscreen();
|
||||||
SDL_SetWindowFullscreen(Session::s_ActiveSession->m_Window, 0);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
SDL_SetWindowFullscreen(Session::s_ActiveSession->m_Window, SDL_WINDOW_FULLSCREEN);
|
|
||||||
}
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -115,195 +122,199 @@ void SdlInputHandler::handleKeyEvent(SDL_KeyboardEvent* event)
|
|||||||
modifiers |= MODIFIER_SHIFT;
|
modifiers |= MODIFIER_SHIFT;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set keycode
|
// Set keycode. We explicitly use scancode here because GFE will try to correct
|
||||||
if (event->keysym.sym >= SDLK_0 && event->keysym.sym <= SDLK_9) {
|
// for AZERTY layouts on the host but it depends on receiving VK_ values matching
|
||||||
keyCode = (event->keysym.sym - SDLK_0) + VK_0;
|
// 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;
|
||||||
}
|
}
|
||||||
else if (event->keysym.sym >= SDLK_a && event->keysym.sym <= SDLK_z) {
|
else if (event->keysym.scancode >= SDL_SCANCODE_A && event->keysym.scancode <= SDL_SCANCODE_Z) {
|
||||||
keyCode = (event->keysym.sym - SDLK_a) + VK_A;
|
keyCode = (event->keysym.scancode - SDL_SCANCODE_A) + VK_A;
|
||||||
}
|
}
|
||||||
else if (event->keysym.sym >= SDLK_F1 && event->keysym.sym <= SDLK_F12) {
|
else if (event->keysym.scancode >= SDL_SCANCODE_F1 && event->keysym.scancode <= SDL_SCANCODE_F12) {
|
||||||
keyCode = (event->keysym.sym - SDLK_F1) + VK_F1;
|
keyCode = (event->keysym.scancode - SDL_SCANCODE_F1) + VK_F1;
|
||||||
}
|
}
|
||||||
else if (event->keysym.sym >= SDLK_F13 && event->keysym.sym <= SDLK_F24) {
|
else if (event->keysym.scancode >= SDL_SCANCODE_F13 && event->keysym.scancode <= SDL_SCANCODE_F24) {
|
||||||
keyCode = (event->keysym.sym - SDLK_F13) + VK_F13;
|
keyCode = (event->keysym.scancode - SDL_SCANCODE_F13) + VK_F13;
|
||||||
}
|
}
|
||||||
else if (event->keysym.sym >= SDLK_KP_1 && event->keysym.sym <= SDLK_KP_9) {
|
else if (event->keysym.scancode >= SDL_SCANCODE_KP_1 && event->keysym.scancode <= SDL_SCANCODE_KP_9) {
|
||||||
// SDL defines SDLK_KP_0 > SDLK_KP_9, so we need to handle that manually
|
// SDL defines SDL_SCANCODE_KP_0 > SDL_SCANCODE_KP_9, so we need to handle that manually
|
||||||
keyCode = (event->keysym.sym - SDLK_KP_1) + VK_NUMPAD1;
|
keyCode = (event->keysym.scancode - SDL_SCANCODE_KP_1) + VK_NUMPAD0 + 1;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
switch (event->keysym.sym) {
|
switch (event->keysym.scancode) {
|
||||||
case SDLK_BACKSPACE:
|
case SDL_SCANCODE_BACKSPACE:
|
||||||
keyCode = 0x08;
|
keyCode = 0x08;
|
||||||
break;
|
break;
|
||||||
case SDLK_TAB:
|
case SDL_SCANCODE_TAB:
|
||||||
keyCode = 0x09;
|
keyCode = 0x09;
|
||||||
break;
|
break;
|
||||||
case SDLK_CLEAR:
|
case SDL_SCANCODE_CLEAR:
|
||||||
keyCode = 0x0C;
|
keyCode = 0x0C;
|
||||||
break;
|
break;
|
||||||
case SDLK_RETURN:
|
case SDL_SCANCODE_RETURN:
|
||||||
keyCode = 0x0D;
|
keyCode = 0x0D;
|
||||||
break;
|
break;
|
||||||
case SDLK_PAUSE:
|
case SDL_SCANCODE_PAUSE:
|
||||||
keyCode = 0x13;
|
keyCode = 0x13;
|
||||||
break;
|
break;
|
||||||
case SDLK_CAPSLOCK:
|
case SDL_SCANCODE_CAPSLOCK:
|
||||||
keyCode = 0x14;
|
keyCode = 0x14;
|
||||||
break;
|
break;
|
||||||
case SDLK_ESCAPE:
|
case SDL_SCANCODE_ESCAPE:
|
||||||
keyCode = 0x1B;
|
keyCode = 0x1B;
|
||||||
break;
|
break;
|
||||||
case SDLK_SPACE:
|
case SDL_SCANCODE_SPACE:
|
||||||
keyCode = 0x20;
|
keyCode = 0x20;
|
||||||
break;
|
break;
|
||||||
case SDLK_PAGEUP:
|
case SDL_SCANCODE_PAGEUP:
|
||||||
keyCode = 0x21;
|
keyCode = 0x21;
|
||||||
break;
|
break;
|
||||||
case SDLK_PAGEDOWN:
|
case SDL_SCANCODE_PAGEDOWN:
|
||||||
keyCode = 0x22;
|
keyCode = 0x22;
|
||||||
break;
|
break;
|
||||||
case SDLK_END:
|
case SDL_SCANCODE_END:
|
||||||
keyCode = 0x23;
|
keyCode = 0x23;
|
||||||
break;
|
break;
|
||||||
case SDLK_HOME:
|
case SDL_SCANCODE_HOME:
|
||||||
keyCode = 0x24;
|
keyCode = 0x24;
|
||||||
break;
|
break;
|
||||||
case SDLK_LEFT:
|
case SDL_SCANCODE_LEFT:
|
||||||
keyCode = 0x25;
|
keyCode = 0x25;
|
||||||
break;
|
break;
|
||||||
case SDLK_UP:
|
case SDL_SCANCODE_UP:
|
||||||
keyCode = 0x26;
|
keyCode = 0x26;
|
||||||
break;
|
break;
|
||||||
case SDLK_RIGHT:
|
case SDL_SCANCODE_RIGHT:
|
||||||
keyCode = 0x27;
|
keyCode = 0x27;
|
||||||
break;
|
break;
|
||||||
case SDLK_DOWN:
|
case SDL_SCANCODE_DOWN:
|
||||||
keyCode = 0x28;
|
keyCode = 0x28;
|
||||||
break;
|
break;
|
||||||
case SDLK_SELECT:
|
case SDL_SCANCODE_SELECT:
|
||||||
keyCode = 0x29;
|
keyCode = 0x29;
|
||||||
break;
|
break;
|
||||||
case SDLK_EXECUTE:
|
case SDL_SCANCODE_EXECUTE:
|
||||||
keyCode = 0x2B;
|
keyCode = 0x2B;
|
||||||
break;
|
break;
|
||||||
case SDLK_PRINTSCREEN:
|
case SDL_SCANCODE_PRINTSCREEN:
|
||||||
keyCode = 0x2C;
|
keyCode = 0x2C;
|
||||||
break;
|
break;
|
||||||
case SDLK_INSERT:
|
case SDL_SCANCODE_INSERT:
|
||||||
keyCode = 0x2D;
|
keyCode = 0x2D;
|
||||||
break;
|
break;
|
||||||
case SDLK_DELETE:
|
case SDL_SCANCODE_DELETE:
|
||||||
keyCode = 0x2E;
|
keyCode = 0x2E;
|
||||||
break;
|
break;
|
||||||
case SDLK_HELP:
|
case SDL_SCANCODE_HELP:
|
||||||
keyCode = 0x2F;
|
keyCode = 0x2F;
|
||||||
break;
|
break;
|
||||||
case SDLK_KP_0:
|
case SDL_SCANCODE_KP_0:
|
||||||
// See comment above about why we only handle KP_0 here
|
// See comment above about why we only handle SDL_SCANCODE_KP_0 here
|
||||||
keyCode = 0x60;
|
keyCode = VK_NUMPAD0;
|
||||||
break;
|
break;
|
||||||
case SDLK_KP_MULTIPLY:
|
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:
|
||||||
keyCode = 0x6A;
|
keyCode = 0x6A;
|
||||||
break;
|
break;
|
||||||
case SDLK_KP_PLUS:
|
case SDL_SCANCODE_KP_PLUS:
|
||||||
keyCode = 0x6B;
|
keyCode = 0x6B;
|
||||||
break;
|
break;
|
||||||
case SDLK_KP_COMMA:
|
case SDL_SCANCODE_KP_COMMA:
|
||||||
keyCode = 0x6C;
|
keyCode = 0x6C;
|
||||||
break;
|
break;
|
||||||
case SDLK_KP_MINUS:
|
case SDL_SCANCODE_KP_MINUS:
|
||||||
keyCode = 0x6D;
|
keyCode = 0x6D;
|
||||||
break;
|
break;
|
||||||
case SDLK_KP_DECIMAL:
|
case SDL_SCANCODE_KP_DECIMAL:
|
||||||
keyCode = 0x6E;
|
keyCode = 0x6E;
|
||||||
break;
|
break;
|
||||||
case SDLK_KP_DIVIDE:
|
case SDL_SCANCODE_KP_DIVIDE:
|
||||||
keyCode = 0x6F;
|
keyCode = 0x6F;
|
||||||
break;
|
break;
|
||||||
case SDLK_NUMLOCKCLEAR:
|
case SDL_SCANCODE_NUMLOCKCLEAR:
|
||||||
keyCode = 0x90;
|
keyCode = 0x90;
|
||||||
break;
|
break;
|
||||||
case SDLK_SCROLLLOCK:
|
case SDL_SCANCODE_SCROLLLOCK:
|
||||||
keyCode = 0x91;
|
keyCode = 0x91;
|
||||||
break;
|
break;
|
||||||
case SDLK_LSHIFT:
|
case SDL_SCANCODE_LSHIFT:
|
||||||
keyCode = 0xA0;
|
keyCode = 0xA0;
|
||||||
break;
|
break;
|
||||||
case SDLK_RSHIFT:
|
case SDL_SCANCODE_RSHIFT:
|
||||||
keyCode = 0xA1;
|
keyCode = 0xA1;
|
||||||
break;
|
break;
|
||||||
case SDLK_LCTRL:
|
case SDL_SCANCODE_LCTRL:
|
||||||
keyCode = 0xA2;
|
keyCode = 0xA2;
|
||||||
break;
|
break;
|
||||||
case SDLK_RCTRL:
|
case SDL_SCANCODE_RCTRL:
|
||||||
keyCode = 0xA3;
|
keyCode = 0xA3;
|
||||||
break;
|
break;
|
||||||
case SDLK_LALT:
|
case SDL_SCANCODE_LALT:
|
||||||
keyCode = 0xA4;
|
keyCode = 0xA4;
|
||||||
break;
|
break;
|
||||||
case SDLK_RALT:
|
case SDL_SCANCODE_RALT:
|
||||||
keyCode = 0xA5;
|
keyCode = 0xA5;
|
||||||
break;
|
break;
|
||||||
case SDLK_AC_BACK:
|
case SDL_SCANCODE_AC_BACK:
|
||||||
keyCode = 0xA6;
|
keyCode = 0xA6;
|
||||||
break;
|
break;
|
||||||
case SDLK_AC_FORWARD:
|
case SDL_SCANCODE_AC_FORWARD:
|
||||||
keyCode = 0xA7;
|
keyCode = 0xA7;
|
||||||
break;
|
break;
|
||||||
case SDLK_AC_REFRESH:
|
case SDL_SCANCODE_AC_REFRESH:
|
||||||
keyCode = 0xA8;
|
keyCode = 0xA8;
|
||||||
break;
|
break;
|
||||||
case SDLK_AC_STOP:
|
case SDL_SCANCODE_AC_STOP:
|
||||||
keyCode = 0xA9;
|
keyCode = 0xA9;
|
||||||
break;
|
break;
|
||||||
case SDLK_AC_SEARCH:
|
case SDL_SCANCODE_AC_SEARCH:
|
||||||
keyCode = 0xAA;
|
keyCode = 0xAA;
|
||||||
break;
|
break;
|
||||||
case SDLK_AC_BOOKMARKS:
|
case SDL_SCANCODE_AC_BOOKMARKS:
|
||||||
keyCode = 0xAB;
|
keyCode = 0xAB;
|
||||||
break;
|
break;
|
||||||
case SDLK_AC_HOME:
|
case SDL_SCANCODE_AC_HOME:
|
||||||
keyCode = 0xAC;
|
keyCode = 0xAC;
|
||||||
break;
|
break;
|
||||||
case SDLK_SEMICOLON:
|
case SDL_SCANCODE_SEMICOLON:
|
||||||
keyCode = 0xBA;
|
keyCode = 0xBA;
|
||||||
break;
|
break;
|
||||||
case SDLK_PLUS:
|
case SDL_SCANCODE_COMMA:
|
||||||
keyCode = 0xBB;
|
|
||||||
break;
|
|
||||||
case SDLK_COMMA:
|
|
||||||
keyCode = 0xBC;
|
keyCode = 0xBC;
|
||||||
break;
|
break;
|
||||||
case SDLK_MINUS:
|
case SDL_SCANCODE_MINUS:
|
||||||
keyCode = 0xBD;
|
keyCode = 0xBD;
|
||||||
break;
|
break;
|
||||||
case SDLK_PERIOD:
|
case SDL_SCANCODE_PERIOD:
|
||||||
keyCode = 0xBE;
|
keyCode = 0xBE;
|
||||||
break;
|
break;
|
||||||
case SDLK_SLASH:
|
case SDL_SCANCODE_SLASH:
|
||||||
keyCode = 0xBF;
|
keyCode = 0xBF;
|
||||||
break;
|
break;
|
||||||
case SDLK_BACKQUOTE:
|
case SDL_SCANCODE_GRAVE:
|
||||||
keyCode = 0xC0;
|
keyCode = 0xC0;
|
||||||
break;
|
break;
|
||||||
case SDLK_LEFTBRACKET:
|
case SDL_SCANCODE_LEFTBRACKET:
|
||||||
keyCode = 0xDB;
|
keyCode = 0xDB;
|
||||||
break;
|
break;
|
||||||
case SDLK_BACKSLASH:
|
case SDL_SCANCODE_BACKSLASH:
|
||||||
keyCode = 0xDC;
|
keyCode = 0xDC;
|
||||||
break;
|
break;
|
||||||
case SDLK_RIGHTBRACKET:
|
case SDL_SCANCODE_RIGHTBRACKET:
|
||||||
keyCode = 0xDD;
|
keyCode = 0xDD;
|
||||||
break;
|
break;
|
||||||
case SDLK_QUOTE:
|
case SDL_SCANCODE_APOSTROPHE:
|
||||||
keyCode = 0xDE;
|
keyCode = 0xDE;
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
"Unhandled button event: %d",
|
"Unhandled button event: %d",
|
||||||
event->keysym.sym);
|
event->keysym.scancode);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+309
-99
@@ -5,16 +5,38 @@
|
|||||||
#include <SDL.h>
|
#include <SDL.h>
|
||||||
#include "utils.h"
|
#include "utils.h"
|
||||||
|
|
||||||
#ifdef __APPLE__
|
#ifdef HAVE_FFMPEG
|
||||||
#include "video/avfoundation.h"
|
#include "video/ffmpeg.h"
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#include "video/ffmpeg.h"
|
#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 <QtEndian>
|
||||||
#include <QCoreApplication>
|
#include <QCoreApplication>
|
||||||
#include <QThreadPool>
|
#include <QThreadPool>
|
||||||
|
#include <QSvgRenderer>
|
||||||
|
#include <QPainter>
|
||||||
|
#include <QImage>
|
||||||
|
|
||||||
CONNECTION_LISTENER_CALLBACKS Session::k_ConnCallbacks = {
|
CONNECTION_LISTENER_CALLBACKS Session::k_ConnCallbacks = {
|
||||||
Session::clStageStarting,
|
Session::clStageStarting,
|
||||||
@@ -86,21 +108,22 @@ bool Session::chooseDecoder(StreamingPreferences::VideoDecoderSelection vds,
|
|||||||
SDL_Window* window, int videoFormat, int width, int height,
|
SDL_Window* window, int videoFormat, int width, int height,
|
||||||
int frameRate, IVideoDecoder*& chosenDecoder)
|
int frameRate, IVideoDecoder*& chosenDecoder)
|
||||||
{
|
{
|
||||||
#ifdef __APPLE__
|
#ifdef HAVE_SLVIDEO
|
||||||
chosenDecoder = AVFoundationDecoderFactory::createDecoder();
|
chosenDecoder = new SLVideoDecoder();
|
||||||
if (chosenDecoder->initialize(vds, window, videoFormat, width, height, frameRate)) {
|
if (chosenDecoder->initialize(vds, window, videoFormat, width, height, frameRate)) {
|
||||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
"AVFoundation-based video decoder chosen");
|
"SLVideo video decoder chosen");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
"Unable to load AVFoundation decoder");
|
"Unable to load SLVideo decoder");
|
||||||
delete chosenDecoder;
|
delete chosenDecoder;
|
||||||
chosenDecoder = nullptr;
|
chosenDecoder = nullptr;
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
#ifdef HAVE_FFMPEG
|
||||||
chosenDecoder = new FFmpegVideoDecoder();
|
chosenDecoder = new FFmpegVideoDecoder();
|
||||||
if (chosenDecoder->initialize(vds, window, videoFormat, width, height, frameRate)) {
|
if (chosenDecoder->initialize(vds, window, videoFormat, width, height, frameRate)) {
|
||||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
@@ -113,6 +136,7 @@ bool Session::chooseDecoder(StreamingPreferences::VideoDecoderSelection vds,
|
|||||||
delete chosenDecoder;
|
delete chosenDecoder;
|
||||||
chosenDecoder = nullptr;
|
chosenDecoder = nullptr;
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
// If we reach this, we didn't initialize any decoders successfully
|
// If we reach this, we didn't initialize any decoders successfully
|
||||||
return false;
|
return false;
|
||||||
@@ -120,12 +144,17 @@ bool Session::chooseDecoder(StreamingPreferences::VideoDecoderSelection vds,
|
|||||||
|
|
||||||
int Session::drSetup(int videoFormat, int width, int height, int frameRate, void *, int)
|
int Session::drSetup(int videoFormat, int width, int height, int frameRate, void *, int)
|
||||||
{
|
{
|
||||||
if (!chooseDecoder(s_ActiveSession->m_Preferences.videoDecoderSelection,
|
s_ActiveSession->m_ActiveVideoFormat = videoFormat;
|
||||||
s_ActiveSession->m_Window,
|
s_ActiveSession->m_ActiveVideoWidth = width;
|
||||||
videoFormat, width, height, frameRate,
|
s_ActiveSession->m_ActiveVideoHeight = height;
|
||||||
s_ActiveSession->m_VideoDecoder)) {
|
s_ActiveSession->m_ActiveVideoFrameRate = frameRate;
|
||||||
return -1;
|
|
||||||
}
|
// 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);
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
@@ -136,7 +165,16 @@ int Session::drSubmitDecodeUnit(PDECODE_UNIT du)
|
|||||||
// from underneath the session when we initiate destruction.
|
// from underneath the session when we initiate destruction.
|
||||||
// We need to destroy the decoder on the main thread to satisfy
|
// We need to destroy the decoder on the main thread to satisfy
|
||||||
// some API constraints (like DXVA2).
|
// some API constraints (like DXVA2).
|
||||||
|
|
||||||
SDL_AtomicLock(&s_ActiveSession->m_DecoderLock);
|
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;
|
IVideoDecoder* decoder = s_ActiveSession->m_VideoDecoder;
|
||||||
if (decoder != nullptr) {
|
if (decoder != nullptr) {
|
||||||
int ret = decoder->submitDecodeUnit(du);
|
int ret = decoder->submitDecodeUnit(du);
|
||||||
@@ -154,23 +192,35 @@ bool Session::isHardwareDecodeAvailable(StreamingPreferences::VideoDecoderSelect
|
|||||||
{
|
{
|
||||||
IVideoDecoder* decoder;
|
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);
|
SDL_Window* window = SDL_CreateWindow("", 0, 0, width, height, SDL_WINDOW_HIDDEN);
|
||||||
if (!window) {
|
if (!window) {
|
||||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
"Failed to create window for hardware decode test: %s",
|
"Failed to create window for hardware decode test: %s",
|
||||||
SDL_GetError());
|
SDL_GetError());
|
||||||
|
SDL_QuitSubSystem(SDL_INIT_VIDEO);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!chooseDecoder(vds, window, videoFormat, width, height, frameRate, decoder)) {
|
if (!chooseDecoder(vds, window, videoFormat, width, height, frameRate, decoder)) {
|
||||||
SDL_DestroyWindow(window);
|
SDL_DestroyWindow(window);
|
||||||
|
SDL_QuitSubSystem(SDL_INIT_VIDEO);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
SDL_DestroyWindow(window);
|
SDL_DestroyWindow(window);
|
||||||
|
|
||||||
bool ret = decoder->isHardwareAccelerated();
|
bool ret = decoder->isHardwareAccelerated();
|
||||||
|
|
||||||
delete decoder;
|
delete decoder;
|
||||||
|
SDL_QuitSubSystem(SDL_INIT_VIDEO);
|
||||||
|
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -179,7 +229,8 @@ Session::Session(NvComputer* computer, NvApp& app)
|
|||||||
m_App(app),
|
m_App(app),
|
||||||
m_Window(nullptr),
|
m_Window(nullptr),
|
||||||
m_VideoDecoder(nullptr),
|
m_VideoDecoder(nullptr),
|
||||||
m_DecoderLock(0)
|
m_DecoderLock(0),
|
||||||
|
m_NeedsIdr(false)
|
||||||
{
|
{
|
||||||
LiInitializeVideoCallbacks(&m_VideoCallbacks);
|
LiInitializeVideoCallbacks(&m_VideoCallbacks);
|
||||||
m_VideoCallbacks.setup = drSetup;
|
m_VideoCallbacks.setup = drSetup;
|
||||||
@@ -197,14 +248,16 @@ Session::Session(NvComputer* computer, NvApp& app)
|
|||||||
m_StreamConfig.fps = m_Preferences.fps;
|
m_StreamConfig.fps = m_Preferences.fps;
|
||||||
m_StreamConfig.bitrate = m_Preferences.bitrateKbps;
|
m_StreamConfig.bitrate = m_Preferences.bitrateKbps;
|
||||||
m_StreamConfig.hevcBitratePercentageMultiplier = 75;
|
m_StreamConfig.hevcBitratePercentageMultiplier = 75;
|
||||||
for (unsigned int i = 0; i < sizeof(m_StreamConfig.remoteInputAesKey); i++) {
|
RAND_bytes(reinterpret_cast<unsigned char*>(m_StreamConfig.remoteInputAesKey),
|
||||||
m_StreamConfig.remoteInputAesKey[i] =
|
sizeof(m_StreamConfig.remoteInputAesKey));
|
||||||
(char)(QRandomGenerator::global()->generate() % 256);
|
|
||||||
}
|
// Only the first 4 bytes are populated in the RI key IV
|
||||||
*(int*)m_StreamConfig.remoteInputAesIv = qToBigEndian(QRandomGenerator::global()->generate());
|
RAND_bytes(reinterpret_cast<unsigned char*>(m_StreamConfig.remoteInputAesIv), 4);
|
||||||
|
|
||||||
switch (m_Preferences.audioConfig)
|
switch (m_Preferences.audioConfig)
|
||||||
{
|
{
|
||||||
case StreamingPreferences::AC_AUTO:
|
case StreamingPreferences::AC_AUTO:
|
||||||
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Autodetecting audio configuration");
|
||||||
m_StreamConfig.audioConfiguration = sdlDetermineAudioConfiguration();
|
m_StreamConfig.audioConfiguration = sdlDetermineAudioConfiguration();
|
||||||
break;
|
break;
|
||||||
case StreamingPreferences::AC_FORCE_STEREO:
|
case StreamingPreferences::AC_FORCE_STEREO:
|
||||||
@@ -215,6 +268,10 @@ Session::Session(NvComputer* computer, NvApp& app)
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Audio configuration: %d",
|
||||||
|
m_StreamConfig.audioConfiguration);
|
||||||
|
|
||||||
switch (m_Preferences.videoCodecConfig)
|
switch (m_Preferences.videoCodecConfig)
|
||||||
{
|
{
|
||||||
case StreamingPreferences::VCC_AUTO:
|
case StreamingPreferences::VCC_AUTO:
|
||||||
@@ -261,23 +318,30 @@ bool Session::validateLaunch()
|
|||||||
QStringList warningList;
|
QStringList warningList;
|
||||||
|
|
||||||
if (m_StreamConfig.supportsHevc) {
|
if (m_StreamConfig.supportsHevc) {
|
||||||
if (m_Preferences.videoCodecConfig == StreamingPreferences::VCC_FORCE_HEVC ||
|
bool hevcForced = m_Preferences.videoCodecConfig == StreamingPreferences::VCC_FORCE_HEVC ||
|
||||||
m_Preferences.videoCodecConfig == StreamingPreferences::VCC_FORCE_HEVC_HDR) {
|
m_Preferences.videoCodecConfig == StreamingPreferences::VCC_FORCE_HEVC_HDR;
|
||||||
if (m_Computer->maxLumaPixelsHEVC == 0) {
|
|
||||||
emit displayLaunchWarning("Your host PC GPU doesn't support HEVC. "
|
if (!isHardwareDecodeAvailable(m_Preferences.videoDecoderSelection,
|
||||||
"A GeForce GTX 900-series (Maxwell) or later GPU is required for HEVC streaming.");
|
VIDEO_FORMAT_H265,
|
||||||
}
|
m_StreamConfig.width,
|
||||||
}
|
m_StreamConfig.height,
|
||||||
else if (!isHardwareDecodeAvailable(m_Preferences.videoDecoderSelection,
|
m_StreamConfig.fps)) {
|
||||||
VIDEO_FORMAT_H265,
|
|
||||||
m_StreamConfig.width,
|
|
||||||
m_StreamConfig.height,
|
|
||||||
m_StreamConfig.fps)) {
|
|
||||||
// NOTE: HEVC currently uses only 1 slice regardless of what
|
// NOTE: HEVC currently uses only 1 slice regardless of what
|
||||||
// we provide in CAPABILITY_SLICES_PER_FRAME(), so we should
|
// we provide in CAPABILITY_SLICES_PER_FRAME(), so we should
|
||||||
// never use it for software decoding (unless common-c starts
|
// never use it for software decoding (unless common-c starts
|
||||||
// respecting it for HEVC).
|
// respecting it for HEVC).
|
||||||
m_StreamConfig.supportsHevc = false;
|
m_StreamConfig.supportsHevc = false;
|
||||||
|
|
||||||
|
if (hevcForced) {
|
||||||
|
emit displayLaunchWarning("This PC's GPU doesn't support HEVC decoding.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hevcForced) {
|
||||||
|
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.");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -299,7 +363,7 @@ bool Session::validateLaunch()
|
|||||||
m_StreamConfig.width,
|
m_StreamConfig.width,
|
||||||
m_StreamConfig.height,
|
m_StreamConfig.height,
|
||||||
m_StreamConfig.fps)) {
|
m_StreamConfig.fps)) {
|
||||||
emit displayLaunchWarning("Your client PC GPU doesn't support HEVC Main10 decoding for HDR streaming.");
|
emit displayLaunchWarning("This PC's GPU doesn't support HEVC Main10 decoding for HDR streaming.");
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
// TODO: Also validate display capabilites
|
// TODO: Also validate display capabilites
|
||||||
@@ -339,9 +403,6 @@ class DeferredSessionCleanupTask : public QRunnable
|
|||||||
{
|
{
|
||||||
// Finish cleanup of the connection state
|
// Finish cleanup of the connection state
|
||||||
LiStopConnection();
|
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
|
// Allow another session to start now that we're cleaned up
|
||||||
Session::s_ActiveSession = nullptr;
|
Session::s_ActiveSession = nullptr;
|
||||||
@@ -349,6 +410,103 @@ class DeferredSessionCleanupTask : public QRunnable
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
void Session::getWindowDimensions(bool fullScreen,
|
||||||
|
int& x, int& y,
|
||||||
|
int& width, int& height)
|
||||||
|
{
|
||||||
|
int displayIndex = 0;
|
||||||
|
|
||||||
|
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, ¤tMode) == 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
getWindowDimensions(fullScreen, x, y, width, height);
|
||||||
|
|
||||||
|
SDL_SetWindowPosition(m_Window, x, y);
|
||||||
|
SDL_SetWindowSize(m_Window, width, height);
|
||||||
|
|
||||||
|
if (fullScreen) {
|
||||||
|
SDL_SetWindowFullscreen(m_Window, SDL_OS_FULLSCREEN_FLAG);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void Session::exec()
|
void Session::exec()
|
||||||
{
|
{
|
||||||
// Check for validation errors/warnings and emit
|
// Check for validation errors/warnings and emit
|
||||||
@@ -392,64 +550,12 @@ void Session::exec()
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
int flags = SDL_WINDOW_HIDDEN;
|
SDL_assert(!SDL_WasInit(SDL_INIT_VIDEO));
|
||||||
int width, height;
|
if (SDL_InitSubSystem(SDL_INIT_VIDEO) != 0) {
|
||||||
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, ¤t) == 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_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
"SDL_CreateWindow() failed: %s",
|
"SDL_InitSubSystem(SDL_INIT_VIDEO) failed: %s",
|
||||||
SDL_GetError());
|
SDL_GetError());
|
||||||
|
emit displayLaunchError(QString::fromLocal8Bit(SDL_GetError()));
|
||||||
s_ActiveSessionSemaphore.release();
|
s_ActiveSessionSemaphore.release();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -477,6 +583,7 @@ void Session::exec()
|
|||||||
// We already displayed an error dialog in the stage failure
|
// We already displayed an error dialog in the stage failure
|
||||||
// listener.
|
// listener.
|
||||||
s_ActiveSessionSemaphore.release();
|
s_ActiveSessionSemaphore.release();
|
||||||
|
SDL_QuitSubSystem(SDL_INIT_VIDEO);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -484,11 +591,67 @@ void Session::exec()
|
|||||||
emit connectionStarted();
|
emit connectionStarted();
|
||||||
QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents);
|
QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents);
|
||||||
|
|
||||||
// Show the streaming window
|
int x, y, width, height;
|
||||||
SDL_ShowWindow(m_Window);
|
getWindowDimensions(m_Preferences.fullScreen,
|
||||||
|
x, y, width, height);
|
||||||
|
|
||||||
// Capture the mouse
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
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.
|
||||||
SDL_SetRelativeMouseMode(SDL_TRUE);
|
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
|
// Disable the screen saver
|
||||||
SDL_DisableScreenSaver();
|
SDL_DisableScreenSaver();
|
||||||
@@ -504,7 +667,15 @@ void Session::exec()
|
|||||||
// Hijack this thread to be the SDL main thread. We have to do this
|
// Hijack this thread to be the SDL main thread. We have to do this
|
||||||
// because we want to suspend all Qt processing until the stream is over.
|
// because we want to suspend all Qt processing until the stream is over.
|
||||||
SDL_Event event;
|
SDL_Event event;
|
||||||
while (SDL_WaitEvent(&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;
|
||||||
|
}
|
||||||
switch (event.type) {
|
switch (event.type) {
|
||||||
case SDL_QUIT:
|
case SDL_QUIT:
|
||||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
@@ -529,6 +700,41 @@ void Session::exec()
|
|||||||
m_VideoDecoder->renderFrame(&event.user);
|
m_VideoDecoder->renderFrame(&event.user);
|
||||||
break;
|
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_KEYUP:
|
||||||
case SDL_KEYDOWN:
|
case SDL_KEYDOWN:
|
||||||
inputHandler.handleKeyEvent(&event.key);
|
inputHandler.handleKeyEvent(&event.key);
|
||||||
@@ -566,9 +772,6 @@ DispatchDeferredCleanup:
|
|||||||
// so we can return to the Qt GUI ASAP.
|
// so we can return to the Qt GUI ASAP.
|
||||||
SDL_SetRelativeMouseMode(SDL_FALSE);
|
SDL_SetRelativeMouseMode(SDL_FALSE);
|
||||||
SDL_EnableScreenSaver();
|
SDL_EnableScreenSaver();
|
||||||
if (m_Window != nullptr) {
|
|
||||||
SDL_HideWindow(m_Window);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Destroy the decoder, since this must be done on the main thread
|
// Destroy the decoder, since this must be done on the main thread
|
||||||
SDL_AtomicLock(&m_DecoderLock);
|
SDL_AtomicLock(&m_DecoderLock);
|
||||||
@@ -576,6 +779,13 @@ DispatchDeferredCleanup:
|
|||||||
m_VideoDecoder = nullptr;
|
m_VideoDecoder = nullptr;
|
||||||
SDL_AtomicUnlock(&m_DecoderLock);
|
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.
|
// Cleanup can take a while, so dispatch it to a worker thread.
|
||||||
// When it is complete, it will release our s_ActiveSessionSemaphore
|
// When it is complete, it will release our s_ActiveSessionSemaphore
|
||||||
// reference.
|
// reference.
|
||||||
|
|||||||
@@ -21,6 +21,10 @@ public:
|
|||||||
|
|
||||||
Q_INVOKABLE void exec();
|
Q_INVOKABLE void exec();
|
||||||
|
|
||||||
|
static
|
||||||
|
bool isHardwareDecodeAvailable(StreamingPreferences::VideoDecoderSelection vds,
|
||||||
|
int videoFormat, int width, int height, int frameRate);
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
void stageStarting(QString stage);
|
void stageStarting(QString stage);
|
||||||
|
|
||||||
@@ -39,9 +43,11 @@ private:
|
|||||||
|
|
||||||
int sdlDetermineAudioConfiguration();
|
int sdlDetermineAudioConfiguration();
|
||||||
|
|
||||||
static
|
void getWindowDimensions(bool fullScreen,
|
||||||
bool isHardwareDecodeAvailable(StreamingPreferences::VideoDecoderSelection vds,
|
int& x, int& y,
|
||||||
int videoFormat, int width, int height, int frameRate);
|
int& width, int& height);
|
||||||
|
|
||||||
|
void toggleFullscreen();
|
||||||
|
|
||||||
static
|
static
|
||||||
bool chooseDecoder(StreamingPreferences::VideoDecoderSelection vds,
|
bool chooseDecoder(StreamingPreferences::VideoDecoderSelection vds,
|
||||||
@@ -94,6 +100,12 @@ private:
|
|||||||
SDL_Window* m_Window;
|
SDL_Window* m_Window;
|
||||||
IVideoDecoder* m_VideoDecoder;
|
IVideoDecoder* m_VideoDecoder;
|
||||||
SDL_SpinLock m_DecoderLock;
|
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 SDL_AudioDeviceID s_AudioDevice;
|
||||||
static OpusMSDecoder* s_OpusDecoder;
|
static OpusMSDecoder* s_OpusDecoder;
|
||||||
|
|||||||
@@ -1,11 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include "decoder.h"
|
|
||||||
|
|
||||||
// A factory is required to avoid pulling in
|
|
||||||
// incompatible Objective-C headers.
|
|
||||||
class AVFoundationDecoderFactory {
|
|
||||||
public:
|
|
||||||
static
|
|
||||||
IVideoDecoder* createDecoder();
|
|
||||||
};
|
|
||||||
@@ -1,521 +0,0 @@
|
|||||||
#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();
|
|
||||||
}
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
#include <Initguid.h>
|
#include <initguid.h>
|
||||||
#include "dxva2.h"
|
#include "dxva2.h"
|
||||||
|
|
||||||
#include <Limelight.h>
|
#include <Limelight.h>
|
||||||
@@ -8,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(); }
|
#define SAFE_COM_RELEASE(x) if (x) { (x)->Release(); }
|
||||||
|
|
||||||
DXVA2Renderer::DXVA2Renderer() :
|
DXVA2Renderer::DXVA2Renderer() :
|
||||||
m_FrameIndex(0),
|
|
||||||
m_SurfacesUsed(0),
|
|
||||||
m_SdlRenderer(nullptr),
|
m_SdlRenderer(nullptr),
|
||||||
m_DecService(nullptr),
|
m_DecService(nullptr),
|
||||||
m_Decoder(nullptr),
|
m_Decoder(nullptr),
|
||||||
|
m_SurfacesUsed(0),
|
||||||
m_Pool(nullptr),
|
m_Pool(nullptr),
|
||||||
m_Device(nullptr),
|
m_Device(nullptr),
|
||||||
m_RenderTarget(nullptr),
|
m_RenderTarget(nullptr),
|
||||||
m_ProcService(nullptr),
|
m_ProcService(nullptr),
|
||||||
m_Processor(nullptr)
|
m_Processor(nullptr),
|
||||||
|
m_FrameIndex(0)
|
||||||
{
|
{
|
||||||
RtlZeroMemory(m_DecSurfaces, sizeof(m_DecSurfaces));
|
RtlZeroMemory(m_DecSurfaces, sizeof(m_DecSurfaces));
|
||||||
RtlZeroMemory(&m_DXVAContext, sizeof(m_DXVAContext));
|
RtlZeroMemory(&m_DXVAContext, sizeof(m_DXVAContext));
|
||||||
@@ -87,6 +87,9 @@ bool DXVA2Renderer::prepareDecoderContext(AVCodecContext* context)
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Using DXVA2 accelerated renderer");
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -101,8 +104,8 @@ int DXVA2Renderer::ffGetBuffer2(AVCodecContext* context, AVFrame* frame, int)
|
|||||||
|
|
||||||
frame->data[3] = frame->buf[0]->data;
|
frame->data[3] = frame->buf[0]->data;
|
||||||
frame->format = AV_PIX_FMT_DXVA2_VLD;
|
frame->format = AV_PIX_FMT_DXVA2_VLD;
|
||||||
frame->width = me->m_Width;
|
frame->width = me->m_VideoWidth;
|
||||||
frame->height = me->m_Height;
|
frame->height = me->m_VideoHeight;
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
@@ -126,6 +129,10 @@ bool DXVA2Renderer::initializeDecoder()
|
|||||||
{
|
{
|
||||||
HRESULT hr;
|
HRESULT hr;
|
||||||
|
|
||||||
|
if (isDecoderBlacklisted()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
hr = DXVA2CreateVideoService(m_Device, IID_IDirectXVideoDecoderService,
|
hr = DXVA2CreateVideoService(m_Device, IID_IDirectXVideoDecoderService,
|
||||||
reinterpret_cast<void**>(&m_DecService));
|
reinterpret_cast<void**>(&m_DecService));
|
||||||
if (FAILED(hr)) {
|
if (FAILED(hr)) {
|
||||||
@@ -218,8 +225,8 @@ bool DXVA2Renderer::initializeDecoder()
|
|||||||
alignment = 16;
|
alignment = 16;
|
||||||
}
|
}
|
||||||
|
|
||||||
hr = m_DecService->CreateSurface(FFALIGN(m_Width, alignment),
|
hr = m_DecService->CreateSurface(FFALIGN(m_VideoWidth, alignment),
|
||||||
FFALIGN(m_Height, alignment),
|
FFALIGN(m_VideoHeight, alignment),
|
||||||
ARRAYSIZE(m_DecSurfaces) - 1,
|
ARRAYSIZE(m_DecSurfaces) - 1,
|
||||||
m_Desc.Format,
|
m_Desc.Format,
|
||||||
D3DPOOL_DEFAULT,
|
D3DPOOL_DEFAULT,
|
||||||
@@ -262,6 +269,9 @@ bool DXVA2Renderer::initializeRenderer()
|
|||||||
D3DSURFACE_DESC renderTargetDesc;
|
D3DSURFACE_DESC renderTargetDesc;
|
||||||
m_RenderTarget->GetDesc(&renderTargetDesc);
|
m_RenderTarget->GetDesc(&renderTargetDesc);
|
||||||
|
|
||||||
|
m_DisplayWidth = renderTargetDesc.Width;
|
||||||
|
m_DisplayHeight = renderTargetDesc.Height;
|
||||||
|
|
||||||
hr = DXVA2CreateVideoService(m_Device, IID_IDirectXVideoProcessorService,
|
hr = DXVA2CreateVideoService(m_Device, IID_IDirectXVideoProcessorService,
|
||||||
reinterpret_cast<void**>(&m_ProcService));
|
reinterpret_cast<void**>(&m_ProcService));
|
||||||
|
|
||||||
@@ -302,6 +312,14 @@ bool DXVA2Renderer::initializeRenderer()
|
|||||||
caps.VideoProcessorOperations);
|
caps.VideoProcessorOperations);
|
||||||
continue;
|
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_Brightness, &m_BrightnessRange);
|
||||||
m_ProcService->GetProcAmpRange(guids[i], &m_Desc, renderTargetDesc.Format, DXVA2_ProcAmp_Contrast, &m_ContrastRange);
|
m_ProcService->GetProcAmpRange(guids[i], &m_Desc, renderTargetDesc.Format, DXVA2_ProcAmp_Contrast, &m_ContrastRange);
|
||||||
@@ -338,24 +356,117 @@ bool DXVA2Renderer::initializeRenderer()
|
|||||||
return true;
|
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)
|
bool DXVA2Renderer::initialize(SDL_Window* window, int videoFormat, int width, int height)
|
||||||
{
|
{
|
||||||
m_VideoFormat = videoFormat;
|
m_VideoFormat = videoFormat;
|
||||||
m_Width = width;
|
m_VideoWidth = width;
|
||||||
m_Height = height;
|
m_VideoHeight = height;
|
||||||
|
|
||||||
// FFmpeg will be decoding on different threads than the main thread that we're
|
// 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
|
// currently running on right now. We must set this hint so SDL will pass
|
||||||
// D3DCREATE_MULTITHREADED to IDirect3D9::CreateDevice().
|
// D3DCREATE_MULTITHREADED to IDirect3D9::CreateDevice().
|
||||||
SDL_SetHint(SDL_HINT_RENDER_DIRECT3D_THREADSAFE, "1");
|
SDL_SetHint(SDL_HINT_RENDER_DIRECT3D_THREADSAFE, "1");
|
||||||
|
|
||||||
// We want VSYNC when running in full-screen. DWM will give us
|
// We require full-screen desktop mode to avoid having to enable V-sync
|
||||||
// tear-free video when running composited (windowed) without any
|
// to synchronize frame delivery (which has a much higher latency penalty).
|
||||||
// extra latency waiting for VSYNC.
|
m_SdlRenderer = SDL_CreateRenderer(window, -1, SDL_RENDERER_ACCELERATED);
|
||||||
m_SdlRenderer = SDL_CreateRenderer(window, -1,
|
|
||||||
SDL_RENDERER_ACCELERATED |
|
|
||||||
((SDL_GetWindowFlags(window) & SDL_WINDOW_FULLSCREEN) ?
|
|
||||||
SDL_RENDERER_PRESENTVSYNC : 0));
|
|
||||||
if (!m_SdlRenderer) {
|
if (!m_SdlRenderer) {
|
||||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
"SDL_CreateRenderer() failed: %s",
|
"SDL_CreateRenderer() failed: %s",
|
||||||
@@ -365,15 +476,20 @@ bool DXVA2Renderer::initialize(SDL_Window* window, int videoFormat, int width, i
|
|||||||
|
|
||||||
m_Device = SDL_RenderGetD3D9Device(m_SdlRenderer);
|
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));
|
RtlZeroMemory(&m_Desc, sizeof(m_Desc));
|
||||||
m_Desc.SampleWidth = m_Width;
|
m_Desc.SampleWidth = m_VideoWidth;
|
||||||
m_Desc.SampleHeight = m_Height;
|
m_Desc.SampleHeight = m_VideoHeight;
|
||||||
m_Desc.SampleFormat.VideoChromaSubsampling = DXVA2_VideoChromaSubsampling_ProgressiveChroma;
|
m_Desc.SampleFormat.VideoChromaSubsampling = DXVA2_VideoChromaSubsampling_Unknown;
|
||||||
m_Desc.SampleFormat.NominalRange = DXVA2_NominalRange_0_255;
|
m_Desc.SampleFormat.NominalRange = DXVA2_NominalRange_Unknown;
|
||||||
m_Desc.SampleFormat.VideoTransferMatrix = DXVA2_VideoTransferMatrix_BT709;
|
m_Desc.SampleFormat.VideoTransferMatrix = DXVA2_VideoTransferMatrix_Unknown;
|
||||||
m_Desc.SampleFormat.VideoLighting = DXVA2_VideoLighting_dim;
|
m_Desc.SampleFormat.VideoLighting = DXVA2_VideoLighting_Unknown;
|
||||||
m_Desc.SampleFormat.VideoPrimaries = DXVA2_VideoPrimaries_BT709;
|
m_Desc.SampleFormat.VideoPrimaries = DXVA2_VideoPrimaries_Unknown;
|
||||||
m_Desc.SampleFormat.VideoTransferFunction = DXVA2_VideoTransFunc_709;
|
m_Desc.SampleFormat.VideoTransferFunction = DXVA2_VideoTransFunc_Unknown;
|
||||||
m_Desc.SampleFormat.SampleFormat = DXVA2_SampleProgressiveFrame;
|
m_Desc.SampleFormat.SampleFormat = DXVA2_SampleProgressiveFrame;
|
||||||
m_Desc.Format = (D3DFORMAT)MAKEFOURCC('N','V','1','2');
|
m_Desc.Format = (D3DFORMAT)MAKEFOURCC('N','V','1','2');
|
||||||
|
|
||||||
@@ -393,23 +509,95 @@ void DXVA2Renderer::renderFrame(AVFrame* frame)
|
|||||||
IDirect3DSurface9* surface = reinterpret_cast<IDirect3DSurface9*>(frame->data[3]);
|
IDirect3DSurface9* surface = reinterpret_cast<IDirect3DSurface9*>(frame->data[3]);
|
||||||
HRESULT hr;
|
HRESULT hr;
|
||||||
|
|
||||||
hr = m_Device->TestCooperativeLevel();
|
switch (frame->color_range) {
|
||||||
switch (hr) {
|
case AVCOL_RANGE_JPEG:
|
||||||
case D3D_OK:
|
m_Desc.SampleFormat.NominalRange = DXVA2_NominalRange_0_255;
|
||||||
|
break;
|
||||||
|
case AVCOL_RANGE_MPEG:
|
||||||
|
m_Desc.SampleFormat.NominalRange = DXVA2_NominalRange_16_235;
|
||||||
break;
|
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:
|
default:
|
||||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
m_Desc.SampleFormat.NominalRange = DXVA2_NominalRange_Unknown;
|
||||||
"Unknown D3D error: %x",
|
break;
|
||||||
hr);
|
}
|
||||||
return;
|
|
||||||
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
DXVA2_VideoSample sample = {};
|
DXVA2_VideoSample sample = {};
|
||||||
@@ -418,18 +606,31 @@ void DXVA2Renderer::renderFrame(AVFrame* frame)
|
|||||||
sample.SrcSurface = surface;
|
sample.SrcSurface = surface;
|
||||||
sample.SrcRect.right = m_Desc.SampleWidth;
|
sample.SrcRect.right = m_Desc.SampleWidth;
|
||||||
sample.SrcRect.bottom = m_Desc.SampleHeight;
|
sample.SrcRect.bottom = m_Desc.SampleHeight;
|
||||||
sample.DstRect = sample.SrcRect;
|
|
||||||
sample.SampleFormat = m_Desc.SampleFormat;
|
sample.SampleFormat = m_Desc.SampleFormat;
|
||||||
sample.PlanarAlpha = DXVA2_Fixed32OpaqueAlpha();
|
sample.PlanarAlpha = DXVA2_Fixed32OpaqueAlpha();
|
||||||
|
|
||||||
|
// Center in frame and preserve aspect ratio
|
||||||
|
double srcAspectRatio = (double)m_Desc.SampleWidth / (double)m_Desc.SampleHeight;
|
||||||
|
double dstAspectRatio = (double)m_DisplayWidth / (double)m_DisplayHeight;
|
||||||
|
if (dstAspectRatio < srcAspectRatio) {
|
||||||
|
// Greater height per width
|
||||||
|
int drawHeight = (int)(m_DisplayWidth / srcAspectRatio);
|
||||||
|
sample.DstRect.top = (m_DisplayHeight - drawHeight) / 2;
|
||||||
|
sample.DstRect.bottom = sample.DstRect.bottom + drawHeight;
|
||||||
|
sample.DstRect.right = m_DisplayWidth;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Greater width per height
|
||||||
|
int drawWidth = (int)(m_DisplayHeight * srcAspectRatio);
|
||||||
|
sample.DstRect.bottom = m_DisplayHeight;
|
||||||
|
sample.DstRect.left = (m_DisplayWidth - drawWidth) / 2;
|
||||||
|
sample.DstRect.right = sample.DstRect.left + drawWidth;
|
||||||
|
}
|
||||||
|
|
||||||
DXVA2_VideoProcessBltParams bltParams = {};
|
DXVA2_VideoProcessBltParams bltParams = {};
|
||||||
|
|
||||||
bltParams.TargetFrame = m_FrameIndex++;
|
bltParams.TargetFrame = m_FrameIndex++;
|
||||||
bltParams.TargetRect.right = m_Desc.SampleWidth;
|
bltParams.TargetRect = sample.DstRect;
|
||||||
bltParams.TargetRect.bottom = m_Desc.SampleHeight;
|
|
||||||
bltParams.BackgroundColor.Y = 0x1000;
|
|
||||||
bltParams.BackgroundColor.Cb = 0x8000;
|
|
||||||
bltParams.BackgroundColor.Cr = 0x8000;
|
|
||||||
bltParams.BackgroundColor.Alpha = 0xFFFF;
|
bltParams.BackgroundColor.Alpha = 0xFFFF;
|
||||||
|
|
||||||
bltParams.DestFormat.SampleFormat = DXVA2_SampleProgressiveFrame;
|
bltParams.DestFormat.SampleFormat = DXVA2_SampleProgressiveFrame;
|
||||||
@@ -441,12 +642,33 @@ void DXVA2Renderer::renderFrame(AVFrame* frame)
|
|||||||
|
|
||||||
bltParams.Alpha = DXVA2_Fixed32OpaqueAlpha();
|
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);
|
hr = m_Processor->VideoProcessBlt(m_RenderTarget, &bltParams, &sample, 1, nullptr);
|
||||||
|
av_frame_free(&frame);
|
||||||
|
|
||||||
if (FAILED(hr)) {
|
if (FAILED(hr)) {
|
||||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
"VideoProcessBlt() failed: %x",
|
"VideoProcessBlt() failed: %x",
|
||||||
hr);
|
hr);
|
||||||
}
|
}
|
||||||
|
|
||||||
m_Device->Present(nullptr, nullptr, nullptr, nullptr);
|
// We must try to present to trigger SDL's logic to recover the render target,
|
||||||
|
// even if VideoProcessBlt() fails.
|
||||||
|
SDL_RenderPresent(m_SdlRenderer);
|
||||||
}
|
}
|
||||||
@@ -24,6 +24,7 @@ public:
|
|||||||
private:
|
private:
|
||||||
bool initializeDecoder();
|
bool initializeDecoder();
|
||||||
bool initializeRenderer();
|
bool initializeRenderer();
|
||||||
|
bool isDecoderBlacklisted();
|
||||||
|
|
||||||
static
|
static
|
||||||
AVBufferRef* ffPoolAlloc(void* opaque, int size);
|
AVBufferRef* ffPoolAlloc(void* opaque, int size);
|
||||||
@@ -39,8 +40,11 @@ private:
|
|||||||
const enum AVPixelFormat* pixFmts);
|
const enum AVPixelFormat* pixFmts);
|
||||||
|
|
||||||
int m_VideoFormat;
|
int m_VideoFormat;
|
||||||
int m_Width;
|
int m_VideoWidth;
|
||||||
int m_Height;
|
int m_VideoHeight;
|
||||||
|
|
||||||
|
int m_DisplayWidth;
|
||||||
|
int m_DisplayHeight;
|
||||||
|
|
||||||
SDL_Renderer* m_SdlRenderer;
|
SDL_Renderer* m_SdlRenderer;
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
#include "renderer.h"
|
#include "renderer.h"
|
||||||
|
|
||||||
|
#include <Limelight.h>
|
||||||
|
|
||||||
SdlRenderer::SdlRenderer()
|
SdlRenderer::SdlRenderer()
|
||||||
: m_Renderer(nullptr),
|
: m_Renderer(nullptr),
|
||||||
m_Texture(nullptr)
|
m_Texture(nullptr)
|
||||||
@@ -21,14 +23,26 @@ SdlRenderer::~SdlRenderer()
|
|||||||
bool SdlRenderer::prepareDecoderContext(AVCodecContext*)
|
bool SdlRenderer::prepareDecoderContext(AVCodecContext*)
|
||||||
{
|
{
|
||||||
/* Nothing to do */
|
/* Nothing to do */
|
||||||
|
|
||||||
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Using SDL software renderer");
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool SdlRenderer::initialize(SDL_Window* window,
|
bool SdlRenderer::initialize(SDL_Window* window,
|
||||||
int,
|
int videoFormat,
|
||||||
int width,
|
int width,
|
||||||
int height)
|
int height)
|
||||||
{
|
{
|
||||||
|
// 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).
|
||||||
|
if (videoFormat != VIDEO_FORMAT_H264) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
m_Renderer = SDL_CreateRenderer(window, -1, SDL_RENDERER_ACCELERATED);
|
m_Renderer = SDL_CreateRenderer(window, -1, SDL_RENDERER_ACCELERATED);
|
||||||
if (!m_Renderer) {
|
if (!m_Renderer) {
|
||||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
@@ -41,6 +55,11 @@ bool SdlRenderer::initialize(SDL_Window* window,
|
|||||||
// logical rendering surface size is equal to the stream size
|
// logical rendering surface size is equal to the stream size
|
||||||
SDL_RenderSetLogicalSize(m_Renderer, width, height);
|
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,
|
m_Texture = SDL_CreateTexture(m_Renderer,
|
||||||
SDL_PIXELFORMAT_YV12,
|
SDL_PIXELFORMAT_YV12,
|
||||||
SDL_TEXTUREACCESS_STREAMING,
|
SDL_TEXTUREACCESS_STREAMING,
|
||||||
@@ -65,6 +84,10 @@ void SdlRenderer::renderFrame(AVFrame* frame)
|
|||||||
frame->linesize[1],
|
frame->linesize[1],
|
||||||
frame->data[2],
|
frame->data[2],
|
||||||
frame->linesize[2]);
|
frame->linesize[2]);
|
||||||
|
|
||||||
|
// Done with the frame now
|
||||||
|
av_frame_free(&frame);
|
||||||
|
|
||||||
SDL_RenderClear(m_Renderer);
|
SDL_RenderClear(m_Renderer);
|
||||||
SDL_RenderCopy(m_Renderer, m_Texture, nullptr, nullptr);
|
SDL_RenderCopy(m_Renderer, m_Texture, nullptr, nullptr);
|
||||||
SDL_RenderPresent(m_Renderer);
|
SDL_RenderPresent(m_Renderer);
|
||||||
|
|||||||
@@ -0,0 +1,136 @@
|
|||||||
|
#include "vaapi.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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
// Center in frame and preserve aspect ratio
|
||||||
|
int x, y, width, height;
|
||||||
|
double srcAspectRatio = (double)m_VideoWidth / (double)m_VideoHeight;
|
||||||
|
double dstAspectRatio = (double)m_DisplayWidth / (double)m_DisplayHeight;
|
||||||
|
if (dstAspectRatio < srcAspectRatio) {
|
||||||
|
// Greater height per width
|
||||||
|
int drawHeight = (int)(m_DisplayWidth / srcAspectRatio);
|
||||||
|
y = (m_DisplayHeight - drawHeight) / 2;
|
||||||
|
height = drawHeight;
|
||||||
|
x = 0;
|
||||||
|
width = m_DisplayWidth;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Greater width per height
|
||||||
|
int drawWidth = (int)(m_DisplayHeight * srcAspectRatio);
|
||||||
|
y = 0;
|
||||||
|
height = m_DisplayHeight;
|
||||||
|
x = (m_DisplayWidth - drawWidth) / 2;
|
||||||
|
width = drawWidth;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_vaPutSurface(vaDeviceContext->display,
|
||||||
|
surface,
|
||||||
|
m_XWindow,
|
||||||
|
0, 0,
|
||||||
|
m_VideoWidth, m_VideoHeight,
|
||||||
|
x, y,
|
||||||
|
width, height,
|
||||||
|
NULL, 0, 0);
|
||||||
|
|
||||||
|
av_frame_free(&frame);
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
#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;
|
||||||
|
};
|
||||||
|
|
||||||
@@ -7,9 +7,14 @@
|
|||||||
#include <SDL_syswm.h>
|
#include <SDL_syswm.h>
|
||||||
#include <Limelight.h>
|
#include <Limelight.h>
|
||||||
|
|
||||||
|
#include <QQueue>
|
||||||
|
|
||||||
#import <Cocoa/Cocoa.h>
|
#import <Cocoa/Cocoa.h>
|
||||||
#import <VideoToolbox/VideoToolbox.h>
|
#import <VideoToolbox/VideoToolbox.h>
|
||||||
#import <AVFoundation/AVFoundation.h>
|
#import <AVFoundation/AVFoundation.h>
|
||||||
|
#import <CoreVideo/CoreVideo.h>
|
||||||
|
|
||||||
|
#define FRAME_HISTORY_ENTRIES 8
|
||||||
|
|
||||||
class VTRenderer : public IFFmpegRenderer
|
class VTRenderer : public IFFmpegRenderer
|
||||||
{
|
{
|
||||||
@@ -18,9 +23,10 @@ public:
|
|||||||
: m_HwContext(nullptr),
|
: m_HwContext(nullptr),
|
||||||
m_DisplayLayer(nullptr),
|
m_DisplayLayer(nullptr),
|
||||||
m_FormatDesc(nullptr),
|
m_FormatDesc(nullptr),
|
||||||
m_View(nullptr)
|
m_View(nullptr),
|
||||||
|
m_DisplayLink(nullptr),
|
||||||
|
m_FrameQueueLock(0)
|
||||||
{
|
{
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
virtual ~VTRenderer()
|
virtual ~VTRenderer()
|
||||||
@@ -32,8 +38,132 @@ public:
|
|||||||
if (m_FormatDesc != nullptr) {
|
if (m_FormatDesc != nullptr) {
|
||||||
CFRelease(m_FormatDesc);
|
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,
|
virtual bool initialize(SDL_Window* window,
|
||||||
int videoFormat,
|
int videoFormat,
|
||||||
@@ -113,60 +243,34 @@ public:
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
CVDisplayLinkCreateWithActiveCGDisplays(&m_DisplayLink);
|
||||||
|
CVDisplayLinkSetOutputCallback(m_DisplayLink, displayLinkOutputCallback, this);
|
||||||
|
CVDisplayLinkStart(m_DisplayLink);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
virtual bool prepareDecoderContext(AVCodecContext* context) override
|
virtual bool prepareDecoderContext(AVCodecContext* context) override
|
||||||
{
|
{
|
||||||
context->hw_device_ctx = av_buffer_ref(m_HwContext);
|
context->hw_device_ctx = av_buffer_ref(m_HwContext);
|
||||||
|
|
||||||
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Using VideoToolbox accelerated renderer");
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
virtual void renderFrame(AVFrame* frame) override
|
virtual void renderFrame(AVFrame* frame) override
|
||||||
{
|
{
|
||||||
CVPixelBufferRef pixBuf = reinterpret_cast<CVPixelBufferRef>(frame->data[3]);
|
|
||||||
OSStatus status;
|
|
||||||
|
|
||||||
if (m_DisplayLayer.status == AVQueuedSampleBufferRenderingStatusFailed) {
|
if (m_DisplayLayer.status == AVQueuedSampleBufferRenderingStatusFailed) {
|
||||||
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
"Resetting failed AVSampleBufferDisplay layer");
|
"Resetting failed AVSampleBufferDisplay layer");
|
||||||
setupDisplayLayer();
|
setupDisplayLayer();
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the format has changed or doesn't exist yet, construct it with the
|
SDL_AtomicLock(&m_FrameQueueLock);
|
||||||
// pixel buffer data
|
m_FrameQueue.enqueue(frame);
|
||||||
if (!m_FormatDesc || !CMVideoFormatDescriptionMatchesImageBuffer(m_FormatDesc, pixBuf)) {
|
SDL_AtomicUnlock(&m_FrameQueueLock);
|
||||||
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:
|
private:
|
||||||
@@ -192,6 +296,10 @@ private:
|
|||||||
AVSampleBufferDisplayLayer* m_DisplayLayer;
|
AVSampleBufferDisplayLayer* m_DisplayLayer;
|
||||||
CMVideoFormatDescriptionRef m_FormatDesc;
|
CMVideoFormatDescriptionRef m_FormatDesc;
|
||||||
NSView* m_View;
|
NSView* m_View;
|
||||||
|
CVDisplayLinkRef m_DisplayLink;
|
||||||
|
QQueue<AVFrame*> m_FrameQueue;
|
||||||
|
QQueue<int> m_FrameQueueHistory;
|
||||||
|
SDL_SpinLock m_FrameQueueLock;
|
||||||
};
|
};
|
||||||
|
|
||||||
IFFmpegRenderer* VTRendererFactory::createRenderer() {
|
IFFmpegRenderer* VTRendererFactory::createRenderer() {
|
||||||
|
|||||||
@@ -1,10 +1,18 @@
|
|||||||
#include <Limelight.h>
|
#include <Limelight.h>
|
||||||
#include "ffmpeg.h"
|
#include "ffmpeg.h"
|
||||||
|
|
||||||
#ifdef _WIN32
|
#ifdef Q_OS_WIN32
|
||||||
#include "ffmpeg-renderers/dxva2.h"
|
#include "ffmpeg-renderers/dxva2.h"
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
#ifdef Q_OS_DARWIN
|
||||||
|
#include "ffmpeg-renderers/vt.h"
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifdef HAVE_LIBVA
|
||||||
|
#include "ffmpeg-renderers/vaapi.h"
|
||||||
|
#endif
|
||||||
|
|
||||||
bool FFmpegVideoDecoder::chooseDecoder(
|
bool FFmpegVideoDecoder::chooseDecoder(
|
||||||
StreamingPreferences::VideoDecoderSelection vds,
|
StreamingPreferences::VideoDecoderSelection vds,
|
||||||
SDL_Window* window,
|
SDL_Window* window,
|
||||||
@@ -56,10 +64,20 @@ bool FFmpegVideoDecoder::chooseDecoder(
|
|||||||
|
|
||||||
// Look for acceleration types we support
|
// Look for acceleration types we support
|
||||||
switch (config->device_type) {
|
switch (config->device_type) {
|
||||||
#ifdef _WIN32
|
#ifdef Q_OS_WIN32
|
||||||
case AV_HWDEVICE_TYPE_DXVA2:
|
case AV_HWDEVICE_TYPE_DXVA2:
|
||||||
newRenderer = new DXVA2Renderer();
|
newRenderer = new DXVA2Renderer();
|
||||||
break;
|
break;
|
||||||
|
#endif
|
||||||
|
#ifdef Q_OS_DARWIN
|
||||||
|
case AV_HWDEVICE_TYPE_VIDEOTOOLBOX:
|
||||||
|
newRenderer = VTRendererFactory::createRenderer();
|
||||||
|
break;
|
||||||
|
#endif
|
||||||
|
#ifdef HAVE_LIBVA
|
||||||
|
case AV_HWDEVICE_TYPE_VAAPI:
|
||||||
|
newRenderer = new VAAPIRenderer();
|
||||||
|
break;
|
||||||
#endif
|
#endif
|
||||||
default:
|
default:
|
||||||
continue;
|
continue;
|
||||||
@@ -89,6 +107,7 @@ FFmpegVideoDecoder::FFmpegVideoDecoder()
|
|||||||
m_Renderer(nullptr)
|
m_Renderer(nullptr)
|
||||||
{
|
{
|
||||||
av_init_packet(&m_Pkt);
|
av_init_packet(&m_Pkt);
|
||||||
|
SDL_AtomicSet(&m_QueuedFrames, 0);
|
||||||
|
|
||||||
// Use linear filtering when renderer scaling is required
|
// Use linear filtering when renderer scaling is required
|
||||||
SDL_SetHint(SDL_HINT_RENDER_SCALE_QUALITY, "1");
|
SDL_SetHint(SDL_HINT_RENDER_SCALE_QUALITY, "1");
|
||||||
@@ -96,14 +115,31 @@ FFmpegVideoDecoder::FFmpegVideoDecoder()
|
|||||||
|
|
||||||
FFmpegVideoDecoder::~FFmpegVideoDecoder()
|
FFmpegVideoDecoder::~FFmpegVideoDecoder()
|
||||||
{
|
{
|
||||||
avcodec_close(m_VideoDecoderCtx);
|
// Drop any frames still queued to ensure
|
||||||
av_free(m_VideoDecoderCtx);
|
// they are properly freed.
|
||||||
m_VideoDecoderCtx = nullptr;
|
SDL_Event event;
|
||||||
|
|
||||||
m_HwDecodeCfg = nullptr;
|
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);
|
||||||
|
|
||||||
delete m_Renderer;
|
delete m_Renderer;
|
||||||
m_Renderer = nullptr;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool FFmpegVideoDecoder::initialize(
|
bool FFmpegVideoDecoder::initialize(
|
||||||
@@ -208,14 +244,9 @@ int FFmpegVideoDecoder::submitDecodeUnit(PDECODE_UNIT du)
|
|||||||
|
|
||||||
err = avcodec_receive_frame(m_VideoDecoderCtx, frame);
|
err = avcodec_receive_frame(m_VideoDecoderCtx, frame);
|
||||||
if (err == 0) {
|
if (err == 0) {
|
||||||
SDL_Event event;
|
// Queue the frame for rendering from the main thread
|
||||||
|
SDL_AtomicIncRef(&m_QueuedFrames);
|
||||||
event.type = SDL_USEREVENT;
|
queueFrame(frame);
|
||||||
event.user.code = SDL_CODE_FRAME_READY;
|
|
||||||
event.user.data1 = frame;
|
|
||||||
|
|
||||||
// The main thread will handle freeing this
|
|
||||||
SDL_PushEvent(&event);
|
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
av_frame_free(&frame);
|
av_frame_free(&frame);
|
||||||
@@ -229,7 +260,7 @@ void FFmpegVideoDecoder::renderFrame(SDL_UserEvent* event)
|
|||||||
{
|
{
|
||||||
AVFrame* frame = reinterpret_cast<AVFrame*>(event->data1);
|
AVFrame* frame = reinterpret_cast<AVFrame*>(event->data1);
|
||||||
m_Renderer->renderFrame(frame);
|
m_Renderer->renderFrame(frame);
|
||||||
av_frame_free(&frame);
|
SDL_AtomicDecRef(&m_QueuedFrames);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Called on main thread
|
// Called on main thread
|
||||||
@@ -237,4 +268,5 @@ void FFmpegVideoDecoder::dropFrame(SDL_UserEvent* event)
|
|||||||
{
|
{
|
||||||
AVFrame* frame = reinterpret_cast<AVFrame*>(event->data1);
|
AVFrame* frame = reinterpret_cast<AVFrame*>(event->data1);
|
||||||
av_frame_free(&frame);
|
av_frame_free(&frame);
|
||||||
|
SDL_AtomicDecRef(&m_QueuedFrames);
|
||||||
}
|
}
|
||||||
@@ -38,4 +38,5 @@ private:
|
|||||||
QByteArray m_DecodeBuffer;
|
QByteArray m_DecodeBuffer;
|
||||||
const AVCodecHWConfig* m_HwDecodeCfg;
|
const AVCodecHWConfig* m_HwDecodeCfg;
|
||||||
IFFmpegRenderer* m_Renderer;
|
IFFmpegRenderer* m_Renderer;
|
||||||
|
SDL_atomic_t m_QueuedFrames;
|
||||||
};
|
};
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
#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;
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
#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
@@ -1,5 +1,5 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#define THROW_BAD_ALLOC_IF_NULL(x) \
|
#define THROW_BAD_ALLOC_IF_NULL(x) \
|
||||||
if ((x) == nullptr) throw new std::bad_alloc()
|
if ((x) == nullptr) throw std::bad_alloc()
|
||||||
|
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
SOURCES = main.cpp
|
||||||
|
LIBS += -lSLVideo
|
||||||
|
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
#include <SLVideo.h>
|
||||||
|
|
||||||
|
int main()
|
||||||
|
{
|
||||||
|
SLVideo_CreateContext();
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
Versions/Current/Headers
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
Versions/Current/Resources
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
Versions/Current/SDL2
|
||||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
Loaded 100 of 213 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user