Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
860d317330 | ||
|
|
bab11a731e | ||
|
|
df9e7bead0 | ||
|
|
962dc847e7 | ||
|
|
566181e8d7 | ||
|
|
5f5f835b2e | ||
|
|
c0e697eb58 | ||
|
|
d3c2f50b20 | ||
|
|
f1ee1eeb63 | ||
|
|
bb18519e44 | ||
|
|
8e2ad133ea | ||
|
|
5e82e8413f | ||
|
|
917f6e5ee9 | ||
|
|
e1ca6e644e | ||
|
|
f1e89242cb | ||
|
|
91145cde0f | ||
|
|
8ae01fae3f | ||
|
|
4e60ccb2ff | ||
|
|
186052f21d | ||
|
|
0fb3bb0727 | ||
|
|
8f61a11452 | ||
|
|
4b1d3b0125 | ||
|
|
3f81d055af | ||
|
|
c61182bcb2 | ||
|
|
2fdfb3e998 | ||
|
|
9beb51ecac | ||
|
|
7be167856e | ||
|
|
e1d68ef097 | ||
|
|
5ec256a734 | ||
|
|
e4d7d190e2 | ||
|
|
b6763bb9f0 | ||
|
|
6d9c0c96c9 | ||
|
|
73dd5630df | ||
|
|
b76645e26b | ||
|
|
9cc20c27ab | ||
|
|
36b42f6e50 | ||
|
|
45ebf2ca7d | ||
|
|
2140c94cd1 | ||
|
|
470769e910 | ||
|
|
493165f57f | ||
|
|
1dcf55515b | ||
|
|
b04bc5117d | ||
|
|
5cbb38091b | ||
|
|
b076744f00 | ||
|
|
723e815546 | ||
|
|
ca72cbb135 | ||
|
|
1e8c2b179a | ||
|
|
f2ed3a8502 | ||
|
|
2bcc15e1d2 | ||
|
|
4935baa72a | ||
|
|
10cfaa2a5f | ||
|
|
3117e360f4 | ||
|
|
48a770edec | ||
|
|
fca402138f | ||
|
|
2853e498b0 | ||
|
|
509dba2264 | ||
|
|
b8dd131af8 | ||
|
|
0c18bcdd5e | ||
|
|
624578f286 | ||
|
|
c18e99c9ec | ||
|
|
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
@@ -0,0 +1,8 @@
|
|||||||
|
# ProBot No Response (https://probot.github.io/apps/no-response/)
|
||||||
|
|
||||||
|
daysUntilClose: 7
|
||||||
|
responseRequiredLabel: 'need more info'
|
||||||
|
closeComment: >
|
||||||
|
This issue has been automatically closed because there was no response to a
|
||||||
|
request for more information from the issue opener. Please leave a comment or
|
||||||
|
open a new issue if you have additional information related to this issue.
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
# ProBot Stale (https://probot.github.io/apps/stale/)
|
||||||
|
|
||||||
|
daysUntilStale: 90
|
||||||
|
daysUntilClose: 7
|
||||||
|
exemptLabels:
|
||||||
|
- accepted
|
||||||
|
- bug
|
||||||
|
- enhancement
|
||||||
|
- meta
|
||||||
|
staleLabel: stale
|
||||||
|
markComment: >
|
||||||
|
This issue has been automatically marked as stale because it has not had
|
||||||
|
recent activity. It will be closed if no further activity occurs.
|
||||||
|
closeComment: false
|
||||||
@@ -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
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
language: cpp
|
||||||
|
|
||||||
|
git:
|
||||||
|
depth: 1
|
||||||
|
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- os: osx
|
||||||
|
compiler: clang
|
||||||
|
- os: linux
|
||||||
|
sudo: required
|
||||||
|
dist: xenial
|
||||||
|
compiler: gcc
|
||||||
|
addons:
|
||||||
|
apt:
|
||||||
|
sources:
|
||||||
|
- sourceline: 'ppa:beineri/opt-qt596-xenial'
|
||||||
|
- sourceline: 'ppa:jonathonf/ffmpeg-4'
|
||||||
|
packages:
|
||||||
|
- qt59base
|
||||||
|
- qt59quickcontrols2
|
||||||
|
- qt59svg
|
||||||
|
- libesd0-dev
|
||||||
|
- libgl-dev
|
||||||
|
- libegl1-mesa-dev
|
||||||
|
- libxkbcommon-dev
|
||||||
|
- wayland-protocols
|
||||||
|
- libopus-dev
|
||||||
|
- libavcodec-dev
|
||||||
|
- libavutil-dev
|
||||||
|
- libva-dev
|
||||||
|
- libvdpau-dev
|
||||||
|
|
||||||
|
install:
|
||||||
|
- '[ "$TRAVIS_OS_NAME" != osx ] || brew install qt5'
|
||||||
|
- '[ "$TRAVIS_OS_NAME" != osx ] || brew link --force qt5'
|
||||||
|
- if [[ "$TRAVIS_OS_NAME" = "linux" ]]; then export SDL2_VER=2.0.8 && wget https://www.libsdl.org/release/SDL2-$SDL2_VER.tar.gz && tar -xvf SDL2-$SDL2_VER.tar.gz && cd SDL2-$SDL2_VER && ./configure && make && sudo make install && cd ..; fi
|
||||||
|
|
||||||
|
before_script:
|
||||||
|
- '[ "$TRAVIS_OS_NAME" != "linux" ] || source /opt/qt59/bin/qt59-env.sh'
|
||||||
|
- qmake -v
|
||||||
|
- '[[ "$TRAVIS_OS_NAME" != linux || "$CXX" != clang++ ]] || export QMAKESPEC=linux-clang'
|
||||||
|
- '[[ "$TRAVIS_OS_NAME" != linux || "$CXX" != g++ ]] || export QMAKESPEC=linux-g++'
|
||||||
|
- '[ "$TRAVIS_OS_NAME" != osx ] || export QMAKESPEC=macx-clang'
|
||||||
|
|
||||||
|
script:
|
||||||
|
- qmake moonlight-qt.pro -spec $QMAKESPEC
|
||||||
|
- make debug
|
||||||
|
- make release
|
||||||
@@ -1,20 +1,34 @@
|
|||||||
# 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.
|
See the [releases page](https://github.com/moonlight-stream/moonlight-qt/releases) for download links for Windows and Mac. For Linux, we're on [Flathub](https://flathub.org/apps/details/com.moonlight_stream.Moonlight).
|
||||||
|
|
||||||
|
You can follow development on our [Discord server](https://discord.gg/6ERtzFY).
|
||||||
|
|
||||||
|
[](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
|
||||||
3. Run `git submodule update --init --recursive` from within `moonlight-qt/`
|
- On Windows, install Visual Studio 2017 and select MSVC 2017 toolchain during Qt installation
|
||||||
4. Open the project in Qt Creator or build from qmake on the command line
|
- On Mac, install the latest version of XCode
|
||||||
|
- On Linux, install your distro equivalents of: `openssl-devel qt5-devel SDL2-devel ffmpeg-devel qt5-qtquickcontrols2-devel libva-devel libvdpau-devel`
|
||||||
|
2. Run `git submodule update --init --recursive` from within `moonlight-qt/`
|
||||||
|
3. Open the project in Qt Creator or build from qmake on the command line
|
||||||
|
|
||||||
## 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
|
||||||
|
|||||||
@@ -3,24 +3,30 @@
|
|||||||
<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.11</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.2.0</string>
|
||||||
|
<key>CFBundleShortVersionString</key>
|
||||||
|
<string>0.2.0</string>
|
||||||
|
<key>CFBundleDisplayName</key>
|
||||||
|
<string>Moonlight</string>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
@@ -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,41 @@ 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
|
||||||
|
|
||||||
|
packagesExist(libavcodec) {
|
||||||
|
PKGCONFIG += libavcodec libavutil
|
||||||
|
CONFIG += ffmpeg
|
||||||
|
|
||||||
|
packagesExist(libva) {
|
||||||
|
packagesExist(libva-x11) {
|
||||||
|
CONFIG += libva-x11
|
||||||
|
}
|
||||||
|
packagesExist(libva-wayland) {
|
||||||
|
CONFIG += libva-wayland
|
||||||
|
}
|
||||||
|
CONFIG += libva
|
||||||
|
}
|
||||||
|
|
||||||
|
packagesExist(vdpau) {
|
||||||
|
CONFIG += libvdpau
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
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 CoreGraphics -framework CoreMedia -framework AppKit
|
LIBS += -lobjc -framework VideoToolbox -framework AVFoundation -framework CoreVideo -framework CoreGraphics -framework CoreMedia -framework AppKit
|
||||||
|
CONFIG += ffmpeg
|
||||||
}
|
}
|
||||||
|
|
||||||
SOURCES += \
|
SOURCES += \
|
||||||
@@ -55,17 +85,10 @@ 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
|
streaming/streamutils.cpp \
|
||||||
|
backend/autoupdatechecker.cpp
|
||||||
win32 {
|
|
||||||
SOURCES += streaming/video/ffmpeg-renderers/dxva2.cpp
|
|
||||||
}
|
|
||||||
macx {
|
|
||||||
SOURCES += streaming/video/ffmpeg-renderers/vt.mm
|
|
||||||
}
|
|
||||||
|
|
||||||
HEADERS += \
|
HEADERS += \
|
||||||
utils.h \
|
utils.h \
|
||||||
@@ -80,13 +103,66 @@ HEADERS += \
|
|||||||
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/streamutils.h \
|
||||||
streaming/video/ffmpeg-renderers/renderer.h
|
backend/autoupdatechecker.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)
|
||||||
|
|
||||||
|
PKGCONFIG += libva
|
||||||
|
DEFINES += HAVE_LIBVA
|
||||||
|
SOURCES += streaming/video/ffmpeg-renderers/vaapi.cpp
|
||||||
|
HEADERS += streaming/video/ffmpeg-renderers/vaapi.h
|
||||||
|
}
|
||||||
|
libva-x11 {
|
||||||
|
message(VAAPI X11 support enabled)
|
||||||
|
|
||||||
|
PKGCONFIG += libva-x11
|
||||||
|
DEFINES += HAVE_LIBVA_X11
|
||||||
|
}
|
||||||
|
libva-wayland {
|
||||||
|
message(VAAPI Wayland support enabled)
|
||||||
|
|
||||||
|
PKGCONFIG += libva-wayland
|
||||||
|
DEFINES += HAVE_LIBVA_WAYLAND
|
||||||
|
}
|
||||||
|
libvdpau {
|
||||||
|
message(VDPAU renderer selected)
|
||||||
|
|
||||||
|
DEFINES += HAVE_LIBVDPAU
|
||||||
|
SOURCES += streaming/video/ffmpeg-renderers/vdpau.cpp
|
||||||
|
HEADERS += streaming/video/ffmpeg-renderers/vdpau.h
|
||||||
|
}
|
||||||
|
config_SLVideo {
|
||||||
|
message(SLVideo decoder/renderer selected)
|
||||||
|
|
||||||
|
DEFINES += HAVE_SLVIDEO
|
||||||
|
LIBS += -lSLVideo
|
||||||
|
SOURCES += streaming/video/sl.cpp
|
||||||
|
HEADERS += streaming/video/sl.h
|
||||||
|
}
|
||||||
win32 {
|
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 {
|
||||||
|
message(VideoToolbox renderer selected)
|
||||||
|
|
||||||
|
SOURCES += streaming/video/ffmpeg-renderers/vt.mm
|
||||||
HEADERS += streaming/video/ffmpeg-renderers/vt.h
|
HEADERS += streaming/video/ffmpeg-renderers/vt.h
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -107,13 +183,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 +190,37 @@ 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.2.0
|
||||||
|
DEFINES += VERSION_STR=\\\"0.2.0\\\"
|
||||||
@@ -0,0 +1,142 @@
|
|||||||
|
#include "autoupdatechecker.h"
|
||||||
|
|
||||||
|
#include <QNetworkReply>
|
||||||
|
#include <QJsonDocument>
|
||||||
|
#include <QJsonArray>
|
||||||
|
#include <QJsonObject>
|
||||||
|
|
||||||
|
AutoUpdateChecker::AutoUpdateChecker(QObject *parent) :
|
||||||
|
QObject(parent),
|
||||||
|
m_Nam(this)
|
||||||
|
{
|
||||||
|
// Never communicate over HTTP
|
||||||
|
m_Nam.setStrictTransportSecurityEnabled(true);
|
||||||
|
|
||||||
|
connect(&m_Nam, SIGNAL(finished(QNetworkReply*)),
|
||||||
|
this, SLOT(handleUpdateCheckRequestFinished(QNetworkReply*)));
|
||||||
|
|
||||||
|
QString currentVersion(VERSION_STR);
|
||||||
|
qDebug() << "Current Moonlight version:" << currentVersion;
|
||||||
|
parseStringToVersionQuad(currentVersion, m_CurrentVersionQuad);
|
||||||
|
|
||||||
|
// Should at least have a 1.0-style version number
|
||||||
|
Q_ASSERT(m_CurrentVersionQuad.count() > 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
void AutoUpdateChecker::start()
|
||||||
|
{
|
||||||
|
#if defined(Q_OS_WIN32) || defined(Q_OS_DARWIN) // Only run update checker on platforms without auto-update
|
||||||
|
// We'll get a callback when this is finished
|
||||||
|
QUrl url("https://moonlight-stream.com/updates/qt.json");
|
||||||
|
m_Nam.get(QNetworkRequest(url));
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
void AutoUpdateChecker::parseStringToVersionQuad(QString& string, QVector<int>& version)
|
||||||
|
{
|
||||||
|
QStringList list = string.split('.');
|
||||||
|
for (const QString& component : list) {
|
||||||
|
version.append(component.toInt());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void AutoUpdateChecker::handleUpdateCheckRequestFinished(QNetworkReply* reply)
|
||||||
|
{
|
||||||
|
Q_ASSERT(reply->isFinished());
|
||||||
|
|
||||||
|
if (reply->error() == QNetworkReply::NoError) {
|
||||||
|
QTextStream stream(reply);
|
||||||
|
stream.setCodec("UTF-8");
|
||||||
|
|
||||||
|
// Read all data and queue the reply for deletion
|
||||||
|
QString jsonString = stream.readAll();
|
||||||
|
reply->deleteLater();
|
||||||
|
|
||||||
|
QJsonParseError error;
|
||||||
|
QJsonDocument jsonDoc = QJsonDocument::fromJson(jsonString.toUtf8(), &error);
|
||||||
|
if (jsonDoc.isNull()) {
|
||||||
|
qWarning() << "Update manifest malformed:" << error.errorString();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
QJsonArray array = jsonDoc.array();
|
||||||
|
if (array.isEmpty()) {
|
||||||
|
qWarning() << "Update manifest doesn't contain an array";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (QJsonValueRef updateEntry : array) {
|
||||||
|
if (updateEntry.isObject()) {
|
||||||
|
QJsonObject updateObj = updateEntry.toObject();
|
||||||
|
if (!updateObj.contains("platform") ||
|
||||||
|
!updateObj.contains("arch") ||
|
||||||
|
!updateObj.contains("version") ||
|
||||||
|
!updateObj.contains("browser_url")) {
|
||||||
|
qWarning() << "Update manifest entry missing vital field";
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!updateObj["platform"].isString() ||
|
||||||
|
!updateObj["arch"].isString() ||
|
||||||
|
!updateObj["version"].isString() ||
|
||||||
|
!updateObj["browser_url"].isString()) {
|
||||||
|
qWarning() << "Update manifest entry has unexpected vital field type";
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (updateObj["arch"] == QSysInfo::buildCpuArchitecture() &&
|
||||||
|
updateObj["platform"] == QSysInfo::productType()) {
|
||||||
|
qDebug() << "Found update manifest match for current platform";
|
||||||
|
|
||||||
|
QString latestVersion = updateObj["version"].toString();
|
||||||
|
qDebug() << "Latest version of Moonlight for this platform is:" << latestVersion;
|
||||||
|
|
||||||
|
QVector<int> latestVersionQuad;
|
||||||
|
|
||||||
|
parseStringToVersionQuad(latestVersion, latestVersionQuad);
|
||||||
|
|
||||||
|
for (int i = 0;; i++) {
|
||||||
|
int latestVer = 0;
|
||||||
|
int currentVer = 0;
|
||||||
|
|
||||||
|
// Treat missing decimal places as 0
|
||||||
|
if (i < latestVersionQuad.count()) {
|
||||||
|
latestVer = latestVersionQuad[i];
|
||||||
|
}
|
||||||
|
if (i < m_CurrentVersionQuad.count()) {
|
||||||
|
currentVer = m_CurrentVersionQuad[i];
|
||||||
|
}
|
||||||
|
if (i >= latestVersionQuad.count() && i >= m_CurrentVersionQuad.count()) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentVer < latestVer) {
|
||||||
|
qDebug() << "Update available";
|
||||||
|
|
||||||
|
emit onUpdateAvailable(updateObj["browser_url"].toString());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
else if (currentVer > latestVer) {
|
||||||
|
qDebug() << "Update manifest version lower than current version";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
qDebug() << "Update manifest version equal to current version";
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
qWarning() << "Update manifest contained unrecognized entry:" << updateEntry.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
qWarning() << "No entry in update manifest found for current platform: "
|
||||||
|
<< QSysInfo::buildCpuArchitecture() << QSysInfo::productType();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
qWarning() << "Update checking failed with error: " << reply->error();
|
||||||
|
reply->deleteLater();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QObject>
|
||||||
|
#include <QNetworkAccessManager>
|
||||||
|
|
||||||
|
class AutoUpdateChecker : public QObject
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
public:
|
||||||
|
explicit AutoUpdateChecker(QObject *parent = nullptr);
|
||||||
|
|
||||||
|
Q_INVOKABLE void start();
|
||||||
|
|
||||||
|
signals:
|
||||||
|
void onUpdateAvailable(QString url);
|
||||||
|
|
||||||
|
private slots:
|
||||||
|
void handleUpdateCheckRequestFinished(QNetworkReply* reply);
|
||||||
|
|
||||||
|
private:
|
||||||
|
void parseStringToVersionQuad(QString& string, QVector<int>& version);
|
||||||
|
|
||||||
|
QVector<int> m_CurrentVersionQuad;
|
||||||
|
QNetworkAccessManager m_Nam;
|
||||||
|
};
|
||||||
@@ -51,6 +51,8 @@ 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;
|
||||||
|
this->gpuModel = nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
@@ -133,19 +135,21 @@ NvComputer::NvComputer(QString address, QString serverInfo)
|
|||||||
this->currentGameId = NvHTTP::getCurrentGame(serverInfo);
|
this->currentGameId = NvHTTP::getCurrentGame(serverInfo);
|
||||||
this->appVersion = NvHTTP::getXmlString(serverInfo, "appversion");
|
this->appVersion = NvHTTP::getXmlString(serverInfo, "appversion");
|
||||||
this->gfeVersion = NvHTTP::getXmlString(serverInfo, "GfeVersion");
|
this->gfeVersion = NvHTTP::getXmlString(serverInfo, "GfeVersion");
|
||||||
|
this->gpuModel = NvHTTP::getXmlString(serverInfo, "gputype");
|
||||||
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 +188,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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -264,6 +268,7 @@ bool NvComputer::update(NvComputer& that)
|
|||||||
ASSIGN_IF_CHANGED(gfeVersion);
|
ASSIGN_IF_CHANGED(gfeVersion);
|
||||||
ASSIGN_IF_CHANGED(appVersion);
|
ASSIGN_IF_CHANGED(appVersion);
|
||||||
ASSIGN_IF_CHANGED(maxLumaPixelsHEVC);
|
ASSIGN_IF_CHANGED(maxLumaPixelsHEVC);
|
||||||
|
ASSIGN_IF_CHANGED(gpuModel);
|
||||||
ASSIGN_IF_CHANGED_AND_NONEMPTY(appList);
|
ASSIGN_IF_CHANGED_AND_NONEMPTY(appList);
|
||||||
ASSIGN_IF_CHANGED_AND_NONEMPTY(displayModes);
|
ASSIGN_IF_CHANGED_AND_NONEMPTY(displayModes);
|
||||||
return changed;
|
return changed;
|
||||||
@@ -286,6 +291,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 +367,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 +386,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 +451,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 +511,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);
|
||||||
|
|
||||||
@@ -61,6 +65,7 @@ public:
|
|||||||
QVector<NvDisplayMode> displayModes;
|
QVector<NvDisplayMode> displayModes;
|
||||||
int maxLumaPixelsHEVC;
|
int maxLumaPixelsHEVC;
|
||||||
int serverCodecModeSupport;
|
int serverCodecModeSupport;
|
||||||
|
QString gpuModel;
|
||||||
|
|
||||||
// Persisted traits
|
// Persisted traits
|
||||||
QString localAddress;
|
QString localAddress;
|
||||||
@@ -148,31 +153,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 +249,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 +259,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 +273,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 +342,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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -67,6 +67,7 @@ NvHTTP::getServerInfo()
|
|||||||
serverInfo = openConnectionToString(m_BaseUrlHttps,
|
serverInfo = openConnectionToString(m_BaseUrlHttps,
|
||||||
"serverinfo",
|
"serverinfo",
|
||||||
nullptr,
|
nullptr,
|
||||||
|
true,
|
||||||
true);
|
true);
|
||||||
// Throws if the request failed
|
// Throws if the request failed
|
||||||
verifyResponseStatus(serverInfo);
|
verifyResponseStatus(serverInfo);
|
||||||
@@ -79,6 +80,7 @@ NvHTTP::getServerInfo()
|
|||||||
serverInfo = openConnectionToString(m_BaseUrlHttp,
|
serverInfo = openConnectionToString(m_BaseUrlHttp,
|
||||||
"serverinfo",
|
"serverinfo",
|
||||||
nullptr,
|
nullptr,
|
||||||
|
true,
|
||||||
true);
|
true);
|
||||||
verifyResponseStatus(serverInfo);
|
verifyResponseStatus(serverInfo);
|
||||||
}
|
}
|
||||||
@@ -123,6 +125,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 +139,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 +156,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);
|
||||||
|
|
||||||
@@ -215,6 +227,7 @@ NvHTTP::getAppList()
|
|||||||
QString appxml = openConnectionToString(m_BaseUrlHttps,
|
QString appxml = openConnectionToString(m_BaseUrlHttps,
|
||||||
"applist",
|
"applist",
|
||||||
nullptr,
|
nullptr,
|
||||||
|
true,
|
||||||
true);
|
true);
|
||||||
verifyResponseStatus(appxml);
|
verifyResponseStatus(appxml);
|
||||||
|
|
||||||
@@ -265,7 +278,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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -325,12 +341,15 @@ QString
|
|||||||
NvHTTP::openConnectionToString(QUrl baseUrl,
|
NvHTTP::openConnectionToString(QUrl baseUrl,
|
||||||
QString command,
|
QString command,
|
||||||
QString arguments,
|
QString arguments,
|
||||||
bool enableTimeout)
|
bool enableTimeout,
|
||||||
|
bool suppressLogging)
|
||||||
{
|
{
|
||||||
QNetworkReply* reply = openConnection(baseUrl, command, arguments, enableTimeout);
|
QNetworkReply* reply = openConnection(baseUrl, command, arguments, enableTimeout, suppressLogging);
|
||||||
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;
|
||||||
@@ -340,7 +359,8 @@ QNetworkReply*
|
|||||||
NvHTTP::openConnection(QUrl baseUrl,
|
NvHTTP::openConnection(QUrl baseUrl,
|
||||||
QString command,
|
QString command,
|
||||||
QString arguments,
|
QString arguments,
|
||||||
bool enableTimeout)
|
bool enableTimeout,
|
||||||
|
bool suppressLogging)
|
||||||
{
|
{
|
||||||
// Build a URL for the request
|
// Build a URL for the request
|
||||||
QUrl url(baseUrl);
|
QUrl url(baseUrl);
|
||||||
@@ -366,13 +386,17 @@ 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();
|
if (!suppressLogging) {
|
||||||
|
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();
|
if (!suppressLogging) {
|
||||||
|
qWarning() << "Aborting timed out request for" << url.toString();
|
||||||
|
}
|
||||||
reply->abort();
|
reply->abort();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -383,7 +407,9 @@ 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();
|
if (!suppressLogging) {
|
||||||
|
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;
|
||||||
|
|||||||
@@ -107,7 +107,8 @@ public:
|
|||||||
openConnectionToString(QUrl baseUrl,
|
openConnectionToString(QUrl baseUrl,
|
||||||
QString command,
|
QString command,
|
||||||
QString arguments,
|
QString arguments,
|
||||||
bool enableTimeout);
|
bool enableTimeout,
|
||||||
|
bool suppressLogging = false);
|
||||||
|
|
||||||
static
|
static
|
||||||
QVector<int>
|
QVector<int>
|
||||||
@@ -143,7 +144,8 @@ private:
|
|||||||
openConnection(QUrl baseUrl,
|
openConnection(QUrl baseUrl,
|
||||||
QString command,
|
QString command,
|
||||||
QString arguments,
|
QString arguments,
|
||||||
bool enableTimeout);
|
bool enableTimeout,
|
||||||
|
bool suppressLogging = false);
|
||||||
|
|
||||||
QString m_Address;
|
QString m_Address;
|
||||||
QNetworkAccessManager m_Nam;
|
QNetworkAccessManager m_Nam;
|
||||||
|
|||||||
@@ -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,100 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<component type="desktop">
|
||||||
|
<id>com.moonlight_stream.Moonlight</id>
|
||||||
|
<metadata_license>CC0-1.0</metadata_license>
|
||||||
|
<project_license>GPL-3.0+</project_license>
|
||||||
|
<name>Moonlight</name>
|
||||||
|
<summary>Stream games from your NVIDIA GameStream-enabled PC</summary>
|
||||||
|
|
||||||
|
<description>
|
||||||
|
<p>Moonlight can stream games and other applications from a PC with an NVIDIA GeForce GTX 600-series or higher GPU and GeForce Experience installed.</p>
|
||||||
|
<p>Features include:</p>
|
||||||
|
<ul>
|
||||||
|
<li>Streaming at up to 4K resolution</li>
|
||||||
|
<li>Support for up to 120 FPS streaming on high refresh rate monitors</li>
|
||||||
|
<li>Hardware accelerated video decoding with VAAPI and VDPAU support</li>
|
||||||
|
<li>5.1 surround sound audio support</li>
|
||||||
|
<li>HEVC support for better video compression efficiency</li>
|
||||||
|
<li>Gamepad support with SDL mapping compatibility</li>
|
||||||
|
</ul>
|
||||||
|
</description>
|
||||||
|
|
||||||
|
<url type="homepage">https://moonlight-stream.com</url>
|
||||||
|
<url type="bugtracker">https://github.com/moonlight-stream/moonlight-qt/issues</url>
|
||||||
|
<url type="help">https://github.com/moonlight-stream/moonlight-docs/wiki/Setup-Guide</url>
|
||||||
|
|
||||||
|
<screenshots>
|
||||||
|
<screenshot type="default">
|
||||||
|
<image>https://moonlight-stream.com/appstream_resources/moonlight_screenshot_1.png</image>
|
||||||
|
</screenshot>
|
||||||
|
<screenshot>
|
||||||
|
<image>https://moonlight-stream.com/appstream_resources/moonlight_screenshot_2.png</image>
|
||||||
|
</screenshot>
|
||||||
|
</screenshots>
|
||||||
|
|
||||||
|
<releases>
|
||||||
|
<release version="0.2.0" date="2018-08-12">
|
||||||
|
<description>
|
||||||
|
<p>Bugfixes:</p>
|
||||||
|
<ul>
|
||||||
|
<li>Fixed functionality of '=', 'Numpad Dot', and 'Numpad Enter' keys</li>
|
||||||
|
<li>Fixed uninitialized variable causing strange gamepad behavior</li>
|
||||||
|
<li>Added a warning for incompatible configs and decoder failures</li>
|
||||||
|
<li>Added a warning for Wayland performance issues</li>
|
||||||
|
<li>Stopped capturing the mouse by default for windowed streams</li>
|
||||||
|
</ul>
|
||||||
|
</description>
|
||||||
|
</release>
|
||||||
|
<release version="0.1.0" date="2018-08-05">
|
||||||
|
<description>
|
||||||
|
<p>New features:</p>
|
||||||
|
<ul>
|
||||||
|
<li>Allow streaming at the client's native resolution</li>
|
||||||
|
<li>VDPAU hardware decoding support for NVIDIA GPUs</li>
|
||||||
|
<li>Updated UI to replace status text with icons</li>
|
||||||
|
<li>Stream window is now resizable</li>
|
||||||
|
<li>Display warnings during game launch for configuration issues</li>
|
||||||
|
</ul>
|
||||||
|
<p>Bugfixes:</p>
|
||||||
|
<ul>
|
||||||
|
<li>Gamepad input now works even if the app doesn't have focus</li>
|
||||||
|
<li>Fixed streaming black screen if the host supported HEVC but the client didn't</li>
|
||||||
|
<li>Fixed broken video scaling causing extra stretching and off-center video</li>
|
||||||
|
<li>Fixed gamepad input issue causing some Y axis inputs to be reversed</li>
|
||||||
|
</ul>
|
||||||
|
</description>
|
||||||
|
</release>
|
||||||
|
<release version="0.0.5" date="2018-08-01">
|
||||||
|
<description>
|
||||||
|
<p>Changes from 0.0.4:</p>
|
||||||
|
<ul>
|
||||||
|
<li>Fixed a bug causing increased frame drops and input lag</li>
|
||||||
|
<li>Added support for quitting running games</li>
|
||||||
|
<li>Added support for waking sleeping PCs with Wake-on-LAN</li>
|
||||||
|
<li>Increased the FPS limit to 120 FPS</li>
|
||||||
|
</ul>
|
||||||
|
</description>
|
||||||
|
</release>
|
||||||
|
<release version="0.0.4a" date="2018-07-30">
|
||||||
|
<description>
|
||||||
|
<p>Initial pre-alpha release for Linux. Please file bugs!</p>
|
||||||
|
</description>
|
||||||
|
</release>
|
||||||
|
</releases>
|
||||||
|
|
||||||
|
<categories>
|
||||||
|
<category>Game</category>
|
||||||
|
<category>Qt</category>
|
||||||
|
</categories>
|
||||||
|
|
||||||
|
<developer_name>Moonlight Game Streaming Team</developer_name>
|
||||||
|
<update_contact>cameron@moonlight-stream.com</update_contact>
|
||||||
|
|
||||||
|
<launchable type="desktop-id">com.moonlight_stream.Moonlight.desktop</launchable>
|
||||||
|
|
||||||
|
<provides>
|
||||||
|
<binary>moonlight</binary>
|
||||||
|
</provides>
|
||||||
|
|
||||||
|
<content_rating type="oars-1.0" />
|
||||||
|
</component>
|
||||||
@@ -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;
|
||||||
@@ -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;
|
||||||
@@ -27,14 +28,24 @@ 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
|
||||||
|
|
||||||
|
function computerLost()
|
||||||
|
{
|
||||||
|
// Go back to the PC view on PC loss
|
||||||
|
stackView.pop()
|
||||||
|
}
|
||||||
|
|
||||||
onVisibleChanged: {
|
onVisibleChanged: {
|
||||||
if (visible) {
|
if (visible) {
|
||||||
// Start polling when this view is shown
|
// Start polling when this view is shown
|
||||||
ComputerManager.startPolling()
|
ComputerManager.startPolling()
|
||||||
|
|
||||||
|
appModel.computerLost.connect(computerLost)
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
// Stop polling when this view is not on top
|
// Stop polling when this view is not on top
|
||||||
ComputerManager.stopPollingAsync()
|
ComputerManager.stopPollingAsync()
|
||||||
|
|
||||||
|
appModel.computerLost.disconnect(computerLost)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -50,12 +61,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;
|
||||||
@@ -66,29 +71,110 @@ GridView {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Image {
|
||||||
|
id: runningIcon
|
||||||
|
anchors.centerIn: appIcon
|
||||||
|
visible: model.running
|
||||||
|
source: "qrc:/res/baseline-play_circle_filled_white-48px.svg"
|
||||||
|
sourceSize {
|
||||||
|
width: 75
|
||||||
|
height: 75
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Text {
|
Text {
|
||||||
id: appNameText
|
id: appNameText
|
||||||
text: model.name
|
text: 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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,20 @@ 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.isRunningWayland()) {
|
||||||
|
waylandDialog.open()
|
||||||
|
}
|
||||||
|
else if (!prefs.hasAnyHardwareAcceleration()) {
|
||||||
|
noHwDecoderDialog.open()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onVisibleChanged: {
|
onVisibleChanged: {
|
||||||
@@ -72,14 +84,14 @@ GridView {
|
|||||||
return model
|
return model
|
||||||
}
|
}
|
||||||
|
|
||||||
model: createModel()
|
model: computerModel
|
||||||
|
|
||||||
delegate: Item {
|
delegate: Item {
|
||||||
width: 300; height: 300;
|
width: 300; height: 300;
|
||||||
|
|
||||||
Image {
|
Image {
|
||||||
id: pcIcon
|
id: pcIcon
|
||||||
anchors.horizontalCenter: parent.horizontalCenter;
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
source: {
|
source: {
|
||||||
model.addPc ? "qrc:/res/ic_add_to_queue_white_48px.svg" : "qrc:/res/ic_tv_white_48px.svg"
|
model.addPc ? "qrc:/res/ic_add_to_queue_white_48px.svg" : "qrc:/res/ic_tv_white_48px.svg"
|
||||||
}
|
}
|
||||||
@@ -89,6 +101,30 @@ GridView {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Image {
|
||||||
|
// TODO: Tooltip
|
||||||
|
id: stateIcon
|
||||||
|
anchors.horizontalCenter: pcIcon.horizontalCenter
|
||||||
|
anchors.verticalCenter: pcIcon.verticalCenter
|
||||||
|
anchors.verticalCenterOffset: -10
|
||||||
|
visible: !model.addPc && !model.statusUnknown && (!model.online || !model.paired)
|
||||||
|
source: !model.online ? "qrc:/res/baseline-warning-24px.svg" : "qrc:/res/baseline-lock-24px.svg"
|
||||||
|
sourceSize {
|
||||||
|
width: 75
|
||||||
|
height: 75
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
BusyIndicator {
|
||||||
|
id: statusUnknownSpinner
|
||||||
|
anchors.horizontalCenter: pcIcon.horizontalCenter
|
||||||
|
anchors.verticalCenter: pcIcon.verticalCenter
|
||||||
|
anchors.verticalCenterOffset: -10
|
||||||
|
width: 75
|
||||||
|
height: 75
|
||||||
|
visible: !model.addPc && model.statusUnknown
|
||||||
|
}
|
||||||
|
|
||||||
Text {
|
Text {
|
||||||
id: pcNameText
|
id: pcNameText
|
||||||
text: model.name
|
text: model.name
|
||||||
@@ -101,76 +137,70 @@ GridView {
|
|||||||
wrapMode: Text.Wrap
|
wrapMode: Text.Wrap
|
||||||
}
|
}
|
||||||
|
|
||||||
Text {
|
Menu {
|
||||||
function getStatusText(model)
|
id: pcContextMenu
|
||||||
{
|
MenuItem {
|
||||||
if (model.online) {
|
text: "Wake PC"
|
||||||
var text = "<font color=\"green\">Online</font>"
|
onTriggered: computerModel.wakeComputer(index)
|
||||||
text += "<font color=\"white\"> - </font>"
|
visible: !model.addPc && !model.online && model.wakeable
|
||||||
if (model.paired) {
|
height: visible ? implicitHeight : 0
|
||||||
text += "<font color=\"skyblue\">Paired</font>"
|
}
|
||||||
}
|
MenuItem {
|
||||||
else if (model.busy) {
|
text: "Delete PC"
|
||||||
text += "<font color=\"red\">Busy</font>"
|
onTriggered: {
|
||||||
}
|
deletePcDialog.pcIndex = index
|
||||||
else {
|
// get confirmation first, actual closing is called from the dialog
|
||||||
text += "<font color=\"red\">Not Paired</font>"
|
deletePcDialog.open()
|
||||||
}
|
|
||||||
return text
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
return "<font color=\"red\">Offline</font>";
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
id: pcPairedText
|
|
||||||
text: getStatusText(model)
|
|
||||||
visible: !model.addPc
|
|
||||||
|
|
||||||
width: parent.width
|
|
||||||
anchors.top: pcNameText.bottom
|
|
||||||
font.pointSize: 24
|
|
||||||
horizontalAlignment: Text.AlignHCenter
|
|
||||||
}
|
}
|
||||||
|
|
||||||
MouseArea {
|
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 +214,31 @@ 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 {
|
||||||
|
id: waylandDialog
|
||||||
|
modality:Qt.WindowModal
|
||||||
|
icon: StandardIcon.Warning
|
||||||
|
standardButtons: StandardButton.Ok | StandardButton.Help
|
||||||
|
text: "Moonlight does not support hardware acceleration on Wayland. Continuing on Wayland may result in poor streaming performance. " +
|
||||||
|
"Please switch to an X session for optimal performance."
|
||||||
|
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 +251,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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,153 @@ 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
|
// Add native resolutions for all attached displays
|
||||||
var saved_fps = prefs.fps
|
for (var displayIndex = 0;; displayIndex++) {
|
||||||
currentIndex = 0
|
var screenRect = prefs.getDisplayResolution(displayIndex)
|
||||||
for(var i = 0; i < resolutionComboBox.count; i++) {
|
if (screenRect.width === 0) {
|
||||||
var el_width = parseInt(resolutionListModel.get(i).video_width);
|
// Exceeded max count of displays
|
||||||
var el_height = parseInt(resolutionListModel.get(i).video_height);
|
break
|
||||||
var el_fps = parseInt(resolutionListModel.get(i).video_fps);
|
}
|
||||||
if(saved_width === el_width &&
|
|
||||||
saved_height === el_height &&
|
var indexToAdd = 0
|
||||||
saved_fps === el_fps) {
|
for (var j = 0; j < resolutionComboBox.count; j++) {
|
||||||
currentIndex = i
|
var existing_width = parseInt(resolutionListModel.get(j).video_width);
|
||||||
}
|
var existing_height = parseInt(resolutionListModel.get(j).video_height);
|
||||||
|
|
||||||
|
if (screenRect.width === existing_width && screenRect.height === existing_height) {
|
||||||
|
// Duplicate entry, skip
|
||||||
|
indexToAdd = -1
|
||||||
|
break
|
||||||
|
}
|
||||||
|
else if (screenRect.width * screenRect.height > existing_width * existing_height) {
|
||||||
|
// Candidate entrypoint after this entry
|
||||||
|
indexToAdd = j + 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Insert this display's resolution if it's not a duplicate
|
||||||
|
if (indexToAdd >= 0) {
|
||||||
|
resolutionListModel.insert(indexToAdd,
|
||||||
|
{
|
||||||
|
"text": "Native ("+screenRect.width+"x"+screenRect.height+")",
|
||||||
|
"video_width": ""+screenRect.width,
|
||||||
|
"video_height": ""+screenRect.height
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// load the saved width/height, and iterate through the ComboBox until a match is found
|
||||||
|
// and set it to that index.
|
||||||
|
var saved_width = prefs.width
|
||||||
|
var saved_height = prefs.height
|
||||||
|
currentIndex = 0
|
||||||
|
for (var i = 0; i < resolutionComboBox.count; i++) {
|
||||||
|
var el_width = parseInt(resolutionListModel.get(i).video_width);
|
||||||
|
var el_height = parseInt(resolutionListModel.get(i).video_height);
|
||||||
|
|
||||||
|
// Pick the highest value lesser or equal to the saved resolution
|
||||||
|
if (saved_width * saved_height >= el_width * el_height) {
|
||||||
|
currentIndex = i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
id: resolutionComboBox
|
||||||
|
width: 200
|
||||||
|
textRole: "text"
|
||||||
|
model: ListModel {
|
||||||
|
id: resolutionListModel
|
||||||
|
// Other elements may be added at runtime
|
||||||
|
// based on attached display resolution
|
||||||
|
ListElement {
|
||||||
|
text: "720p"
|
||||||
|
video_width: "1280"
|
||||||
|
video_height: "720"
|
||||||
|
}
|
||||||
|
ListElement {
|
||||||
|
text: "1080p"
|
||||||
|
video_width: "1920"
|
||||||
|
video_height: "1080"
|
||||||
|
}
|
||||||
|
ListElement {
|
||||||
|
text: "1440p"
|
||||||
|
video_width: "2560"
|
||||||
|
video_height: "1440"
|
||||||
|
}
|
||||||
|
ListElement {
|
||||||
|
text: "4K"
|
||||||
|
video_width: "3840"
|
||||||
|
video_height: "2160"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// ::onActivated must be used, as it only listens for when the index is changed by a human
|
||||||
|
onActivated : {
|
||||||
|
prefs.width = parseInt(resolutionListModel.get(currentIndex).video_width)
|
||||||
|
prefs.height = parseInt(resolutionListModel.get(currentIndex).video_height)
|
||||||
|
|
||||||
|
prefs.bitrateKbps = prefs.getDefaultBitrate(prefs.width, prefs.height, prefs.fps);
|
||||||
|
slider.value = prefs.bitrateKbps
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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);
|
||||||
|
|
||||||
|
// Pick the highest value lesser or equal to the saved FPS
|
||||||
|
if (saved_fps >= el_fps) {
|
||||||
|
currentIndex = i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
id: fpsComboBox
|
||||||
|
textRole: "text"
|
||||||
|
model: ListModel {
|
||||||
|
id: fpsListModel
|
||||||
|
ListElement {
|
||||||
|
text: "30 FPS"
|
||||||
|
video_fps: "30"
|
||||||
|
}
|
||||||
|
ListElement {
|
||||||
|
text: "60 FPS"
|
||||||
|
video_fps: "60"
|
||||||
|
}
|
||||||
|
// A higher value may be added at runtime
|
||||||
|
// based on the attached display refresh rate
|
||||||
|
}
|
||||||
|
// ::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 +219,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 +245,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 +254,65 @@ 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)
|
||||||
|
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 +330,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 +368,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 +377,105 @@ 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)
|
||||||
|
textRole: "text"
|
||||||
|
model: ListModel {
|
||||||
|
id: decoderListModel
|
||||||
|
ListElement {
|
||||||
|
text: "Automatic (Recommended)"
|
||||||
|
val: StreamingPreferences.VDS_AUTO
|
||||||
|
}
|
||||||
|
ListElement {
|
||||||
|
text: "Force software decoding"
|
||||||
|
val: StreamingPreferences.VDS_FORCE_SOFTWARE
|
||||||
|
}
|
||||||
|
ListElement {
|
||||||
|
text: "Force hardware decoding"
|
||||||
|
val: StreamingPreferences.VDS_FORCE_HARDWARE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// ::onActivated must be used, as it only listens for when the index is changed by a human
|
||||||
|
onActivated : {
|
||||||
|
prefs.videoDecoderSelection = decoderListModel.get(currentIndex).val
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Label {
|
||||||
|
width: parent.width
|
||||||
|
id: resVCCTitle
|
||||||
|
text: qsTr("Video codec")
|
||||||
|
font.pointSize: 12
|
||||||
|
wrapMode: Text.Wrap
|
||||||
|
color: "white"
|
||||||
|
}
|
||||||
|
|
||||||
|
ComboBox {
|
||||||
|
// ignore setting the index at first, and actually set it when the component is loaded
|
||||||
|
Component.onCompleted: {
|
||||||
|
var saved_vcc = prefs.videoCodecConfig
|
||||||
|
currentIndex = 0
|
||||||
|
for(var i = 0; i < codecListModel.count; i++) {
|
||||||
|
var el_vcc = codecListModel.get(i).val;
|
||||||
|
if(saved_vcc === el_vcc) {
|
||||||
|
currentIndex = i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
id: codecComboBox
|
||||||
|
width: Math.min(bitrateDesc.implicitWidth, parent.width)
|
||||||
|
textRole: "text"
|
||||||
|
model: ListModel {
|
||||||
|
id: codecListModel
|
||||||
|
ListElement {
|
||||||
|
text: "Automatic (Recommended)"
|
||||||
|
val: StreamingPreferences.VCC_AUTO
|
||||||
|
}
|
||||||
|
ListElement {
|
||||||
|
text: "Force H.264"
|
||||||
|
val: StreamingPreferences.VCC_FORCE_H264
|
||||||
|
}
|
||||||
|
ListElement {
|
||||||
|
text: "Force HEVC"
|
||||||
|
val: StreamingPreferences.VCC_FORCE_HEVC
|
||||||
|
}
|
||||||
|
// HDR seems to be broken in GFE 3.14.1, and even when that's fixed
|
||||||
|
// we'll probably need to gate this feature on OS support in our
|
||||||
|
// renderers.
|
||||||
|
/* ListElement {
|
||||||
|
text: "Force HEVC HDR"
|
||||||
|
val: StreamingPreferences.VCC_FORCE_HEVC_HDR
|
||||||
|
} */
|
||||||
|
}
|
||||||
|
// ::onActivated must be used, as it only listens for when the index is changed by a human
|
||||||
|
onActivated : {
|
||||||
|
prefs.videoCodecConfig = codecListModel.get(currentIndex).val
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
@@ -47,7 +47,13 @@ Item {
|
|||||||
|
|
||||||
function displayLaunchWarning(text)
|
function displayLaunchWarning(text)
|
||||||
{
|
{
|
||||||
// TODO: toast
|
// This toast appears for 3 seconds, just shorter than how long
|
||||||
|
// Session will wait for it to be displayed. This gives it time
|
||||||
|
// to transition to invisible before continuing.
|
||||||
|
var toast = Qt.createQmlObject('import QtQuick.Controls 2.3; ToolTip {}', parent, '')
|
||||||
|
toast.text = text
|
||||||
|
toast.timeout = 3000
|
||||||
|
toast.visible = true
|
||||||
}
|
}
|
||||||
|
|
||||||
onVisibleChanged: {
|
onVisibleChanged: {
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -77,6 +108,14 @@ void AppModel::handleComputerStateChanged(NvComputer* computer)
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If the computer has gone offline or we've been unpaired,
|
||||||
|
// signal the UI so we can go back to the PC view.
|
||||||
|
if (m_Computer->state == NvComputer::CS_OFFLINE ||
|
||||||
|
m_Computer->pairState == NvComputer::PS_NOT_PAIRED) {
|
||||||
|
emit computerLost();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// First, process additions/removals from the app list. This
|
// First, process additions/removals from the app list. This
|
||||||
// is required because the new game may now be running, so
|
// is required because the new game may now be running, so
|
||||||
// we can't check that first.
|
// we can't check that first.
|
||||||
@@ -123,10 +162,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;
|
||||||
@@ -36,6 +42,9 @@ private slots:
|
|||||||
|
|
||||||
void handleBoxArtLoaded(NvComputer* computer, NvApp app, QUrl image);
|
void handleBoxArtLoaded(NvComputer* computer, NvApp app, QUrl image);
|
||||||
|
|
||||||
|
signals:
|
||||||
|
void computerLost();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
NvComputer* m_Computer;
|
NvComputer* m_Computer;
|
||||||
BoxArtManager m_BoxArtManager;
|
BoxArtManager m_BoxArtManager;
|
||||||
|
|||||||
@@ -46,8 +46,12 @@ 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;
|
||||||
|
case StatusUnknownRole:
|
||||||
|
return computer->state == NvComputer::CS_UNKNOWN;
|
||||||
default:
|
default:
|
||||||
return QVariant();
|
return QVariant();
|
||||||
}
|
}
|
||||||
@@ -74,6 +78,8 @@ 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";
|
||||||
|
names[StatusUnknownRole] = "statusUnknown";
|
||||||
|
|
||||||
return names;
|
return names;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ class ComputerModel : public QAbstractListModel
|
|||||||
OnlineRole,
|
OnlineRole,
|
||||||
PairedRole,
|
PairedRole,
|
||||||
BusyRole,
|
BusyRole,
|
||||||
|
WakeableRole,
|
||||||
|
StatusUnknownRole,
|
||||||
AddPcRole
|
AddPcRole
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import QtQuick.Layouts 1.3
|
|||||||
|
|
||||||
import QtQuick.Controls.Material 2.1
|
import QtQuick.Controls.Material 2.1
|
||||||
|
|
||||||
|
import AutoUpdateChecker 1.0
|
||||||
|
|
||||||
ApplicationWindow {
|
ApplicationWindow {
|
||||||
id: window
|
id: window
|
||||||
visible: true
|
visible: true
|
||||||
@@ -66,25 +68,72 @@ ApplicationWindow {
|
|||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ToolButton {
|
||||||
|
property string browserUrl: ""
|
||||||
|
|
||||||
|
id: updateButton
|
||||||
|
icon.source: "qrc:/res/update.svg"
|
||||||
|
icon.color: "#0BF200"
|
||||||
|
|
||||||
|
ToolTip.delay: 1000
|
||||||
|
ToolTip.timeout: 3000
|
||||||
|
ToolTip.visible: hovered
|
||||||
|
ToolTip.text: "Update available for Moonlight"
|
||||||
|
|
||||||
|
// Invisible until we get a callback notifying us that
|
||||||
|
// an update is available
|
||||||
|
visible: false
|
||||||
|
|
||||||
|
onClicked: Qt.openUrlExternally(browserUrl);
|
||||||
|
|
||||||
|
Menu {
|
||||||
|
x: parent.width
|
||||||
|
transformOrigin: Menu.TopRight
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function updateAvailable(url)
|
||||||
|
{
|
||||||
|
updateButton.browserUrl = url
|
||||||
|
updateButton.visible = true
|
||||||
|
}
|
||||||
|
|
||||||
|
Component.onCompleted: {
|
||||||
|
AutoUpdateChecker.onUpdateAvailable.connect(updateAvailable)
|
||||||
|
AutoUpdateChecker.start()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
ToolButton {
|
ToolButton {
|
||||||
icon.source: "qrc:/res/question_mark.svg"
|
icon.source: "qrc:/res/question_mark.svg"
|
||||||
|
|
||||||
|
ToolTip.delay: 1000
|
||||||
|
ToolTip.timeout: 3000
|
||||||
|
ToolTip.visible: hovered
|
||||||
|
ToolTip.text: "Help"
|
||||||
|
|
||||||
// TODO need to make sure browser is brought to foreground.
|
// TODO need to make sure browser is brought to foreground.
|
||||||
onClicked: Qt.openUrlExternally("https://github.com/moonlight-stream/moonlight-docs/wiki/Setup-Guide");
|
onClicked: Qt.openUrlExternally("https://github.com/moonlight-stream/moonlight-docs/wiki/Setup-Guide");
|
||||||
|
|
||||||
Menu {
|
Menu {
|
||||||
id: helpButton
|
|
||||||
x: parent.width
|
x: parent.width
|
||||||
transformOrigin: Menu.TopRight
|
transformOrigin: Menu.TopRight
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ToolButton {
|
ToolButton {
|
||||||
|
// TODO: Implement gamepad mapping then unhide this button
|
||||||
|
visible: false
|
||||||
|
|
||||||
|
ToolTip.delay: 1000
|
||||||
|
ToolTip.timeout: 3000
|
||||||
|
ToolTip.visible: hovered
|
||||||
|
ToolTip.text: "Gamepad Mapper"
|
||||||
|
|
||||||
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")
|
||||||
|
|
||||||
Menu {
|
Menu {
|
||||||
id: gamepadMappingMenu
|
|
||||||
x: parent.width
|
x: parent.width
|
||||||
transformOrigin: Menu.TopRight
|
transformOrigin: Menu.TopRight
|
||||||
}
|
}
|
||||||
@@ -94,8 +143,12 @@ ApplicationWindow {
|
|||||||
icon.source: "qrc:/res/settings.svg"
|
icon.source: "qrc:/res/settings.svg"
|
||||||
onClicked: navigateTo("qrc:/gui/SettingsView.qml", "Settings")
|
onClicked: navigateTo("qrc:/gui/SettingsView.qml", "Settings")
|
||||||
|
|
||||||
|
ToolTip.delay: 1000
|
||||||
|
ToolTip.timeout: 3000
|
||||||
|
ToolTip.visible: hovered
|
||||||
|
ToolTip.text: "Settings"
|
||||||
|
|
||||||
Menu {
|
Menu {
|
||||||
id: optionsMenu
|
|
||||||
x: parent.width
|
x: parent.width
|
||||||
transformOrigin: Menu.TopRight
|
transformOrigin: Menu.TopRight
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -11,11 +13,144 @@
|
|||||||
|
|
||||||
#include "gui/computermodel.h"
|
#include "gui/computermodel.h"
|
||||||
#include "gui/appmodel.h"
|
#include "gui/appmodel.h"
|
||||||
|
#include "backend/autoupdatechecker.h"
|
||||||
#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
|
||||||
|
#ifdef Q_OS_DARWIN
|
||||||
|
// On macOS, $TMPDIR is some random folder under /var/folders/ that nobody can
|
||||||
|
// easily find, so use the system's global tmp directory instead.
|
||||||
|
QDir tempDir("/tmp");
|
||||||
|
#else
|
||||||
|
QDir tempDir(QDir::tempPath());
|
||||||
|
#endif
|
||||||
|
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
|
||||||
@@ -44,6 +179,11 @@ int main(int argc, char *argv[])
|
|||||||
[](QQmlEngine*, QJSEngine*) -> QObject* {
|
[](QQmlEngine*, QJSEngine*) -> QObject* {
|
||||||
return new ComputerManager();
|
return new ComputerManager();
|
||||||
});
|
});
|
||||||
|
qmlRegisterSingletonType<AutoUpdateChecker>("AutoUpdateChecker", 1, 0,
|
||||||
|
"AutoUpdateChecker",
|
||||||
|
[](QQmlEngine*, QJSEngine*) -> QObject* {
|
||||||
|
return new AutoUpdateChecker();
|
||||||
|
});
|
||||||
|
|
||||||
QQuickStyle::setStyle("Material");
|
QQuickStyle::setStyle("Material");
|
||||||
|
|
||||||
@@ -53,12 +193,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());
|
||||||
|
|||||||
|
After Width: | Height: | Size: 345 KiB |
|
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>
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="#FFFFFF" width="24" height="24" viewBox="0 0 24 24">
|
||||||
|
<path d="M0 0h24v24H0z" fill="none"/>
|
||||||
|
<path d="M18 8h-1V6c0-2.76-2.24-5-5-5S7 3.24 7 6v2H6c-1.1 0-2 .9-2 2v10c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V10c0-1.1-.9-2-2-2zm-6 9c-1.1 0-2-.9-2-2s.9-2 2-2 2 .9 2 2-.9 2-2 2zm3.1-9H8.9V6c0-1.71 1.39-3.1 3.1-3.1 1.71 0 3.1 1.39 3.1 3.1v2z"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 390 B |
@@ -0,0 +1,4 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48" fill="#fff">
|
||||||
|
<path d="M0 0h48v48H0z" fill="none"/>
|
||||||
|
<path d="M24 4C12.95 4 4 12.95 4 24s8.95 20 20 20 20-8.95 20-20S35.05 4 24 4zm-4 29V15l12 9-12 9z"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 250 B |
@@ -0,0 +1,4 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="#FFFFFF" width="24" height="24" viewBox="0 0 24 24">
|
||||||
|
<path d="M0 0h24v24H0z" fill="none"/>
|
||||||
|
<path d="M1 21h22L12 2 1 21zm12-3h-2v-2h2v2zm0-4h-2v-4h2v4z"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 215 B |
@@ -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 |
@@ -0,0 +1,3 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="24" height="24" viewBox="0 0 24 24">
|
||||||
|
<path d="M21,10.12H14.22L16.96,7.3C14.23,4.6 9.81,4.5 7.08,7.2C4.35,9.91 4.35,14.28 7.08,17C9.81,19.7 14.23,19.7 16.96,17C18.32,15.65 19,14.08 19,12.1H21C21,14.08 20.12,16.65 18.36,18.39C14.85,21.87 9.15,21.87 5.64,18.39C2.14,14.92 2.11,9.28 5.62,5.81C9.13,2.34 14.76,2.34 18.27,5.81L21,3V10.12M12.5,8V12.25L16,14.33L15.28,15.54L11,13V8H12.5Z" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 484 B |
@@ -5,10 +5,14 @@
|
|||||||
<file>res/ic_videogame_asset_white_48px.svg</file>
|
<file>res/ic_videogame_asset_white_48px.svg</file>
|
||||||
<file>res/ic_tv_white_48px.svg</file>
|
<file>res/ic_tv_white_48px.svg</file>
|
||||||
<file>res/ic_add_to_queue_white_48px.svg</file>
|
<file>res/ic_add_to_queue_white_48px.svg</file>
|
||||||
|
<file>res/baseline-lock-24px.svg</file>
|
||||||
|
<file>res/baseline-play_circle_filled_white-48px.svg</file>
|
||||||
|
<file>res/baseline-warning-24px.svg</file>
|
||||||
<file>res/no_app_image.png</file>
|
<file>res/no_app_image.png</file>
|
||||||
<file>res/settings.svg</file>
|
<file>res/settings.svg</file>
|
||||||
<file>res/arrow_left.svg</file>
|
<file>res/arrow_left.svg</file>
|
||||||
<file>res/question_mark.svg</file>
|
<file>res/question_mark.svg</file>
|
||||||
<file>res/moonlight.svg</file>
|
<file>res/moonlight.svg</file>
|
||||||
|
<file>res/update.svg</file>
|
||||||
</qresource>
|
</qresource>
|
||||||
</RCC>
|
</RCC>
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
#include "streamingpreferences.h"
|
#include "streamingpreferences.h"
|
||||||
|
#include "streaming/session.hpp"
|
||||||
|
|
||||||
#include <QSettings>
|
#include <QSettings>
|
||||||
|
|
||||||
@@ -57,24 +58,93 @@ void StreamingPreferences::save()
|
|||||||
settings.setValue(SER_VIDEODEC, static_cast<int>(videoDecoderSelection));
|
settings.setValue(SER_VIDEODEC, static_cast<int>(videoDecoderSelection));
|
||||||
}
|
}
|
||||||
|
|
||||||
int StreamingPreferences::getDefaultBitrate(int width, int height, int fps)
|
bool StreamingPreferences::hasAnyHardwareAcceleration()
|
||||||
{
|
{
|
||||||
if (width * height * fps <= 1280 * 720 * 30) {
|
// Always use VDS_AUTO to avoid spamming the user with warnings
|
||||||
return 5000;
|
// if they've forced software decoding.
|
||||||
|
return Session::isHardwareDecodeAvailable(VDS_AUTO,
|
||||||
|
VIDEO_FORMAT_H264,
|
||||||
|
1920, 1080, 60);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool StreamingPreferences::isRunningWayland()
|
||||||
|
{
|
||||||
|
return qgetenv("XDG_SESSION_TYPE") == QByteArray("wayland");
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
}
|
}
|
||||||
else if (width * height * fps <= 1280 * 720 * 60) {
|
|
||||||
return 10000;
|
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));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else if (width * height * fps <= 1920 * 1080 * 30) {
|
|
||||||
return 10000;
|
SDL_QuitSubSystem(SDL_INIT_VIDEO);
|
||||||
|
|
||||||
|
return maxFrameRate;
|
||||||
|
}
|
||||||
|
|
||||||
|
QRect StreamingPreferences::getDisplayResolution(int displayIndex)
|
||||||
|
{
|
||||||
|
if (SDL_InitSubSystem(SDL_INIT_VIDEO) != 0) {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"SDL_InitSubSystem(SDL_INIT_VIDEO) failed: %s",
|
||||||
|
SDL_GetError());
|
||||||
|
return QRect();
|
||||||
}
|
}
|
||||||
else if (width * height * fps <= 1920 * 1080 * 60) {
|
|
||||||
return 20000;
|
if (displayIndex >= SDL_GetNumVideoDisplays()) {
|
||||||
|
SDL_QuitSubSystem(SDL_INIT_VIDEO);
|
||||||
|
return QRect();
|
||||||
}
|
}
|
||||||
else if (width * height * fps <= 3840 * 2160 * 30) {
|
|
||||||
return 40000;
|
SDL_DisplayMode mode;
|
||||||
|
int err = SDL_GetCurrentDisplayMode(displayIndex, &mode);
|
||||||
|
SDL_QuitSubSystem(SDL_INIT_VIDEO);
|
||||||
|
|
||||||
|
if (err == 0) {
|
||||||
|
return QRect(0, 0, mode.w, mode.h);
|
||||||
}
|
}
|
||||||
else /* if (width * height * fps <= 3840 * 2160 * 60) */ {
|
else {
|
||||||
return 80000;
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"SDL_GetCurrentDisplayMode() failed: %s",
|
||||||
|
SDL_GetError());
|
||||||
|
return QRect();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int StreamingPreferences::getDefaultBitrate(int width, int height, int fps)
|
||||||
|
{
|
||||||
|
// This table prefers 16:10 resolutions because they are
|
||||||
|
// only slightly more pixels than the 16:9 equivalents, so
|
||||||
|
// we don't want to bump those 16:10 resolutions up to the
|
||||||
|
// next 16:9 slot.
|
||||||
|
|
||||||
|
// This covers 1280x720 and 1280x800 too
|
||||||
|
if (width * height <= 1366 * 768) {
|
||||||
|
return static_cast<int>(5000 * (fps / 30.0));
|
||||||
|
}
|
||||||
|
else if (width * height <= 1920 * 1200) {
|
||||||
|
return static_cast<int>(10000 * (fps / 30.0));
|
||||||
|
}
|
||||||
|
else if (width * height <= 2560 * 1600) {
|
||||||
|
return static_cast<int>(20000 * (fps / 30.0));
|
||||||
|
}
|
||||||
|
else /* if (width * height <= 3840 * 2160) */ {
|
||||||
|
return static_cast<int>(40000 * (fps / 30.0));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <QObject>
|
#include <QObject>
|
||||||
|
#include <QRect>
|
||||||
|
|
||||||
class StreamingPreferences : public QObject
|
class StreamingPreferences : public QObject
|
||||||
{
|
{
|
||||||
@@ -14,6 +15,14 @@ public:
|
|||||||
|
|
||||||
Q_INVOKABLE void save();
|
Q_INVOKABLE void save();
|
||||||
|
|
||||||
|
Q_INVOKABLE static bool hasAnyHardwareAcceleration();
|
||||||
|
|
||||||
|
Q_INVOKABLE static bool isRunningWayland();
|
||||||
|
|
||||||
|
Q_INVOKABLE static int getMaximumStreamingFrameRate();
|
||||||
|
|
||||||
|
Q_INVOKABLE QRect getDisplayResolution(int displayIndex);
|
||||||
|
|
||||||
void reload();
|
void reload();
|
||||||
|
|
||||||
enum AudioConfig
|
enum AudioConfig
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -2,6 +2,8 @@
|
|||||||
#include <SDL.h>
|
#include <SDL.h>
|
||||||
#include "streaming/session.hpp"
|
#include "streaming/session.hpp"
|
||||||
|
|
||||||
|
#include <QtGlobal>
|
||||||
|
|
||||||
#define VK_0 0x30
|
#define VK_0 0x30
|
||||||
#define VK_A 0x41
|
#define VK_A 0x41
|
||||||
|
|
||||||
@@ -9,7 +11,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,10 +25,26 @@ const int SdlInputHandler::k_ButtonMap[] = {
|
|||||||
SdlInputHandler::SdlInputHandler(bool multiController)
|
SdlInputHandler::SdlInputHandler(bool multiController)
|
||||||
: m_MultiController(multiController)
|
: m_MultiController(multiController)
|
||||||
{
|
{
|
||||||
|
// Allow gamepad input when the app doesn't have focus
|
||||||
|
SDL_SetHint(SDL_HINT_JOYSTICK_ALLOW_BACKGROUND_EVENTS, "1");
|
||||||
|
|
||||||
|
// We need to reinit this each time, since you only get
|
||||||
|
// an initial set of gamepad arrival events once per init.
|
||||||
|
SDL_assert(!SDL_WasInit(SDL_INIT_GAMECONTROLLER));
|
||||||
|
if (SDL_InitSubSystem(SDL_INIT_GAMECONTROLLER) != 0) {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"SDL_InitSubSystem(SDL_INIT_GAMECONTROLLER) failed: %s",
|
||||||
|
SDL_GetError());
|
||||||
|
}
|
||||||
|
|
||||||
if (!m_MultiController) {
|
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;
|
||||||
}
|
}
|
||||||
|
else {
|
||||||
|
// Otherwise, detect gamepads on the fly
|
||||||
|
m_GamepadMask = 0;
|
||||||
|
}
|
||||||
|
|
||||||
SDL_zero(m_GamepadState);
|
SDL_zero(m_GamepadState);
|
||||||
}
|
}
|
||||||
@@ -38,6 +56,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 +72,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 +109,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 +131,203 @@ 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_KP_ENTER: // FIXME: Is this correct?
|
||||||
|
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_PERIOD:
|
||||||
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_EQUALS:
|
||||||
keyCode = 0xBB;
|
keyCode = 0xBB;
|
||||||
break;
|
break;
|
||||||
case SDLK_COMMA:
|
case SDL_SCANCODE_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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -366,8 +390,8 @@ void SdlInputHandler::handleMouseMotionEvent(SDL_MouseMotionEvent* event)
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (event->xrel != 0 || event->yrel != 0) {
|
if (event->xrel != 0 || event->yrel != 0) {
|
||||||
LiSendMouseMoveEvent((unsigned short)event->xrel,
|
LiSendMouseMoveEvent((short)event->xrel,
|
||||||
(unsigned short)event->yrel);
|
(short)event->yrel);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -427,13 +451,18 @@ void SdlInputHandler::handleControllerAxisEvent(SDL_ControllerAxisEvent* event)
|
|||||||
state->lsX = event->value;
|
state->lsX = event->value;
|
||||||
break;
|
break;
|
||||||
case SDL_CONTROLLER_AXIS_LEFTY:
|
case SDL_CONTROLLER_AXIS_LEFTY:
|
||||||
state->lsY = -event->value;
|
// Signed values have one more negative value than
|
||||||
|
// positive value, so inverting the sign on -32768
|
||||||
|
// could actually cause the value to overflow and
|
||||||
|
// wrap around to be negative again. Avoid that by
|
||||||
|
// capping the value at 32767.
|
||||||
|
state->lsY = -qMax(event->value, (short)-32767);
|
||||||
break;
|
break;
|
||||||
case SDL_CONTROLLER_AXIS_RIGHTX:
|
case SDL_CONTROLLER_AXIS_RIGHTX:
|
||||||
state->rsX = event->value;
|
state->rsX = event->value;
|
||||||
break;
|
break;
|
||||||
case SDL_CONTROLLER_AXIS_RIGHTY:
|
case SDL_CONTROLLER_AXIS_RIGHTY:
|
||||||
state->rsY = -event->value;
|
state->rsY = -qMax(event->value, (short)-32767);
|
||||||
break;
|
break;
|
||||||
case SDL_CONTROLLER_AXIS_TRIGGERLEFT:
|
case SDL_CONTROLLER_AXIS_TRIGGERLEFT:
|
||||||
state->lt = (unsigned char)(event->value * 255UL / 32767);
|
state->lt = (unsigned char)(event->value * 255UL / 32767);
|
||||||
|
|||||||
@@ -5,12 +5,38 @@
|
|||||||
#include <SDL.h>
|
#include <SDL.h>
|
||||||
#include "utils.h"
|
#include "utils.h"
|
||||||
|
|
||||||
|
#ifdef HAVE_FFMPEG
|
||||||
#include "video/ffmpeg.h"
|
#include "video/ffmpeg.h"
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifdef HAVE_SLVIDEO
|
||||||
|
#include "video/sl.h"
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#if defined(Q_OS_WIN32) || defined(Q_OS_DARWIN)
|
||||||
|
// Using full-screen desktop allows us to avoid needing to enable V-sync on Windows
|
||||||
|
// and it also avoids some strange flickering issues on my Win7 test machine
|
||||||
|
// with Intel HD 5500 graphics. It also allows Spaces to work on macOS.
|
||||||
|
#define SDL_OS_FULLSCREEN_FLAG SDL_WINDOW_FULLSCREEN_DESKTOP
|
||||||
|
#else
|
||||||
|
#define SDL_OS_FULLSCREEN_FLAG SDL_WINDOW_FULLSCREEN
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifdef Q_OS_WIN32
|
||||||
|
// Scaling the icon down on Win32 looks dreadful, so render at lower res
|
||||||
|
#define ICON_SIZE 32
|
||||||
|
#else
|
||||||
|
#define ICON_SIZE 64
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#include <openssl/rand.h>
|
||||||
|
|
||||||
#include <QRandomGenerator>
|
|
||||||
#include <QtEndian>
|
#include <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,
|
||||||
@@ -82,6 +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 HAVE_SLVIDEO
|
||||||
|
chosenDecoder = new SLVideoDecoder();
|
||||||
|
if (chosenDecoder->initialize(vds, window, videoFormat, width, height, frameRate)) {
|
||||||
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"SLVideo video decoder chosen");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Unable to load SLVideo decoder");
|
||||||
|
delete chosenDecoder;
|
||||||
|
chosenDecoder = nullptr;
|
||||||
|
}
|
||||||
|
#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,
|
||||||
@@ -94,6 +136,11 @@ bool Session::chooseDecoder(StreamingPreferences::VideoDecoderSelection vds,
|
|||||||
delete chosenDecoder;
|
delete chosenDecoder;
|
||||||
chosenDecoder = nullptr;
|
chosenDecoder = nullptr;
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#if !defined(HAVE_FFMPEG) && !defined(HAVE_SLVIDEO)
|
||||||
|
#error No video decoding libraries available!
|
||||||
|
#endif
|
||||||
|
|
||||||
// If we reach this, we didn't initialize any decoders successfully
|
// If we reach this, we didn't initialize any decoders successfully
|
||||||
return false;
|
return false;
|
||||||
@@ -101,12 +148,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;
|
||||||
}
|
}
|
||||||
@@ -117,7 +169,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);
|
||||||
@@ -135,23 +196,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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -160,8 +233,11 @@ 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)
|
||||||
{
|
{
|
||||||
|
qDebug() << "Server GPU:" << m_Computer->gpuModel;
|
||||||
|
|
||||||
LiInitializeVideoCallbacks(&m_VideoCallbacks);
|
LiInitializeVideoCallbacks(&m_VideoCallbacks);
|
||||||
m_VideoCallbacks.setup = drSetup;
|
m_VideoCallbacks.setup = drSetup;
|
||||||
m_VideoCallbacks.submitDecodeUnit = drSubmitDecodeUnit;
|
m_VideoCallbacks.submitDecodeUnit = drSubmitDecodeUnit;
|
||||||
@@ -170,7 +246,11 @@ Session::Session(NvComputer* computer, NvApp& app)
|
|||||||
m_VideoCallbacks.capabilities |= CAPABILITY_DIRECT_SUBMIT;
|
m_VideoCallbacks.capabilities |= CAPABILITY_DIRECT_SUBMIT;
|
||||||
|
|
||||||
// Slice up to 4 times for parallel decode, once slice per core
|
// Slice up to 4 times for parallel decode, once slice per core
|
||||||
m_VideoCallbacks.capabilities |= CAPABILITY_SLICES_PER_FRAME(qMin(MAX_SLICES, SDL_GetCPUCount()));
|
int slices = qMin(MAX_SLICES, SDL_GetCPUCount());
|
||||||
|
m_VideoCallbacks.capabilities |= CAPABILITY_SLICES_PER_FRAME(slices);
|
||||||
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Encoder configured for %d slices per frame",
|
||||||
|
slices);
|
||||||
|
|
||||||
LiInitializeStreamConfiguration(&m_StreamConfig);
|
LiInitializeStreamConfiguration(&m_StreamConfig);
|
||||||
m_StreamConfig.width = m_Preferences.width;
|
m_StreamConfig.width = m_Preferences.width;
|
||||||
@@ -178,14 +258,21 @@ 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++) {
|
|
||||||
m_StreamConfig.remoteInputAesKey[i] =
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
(char)(QRandomGenerator::global()->generate() % 256);
|
"Video bitrate: %d kbps",
|
||||||
}
|
m_StreamConfig.bitrate);
|
||||||
*(int*)m_StreamConfig.remoteInputAesIv = qToBigEndian(QRandomGenerator::global()->generate());
|
|
||||||
|
RAND_bytes(reinterpret_cast<unsigned char*>(m_StreamConfig.remoteInputAesKey),
|
||||||
|
sizeof(m_StreamConfig.remoteInputAesKey));
|
||||||
|
|
||||||
|
// Only the first 4 bytes are populated in the RI key IV
|
||||||
|
RAND_bytes(reinterpret_cast<unsigned char*>(m_StreamConfig.remoteInputAesIv), 4);
|
||||||
|
|
||||||
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:
|
||||||
@@ -196,6 +283,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:
|
||||||
@@ -237,28 +328,59 @@ Session::Session(NvComputer* computer, NvApp& app)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void Session::emitLaunchWarning(QString text)
|
||||||
|
{
|
||||||
|
// Emit the warning to the UI
|
||||||
|
emit displayLaunchWarning(text);
|
||||||
|
|
||||||
|
// Wait a little bit so the user can actually read what we just said.
|
||||||
|
// This wait is a little longer than the actual toast timeout (3 seconds)
|
||||||
|
// to allow it to transition off the screen before continuing.
|
||||||
|
uint32_t start = SDL_GetTicks();
|
||||||
|
while (!SDL_TICKS_PASSED(SDL_GetTicks(), start + 3500)) {
|
||||||
|
// Pump the UI loop while we wait
|
||||||
|
SDL_Delay(5);
|
||||||
|
QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
bool Session::validateLaunch()
|
bool Session::validateLaunch()
|
||||||
{
|
{
|
||||||
QStringList warningList;
|
QStringList warningList;
|
||||||
|
|
||||||
|
if (m_Preferences.videoDecoderSelection == StreamingPreferences::VDS_FORCE_SOFTWARE) {
|
||||||
|
emitLaunchWarning("Your settings selection to force software decoding may cause poor streaming performance.");
|
||||||
|
}
|
||||||
|
|
||||||
if (m_StreamConfig.supportsHevc) {
|
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,
|
||||||
|
m_StreamConfig.fps) &&
|
||||||
|
m_Preferences.videoDecoderSelection == StreamingPreferences::VDS_AUTO) {
|
||||||
|
if (hevcForced) {
|
||||||
|
emitLaunchWarning("Using software decoding due to your selection to force HEVC without GPU support. This may cause poor streaming performance.");
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
emitLaunchWarning("This PC's GPU doesn't support HEVC decoding.");
|
||||||
|
m_StreamConfig.supportsHevc = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (!isHardwareDecodeAvailable(m_Preferences.videoDecoderSelection,
|
|
||||||
VIDEO_FORMAT_H265,
|
if (hevcForced) {
|
||||||
m_StreamConfig.width,
|
if (m_Computer->maxLumaPixelsHEVC == 0) {
|
||||||
m_StreamConfig.height,
|
emitLaunchWarning("Your host PC GPU doesn't support HEVC. "
|
||||||
m_StreamConfig.fps)) {
|
"A GeForce GTX 900-series (Maxwell) or later GPU is required for HEVC streaming.");
|
||||||
// NOTE: HEVC currently uses only 1 slice regardless of what
|
|
||||||
// we provide in CAPABILITY_SLICES_PER_FRAME(), so we should
|
// Moonlight-common-c will handle this case already, but we want
|
||||||
// never use it for software decoding (unless common-c starts
|
// to set this explicitly here so we can do our hardware acceleration
|
||||||
// respecting it for HEVC).
|
// check below.
|
||||||
m_StreamConfig.supportsHevc = false;
|
m_StreamConfig.supportsHevc = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -268,19 +390,19 @@ bool Session::validateLaunch()
|
|||||||
|
|
||||||
// Check that the app supports HDR
|
// Check that the app supports HDR
|
||||||
if (!m_App.hdrSupported) {
|
if (!m_App.hdrSupported) {
|
||||||
emit displayLaunchWarning(m_App.name + " doesn't support HDR10.");
|
emitLaunchWarning(m_App.name + " doesn't support HDR10.");
|
||||||
}
|
}
|
||||||
// Check that the server GPU supports HDR
|
// Check that the server GPU supports HDR
|
||||||
else if (!(m_Computer->serverCodecModeSupport & 0x200)) {
|
else if (!(m_Computer->serverCodecModeSupport & 0x200)) {
|
||||||
emit displayLaunchWarning("Your host PC GPU doesn't support HDR streaming. "
|
emitLaunchWarning("Your host PC GPU doesn't support HDR streaming. "
|
||||||
"A GeForce GTX 1000-series (Pascal) or later GPU is required for HDR streaming.");
|
"A GeForce GTX 1000-series (Pascal) or later GPU is required for HDR streaming.");
|
||||||
}
|
}
|
||||||
else if (!isHardwareDecodeAvailable(m_Preferences.videoDecoderSelection,
|
else if (!isHardwareDecodeAvailable(m_Preferences.videoDecoderSelection,
|
||||||
VIDEO_FORMAT_H265_MAIN10,
|
VIDEO_FORMAT_H265_MAIN10,
|
||||||
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.");
|
emitLaunchWarning("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
|
||||||
@@ -293,7 +415,7 @@ bool Session::validateLaunch()
|
|||||||
if (m_StreamConfig.width >= 3840) {
|
if (m_StreamConfig.width >= 3840) {
|
||||||
// Only allow 4K on GFE 3.x+
|
// Only allow 4K on GFE 3.x+
|
||||||
if (m_Computer->gfeVersion.isNull() || m_Computer->gfeVersion.startsWith("2.")) {
|
if (m_Computer->gfeVersion.isNull() || m_Computer->gfeVersion.startsWith("2.")) {
|
||||||
emit displayLaunchWarning("GeForce Experience 3.0 or higher is required for 4K streaming.");
|
emitLaunchWarning("GeForce Experience 3.0 or higher is required for 4K streaming.");
|
||||||
|
|
||||||
m_StreamConfig.width = 1920;
|
m_StreamConfig.width = 1920;
|
||||||
m_StreamConfig.height = 1080;
|
m_StreamConfig.height = 1080;
|
||||||
@@ -301,15 +423,31 @@ bool Session::validateLaunch()
|
|||||||
// This list is sorted from least to greatest
|
// This list is sorted from least to greatest
|
||||||
else if (m_Computer->displayModes.last().width < 3840 ||
|
else if (m_Computer->displayModes.last().width < 3840 ||
|
||||||
(m_Computer->displayModes.last().refreshRate < 60 && m_StreamConfig.fps >= 60)) {
|
(m_Computer->displayModes.last().refreshRate < 60 && m_StreamConfig.fps >= 60)) {
|
||||||
emit displayLaunchWarning("Your host PC GPU doesn't support 4K streaming. "
|
emitLaunchWarning("Your host PC GPU doesn't support 4K streaming. "
|
||||||
"A GeForce GTX 900-series (Maxwell) or later GPU is required for 4K streaming.");
|
"A GeForce GTX 900-series (Maxwell) or later GPU is required for 4K streaming.");
|
||||||
|
|
||||||
m_StreamConfig.width = 1920;
|
m_StreamConfig.width = 1920;
|
||||||
m_StreamConfig.height = 1080;
|
m_StreamConfig.height = 1080;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Always allow the launch to proceed for now
|
if (m_Preferences.videoDecoderSelection == StreamingPreferences::VDS_FORCE_HARDWARE &&
|
||||||
|
!isHardwareDecodeAvailable(m_Preferences.videoDecoderSelection,
|
||||||
|
m_StreamConfig.supportsHevc ? VIDEO_FORMAT_H265 : VIDEO_FORMAT_H264,
|
||||||
|
m_StreamConfig.width,
|
||||||
|
m_StreamConfig.height,
|
||||||
|
m_StreamConfig.fps)) {
|
||||||
|
if (m_Preferences.videoCodecConfig == StreamingPreferences::VCC_AUTO) {
|
||||||
|
emit displayLaunchError("Your selection to force hardware decoding cannot be satisfied due to missing hardware decoding support on this PC's GPU.");
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
emit displayLaunchError("Your codec selection and force hardware decoding setting are not compatible. This PC's GPU lacks support for decoding your chosen codec.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fail the launch, because we won't manage to get a decoder for the actual stream
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -320,9 +458,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;
|
||||||
@@ -330,6 +465,130 @@ class DeferredSessionCleanupTask : public QRunnable
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
void Session::getWindowDimensions(bool fullScreen,
|
||||||
|
int& x, int& y,
|
||||||
|
int& width, int& height)
|
||||||
|
{
|
||||||
|
int displayIndex = 0;
|
||||||
|
|
||||||
|
// If there's a display matching this exact resolution, pick that
|
||||||
|
// one (for native full-screen streaming). Otherwise, assume
|
||||||
|
// display 0 for now. TODO: Default to the screen that the Qt window is on
|
||||||
|
if (fullScreen) {
|
||||||
|
for (int i = 0; i < SDL_GetNumVideoDisplays(); i++) {
|
||||||
|
SDL_DisplayMode mode;
|
||||||
|
if (SDL_GetCurrentDisplayMode(i, &mode) == 0 &&
|
||||||
|
m_ActiveVideoWidth == mode.w &&
|
||||||
|
m_ActiveVideoHeight == mode.h) {
|
||||||
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Found exact resolution match on display: %d",
|
||||||
|
i);
|
||||||
|
displayIndex = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (m_Window != nullptr) {
|
||||||
|
displayIndex = SDL_GetWindowDisplayIndex(m_Window);
|
||||||
|
if (displayIndex < 0) {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"SDL_GetWindowDisplayIndex() failed: %s",
|
||||||
|
SDL_GetError());
|
||||||
|
|
||||||
|
// Assume display 0
|
||||||
|
displayIndex = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fullScreen) {
|
||||||
|
SDL_DisplayMode currentMode;
|
||||||
|
|
||||||
|
if (SDL_GetCurrentDisplayMode(displayIndex, ¤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;
|
||||||
|
|
||||||
|
// If the stream window can fit within the usable drawing area with 1:1
|
||||||
|
// scaling, do that rather than filling the screen.
|
||||||
|
if (m_StreamConfig.width < width && m_StreamConfig.height < height) {
|
||||||
|
width = m_StreamConfig.width;
|
||||||
|
height = m_StreamConfig.height;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"SDL_GetDisplayUsableBounds() failed: %s",
|
||||||
|
SDL_GetError());
|
||||||
|
|
||||||
|
width = m_StreamConfig.width;
|
||||||
|
height = m_StreamConfig.height;
|
||||||
|
x = y = SDL_WINDOWPOS_UNDEFINED_DISPLAY(displayIndex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Session::toggleFullscreen()
|
||||||
|
{
|
||||||
|
bool fullScreen = !(SDL_GetWindowFlags(m_Window) & SDL_OS_FULLSCREEN_FLAG);
|
||||||
|
|
||||||
|
int x, y, width, height;
|
||||||
|
|
||||||
|
// If we're leaving full screen, toggle out before setting our size and position
|
||||||
|
// that way we have accurate display size metrics (not the size our stream changed it to).
|
||||||
|
if (!fullScreen) {
|
||||||
|
SDL_SetWindowFullscreen(m_Window, 0);
|
||||||
|
SDL_SetWindowResizable(m_Window, SDL_TRUE);
|
||||||
|
}
|
||||||
|
|
||||||
|
getWindowDimensions(fullScreen, x, y, width, height);
|
||||||
|
|
||||||
|
SDL_SetWindowPosition(m_Window, x, y);
|
||||||
|
SDL_SetWindowSize(m_Window, width, height);
|
||||||
|
|
||||||
|
if (fullScreen) {
|
||||||
|
SDL_SetWindowResizable(m_Window, SDL_FALSE);
|
||||||
|
SDL_SetWindowFullscreen(m_Window, SDL_OS_FULLSCREEN_FLAG);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void Session::exec()
|
void Session::exec()
|
||||||
{
|
{
|
||||||
// Check for validation errors/warnings and emit
|
// Check for validation errors/warnings and emit
|
||||||
@@ -356,6 +615,18 @@ void Session::exec()
|
|||||||
Q_ASSERT(m_Computer->currentGameId == 0 ||
|
Q_ASSERT(m_Computer->currentGameId == 0 ||
|
||||||
m_Computer->currentGameId == m_App.id);
|
m_Computer->currentGameId == m_App.id);
|
||||||
|
|
||||||
|
bool enableGameOptimizations = false;
|
||||||
|
for (const NvDisplayMode &mode : m_Computer->displayModes) {
|
||||||
|
if (mode.width == m_StreamConfig.width &&
|
||||||
|
mode.height == m_StreamConfig.height) {
|
||||||
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Found host supported resolution: %dx%d",
|
||||||
|
mode.width, mode.height);
|
||||||
|
enableGameOptimizations = prefs.gameOptimizations;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
NvHTTP http(m_Computer->activeAddress);
|
NvHTTP http(m_Computer->activeAddress);
|
||||||
if (m_Computer->currentGameId != 0) {
|
if (m_Computer->currentGameId != 0) {
|
||||||
@@ -363,7 +634,7 @@ void Session::exec()
|
|||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
http.launchApp(m_App.id, &m_StreamConfig,
|
http.launchApp(m_App.id, &m_StreamConfig,
|
||||||
prefs.gameOptimizations,
|
enableGameOptimizations,
|
||||||
prefs.playAudioOnHost,
|
prefs.playAudioOnHost,
|
||||||
inputHandler.getAttachedGamepadMask());
|
inputHandler.getAttachedGamepadMask());
|
||||||
}
|
}
|
||||||
@@ -373,64 +644,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;
|
||||||
}
|
}
|
||||||
@@ -458,6 +677,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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -465,11 +685,73 @@ 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",
|
||||||
SDL_SetRelativeMouseMode(SDL_TRUE);
|
x,
|
||||||
|
y,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
m_Preferences.fullScreen ?
|
||||||
|
SDL_OS_FULLSCREEN_FLAG : 0);
|
||||||
|
if (!m_Window) {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"SDL_CreateWindow() failed: %s",
|
||||||
|
SDL_GetError());
|
||||||
|
s_ActiveSessionSemaphore.release();
|
||||||
|
SDL_QuitSubSystem(SDL_INIT_VIDEO);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// For non-full screen windows, call getWindowDimensions()
|
||||||
|
// again after creating a window to allow it to account
|
||||||
|
// for window chrome size.
|
||||||
|
if (!m_Preferences.fullScreen) {
|
||||||
|
getWindowDimensions(false, x, y, width, height);
|
||||||
|
|
||||||
|
SDL_SetWindowPosition(m_Window, x, y);
|
||||||
|
SDL_SetWindowSize(m_Window, width, height);
|
||||||
|
|
||||||
|
// Passing SDL_WINDOW_RESIZABLE to set this during window
|
||||||
|
// creation causes our window to be full screen for some reason
|
||||||
|
SDL_SetWindowResizable(m_Window, SDL_TRUE);
|
||||||
|
}
|
||||||
|
|
||||||
|
QSvgRenderer svgIconRenderer(QString(":/res/moonlight.svg"));
|
||||||
|
QImage svgImage(ICON_SIZE, ICON_SIZE, QImage::Format_RGBA8888);
|
||||||
|
svgImage.fill(0);
|
||||||
|
|
||||||
|
QPainter svgPainter(&svgImage);
|
||||||
|
svgIconRenderer.render(&svgPainter);
|
||||||
|
SDL_Surface* iconSurface = SDL_CreateRGBSurfaceWithFormatFrom((void*)svgImage.constBits(),
|
||||||
|
svgImage.width(),
|
||||||
|
svgImage.height(),
|
||||||
|
32,
|
||||||
|
4 * svgImage.width(),
|
||||||
|
SDL_PIXELFORMAT_RGBA32);
|
||||||
|
#ifndef Q_OS_DARWIN
|
||||||
|
// Other platforms seem to preserve our Qt icon when creating a new window
|
||||||
|
if (iconSurface != nullptr) {
|
||||||
|
SDL_SetWindowIcon(m_Window, iconSurface);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifndef QT_DEBUG
|
||||||
|
// Capture the mouse by default on release builds only.
|
||||||
|
// This prevents the mouse from becoming trapped inside
|
||||||
|
// Moonlight when it's halted at a debug break.
|
||||||
|
if (m_Preferences.fullScreen) {
|
||||||
|
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();
|
||||||
@@ -485,7 +767,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,
|
||||||
@@ -510,6 +800,42 @@ 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");
|
||||||
|
emit displayLaunchError("Unable to initialize video decoder. Please check your streaming settings and try again.");
|
||||||
|
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);
|
||||||
@@ -547,9 +873,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);
|
||||||
@@ -557,6 +880,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);
|
||||||
|
|
||||||
@@ -35,13 +39,17 @@ signals:
|
|||||||
private:
|
private:
|
||||||
bool validateLaunch();
|
bool validateLaunch();
|
||||||
|
|
||||||
|
void emitLaunchWarning(QString text);
|
||||||
|
|
||||||
int getDecoderCapabilities();
|
int getDecoderCapabilities();
|
||||||
|
|
||||||
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 +102,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;
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
#include "streamutils.h"
|
||||||
|
|
||||||
|
void StreamUtils::scaleSourceToDestinationSurface(SDL_Rect* src, SDL_Rect* dst)
|
||||||
|
{
|
||||||
|
int dstH = dst->w * src->h / src->w;
|
||||||
|
int dstW = dst->h * src->w / src->h;
|
||||||
|
|
||||||
|
if (dstH > dst->h) {
|
||||||
|
dst->y = 0;
|
||||||
|
dst->x = (dst->w - dstW) / 2;
|
||||||
|
dst->w = dstW;
|
||||||
|
SDL_assert(dst->w * src->h / src->w <= dst->h);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
dst->x = 0;
|
||||||
|
dst->y = (dst->h - dstH) / 2;
|
||||||
|
dst->h = dstH;
|
||||||
|
SDL_assert(dst->h * src->w / src->h <= dst->w);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <SDL.h>
|
||||||
|
|
||||||
|
class StreamUtils
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
static
|
||||||
|
void scaleSourceToDestinationSurface(SDL_Rect* src, SDL_Rect* dst);
|
||||||
|
};
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
#include <Initguid.h>
|
#include <initguid.h>
|
||||||
#include "dxva2.h"
|
#include "dxva2.h"
|
||||||
|
#include "../ffmpeg.h"
|
||||||
|
#include <streaming/streamutils.h>
|
||||||
|
|
||||||
#include <Limelight.h>
|
#include <Limelight.h>
|
||||||
|
|
||||||
@@ -8,16 +10,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));
|
||||||
@@ -75,11 +77,8 @@ bool DXVA2Renderer::prepareDecoderContext(AVCodecContext* context)
|
|||||||
|
|
||||||
context->hwaccel_context = &m_DXVAContext;
|
context->hwaccel_context = &m_DXVAContext;
|
||||||
|
|
||||||
context->get_format = ffGetFormat;
|
|
||||||
context->get_buffer2 = ffGetBuffer2;
|
context->get_buffer2 = ffGetBuffer2;
|
||||||
|
|
||||||
context->opaque = this;
|
|
||||||
|
|
||||||
m_Pool = av_buffer_pool_init2(ARRAYSIZE(m_DecSurfaces), this, ffPoolAlloc, nullptr);
|
m_Pool = av_buffer_pool_init2(ARRAYSIZE(m_DecSurfaces), this, ffPoolAlloc, nullptr);
|
||||||
if (!m_Pool) {
|
if (!m_Pool) {
|
||||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
@@ -87,12 +86,15 @@ bool DXVA2Renderer::prepareDecoderContext(AVCodecContext* context)
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Using DXVA2 accelerated renderer");
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
int DXVA2Renderer::ffGetBuffer2(AVCodecContext* context, AVFrame* frame, int)
|
int DXVA2Renderer::ffGetBuffer2(AVCodecContext* context, AVFrame* frame, int)
|
||||||
{
|
{
|
||||||
DXVA2Renderer* me = reinterpret_cast<DXVA2Renderer*>(context->opaque);
|
DXVA2Renderer* me = (DXVA2Renderer*)((FFmpegVideoDecoder*)context->opaque)->getRenderer();
|
||||||
|
|
||||||
frame->buf[0] = av_buffer_pool_get(me->m_Pool);
|
frame->buf[0] = av_buffer_pool_get(me->m_Pool);
|
||||||
if (!frame->buf[0]) {
|
if (!frame->buf[0]) {
|
||||||
@@ -101,31 +103,20 @@ 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
enum AVPixelFormat DXVA2Renderer::ffGetFormat(AVCodecContext*,
|
|
||||||
const enum AVPixelFormat* pixFmts)
|
|
||||||
{
|
|
||||||
const enum AVPixelFormat *p;
|
|
||||||
|
|
||||||
for (p = pixFmts; *p != -1; p++) {
|
|
||||||
if (*p == AV_PIX_FMT_DXVA2_VLD)
|
|
||||||
return *p;
|
|
||||||
}
|
|
||||||
|
|
||||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
|
||||||
"Failed to find DXVA2 HW surface format");
|
|
||||||
return AV_PIX_FMT_NONE;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool DXVA2Renderer::initializeDecoder()
|
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)) {
|
||||||
@@ -208,18 +199,11 @@ bool DXVA2Renderer::initializeDecoder()
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
int alignment;
|
// Alignment was already taken care of
|
||||||
|
SDL_assert(m_Desc.SampleWidth % 16 == 0);
|
||||||
// HEVC using DXVA requires 128B alignment
|
SDL_assert(m_Desc.SampleHeight % 16 == 0);
|
||||||
if (m_VideoFormat & VIDEO_FORMAT_MASK_H265) {
|
hr = m_DecService->CreateSurface(m_Desc.SampleWidth,
|
||||||
alignment = 128;
|
m_Desc.SampleHeight,
|
||||||
}
|
|
||||||
else {
|
|
||||||
alignment = 16;
|
|
||||||
}
|
|
||||||
|
|
||||||
hr = m_DecService->CreateSurface(FFALIGN(m_Width, alignment),
|
|
||||||
FFALIGN(m_Height, alignment),
|
|
||||||
ARRAYSIZE(m_DecSurfaces) - 1,
|
ARRAYSIZE(m_DecSurfaces) - 1,
|
||||||
m_Desc.Format,
|
m_Desc.Format,
|
||||||
D3DPOOL_DEFAULT,
|
D3DPOOL_DEFAULT,
|
||||||
@@ -262,6 +246,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 +289,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 +333,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 +453,31 @@ 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.SampleHeight = m_Height;
|
int alignment;
|
||||||
m_Desc.SampleFormat.VideoChromaSubsampling = DXVA2_VideoChromaSubsampling_ProgressiveChroma;
|
|
||||||
m_Desc.SampleFormat.NominalRange = DXVA2_NominalRange_0_255;
|
// HEVC using DXVA requires 128B alignment
|
||||||
m_Desc.SampleFormat.VideoTransferMatrix = DXVA2_VideoTransferMatrix_BT709;
|
if (m_VideoFormat & VIDEO_FORMAT_MASK_H265) {
|
||||||
m_Desc.SampleFormat.VideoLighting = DXVA2_VideoLighting_dim;
|
alignment = 128;
|
||||||
m_Desc.SampleFormat.VideoPrimaries = DXVA2_VideoPrimaries_BT709;
|
}
|
||||||
m_Desc.SampleFormat.VideoTransferFunction = DXVA2_VideoTransFunc_709;
|
else {
|
||||||
|
alignment = 16;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_Desc.SampleWidth = FFALIGN(m_VideoWidth, alignment);
|
||||||
|
m_Desc.SampleHeight = FFALIGN(m_VideoHeight, alignment);
|
||||||
|
m_Desc.SampleFormat.VideoChromaSubsampling = DXVA2_VideoChromaSubsampling_Unknown;
|
||||||
|
m_Desc.SampleFormat.NominalRange = DXVA2_NominalRange_Unknown;
|
||||||
|
m_Desc.SampleFormat.VideoTransferMatrix = DXVA2_VideoTransferMatrix_Unknown;
|
||||||
|
m_Desc.SampleFormat.VideoLighting = DXVA2_VideoLighting_Unknown;
|
||||||
|
m_Desc.SampleFormat.VideoPrimaries = DXVA2_VideoPrimaries_Unknown;
|
||||||
|
m_Desc.SampleFormat.VideoTransferFunction = DXVA2_VideoTransFunc_Unknown;
|
||||||
m_Desc.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,43 +497,126 @@ 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 = {};
|
||||||
sample.Start = m_FrameIndex;
|
sample.Start = m_FrameIndex;
|
||||||
sample.End = m_FrameIndex + 1;
|
sample.End = m_FrameIndex + 1;
|
||||||
sample.SrcSurface = surface;
|
sample.SrcSurface = surface;
|
||||||
sample.SrcRect.right = m_Desc.SampleWidth;
|
sample.SrcRect.right = m_VideoWidth;
|
||||||
sample.SrcRect.bottom = m_Desc.SampleHeight;
|
sample.SrcRect.bottom = m_VideoHeight;
|
||||||
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
|
||||||
|
SDL_Rect src, dst;
|
||||||
|
src.x = src.y = 0;
|
||||||
|
src.w = m_VideoWidth;
|
||||||
|
src.h = m_VideoHeight;
|
||||||
|
dst.x = dst.y = 0;
|
||||||
|
dst.w = m_DisplayWidth;
|
||||||
|
dst.h = m_DisplayHeight;
|
||||||
|
|
||||||
|
StreamUtils::scaleSourceToDestinationSurface(&src, &dst);
|
||||||
|
|
||||||
|
sample.DstRect.left = dst.x;
|
||||||
|
sample.DstRect.right = dst.x + dst.w;
|
||||||
|
sample.DstRect.top = dst.y;
|
||||||
|
sample.DstRect.bottom = dst.y + dst.h;
|
||||||
|
|
||||||
DXVA2_VideoProcessBltParams bltParams = {};
|
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 +628,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);
|
||||||
@@ -34,13 +35,12 @@ private:
|
|||||||
static
|
static
|
||||||
int ffGetBuffer2(AVCodecContext* context, AVFrame* frame, int flags);
|
int ffGetBuffer2(AVCodecContext* context, AVFrame* frame, int flags);
|
||||||
|
|
||||||
static
|
|
||||||
enum AVPixelFormat ffGetFormat(AVCodecContext*,
|
|
||||||
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,6 +23,10 @@ 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -41,6 +47,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 +76,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,202 @@
|
|||||||
|
#include "vaapi.h"
|
||||||
|
#include <streaming/streamutils.h>
|
||||||
|
|
||||||
|
#include <SDL_syswm.h>
|
||||||
|
|
||||||
|
VAAPIRenderer::VAAPIRenderer()
|
||||||
|
: m_HwContext(nullptr)
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
VAAPIRenderer::~VAAPIRenderer()
|
||||||
|
{
|
||||||
|
if (m_HwContext != nullptr) {
|
||||||
|
AVHWDeviceContext* deviceContext = (AVHWDeviceContext*)m_HwContext->data;
|
||||||
|
AVVAAPIDeviceContext* vaDeviceContext = (AVVAAPIDeviceContext*)deviceContext->hwctx;
|
||||||
|
|
||||||
|
// Hold onto this VADisplay since we'll need it to uninitialize VAAPI
|
||||||
|
VADisplay display = vaDeviceContext->display;
|
||||||
|
|
||||||
|
av_buffer_unref(&m_HwContext);
|
||||||
|
|
||||||
|
if (display) {
|
||||||
|
vaTerminate(display);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"SDL_GetWindowWMInfo() failed: %s",
|
||||||
|
SDL_GetError());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_HwContext = av_hwdevice_ctx_alloc(AV_HWDEVICE_TYPE_VAAPI);
|
||||||
|
if (!m_HwContext) {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Failed to allocate VAAPI context");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
AVHWDeviceContext* deviceContext = (AVHWDeviceContext*)m_HwContext->data;
|
||||||
|
AVVAAPIDeviceContext* vaDeviceContext = (AVVAAPIDeviceContext*)deviceContext->hwctx;
|
||||||
|
|
||||||
|
m_WindowSystem = info.subsystem;
|
||||||
|
if (info.subsystem == SDL_SYSWM_X11) {
|
||||||
|
#ifdef HAVE_LIBVA_X11
|
||||||
|
m_XWindow = info.info.x11.window;
|
||||||
|
vaDeviceContext->display = vaGetDisplay(info.info.x11.display);
|
||||||
|
if (!vaDeviceContext->display) {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Unable to open X11 display for VAAPI");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Moonlight not compiled with VAAPI X11 support!");
|
||||||
|
return false;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
else if (info.subsystem == SDL_SYSWM_WAYLAND) {
|
||||||
|
#ifdef HAVE_LIBVA_WAYLAND
|
||||||
|
m_WaylandSurface = info.info.wl.surface;
|
||||||
|
m_WaylandDisplay = info.info.wl.display;
|
||||||
|
vaDeviceContext->display = vaGetDisplayWl(info.info.wl.display);
|
||||||
|
if (!vaDeviceContext->display) {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Unable to open Wayland display for VAAPI");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Moonlight not compiled with VAAPI Wayland support!");
|
||||||
|
return false;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Unsupported VAAPI rendering subsystem: %d",
|
||||||
|
info.subsystem);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
int major, minor;
|
||||||
|
VAStatus status;
|
||||||
|
status = vaInitialize(vaDeviceContext->display, &major, &minor);
|
||||||
|
if (status != VA_STATUS_SUCCESS) {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Failed to initialize VAAPI: %d",
|
||||||
|
status);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Initialized VAAPI %d.%d",
|
||||||
|
major, minor);
|
||||||
|
|
||||||
|
// This will populate the driver_quirks
|
||||||
|
err = av_hwdevice_ctx_init(m_HwContext);
|
||||||
|
if (err < 0) {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Failed to initialize VAAPI context: %d",
|
||||||
|
err);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// This quirk is set for the VDPAU wrapper which doesn't work with our VAAPI renderer
|
||||||
|
if (vaDeviceContext->driver_quirks & AV_VAAPI_DRIVER_QUIRK_SURFACE_ATTRIBUTES) {
|
||||||
|
// Fail and let our VDPAU renderer pick this up
|
||||||
|
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Avoiding VDPAU wrapper for VAAPI decoding");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool
|
||||||
|
VAAPIRenderer::prepareDecoderContext(AVCodecContext* context)
|
||||||
|
{
|
||||||
|
context->hw_device_ctx = av_buffer_ref(m_HwContext);
|
||||||
|
|
||||||
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Using VAAPI accelerated renderer on %s",
|
||||||
|
m_WindowSystem == SDL_SYSWM_X11 ? "X11" : "Wayland");
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
VAAPIRenderer::renderFrame(AVFrame* frame)
|
||||||
|
{
|
||||||
|
VASurfaceID surface = (VASurfaceID)(uintptr_t)frame->data[3];
|
||||||
|
AVHWDeviceContext* deviceContext = (AVHWDeviceContext*)m_HwContext->data;
|
||||||
|
AVVAAPIDeviceContext* vaDeviceContext = (AVVAAPIDeviceContext*)deviceContext->hwctx;
|
||||||
|
|
||||||
|
SDL_Rect src, dst;
|
||||||
|
src.x = src.y = 0;
|
||||||
|
src.w = m_VideoWidth;
|
||||||
|
src.h = m_VideoHeight;
|
||||||
|
dst.x = dst.y = 0;
|
||||||
|
dst.w = m_DisplayWidth;
|
||||||
|
dst.h = m_DisplayHeight;
|
||||||
|
|
||||||
|
StreamUtils::scaleSourceToDestinationSurface(&src, &dst);
|
||||||
|
|
||||||
|
if (m_WindowSystem == SDL_SYSWM_X11) {
|
||||||
|
#ifdef HAVE_LIBVA_X11
|
||||||
|
vaPutSurface(vaDeviceContext->display,
|
||||||
|
surface,
|
||||||
|
m_XWindow,
|
||||||
|
0, 0,
|
||||||
|
m_VideoWidth, m_VideoHeight,
|
||||||
|
dst.x, dst.y,
|
||||||
|
dst.w, dst.h,
|
||||||
|
NULL, 0, 0);
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
else if (m_WindowSystem == SDL_SYSWM_WAYLAND) {
|
||||||
|
#ifdef HAVE_LIBVA_WAYLAND
|
||||||
|
struct wl_buffer* buffer;
|
||||||
|
VAStatus status;
|
||||||
|
|
||||||
|
status = vaGetSurfaceBufferWl(vaDeviceContext->display,
|
||||||
|
surface,
|
||||||
|
VA_FRAME_PICTURE,
|
||||||
|
&buffer);
|
||||||
|
if (status == VA_STATUS_SUCCESS) {
|
||||||
|
wl_surface_attach(m_WaylandSurface, buffer, 0, 0);
|
||||||
|
wl_surface_damage(m_WaylandSurface, dst.x, dst.y, dst.w, dst.h);
|
||||||
|
|
||||||
|
wl_display_flush(m_WaylandDisplay);
|
||||||
|
wl_surface_commit(m_WaylandSurface);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"vaGetSurfaceBufferWl failed(): %d",
|
||||||
|
status);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// We don't accept anything else in initialize().
|
||||||
|
SDL_assert(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
av_frame_free(&frame);
|
||||||
|
}
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "renderer.h"
|
||||||
|
|
||||||
|
// Avoid X11 if SDL was built without it
|
||||||
|
#ifndef SDL_VIDEO_DRIVER_X11
|
||||||
|
#warning Unable to use libva-x11 without SDL support
|
||||||
|
#undef HAVE_LIBVA_X11
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// Avoid Wayland if SDL was built without it
|
||||||
|
#ifndef SDL_VIDEO_DRIVER_WAYLAND
|
||||||
|
#warning Unable to use libva-wayland without SDL support
|
||||||
|
#undef HAVE_LIBVA_WAYLAND
|
||||||
|
#endif
|
||||||
|
|
||||||
|
extern "C" {
|
||||||
|
#include <va/va.h>
|
||||||
|
#ifdef HAVE_LIBVA_X11
|
||||||
|
#include <va/va_x11.h>
|
||||||
|
#endif
|
||||||
|
#ifdef HAVE_LIBVA_WAYLAND
|
||||||
|
#include <va/va_wayland.h>
|
||||||
|
#endif
|
||||||
|
#include <libavutil/hwcontext_vaapi.h>
|
||||||
|
}
|
||||||
|
|
||||||
|
class VAAPIRenderer : public IFFmpegRenderer
|
||||||
|
{
|
||||||
|
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:
|
||||||
|
int m_WindowSystem;
|
||||||
|
AVBufferRef* m_HwContext;
|
||||||
|
|
||||||
|
#ifdef HAVE_LIBVA_X11
|
||||||
|
Window m_XWindow;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifdef HAVE_LIBVA_WAYLAND
|
||||||
|
struct wl_surface* m_WaylandSurface;
|
||||||
|
struct wl_display* m_WaylandDisplay;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
int m_VideoWidth;
|
||||||
|
int m_VideoHeight;
|
||||||
|
int m_DisplayWidth;
|
||||||
|
int m_DisplayHeight;
|
||||||
|
};
|
||||||
@@ -0,0 +1,332 @@
|
|||||||
|
#include "vdpau.h"
|
||||||
|
#include <streaming/streamutils.h>
|
||||||
|
|
||||||
|
#include <SDL_syswm.h>
|
||||||
|
|
||||||
|
#define BAIL_ON_FAIL(status, something) if ((status) != VDP_STATUS_OK) { \
|
||||||
|
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, \
|
||||||
|
#something " failed: %d", (status)); \
|
||||||
|
return false; \
|
||||||
|
}
|
||||||
|
|
||||||
|
#define GET_PROC_ADDRESS(id, func) status = vdpauCtx->get_proc_address(m_Device, id, (void**)func); \
|
||||||
|
BAIL_ON_FAIL(status, id)
|
||||||
|
|
||||||
|
const VdpRGBAFormat VDPAURenderer::k_OutputFormats[] = {
|
||||||
|
VDP_RGBA_FORMAT_B8G8R8A8,
|
||||||
|
VDP_RGBA_FORMAT_R8G8B8A8
|
||||||
|
};
|
||||||
|
|
||||||
|
VDPAURenderer::VDPAURenderer()
|
||||||
|
: m_HwContext(nullptr),
|
||||||
|
m_PresentationQueueTarget(0),
|
||||||
|
m_PresentationQueue(0),
|
||||||
|
m_VideoMixer(0),
|
||||||
|
m_NextSurfaceIndex(0)
|
||||||
|
{
|
||||||
|
SDL_zero(m_OutputSurface);
|
||||||
|
}
|
||||||
|
|
||||||
|
VDPAURenderer::~VDPAURenderer()
|
||||||
|
{
|
||||||
|
if (m_PresentationQueue != 0) {
|
||||||
|
m_VdpPresentationQueueDestroy(m_PresentationQueue);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (m_VideoMixer != 0) {
|
||||||
|
m_VdpVideoMixerDestroy(m_VideoMixer);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (m_PresentationQueueTarget != 0) {
|
||||||
|
m_VdpPresentationQueueTargetDestroy(m_PresentationQueueTarget);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < OUTPUT_SURFACE_COUNT; i++) {
|
||||||
|
if (m_OutputSurface[i] != 0) {
|
||||||
|
m_VdpOutputSurfaceDestroy(m_OutputSurface[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// This must be done last as it frees VDPAU context required to call
|
||||||
|
// the functions above.
|
||||||
|
if (m_HwContext != nullptr) {
|
||||||
|
av_buffer_unref(&m_HwContext);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool VDPAURenderer::initialize(SDL_Window* window, int, int width, int height)
|
||||||
|
{
|
||||||
|
int err;
|
||||||
|
VdpStatus status;
|
||||||
|
SDL_SysWMinfo info;
|
||||||
|
|
||||||
|
m_VideoWidth = width;
|
||||||
|
m_VideoHeight = height;
|
||||||
|
|
||||||
|
err = av_hwdevice_ctx_create(&m_HwContext,
|
||||||
|
AV_HWDEVICE_TYPE_VDPAU,
|
||||||
|
nullptr, nullptr, 0);
|
||||||
|
if (err < 0) {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Failed to create VDPAU context: %d",
|
||||||
|
err);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
AVHWDeviceContext* devCtx = (AVHWDeviceContext*)m_HwContext->data;
|
||||||
|
AVVDPAUDeviceContext* vdpauCtx = (AVVDPAUDeviceContext*)devCtx->hwctx;
|
||||||
|
m_Device = vdpauCtx->device;
|
||||||
|
|
||||||
|
GET_PROC_ADDRESS(VDP_FUNC_ID_GET_ERROR_STRING, &m_VdpGetErrorString);
|
||||||
|
GET_PROC_ADDRESS(VDP_FUNC_ID_PRESENTATION_QUEUE_TARGET_DESTROY, &m_VdpPresentationQueueTargetDestroy);
|
||||||
|
GET_PROC_ADDRESS(VDP_FUNC_ID_VIDEO_MIXER_CREATE, &m_VdpVideoMixerCreate);
|
||||||
|
GET_PROC_ADDRESS(VDP_FUNC_ID_VIDEO_MIXER_DESTROY, &m_VdpVideoMixerDestroy);
|
||||||
|
GET_PROC_ADDRESS(VDP_FUNC_ID_VIDEO_MIXER_RENDER, &m_VdpVideoMixerRender);
|
||||||
|
GET_PROC_ADDRESS(VDP_FUNC_ID_PRESENTATION_QUEUE_CREATE, &m_VdpPresentationQueueCreate);
|
||||||
|
GET_PROC_ADDRESS(VDP_FUNC_ID_PRESENTATION_QUEUE_DESTROY, &m_VdpPresentationQueueDestroy);
|
||||||
|
GET_PROC_ADDRESS(VDP_FUNC_ID_PRESENTATION_QUEUE_DISPLAY, &m_VdpPresentationQueueDisplay);
|
||||||
|
GET_PROC_ADDRESS(VDP_FUNC_ID_PRESENTATION_QUEUE_SET_BACKGROUND_COLOR, &m_VdpPresentationQueueSetBackgroundColor);
|
||||||
|
GET_PROC_ADDRESS(VDP_FUNC_ID_PRESENTATION_QUEUE_BLOCK_UNTIL_SURFACE_IDLE, &m_VdpPresentationQueueBlockUntilSurfaceIdle);
|
||||||
|
GET_PROC_ADDRESS(VDP_FUNC_ID_OUTPUT_SURFACE_CREATE, &m_VdpOutputSurfaceCreate);
|
||||||
|
GET_PROC_ADDRESS(VDP_FUNC_ID_OUTPUT_SURFACE_DESTROY, &m_VdpOutputSurfaceDestroy);
|
||||||
|
GET_PROC_ADDRESS(VDP_FUNC_ID_OUTPUT_SURFACE_QUERY_CAPABILITIES, &m_VdpOutputSurfaceQueryCapabilities);
|
||||||
|
GET_PROC_ADDRESS(VDP_FUNC_ID_VIDEO_SURFACE_GET_PARAMETERS, &m_VdpVideoSurfaceGetParameters);
|
||||||
|
|
||||||
|
SDL_GetWindowSize(window, (int*)&m_DisplayWidth, (int*)&m_DisplayHeight);
|
||||||
|
|
||||||
|
SDL_VERSION(&info.version);
|
||||||
|
|
||||||
|
if (!SDL_GetWindowWMInfo(window, &info)) {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"SDL_GetWindowWMInfo() failed: %s",
|
||||||
|
SDL_GetError());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
SDL_assert(info.subsystem == SDL_SYSWM_X11);
|
||||||
|
|
||||||
|
if (info.subsystem == SDL_SYSWM_X11) {
|
||||||
|
GET_PROC_ADDRESS(VDP_FUNC_ID_PRESENTATION_QUEUE_TARGET_CREATE_X11,
|
||||||
|
&m_VdpPresentationQueueTargetCreateX11);
|
||||||
|
status = m_VdpPresentationQueueTargetCreateX11(m_Device,
|
||||||
|
info.info.x11.window,
|
||||||
|
&m_PresentationQueueTarget);
|
||||||
|
if (status != VDP_STATUS_OK) {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"VdpPresentationQueueTargetCreateX11() failed: %s",
|
||||||
|
m_VdpGetErrorString(status));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (info.subsystem == SDL_SYSWM_WAYLAND) {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"VDPAU backend does not currently support Wayland");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Unsupported VDPAU rendering subsystem: %d",
|
||||||
|
info.subsystem);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try our available output formats to find something the GPU supports
|
||||||
|
bool foundFormat = false;
|
||||||
|
for (int i = 0; i < OUTPUT_SURFACE_FORMAT_COUNT; i++) {
|
||||||
|
VdpBool supported;
|
||||||
|
uint32_t maxWidth, maxHeight;
|
||||||
|
status = m_VdpOutputSurfaceQueryCapabilities(m_Device, k_OutputFormats[i],
|
||||||
|
&supported, &maxWidth, &maxHeight);
|
||||||
|
if (status != VDP_STATUS_OK) {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"VdpOutputSurfaceQueryCapabilities() failed: %s",
|
||||||
|
m_VdpGetErrorString(status));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (supported) {
|
||||||
|
if (m_DisplayWidth <= maxWidth && m_DisplayHeight <= maxHeight) {
|
||||||
|
m_OutputSurfaceFormat = k_OutputFormats[i];
|
||||||
|
foundFormat = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Display size not within capabilities %dx%d vs %dx%d",
|
||||||
|
m_DisplayWidth, m_DisplayWidth,
|
||||||
|
maxWidth, maxHeight);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!foundFormat) {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"No compatible output surface format found!");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the output surfaces
|
||||||
|
for (int i = 0; i < OUTPUT_SURFACE_COUNT; i++) {
|
||||||
|
// It seems there's some lazy freeing going on or something in VDPAU
|
||||||
|
// because we can get VDP_STATUS_RESOURCES, then wait a bit and it'll
|
||||||
|
// complete without a problem.
|
||||||
|
int tries = 1;
|
||||||
|
do {
|
||||||
|
status = m_VdpOutputSurfaceCreate(m_Device, m_OutputSurfaceFormat,
|
||||||
|
m_DisplayWidth, m_DisplayHeight,
|
||||||
|
&m_OutputSurface[i]);
|
||||||
|
if (status != VDP_STATUS_OK) {
|
||||||
|
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"VdpOutputSurfaceCreate() try #%d: %s",
|
||||||
|
tries,
|
||||||
|
m_VdpGetErrorString(status));
|
||||||
|
SDL_Delay(250);
|
||||||
|
}
|
||||||
|
} while (status == VDP_STATUS_RESOURCES && ++tries <= 10);
|
||||||
|
|
||||||
|
if (status != VDP_STATUS_OK) {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"VdpOutputSurfaceCreate() failed: %s",
|
||||||
|
m_VdpGetErrorString(status));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
status = m_VdpPresentationQueueCreate(m_Device, m_PresentationQueueTarget,
|
||||||
|
&m_PresentationQueue);
|
||||||
|
if (status != VDP_STATUS_OK) {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"VdpPresentationQueueCreate() failed: %s",
|
||||||
|
m_VdpGetErrorString(status));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the background to opaque black
|
||||||
|
VdpColor color = {0.0, 0.0, 0.0, 1.0};
|
||||||
|
m_VdpPresentationQueueSetBackgroundColor(m_PresentationQueue, &color);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool VDPAURenderer::prepareDecoderContext(AVCodecContext* context)
|
||||||
|
{
|
||||||
|
context->hw_device_ctx = av_buffer_ref(m_HwContext);
|
||||||
|
|
||||||
|
// Allow HEVC usage on VDPAU. This was disabled by FFmpeg due to
|
||||||
|
// GL interop issues, but we use VDPAU for rendering so it's no issue.
|
||||||
|
// https://github.com/FFmpeg/FFmpeg/commit/64ecb78b7179cab2dbdf835463104679dbb7c895
|
||||||
|
context->hwaccel_flags |= AV_HWACCEL_FLAG_ALLOW_PROFILE_MISMATCH;
|
||||||
|
|
||||||
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Using VDPAU accelerated renderer");
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void VDPAURenderer::renderFrame(AVFrame* frame)
|
||||||
|
{
|
||||||
|
VdpStatus status;
|
||||||
|
VdpVideoSurface videoSurface = (VdpVideoSurface)(uintptr_t)frame->data[3];
|
||||||
|
|
||||||
|
// This is safe without locking because this is always called on the main thread
|
||||||
|
VdpOutputSurface chosenSurface = m_OutputSurface[m_NextSurfaceIndex];
|
||||||
|
m_NextSurfaceIndex = (m_NextSurfaceIndex + 1) % OUTPUT_SURFACE_COUNT;
|
||||||
|
|
||||||
|
// We need to create the mixer on the fly, because we don't know the dimensions
|
||||||
|
// of our video surfaces in advance of decoding
|
||||||
|
if (m_VideoMixer == 0) {
|
||||||
|
VdpChromaType chroma;
|
||||||
|
uint32_t videoSurfaceWidth, videoSurfaceHeight;
|
||||||
|
status = m_VdpVideoSurfaceGetParameters(videoSurface, &chroma, &videoSurfaceWidth, &videoSurfaceHeight);
|
||||||
|
if (status != VDP_STATUS_OK) {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"VdpVideoSurfaceGetParameters() failed: %s",
|
||||||
|
m_VdpGetErrorString(status));
|
||||||
|
av_frame_free(&frame);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"VDPAU surface size: %dx%d",
|
||||||
|
videoSurfaceWidth, videoSurfaceHeight);
|
||||||
|
|
||||||
|
#define PARAM_COUNT 2
|
||||||
|
const VdpVideoMixerParameter params[PARAM_COUNT] = {
|
||||||
|
VDP_VIDEO_MIXER_PARAMETER_VIDEO_SURFACE_WIDTH,
|
||||||
|
VDP_VIDEO_MIXER_PARAMETER_VIDEO_SURFACE_HEIGHT,
|
||||||
|
};
|
||||||
|
const void* const paramValues[PARAM_COUNT] = {
|
||||||
|
&videoSurfaceWidth,
|
||||||
|
&videoSurfaceHeight,
|
||||||
|
};
|
||||||
|
|
||||||
|
status = m_VdpVideoMixerCreate(m_Device, 0, nullptr,
|
||||||
|
PARAM_COUNT, params, paramValues,
|
||||||
|
&m_VideoMixer);
|
||||||
|
if (status != VDP_STATUS_OK) {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"VdpVideoMixerCreate() failed: %s",
|
||||||
|
m_VdpGetErrorString(status));
|
||||||
|
av_frame_free(&frame);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for this frame to be off the screen
|
||||||
|
VdpTime pts;
|
||||||
|
m_VdpPresentationQueueBlockUntilSurfaceIdle(m_PresentationQueue, chosenSurface, &pts);
|
||||||
|
|
||||||
|
VdpRect sourceRect, outputRect;
|
||||||
|
|
||||||
|
SDL_Rect src, dst;
|
||||||
|
src.x = src.y = 0;
|
||||||
|
src.w = m_VideoWidth;
|
||||||
|
src.h = m_VideoHeight;
|
||||||
|
dst.x = dst.y = 0;
|
||||||
|
dst.w = m_DisplayWidth;
|
||||||
|
dst.h = m_DisplayHeight;
|
||||||
|
|
||||||
|
StreamUtils::scaleSourceToDestinationSurface(&src, &dst);
|
||||||
|
|
||||||
|
outputRect.x0 = dst.x;
|
||||||
|
outputRect.x1 = dst.x + dst.w;
|
||||||
|
outputRect.y0 = dst.y;
|
||||||
|
outputRect.y1 = dst.y + dst.h;
|
||||||
|
|
||||||
|
sourceRect.x0 = sourceRect.y0 = 0;
|
||||||
|
sourceRect.x1 = m_VideoWidth;
|
||||||
|
sourceRect.y1 = m_VideoHeight;
|
||||||
|
|
||||||
|
// Render the next frame into the output surface
|
||||||
|
status = m_VdpVideoMixerRender(m_VideoMixer,
|
||||||
|
VDP_INVALID_HANDLE, nullptr,
|
||||||
|
VDP_VIDEO_MIXER_PICTURE_STRUCTURE_FRAME,
|
||||||
|
0, nullptr,
|
||||||
|
videoSurface,
|
||||||
|
0, nullptr,
|
||||||
|
&sourceRect,
|
||||||
|
chosenSurface,
|
||||||
|
&outputRect,
|
||||||
|
nullptr,
|
||||||
|
0,
|
||||||
|
nullptr);
|
||||||
|
|
||||||
|
// The decoder can have this surface back now
|
||||||
|
av_frame_free(&frame);
|
||||||
|
|
||||||
|
if (status != VDP_STATUS_OK) {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"VdpVideoMixerRender() failed: %s",
|
||||||
|
m_VdpGetErrorString(status));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Queue the frame for display immediately
|
||||||
|
status = m_VdpPresentationQueueDisplay(m_PresentationQueue, chosenSurface, 0, 0, 0);
|
||||||
|
if (status != VDP_STATUS_OK) {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"VdpPresentationQueueDisplay() failed: %s",
|
||||||
|
m_VdpGetErrorString(status));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "renderer.h"
|
||||||
|
|
||||||
|
extern "C" {
|
||||||
|
#include <vdpau/vdpau.h>
|
||||||
|
#include <vdpau/vdpau_x11.h>
|
||||||
|
#include <libavutil/hwcontext_vdpau.h>
|
||||||
|
}
|
||||||
|
|
||||||
|
class VDPAURenderer : public IFFmpegRenderer
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
VDPAURenderer();
|
||||||
|
virtual ~VDPAURenderer();
|
||||||
|
virtual bool initialize(SDL_Window* window,
|
||||||
|
int videoFormat,
|
||||||
|
int width,
|
||||||
|
int height);
|
||||||
|
virtual bool prepareDecoderContext(AVCodecContext* context);
|
||||||
|
virtual void renderFrame(AVFrame* frame);
|
||||||
|
|
||||||
|
private:
|
||||||
|
uint32_t m_VideoWidth, m_VideoHeight;
|
||||||
|
uint32_t m_DisplayWidth, m_DisplayHeight;
|
||||||
|
AVBufferRef* m_HwContext;
|
||||||
|
VdpPresentationQueueTarget m_PresentationQueueTarget;
|
||||||
|
VdpPresentationQueue m_PresentationQueue;
|
||||||
|
VdpVideoMixer m_VideoMixer;
|
||||||
|
VdpRGBAFormat m_OutputSurfaceFormat;
|
||||||
|
VdpDevice m_Device;
|
||||||
|
|
||||||
|
#define OUTPUT_SURFACE_COUNT 3
|
||||||
|
VdpOutputSurface m_OutputSurface[OUTPUT_SURFACE_COUNT];
|
||||||
|
int m_NextSurfaceIndex;
|
||||||
|
|
||||||
|
#define OUTPUT_SURFACE_FORMAT_COUNT 2
|
||||||
|
static const VdpRGBAFormat k_OutputFormats[OUTPUT_SURFACE_FORMAT_COUNT];
|
||||||
|
|
||||||
|
VdpGetErrorString* m_VdpGetErrorString;
|
||||||
|
VdpPresentationQueueTargetDestroy* m_VdpPresentationQueueTargetDestroy;
|
||||||
|
VdpVideoMixerCreate* m_VdpVideoMixerCreate;
|
||||||
|
VdpVideoMixerDestroy* m_VdpVideoMixerDestroy;
|
||||||
|
VdpVideoMixerRender* m_VdpVideoMixerRender;
|
||||||
|
VdpPresentationQueueCreate* m_VdpPresentationQueueCreate;
|
||||||
|
VdpPresentationQueueDestroy* m_VdpPresentationQueueDestroy;
|
||||||
|
VdpPresentationQueueDisplay* m_VdpPresentationQueueDisplay;
|
||||||
|
VdpPresentationQueueSetBackgroundColor* m_VdpPresentationQueueSetBackgroundColor;
|
||||||
|
VdpPresentationQueueBlockUntilSurfaceIdle* m_VdpPresentationQueueBlockUntilSurfaceIdle;
|
||||||
|
VdpOutputSurfaceCreate* m_VdpOutputSurfaceCreate;
|
||||||
|
VdpOutputSurfaceDestroy* m_VdpOutputSurfaceDestroy;
|
||||||
|
VdpOutputSurfaceQueryCapabilities* m_VdpOutputSurfaceQueryCapabilities;
|
||||||
|
VdpVideoSurfaceGetParameters* m_VdpVideoSurfaceGetParameters;
|
||||||
|
|
||||||
|
// X11 stuff
|
||||||
|
VdpPresentationQueueTargetCreateX11* m_VdpPresentationQueueTargetCreateX11;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
@@ -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,103 +1,62 @@
|
|||||||
#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 __APPLE__
|
#ifdef Q_OS_DARWIN
|
||||||
#include "ffmpeg-renderers/vt.h"
|
#include "ffmpeg-renderers/vt.h"
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
bool FFmpegVideoDecoder::chooseDecoder(
|
#ifdef HAVE_LIBVA
|
||||||
StreamingPreferences::VideoDecoderSelection vds,
|
#include "ffmpeg-renderers/vaapi.h"
|
||||||
SDL_Window* window,
|
|
||||||
int videoFormat,
|
|
||||||
int width, int height,
|
|
||||||
AVCodec*& chosenDecoder,
|
|
||||||
const AVCodecHWConfig*& chosenHwConfig,
|
|
||||||
IFFmpegRenderer*& newRenderer)
|
|
||||||
{
|
|
||||||
if (videoFormat & VIDEO_FORMAT_MASK_H264) {
|
|
||||||
chosenDecoder = avcodec_find_decoder(AV_CODEC_ID_H264);
|
|
||||||
}
|
|
||||||
else if (videoFormat & VIDEO_FORMAT_MASK_H265) {
|
|
||||||
chosenDecoder = avcodec_find_decoder(AV_CODEC_ID_HEVC);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
Q_ASSERT(false);
|
|
||||||
chosenDecoder = nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!chosenDecoder) {
|
|
||||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
|
||||||
"Unable to find decoder for format: %x",
|
|
||||||
videoFormat);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (int i = 0;; i++) {
|
|
||||||
const AVCodecHWConfig *config = avcodec_get_hw_config(chosenDecoder, i);
|
|
||||||
if (!config || vds == StreamingPreferences::VDS_FORCE_SOFTWARE) {
|
|
||||||
// No matching hardware acceleration support.
|
|
||||||
// This is not an error.
|
|
||||||
chosenHwConfig = nullptr;
|
|
||||||
newRenderer = new SdlRenderer();
|
|
||||||
if (vds != StreamingPreferences::VDS_FORCE_HARDWARE &&
|
|
||||||
newRenderer->initialize(window, videoFormat, width, height)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
delete newRenderer;
|
|
||||||
newRenderer = nullptr;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!(config->methods & AV_CODEC_HW_CONFIG_METHOD_HW_DEVICE_CTX)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Look for acceleration types we support
|
|
||||||
switch (config->device_type) {
|
|
||||||
#ifdef _WIN32
|
|
||||||
case AV_HWDEVICE_TYPE_DXVA2:
|
|
||||||
newRenderer = new DXVA2Renderer();
|
|
||||||
break;
|
|
||||||
#endif
|
#endif
|
||||||
#ifdef __APPLE__
|
|
||||||
case AV_HWDEVICE_TYPE_VIDEOTOOLBOX:
|
|
||||||
newRenderer = VTRendererFactory::createRenderer();
|
|
||||||
break;
|
|
||||||
#endif
|
|
||||||
default:
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (newRenderer->initialize(window, videoFormat, width, height)) {
|
#ifdef HAVE_LIBVDPAU
|
||||||
chosenHwConfig = config;
|
#include "ffmpeg-renderers/vdpau.h"
|
||||||
return true;
|
#endif
|
||||||
}
|
|
||||||
else {
|
// This is gross but it allows us to use sizeof()
|
||||||
// Failed to initialize
|
#include "ffmpeg_videosamples.cpp"
|
||||||
delete newRenderer;
|
|
||||||
newRenderer = nullptr;
|
#define FAILED_DECODES_RESET_THRESHOLD 20
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
bool FFmpegVideoDecoder::isHardwareAccelerated()
|
bool FFmpegVideoDecoder::isHardwareAccelerated()
|
||||||
{
|
{
|
||||||
return m_HwDecodeCfg != nullptr;
|
return m_HwDecodeCfg != nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum AVPixelFormat FFmpegVideoDecoder::ffGetFormat(AVCodecContext* context,
|
||||||
|
const enum AVPixelFormat* pixFmts)
|
||||||
|
{
|
||||||
|
FFmpegVideoDecoder* decoder = (FFmpegVideoDecoder*)context->opaque;
|
||||||
|
const enum AVPixelFormat *p;
|
||||||
|
|
||||||
|
for (p = pixFmts; *p != -1; p++) {
|
||||||
|
// Only match our hardware decoding codec or SW pixel
|
||||||
|
// format (if not using hardware decoding). It's crucial
|
||||||
|
// to override the default get_format() which will try
|
||||||
|
// to gracefully fall back to software decode and break us.
|
||||||
|
if (*p == (decoder->m_HwDecodeCfg ?
|
||||||
|
decoder->m_HwDecodeCfg->pix_fmt :
|
||||||
|
context->pix_fmt)) {
|
||||||
|
return *p;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return AV_PIX_FMT_NONE;
|
||||||
|
}
|
||||||
|
|
||||||
FFmpegVideoDecoder::FFmpegVideoDecoder()
|
FFmpegVideoDecoder::FFmpegVideoDecoder()
|
||||||
: m_VideoDecoderCtx(nullptr),
|
: m_VideoDecoderCtx(nullptr),
|
||||||
m_DecodeBuffer(1024 * 1024, 0),
|
m_DecodeBuffer(1024 * 1024, 0),
|
||||||
m_HwDecodeCfg(nullptr),
|
m_HwDecodeCfg(nullptr),
|
||||||
m_Renderer(nullptr)
|
m_Renderer(nullptr),
|
||||||
|
m_ConsecutiveFailedDecodes(0)
|
||||||
{
|
{
|
||||||
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");
|
||||||
@@ -105,32 +64,46 @@ FFmpegVideoDecoder::FFmpegVideoDecoder()
|
|||||||
|
|
||||||
FFmpegVideoDecoder::~FFmpegVideoDecoder()
|
FFmpegVideoDecoder::~FFmpegVideoDecoder()
|
||||||
{
|
{
|
||||||
avcodec_close(m_VideoDecoderCtx);
|
reset();
|
||||||
av_free(m_VideoDecoderCtx);
|
}
|
||||||
m_VideoDecoderCtx = nullptr;
|
|
||||||
|
|
||||||
m_HwDecodeCfg = nullptr;
|
IFFmpegRenderer* FFmpegVideoDecoder::getRenderer()
|
||||||
|
{
|
||||||
|
return m_Renderer;
|
||||||
|
}
|
||||||
|
|
||||||
|
void FFmpegVideoDecoder::reset()
|
||||||
|
{
|
||||||
|
// Drop any frames still queued to ensure
|
||||||
|
// they are properly freed.
|
||||||
|
SDL_Event event;
|
||||||
|
|
||||||
|
while (SDL_AtomicGet(&m_QueuedFrames) > 0) {
|
||||||
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Waiting for %d frames to return",
|
||||||
|
SDL_AtomicGet(&m_QueuedFrames));
|
||||||
|
|
||||||
|
if (SDL_PeepEvents(&event,
|
||||||
|
1,
|
||||||
|
SDL_GETEVENT,
|
||||||
|
SDL_USEREVENT,
|
||||||
|
SDL_USEREVENT) == 1) {
|
||||||
|
dropFrame(&event.user);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
SDL_Delay(100);
|
||||||
|
SDL_PumpEvents();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
avcodec_free_context(&m_VideoDecoderCtx);
|
||||||
|
|
||||||
delete m_Renderer;
|
delete m_Renderer;
|
||||||
m_Renderer = nullptr;
|
m_Renderer = nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool FFmpegVideoDecoder::initialize(
|
bool FFmpegVideoDecoder::completeInitialization(AVCodec* decoder, int videoFormat, int width, int height, bool testOnly)
|
||||||
StreamingPreferences::VideoDecoderSelection vds,
|
|
||||||
SDL_Window* window,
|
|
||||||
int videoFormat,
|
|
||||||
int width,
|
|
||||||
int height,
|
|
||||||
int)
|
|
||||||
{
|
{
|
||||||
AVCodec* decoder;
|
|
||||||
|
|
||||||
if (!chooseDecoder(vds, window, videoFormat, width, height,
|
|
||||||
decoder, m_HwDecodeCfg, m_Renderer)) {
|
|
||||||
// Error logged in chooseDecoder()
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
m_VideoDecoderCtx = avcodec_alloc_context3(decoder);
|
m_VideoDecoderCtx = avcodec_alloc_context3(decoder);
|
||||||
if (!m_VideoDecoderCtx) {
|
if (!m_VideoDecoderCtx) {
|
||||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
@@ -155,12 +128,20 @@ bool FFmpegVideoDecoder::initialize(
|
|||||||
m_VideoDecoderCtx->width = width;
|
m_VideoDecoderCtx->width = width;
|
||||||
m_VideoDecoderCtx->height = height;
|
m_VideoDecoderCtx->height = height;
|
||||||
m_VideoDecoderCtx->pix_fmt = AV_PIX_FMT_YUV420P; // FIXME: HDR
|
m_VideoDecoderCtx->pix_fmt = AV_PIX_FMT_YUV420P; // FIXME: HDR
|
||||||
|
m_VideoDecoderCtx->get_format = ffGetFormat;
|
||||||
|
|
||||||
// Allow the renderer to attach data to this decoder
|
// Allow the renderer to attach data to this decoder
|
||||||
if (!m_Renderer->prepareDecoderContext(m_VideoDecoderCtx)) {
|
if (!m_Renderer->prepareDecoderContext(m_VideoDecoderCtx)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Nobody must override our ffGetFormat
|
||||||
|
SDL_assert(m_VideoDecoderCtx->get_format == ffGetFormat);
|
||||||
|
|
||||||
|
// Stash a pointer to this object in the context
|
||||||
|
SDL_assert(m_VideoDecoderCtx->opaque == nullptr);
|
||||||
|
m_VideoDecoderCtx->opaque = this;
|
||||||
|
|
||||||
int err = avcodec_open2(m_VideoDecoderCtx, decoder, nullptr);
|
int err = avcodec_open2(m_VideoDecoderCtx, decoder, nullptr);
|
||||||
if (err < 0) {
|
if (err < 0) {
|
||||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
@@ -169,9 +150,148 @@ bool FFmpegVideoDecoder::initialize(
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FFMpeg doesn't completely initialize the codec until the codec
|
||||||
|
// config data comes in. This would be too late for us to change
|
||||||
|
// our minds on the selected video codec, so we'll do a trial run
|
||||||
|
// now to see if things will actually work when the video stream
|
||||||
|
// comes in.
|
||||||
|
if (testOnly) {
|
||||||
|
if (videoFormat & VIDEO_FORMAT_MASK_H264) {
|
||||||
|
m_Pkt.data = (uint8_t*)k_H264TestFrame;
|
||||||
|
m_Pkt.size = sizeof(k_H264TestFrame);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
m_Pkt.data = (uint8_t*)k_HEVCTestFrame;
|
||||||
|
m_Pkt.size = sizeof(k_HEVCTestFrame);
|
||||||
|
}
|
||||||
|
|
||||||
|
err = avcodec_send_packet(m_VideoDecoderCtx, &m_Pkt);
|
||||||
|
if (err < 0) {
|
||||||
|
char errorstring[512];
|
||||||
|
av_strerror(err, errorstring, sizeof(errorstring));
|
||||||
|
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Test decode failed: %s", errorstring);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifdef QT_DEBUG
|
||||||
|
// Restore default log level before streaming
|
||||||
|
av_log_set_level(AV_LOG_INFO);
|
||||||
|
#endif
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
IFFmpegRenderer* FFmpegVideoDecoder::createAcceleratedRenderer(const AVCodecHWConfig* hwDecodeCfg)
|
||||||
|
{
|
||||||
|
if (!(hwDecodeCfg->methods & AV_CODEC_HW_CONFIG_METHOD_HW_DEVICE_CTX)) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Look for acceleration types we support
|
||||||
|
switch (hwDecodeCfg->device_type) {
|
||||||
|
#ifdef Q_OS_WIN32
|
||||||
|
case AV_HWDEVICE_TYPE_DXVA2:
|
||||||
|
return new DXVA2Renderer();
|
||||||
|
#endif
|
||||||
|
#ifdef Q_OS_DARWIN
|
||||||
|
case AV_HWDEVICE_TYPE_VIDEOTOOLBOX:
|
||||||
|
return VTRendererFactory::createRenderer();
|
||||||
|
#endif
|
||||||
|
#ifdef HAVE_LIBVA
|
||||||
|
case AV_HWDEVICE_TYPE_VAAPI:
|
||||||
|
return new VAAPIRenderer();
|
||||||
|
#endif
|
||||||
|
#ifdef HAVE_LIBVDPAU
|
||||||
|
case AV_HWDEVICE_TYPE_VDPAU:
|
||||||
|
return new VDPAURenderer();
|
||||||
|
#endif
|
||||||
|
default:
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool FFmpegVideoDecoder::initialize(
|
||||||
|
StreamingPreferences::VideoDecoderSelection vds,
|
||||||
|
SDL_Window* window,
|
||||||
|
int videoFormat,
|
||||||
|
int width,
|
||||||
|
int height,
|
||||||
|
int)
|
||||||
|
{
|
||||||
|
AVCodec* decoder;
|
||||||
|
|
||||||
|
#ifdef QT_DEBUG
|
||||||
|
// Increase log level during initialization
|
||||||
|
av_log_set_level(AV_LOG_DEBUG);
|
||||||
|
#endif
|
||||||
|
|
||||||
|
if (videoFormat & VIDEO_FORMAT_MASK_H264) {
|
||||||
|
decoder = avcodec_find_decoder(AV_CODEC_ID_H264);
|
||||||
|
}
|
||||||
|
else if (videoFormat & VIDEO_FORMAT_MASK_H265) {
|
||||||
|
decoder = avcodec_find_decoder(AV_CODEC_ID_HEVC);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Q_ASSERT(false);
|
||||||
|
decoder = nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!decoder) {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Unable to find decoder for format: %x",
|
||||||
|
videoFormat);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0;; i++) {
|
||||||
|
const AVCodecHWConfig *config = avcodec_get_hw_config(decoder, i);
|
||||||
|
if (!config || vds == StreamingPreferences::VDS_FORCE_SOFTWARE) {
|
||||||
|
// No matching hardware acceleration support.
|
||||||
|
// This is not an error.
|
||||||
|
m_HwDecodeCfg = nullptr;
|
||||||
|
m_Renderer = new SdlRenderer();
|
||||||
|
if (vds != StreamingPreferences::VDS_FORCE_HARDWARE &&
|
||||||
|
m_Renderer->initialize(window, videoFormat, width, height) &&
|
||||||
|
completeInitialization(decoder, videoFormat, width, height, false)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
reset();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
m_Renderer = createAcceleratedRenderer(config);
|
||||||
|
if (!m_Renderer) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_HwDecodeCfg = config;
|
||||||
|
// Submit test frame to ensure this codec really works
|
||||||
|
if (m_Renderer->initialize(window, videoFormat, width, height) &&
|
||||||
|
completeInitialization(decoder, videoFormat, width, height, true)) {
|
||||||
|
// OK, it worked, so now let's initialize it for real
|
||||||
|
reset();
|
||||||
|
if ((m_Renderer = createAcceleratedRenderer(config)) != nullptr &&
|
||||||
|
m_Renderer->initialize(window, videoFormat, width, height) &&
|
||||||
|
completeInitialization(decoder, videoFormat, width, height, false)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
SDL_LogCritical(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Decoder failed to initialize after successful test");
|
||||||
|
reset();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Failed to initialize or test frame failed, so keep looking
|
||||||
|
reset();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
int FFmpegVideoDecoder::submitDecodeUnit(PDECODE_UNIT du)
|
int FFmpegVideoDecoder::submitDecodeUnit(PDECODE_UNIT du)
|
||||||
{
|
{
|
||||||
PLENTRY entry = du->bufferList;
|
PLENTRY entry = du->bufferList;
|
||||||
@@ -197,12 +317,23 @@ int FFmpegVideoDecoder::submitDecodeUnit(PDECODE_UNIT du)
|
|||||||
|
|
||||||
err = avcodec_send_packet(m_VideoDecoderCtx, &m_Pkt);
|
err = avcodec_send_packet(m_VideoDecoderCtx, &m_Pkt);
|
||||||
if (err < 0) {
|
if (err < 0) {
|
||||||
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
|
||||||
"Decoding failed: %d", err);
|
|
||||||
char errorstring[512];
|
char errorstring[512];
|
||||||
av_strerror(err, errorstring, sizeof(errorstring));
|
av_strerror(err, errorstring, sizeof(errorstring));
|
||||||
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
"Decoding failed: %s", errorstring);
|
"avcodec_send_packet() failed: %s", errorstring);
|
||||||
|
|
||||||
|
// If we've failed a bunch of decodes in a row, the decoder/renderer is
|
||||||
|
// clearly unhealthy, so let's generate a synthetic reset event to trigger
|
||||||
|
// the event loop to destroy and recreate the decoder.
|
||||||
|
if (++m_ConsecutiveFailedDecodes == FAILED_DECODES_RESET_THRESHOLD) {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Resetting decoder due to consistent failure");
|
||||||
|
|
||||||
|
SDL_Event event;
|
||||||
|
event.type = SDL_RENDER_DEVICE_RESET;
|
||||||
|
SDL_PushEvent(&event);
|
||||||
|
}
|
||||||
|
|
||||||
return DR_NEED_IDR;
|
return DR_NEED_IDR;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -217,17 +348,29 @@ 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;
|
// Reset failed decodes count if we reached this far
|
||||||
|
m_ConsecutiveFailedDecodes = 0;
|
||||||
|
|
||||||
event.type = SDL_USEREVENT;
|
// Queue the frame for rendering from the main thread
|
||||||
event.user.code = SDL_CODE_FRAME_READY;
|
SDL_AtomicIncRef(&m_QueuedFrames);
|
||||||
event.user.data1 = frame;
|
queueFrame(frame);
|
||||||
|
|
||||||
// The main thread will handle freeing this
|
|
||||||
SDL_PushEvent(&event);
|
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
av_frame_free(&frame);
|
av_frame_free(&frame);
|
||||||
|
|
||||||
|
char errorstring[512];
|
||||||
|
av_strerror(err, errorstring, sizeof(errorstring));
|
||||||
|
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"avcodec_receive_frame() failed: %s", errorstring);
|
||||||
|
|
||||||
|
if (++m_ConsecutiveFailedDecodes == FAILED_DECODES_RESET_THRESHOLD) {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Resetting decoder due to consistent failure");
|
||||||
|
|
||||||
|
SDL_Event event;
|
||||||
|
event.type = SDL_RENDER_DEVICE_RESET;
|
||||||
|
SDL_PushEvent(&event);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return DR_OK;
|
return DR_OK;
|
||||||
@@ -238,7 +381,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
|
||||||
@@ -246,4 +389,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);
|
||||||
}
|
}
|
||||||
@@ -22,20 +22,28 @@ public:
|
|||||||
virtual void renderFrame(SDL_UserEvent* event) override;
|
virtual void renderFrame(SDL_UserEvent* event) override;
|
||||||
virtual void dropFrame(SDL_UserEvent* event) override;
|
virtual void dropFrame(SDL_UserEvent* event) override;
|
||||||
|
|
||||||
|
virtual IFFmpegRenderer* getRenderer();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
bool
|
bool completeInitialization(AVCodec* decoder, int videoFormat,
|
||||||
chooseDecoder(
|
int width, int height, bool testOnly);
|
||||||
StreamingPreferences::VideoDecoderSelection vds,
|
|
||||||
SDL_Window* window,
|
IFFmpegRenderer* createAcceleratedRenderer(const AVCodecHWConfig* hwDecodeCfg);
|
||||||
int videoFormat,
|
|
||||||
int width, int height,
|
void reset();
|
||||||
AVCodec*& chosenDecoder,
|
|
||||||
const AVCodecHWConfig*& chosenHwConfig,
|
static
|
||||||
IFFmpegRenderer*& newRenderer);
|
enum AVPixelFormat ffGetFormat(AVCodecContext* context,
|
||||||
|
const enum AVPixelFormat* pixFmts);
|
||||||
|
|
||||||
AVPacket m_Pkt;
|
AVPacket m_Pkt;
|
||||||
AVCodecContext* m_VideoDecoderCtx;
|
AVCodecContext* m_VideoDecoderCtx;
|
||||||
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;
|
||||||
|
int m_ConsecutiveFailedDecodes;
|
||||||
|
|
||||||
|
static const uint8_t k_H264TestFrame[];
|
||||||
|
static const uint8_t k_HEVCTestFrame[];
|
||||||
};
|
};
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
#include "ffmpeg.h"
|
||||||
|
|
||||||
|
// 720p 60 FPS H.264 with 1 reference frame
|
||||||
|
const uint8_t FFmpegVideoDecoder::k_H264TestFrame[] = {
|
||||||
|
0x00, 0x00, 0x00, 0x01, 0x67, 0x64, 0x00, 0x20, 0xac, 0x2b, 0x40, 0x28, 0x02, 0xdd, 0x80, 0xb5, 0x06, 0x06, 0x06, 0xa5, 0x00, 0x00, 0x03, 0x03, 0xe8, 0x00, 0x01, 0xd4, 0xc0, 0x8f, 0x4a, 0xa0,
|
||||||
|
0x00, 0x00, 0x00, 0x01, 0x68, 0xee, 0x3c, 0xb0,
|
||||||
|
0x00, 0x00, 0x00, 0x01, 0x65, 0xb8, 0x02, 0x01, 0x67, 0x25, 0x1b, 0xf4, 0xfa, 0x7d, 0x40, 0x1a, 0x78, 0xe5, 0x10, 0x52, 0xc2, 0xee, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0xc6, 0xef, 0xbb, 0x81, 0x85, 0x2d, 0x47, 0xda, 0xca, 0x4c, 0x00, 0x00, 0x03, 0x00, 0x02, 0x7b, 0xcf, 0x80, 0x00, 0x45, 0x40, 0x01, 0x8d, 0xa6, 0x00, 0x01, 0x64, 0x00, 0x0e, 0x03, 0xc8, 0x00, 0x0e, 0x10, 0x00, 0xbd, 0xc5, 0x00, 0x01, 0x11, 0x00, 0x0e, 0xa3, 0x80, 0x00, 0x38, 0xa0,
|
||||||
|
0x00, 0x00, 0x01, 0x65, 0x00, 0x6e, 0x2e, 0x00, 0x83, 0x7f, 0xb4, 0x8e, 0x79, 0xa5, 0xff, 0x84, 0x3f, 0x7f, 0x34, 0x3f, 0x97, 0x1b, 0xaf, 0x31, 0x5f, 0x6e, 0xaa, 0xb6, 0xac, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x78, 0x36, 0x9d, 0xa4, 0x65, 0xf9, 0x1e, 0x5b, 0x3a, 0xb0, 0x40, 0x00, 0x00, 0x03, 0x00, 0x00, 0x41, 0x80, 0x00, 0xc5, 0xc8, 0x00, 0x00, 0xfa, 0x00, 0x03, 0x24, 0x00, 0x0e, 0x20, 0x00, 0x3f, 0x80, 0x01, 0x32, 0x00, 0x08, 0x68, 0x00, 0x3e, 0xc0, 0x03, 0x8a,
|
||||||
|
0x00, 0x00, 0x01, 0x65, 0x00, 0x37, 0x0b, 0x80, 0x21, 0x7f, 0xe3, 0x85, 0x1c, 0xd9, 0xff, 0xe1, 0x1b, 0x01, 0xfa, 0xc0, 0x3e, 0x11, 0x7e, 0xfe, 0x45, 0x5c, 0x43, 0x69, 0x31, 0x4b, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x02, 0x24, 0xae, 0x1a, 0xbb, 0xae, 0x75, 0x9e, 0x35, 0xae, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x03, 0x64, 0x00, 0x09, 0x98, 0x00, 0x1e, 0xc0, 0x00, 0x64, 0x80, 0x01, 0xc4, 0x00, 0x07, 0xf0, 0x00, 0x42, 0xf0, 0x00, 0x00, 0xe1, 0x98, 0x00, 0x05, 0x4b, 0x28, 0x00, 0x06, 0x04,
|
||||||
|
0x00, 0x00, 0x01, 0x65, 0x00, 0x14, 0xa2, 0xe0, 0x08, 0x5f, 0xe3, 0x85, 0x1c, 0xd9, 0xff, 0xe1, 0x1b, 0x01, 0xfa, 0xc0, 0x3e, 0x11, 0x7e, 0xfe, 0x45, 0x5c, 0x43, 0x69, 0x31, 0x4b, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x02, 0x24, 0xb9, 0x7d, 0xb4, 0x70, 0x4d, 0x15, 0xc5, 0x0a, 0x4e, 0x60, 0x00, 0x00, 0x03, 0x00, 0x00, 0x26, 0xa8, 0xb0, 0x00, 0x13, 0xcd, 0xcc, 0x00, 0x07, 0x48, 0x88, 0x00, 0x06, 0x29, 0x69, 0x00, 0x01, 0x16, 0xac, 0x80, 0x00, 0x9e, 0x4e, 0x80, 0x00, 0x3f, 0x84, 0x20, 0x00, 0x6f, 0x41, 0xa0, 0x00, 0x20, 0x00, 0x02, 0x16, 0xb8, 0x00, 0x08, 0x08
|
||||||
|
};
|
||||||
|
|
||||||
|
// 720p 60 FPS HEVC with 1 reference frame
|
||||||
|
const uint8_t FFmpegVideoDecoder::k_HEVCTestFrame[] = {
|
||||||
|
0x00, 0x00, 0x00, 0x01, 0x40, 0x01, 0x0c, 0x01, 0xff, 0xff, 0x01, 0x40, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x78, 0xac, 0x09,
|
||||||
|
0x00, 0x00, 0x00, 0x01, 0x42, 0x01, 0x01, 0x01, 0x40, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x78, 0xa0, 0x02, 0x80, 0x80, 0x2e, 0x1f, 0x13, 0x96, 0xb4, 0xa4, 0x25, 0x92, 0xe3, 0x01, 0x6a, 0x0c, 0x0c, 0x0d, 0x48, 0x20, 0x00, 0x00, 0x03, 0x00, 0x20, 0x00, 0x00, 0x07, 0x85, 0xf1, 0xa2, 0xd0,
|
||||||
|
0x00, 0x00, 0x00, 0x01, 0x44, 0x01, 0xc0, 0xf7, 0xc0, 0xcc, 0x90,
|
||||||
|
0x00, 0x00, 0x00, 0x01, 0x2a, 0x01, 0xac, 0x0c, 0xf0, 0xe6, 0xf6, 0x5e, 0xff, 0xd9, 0x97, 0xeb, 0x44, 0x7d, 0x1f, 0xa4, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x03, 0x52, 0x7c, 0x4e, 0x5b, 0xfe, 0xbe, 0xc2, 0x87, 0x20, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x2b, 0x60,
|
||||||
|
};
|
||||||
@@ -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,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
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
Versions/Current/SDL2
|
||||||