Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6182553d79 | ||
|
|
905a0cdac1 | ||
|
|
f20ddcf26d | ||
|
|
fe26098c2b | ||
|
|
2354a0a640 | ||
|
|
7c3f09c70c | ||
|
|
caf5350fe6 | ||
|
|
84084835ce | ||
|
|
7f38a67ede | ||
|
|
54383b84ec | ||
|
|
948910bfa2 | ||
|
|
ce5dac07d3 | ||
|
|
d4b0057423 | ||
|
|
de8b12f594 | ||
|
|
69e31a0a37 | ||
|
|
c383cd39f4 | ||
|
|
e88d2f0ab2 | ||
|
|
32535578a0 | ||
|
|
2e9af8fef0 | ||
|
|
0aa87aacc6 | ||
|
|
bc38c302ed | ||
|
|
c724902508 | ||
|
|
73604020d7 | ||
|
|
060a00b9c7 | ||
|
|
2cd32bf25a | ||
|
|
81ad1a18fa | ||
|
|
1d94903eef | ||
|
|
2d7dcb495c | ||
|
|
bc580c9aaa | ||
|
|
f8e2740532 | ||
|
|
a079ecd20b | ||
|
|
89c8725766 | ||
|
|
da7d532564 | ||
|
|
4752d4966d | ||
|
|
50f47f1ed5 | ||
|
|
26bdc3e1d5 | ||
|
|
764360c0f3 | ||
|
|
08482577eb | ||
|
|
cabbba2448 | ||
|
|
c48ed7bb0d | ||
|
|
fa4c0e82bd | ||
|
|
c0bf8b9c25 | ||
|
|
d2cff86c97 | ||
|
|
03c0be0720 | ||
|
|
a3361363e4 | ||
|
|
40e142c5c7 | ||
|
|
2703efedef | ||
|
|
c313f1a20b | ||
|
|
b7116657d9 | ||
|
|
2bf3f1b94b | ||
|
|
b78a7971d9 | ||
|
|
5079bc4976 | ||
|
|
555032c15c | ||
|
|
bcff327565 | ||
|
|
e95ff989b1 | ||
|
|
018e8999e2 | ||
|
|
f624a1817e | ||
|
|
202a643e5a | ||
|
|
c75b9c9221 | ||
|
|
9a6f5ba1a8 | ||
|
|
052194714a | ||
|
|
5ccb4a8b7f | ||
|
|
7c3cec0451 | ||
|
|
b79d3b31b3 | ||
|
|
bf5d43d0cf | ||
|
|
f2ac570949 | ||
|
|
37636ef1eb | ||
|
|
c540bec824 | ||
|
|
9288cfa684 | ||
|
|
b1de8b703b | ||
|
|
ccd0777ce9 | ||
|
|
49b67b3e18 | ||
|
|
bc97168dc7 | ||
|
|
0ec04df334 | ||
|
|
26d7946b4a | ||
|
|
292cc579cf | ||
|
|
8492482e55 | ||
|
|
ac8183feee | ||
|
|
8037fb76fc | ||
|
|
754a14941b | ||
|
|
e32bc1a0a3 | ||
|
|
e5a8b49c93 | ||
|
|
b6872456cd | ||
|
|
2a12b018da | ||
|
|
f64486c88f | ||
|
|
80a40ac5f9 | ||
|
|
a1956350b1 | ||
|
|
2c09875556 | ||
|
|
561eed4cbd | ||
|
|
a8f72df7f8 | ||
|
|
ab4127f940 | ||
|
|
f03d12b677 | ||
|
|
81c6202582 | ||
|
|
6cab2bec61 | ||
|
|
94ffda653c | ||
|
|
c2268b45f6 | ||
|
|
38b8a59fe5 | ||
|
|
640e3213ee | ||
|
|
b1799009b3 | ||
|
|
f8e693a060 | ||
|
|
aba806bc32 | ||
|
|
b1fb3f2391 | ||
|
|
041e901c61 | ||
|
|
bb72134992 | ||
|
|
f83853c2fb | ||
|
|
d5f7b20be3 | ||
|
|
b878d64460 | ||
|
|
5397e8ae1a | ||
|
|
46bd8ad83d | ||
|
|
4dac57cf8c | ||
|
|
95cb026446 | ||
|
|
76ea714257 | ||
|
|
5788fa7204 | ||
|
|
d077c6450f | ||
|
|
4499ed3878 | ||
|
|
feb5e3bc78 | ||
|
|
23f1060756 | ||
|
|
6ed512e762 | ||
|
|
947970e07b | ||
|
|
df567b405a | ||
|
|
7b0596d9df | ||
|
|
4b7200ae69 | ||
|
|
710648f3f4 | ||
|
|
d2002b6a62 | ||
|
|
8de42b3199 | ||
|
|
48e78d2cd3 | ||
|
|
1fd099db70 | ||
|
|
2986a40c5a | ||
|
|
8bb6d4d0d9 | ||
|
|
00f08578ee | ||
|
|
6a514c7f51 | ||
|
|
21aad643f3 | ||
|
|
8687448966 | ||
|
|
6fcc8b721b | ||
|
|
b4bbd58c77 | ||
|
|
1a45b8f07a | ||
|
|
4471a858db | ||
|
|
22e712af5a | ||
|
|
9799b82d53 | ||
|
|
78915e0b99 | ||
|
|
c9cc65aae7 | ||
|
|
a4edd9d202 | ||
|
|
7747624c80 | ||
|
|
aab7191a44 | ||
|
|
1f972b1828 | ||
|
|
58b77cf589 | ||
|
|
c064f87889 | ||
|
|
d6b4c8e7da | ||
|
|
49f0b45fa8 | ||
|
|
b19c4bfc5d | ||
|
|
e944c819d9 | ||
|
|
75f631599c | ||
|
|
e0bebeae25 | ||
|
|
32e7ddf272 | ||
|
|
7091e0324e | ||
|
|
8f8532921c | ||
|
|
e9936992ec | ||
|
|
6375055b3e | ||
|
|
88b1ea296c | ||
|
|
2a13a869f3 | ||
|
|
89c342bb6f | ||
|
|
067f5e33aa | ||
|
|
33695f1c6b | ||
|
|
52ef84873e | ||
|
|
c21ba5c808 | ||
|
|
6cf408f12b | ||
|
|
8bdc2d285d | ||
|
|
a66fbcd6be | ||
|
|
93402a62fc | ||
|
|
2a56fb6c70 | ||
|
|
8276c031e9 | ||
|
|
102e5f6de9 | ||
|
|
1f7bdbfbcb | ||
|
|
f2fdd048c7 | ||
|
|
5a2cfc15a6 | ||
|
|
11c7131875 | ||
|
|
34aacc90d7 | ||
|
|
e75dce2543 | ||
|
|
e2774309fb | ||
|
|
dfefdc93c5 | ||
|
|
d46ea28bba | ||
|
|
d35c5bcaba | ||
|
|
2b14856d7f | ||
|
|
3316fdb80e |
No files matched your search
@@ -22,18 +22,19 @@ List the games you've tried that exhibit the issue. To see if the issue is game-
|
|||||||
- Does the problem still occur after reverting settings back to default?
|
- Does the problem still occur after reverting settings back to default?
|
||||||
|
|
||||||
**Client PC details (please complete the following information)**
|
**Client PC details (please complete the following information)**
|
||||||
- OS: [e.g. Windows 10 1803]
|
- OS: [e.g. Windows 10 1809]
|
||||||
- Moonlight Version: [e.g. v0.2.0]
|
- Moonlight Version: [e.g. v0.9.0]
|
||||||
|
|
||||||
**Server PC details (please complete the following information)**
|
**Server PC details (please complete the following information)**
|
||||||
- OS: [e.g. Windows 10 1803]
|
- OS: [e.g. Windows 10 1809]
|
||||||
- GeForce Experience version: [e.g. 3.16.0.140]
|
- GeForce Experience version: [e.g. 3.16.0.140]
|
||||||
- Nvidia GPU driver: [e.g. 417.35]
|
- Nvidia GPU driver: [e.g. 417.35]
|
||||||
|
|
||||||
**Moonlight Logs (please attach)**
|
**Moonlight Logs (please attach)**
|
||||||
- On Windows, `Moonlight-###.log` files can be found in `%TEMP%`. Simply type that into the File Explorer path field to navigate there.
|
- On Windows, `Moonlight-###.log` files can be found in `%TEMP%`. Simply type that into the File Explorer path field to navigate there.
|
||||||
- On macOS, `Moonlight-###.log` files can be found in `/tmp`. In Finder, press Cmd+Shift+G, then type `/tmp` to navigate there.
|
- On macOS, `Moonlight-###.log` files can be found in `/tmp`. In Finder, press Cmd+Shift+G, then type `/tmp` to navigate there.
|
||||||
- On Linux, logs print to the terminal when running via `flatpak run com.moonlight_stream.Moonlight`
|
- On Linux with the Flatpak, logs print to the terminal when running with the command: `flatpak run com.moonlight_stream.Moonlight`
|
||||||
|
- On Linux with the Snap, logs print to the terminal when running with the command: `moonlight`
|
||||||
|
|
||||||
**Additional context**
|
**Additional context**
|
||||||
Anything else you think may be relevant to the issue
|
Anything else you think may be relevant to the issue
|
||||||
@@ -2,4 +2,6 @@
|
|||||||
|
|
||||||
|
|
||||||
**/.vs/
|
**/.vs/
|
||||||
build/
|
build/
|
||||||
|
config.tests/*/.qmake.stash
|
||||||
|
config.tests/*/Makefile
|
||||||
@@ -3,6 +3,9 @@ language: cpp
|
|||||||
git:
|
git:
|
||||||
depth: 1
|
depth: 1
|
||||||
|
|
||||||
|
env:
|
||||||
|
- NJOBS=2
|
||||||
|
|
||||||
matrix:
|
matrix:
|
||||||
include:
|
include:
|
||||||
- os: osx
|
- os: osx
|
||||||
@@ -14,13 +17,12 @@ matrix:
|
|||||||
addons:
|
addons:
|
||||||
apt:
|
apt:
|
||||||
sources:
|
sources:
|
||||||
- sourceline: 'ppa:beineri/opt-qt596-xenial'
|
- sourceline: 'ppa:beineri/opt-qt597-xenial'
|
||||||
- sourceline: 'ppa:jonathonf/ffmpeg-4'
|
- sourceline: 'ppa:jonathonf/ffmpeg-4'
|
||||||
packages:
|
packages:
|
||||||
- qt59base
|
- qt59base
|
||||||
- qt59quickcontrols2
|
- qt59quickcontrols2
|
||||||
- qt59svg
|
- qt59svg
|
||||||
- qt59multimedia
|
|
||||||
- libesd0-dev
|
- libesd0-dev
|
||||||
- libgl-dev
|
- libgl-dev
|
||||||
- libegl1-mesa-dev
|
- libegl1-mesa-dev
|
||||||
@@ -35,7 +37,8 @@ matrix:
|
|||||||
install:
|
install:
|
||||||
- '[ "$TRAVIS_OS_NAME" != osx ] || brew install qt5'
|
- '[ "$TRAVIS_OS_NAME" != osx ] || brew install qt5'
|
||||||
- '[ "$TRAVIS_OS_NAME" != osx ] || brew link --force 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
|
- if [[ "$TRAVIS_OS_NAME" = "linux" ]]; then export SDL2_VER=2.0.9 && wget https://www.libsdl.org/release/SDL2-$SDL2_VER.tar.gz && tar -xvf SDL2-$SDL2_VER.tar.gz && cd SDL2-$SDL2_VER && ./configure && make -j$NJOBS && sudo make install && cd ..; fi
|
||||||
|
- if [[ "$TRAVIS_OS_NAME" = "linux" ]]; then export SDL2_TTF_VER=2.0.15 && wget https://www.libsdl.org/projects/SDL_ttf/release/SDL2_ttf-$SDL2_TTF_VER.tar.gz && tar -xvf SDL2_ttf-$SDL2_TTF_VER.tar.gz && cd SDL2_ttf-$SDL2_TTF_VER && ./configure && make -j$NJOBS && sudo make install && cd ..; fi
|
||||||
|
|
||||||
before_script:
|
before_script:
|
||||||
- '[ "$TRAVIS_OS_NAME" != "linux" ] || source /opt/qt59/bin/qt59-env.sh'
|
- '[ "$TRAVIS_OS_NAME" != "linux" ] || source /opt/qt59/bin/qt59-env.sh'
|
||||||
@@ -46,5 +49,5 @@ before_script:
|
|||||||
|
|
||||||
script:
|
script:
|
||||||
- qmake moonlight-qt.pro -spec $QMAKESPEC
|
- qmake moonlight-qt.pro -spec $QMAKESPEC
|
||||||
- make debug
|
- make -j$NJOBS debug
|
||||||
- make release
|
- make -j$NJOBS release
|
||||||
@@ -134,7 +134,11 @@ private:
|
|||||||
// This DLL shipped with ASUS Sonic Radar 3 improperly handles
|
// This DLL shipped with ASUS Sonic Radar 3 improperly handles
|
||||||
// D3D9 exclusive fullscreen in a way that causes CreateDeviceEx()
|
// D3D9 exclusive fullscreen in a way that causes CreateDeviceEx()
|
||||||
// to deadlock. https://github.com/moonlight-stream/moonlight-qt/issues/102
|
// to deadlock. https://github.com/moonlight-stream/moonlight-qt/issues/102
|
||||||
L"NahimicOSD.dll"
|
L"NahimicOSD.dll",
|
||||||
|
// This DLL has been seen in several crash reports. Some Googling
|
||||||
|
// suggests it's highly unstable and causes issues in many games.
|
||||||
|
L"EZFRD32.dll",
|
||||||
|
L"EZFRD64.dll",
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -16,41 +16,51 @@ You can follow development on our [Discord server](https://discord.gg/6ERtzFY).
|
|||||||
- 5.1 surround sound audio
|
- 5.1 surround sound audio
|
||||||
- HEVC support for better image quality at reduced bandwidth
|
- HEVC support for better image quality at reduced bandwidth
|
||||||
- Keyboard and mouse support
|
- Keyboard and mouse support
|
||||||
- Gamepad support with SDL gamepad mappings
|
- Gamepad support with force feedback for up to 4 players
|
||||||
|
|
||||||
## Downloads
|
## Downloads
|
||||||
- [Windows and macOS](https://github.com/moonlight-stream/moonlight-qt/releases)
|
- [Windows, macOS, and Steam Link](https://github.com/moonlight-stream/moonlight-qt/releases)
|
||||||
- [Linux](https://flathub.org/apps/details/com.moonlight_stream.Moonlight)
|
- [Snap (for Ubuntu-based Linux distros)](https://snapcraft.io/moonlight)
|
||||||
|
- [Flatpak (for other Linux distros)](https://flathub.org/apps/details/com.moonlight_stream.Moonlight)
|
||||||
|
|
||||||
## Building
|
## Building
|
||||||
### General Build Requirements
|
### General Build Requirements
|
||||||
* Qt 5.9 SDK or later
|
* Qt 5.9 SDK or later
|
||||||
|
|
||||||
### Windows-specific Requirements
|
### Windows-specific Build Requirements
|
||||||
* Windows 7 or later
|
* Windows 7 or later
|
||||||
* Visual Studio 2015 or later
|
* [Visual Studio](https://visualstudio.microsoft.com/downloads/) 2015 or later (Community edition is fine)
|
||||||
* Select MSVC Desktop toolchain during Qt installation
|
* Select MSVC Desktop toolchain during Qt installation
|
||||||
* 7-Zip (only if building installers for non-development PCs)
|
* [DirectX SDK](https://www.microsoft.com/en-us/download/details.aspx?id=6812)
|
||||||
* WiX Toolset v3.11 or later (only if building installers for non-development PCs)
|
* [7-Zip](https://www.7-zip.org/) (only if building installers for non-development PCs)
|
||||||
|
* [WiX Toolset](http://wixtoolset.org/releases/) v3.11 or later (only if building installers for non-development PCs)
|
||||||
|
|
||||||
### Mac-specific Requirements
|
### Mac-specific Build Requirements
|
||||||
* macOS El Capitan (10.11) or later
|
* macOS Sierra (10.12) or later
|
||||||
* Xcode with High Sierra (10.13) SDK (Mojave SDK not yet supported until Qt 5.12 later this year)
|
* Xcode 10
|
||||||
* [create-dmg](https://github.com/sindresorhus/create-dmg) (only if building DMGs for use on non-development Macs)
|
* [create-dmg](https://github.com/sindresorhus/create-dmg) (only if building DMGs for use on non-development Macs)
|
||||||
|
|
||||||
### Linux-specific Requirements
|
### Linux-specific Build Requirements
|
||||||
* GCC or Clang
|
* GCC or Clang
|
||||||
* Install your distro equivalents of: `openssl-devel qt5-devel SDL2-devel ffmpeg-devel qt5-qtquickcontrols2-devel libva-devel libvdpau-devel opus-devel pulseaudio-libs-devel alsa-lib-devel`
|
* Install your distro equivalents of: `openssl-devel qt5-devel SDL2-devel ffmpeg-devel qt5-qtquickcontrols2-devel libva-devel libvdpau-devel opus-devel pulseaudio-libs-devel alsa-lib-devel SDL2_ttf-devel`
|
||||||
* FFmpeg 4.0 is required to build. If your distro doesn't package FFmpeg 4.0, you can build and install it from source on http://ffmpeg.org/
|
* FFmpeg 4.0 is required to build. If your distro doesn't package FFmpeg 4.0, you can build and install it from source on http://ffmpeg.org/
|
||||||
|
|
||||||
|
### Steam Link-specific Build Requirements
|
||||||
|
* [Steam Link SDK](https://github.com/ValveSoftware/steamlink-sdk) cloned on your build system
|
||||||
|
* STEAMLINK_SDK_PATH environment variable set to the Steam Link SDK path
|
||||||
|
|
||||||
### Build Setup Steps
|
### Build Setup Steps
|
||||||
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
|
||||||
* You may also use Homebrew on macOS or your Linux distro's package manager for the Qt SDK as long as the packages are Qt 5.9 or later.
|
* You can install Qt via Homebrew on macOS, but you will need to use `brew install qt --with-debug` to be able to create debug builds of Moonlight.
|
||||||
|
* You may also use your Linux distro's package manager for the Qt SDK as long as the packages are Qt 5.9 or later.
|
||||||
|
* This step is not required for building on Steam Link, because the Steam Link SDK includes Qt 5.9.
|
||||||
2. Run `git submodule update --init --recursive` from within `moonlight-qt/`
|
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.
|
3. Open the project in Qt Creator or build from qmake on the command line.
|
||||||
* To build a binary for use on non-development machines, use the scripts in the `scripts` folder.
|
* To build a binary for use on non-development machines, use the scripts in the `scripts` folder.
|
||||||
* For Windows builds, use `scripts\generate-installers.bat`. Execute this script from the root of the repository within a Qt command prompt. Ensure WiX and 7-Zip binary directories are in your `%PATH%`.
|
* For Windows builds, use `scripts\generate-installers.bat`. Execute this script from the root of the repository within a Qt command prompt. Ensure WiX and 7-Zip binary directories are in your `%PATH%`.
|
||||||
* For macOS builds, use `scripts/generate-dmg.sh`. Execute this script from the root of the repository and ensure Qt's `bin` folder is in your `$PATH`.
|
* For macOS builds, use `scripts/generate-dmg.sh`. Execute this script from the root of the repository and ensure Qt's `bin` folder is in your `$PATH`.
|
||||||
|
* For Steam Link builds, run `scripts/build-steamlink-app.sh` from the root of the repository.
|
||||||
|
* To build from the command line for development use, run `qmake moonlight-qt.pro` then `make debug` or `make release`
|
||||||
|
|
||||||
## Contribute
|
## Contribute
|
||||||
1. Fork us
|
1. Fork us
|
||||||
|
|||||||
@@ -23,9 +23,9 @@
|
|||||||
<key>NSSupportsAutomaticGraphicsSwitching</key>
|
<key>NSSupportsAutomaticGraphicsSwitching</key>
|
||||||
<true/>
|
<true/>
|
||||||
<key>CFBundleVersion</key>
|
<key>CFBundleVersion</key>
|
||||||
<string>0.8.0</string>
|
<string>VERSION</string>
|
||||||
<key>CFBundleShortVersionString</key>
|
<key>CFBundleShortVersionString</key>
|
||||||
<string>0.8.0</string>
|
<string>VERSION</string>
|
||||||
<key>CFBundleDisplayName</key>
|
<key>CFBundleDisplayName</key>
|
||||||
<string>Moonlight</string>
|
<string>Moonlight</string>
|
||||||
</dict>
|
</dict>
|
||||||
|
|||||||
@@ -37,32 +37,44 @@ DEFINES += QT_DEPRECATED_WARNINGS
|
|||||||
DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000 # disables all the APIs deprecated before Qt 6.0.0
|
DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000 # disables all the APIs deprecated before Qt 6.0.0
|
||||||
|
|
||||||
win32 {
|
win32 {
|
||||||
INCLUDEPATH += $$PWD/../libs/windows/include
|
INCLUDEPATH += \
|
||||||
|
$$PWD/../libs/windows/include \
|
||||||
|
$$(DXSDK_DIR)/Include
|
||||||
|
|
||||||
contains(QT_ARCH, i386) {
|
contains(QT_ARCH, i386) {
|
||||||
LIBS += -L$$PWD/../libs/windows/lib/x86
|
LIBS += -L$$PWD/../libs/windows/lib/x86
|
||||||
|
LIBS += -L$$(DXSDK_DIR)/Lib/x86
|
||||||
}
|
}
|
||||||
contains(QT_ARCH, x86_64) {
|
contains(QT_ARCH, x86_64) {
|
||||||
LIBS += -L$$PWD/../libs/windows/lib/x64
|
LIBS += -L$$PWD/../libs/windows/lib/x64
|
||||||
|
LIBS += -L$$(DXSDK_DIR)/Lib/x64
|
||||||
}
|
}
|
||||||
|
|
||||||
LIBS += ws2_32.lib winmm.lib dxva2.lib ole32.lib gdi32.lib user32.lib d3d9.lib dwmapi.lib dbghelp.lib
|
LIBS += ws2_32.lib winmm.lib dxva2.lib ole32.lib gdi32.lib user32.lib d3d9.lib dwmapi.lib dbghelp.lib
|
||||||
}
|
}
|
||||||
macx {
|
macx {
|
||||||
INCLUDEPATH += $$PWD/../libs/mac/include $$PWD/../libs/mac/Frameworks/SDL2.framework/Versions/A/Headers
|
INCLUDEPATH += $$PWD/../libs/mac/include
|
||||||
|
INCLUDEPATH += $$PWD/../libs/mac/Frameworks/SDL2.framework/Versions/A/Headers
|
||||||
|
INCLUDEPATH += $$PWD/../libs/mac/Frameworks/SDL2_ttf.framework/Versions/A/Headers
|
||||||
LIBS += -L$$PWD/../libs/mac/lib -F$$PWD/../libs/mac/Frameworks
|
LIBS += -L$$PWD/../libs/mac/lib -F$$PWD/../libs/mac/Frameworks
|
||||||
}
|
}
|
||||||
|
|
||||||
unix:!macx {
|
unix:!macx {
|
||||||
CONFIG += link_pkgconfig soundio
|
CONFIG += link_pkgconfig
|
||||||
PKGCONFIG += openssl sdl2 opus
|
PKGCONFIG += openssl sdl2 SDL2_ttf opus
|
||||||
|
|
||||||
# For libsoundio
|
# SLAudio is used on Steam Link
|
||||||
packagesExist(libpulse) {
|
!config_SL {
|
||||||
PKGCONFIG += libpulse
|
CONFIG += soundio
|
||||||
}
|
}
|
||||||
packagesExist(alsa) {
|
|
||||||
PKGCONFIG += alsa
|
soundio {
|
||||||
|
packagesExist(libpulse) {
|
||||||
|
PKGCONFIG += libpulse
|
||||||
|
}
|
||||||
|
packagesExist(alsa) {
|
||||||
|
PKGCONFIG += alsa
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
packagesExist(libavcodec) {
|
packagesExist(libavcodec) {
|
||||||
@@ -85,11 +97,14 @@ unix:!macx {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
win32 {
|
win32 {
|
||||||
LIBS += -llibssl -llibcrypto -lSDL2 -lavcodec -lavutil -lopus
|
LIBS += -llibssl -llibcrypto -lSDL2 -lSDL2_ttf -lavcodec -lavutil -lopus -ld3dx9
|
||||||
CONFIG += ffmpeg soundio
|
CONFIG += ffmpeg
|
||||||
|
}
|
||||||
|
win32:!winrt {
|
||||||
|
CONFIG += soundio
|
||||||
}
|
}
|
||||||
macx {
|
macx {
|
||||||
LIBS += -lssl -lcrypto -lavcodec.58 -lavutil.56 -lopus -framework SDL2
|
LIBS += -lssl -lcrypto -lavcodec.58 -lavutil.56 -lopus -framework SDL2 -framework SDL2_ttf
|
||||||
LIBS += -lobjc -framework VideoToolbox -framework AVFoundation -framework CoreVideo -framework CoreGraphics -framework CoreMedia -framework AppKit
|
LIBS += -lobjc -framework VideoToolbox -framework AVFoundation -framework CoreVideo -framework CoreGraphics -framework CoreMedia -framework AppKit
|
||||||
|
|
||||||
# For libsoundio
|
# For libsoundio
|
||||||
@@ -121,7 +136,9 @@ SOURCES += \
|
|||||||
backend/autoupdatechecker.cpp \
|
backend/autoupdatechecker.cpp \
|
||||||
path.cpp \
|
path.cpp \
|
||||||
settings/mappingmanager.cpp \
|
settings/mappingmanager.cpp \
|
||||||
gui/sdlgamepadkeynavigation.cpp
|
gui/sdlgamepadkeynavigation.cpp \
|
||||||
|
streaming/video/overlaymanager.cpp \
|
||||||
|
backend/systemproperties.cpp
|
||||||
|
|
||||||
HEADERS += \
|
HEADERS += \
|
||||||
utils.h \
|
utils.h \
|
||||||
@@ -147,7 +164,9 @@ HEADERS += \
|
|||||||
backend/autoupdatechecker.h \
|
backend/autoupdatechecker.h \
|
||||||
path.h \
|
path.h \
|
||||||
settings/mappingmanager.h \
|
settings/mappingmanager.h \
|
||||||
gui/sdlgamepadkeynavigation.h
|
gui/sdlgamepadkeynavigation.h \
|
||||||
|
streaming/video/overlaymanager.h \
|
||||||
|
backend/systemproperties.h
|
||||||
|
|
||||||
# Platform-specific renderers and decoders
|
# Platform-specific renderers and decoders
|
||||||
ffmpeg {
|
ffmpeg {
|
||||||
@@ -160,10 +179,10 @@ ffmpeg {
|
|||||||
streaming/video/ffmpeg-renderers/pacer/pacer.cpp \
|
streaming/video/ffmpeg-renderers/pacer/pacer.cpp \
|
||||||
streaming/video/ffmpeg-renderers/pacer/nullthreadedvsyncsource.cpp
|
streaming/video/ffmpeg-renderers/pacer/nullthreadedvsyncsource.cpp
|
||||||
|
|
||||||
|
|
||||||
HEADERS += \
|
HEADERS += \
|
||||||
streaming/video/ffmpeg.h \
|
streaming/video/ffmpeg.h \
|
||||||
streaming/video/ffmpeg-renderers/renderer.h \
|
streaming/video/ffmpeg-renderers/renderer.h \
|
||||||
|
streaming/video/ffmpeg-renderers/sdlvid.h \
|
||||||
streaming/video/ffmpeg-renderers/pacer/pacer.h \
|
streaming/video/ffmpeg-renderers/pacer/pacer.h \
|
||||||
streaming/video/ffmpeg-renderers/pacer/nullthreadedvsyncsource.h
|
streaming/video/ffmpeg-renderers/pacer/nullthreadedvsyncsource.h
|
||||||
}
|
}
|
||||||
@@ -194,16 +213,20 @@ libvdpau {
|
|||||||
SOURCES += streaming/video/ffmpeg-renderers/vdpau.cpp
|
SOURCES += streaming/video/ffmpeg-renderers/vdpau.cpp
|
||||||
HEADERS += streaming/video/ffmpeg-renderers/vdpau.h
|
HEADERS += streaming/video/ffmpeg-renderers/vdpau.h
|
||||||
}
|
}
|
||||||
config_SLVideo {
|
config_SL {
|
||||||
message(SLVideo decoder/renderer selected)
|
message(Steam Link build configuration selected)
|
||||||
|
|
||||||
DEFINES += HAVE_SLVIDEO
|
DEFINES += STEAM_LINK HAVE_SLVIDEO HAVE_SLAUDIO
|
||||||
LIBS += -lSLVideo
|
LIBS += -lSLVideo -lSLAudio
|
||||||
|
|
||||||
SOURCES += streaming/video/sl.cpp
|
SOURCES += \
|
||||||
HEADERS += streaming/video/sl.h
|
streaming/video/slvid.cpp \
|
||||||
|
streaming/audio/renderers/slaud.cpp
|
||||||
|
HEADERS += \
|
||||||
|
streaming/video/slvid.h \
|
||||||
|
streaming/audio/renderers/slaud.h
|
||||||
}
|
}
|
||||||
win32 {
|
win32:!winrt {
|
||||||
message(DXVA2 renderer selected)
|
message(DXVA2 renderer selected)
|
||||||
|
|
||||||
SOURCES += \
|
SOURCES += \
|
||||||
@@ -257,12 +280,14 @@ 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
|
||||||
|
|
||||||
win32:CONFIG(release, debug|release): LIBS += -L$$OUT_PWD/../soundio/release/ -lsoundio
|
soundio {
|
||||||
else:win32:CONFIG(debug, debug|release): LIBS += -L$$OUT_PWD/../soundio/debug/ -lsoundio
|
win32:CONFIG(release, debug|release): LIBS += -L$$OUT_PWD/../soundio/release/ -lsoundio
|
||||||
else:unix: LIBS += -L$$OUT_PWD/../soundio/ -lsoundio
|
else:win32:CONFIG(debug, debug|release): LIBS += -L$$OUT_PWD/../soundio/debug/ -lsoundio
|
||||||
|
else:unix: LIBS += -L$$OUT_PWD/../soundio/ -lsoundio
|
||||||
|
|
||||||
INCLUDEPATH += $$PWD/../soundio/libsoundio
|
INCLUDEPATH += $$PWD/../soundio/libsoundio
|
||||||
DEPENDPATH += $$PWD/../soundio/libsoundio
|
DEPENDPATH += $$PWD/../soundio/libsoundio
|
||||||
|
}
|
||||||
|
|
||||||
win32:CONFIG(release, debug|release): LIBS += -L$$OUT_PWD/../h264bitstream/release/ -lh264bitstream
|
win32:CONFIG(release, debug|release): LIBS += -L$$OUT_PWD/../h264bitstream/release/ -lh264bitstream
|
||||||
else:win32:CONFIG(debug, debug|release): LIBS += -L$$OUT_PWD/../h264bitstream/debug/ -lh264bitstream
|
else:win32:CONFIG(debug, debug|release): LIBS += -L$$OUT_PWD/../h264bitstream/debug/ -lh264bitstream
|
||||||
@@ -271,11 +296,13 @@ else:unix: LIBS += -L$$OUT_PWD/../h264bitstream/ -lh264bitstream
|
|||||||
INCLUDEPATH += $$PWD/../h264bitstream/h264bitstream
|
INCLUDEPATH += $$PWD/../h264bitstream/h264bitstream
|
||||||
DEPENDPATH += $$PWD/../h264bitstream/h264bitstream
|
DEPENDPATH += $$PWD/../h264bitstream/h264bitstream
|
||||||
|
|
||||||
win32:CONFIG(release, debug|release): LIBS += -L$$OUT_PWD/../AntiHooking/release/ -lAntiHooking
|
!winrt {
|
||||||
else:win32:CONFIG(debug, debug|release): LIBS += -L$$OUT_PWD/../AntiHooking/debug/ -lAntiHooking
|
win32:CONFIG(release, debug|release): LIBS += -L$$OUT_PWD/../AntiHooking/release/ -lAntiHooking
|
||||||
|
else:win32:CONFIG(debug, debug|release): LIBS += -L$$OUT_PWD/../AntiHooking/debug/ -lAntiHooking
|
||||||
|
|
||||||
INCLUDEPATH += $$PWD/../AntiHooking
|
INCLUDEPATH += $$PWD/../AntiHooking
|
||||||
DEPENDPATH += $$PWD/../AntiHooking
|
DEPENDPATH += $$PWD/../AntiHooking
|
||||||
|
}
|
||||||
|
|
||||||
unix:!macx: {
|
unix:!macx: {
|
||||||
isEmpty(PREFIX) {
|
isEmpty(PREFIX) {
|
||||||
@@ -299,10 +326,7 @@ unix:!macx: {
|
|||||||
appstream.files = deploy/linux/com.moonlight_stream.Moonlight.appdata.xml
|
appstream.files = deploy/linux/com.moonlight_stream.Moonlight.appdata.xml
|
||||||
appstream.path = $$PREFIX/$$DATADIR/metainfo/
|
appstream.path = $$PREFIX/$$DATADIR/metainfo/
|
||||||
|
|
||||||
appdata.files = SDL_GameControllerDB/gamecontrollerdb.txt
|
INSTALLS += target desktop icons appstream
|
||||||
appdata.path = "$$PREFIX/$$DATADIR/Moonlight Game Streaming Project/Moonlight/"
|
|
||||||
|
|
||||||
INSTALLS += target appdata desktop icons appstream
|
|
||||||
}
|
}
|
||||||
win32 {
|
win32 {
|
||||||
RC_ICONS = moonlight.ico
|
RC_ICONS = moonlight.ico
|
||||||
@@ -314,9 +338,13 @@ win32 {
|
|||||||
QMAKE_LFLAGS += /MANIFEST:embed /MANIFESTINPUT:$${PWD}/Moonlight.exe.manifest
|
QMAKE_LFLAGS += /MANIFEST:embed /MANIFESTINPUT:$${PWD}/Moonlight.exe.manifest
|
||||||
}
|
}
|
||||||
macx {
|
macx {
|
||||||
QMAKE_INFO_PLIST = $$PWD/Info.plist
|
# Create Info.plist in object dir with the correct version string
|
||||||
|
system(cp $$PWD/Info.plist $$OUT_PWD/Info.plist)
|
||||||
|
system(sed -i -e 's/VERSION/$$cat(version.txt)/g' $$OUT_PWD/Info.plist)
|
||||||
|
|
||||||
APP_BUNDLE_RESOURCES.files = moonlight.icns SDL_GameControllerDB/gamecontrollerdb.txt
|
QMAKE_INFO_PLIST = $$OUT_PWD/Info.plist
|
||||||
|
|
||||||
|
APP_BUNDLE_RESOURCES.files = moonlight.icns
|
||||||
APP_BUNDLE_RESOURCES.path = Contents/Resources
|
APP_BUNDLE_RESOURCES.path = Contents/Resources
|
||||||
|
|
||||||
APP_BUNDLE_FRAMEWORKS.files = $$files(../libs/mac/Frameworks/*.framework, true)
|
APP_BUNDLE_FRAMEWORKS.files = $$files(../libs/mac/Frameworks/*.framework, true)
|
||||||
@@ -327,5 +355,5 @@ macx {
|
|||||||
QMAKE_RPATHDIR += @executable_path/../Frameworks
|
QMAKE_RPATHDIR += @executable_path/../Frameworks
|
||||||
}
|
}
|
||||||
|
|
||||||
VERSION = 0.8.0
|
VERSION = "$$cat(version.txt)"
|
||||||
DEFINES += VERSION_STR=\\\"0.8.0\\\"
|
DEFINES += VERSION_STR=\\\"$$cat(version.txt)\\\"
|
||||||
@@ -116,7 +116,8 @@ void AutoUpdateChecker::handleUpdateCheckRequestFinished(QNetworkReply* reply)
|
|||||||
if (currentVer < latestVer) {
|
if (currentVer < latestVer) {
|
||||||
qDebug() << "Update available";
|
qDebug() << "Update available";
|
||||||
|
|
||||||
emit onUpdateAvailable(updateObj["browser_url"].toString());
|
emit onUpdateAvailable(updateObj["version"].toString(),
|
||||||
|
updateObj["browser_url"].toString());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
else if (currentVer > latestVer) {
|
else if (currentVer > latestVer) {
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ public:
|
|||||||
Q_INVOKABLE void start();
|
Q_INVOKABLE void start();
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
void onUpdateAvailable(QString url);
|
void onUpdateAvailable(QString newVersion, QString url);
|
||||||
|
|
||||||
private slots:
|
private slots:
|
||||||
void handleUpdateCheckRequestFinished(QNetworkReply* reply);
|
void handleUpdateCheckRequestFinished(QNetworkReply* reply);
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
|
|
||||||
#include <QThread>
|
#include <QThread>
|
||||||
#include <QThreadPool>
|
#include <QThreadPool>
|
||||||
|
#include <QCoreApplication>
|
||||||
|
|
||||||
#define SER_HOSTS "hosts"
|
#define SER_HOSTS "hosts"
|
||||||
|
|
||||||
@@ -31,7 +32,7 @@ private:
|
|||||||
|
|
||||||
QString serverInfo;
|
QString serverInfo;
|
||||||
try {
|
try {
|
||||||
serverInfo = http.getServerInfo(NvHTTP::NvLogLevel::NONE);
|
serverInfo = http.getServerInfo(NvHTTP::NvLogLevel::NVLL_NONE);
|
||||||
} catch (...) {
|
} catch (...) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -130,8 +131,12 @@ private:
|
|||||||
emit computerStateChanged(m_Computer);
|
emit computerStateChanged(m_Computer);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wait a bit to poll again
|
// Wait a bit to poll again, but do it in 100 ms chunks
|
||||||
QThread::sleep(3);
|
// so we can be interrupted reasonably quickly.
|
||||||
|
// FIXME: QWaitCondition would be better.
|
||||||
|
for (int i = 0; i < 30 && !isInterruptionRequested(); i++) {
|
||||||
|
QThread::msleep(100);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -157,6 +162,13 @@ ComputerManager::ComputerManager(QObject *parent)
|
|||||||
m_KnownHosts[computer->uuid] = computer;
|
m_KnownHosts[computer->uuid] = computer;
|
||||||
}
|
}
|
||||||
settings.endArray();
|
settings.endArray();
|
||||||
|
|
||||||
|
// To quit in a timely manner, we must block additional requests
|
||||||
|
// after we receive the aboutToQuit() signal. This is neccessary
|
||||||
|
// because NvHTTP uses aboutToQuit() to abort requests in progres
|
||||||
|
// while quitting, however this is a one time signal - additional
|
||||||
|
// requests would not be aborted and block termination.
|
||||||
|
connect(QCoreApplication::instance(), SIGNAL(aboutToQuit()), this, SLOT(handleAboutToQuit()));
|
||||||
}
|
}
|
||||||
|
|
||||||
ComputerManager::~ComputerManager()
|
ComputerManager::~ComputerManager()
|
||||||
@@ -174,38 +186,19 @@ ComputerManager::~ComputerManager()
|
|||||||
delete m_MdnsBrowser;
|
delete m_MdnsBrowser;
|
||||||
m_MdnsBrowser = nullptr;
|
m_MdnsBrowser = nullptr;
|
||||||
|
|
||||||
{
|
// Interrupt polling
|
||||||
QMutableMapIterator<QString, QThread*> i(m_PollThreads);
|
for (ComputerPollingEntry* entry : m_PollEntries) {
|
||||||
|
entry->interrupt();
|
||||||
// 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();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
// Delete all polling entries (and associated threads)
|
||||||
QMutableMapIterator<QString, NvComputer*> i(m_KnownHosts);
|
for (ComputerPollingEntry* entry : m_PollEntries) {
|
||||||
|
delete entry;
|
||||||
|
}
|
||||||
|
|
||||||
// Destroy all NvComputer objects
|
// Destroy all NvComputer objects now that polling is halted
|
||||||
while (i.hasNext()) {
|
for (NvComputer* computer : m_KnownHosts) {
|
||||||
i.next();
|
delete computer;
|
||||||
delete i.value();
|
|
||||||
i.remove();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -265,16 +258,22 @@ void ComputerManager::startPollingComputer(NvComputer* computer)
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (m_PollThreads.contains(computer->uuid)) {
|
ComputerPollingEntry* pollingEntry;
|
||||||
Q_ASSERT(m_PollThreads.value(computer->uuid)->isRunning());
|
|
||||||
return;
|
if (!m_PollEntries.contains(computer->uuid)) {
|
||||||
|
pollingEntry = m_PollEntries[computer->uuid] = new ComputerPollingEntry();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
pollingEntry = m_PollEntries[computer->uuid];
|
||||||
}
|
}
|
||||||
|
|
||||||
PcMonitorThread* thread = new PcMonitorThread(computer);
|
if (!pollingEntry->isActive()) {
|
||||||
connect(thread, SIGNAL(computerStateChanged(NvComputer*)),
|
PcMonitorThread* thread = new PcMonitorThread(computer);
|
||||||
this, SLOT(handleComputerStateChanged(NvComputer*)));
|
connect(thread, SIGNAL(computerStateChanged(NvComputer*)),
|
||||||
m_PollThreads[computer->uuid] = thread;
|
this, SLOT(handleComputerStateChanged(NvComputer*)));
|
||||||
thread->start();
|
pollingEntry->setActiveThread(thread);
|
||||||
|
thread->start();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void ComputerManager::handleMdnsServiceResolved(MdnsPendingComputer* computer,
|
void ComputerManager::handleMdnsServiceResolved(MdnsPendingComputer* computer,
|
||||||
@@ -317,14 +316,14 @@ public:
|
|||||||
|
|
||||||
void run()
|
void run()
|
||||||
{
|
{
|
||||||
QThread* pollingThread;
|
ComputerPollingEntry* pollingEntry;
|
||||||
|
|
||||||
// Only do the minimum amount of work while holding the writer lock.
|
// Only do the minimum amount of work while holding the writer lock.
|
||||||
// We must release it before calling saveHosts().
|
// We must release it before calling saveHosts().
|
||||||
{
|
{
|
||||||
QWriteLocker lock(&m_ComputerManager->m_Lock);
|
QWriteLocker lock(&m_ComputerManager->m_Lock);
|
||||||
|
|
||||||
pollingThread = m_ComputerManager->m_PollThreads.take(m_Computer->uuid);
|
pollingEntry = m_ComputerManager->m_PollEntries.take(m_Computer->uuid);
|
||||||
|
|
||||||
m_ComputerManager->m_KnownHosts.remove(m_Computer->uuid);
|
m_ComputerManager->m_KnownHosts.remove(m_Computer->uuid);
|
||||||
}
|
}
|
||||||
@@ -332,18 +331,8 @@ public:
|
|||||||
// Persist the new host list
|
// Persist the new host list
|
||||||
m_ComputerManager->saveHosts();
|
m_ComputerManager->saveHosts();
|
||||||
|
|
||||||
// Delete the polling thread
|
// Delete the polling entry first. This will stop all polling threads too.
|
||||||
if (pollingThread != nullptr) {
|
delete pollingEntry;
|
||||||
pollingThread->requestInterruption();
|
|
||||||
|
|
||||||
// We must wait here because we're going to delete computer
|
|
||||||
// and we can't do that out from underneath the poller.
|
|
||||||
pollingThread->wait();
|
|
||||||
|
|
||||||
// The thread is safe to delete now
|
|
||||||
Q_ASSERT(pollingThread->isFinished());
|
|
||||||
delete pollingThread;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Finally, delete the computer itself. This must be done
|
// Finally, delete the computer itself. This must be done
|
||||||
// last because the polling thread might be using it.
|
// last because the polling thread might be using it.
|
||||||
@@ -362,6 +351,17 @@ void ComputerManager::deleteHost(NvComputer* computer)
|
|||||||
QThreadPool::globalInstance()->start(new DeferredHostDeletionTask(this, computer));
|
QThreadPool::globalInstance()->start(new DeferredHostDeletionTask(this, computer));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void ComputerManager::handleAboutToQuit()
|
||||||
|
{
|
||||||
|
QWriteLocker lock(&m_Lock);
|
||||||
|
|
||||||
|
// Interrupt polling threads immediately, so they
|
||||||
|
// avoid making additional requests while quitting
|
||||||
|
for (ComputerPollingEntry* entry : m_PollEntries) {
|
||||||
|
entry->interrupt();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class PendingPairingTask : public QObject, public QRunnable
|
class PendingPairingTask : public QObject, public QRunnable
|
||||||
{
|
{
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
@@ -401,7 +401,7 @@ private:
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
} catch (const GfeHttpResponseException& e) {
|
} catch (const GfeHttpResponseException& e) {
|
||||||
emit pairingCompleted(m_Computer, e.toQString());
|
emit pairingCompleted(m_Computer, "GeForce Experience returned error: " + e.toQString());
|
||||||
} catch (const QtNetworkReplyException& e) {
|
} catch (const QtNetworkReplyException& e) {
|
||||||
emit pairingCompleted(m_Computer, e.toQString());
|
emit pairingCompleted(m_Computer, e.toQString());
|
||||||
}
|
}
|
||||||
@@ -498,20 +498,8 @@ void ComputerManager::stopPollingAsync()
|
|||||||
m_MdnsBrowser = nullptr;
|
m_MdnsBrowser = nullptr;
|
||||||
|
|
||||||
// Interrupt all threads, but don't wait for them to terminate
|
// Interrupt all threads, but don't wait for them to terminate
|
||||||
QMutableMapIterator<QString, QThread*> i(m_PollThreads);
|
for (ComputerPollingEntry* entry : m_PollEntries) {
|
||||||
while (i.hasNext()) {
|
entry->interrupt();
|
||||||
i.next();
|
|
||||||
|
|
||||||
QThread* thread = i.value();
|
|
||||||
|
|
||||||
// Let this thread delete itself when it's finished
|
|
||||||
connect(thread, SIGNAL(finished()),
|
|
||||||
thread, SLOT(deleteLater()));
|
|
||||||
|
|
||||||
// The threads will delete themselves when they terminate,
|
|
||||||
// but we remove them from the polling threads map here.
|
|
||||||
i.value()->requestInterruption();
|
|
||||||
i.remove();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -546,12 +534,12 @@ private:
|
|||||||
// around this issue, we will issue the request again after a few seconds if
|
// around this issue, we will issue the request again after a few seconds if
|
||||||
// we see a ServiceUnavailableError error.
|
// we see a ServiceUnavailableError error.
|
||||||
try {
|
try {
|
||||||
serverInfo = http.getServerInfo(NvHTTP::NvLogLevel::VERBOSE);
|
serverInfo = http.getServerInfo(NvHTTP::NVLL_VERBOSE);
|
||||||
} catch (const QtNetworkReplyException& e) {
|
} catch (const QtNetworkReplyException& e) {
|
||||||
if (e.getError() == QNetworkReply::ServiceUnavailableError) {
|
if (e.getError() == QNetworkReply::ServiceUnavailableError) {
|
||||||
qWarning() << "Retrying request in 5 seconds after ServiceUnavailableError";
|
qWarning() << "Retrying request in 5 seconds after ServiceUnavailableError";
|
||||||
QThread::sleep(5);
|
QThread::sleep(5);
|
||||||
serverInfo = http.getServerInfo(NvHTTP::NvLogLevel::VERBOSE);
|
serverInfo = http.getServerInfo(NvHTTP::NVLL_VERBOSE);
|
||||||
qInfo() << "Retry successful";
|
qInfo() << "Retry successful";
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
@@ -611,7 +599,7 @@ private:
|
|||||||
|
|
||||||
// Get the WAN IP address using STUN if we're on mDNS
|
// Get the WAN IP address using STUN if we're on mDNS
|
||||||
quint32 addr;
|
quint32 addr;
|
||||||
int err = LiFindExternalAddressIP4("stun.stunprotocol.org", 3478, &addr);
|
int err = LiFindExternalAddressIP4("stun.moonlight-stream.org", 3478, &addr);
|
||||||
if (err == 0) {
|
if (err == 0) {
|
||||||
newComputer->remoteAddress = QHostAddress(qFromBigEndian(addr)).toString();
|
newComputer->remoteAddress = QHostAddress(qFromBigEndian(addr)).toString();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -51,6 +51,79 @@ private:
|
|||||||
QMdnsEngine::Resolver m_Resolver;
|
QMdnsEngine::Resolver m_Resolver;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
class ComputerPollingEntry
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
ComputerPollingEntry()
|
||||||
|
: m_ActiveThread(nullptr)
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
virtual ~ComputerPollingEntry()
|
||||||
|
{
|
||||||
|
interrupt();
|
||||||
|
|
||||||
|
// interrupt() should have taken care of this
|
||||||
|
Q_ASSERT(m_ActiveThread == nullptr);
|
||||||
|
|
||||||
|
for (QThread* thread : m_InactiveList) {
|
||||||
|
thread->wait();
|
||||||
|
delete thread;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isActive()
|
||||||
|
{
|
||||||
|
cleanInactiveList();
|
||||||
|
|
||||||
|
return m_ActiveThread != nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
void setActiveThread(QThread* thread)
|
||||||
|
{
|
||||||
|
cleanInactiveList();
|
||||||
|
|
||||||
|
Q_ASSERT(!isActive());
|
||||||
|
m_ActiveThread = thread;
|
||||||
|
}
|
||||||
|
|
||||||
|
void interrupt()
|
||||||
|
{
|
||||||
|
cleanInactiveList();
|
||||||
|
|
||||||
|
if (m_ActiveThread != nullptr) {
|
||||||
|
// Interrupt the active thread
|
||||||
|
m_ActiveThread->requestInterruption();
|
||||||
|
|
||||||
|
// Place it on the inactive list awaiting death
|
||||||
|
m_InactiveList.append(m_ActiveThread);
|
||||||
|
|
||||||
|
m_ActiveThread = nullptr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
void cleanInactiveList()
|
||||||
|
{
|
||||||
|
QMutableListIterator<QThread*> i(m_InactiveList);
|
||||||
|
|
||||||
|
// Reap any threads that have finished
|
||||||
|
while (i.hasNext()) {
|
||||||
|
i.next();
|
||||||
|
|
||||||
|
QThread* thread = i.value();
|
||||||
|
if (thread->isFinished()) {
|
||||||
|
delete thread;
|
||||||
|
i.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QThread* m_ActiveThread;
|
||||||
|
QList<QThread*> m_InactiveList;
|
||||||
|
};
|
||||||
|
|
||||||
class ComputerManager : public QObject
|
class ComputerManager : public QObject
|
||||||
{
|
{
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
@@ -88,6 +161,8 @@ signals:
|
|||||||
void quitAppCompleted(QVariant error);
|
void quitAppCompleted(QVariant error);
|
||||||
|
|
||||||
private slots:
|
private slots:
|
||||||
|
void handleAboutToQuit();
|
||||||
|
|
||||||
void handleComputerStateChanged(NvComputer* computer);
|
void handleComputerStateChanged(NvComputer* computer);
|
||||||
|
|
||||||
void handleMdnsServiceResolved(MdnsPendingComputer* computer, const QHostAddress& address);
|
void handleMdnsServiceResolved(MdnsPendingComputer* computer, const QHostAddress& address);
|
||||||
@@ -100,7 +175,7 @@ private:
|
|||||||
int m_PollingRef;
|
int m_PollingRef;
|
||||||
QReadWriteLock m_Lock;
|
QReadWriteLock m_Lock;
|
||||||
QMap<QString, NvComputer*> m_KnownHosts;
|
QMap<QString, NvComputer*> m_KnownHosts;
|
||||||
QMap<QString, QThread*> m_PollThreads;
|
QMap<QString, ComputerPollingEntry*> m_PollEntries;
|
||||||
QMdnsEngine::Server m_MdnsServer;
|
QMdnsEngine::Server m_MdnsServer;
|
||||||
QMdnsEngine::Browser* m_MdnsBrowser;
|
QMdnsEngine::Browser* m_MdnsBrowser;
|
||||||
QMdnsEngine::Cache m_MdnsCache;
|
QMdnsEngine::Cache m_MdnsCache;
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
#include <QUdpSocket>
|
#include <QUdpSocket>
|
||||||
#include <QHostInfo>
|
#include <QHostInfo>
|
||||||
|
#include <QNetworkInterface>
|
||||||
|
|
||||||
#define SER_NAME "hostname"
|
#define SER_NAME "hostname"
|
||||||
#define SER_UUID "uuid"
|
#define SER_UUID "uuid"
|
||||||
@@ -172,6 +173,32 @@ bool NvComputer::wake()
|
|||||||
QVector<QString> addressList = uniqueAddresses();
|
QVector<QString> addressList = uniqueAddresses();
|
||||||
addressList.append("255.255.255.255");
|
addressList.append("255.255.255.255");
|
||||||
|
|
||||||
|
// Try to broadcast on all available NICs
|
||||||
|
for (const QNetworkInterface& nic : QNetworkInterface::allInterfaces()) {
|
||||||
|
// Ensure the interface is up and skip the loopback adapter
|
||||||
|
if ((nic.flags() & QNetworkInterface::IsUp) == 0 ||
|
||||||
|
(nic.flags() & QNetworkInterface::IsLoopBack) != 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
QHostAddress allNodesMulticast("FF02::1");
|
||||||
|
for (const QNetworkAddressEntry& addr : nic.addressEntries()) {
|
||||||
|
// Store the scope ID for this NIC if IPv6 is enabled
|
||||||
|
if (!addr.ip().scopeId().isEmpty()) {
|
||||||
|
allNodesMulticast.setScopeId(addr.ip().scopeId());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip IPv6 which doesn't support broadcast
|
||||||
|
if (!addr.broadcast().isNull()) {
|
||||||
|
addressList.append(addr.broadcast().toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!allNodesMulticast.scopeId().isEmpty()) {
|
||||||
|
addressList.append(allNodesMulticast.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Try all unique address strings or host names
|
// Try all unique address strings or host names
|
||||||
bool success = false;
|
bool success = false;
|
||||||
for (QString& addressString : addressList) {
|
for (QString& addressString : addressList) {
|
||||||
@@ -186,16 +213,14 @@ bool NvComputer::wake()
|
|||||||
for (QHostAddress& address : hostInfo.addresses()) {
|
for (QHostAddress& address : hostInfo.addresses()) {
|
||||||
QUdpSocket sock;
|
QUdpSocket sock;
|
||||||
|
|
||||||
// Bind to any address on the correct protocol
|
// Send to all ports
|
||||||
if (sock.bind(address.protocol() == QUdpSocket::IPv4Protocol ?
|
for (quint16 port : WOL_PORTS) {
|
||||||
QHostAddress::AnyIPv4 : QHostAddress::AnyIPv6)) {
|
if (sock.writeDatagram(wolPayload, address, port)) {
|
||||||
|
qInfo().nospace().noquote() << "Sent WoL packet to " << name << " via " << address.toString() << ":" << port;
|
||||||
// Send to all ports
|
success = true;
|
||||||
for (quint16 port : WOL_PORTS) {
|
}
|
||||||
if (sock.writeDatagram(wolPayload, address, port)) {
|
else {
|
||||||
qInfo().nospace().noquote() << "Send WoL packet to " << name << " via " << address.toString() << ":" << port;
|
qWarning() << "Send failed:" << sock.error();
|
||||||
success = true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,9 @@
|
|||||||
#include <QNetworkProxy>
|
#include <QNetworkProxy>
|
||||||
|
|
||||||
#define REQUEST_TIMEOUT_MS 5000
|
#define REQUEST_TIMEOUT_MS 5000
|
||||||
|
#define LAUNCH_TIMEOUT_MS 120000
|
||||||
|
#define RESUME_TIMEOUT_MS 30000
|
||||||
|
#define QUIT_TIMEOUT_MS 30000
|
||||||
|
|
||||||
NvHTTP::NvHTTP(QString address, QSslCertificate serverCert) :
|
NvHTTP::NvHTTP(QString address, QSslCertificate serverCert) :
|
||||||
m_Address(address),
|
m_Address(address),
|
||||||
@@ -30,6 +33,8 @@ NvHTTP::NvHTTP(QString address, QSslCertificate serverCert) :
|
|||||||
// Never use a proxy server
|
// Never use a proxy server
|
||||||
QNetworkProxy noProxy(QNetworkProxy::NoProxy);
|
QNetworkProxy noProxy(QNetworkProxy::NoProxy);
|
||||||
m_Nam.setProxy(noProxy);
|
m_Nam.setProxy(noProxy);
|
||||||
|
|
||||||
|
connect(&m_Nam, &QNetworkAccessManager::sslErrors, this, &NvHTTP::handleSslErrors);
|
||||||
}
|
}
|
||||||
|
|
||||||
void NvHTTP::setServerCert(QSslCertificate serverCert)
|
void NvHTTP::setServerCert(QSslCertificate serverCert)
|
||||||
@@ -89,7 +94,7 @@ NvHTTP::getServerInfo(NvLogLevel logLevel)
|
|||||||
serverInfo = openConnectionToString(m_BaseUrlHttps,
|
serverInfo = openConnectionToString(m_BaseUrlHttps,
|
||||||
"serverinfo",
|
"serverinfo",
|
||||||
nullptr,
|
nullptr,
|
||||||
true,
|
REQUEST_TIMEOUT_MS,
|
||||||
logLevel);
|
logLevel);
|
||||||
// Throws if the request failed
|
// Throws if the request failed
|
||||||
verifyResponseStatus(serverInfo);
|
verifyResponseStatus(serverInfo);
|
||||||
@@ -102,7 +107,7 @@ NvHTTP::getServerInfo(NvLogLevel logLevel)
|
|||||||
serverInfo = openConnectionToString(m_BaseUrlHttp,
|
serverInfo = openConnectionToString(m_BaseUrlHttp,
|
||||||
"serverinfo",
|
"serverinfo",
|
||||||
nullptr,
|
nullptr,
|
||||||
true,
|
REQUEST_TIMEOUT_MS,
|
||||||
logLevel);
|
logLevel);
|
||||||
verifyResponseStatus(serverInfo);
|
verifyResponseStatus(serverInfo);
|
||||||
}
|
}
|
||||||
@@ -119,7 +124,7 @@ NvHTTP::getServerInfo(NvLogLevel logLevel)
|
|||||||
serverInfo = openConnectionToString(m_BaseUrlHttp,
|
serverInfo = openConnectionToString(m_BaseUrlHttp,
|
||||||
"serverinfo",
|
"serverinfo",
|
||||||
nullptr,
|
nullptr,
|
||||||
true,
|
REQUEST_TIMEOUT_MS,
|
||||||
logLevel);
|
logLevel);
|
||||||
verifyResponseStatus(serverInfo);
|
verifyResponseStatus(serverInfo);
|
||||||
}
|
}
|
||||||
@@ -169,7 +174,10 @@ NvHTTP::launchApp(int appId,
|
|||||||
"appid="+QString::number(appId)+
|
"appid="+QString::number(appId)+
|
||||||
"&mode="+QString::number(streamConfig->width)+"x"+
|
"&mode="+QString::number(streamConfig->width)+"x"+
|
||||||
QString::number(streamConfig->height)+"x"+
|
QString::number(streamConfig->height)+"x"+
|
||||||
QString::number(streamConfig->fps)+
|
// Using an FPS value over 60 causes SOPS to default to 720p60,
|
||||||
|
// so force it to 60 when starting. This won't impact our ability
|
||||||
|
// to get > 60 FPS while actually streaming though.
|
||||||
|
QString::number(streamConfig->fps > 60 ? 60 : 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(riKeyId)+
|
"&rikeyid="+QString::number(riKeyId)+
|
||||||
@@ -180,7 +188,7 @@ NvHTTP::launchApp(int appId,
|
|||||||
"&surroundAudioInfo="+getSurroundAudioInfoString(streamConfig->audioConfiguration)+
|
"&surroundAudioInfo="+getSurroundAudioInfoString(streamConfig->audioConfiguration)+
|
||||||
"&remoteControllersBitmap="+QString::number(gamepadMask)+
|
"&remoteControllersBitmap="+QString::number(gamepadMask)+
|
||||||
"&gcmap="+QString::number(gamepadMask),
|
"&gcmap="+QString::number(gamepadMask),
|
||||||
false);
|
LAUNCH_TIMEOUT_MS);
|
||||||
|
|
||||||
// Throws if the request failed
|
// Throws if the request failed
|
||||||
verifyResponseStatus(response);
|
verifyResponseStatus(response);
|
||||||
@@ -200,7 +208,7 @@ NvHTTP::resumeApp(PSTREAM_CONFIGURATION streamConfig)
|
|||||||
"rikey="+QString(QByteArray(streamConfig->remoteInputAesKey, sizeof(streamConfig->remoteInputAesKey)).toHex())+
|
"rikey="+QString(QByteArray(streamConfig->remoteInputAesKey, sizeof(streamConfig->remoteInputAesKey)).toHex())+
|
||||||
"&rikeyid="+QString::number(riKeyId)+
|
"&rikeyid="+QString::number(riKeyId)+
|
||||||
"&surroundAudioInfo="+getSurroundAudioInfoString(streamConfig->audioConfiguration),
|
"&surroundAudioInfo="+getSurroundAudioInfoString(streamConfig->audioConfiguration),
|
||||||
false);
|
RESUME_TIMEOUT_MS);
|
||||||
|
|
||||||
// Throws if the request failed
|
// Throws if the request failed
|
||||||
verifyResponseStatus(response);
|
verifyResponseStatus(response);
|
||||||
@@ -213,14 +221,14 @@ NvHTTP::quitApp()
|
|||||||
openConnectionToString(m_BaseUrlHttps,
|
openConnectionToString(m_BaseUrlHttps,
|
||||||
"cancel",
|
"cancel",
|
||||||
nullptr,
|
nullptr,
|
||||||
false);
|
QUIT_TIMEOUT_MS);
|
||||||
|
|
||||||
// Throws if the request failed
|
// Throws if the request failed
|
||||||
verifyResponseStatus(response);
|
verifyResponseStatus(response);
|
||||||
|
|
||||||
// Newer GFE versions will just return success even if quitting fails
|
// Newer GFE versions will just return success even if quitting fails
|
||||||
// if we're not the original requestor.
|
// if we're not the original requestor.
|
||||||
if (getCurrentGame(getServerInfo(NvHTTP::ERROR)) != 0) {
|
if (getCurrentGame(getServerInfo(NvHTTP::NVLL_ERROR)) != 0) {
|
||||||
// Generate a synthetic GfeResponseException letting the caller know
|
// Generate a synthetic GfeResponseException letting the caller know
|
||||||
// that they can't kill someone else's stream.
|
// that they can't kill someone else's stream.
|
||||||
throw GfeHttpResponseException(599, "");
|
throw GfeHttpResponseException(599, "");
|
||||||
@@ -260,8 +268,8 @@ NvHTTP::getAppList()
|
|||||||
QString appxml = openConnectionToString(m_BaseUrlHttps,
|
QString appxml = openConnectionToString(m_BaseUrlHttps,
|
||||||
"applist",
|
"applist",
|
||||||
nullptr,
|
nullptr,
|
||||||
true,
|
REQUEST_TIMEOUT_MS,
|
||||||
NvLogLevel::ERROR);
|
NvLogLevel::NVLL_ERROR);
|
||||||
verifyResponseStatus(appxml);
|
verifyResponseStatus(appxml);
|
||||||
|
|
||||||
QXmlStreamReader xmlReader(appxml);
|
QXmlStreamReader xmlReader(appxml);
|
||||||
@@ -328,8 +336,8 @@ NvHTTP::getBoxArt(int appId)
|
|||||||
"appasset",
|
"appasset",
|
||||||
"appid="+QString::number(appId)+
|
"appid="+QString::number(appId)+
|
||||||
"&AssetType=2&AssetIdx=0",
|
"&AssetType=2&AssetIdx=0",
|
||||||
true,
|
REQUEST_TIMEOUT_MS,
|
||||||
NvLogLevel::VERBOSE);
|
NvLogLevel::NVLL_VERBOSE);
|
||||||
QImage image = QImageReader(reply).read();
|
QImage image = QImageReader(reply).read();
|
||||||
delete reply;
|
delete reply;
|
||||||
|
|
||||||
@@ -371,14 +379,36 @@ NvHTTP::getXmlString(QString xml,
|
|||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void NvHTTP::handleSslErrors(QNetworkReply* reply, const QList<QSslError>& errors)
|
||||||
|
{
|
||||||
|
bool ignoreErrors = true;
|
||||||
|
|
||||||
|
if (m_ServerCert.isNull()) {
|
||||||
|
// We should never make an HTTPS request without a cert
|
||||||
|
Q_ASSERT(!m_ServerCert.isNull());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (auto error : errors) {
|
||||||
|
if (m_ServerCert != error.certificate()) {
|
||||||
|
ignoreErrors = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ignoreErrors) {
|
||||||
|
reply->ignoreSslErrors(errors);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
QString
|
QString
|
||||||
NvHTTP::openConnectionToString(QUrl baseUrl,
|
NvHTTP::openConnectionToString(QUrl baseUrl,
|
||||||
QString command,
|
QString command,
|
||||||
QString arguments,
|
QString arguments,
|
||||||
bool enableTimeout,
|
int timeoutMs,
|
||||||
NvLogLevel logLevel)
|
NvLogLevel logLevel)
|
||||||
{
|
{
|
||||||
QNetworkReply* reply = openConnection(baseUrl, command, arguments, enableTimeout, logLevel);
|
QNetworkReply* reply = openConnection(baseUrl, command, arguments, timeoutMs, logLevel);
|
||||||
QString ret;
|
QString ret;
|
||||||
|
|
||||||
QTextStream stream(reply);
|
QTextStream stream(reply);
|
||||||
@@ -393,7 +423,7 @@ QNetworkReply*
|
|||||||
NvHTTP::openConnection(QUrl baseUrl,
|
NvHTTP::openConnection(QUrl baseUrl,
|
||||||
QString command,
|
QString command,
|
||||||
QString arguments,
|
QString arguments,
|
||||||
bool enableTimeout,
|
int timeoutMs,
|
||||||
NvLogLevel logLevel)
|
NvLogLevel logLevel)
|
||||||
{
|
{
|
||||||
// Build a URL for the request
|
// Build a URL for the request
|
||||||
@@ -407,38 +437,21 @@ NvHTTP::openConnection(QUrl baseUrl,
|
|||||||
QUuid::createUuid().toRfc4122().toHex() +
|
QUuid::createUuid().toRfc4122().toHex() +
|
||||||
((arguments != nullptr) ? ("&" + arguments) : ""));
|
((arguments != nullptr) ? ("&" + arguments) : ""));
|
||||||
|
|
||||||
QNetworkRequest request = QNetworkRequest(url);
|
QNetworkRequest request(url);
|
||||||
|
|
||||||
// Add our client certificate
|
// Add our client certificate
|
||||||
request.setSslConfiguration(IdentityManager::get()->getSslConfig());
|
request.setSslConfiguration(IdentityManager::get()->getSslConfig());
|
||||||
|
|
||||||
QNetworkReply* reply = m_Nam.get(request);
|
QNetworkReply* reply = m_Nam.get(request);
|
||||||
|
|
||||||
// Assert that we always have a server cert for HTTPS, since
|
|
||||||
// the request will fail anyway if we do not.
|
|
||||||
Q_ASSERT(!m_ServerCert.isNull() || baseUrl == m_BaseUrlHttp);
|
|
||||||
|
|
||||||
if (!m_ServerCert.isNull()) {
|
|
||||||
// Pin the server certificate received during pairing
|
|
||||||
QList<QSslError> expectedSslErrors;
|
|
||||||
expectedSslErrors.append(QSslError(QSslError::HostNameMismatch, m_ServerCert));
|
|
||||||
expectedSslErrors.append(QSslError(QSslError::SelfSignedCertificate, m_ServerCert));
|
|
||||||
|
|
||||||
// The SecureTransport backend for Qt TLS on macOS throws CertificateUntrusted
|
|
||||||
// instead of SelfSignedCertificate, so we will need to allow that error too.
|
|
||||||
expectedSslErrors.append(QSslError(QSslError::CertificateUntrusted, m_ServerCert));
|
|
||||||
|
|
||||||
reply->ignoreSslErrors(expectedSslErrors);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Run the request with a timeout if requested
|
// Run the request with a timeout if requested
|
||||||
QEventLoop loop;
|
QEventLoop loop;
|
||||||
QObject::connect(reply, SIGNAL(finished()), &loop, SLOT(quit()));
|
connect(reply, SIGNAL(finished()), &loop, SLOT(quit()));
|
||||||
if (enableTimeout)
|
connect(QCoreApplication::instance(), SIGNAL(aboutToQuit()), &loop, SLOT(quit()));
|
||||||
{
|
if (timeoutMs) {
|
||||||
QTimer::singleShot(REQUEST_TIMEOUT_MS, &loop, SLOT(quit()));
|
QTimer::singleShot(timeoutMs, &loop, SLOT(quit()));
|
||||||
}
|
}
|
||||||
if (logLevel >= NvLogLevel::VERBOSE) {
|
if (logLevel >= NvLogLevel::NVLL_VERBOSE) {
|
||||||
qInfo() << "Executing request:" << url.toString();
|
qInfo() << "Executing request:" << url.toString();
|
||||||
}
|
}
|
||||||
loop.exec(QEventLoop::ExcludeUserInputEvents);
|
loop.exec(QEventLoop::ExcludeUserInputEvents);
|
||||||
@@ -446,7 +459,7 @@ NvHTTP::openConnection(QUrl baseUrl,
|
|||||||
// Abort the request if it timed out
|
// Abort the request if it timed out
|
||||||
if (!reply->isFinished())
|
if (!reply->isFinished())
|
||||||
{
|
{
|
||||||
if (logLevel >= NvLogLevel::ERROR) {
|
if (logLevel >= NvLogLevel::NVLL_ERROR) {
|
||||||
qWarning() << "Aborting timed out request for" << url.toString();
|
qWarning() << "Aborting timed out request for" << url.toString();
|
||||||
}
|
}
|
||||||
reply->abort();
|
reply->abort();
|
||||||
@@ -459,7 +472,7 @@ NvHTTP::openConnection(QUrl baseUrl,
|
|||||||
// Handle error
|
// Handle error
|
||||||
if (reply->error() != QNetworkReply::NoError)
|
if (reply->error() != QNetworkReply::NoError)
|
||||||
{
|
{
|
||||||
if (logLevel >= NvLogLevel::ERROR) {
|
if (logLevel >= NvLogLevel::NVLL_ERROR) {
|
||||||
qWarning() << command << " request failed with error " << reply->error();
|
qWarning() << command << " request failed with error " << reply->error();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -470,6 +483,11 @@ NvHTTP::openConnection(QUrl baseUrl,
|
|||||||
delete reply;
|
delete reply;
|
||||||
throw exception;
|
throw exception;
|
||||||
}
|
}
|
||||||
|
else if (reply->error() == QNetworkReply::OperationCanceledError) {
|
||||||
|
QtNetworkReplyException exception(QNetworkReply::TimeoutError, "Request timed out");
|
||||||
|
delete reply;
|
||||||
|
throw exception;
|
||||||
|
}
|
||||||
else {
|
else {
|
||||||
QtNetworkReplyException exception(reply->error(), reply->errorString());
|
QtNetworkReplyException exception(reply->error(), reply->errorString());
|
||||||
delete reply;
|
delete reply;
|
||||||
|
|||||||
@@ -113,13 +113,15 @@ private:
|
|||||||
QString m_ErrorText;
|
QString m_ErrorText;
|
||||||
};
|
};
|
||||||
|
|
||||||
class NvHTTP
|
class NvHTTP : public QObject
|
||||||
{
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
public:
|
public:
|
||||||
enum NvLogLevel {
|
enum NvLogLevel {
|
||||||
NONE,
|
NVLL_NONE,
|
||||||
ERROR,
|
NVLL_ERROR,
|
||||||
VERBOSE
|
NVLL_VERBOSE
|
||||||
};
|
};
|
||||||
|
|
||||||
explicit NvHTTP(QString address, QSslCertificate serverCert);
|
explicit NvHTTP(QString address, QSslCertificate serverCert);
|
||||||
@@ -149,8 +151,8 @@ public:
|
|||||||
openConnectionToString(QUrl baseUrl,
|
openConnectionToString(QUrl baseUrl,
|
||||||
QString command,
|
QString command,
|
||||||
QString arguments,
|
QString arguments,
|
||||||
bool enableTimeout,
|
int timeoutMs,
|
||||||
NvLogLevel logLevel = NvLogLevel::VERBOSE);
|
NvLogLevel logLevel = NvLogLevel::NVLL_VERBOSE);
|
||||||
|
|
||||||
void setServerCert(QSslCertificate serverCert);
|
void setServerCert(QSslCertificate serverCert);
|
||||||
|
|
||||||
@@ -184,11 +186,14 @@ public:
|
|||||||
QUrl m_BaseUrlHttp;
|
QUrl m_BaseUrlHttp;
|
||||||
QUrl m_BaseUrlHttps;
|
QUrl m_BaseUrlHttps;
|
||||||
private:
|
private:
|
||||||
|
void
|
||||||
|
handleSslErrors(QNetworkReply* reply, const QList<QSslError>& errors);
|
||||||
|
|
||||||
QNetworkReply*
|
QNetworkReply*
|
||||||
openConnection(QUrl baseUrl,
|
openConnection(QUrl baseUrl,
|
||||||
QString command,
|
QString command,
|
||||||
QString arguments,
|
QString arguments,
|
||||||
bool enableTimeout,
|
int timeoutMs,
|
||||||
NvLogLevel logLevel);
|
NvLogLevel logLevel);
|
||||||
|
|
||||||
QString m_Address;
|
QString m_Address;
|
||||||
|
|||||||
@@ -8,6 +8,8 @@
|
|||||||
#include <openssl/x509.h>
|
#include <openssl/x509.h>
|
||||||
#include <openssl/evp.h>
|
#include <openssl/evp.h>
|
||||||
|
|
||||||
|
#define REQUEST_TIMEOUT_MS 5000
|
||||||
|
|
||||||
NvPairingManager::NvPairingManager(QString address) :
|
NvPairingManager::NvPairingManager(QString address) :
|
||||||
m_Http(address, QSslCertificate())
|
m_Http(address, QSslCertificate())
|
||||||
{
|
{
|
||||||
@@ -192,7 +194,7 @@ NvPairingManager::pair(QString appVersion, QString pin, QSslCertificate& serverC
|
|||||||
"pair",
|
"pair",
|
||||||
"devicename=roth&updateState=1&phrase=getservercert&salt=" +
|
"devicename=roth&updateState=1&phrase=getservercert&salt=" +
|
||||||
salt.toHex() + "&clientcert=" + IdentityManager::get()->getCertificate().toHex(),
|
salt.toHex() + "&clientcert=" + IdentityManager::get()->getCertificate().toHex(),
|
||||||
false);
|
0);
|
||||||
NvHTTP::verifyResponseStatus(getCert);
|
NvHTTP::verifyResponseStatus(getCert);
|
||||||
if (NvHTTP::getXmlString(getCert, "paired") != "1")
|
if (NvHTTP::getXmlString(getCert, "paired") != "1")
|
||||||
{
|
{
|
||||||
@@ -204,7 +206,7 @@ NvPairingManager::pair(QString appVersion, QString pin, QSslCertificate& serverC
|
|||||||
if (serverCertStr == nullptr)
|
if (serverCertStr == nullptr)
|
||||||
{
|
{
|
||||||
qCritical() << "Server likely already pairing";
|
qCritical() << "Server likely already pairing";
|
||||||
m_Http.openConnectionToString(m_Http.m_BaseUrlHttp, "unpair", nullptr, true);
|
m_Http.openConnectionToString(m_Http.m_BaseUrlHttp, "unpair", nullptr, REQUEST_TIMEOUT_MS);
|
||||||
return PairState::ALREADY_IN_PROGRESS;
|
return PairState::ALREADY_IN_PROGRESS;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -213,7 +215,7 @@ NvPairingManager::pair(QString appVersion, QString pin, QSslCertificate& serverC
|
|||||||
Q_ASSERT(!serverCert.isNull());
|
Q_ASSERT(!serverCert.isNull());
|
||||||
|
|
||||||
qCritical() << "Failed to parse plaincert";
|
qCritical() << "Failed to parse plaincert";
|
||||||
m_Http.openConnectionToString(m_Http.m_BaseUrlHttp, "unpair", nullptr, true);
|
m_Http.openConnectionToString(m_Http.m_BaseUrlHttp, "unpair", nullptr, REQUEST_TIMEOUT_MS);
|
||||||
return PairState::FAILED;
|
return PairState::FAILED;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -226,12 +228,12 @@ NvPairingManager::pair(QString appVersion, QString pin, QSslCertificate& serverC
|
|||||||
"pair",
|
"pair",
|
||||||
"devicename=roth&updateState=1&clientchallenge=" +
|
"devicename=roth&updateState=1&clientchallenge=" +
|
||||||
encryptedChallenge.toHex(),
|
encryptedChallenge.toHex(),
|
||||||
true);
|
REQUEST_TIMEOUT_MS);
|
||||||
NvHTTP::verifyResponseStatus(challengeXml);
|
NvHTTP::verifyResponseStatus(challengeXml);
|
||||||
if (NvHTTP::getXmlString(challengeXml, "paired") != "1")
|
if (NvHTTP::getXmlString(challengeXml, "paired") != "1")
|
||||||
{
|
{
|
||||||
qCritical() << "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, REQUEST_TIMEOUT_MS);
|
||||||
return PairState::FAILED;
|
return PairState::FAILED;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -259,12 +261,12 @@ NvPairingManager::pair(QString appVersion, QString pin, QSslCertificate& serverC
|
|||||||
"pair",
|
"pair",
|
||||||
"devicename=roth&updateState=1&serverchallengeresp=" +
|
"devicename=roth&updateState=1&serverchallengeresp=" +
|
||||||
encryptedChallengeResponseHash.toHex(),
|
encryptedChallengeResponseHash.toHex(),
|
||||||
true);
|
REQUEST_TIMEOUT_MS);
|
||||||
NvHTTP::verifyResponseStatus(respXml);
|
NvHTTP::verifyResponseStatus(respXml);
|
||||||
if (NvHTTP::getXmlString(respXml, "paired") != "1")
|
if (NvHTTP::getXmlString(respXml, "paired") != "1")
|
||||||
{
|
{
|
||||||
qCritical() << "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, REQUEST_TIMEOUT_MS);
|
||||||
return PairState::FAILED;
|
return PairState::FAILED;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -277,7 +279,7 @@ NvPairingManager::pair(QString appVersion, QString pin, QSslCertificate& serverC
|
|||||||
serverCertStr))
|
serverCertStr))
|
||||||
{
|
{
|
||||||
qCritical() << "MITM detected";
|
qCritical() << "MITM detected";
|
||||||
m_Http.openConnectionToString(m_Http.m_BaseUrlHttp, "unpair", nullptr, true);
|
m_Http.openConnectionToString(m_Http.m_BaseUrlHttp, "unpair", nullptr, REQUEST_TIMEOUT_MS);
|
||||||
return PairState::FAILED;
|
return PairState::FAILED;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -288,7 +290,7 @@ NvPairingManager::pair(QString appVersion, QString pin, QSslCertificate& serverC
|
|||||||
if (QCryptographicHash::hash(expectedResponseData, hashAlgo) != serverResponse)
|
if (QCryptographicHash::hash(expectedResponseData, hashAlgo) != serverResponse)
|
||||||
{
|
{
|
||||||
qCritical() << "Incorrect PIN";
|
qCritical() << "Incorrect PIN";
|
||||||
m_Http.openConnectionToString(m_Http.m_BaseUrlHttp, "unpair", nullptr, true);
|
m_Http.openConnectionToString(m_Http.m_BaseUrlHttp, "unpair", nullptr, REQUEST_TIMEOUT_MS);
|
||||||
return PairState::PIN_WRONG;
|
return PairState::PIN_WRONG;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -300,24 +302,24 @@ NvPairingManager::pair(QString appVersion, QString pin, QSslCertificate& serverC
|
|||||||
"pair",
|
"pair",
|
||||||
"devicename=roth&updateState=1&clientpairingsecret=" +
|
"devicename=roth&updateState=1&clientpairingsecret=" +
|
||||||
clientPairingSecret.toHex(),
|
clientPairingSecret.toHex(),
|
||||||
true);
|
REQUEST_TIMEOUT_MS);
|
||||||
NvHTTP::verifyResponseStatus(secretRespXml);
|
NvHTTP::verifyResponseStatus(secretRespXml);
|
||||||
if (NvHTTP::getXmlString(secretRespXml, "paired") != "1")
|
if (NvHTTP::getXmlString(secretRespXml, "paired") != "1")
|
||||||
{
|
{
|
||||||
qCritical() << "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, REQUEST_TIMEOUT_MS);
|
||||||
return PairState::FAILED;
|
return PairState::FAILED;
|
||||||
}
|
}
|
||||||
|
|
||||||
QString pairChallengeXml = m_Http.openConnectionToString(m_Http.m_BaseUrlHttps,
|
QString pairChallengeXml = m_Http.openConnectionToString(m_Http.m_BaseUrlHttps,
|
||||||
"pair",
|
"pair",
|
||||||
"devicename=roth&updateState=1&phrase=pairchallenge",
|
"devicename=roth&updateState=1&phrase=pairchallenge",
|
||||||
true);
|
REQUEST_TIMEOUT_MS);
|
||||||
NvHTTP::verifyResponseStatus(pairChallengeXml);
|
NvHTTP::verifyResponseStatus(pairChallengeXml);
|
||||||
if (NvHTTP::getXmlString(pairChallengeXml, "paired") != "1")
|
if (NvHTTP::getXmlString(pairChallengeXml, "paired") != "1")
|
||||||
{
|
{
|
||||||
qCritical() << "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, REQUEST_TIMEOUT_MS);
|
||||||
return PairState::FAILED;
|
return PairState::FAILED;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,104 @@
|
|||||||
|
#include "systemproperties.h"
|
||||||
|
|
||||||
|
#include "streaming/session.h"
|
||||||
|
#include "streaming/streamutils.h"
|
||||||
|
|
||||||
|
SystemProperties::SystemProperties()
|
||||||
|
{
|
||||||
|
isRunningWayland = qgetenv("XDG_SESSION_TYPE") == "wayland";
|
||||||
|
|
||||||
|
#ifdef Q_OS_WIN32
|
||||||
|
isWow64 = QSysInfo::currentCpuArchitecture() != QSysInfo::buildCpuArchitecture();
|
||||||
|
#else
|
||||||
|
isWow64 = false;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifndef STEAM_LINK
|
||||||
|
hasBrowser = true;
|
||||||
|
#else
|
||||||
|
hasBrowser = false;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
unmappedGamepads = SdlInputHandler::getUnmappedGamepads();
|
||||||
|
|
||||||
|
// Populate data that requires talking to SDL. We do it all in one shot
|
||||||
|
// and cache the results to speed up future queries on this data.
|
||||||
|
querySdlVideoInfo();
|
||||||
|
|
||||||
|
Q_ASSERT(maximumStreamingFrameRate >= 60);
|
||||||
|
Q_ASSERT(!monitorDesktopResolutions.isEmpty());
|
||||||
|
Q_ASSERT(!monitorNativeResolutions.isEmpty());
|
||||||
|
}
|
||||||
|
|
||||||
|
QRect SystemProperties::getDesktopResolution(int displayIndex)
|
||||||
|
{
|
||||||
|
// Returns default constructed QRect if out of bounds
|
||||||
|
return monitorDesktopResolutions.value(displayIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
QRect SystemProperties::getNativeResolution(int displayIndex)
|
||||||
|
{
|
||||||
|
// Returns default constructed QRect if out of bounds
|
||||||
|
return monitorNativeResolutions.value(displayIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
void SystemProperties::querySdlVideoInfo()
|
||||||
|
{
|
||||||
|
monitorDesktopResolutions.clear();
|
||||||
|
monitorNativeResolutions.clear();
|
||||||
|
hasHardwareAcceleration = false;
|
||||||
|
|
||||||
|
// Never let the maximum drop below 60 FPS
|
||||||
|
maximumStreamingFrameRate = 60;
|
||||||
|
|
||||||
|
SDL_DisplayMode bestMode;
|
||||||
|
for (int displayIndex = 0; displayIndex < SDL_GetNumVideoDisplays(); displayIndex++) {
|
||||||
|
SDL_DisplayMode desktopMode;
|
||||||
|
int err;
|
||||||
|
|
||||||
|
err = SDL_GetDesktopDisplayMode(displayIndex, &desktopMode);
|
||||||
|
if (err == 0) {
|
||||||
|
monitorDesktopResolutions.insert(displayIndex, QRect(0, 0, desktopMode.w, desktopMode.h));
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"SDL_GetDesktopDisplayMode() failed: %s",
|
||||||
|
SDL_GetError());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (StreamUtils::getRealDesktopMode(displayIndex, &desktopMode)) {
|
||||||
|
monitorNativeResolutions.insert(displayIndex, QRect(0, 0, desktopMode.w, desktopMode.h));
|
||||||
|
|
||||||
|
// Start at desktop mode and work our way up
|
||||||
|
bestMode = desktopMode;
|
||||||
|
for (int i = 0; i < SDL_GetNumDisplayModes(displayIndex); i++) {
|
||||||
|
SDL_DisplayMode mode;
|
||||||
|
if (SDL_GetDisplayMode(displayIndex, i, &mode) == 0) {
|
||||||
|
if (mode.w == desktopMode.w && mode.h == desktopMode.h) {
|
||||||
|
if (mode.refresh_rate > bestMode.refresh_rate) {
|
||||||
|
bestMode = mode;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
maximumStreamingFrameRate = qMax(maximumStreamingFrameRate, bestMode.refresh_rate);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SDL_Window* testWindow = SDL_CreateWindow("", 0, 0, 1280, 720, SDL_WINDOW_HIDDEN);
|
||||||
|
if (!testWindow) {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Failed to create window for hardware decode test: %s",
|
||||||
|
SDL_GetError());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
hasHardwareAcceleration =
|
||||||
|
Session::isHardwareDecodeAvailable(testWindow,
|
||||||
|
StreamingPreferences::VDS_AUTO,
|
||||||
|
VIDEO_FORMAT_H264,
|
||||||
|
1920, 1080, 60);
|
||||||
|
|
||||||
|
SDL_DestroyWindow(testWindow);
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QObject>
|
||||||
|
#include <QRect>
|
||||||
|
|
||||||
|
class SystemProperties : public QObject
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
SystemProperties();
|
||||||
|
|
||||||
|
Q_PROPERTY(bool hasHardwareAcceleration MEMBER hasHardwareAcceleration CONSTANT)
|
||||||
|
Q_PROPERTY(bool isRunningWayland MEMBER isRunningWayland CONSTANT)
|
||||||
|
Q_PROPERTY(bool isWow64 MEMBER isWow64 CONSTANT)
|
||||||
|
Q_PROPERTY(bool hasBrowser MEMBER hasBrowser CONSTANT)
|
||||||
|
Q_PROPERTY(QString unmappedGamepads MEMBER unmappedGamepads NOTIFY unmappedGamepadsChanged)
|
||||||
|
Q_PROPERTY(int maximumStreamingFrameRate MEMBER maximumStreamingFrameRate CONSTANT)
|
||||||
|
|
||||||
|
Q_INVOKABLE QRect getDesktopResolution(int displayIndex);
|
||||||
|
Q_INVOKABLE QRect getNativeResolution(int displayIndex);
|
||||||
|
|
||||||
|
signals:
|
||||||
|
void unmappedGamepadsChanged();
|
||||||
|
|
||||||
|
private:
|
||||||
|
void querySdlVideoInfo();
|
||||||
|
|
||||||
|
bool hasHardwareAcceleration;
|
||||||
|
bool isRunningWayland;
|
||||||
|
bool isWow64;
|
||||||
|
bool hasBrowser;
|
||||||
|
QString unmappedGamepads;
|
||||||
|
int maximumStreamingFrameRate;
|
||||||
|
QList<QRect> monitorDesktopResolutions;
|
||||||
|
QList<QRect> monitorNativeResolutions;
|
||||||
|
};
|
||||||
|
|
||||||
@@ -59,7 +59,7 @@ public:
|
|||||||
|
|
||||||
void showMessage(QString message, MessageType type) const
|
void showMessage(QString message, MessageType type) const
|
||||||
{
|
{
|
||||||
#if defined(Q_OS_WIN)
|
#if defined(Q_OS_WIN32)
|
||||||
UINT flags = MB_OK | MB_TOPMOST | MB_SETFOREGROUND;
|
UINT flags = MB_OK | MB_TOPMOST | MB_SETFOREGROUND;
|
||||||
flags |= (type == Info ? MB_ICONINFORMATION : MB_ICONERROR);
|
flags |= (type == Info ? MB_ICONINFORMATION : MB_ICONERROR);
|
||||||
QString title = "Moonlight";
|
QString title = "Moonlight";
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
<li>Hardware accelerated video decoding with VAAPI and VDPAU support</li>
|
<li>Hardware accelerated video decoding with VAAPI and VDPAU support</li>
|
||||||
<li>5.1 surround sound audio support</li>
|
<li>5.1 surround sound audio support</li>
|
||||||
<li>HEVC support for better video compression efficiency</li>
|
<li>HEVC support for better video compression efficiency</li>
|
||||||
<li>Gamepad support with SDL mapping compatibility</li>
|
<li>Gamepad support with force feedback for up to 4 players</li>
|
||||||
</ul>
|
</ul>
|
||||||
</description>
|
</description>
|
||||||
|
|
||||||
@@ -33,6 +33,69 @@
|
|||||||
</screenshots>
|
</screenshots>
|
||||||
|
|
||||||
<releases>
|
<releases>
|
||||||
|
<release version="0.10.0" date="2019-04-07">
|
||||||
|
<description>
|
||||||
|
<p>New features:</p>
|
||||||
|
<ul>
|
||||||
|
<li>Redesigned App Grid with larger game artwork and less blank space</li>
|
||||||
|
<li>Redesigned UI dialogs using Material Design</li>
|
||||||
|
<li>Added a connection warning overlay (which can be disabled in settings)</li>
|
||||||
|
<li>Streaming window no longer minimizes when it loses focus in full-screen mode</li>
|
||||||
|
<li>The Steam Link device is now a supported platform</li>
|
||||||
|
</ul>
|
||||||
|
<p>Bugfixes:</p>
|
||||||
|
<ul>
|
||||||
|
<li>Fixed several bugs that could cause UI hangs or lag</li>
|
||||||
|
<li>Fixed rumble support on gamepads with limited rumble motor control</li>
|
||||||
|
<li>Fixed increased memory usage if the software renderer gets delayed</li>
|
||||||
|
<li>Fixed crashes on certain desktop configurations when loading the UI</li>
|
||||||
|
<li>Fixed handling of pairing certificates with a bogus date</li>
|
||||||
|
</ul>
|
||||||
|
</description>
|
||||||
|
</release>
|
||||||
|
<release version="0.9.1" date="2019-02-17">
|
||||||
|
<description>
|
||||||
|
<p>New features:</p>
|
||||||
|
<ul>
|
||||||
|
<li>Added gamepad rumble support</li>
|
||||||
|
<li>Added a dedicated quit button to the running app tile</li>
|
||||||
|
<li>Reduced video latency on Windows and Linux</li>
|
||||||
|
<li>Added a performance overlay for software decoding</li>
|
||||||
|
</ul>
|
||||||
|
<p>Bugfixes:</p>
|
||||||
|
<ul>
|
||||||
|
<li>Fixed delayed first video frame with GFE 3.17</li>
|
||||||
|
<li>Fixed erroneously setting host resolution to 720p in certain configurations</li>
|
||||||
|
<li>The connection termination dialog is no longer displayed for intentional termination</li>
|
||||||
|
<li>Fixed some gamepads only functioning for UI navigation and not in game</li>
|
||||||
|
<li>Fixed audio latency increasing when switching audio devices</li>
|
||||||
|
<li>Fixed VDPAU scaling behavior on AMD cards</li>
|
||||||
|
<li>Fixed several bugs in gamepad navigation behavior</li>
|
||||||
|
<li>Work around a bug in QGnomePlatform that breaks dialog rendering on Linux</li>
|
||||||
|
<li>Updated included SDL gamepad mappings</li>
|
||||||
|
<li>Fixed hotkey behavior on non-latin keyboards</li>
|
||||||
|
<li>Fixed incorrect frame drop percentages in debug stats</li>
|
||||||
|
</ul>
|
||||||
|
</description>
|
||||||
|
</release>
|
||||||
|
<release version="0.8.1" date="2019-01-08">
|
||||||
|
<description>
|
||||||
|
<p>New features:</p>
|
||||||
|
<ul>
|
||||||
|
<li>Stricter adherence to the specified bitrate when streaming over the Internet</li>
|
||||||
|
</ul>
|
||||||
|
<p>Bugfixes:</p>
|
||||||
|
<ul>
|
||||||
|
<li>Fixed audio crackling that impacted certain systems</li>
|
||||||
|
<li>Fixed Wake-on-LAN packets failing to send on some systems</li>
|
||||||
|
<li>Reduced audio latency when resuming a session where audio is already playing</li>
|
||||||
|
<li>Added option to enable the old SDL audio renderer by setting ML_AUDIO=SDL</li>
|
||||||
|
<li>Updated to use the new Qt 5.12 Flatpak runtime</li>
|
||||||
|
<li>Updated to FFmpeg 4.1</li>
|
||||||
|
<li>Minor UI tweaks</li>
|
||||||
|
</ul>
|
||||||
|
</description>
|
||||||
|
</release>
|
||||||
<release version="0.8.0" date="2018-12-25">
|
<release version="0.8.0" date="2018-12-25">
|
||||||
<description>
|
<description>
|
||||||
<p>New features:</p>
|
<p>New features:</p>
|
||||||
|
|||||||
|
After Width: | Height: | Size: 2.9 KiB |
@@ -0,0 +1,9 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
# The default HOME is not persistent, so override
|
||||||
|
# it to a path on the onboard flash. Otherwise our
|
||||||
|
# pairing data will be lost each reboot.
|
||||||
|
HOME=/usr/local/moonlight
|
||||||
|
|
||||||
|
# Write output to a logfile in /tmp
|
||||||
|
exec ./bin/moonlight > /tmp/moonlight.log
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
name=Moonlight
|
||||||
|
icon=moonlight.png
|
||||||
|
run=moonlight.sh
|
||||||
@@ -1,29 +1,20 @@
|
|||||||
import QtQuick 2.9
|
import QtQuick 2.9
|
||||||
import QtQuick.Dialogs 1.2
|
|
||||||
import QtQuick.Controls 2.2
|
import QtQuick.Controls 2.2
|
||||||
|
|
||||||
import AppModel 1.0
|
import AppModel 1.0
|
||||||
import ComputerManager 1.0
|
import ComputerManager 1.0
|
||||||
import SdlGamepadKeyNavigation 1.0
|
|
||||||
|
|
||||||
GridView {
|
CenteredGridView {
|
||||||
property int computerIndex
|
property int computerIndex
|
||||||
property AppModel appModel : createModel()
|
property AppModel appModel : createModel()
|
||||||
|
property bool activated
|
||||||
|
|
||||||
id: appGrid
|
id: appGrid
|
||||||
focus: true
|
focus: true
|
||||||
activeFocusOnTab: true
|
activeFocusOnTab: true
|
||||||
anchors.fill: parent
|
topMargin: 20
|
||||||
anchors.leftMargin: (parent.width % (cellWidth + anchors.rightMargin)) / 2
|
bottomMargin: 5
|
||||||
anchors.topMargin: 20
|
cellWidth: 230; cellHeight: 297;
|
||||||
anchors.rightMargin: 5
|
|
||||||
anchors.bottomMargin: 5
|
|
||||||
cellWidth: 225; cellHeight: 385;
|
|
||||||
|
|
||||||
// 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 computerLost()
|
function computerLost()
|
||||||
{
|
{
|
||||||
@@ -32,23 +23,20 @@ GridView {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Component.onCompleted: {
|
Component.onCompleted: {
|
||||||
// Don't show any highlighted item until interacting with them
|
// Don't show any highlighted item until interacting with them.
|
||||||
|
// We do this here instead of onActivated to avoid losing the user's
|
||||||
|
// selection when backing out of a different page of the app.
|
||||||
currentIndex = -1
|
currentIndex = -1
|
||||||
}
|
}
|
||||||
|
|
||||||
SdlGamepadKeyNavigation {
|
StackView.onActivated: {
|
||||||
id: gamepadKeyNav
|
appModel.computerLost.connect(computerLost)
|
||||||
|
activated = true
|
||||||
}
|
}
|
||||||
|
|
||||||
onVisibleChanged: {
|
StackView.onDeactivating: {
|
||||||
if (visible) {
|
appModel.computerLost.disconnect(computerLost)
|
||||||
appModel.computerLost.connect(computerLost)
|
activated = false
|
||||||
gamepadKeyNav.enable()
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
appModel.computerLost.disconnect(computerLost)
|
|
||||||
gamepadKeyNav.disable()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function createModel()
|
function createModel()
|
||||||
@@ -61,50 +49,110 @@ GridView {
|
|||||||
model: appModel
|
model: appModel
|
||||||
|
|
||||||
delegate: NavigableItemDelegate {
|
delegate: NavigableItemDelegate {
|
||||||
width: 200; height: 335;
|
width: 220; height: 287;
|
||||||
grid: appGrid
|
grid: appGrid
|
||||||
|
|
||||||
Image {
|
Image {
|
||||||
|
property bool isPlaceholder: false
|
||||||
|
|
||||||
id: appIcon
|
id: appIcon
|
||||||
anchors.horizontalCenter: parent.horizontalCenter;
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
y: 20
|
y: 10
|
||||||
source: model.boxart
|
source: model.boxart
|
||||||
sourceSize {
|
|
||||||
width: 150
|
|
||||||
height: 200
|
|
||||||
}
|
|
||||||
width: sourceSize.width
|
|
||||||
height: sourceSize.height
|
|
||||||
fillMode: Image.Pad
|
|
||||||
}
|
|
||||||
|
|
||||||
Image {
|
onSourceSizeChanged: {
|
||||||
id: runningIcon
|
if ((width == 130 && height == 180) || // GFE 2.0 placeholder image
|
||||||
anchors.centerIn: appIcon
|
(width == 628 && height == 888) || // GFE 3.0 placeholder image
|
||||||
visible: model.running
|
(width == 200 && height == 266)) // Our no_app_image.png
|
||||||
source: "qrc:/res/baseline-play_circle_filled_white-48px.svg"
|
{
|
||||||
sourceSize {
|
isPlaceholder = true
|
||||||
width: 75
|
}
|
||||||
height: 75
|
else
|
||||||
}
|
{
|
||||||
}
|
isPlaceholder = false
|
||||||
|
}
|
||||||
|
|
||||||
Label {
|
width = 200
|
||||||
id: appNameText
|
height = 267
|
||||||
text: model.name
|
}
|
||||||
width: parent.width
|
|
||||||
height: 125
|
|
||||||
anchors.top: appIcon.bottom
|
|
||||||
font.pointSize: 22
|
|
||||||
horizontalAlignment: Text.AlignHCenter
|
|
||||||
wrapMode: Text.Wrap
|
|
||||||
elide: Text.ElideRight
|
|
||||||
|
|
||||||
// Display a tooltip with the full name if it's truncated
|
// Display a tooltip with the full name if it's truncated
|
||||||
ToolTip.text: model.name
|
ToolTip.text: model.name
|
||||||
ToolTip.delay: 1000
|
ToolTip.delay: 1000
|
||||||
ToolTip.timeout: 5000
|
ToolTip.timeout: 5000
|
||||||
ToolTip.visible: (parent.hovered || parent.highlighted) && truncated
|
ToolTip.visible: (parent.hovered || parent.highlighted) && (!appNameText.visible || appNameText.truncated)
|
||||||
|
}
|
||||||
|
|
||||||
|
ToolButton {
|
||||||
|
id: resumeButton
|
||||||
|
anchors.horizontalCenterOffset: appIcon.isPlaceholder ? -47 : 0
|
||||||
|
anchors.verticalCenterOffset: appIcon.isPlaceholder ? -75 : -60
|
||||||
|
anchors.centerIn: appIcon
|
||||||
|
visible: model.running
|
||||||
|
implicitWidth: 125
|
||||||
|
implicitHeight: 125
|
||||||
|
|
||||||
|
Image {
|
||||||
|
source: "qrc:/res/baseline-play_circle_filled_white-48px.svg"
|
||||||
|
anchors.centerIn: parent
|
||||||
|
sourceSize {
|
||||||
|
width: 75
|
||||||
|
height: 75
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onClicked: {
|
||||||
|
launchOrResumeSelectedApp()
|
||||||
|
}
|
||||||
|
|
||||||
|
ToolTip.text: "Resume Game"
|
||||||
|
ToolTip.delay: 1000
|
||||||
|
ToolTip.timeout: 3000
|
||||||
|
ToolTip.visible: hovered
|
||||||
|
}
|
||||||
|
|
||||||
|
ToolButton {
|
||||||
|
id: quitButton
|
||||||
|
anchors.horizontalCenterOffset: appIcon.isPlaceholder ? 47 : 0
|
||||||
|
anchors.verticalCenterOffset: appIcon.isPlaceholder ? -75 : 60
|
||||||
|
anchors.centerIn: appIcon
|
||||||
|
visible: model.running
|
||||||
|
implicitWidth: 125
|
||||||
|
implicitHeight: 125
|
||||||
|
|
||||||
|
Image {
|
||||||
|
source: "qrc:/res/baseline-cancel-24px.svg"
|
||||||
|
anchors.centerIn: parent
|
||||||
|
sourceSize {
|
||||||
|
width: 75
|
||||||
|
height: 75
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onClicked: {
|
||||||
|
doQuitGame()
|
||||||
|
}
|
||||||
|
|
||||||
|
ToolTip.text: "Quit Game"
|
||||||
|
ToolTip.delay: 1000
|
||||||
|
ToolTip.timeout: 3000
|
||||||
|
ToolTip.visible: hovered
|
||||||
|
}
|
||||||
|
|
||||||
|
Label {
|
||||||
|
id: appNameText
|
||||||
|
visible: appIcon.isPlaceholder
|
||||||
|
text: model.name
|
||||||
|
width: appIcon.width
|
||||||
|
height: model.running ? 175 : appIcon.height
|
||||||
|
anchors.left: appIcon.left
|
||||||
|
anchors.right: appIcon.right
|
||||||
|
anchors.bottom: appIcon.bottom
|
||||||
|
font.pointSize: 22
|
||||||
|
verticalAlignment: Text.AlignVCenter
|
||||||
|
horizontalAlignment: Text.AlignHCenter
|
||||||
|
wrapMode: Text.Wrap
|
||||||
|
elide: Text.ElideRight
|
||||||
}
|
}
|
||||||
|
|
||||||
function launchOrResumeSelectedApp()
|
function launchOrResumeSelectedApp()
|
||||||
@@ -125,55 +173,64 @@ GridView {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onClicked: {
|
onClicked: {
|
||||||
// Nothing is running or this app is running
|
// Only allow clicking on the box art for non-running games.
|
||||||
launchOrResumeSelectedApp()
|
// For running games, buttons will appear to resume or quit which
|
||||||
|
// will handle starting the game and clicks on the box art will
|
||||||
|
// be ignored.
|
||||||
|
if (!model.running) {
|
||||||
|
launchOrResumeSelectedApp()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
MouseArea {
|
Keys.onReturnPressed: {
|
||||||
anchors.fill: parent
|
// Open the app context menu if activated via the gamepad or keyboard
|
||||||
acceptedButtons: Qt.RightButton
|
// for running games. If the game isn't running, the above onClicked
|
||||||
onClicked: {
|
// method will handle the launch.
|
||||||
// popup() ensures the menu appears under the mouse cursor
|
if (model.running) {
|
||||||
appContextMenu.popup()
|
// This will be keyboard/gamepad driven so use
|
||||||
|
// open() instead of popup()
|
||||||
|
appContextMenu.open()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Keys.onMenuPressed: {
|
Keys.onMenuPressed: {
|
||||||
// We must use open() here so the menu is positioned on
|
if (model.running) {
|
||||||
// the ItemDelegate and not where the mouse cursor is
|
// This will be keyboard/gamepad driven so use
|
||||||
appContextMenu.open()
|
// open() instead of popup()
|
||||||
|
appContextMenu.open()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Menu {
|
function doQuitGame() {
|
||||||
|
quitAppDialog.appName = appModel.getRunningAppName()
|
||||||
|
quitAppDialog.segueToStream = false
|
||||||
|
quitAppDialog.open()
|
||||||
|
}
|
||||||
|
|
||||||
|
NavigableMenu {
|
||||||
id: appContextMenu
|
id: appContextMenu
|
||||||
NavigableMenuItem {
|
NavigableMenuItem {
|
||||||
|
parentMenu: appContextMenu
|
||||||
text: model.running ? "Resume Game" : "Launch Game"
|
text: model.running ? "Resume Game" : "Launch Game"
|
||||||
onTriggered: {
|
onTriggered: launchOrResumeSelectedApp()
|
||||||
appContextMenu.close()
|
|
||||||
launchOrResumeSelectedApp()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
NavigableMenuItem {
|
NavigableMenuItem {
|
||||||
|
parentMenu: appContextMenu
|
||||||
text: "Quit Game"
|
text: "Quit Game"
|
||||||
onTriggered: {
|
onTriggered: doQuitGame()
|
||||||
quitAppDialog.appName = appModel.getRunningAppName()
|
|
||||||
quitAppDialog.segueToStream = false
|
|
||||||
quitAppDialog.open()
|
|
||||||
}
|
|
||||||
visible: model.running
|
visible: model.running
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
MessageDialog {
|
NavigableMessageDialog {
|
||||||
id: quitAppDialog
|
id: quitAppDialog
|
||||||
modality:Qt.WindowModal
|
|
||||||
property string appName : ""
|
property string appName : ""
|
||||||
property bool segueToStream : false
|
property bool segueToStream : false
|
||||||
property string nextAppName: ""
|
property string nextAppName: ""
|
||||||
property int nextAppIndex: 0
|
property int nextAppIndex: 0
|
||||||
text:"Are you sure you want to quit " + appName +"? Any unsaved progress will be lost."
|
text:"Are you sure you want to quit " + appName +"? Any unsaved progress will be lost."
|
||||||
standardButtons: StandardButton.Yes | StandardButton.No
|
standardButtons: Dialog.Yes | Dialog.No
|
||||||
|
|
||||||
function quitApp() {
|
function quitApp() {
|
||||||
var component = Qt.createComponent("QuitSegue.qml")
|
var component = Qt.createComponent("QuitSegue.qml")
|
||||||
@@ -195,20 +252,21 @@ GridView {
|
|||||||
appModel.quitRunningApp()
|
appModel.quitRunningApp()
|
||||||
}
|
}
|
||||||
|
|
||||||
onYes: quitApp()
|
|
||||||
|
|
||||||
// For keyboard/gamepad navigation
|
|
||||||
onAccepted: quitApp()
|
onAccepted: quitApp()
|
||||||
}
|
}
|
||||||
|
|
||||||
ScrollBar.vertical: ScrollBar {
|
ScrollBar.vertical: ScrollBar {
|
||||||
|
// Manually hide the scrollbar to prevent it from being drawn on top
|
||||||
|
// of the StreamSegue during the transition. It can sometimes get stuck
|
||||||
|
// there since we're not consistently pumping the event loop while
|
||||||
|
// starting the stream.
|
||||||
|
visible: activated
|
||||||
|
|
||||||
parent: appGrid.parent
|
parent: appGrid.parent
|
||||||
anchors {
|
anchors {
|
||||||
top: appGrid.top
|
top: parent.top
|
||||||
left: appGrid.right
|
right: parent.right
|
||||||
bottom: appGrid.bottom
|
bottom: parent.bottom
|
||||||
|
|
||||||
leftMargin: -10
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
import QtQuick 2.9
|
||||||
|
import QtQuick.Controls 2.2
|
||||||
|
|
||||||
|
GridView {
|
||||||
|
// Detect Qt 5.11 or earlier using presence of synchronousDrag.
|
||||||
|
// Prior to 5.12, the leftMargin and rightMargin values did not work.
|
||||||
|
property bool hasBrokenMargins: this.synchronousDrag === undefined
|
||||||
|
|
||||||
|
property int minMargin: 10
|
||||||
|
property real availableWidth: (parent.width - 2 * minMargin)
|
||||||
|
property int itemsPerRow: availableWidth / cellWidth
|
||||||
|
property real horizontalMargin: itemsPerRow < count && availableWidth >= cellWidth ?
|
||||||
|
(availableWidth % cellWidth) / 2 : minMargin
|
||||||
|
|
||||||
|
function updateMargins() {
|
||||||
|
leftMargin = horizontalMargin
|
||||||
|
rightMargin = horizontalMargin
|
||||||
|
|
||||||
|
if (hasBrokenMargins) {
|
||||||
|
anchors.leftMargin = leftMargin
|
||||||
|
anchors.rightMargin = rightMargin
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onHorizontalMarginChanged: {
|
||||||
|
updateMargins()
|
||||||
|
}
|
||||||
|
|
||||||
|
Component.onCompleted: {
|
||||||
|
if (hasBrokenMargins) {
|
||||||
|
// This will cause an anchor conflict with the parent StackView
|
||||||
|
// which breaks animation, but otherwise the grid will not be
|
||||||
|
// centered in the window.
|
||||||
|
anchors.fill = parent
|
||||||
|
anchors.topMargin = topMargin
|
||||||
|
anchors.bottomMargin = bottomMargin
|
||||||
|
}
|
||||||
|
|
||||||
|
updateMargins()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,13 +1,10 @@
|
|||||||
import QtQuick 2.0
|
import QtQuick 2.0
|
||||||
import QtQuick.Controls 2.2
|
import QtQuick.Controls 2.2
|
||||||
import QtQuick.Dialogs 1.2
|
|
||||||
|
|
||||||
import ComputerManager 1.0
|
import ComputerManager 1.0
|
||||||
import Session 1.0
|
import Session 1.0
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
anchors.fill: parent
|
|
||||||
|
|
||||||
function onSearchingComputer() {
|
function onSearchingComputer() {
|
||||||
stageLabel.text = "Establishing connection to PC..."
|
stageLabel.text = "Establishing connection to PC..."
|
||||||
}
|
}
|
||||||
@@ -21,13 +18,8 @@ Item {
|
|||||||
errorDialog.open()
|
errorDialog.open()
|
||||||
}
|
}
|
||||||
|
|
||||||
// The StackView will trigger a visibility change when
|
StackView.onActivated: {
|
||||||
// we're pushed onto it, causing our onVisibleChanged
|
if (!launcher.isExecuted()) {
|
||||||
// routine to run, but only if we start as invisible
|
|
||||||
visible: false
|
|
||||||
|
|
||||||
onVisibleChanged: {
|
|
||||||
if (visible && !launcher.isExecuted()) {
|
|
||||||
toolBar.visible = false
|
toolBar.visible = false
|
||||||
launcher.searchingComputer.connect(onSearchingComputer)
|
launcher.searchingComputer.connect(onSearchingComputer)
|
||||||
launcher.quittingApp.connect(onQuittingApp)
|
launcher.quittingApp.connect(onQuittingApp)
|
||||||
@@ -55,16 +47,11 @@ Item {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
MessageDialog {
|
ErrorMessageDialog {
|
||||||
id: errorDialog
|
id: errorDialog
|
||||||
modality:Qt.WindowModal
|
|
||||||
icon: StandardIcon.Critical
|
|
||||||
standardButtons: StandardButton.Ok
|
|
||||||
|
|
||||||
onVisibleChanged: {
|
onClosed: {
|
||||||
if (!visible) {
|
Qt.quit()
|
||||||
Qt.quit()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,13 +1,9 @@
|
|||||||
import QtQml 2.2
|
|
||||||
import QtQuick 2.0
|
import QtQuick 2.0
|
||||||
import QtQuick.Controls 2.2
|
import QtQuick.Controls 2.2
|
||||||
import QtQuick.Dialogs 1.2
|
|
||||||
|
|
||||||
import ComputerManager 1.0
|
import ComputerManager 1.0
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
visible: false
|
|
||||||
|
|
||||||
function onSearchingComputer() {
|
function onSearchingComputer() {
|
||||||
stageLabel.text = "Establishing connection to PC..."
|
stageLabel.text = "Establishing connection to PC..."
|
||||||
}
|
}
|
||||||
@@ -36,8 +32,8 @@ Item {
|
|||||||
quitAppDialog.open()
|
quitAppDialog.open()
|
||||||
}
|
}
|
||||||
|
|
||||||
onVisibleChanged: {
|
StackView.onActivated: {
|
||||||
if (visible && !launcher.isExecuted()) {
|
if (!launcher.isExecuted()) {
|
||||||
toolBar.visible = false
|
toolBar.visible = false
|
||||||
launcher.searchingComputer.connect(onSearchingComputer)
|
launcher.searchingComputer.connect(onSearchingComputer)
|
||||||
launcher.searchingApp.connect(onSearchingApp)
|
launcher.searchingApp.connect(onSearchingApp)
|
||||||
@@ -66,26 +62,18 @@ Item {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
MessageDialog {
|
ErrorMessageDialog {
|
||||||
id: errorDialog
|
id: errorDialog
|
||||||
modality:Qt.WindowModal
|
|
||||||
icon: StandardIcon.Critical
|
onClosed: {
|
||||||
standardButtons: StandardButton.Ok | StandardButton.Help
|
Qt.quit();
|
||||||
onHelp: {
|
|
||||||
Qt.openUrlExternally("https://github.com/moonlight-stream/moonlight-docs/wiki/Troubleshooting");
|
|
||||||
}
|
|
||||||
onVisibleChanged: {
|
|
||||||
if (!visible) {
|
|
||||||
Qt.quit();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
MessageDialog {
|
NavigableMessageDialog {
|
||||||
id: quitAppDialog
|
id: quitAppDialog
|
||||||
modality:Qt.WindowModal
|
|
||||||
text:"Are you sure you want to quit " + appName +"? Any unsaved progress will be lost."
|
text:"Are you sure you want to quit " + appName +"? Any unsaved progress will be lost."
|
||||||
standardButtons: StandardButton.Yes | StandardButton.No
|
standardButtons: Dialog.Yes | Dialog.No
|
||||||
property string appName : ""
|
property string appName : ""
|
||||||
|
|
||||||
function quitApp() {
|
function quitApp() {
|
||||||
@@ -96,29 +84,7 @@ Item {
|
|||||||
launcher.quitRunningApp()
|
launcher.quitRunningApp()
|
||||||
}
|
}
|
||||||
|
|
||||||
onYes: quitApp()
|
|
||||||
|
|
||||||
// For keyboard/gamepad navigation
|
|
||||||
onAccepted: quitApp()
|
onAccepted: quitApp()
|
||||||
|
onRejected: Qt.quit()
|
||||||
// Exit process if app quit is rejected (reacts also to closing of the
|
|
||||||
// dialog from title bar's close button).
|
|
||||||
// Note: this depends on undocumented behavior of visibleChanged()
|
|
||||||
// signal being emitted before yes() or accepted() has been emitted.
|
|
||||||
onVisibleChanged: {
|
|
||||||
if (!visible) {
|
|
||||||
quitTimer.start()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Component.onCompleted: {
|
|
||||||
yes.connect(quitTimer.stop)
|
|
||||||
accepted.connect(quitTimer.stop)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Timer {
|
|
||||||
id: quitTimer
|
|
||||||
interval: 100
|
|
||||||
onTriggered: Qt.quit()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
import QtQuick 2.0
|
||||||
|
import QtQuick.Controls 2.2
|
||||||
|
|
||||||
|
import SystemProperties 1.0
|
||||||
|
|
||||||
|
NavigableMessageDialog {
|
||||||
|
standardButtons: Dialog.Ok | (SystemProperties.hasBrowser ? Dialog.Help : 0)
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import QtQuick 2.0
|
||||||
|
import QtQuick.Controls 2.2
|
||||||
|
|
||||||
|
Dialog {
|
||||||
|
parent: overlay
|
||||||
|
|
||||||
|
x: Math.round((parent.width - width) / 2)
|
||||||
|
y: Math.round((parent.height - height) / 2)
|
||||||
|
|
||||||
|
onAboutToHide: {
|
||||||
|
// We must force focus back to the last item for platforms without
|
||||||
|
// support for more than one active window like Steam Link. If
|
||||||
|
// we don't, gamepad and keyboard navigation will break after a
|
||||||
|
// dialog appears.
|
||||||
|
stackView.forceActiveFocus()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
import QtQuick 2.0
|
||||||
|
import QtQuick.Controls 2.2
|
||||||
|
|
||||||
|
Menu {
|
||||||
|
onOpened: {
|
||||||
|
itemAt(0).forceActiveFocus(Qt.TabFocusReason)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,11 +2,27 @@ import QtQuick 2.0
|
|||||||
import QtQuick.Controls 2.2
|
import QtQuick.Controls 2.2
|
||||||
|
|
||||||
MenuItem {
|
MenuItem {
|
||||||
|
// Qt 5.10 has a menu property, but we need to support 5.9
|
||||||
|
// so we must make our own.
|
||||||
|
property Menu parentMenu
|
||||||
|
|
||||||
// Ensure focus can't be given to an invisible item
|
// Ensure focus can't be given to an invisible item
|
||||||
enabled: visible
|
enabled: visible
|
||||||
height: visible ? implicitHeight : 0
|
height: visible ? implicitHeight : 0
|
||||||
|
focusPolicy: visible ? Qt.TabFocus : Qt.NoFocus
|
||||||
|
|
||||||
|
onTriggered: {
|
||||||
|
// We must close the context menu first or
|
||||||
|
// it can steal focus from any dialogs that
|
||||||
|
// onTriggered may spawn.
|
||||||
|
parentMenu.close()
|
||||||
|
}
|
||||||
|
|
||||||
Keys.onReturnPressed: {
|
Keys.onReturnPressed: {
|
||||||
triggered()
|
triggered()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Keys.onEscapePressed: {
|
||||||
|
parentMenu.close()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
import QtQuick 2.0
|
||||||
|
import QtQuick.Controls 2.2
|
||||||
|
import QtQuick.Layouts 1.2
|
||||||
|
|
||||||
|
NavigableDialog {
|
||||||
|
id: dialog
|
||||||
|
|
||||||
|
property alias text: dialogLabel.dialogText
|
||||||
|
|
||||||
|
property string helpText
|
||||||
|
property string helpUrl : "https://github.com/moonlight-stream/moonlight-docs/wiki/Troubleshooting"
|
||||||
|
property string helpTextSeparator : " "
|
||||||
|
|
||||||
|
onOpened: {
|
||||||
|
// Force keyboard focus on the label so keyboard navigation works
|
||||||
|
dialogLabel.forceActiveFocus()
|
||||||
|
}
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
spacing: 10
|
||||||
|
|
||||||
|
Image {
|
||||||
|
source: (standardButtons & Dialog.Yes) ?
|
||||||
|
"qrc:/res/baseline-help_outline-24px.svg" :
|
||||||
|
"qrc:/res/baseline-error_outline-24px.svg"
|
||||||
|
sourceSize {
|
||||||
|
// The icon should be square so use the height as the width too
|
||||||
|
width: 50
|
||||||
|
height: 50
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Label {
|
||||||
|
property string dialogText
|
||||||
|
|
||||||
|
id: dialogLabel
|
||||||
|
text: dialogText + ((helpText && (standardButtons & Dialog.Help)) ? (helpTextSeparator + helpText) : "")
|
||||||
|
wrapMode: Text.Wrap
|
||||||
|
elide: Label.ElideRight
|
||||||
|
|
||||||
|
// Cap the width so the dialog doesn't grow horizontally forever. This
|
||||||
|
// will cause word wrap to kick in.
|
||||||
|
Layout.maximumWidth: 400
|
||||||
|
Layout.maximumHeight: 400
|
||||||
|
|
||||||
|
Keys.onReturnPressed: {
|
||||||
|
accept()
|
||||||
|
}
|
||||||
|
|
||||||
|
Keys.onEscapePressed: {
|
||||||
|
reject()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
footer: DialogButtonBox {
|
||||||
|
id: dialogButtonBox
|
||||||
|
standardButtons: dialog.standardButtons
|
||||||
|
|
||||||
|
onHelpRequested: {
|
||||||
|
Qt.openUrlExternally(helpUrl)
|
||||||
|
close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,57 +1,36 @@
|
|||||||
import QtQuick 2.9
|
import QtQuick 2.9
|
||||||
import QtQuick.Controls 2.2
|
import QtQuick.Controls 2.2
|
||||||
import QtQuick.Dialogs 1.2
|
|
||||||
import QtQuick.Layouts 1.3
|
|
||||||
import QtQuick.Window 2.2
|
|
||||||
|
|
||||||
import ComputerModel 1.0
|
import ComputerModel 1.0
|
||||||
|
|
||||||
import ComputerManager 1.0
|
import ComputerManager 1.0
|
||||||
import StreamingPreferences 1.0
|
import StreamingPreferences 1.0
|
||||||
import SdlGamepadKeyNavigation 1.0
|
|
||||||
|
|
||||||
GridView {
|
CenteredGridView {
|
||||||
property ComputerModel computerModel : createModel()
|
property ComputerModel computerModel : createModel()
|
||||||
|
|
||||||
id: pcGrid
|
id: pcGrid
|
||||||
focus: true
|
focus: true
|
||||||
activeFocusOnTab: true
|
activeFocusOnTab: true
|
||||||
anchors.fill: parent
|
topMargin: 20
|
||||||
anchors.leftMargin: (parent.width % (cellWidth + anchors.rightMargin)) / 2
|
bottomMargin: 5
|
||||||
anchors.topMargin: 20
|
cellWidth: 310; cellHeight: 350;
|
||||||
anchors.rightMargin: 5
|
|
||||||
anchors.bottomMargin: 5
|
|
||||||
cellWidth: 350; cellHeight: 350;
|
|
||||||
objectName: "Computers"
|
objectName: "Computers"
|
||||||
|
|
||||||
// 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
|
|
||||||
|
|
||||||
StreamingPreferences {
|
|
||||||
id: prefs
|
|
||||||
}
|
|
||||||
|
|
||||||
SdlGamepadKeyNavigation {
|
|
||||||
id: gamepadKeyNav
|
|
||||||
}
|
|
||||||
|
|
||||||
onVisibleChanged: {
|
|
||||||
if (visible) {
|
|
||||||
gamepadKeyNav.enable()
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
gamepadKeyNav.disable()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Component.onCompleted: {
|
Component.onCompleted: {
|
||||||
|
// Don't show any highlighted item until interacting with them.
|
||||||
|
// We do this here instead of onActivated to avoid losing the user's
|
||||||
|
// selection when backing out of a different page of the app.
|
||||||
|
currentIndex = -1
|
||||||
|
}
|
||||||
|
|
||||||
|
StackView.onActivated: {
|
||||||
// Setup signals on CM
|
// Setup signals on CM
|
||||||
ComputerManager.computerAddCompleted.connect(addComplete)
|
ComputerManager.computerAddCompleted.connect(addComplete)
|
||||||
|
}
|
||||||
|
|
||||||
// Don't show any highlighted item until interacting with them
|
StackView.onDeactivating: {
|
||||||
currentIndex = -1
|
ComputerManager.computerAddCompleted.disconnect(addComplete)
|
||||||
}
|
}
|
||||||
|
|
||||||
function pairingComplete(error)
|
function pairingComplete(error)
|
||||||
@@ -62,6 +41,7 @@ GridView {
|
|||||||
// Display a failed dialog if we got an error
|
// Display a failed dialog if we got an error
|
||||||
if (error !== undefined) {
|
if (error !== undefined) {
|
||||||
errorDialog.text = error
|
errorDialog.text = error
|
||||||
|
errorDialog.helpText = ""
|
||||||
errorDialog.open()
|
errorDialog.open()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -69,7 +49,8 @@ GridView {
|
|||||||
function addComplete(success)
|
function addComplete(success)
|
||||||
{
|
{
|
||||||
if (!success) {
|
if (!success) {
|
||||||
errorDialog.text = "Unable to connect to the specified PC. Click the Help button for possible solutions."
|
errorDialog.text = "Unable to connect to the specified PC."
|
||||||
|
errorDialog.helpText = "Click the Help button for possible solutions."
|
||||||
errorDialog.open()
|
errorDialog.open()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -82,6 +63,27 @@ GridView {
|
|||||||
return model
|
return model
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Row {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
spacing: 5
|
||||||
|
visible: pcGrid.count === 0
|
||||||
|
|
||||||
|
BusyIndicator {
|
||||||
|
id: searchSpinner
|
||||||
|
visible: StreamingPreferences.enableMdns
|
||||||
|
}
|
||||||
|
|
||||||
|
Label {
|
||||||
|
height: searchSpinner.height
|
||||||
|
elide: Label.ElideRight
|
||||||
|
text: StreamingPreferences.enableMdns ? "Searching for PCs with NVIDIA GameStream enabled..."
|
||||||
|
: "Automatic PC discovery is disabled. Add your PC manually."
|
||||||
|
font.pointSize: 20
|
||||||
|
verticalAlignment: Text.AlignVCenter
|
||||||
|
wrapMode: Text.Wrap
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
model: computerModel
|
model: computerModel
|
||||||
|
|
||||||
delegate: NavigableItemDelegate {
|
delegate: NavigableItemDelegate {
|
||||||
@@ -91,9 +93,7 @@ GridView {
|
|||||||
Image {
|
Image {
|
||||||
id: pcIcon
|
id: pcIcon
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
source: {
|
source: "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"
|
|
||||||
}
|
|
||||||
sourceSize {
|
sourceSize {
|
||||||
width: 200
|
width: 200
|
||||||
height: 200
|
height: 200
|
||||||
@@ -106,7 +106,7 @@ GridView {
|
|||||||
anchors.horizontalCenter: pcIcon.horizontalCenter
|
anchors.horizontalCenter: pcIcon.horizontalCenter
|
||||||
anchors.verticalCenter: pcIcon.verticalCenter
|
anchors.verticalCenter: pcIcon.verticalCenter
|
||||||
anchors.verticalCenterOffset: -10
|
anchors.verticalCenterOffset: -10
|
||||||
visible: !model.addPc && !model.statusUnknown && (!model.online || !model.paired)
|
visible: !model.statusUnknown && (!model.online || !model.paired)
|
||||||
source: !model.online ? "qrc:/res/baseline-warning-24px.svg" : "qrc:/res/baseline-lock-24px.svg"
|
source: !model.online ? "qrc:/res/baseline-warning-24px.svg" : "qrc:/res/baseline-lock-24px.svg"
|
||||||
sourceSize {
|
sourceSize {
|
||||||
width: 75
|
width: 75
|
||||||
@@ -121,7 +121,7 @@ GridView {
|
|||||||
anchors.verticalCenterOffset: -10
|
anchors.verticalCenterOffset: -10
|
||||||
width: 75
|
width: 75
|
||||||
height: 75
|
height: 75
|
||||||
visible: !model.addPc && model.statusUnknown
|
visible: model.statusUnknown
|
||||||
}
|
}
|
||||||
|
|
||||||
Label {
|
Label {
|
||||||
@@ -135,14 +135,10 @@ GridView {
|
|||||||
wrapMode: Text.Wrap
|
wrapMode: Text.Wrap
|
||||||
}
|
}
|
||||||
|
|
||||||
Menu {
|
NavigableMenu {
|
||||||
id: pcContextMenu
|
id: pcContextMenu
|
||||||
NavigableMenuItem {
|
NavigableMenuItem {
|
||||||
text: "Wake PC"
|
parentMenu: pcContextMenu
|
||||||
onTriggered: computerModel.wakeComputer(index)
|
|
||||||
visible: !model.addPc && !model.online && model.wakeable
|
|
||||||
}
|
|
||||||
NavigableMenuItem {
|
|
||||||
text: "Delete PC"
|
text: "Delete PC"
|
||||||
onTriggered: {
|
onTriggered: {
|
||||||
deletePcDialog.pcIndex = index
|
deletePcDialog.pcIndex = index
|
||||||
@@ -150,13 +146,16 @@ GridView {
|
|||||||
deletePcDialog.open()
|
deletePcDialog.open()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
NavigableMenuItem {
|
||||||
|
parentMenu: pcContextMenu
|
||||||
|
text: "Wake PC"
|
||||||
|
onTriggered: computerModel.wakeComputer(index)
|
||||||
|
visible: !model.online && model.wakeable
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onClicked: {
|
onClicked: {
|
||||||
if (model.addPc) {
|
if (model.online) {
|
||||||
addPcDialog.open()
|
|
||||||
}
|
|
||||||
else if (model.online) {
|
|
||||||
if (model.paired) {
|
if (model.paired) {
|
||||||
// go to game view
|
// go to game view
|
||||||
var component = Qt.createComponent("AppView.qml")
|
var component = Qt.createComponent("AppView.qml")
|
||||||
@@ -176,7 +175,8 @@ GridView {
|
|||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
// cannot pair while something is streaming or attempting to pair
|
// 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.text = "You cannot pair while a previous session is still running on the host PC. Quit any running games or reboot the host PC, then try pairing again."
|
||||||
|
errorDialog.helpText = ""
|
||||||
errorDialog.open()
|
errorDialog.open()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -186,111 +186,72 @@ GridView {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onPressAndHold: {
|
||||||
|
// popup() ensures the menu appears under the mouse cursor
|
||||||
|
if (pcContextMenu.popup) {
|
||||||
|
pcContextMenu.popup()
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Qt 5.9 doesn't have popup()
|
||||||
|
pcContextMenu.open()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
MouseArea {
|
MouseArea {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
acceptedButtons: Qt.RightButton;
|
acceptedButtons: Qt.RightButton;
|
||||||
onClicked: {
|
onClicked: {
|
||||||
if (!model.addPc) {
|
parent.onPressAndHold()
|
||||||
// popup() ensures the menu appears under the mouse cursor
|
|
||||||
pcContextMenu.popup()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Keys.onMenuPressed: {
|
Keys.onMenuPressed: {
|
||||||
if (!model.addPc) {
|
// We must use open() here so the menu is positioned on
|
||||||
// We must use open() here so the menu is positioned on
|
// the ItemDelegate and not where the mouse cursor is
|
||||||
// the ItemDelegate and not where the mouse cursor is
|
pcContextMenu.open()
|
||||||
pcContextMenu.open()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
MessageDialog {
|
ErrorMessageDialog {
|
||||||
id: errorDialog
|
id: errorDialog
|
||||||
// don't allow edits to the rest of the window while open
|
|
||||||
modality:Qt.WindowModal
|
// Using Setup-Guide here instead of Troubleshooting because it's likely that users
|
||||||
icon: StandardIcon.Critical
|
// will arrive here by forgetting to enable GameStream or not forwarding ports.
|
||||||
standardButtons: StandardButton.Ok | StandardButton.Help
|
helpUrl: "https://github.com/moonlight-stream/moonlight-docs/wiki/Setup-Guide"
|
||||||
onHelp: {
|
|
||||||
// Using Setup-Guide here instead of Troubleshooting because it's likely that users
|
|
||||||
// will arrive here by forgetting to enable GameStream or not forwarding ports.
|
|
||||||
Qt.openUrlExternally("https://github.com/moonlight-stream/moonlight-docs/wiki/Setup-Guide");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
MessageDialog {
|
NavigableMessageDialog {
|
||||||
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
|
||||||
modality:Qt.WindowModal
|
|
||||||
property string pin : "0000"
|
property string pin : "0000"
|
||||||
text:"Please enter " + pin + " on your GameStream PC. This dialog will close when pairing is completed."
|
text:"Please enter " + pin + " on your GameStream PC. This dialog will close when pairing is completed."
|
||||||
standardButtons: StandardButton.Cancel
|
standardButtons: Dialog.Cancel
|
||||||
onRejected: {
|
onRejected: {
|
||||||
// FIXME: We should interrupt pairing here
|
// FIXME: We should interrupt pairing here
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
MessageDialog {
|
NavigableMessageDialog {
|
||||||
id: deletePcDialog
|
id: deletePcDialog
|
||||||
// don't allow edits to the rest of the window while open
|
// don't allow edits to the rest of the window while open
|
||||||
modality:Qt.WindowModal
|
|
||||||
property int pcIndex : -1;
|
property int pcIndex : -1;
|
||||||
text:"Are you sure you want to remove this PC?"
|
text:"Are you sure you want to remove this PC?"
|
||||||
standardButtons: StandardButton.Yes | StandardButton.No
|
standardButtons: Dialog.Yes | Dialog.No
|
||||||
|
|
||||||
function deletePc() {
|
function deletePc() {
|
||||||
console.log("deleting PC pairing for PC at index: " + pcIndex)
|
console.log("deleting PC pairing for PC at index: " + pcIndex)
|
||||||
computerModel.deleteComputer(pcIndex);
|
computerModel.deleteComputer(pcIndex);
|
||||||
}
|
}
|
||||||
|
|
||||||
onYes: deletePc()
|
|
||||||
|
|
||||||
// For keyboard/gamepad activation
|
|
||||||
onAccepted: deletePc()
|
onAccepted: deletePc()
|
||||||
}
|
}
|
||||||
|
|
||||||
Dialog {
|
|
||||||
id: addPcDialog
|
|
||||||
property string label: "Enter the IP address of your GameStream PC"
|
|
||||||
property string hint: "192.168.1.100"
|
|
||||||
property alias editText : editTextItem
|
|
||||||
|
|
||||||
standardButtons: StandardButton.Ok | StandardButton.Cancel
|
|
||||||
onVisibleChanged: {
|
|
||||||
editTextItem.focus = true
|
|
||||||
editTextItem.selectAll()
|
|
||||||
}
|
|
||||||
onAccepted: {
|
|
||||||
ComputerManager.addNewHost(editText.text, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
ColumnLayout {
|
|
||||||
Text {
|
|
||||||
id: addPcDialogLabel
|
|
||||||
text: addPcDialog.label
|
|
||||||
}
|
|
||||||
Rectangle {
|
|
||||||
implicitWidth: parent.parent.width
|
|
||||||
height: editTextItem.height
|
|
||||||
|
|
||||||
TextInput {
|
|
||||||
id: editTextItem
|
|
||||||
inputMethodHints: Qt.ImhPreferUppercase
|
|
||||||
text: addPcDialog.hint
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ScrollBar.vertical: ScrollBar {
|
ScrollBar.vertical: ScrollBar {
|
||||||
parent: pcGrid.parent
|
parent: pcGrid.parent
|
||||||
anchors {
|
anchors {
|
||||||
top: pcGrid.top
|
top: parent.top
|
||||||
left: pcGrid.right
|
right: parent.right
|
||||||
bottom: pcGrid.bottom
|
bottom: parent.bottom
|
||||||
|
|
||||||
leftMargin: -10
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
import QtQuick 2.0
|
import QtQuick 2.0
|
||||||
import QtQuick.Controls 2.2
|
import QtQuick.Controls 2.2
|
||||||
import QtQuick.Dialogs 1.2
|
|
||||||
|
|
||||||
import ComputerManager 1.0
|
import ComputerManager 1.0
|
||||||
import Session 1.0
|
import Session 1.0
|
||||||
@@ -12,13 +11,6 @@ Item {
|
|||||||
|
|
||||||
property string stageText : "Quitting " + appName + "..."
|
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)
|
function quitAppCompleted(error)
|
||||||
{
|
{
|
||||||
// Display a failed dialog if we got an error
|
// Display a failed dialog if we got an error
|
||||||
@@ -27,26 +19,32 @@ Item {
|
|||||||
errorDialog.open()
|
errorDialog.open()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Exit this view
|
|
||||||
stackView.pop()
|
|
||||||
|
|
||||||
// If we're supposed to launch another game after this, do so now
|
// If we're supposed to launch another game after this, do so now
|
||||||
if (error === undefined && nextSession !== null) {
|
if (error === undefined && nextSession !== null) {
|
||||||
var component = Qt.createComponent("StreamSegue.qml")
|
var component = Qt.createComponent("StreamSegue.qml")
|
||||||
var segue = component.createObject(stackView, {"appName": nextAppName, "session": nextSession})
|
var segue = component.createObject(stackView, {"appName": nextAppName, "session": nextSession})
|
||||||
stackView.push(segue)
|
stackView.replace(segue)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Exit this view
|
||||||
|
stackView.pop()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onVisibleChanged: {
|
StackView.onActivated: {
|
||||||
if (visible) {
|
// Hide the toolbar before we start loading
|
||||||
// Connect the quit completion signal
|
toolBar.visible = false
|
||||||
ComputerManager.quitAppCompleted.connect(quitAppCompleted)
|
|
||||||
}
|
// Connect the quit completion signal
|
||||||
else {
|
ComputerManager.quitAppCompleted.connect(quitAppCompleted)
|
||||||
// Disconnect the signal
|
}
|
||||||
ComputerManager.quitAppCompleted.disconnect(quitAppCompleted)
|
|
||||||
}
|
StackView.onDeactivating: {
|
||||||
|
// Show the toolbar again
|
||||||
|
toolBar.visible = true
|
||||||
|
|
||||||
|
// Disconnect the signal
|
||||||
|
ComputerManager.quitAppCompleted.disconnect(quitAppCompleted)
|
||||||
}
|
}
|
||||||
|
|
||||||
Row {
|
Row {
|
||||||
@@ -68,10 +66,7 @@ Item {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
MessageDialog {
|
ErrorMessageDialog {
|
||||||
id: errorDialog
|
id: errorDialog
|
||||||
modality:Qt.WindowModal
|
|
||||||
icon: StandardIcon.Critical
|
|
||||||
standardButtons: StandardButton.Ok
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -4,11 +4,11 @@ import QtQuick.Controls 2.2
|
|||||||
import StreamingPreferences 1.0
|
import StreamingPreferences 1.0
|
||||||
import ComputerManager 1.0
|
import ComputerManager 1.0
|
||||||
import SdlGamepadKeyNavigation 1.0
|
import SdlGamepadKeyNavigation 1.0
|
||||||
|
import SystemProperties 1.0
|
||||||
|
|
||||||
Flickable {
|
Flickable {
|
||||||
id: settingsPage
|
id: settingsPage
|
||||||
objectName: "Settings"
|
objectName: "Settings"
|
||||||
anchors.fill: parent
|
|
||||||
|
|
||||||
contentWidth: settingsColumn1.width > settingsColumn2.width ? settingsColumn1.width : settingsColumn2.width
|
contentWidth: settingsColumn1.width > settingsColumn2.width ? settingsColumn1.width : settingsColumn2.width
|
||||||
contentHeight: settingsColumn1.height > settingsColumn2.height ? settingsColumn1.height : settingsColumn2.height
|
contentHeight: settingsColumn1.height > settingsColumn2.height ? settingsColumn1.height : settingsColumn2.height
|
||||||
@@ -24,31 +24,15 @@ Flickable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
StreamingPreferences {
|
StackView.onActivated: {
|
||||||
id: prefs
|
SdlGamepadKeyNavigation.setSettingsMode(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
// The StackView will trigger a visibility change when
|
StackView.onDeactivating: {
|
||||||
// we're pushed onto it, causing our onVisibleChanged
|
SdlGamepadKeyNavigation.setSettingsMode(false)
|
||||||
// routine to run, but only if we start as invisible
|
|
||||||
visible: false
|
|
||||||
|
|
||||||
SdlGamepadKeyNavigation {
|
// Save the prefs so the Session can observe the changes
|
||||||
id: gamepadKeyNav
|
StreamingPreferences.save()
|
||||||
}
|
|
||||||
|
|
||||||
onVisibleChanged: {
|
|
||||||
if (visible) {
|
|
||||||
gamepadKeyNav.setSettingsMode(true)
|
|
||||||
gamepadKeyNav.enable()
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
gamepadKeyNav.disable()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Component.onDestruction: {
|
|
||||||
prefs.save()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Column {
|
Column {
|
||||||
@@ -98,10 +82,10 @@ Flickable {
|
|||||||
// Some platforms have different desktop resolutions
|
// Some platforms have different desktop resolutions
|
||||||
// and native resolutions (like macOS with Retina displays)
|
// and native resolutions (like macOS with Retina displays)
|
||||||
if (displayResIndex == 0) {
|
if (displayResIndex == 0) {
|
||||||
screenRect = prefs.getDesktopResolution(displayIndex)
|
screenRect = SystemProperties.getDesktopResolution(displayIndex)
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
screenRect = prefs.getNativeResolution(displayIndex)
|
screenRect = SystemProperties.getNativeResolution(displayIndex)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (screenRect.width === 0) {
|
if (screenRect.width === 0) {
|
||||||
@@ -140,8 +124,8 @@ Flickable {
|
|||||||
|
|
||||||
// load the saved width/height, and iterate through the ComboBox until a match is found
|
// load the saved width/height, and iterate through the ComboBox until a match is found
|
||||||
// and set it to that index.
|
// and set it to that index.
|
||||||
var saved_width = prefs.width
|
var saved_width = StreamingPreferences.width
|
||||||
var saved_height = prefs.height
|
var saved_height = StreamingPreferences.height
|
||||||
currentIndex = 0
|
currentIndex = 0
|
||||||
for (var i = 0; i < resolutionListModel.count; i++) {
|
for (var i = 0; i < resolutionListModel.count; i++) {
|
||||||
var el_width = parseInt(resolutionListModel.get(i).video_width);
|
var el_width = parseInt(resolutionListModel.get(i).video_width);
|
||||||
@@ -190,12 +174,14 @@ Flickable {
|
|||||||
var selectedHeight = parseInt(resolutionListModel.get(currentIndex).video_height)
|
var selectedHeight = parseInt(resolutionListModel.get(currentIndex).video_height)
|
||||||
|
|
||||||
// Only modify the bitrate if the values actually changed
|
// Only modify the bitrate if the values actually changed
|
||||||
if (prefs.width !== selectedWidth || prefs.height !== selectedHeight) {
|
if (StreamingPreferences.width !== selectedWidth || StreamingPreferences.height !== selectedHeight) {
|
||||||
prefs.width = selectedWidth
|
StreamingPreferences.width = selectedWidth
|
||||||
prefs.height = selectedHeight
|
StreamingPreferences.height = selectedHeight
|
||||||
|
|
||||||
prefs.bitrateKbps = prefs.getDefaultBitrate(prefs.width, prefs.height, prefs.fps);
|
StreamingPreferences.bitrateKbps = StreamingPreferences.getDefaultBitrate(StreamingPreferences.width,
|
||||||
slider.value = prefs.bitrateKbps
|
StreamingPreferences.height,
|
||||||
|
StreamingPreferences.fps);
|
||||||
|
slider.value = StreamingPreferences.bitrateKbps
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -204,15 +190,14 @@ Flickable {
|
|||||||
function createModel() {
|
function createModel() {
|
||||||
var fpsListModel = Qt.createQmlObject('import QtQuick 2.0; ListModel {}', parent, '')
|
var fpsListModel = Qt.createQmlObject('import QtQuick 2.0; ListModel {}', parent, '')
|
||||||
|
|
||||||
// Get the max supported FPS on this system
|
var max_fps = SystemProperties.maximumStreamingFrameRate
|
||||||
var max_fps = prefs.getMaximumStreamingFrameRate();
|
|
||||||
|
|
||||||
// Default entries
|
// Default entries
|
||||||
fpsListModel.append({"text": "30 FPS", "video_fps": "30"})
|
fpsListModel.append({"text": "30 FPS", "video_fps": "30"})
|
||||||
fpsListModel.append({"text": "60 FPS", "video_fps": "60"})
|
fpsListModel.append({"text": "60 FPS", "video_fps": "60"})
|
||||||
|
|
||||||
// Add unsupported FPS values that come before the display max FPS
|
// Add unsupported FPS values that come before the display max FPS
|
||||||
if (prefs.unsupportedFps) {
|
if (StreamingPreferences.unsupportedFps) {
|
||||||
if (max_fps > 90) {
|
if (max_fps > 90) {
|
||||||
fpsListModel.append({"text": "90 FPS (Unsupported)", "video_fps": "90"})
|
fpsListModel.append({"text": "90 FPS (Unsupported)", "video_fps": "90"})
|
||||||
}
|
}
|
||||||
@@ -225,7 +210,7 @@ Flickable {
|
|||||||
// handle wonky displays that report just over 60 Hz.
|
// handle wonky displays that report just over 60 Hz.
|
||||||
if (max_fps > 64) {
|
if (max_fps > 64) {
|
||||||
// Mark any FPS value greater than 120 as unsupported
|
// Mark any FPS value greater than 120 as unsupported
|
||||||
if (prefs.unsupportedFps && max_fps > 120) {
|
if (StreamingPreferences.unsupportedFps && max_fps > 120) {
|
||||||
fpsListModel.append({"text": max_fps+" FPS (Unsupported)", "video_fps": ""+max_fps})
|
fpsListModel.append({"text": max_fps+" FPS (Unsupported)", "video_fps": ""+max_fps})
|
||||||
}
|
}
|
||||||
else if (max_fps > 120) {
|
else if (max_fps > 120) {
|
||||||
@@ -237,7 +222,7 @@ Flickable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Add unsupported FPS values that come after the display max FPS
|
// Add unsupported FPS values that come after the display max FPS
|
||||||
if (prefs.unsupportedFps) {
|
if (StreamingPreferences.unsupportedFps) {
|
||||||
if (max_fps < 90) {
|
if (max_fps < 90) {
|
||||||
fpsListModel.append({"text": "90 FPS (Unsupported)", "video_fps": "90"})
|
fpsListModel.append({"text": "90 FPS (Unsupported)", "video_fps": "90"})
|
||||||
}
|
}
|
||||||
@@ -252,7 +237,7 @@ Flickable {
|
|||||||
function reinitialize() {
|
function reinitialize() {
|
||||||
model = createModel()
|
model = createModel()
|
||||||
|
|
||||||
var saved_fps = prefs.fps
|
var saved_fps = StreamingPreferences.fps
|
||||||
currentIndex = 0
|
currentIndex = 0
|
||||||
for (var i = 0; i < model.count; i++) {
|
for (var i = 0; i < model.count; i++) {
|
||||||
var el_fps = parseInt(model.get(i).video_fps);
|
var el_fps = parseInt(model.get(i).video_fps);
|
||||||
@@ -279,11 +264,13 @@ Flickable {
|
|||||||
var selectedFps = parseInt(model.get(currentIndex).video_fps)
|
var selectedFps = parseInt(model.get(currentIndex).video_fps)
|
||||||
|
|
||||||
// Only modify the bitrate if the values actually changed
|
// Only modify the bitrate if the values actually changed
|
||||||
if (prefs.fps !== selectedFps) {
|
if (StreamingPreferences.fps !== selectedFps) {
|
||||||
prefs.fps = selectedFps
|
StreamingPreferences.fps = selectedFps
|
||||||
|
|
||||||
prefs.bitrateKbps = prefs.getDefaultBitrate(prefs.width, prefs.height, prefs.fps);
|
StreamingPreferences.bitrateKbps = StreamingPreferences.getDefaultBitrate(StreamingPreferences.width,
|
||||||
slider.value = prefs.bitrateKbps
|
StreamingPreferences.height,
|
||||||
|
StreamingPreferences.fps);
|
||||||
|
slider.value = StreamingPreferences.bitrateKbps
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -307,9 +294,8 @@ Flickable {
|
|||||||
|
|
||||||
Slider {
|
Slider {
|
||||||
id: slider
|
id: slider
|
||||||
wheelEnabled: true
|
|
||||||
|
|
||||||
value: prefs.bitrateKbps
|
value: StreamingPreferences.bitrateKbps
|
||||||
|
|
||||||
stepSize: 500
|
stepSize: 500
|
||||||
from : 500
|
from : 500
|
||||||
@@ -320,13 +306,8 @@ Flickable {
|
|||||||
|
|
||||||
onValueChanged: {
|
onValueChanged: {
|
||||||
bitrateTitle.text = "Video bitrate: " + (value / 1000.0) + " Mbps"
|
bitrateTitle.text = "Video bitrate: " + (value / 1000.0) + " Mbps"
|
||||||
prefs.bitrateKbps = value
|
StreamingPreferences.bitrateKbps = value
|
||||||
}
|
}
|
||||||
|
|
||||||
ToolTip.delay: 1000
|
|
||||||
ToolTip.timeout: 5000
|
|
||||||
ToolTip.visible: hovered
|
|
||||||
ToolTip.text: "This slider controls video bitrate only. Audio data will add an additional 1-2 Mbps on top of this."
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Label {
|
Label {
|
||||||
@@ -340,7 +321,7 @@ Flickable {
|
|||||||
AutoResizingComboBox {
|
AutoResizingComboBox {
|
||||||
// ignore setting the index at first, and actually set it when the component is loaded
|
// ignore setting the index at first, and actually set it when the component is loaded
|
||||||
Component.onCompleted: {
|
Component.onCompleted: {
|
||||||
var savedWm = prefs.windowMode
|
var savedWm = StreamingPreferences.windowMode
|
||||||
currentIndex = 0
|
currentIndex = 0
|
||||||
for (var i = 0; i < windowModeListModel.count; i++) {
|
for (var i = 0; i < windowModeListModel.count; i++) {
|
||||||
var thisWm = windowModeListModel.get(i).val;
|
var thisWm = windowModeListModel.get(i).val;
|
||||||
@@ -371,7 +352,7 @@ Flickable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
onActivated: {
|
onActivated: {
|
||||||
prefs.windowMode = windowModeListModel.get(currentIndex).val
|
StreamingPreferences.windowMode = windowModeListModel.get(currentIndex).val
|
||||||
}
|
}
|
||||||
|
|
||||||
ToolTip.delay: 1000
|
ToolTip.delay: 1000
|
||||||
@@ -385,9 +366,9 @@ Flickable {
|
|||||||
hoverEnabled: true
|
hoverEnabled: true
|
||||||
text: "V-Sync"
|
text: "V-Sync"
|
||||||
font.pointSize: 12
|
font.pointSize: 12
|
||||||
checked: prefs.enableVsync
|
checked: StreamingPreferences.enableVsync
|
||||||
onCheckedChanged: {
|
onCheckedChanged: {
|
||||||
prefs.enableVsync = checked
|
StreamingPreferences.enableVsync = checked
|
||||||
}
|
}
|
||||||
|
|
||||||
ToolTip.delay: 1000
|
ToolTip.delay: 1000
|
||||||
@@ -401,10 +382,10 @@ Flickable {
|
|||||||
hoverEnabled: true
|
hoverEnabled: true
|
||||||
text: "Frame pacing"
|
text: "Frame pacing"
|
||||||
font.pointSize: 12
|
font.pointSize: 12
|
||||||
enabled: prefs.enableVsync
|
enabled: StreamingPreferences.enableVsync
|
||||||
checked: prefs.enableVsync && prefs.framePacing
|
checked: StreamingPreferences.enableVsync && StreamingPreferences.framePacing
|
||||||
onCheckedChanged: {
|
onCheckedChanged: {
|
||||||
prefs.framePacing = checked
|
StreamingPreferences.framePacing = checked
|
||||||
}
|
}
|
||||||
ToolTip.delay: 1000
|
ToolTip.delay: 1000
|
||||||
ToolTip.timeout: 5000
|
ToolTip.timeout: 5000
|
||||||
@@ -437,7 +418,7 @@ Flickable {
|
|||||||
AutoResizingComboBox {
|
AutoResizingComboBox {
|
||||||
// ignore setting the index at first, and actually set it when the component is loaded
|
// ignore setting the index at first, and actually set it when the component is loaded
|
||||||
Component.onCompleted: {
|
Component.onCompleted: {
|
||||||
var saved_audio = prefs.audioConfig
|
var saved_audio = StreamingPreferences.audioConfig
|
||||||
currentIndex = 0
|
currentIndex = 0
|
||||||
for (var i = 0; i < audioListModel.count; i++) {
|
for (var i = 0; i < audioListModel.count; i++) {
|
||||||
var el_audio = audioListModel.get(i).val;
|
var el_audio = audioListModel.get(i).val;
|
||||||
@@ -464,7 +445,7 @@ Flickable {
|
|||||||
}
|
}
|
||||||
// ::onActivated must be used, as it only listens for when the index is changed by a human
|
// ::onActivated must be used, as it only listens for when the index is changed by a human
|
||||||
onActivated : {
|
onActivated : {
|
||||||
prefs.audioConfig = audioListModel.get(currentIndex).val
|
StreamingPreferences.audioConfig = audioListModel.get(currentIndex).val
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -486,9 +467,19 @@ Flickable {
|
|||||||
id: startMaximizedCheck
|
id: startMaximizedCheck
|
||||||
text: "Maximize Moonlight window on startup"
|
text: "Maximize Moonlight window on startup"
|
||||||
font.pointSize: 12
|
font.pointSize: 12
|
||||||
checked: !prefs.startWindowed
|
checked: !StreamingPreferences.startWindowed
|
||||||
onCheckedChanged: {
|
onCheckedChanged: {
|
||||||
prefs.startWindowed = !checked
|
StreamingPreferences.startWindowed = !checked
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
CheckBox {
|
||||||
|
id: connectionWarningsCheck
|
||||||
|
text: "Show connection quality warnings"
|
||||||
|
font.pointSize: 12
|
||||||
|
checked: StreamingPreferences.connectionWarnings
|
||||||
|
onCheckedChanged: {
|
||||||
|
StreamingPreferences.connectionWarnings = checked
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -516,9 +507,9 @@ Flickable {
|
|||||||
id: singleControllerCheck
|
id: singleControllerCheck
|
||||||
text: "Force gamepad #1 always present"
|
text: "Force gamepad #1 always present"
|
||||||
font.pointSize: 12
|
font.pointSize: 12
|
||||||
checked: !prefs.multiController
|
checked: !StreamingPreferences.multiController
|
||||||
onCheckedChanged: {
|
onCheckedChanged: {
|
||||||
prefs.multiController = !checked
|
StreamingPreferences.multiController = !checked
|
||||||
}
|
}
|
||||||
|
|
||||||
ToolTip.delay: 1000
|
ToolTip.delay: 1000
|
||||||
@@ -533,9 +524,9 @@ Flickable {
|
|||||||
hoverEnabled: true
|
hoverEnabled: true
|
||||||
text: "Raw mouse input"
|
text: "Raw mouse input"
|
||||||
font.pointSize: 12
|
font.pointSize: 12
|
||||||
checked: !prefs.mouseAcceleration
|
checked: !StreamingPreferences.mouseAcceleration
|
||||||
onCheckedChanged: {
|
onCheckedChanged: {
|
||||||
prefs.mouseAcceleration = !checked
|
StreamingPreferences.mouseAcceleration = !checked
|
||||||
}
|
}
|
||||||
|
|
||||||
ToolTip.delay: 1000
|
ToolTip.delay: 1000
|
||||||
@@ -561,9 +552,9 @@ Flickable {
|
|||||||
id: optimizeGameSettingsCheck
|
id: optimizeGameSettingsCheck
|
||||||
text: "Optimize game settings"
|
text: "Optimize game settings"
|
||||||
font.pointSize: 12
|
font.pointSize: 12
|
||||||
checked: prefs.gameOptimizations
|
checked: StreamingPreferences.gameOptimizations
|
||||||
onCheckedChanged: {
|
onCheckedChanged: {
|
||||||
prefs.gameOptimizations = checked
|
StreamingPreferences.gameOptimizations = checked
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -571,9 +562,9 @@ Flickable {
|
|||||||
id: audioPcCheck
|
id: audioPcCheck
|
||||||
text: "Play audio on host PC"
|
text: "Play audio on host PC"
|
||||||
font.pointSize: 12
|
font.pointSize: 12
|
||||||
checked: prefs.playAudioOnHost
|
checked: StreamingPreferences.playAudioOnHost
|
||||||
onCheckedChanged: {
|
onCheckedChanged: {
|
||||||
prefs.playAudioOnHost = checked
|
StreamingPreferences.playAudioOnHost = checked
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -601,7 +592,7 @@ Flickable {
|
|||||||
AutoResizingComboBox {
|
AutoResizingComboBox {
|
||||||
// ignore setting the index at first, and actually set it when the component is loaded
|
// ignore setting the index at first, and actually set it when the component is loaded
|
||||||
Component.onCompleted: {
|
Component.onCompleted: {
|
||||||
var saved_vds = prefs.videoDecoderSelection
|
var saved_vds = StreamingPreferences.videoDecoderSelection
|
||||||
currentIndex = 0
|
currentIndex = 0
|
||||||
for (var i = 0; i < decoderListModel.count; i++) {
|
for (var i = 0; i < decoderListModel.count; i++) {
|
||||||
var el_vds = decoderListModel.get(i).val;
|
var el_vds = decoderListModel.get(i).val;
|
||||||
@@ -632,7 +623,7 @@ Flickable {
|
|||||||
}
|
}
|
||||||
// ::onActivated must be used, as it only listens for when the index is changed by a human
|
// ::onActivated must be used, as it only listens for when the index is changed by a human
|
||||||
onActivated : {
|
onActivated : {
|
||||||
prefs.videoDecoderSelection = decoderListModel.get(currentIndex).val
|
StreamingPreferences.videoDecoderSelection = decoderListModel.get(currentIndex).val
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -647,7 +638,7 @@ Flickable {
|
|||||||
AutoResizingComboBox {
|
AutoResizingComboBox {
|
||||||
// ignore setting the index at first, and actually set it when the component is loaded
|
// ignore setting the index at first, and actually set it when the component is loaded
|
||||||
Component.onCompleted: {
|
Component.onCompleted: {
|
||||||
var saved_vcc = prefs.videoCodecConfig
|
var saved_vcc = StreamingPreferences.videoCodecConfig
|
||||||
currentIndex = 0
|
currentIndex = 0
|
||||||
for(var i = 0; i < codecListModel.count; i++) {
|
for(var i = 0; i < codecListModel.count; i++) {
|
||||||
var el_vcc = codecListModel.get(i).val;
|
var el_vcc = codecListModel.get(i).val;
|
||||||
@@ -685,7 +676,7 @@ Flickable {
|
|||||||
}
|
}
|
||||||
// ::onActivated must be used, as it only listens for when the index is changed by a human
|
// ::onActivated must be used, as it only listens for when the index is changed by a human
|
||||||
onActivated : {
|
onActivated : {
|
||||||
prefs.videoCodecConfig = codecListModel.get(currentIndex).val
|
StreamingPreferences.videoCodecConfig = codecListModel.get(currentIndex).val
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -693,13 +684,17 @@ Flickable {
|
|||||||
id: unlockUnsupportedFps
|
id: unlockUnsupportedFps
|
||||||
text: "Unlock unsupported FPS options"
|
text: "Unlock unsupported FPS options"
|
||||||
font.pointSize: 12
|
font.pointSize: 12
|
||||||
checked: prefs.unsupportedFps
|
checked: StreamingPreferences.unsupportedFps
|
||||||
onCheckedChanged: {
|
onCheckedChanged: {
|
||||||
prefs.unsupportedFps = checked
|
// This is called on init, so only do the work if we've
|
||||||
|
// actually changed the value.
|
||||||
|
if (StreamingPreferences.unsupportedFps != checked) {
|
||||||
|
StreamingPreferences.unsupportedFps = checked
|
||||||
|
|
||||||
// The selectable FPS values depend on whether
|
// The selectable FPS values depend on whether
|
||||||
// this option is enabled or not
|
// this option is enabled or not
|
||||||
fpsComboBox.reinitialize()
|
fpsComboBox.reinitialize()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -707,18 +702,22 @@ Flickable {
|
|||||||
id: enableMdns
|
id: enableMdns
|
||||||
text: "Automatically find PCs on the local network (Recommended)"
|
text: "Automatically find PCs on the local network (Recommended)"
|
||||||
font.pointSize: 12
|
font.pointSize: 12
|
||||||
checked: prefs.enableMdns
|
checked: StreamingPreferences.enableMdns
|
||||||
onCheckedChanged: {
|
onCheckedChanged: {
|
||||||
prefs.enableMdns = checked
|
// This is called on init, so only do the work if we've
|
||||||
|
// actually changed the value.
|
||||||
|
if (StreamingPreferences.enableMdns != checked) {
|
||||||
|
StreamingPreferences.enableMdns = checked
|
||||||
|
|
||||||
// We must save the updated preference to ensure
|
// We must save the updated preference to ensure
|
||||||
// ComputerManager can observe the change internally.
|
// ComputerManager can observe the change internally.
|
||||||
prefs.save()
|
StreamingPreferences.save()
|
||||||
|
|
||||||
// Restart polling so the mDNS change takes effect
|
// Restart polling so the mDNS change takes effect
|
||||||
if (window.pollingActive) {
|
if (window.pollingActive) {
|
||||||
ComputerManager.stopPollingAsync()
|
ComputerManager.stopPollingAsync()
|
||||||
ComputerManager.startPolling()
|
ComputerManager.startPolling()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -727,9 +726,9 @@ Flickable {
|
|||||||
id: quitAppAfter
|
id: quitAppAfter
|
||||||
text: "Quit app after quitting session"
|
text: "Quit app after quitting session"
|
||||||
font.pointSize: 12
|
font.pointSize: 12
|
||||||
checked: prefs.quitAppAfter
|
checked: StreamingPreferences.quitAppAfter
|
||||||
onCheckedChanged: {
|
onCheckedChanged: {
|
||||||
prefs.quitAppAfter = checked
|
StreamingPreferences.quitAppAfter = checked
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
import QtQuick 2.0
|
import QtQuick 2.0
|
||||||
import QtQuick.Controls 2.2
|
import QtQuick.Controls 2.2
|
||||||
import QtQuick.Dialogs 1.2
|
|
||||||
import QtQuick.Window 2.2
|
import QtQuick.Window 2.2
|
||||||
|
|
||||||
import ComputerManager 1.0
|
|
||||||
import SdlGamepadKeyNavigation 1.0
|
import SdlGamepadKeyNavigation 1.0
|
||||||
import Session 1.0
|
import Session 1.0
|
||||||
|
|
||||||
@@ -13,13 +11,6 @@ Item {
|
|||||||
property string stageText : "Starting " + appName + "..."
|
property string stageText : "Starting " + appName + "..."
|
||||||
property bool quitAfter : false
|
property bool quitAfter : false
|
||||||
|
|
||||||
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 stageStarting(stage)
|
function stageStarting(stage)
|
||||||
{
|
{
|
||||||
// Update the spinner text
|
// Update the spinner text
|
||||||
@@ -28,8 +19,8 @@ Item {
|
|||||||
|
|
||||||
function stageFailed(stage, errorCode)
|
function stageFailed(stage, errorCode)
|
||||||
{
|
{
|
||||||
|
// Display the error dialog after Session::exec() returns
|
||||||
errorDialog.text = "Starting " + stage + " failed: Error " + errorCode
|
errorDialog.text = "Starting " + stage + " failed: Error " + errorCode
|
||||||
errorDialog.open()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function connectionStarted()
|
function connectionStarted()
|
||||||
@@ -38,6 +29,7 @@ Item {
|
|||||||
// see them briefly when we pop off the StackView
|
// see them briefly when we pop off the StackView
|
||||||
stageSpinner.visible = false
|
stageSpinner.visible = false
|
||||||
stageLabel.visible = false
|
stageLabel.visible = false
|
||||||
|
hintText.visible = false
|
||||||
|
|
||||||
// Hide the window now that streaming has begun
|
// Hide the window now that streaming has begun
|
||||||
window.visible = false
|
window.visible = false
|
||||||
@@ -54,7 +46,7 @@ Item {
|
|||||||
// This toast appears for 3 seconds, just shorter than how long
|
// This toast appears for 3 seconds, just shorter than how long
|
||||||
// Session will wait for it to be displayed. This gives it time
|
// Session will wait for it to be displayed. This gives it time
|
||||||
// to transition to invisible before continuing.
|
// to transition to invisible before continuing.
|
||||||
var toast = Qt.createQmlObject('import QtQuick.Controls 2.3; ToolTip {}', parent, '')
|
var toast = Qt.createQmlObject('import QtQuick.Controls 2.2; ToolTip {}', parent, '')
|
||||||
toast.text = text
|
toast.text = text
|
||||||
toast.timeout = 3000
|
toast.timeout = 3000
|
||||||
toast.visible = true
|
toast.visible = true
|
||||||
@@ -72,6 +64,9 @@ Item {
|
|||||||
|
|
||||||
function sessionFinished()
|
function sessionFinished()
|
||||||
{
|
{
|
||||||
|
// Enable GUI gamepad usage now
|
||||||
|
SdlGamepadKeyNavigation.enable()
|
||||||
|
|
||||||
if (quitAfter) {
|
if (quitAfter) {
|
||||||
if (errorDialog.text) {
|
if (errorDialog.text) {
|
||||||
// Quit when the error dialog is acknowledged
|
// Quit when the error dialog is acknowledged
|
||||||
@@ -98,42 +93,53 @@ Item {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// It's important that we don't call enable() here
|
StackView.onDeactivating: {
|
||||||
// or it may interfere with the Session instance
|
// Show the toolbar again when popped off the stack
|
||||||
// getting notified of initial connected gamepads.
|
toolBar.visible = true
|
||||||
SdlGamepadKeyNavigation {
|
|
||||||
id: gamepadKeyNav
|
// Enable GUI gamepad usage now
|
||||||
|
SdlGamepadKeyNavigation.enable()
|
||||||
}
|
}
|
||||||
|
|
||||||
onVisibleChanged: {
|
StackView.onActivated: {
|
||||||
if (visible) {
|
// Hide the toolbar before we start loading
|
||||||
// Hide the toolbar before we start loading
|
toolBar.visible = false
|
||||||
toolBar.visible = false
|
|
||||||
|
|
||||||
|
// Hook up our signals
|
||||||
|
session.stageStarting.connect(stageStarting)
|
||||||
|
session.stageFailed.connect(stageFailed)
|
||||||
|
session.connectionStarted.connect(connectionStarted)
|
||||||
|
session.displayLaunchError.connect(displayLaunchError)
|
||||||
|
session.displayLaunchWarning.connect(displayLaunchWarning)
|
||||||
|
session.quitStarting.connect(quitStarting)
|
||||||
|
session.sessionFinished.connect(sessionFinished)
|
||||||
|
|
||||||
|
// Kick off the stream
|
||||||
|
streamLoader.active = true
|
||||||
|
}
|
||||||
|
|
||||||
|
Loader {
|
||||||
|
id: streamLoader
|
||||||
|
active: false
|
||||||
|
asynchronous: true
|
||||||
|
|
||||||
|
onLoaded: {
|
||||||
// Set the hint text. We do this here rather than
|
// Set the hint text. We do this here rather than
|
||||||
// in the hintText control itself to synchronize
|
// in the hintText control itself to synchronize
|
||||||
// with Session.exec() which requires no concurrent
|
// with Session.exec() which requires no concurrent
|
||||||
// gamepad usage.
|
// gamepad usage.
|
||||||
hintText.text = gamepadKeyNav.getConnectedGamepads() > 0 ?
|
hintText.text = SdlGamepadKeyNavigation.getConnectedGamepads() > 0 ?
|
||||||
"Tip: Press Start+Select+L1+R1 to disconnect your session" :
|
"Tip: Press Start+Select+L1+R1 to disconnect your session" :
|
||||||
"Tip: Press Ctrl+Alt+Shift+Q to disconnect your session"
|
"Tip: Press Ctrl+Alt+Shift+Q to disconnect your session"
|
||||||
|
|
||||||
// Hook up our signals
|
// Stop GUI gamepad usage now
|
||||||
session.stageStarting.connect(stageStarting)
|
SdlGamepadKeyNavigation.disable()
|
||||||
session.stageFailed.connect(stageFailed)
|
|
||||||
session.connectionStarted.connect(connectionStarted)
|
|
||||||
session.displayLaunchError.connect(displayLaunchError)
|
|
||||||
session.displayLaunchWarning.connect(displayLaunchWarning)
|
|
||||||
session.quitStarting.connect(quitStarting)
|
|
||||||
session.sessionFinished.connect(sessionFinished)
|
|
||||||
|
|
||||||
// Run the streaming session to completion
|
// Run the streaming session to completion
|
||||||
session.exec(Screen.virtualX, Screen.virtualY)
|
session.exec(Screen.virtualX, Screen.virtualY)
|
||||||
}
|
}
|
||||||
else if (!quitAfter) {
|
|
||||||
// Show the toolbar again when we become hidden
|
sourceComponent: Item {}
|
||||||
toolBar.visible = true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Row {
|
Row {
|
||||||
@@ -166,21 +172,10 @@ Item {
|
|||||||
wrapMode: Text.Wrap
|
wrapMode: Text.Wrap
|
||||||
}
|
}
|
||||||
|
|
||||||
MessageDialog {
|
ErrorMessageDialog {
|
||||||
id: errorDialog
|
id: errorDialog
|
||||||
modality:Qt.WindowModal
|
|
||||||
icon: StandardIcon.Critical
|
|
||||||
standardButtons: StandardButton.Ok | StandardButton.Help
|
|
||||||
|
|
||||||
onAccepted: {
|
|
||||||
if (quitAfter) {
|
|
||||||
Qt.quit()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onHelp: {
|
|
||||||
Qt.openUrlExternally("https://github.com/moonlight-stream/moonlight-docs/wiki/Troubleshooting");
|
|
||||||
|
|
||||||
|
onClosed: {
|
||||||
if (quitAfter) {
|
if (quitAfter) {
|
||||||
Qt.quit()
|
Qt.quit()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,18 +22,6 @@ QVariant ComputerModel::data(const QModelIndex& index, int role) const
|
|||||||
return QVariant();
|
return QVariant();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (index.row() == m_Computers.count()) {
|
|
||||||
// We insert a synthetic item at the end for the Add PC option
|
|
||||||
switch (role) {
|
|
||||||
case NameRole:
|
|
||||||
return "Add PC";
|
|
||||||
case AddPcRole:
|
|
||||||
return true;
|
|
||||||
default:
|
|
||||||
return QVariant();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Q_ASSERT(index.row() < m_Computers.count());
|
Q_ASSERT(index.row() < m_Computers.count());
|
||||||
|
|
||||||
NvComputer* computer = m_Computers[index.row()];
|
NvComputer* computer = m_Computers[index.row()];
|
||||||
@@ -50,8 +38,6 @@ QVariant ComputerModel::data(const QModelIndex& index, int role) const
|
|||||||
return computer->currentGameId != 0;
|
return computer->currentGameId != 0;
|
||||||
case WakeableRole:
|
case WakeableRole:
|
||||||
return !computer->macAddress.isEmpty();
|
return !computer->macAddress.isEmpty();
|
||||||
case AddPcRole:
|
|
||||||
return false;
|
|
||||||
case StatusUnknownRole:
|
case StatusUnknownRole:
|
||||||
return computer->state == NvComputer::CS_UNKNOWN;
|
return computer->state == NvComputer::CS_UNKNOWN;
|
||||||
default:
|
default:
|
||||||
@@ -67,8 +53,7 @@ int ComputerModel::rowCount(const QModelIndex& parent) const
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add PC placeholder counts as 1
|
return m_Computers.count();
|
||||||
return m_Computers.count() + 1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
QHash<int, QByteArray> ComputerModel::roleNames() const
|
QHash<int, QByteArray> ComputerModel::roleNames() const
|
||||||
@@ -79,7 +64,6 @@ QHash<int, QByteArray> ComputerModel::roleNames() const
|
|||||||
names[OnlineRole] = "online";
|
names[OnlineRole] = "online";
|
||||||
names[PairedRole] = "paired";
|
names[PairedRole] = "paired";
|
||||||
names[BusyRole] = "busy";
|
names[BusyRole] = "busy";
|
||||||
names[AddPcRole] = "addPc";
|
|
||||||
names[WakeableRole] = "wakeable";
|
names[WakeableRole] = "wakeable";
|
||||||
names[StatusUnknownRole] = "statusUnknown";
|
names[StatusUnknownRole] = "statusUnknown";
|
||||||
|
|
||||||
|
|||||||
@@ -14,8 +14,7 @@ class ComputerModel : public QAbstractListModel
|
|||||||
PairedRole,
|
PairedRole,
|
||||||
BusyRole,
|
BusyRole,
|
||||||
WakeableRole,
|
WakeableRole,
|
||||||
StatusUnknownRole,
|
StatusUnknownRole
|
||||||
AddPcRole
|
|
||||||
};
|
};
|
||||||
|
|
||||||
public:
|
public:
|
||||||
|
|||||||
@@ -1,14 +1,13 @@
|
|||||||
import QtQuick 2.9
|
import QtQuick 2.9
|
||||||
import QtQuick.Controls 2.2
|
import QtQuick.Controls 2.2
|
||||||
import QtQuick.Dialogs 1.2
|
|
||||||
import QtQuick.Layouts 1.3
|
import QtQuick.Layouts 1.3
|
||||||
import QtQuick.Window 2.2
|
import QtQuick.Window 2.2
|
||||||
|
|
||||||
import QtQuick.Controls.Material 2.1
|
|
||||||
|
|
||||||
import ComputerManager 1.0
|
import ComputerManager 1.0
|
||||||
import AutoUpdateChecker 1.0
|
import AutoUpdateChecker 1.0
|
||||||
import StreamingPreferences 1.0
|
import StreamingPreferences 1.0
|
||||||
|
import SystemProperties 1.0
|
||||||
|
import SdlGamepadKeyNavigation 1.0
|
||||||
|
|
||||||
ApplicationWindow {
|
ApplicationWindow {
|
||||||
property bool pollingActive: false
|
property bool pollingActive: false
|
||||||
@@ -18,13 +17,10 @@ ApplicationWindow {
|
|||||||
width: 1280
|
width: 1280
|
||||||
height: 600
|
height: 600
|
||||||
|
|
||||||
visibility: prefs.startWindowed ? "Windowed" : "Maximized"
|
visibility: StreamingPreferences.startWindowed ? "Windowed" : "Maximized"
|
||||||
|
|
||||||
Material.theme: Material.Dark
|
Component.onCompleted: {
|
||||||
Material.accent: Material.Purple
|
SdlGamepadKeyNavigation.enable()
|
||||||
|
|
||||||
StreamingPreferences {
|
|
||||||
id: prefs
|
|
||||||
}
|
}
|
||||||
|
|
||||||
StackView {
|
StackView {
|
||||||
@@ -142,19 +138,18 @@ ApplicationWindow {
|
|||||||
// onAfterRendering call and potentially reenter this code.
|
// onAfterRendering call and potentially reenter this code.
|
||||||
initialized = true;
|
initialized = true;
|
||||||
|
|
||||||
if (prefs.isRunningWayland()) {
|
if (SystemProperties.isRunningWayland) {
|
||||||
waylandDialog.open()
|
waylandDialog.open()
|
||||||
}
|
}
|
||||||
else if (prefs.isWow64()) {
|
else if (SystemProperties.isWow64) {
|
||||||
wow64Dialog.open()
|
wow64Dialog.open()
|
||||||
}
|
}
|
||||||
else if (!prefs.hasAnyHardwareAcceleration()) {
|
else if (!SystemProperties.hasHardwareAcceleration) {
|
||||||
noHwDecoderDialog.open()
|
noHwDecoderDialog.open()
|
||||||
}
|
}
|
||||||
|
|
||||||
var unmappedGamepads = prefs.getUnmappedGamepads()
|
if (SystemProperties.unmappedGamepads) {
|
||||||
if (unmappedGamepads) {
|
unmappedGamepadDialog.unmappedGamepads = SystemProperties.unmappedGamepads
|
||||||
unmappedGamepadDialog.unmappedGamepads = unmappedGamepads
|
|
||||||
unmappedGamepadDialog.open()
|
unmappedGamepadDialog.open()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -218,6 +213,39 @@ ApplicationWindow {
|
|||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
NavigableToolButton {
|
||||||
|
id: addPcButton
|
||||||
|
visible: stackView.currentItem.objectName === "Computers"
|
||||||
|
|
||||||
|
Image {
|
||||||
|
source: "qrc:/res/ic_add_to_queue_white_48px.svg"
|
||||||
|
anchors.centerIn: parent
|
||||||
|
sourceSize {
|
||||||
|
width: toolBar.height - toolBar.anchors.bottomMargin - toolBar.anchors.topMargin
|
||||||
|
height: toolBar.height - toolBar.anchors.bottomMargin - toolBar.anchors.topMargin
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ToolTip.delay: 1000
|
||||||
|
ToolTip.timeout: 3000
|
||||||
|
ToolTip.visible: hovered
|
||||||
|
ToolTip.text: "Add PC manually" + (newPcShortcut.nativeText ? (" ("+newPcShortcut.nativeText+")") : "")
|
||||||
|
|
||||||
|
Shortcut {
|
||||||
|
id: newPcShortcut
|
||||||
|
sequence: StandardKey.New
|
||||||
|
onActivated: addPcButton.clicked()
|
||||||
|
}
|
||||||
|
|
||||||
|
onClicked: {
|
||||||
|
addPcDialog.open()
|
||||||
|
}
|
||||||
|
|
||||||
|
Keys.onDownPressed: {
|
||||||
|
stackView.currentItem.forceActiveFocus(Qt.TabFocus)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
NavigableToolButton {
|
NavigableToolButton {
|
||||||
property string browserUrl: ""
|
property string browserUrl: ""
|
||||||
|
|
||||||
@@ -235,7 +263,6 @@ ApplicationWindow {
|
|||||||
ToolTip.delay: 1000
|
ToolTip.delay: 1000
|
||||||
ToolTip.timeout: 3000
|
ToolTip.timeout: 3000
|
||||||
ToolTip.visible: hovered
|
ToolTip.visible: hovered
|
||||||
ToolTip.text: "Update available for Moonlight"
|
|
||||||
|
|
||||||
// Invisible until we get a callback notifying us that
|
// Invisible until we get a callback notifying us that
|
||||||
// an update is available
|
// an update is available
|
||||||
@@ -243,8 +270,9 @@ ApplicationWindow {
|
|||||||
|
|
||||||
onClicked: Qt.openUrlExternally(browserUrl);
|
onClicked: Qt.openUrlExternally(browserUrl);
|
||||||
|
|
||||||
function updateAvailable(url)
|
function updateAvailable(version, url)
|
||||||
{
|
{
|
||||||
|
ToolTip.text = "Update available for Moonlight: Version " + version
|
||||||
updateButton.browserUrl = url
|
updateButton.browserUrl = url
|
||||||
updateButton.visible = true
|
updateButton.visible = true
|
||||||
}
|
}
|
||||||
@@ -261,6 +289,7 @@ ApplicationWindow {
|
|||||||
|
|
||||||
NavigableToolButton {
|
NavigableToolButton {
|
||||||
id: helpButton
|
id: helpButton
|
||||||
|
visible: SystemProperties.hasBrowser
|
||||||
|
|
||||||
Image {
|
Image {
|
||||||
source: "qrc:/res/question_mark.svg"
|
source: "qrc:/res/question_mark.svg"
|
||||||
@@ -347,67 +376,81 @@ ApplicationWindow {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
MessageDialog {
|
ErrorMessageDialog {
|
||||||
id: noHwDecoderDialog
|
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. " +
|
text: "No functioning hardware accelerated H.264 video decoder was detected by Moonlight. " +
|
||||||
"Your streaming performance may be severely degraded in this configuration. " +
|
"Your streaming performance may be severely degraded in this configuration."
|
||||||
"Click the Help button for more information on solving this problem."
|
helpText: "Click the Help button for more information on solving this problem."
|
||||||
onHelp: {
|
helpUrl: "https://github.com/moonlight-stream/moonlight-docs/wiki/Fixing-Hardware-Decoding-Problems"
|
||||||
Qt.openUrlExternally("https://github.com/moonlight-stream/moonlight-docs/wiki/Fixing-Hardware-Decoding-Problems");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
MessageDialog {
|
ErrorMessageDialog {
|
||||||
id: waylandDialog
|
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. " +
|
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."
|
"Please switch to an X session for optimal performance."
|
||||||
onHelp: {
|
helpText: "Click the Help button for more information."
|
||||||
Qt.openUrlExternally("https://github.com/moonlight-stream/moonlight-docs/wiki/Fixing-Hardware-Decoding-Problems");
|
helpUrl: "https://github.com/moonlight-stream/moonlight-docs/wiki/Fixing-Hardware-Decoding-Problems"
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
MessageDialog {
|
NavigableMessageDialog {
|
||||||
id: wow64Dialog
|
id: wow64Dialog
|
||||||
modality:Qt.WindowModal
|
standardButtons: Dialog.Ok | Dialog.Cancel
|
||||||
icon: StandardIcon.Warning
|
|
||||||
standardButtons: StandardButton.Ok | StandardButton.Cancel
|
|
||||||
text: "This PC is running a 64-bit version of Windows. Please download the x64 version of Moonlight for the best streaming performance."
|
text: "This PC is running a 64-bit version of Windows. Please download the x64 version of Moonlight for the best streaming performance."
|
||||||
onAccepted: {
|
onAccepted: {
|
||||||
Qt.openUrlExternally("https://github.com/moonlight-stream/moonlight-qt/releases");
|
Qt.openUrlExternally("https://github.com/moonlight-stream/moonlight-qt/releases");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
MessageDialog {
|
ErrorMessageDialog {
|
||||||
id: unmappedGamepadDialog
|
id: unmappedGamepadDialog
|
||||||
property string unmappedGamepads : ""
|
property string unmappedGamepads : ""
|
||||||
modality:Qt.WindowModal
|
text: "Moonlight detected gamepads without a mapping:\n" + unmappedGamepads
|
||||||
icon: StandardIcon.Warning
|
helpTextSeparator: "\n\n"
|
||||||
standardButtons: StandardButton.Ok | StandardButton.Help
|
helpText: "Click the Help button for information on how to map your gamepads."
|
||||||
text: "Moonlight detected gamepads without a proper mapping. " +
|
helpUrl: "https://github.com/moonlight-stream/moonlight-docs/wiki/Gamepad-Mapping"
|
||||||
"The following gamepads will not function until this is resolved: " + unmappedGamepads + "\n\n" +
|
|
||||||
"Click the Help button for information on how to map your gamepads."
|
|
||||||
onHelp: {
|
|
||||||
Qt.openUrlExternally("https://github.com/moonlight-stream/moonlight-docs/wiki/Gamepad-Mapping");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// This dialog appears when quitting via keyboard or gamepad button
|
// This dialog appears when quitting via keyboard or gamepad button
|
||||||
MessageDialog {
|
NavigableMessageDialog {
|
||||||
id: quitConfirmationDialog
|
id: quitConfirmationDialog
|
||||||
modality:Qt.WindowModal
|
standardButtons: Dialog.Yes | Dialog.No
|
||||||
icon: StandardIcon.Warning
|
|
||||||
standardButtons: StandardButton.Yes | StandardButton.No
|
|
||||||
text: "Are you sure you want to quit?"
|
text: "Are you sure you want to quit?"
|
||||||
|
|
||||||
onYes: Qt.quit()
|
|
||||||
|
|
||||||
// For keyboard/gamepad navigation
|
// For keyboard/gamepad navigation
|
||||||
onAccepted: Qt.quit()
|
onAccepted: Qt.quit()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
NavigableDialog {
|
||||||
|
id: addPcDialog
|
||||||
|
property string label: "Enter the IP address of your GameStream PC:"
|
||||||
|
|
||||||
|
standardButtons: Dialog.Ok | Dialog.Cancel
|
||||||
|
|
||||||
|
onOpened: {
|
||||||
|
// Force keyboard focus on the textbox so keyboard navigation works
|
||||||
|
editText.forceActiveFocus()
|
||||||
|
}
|
||||||
|
|
||||||
|
onAccepted: {
|
||||||
|
if (editText.text) {
|
||||||
|
ComputerManager.addNewHost(editText.text, false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
Label {
|
||||||
|
text: addPcDialog.label
|
||||||
|
font.bold: true
|
||||||
|
}
|
||||||
|
|
||||||
|
TextField {
|
||||||
|
id: editText
|
||||||
|
Layout.fillWidth: true
|
||||||
|
focus: true
|
||||||
|
|
||||||
|
Keys.onReturnPressed: {
|
||||||
|
addPcDialog.accept()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -91,6 +91,12 @@ void SdlGamepadKeyNavigation::onPollingTimerFired()
|
|||||||
|
|
||||||
while (SDL_PollEvent(&event)) {
|
while (SDL_PollEvent(&event)) {
|
||||||
switch (event.type) {
|
switch (event.type) {
|
||||||
|
case SDL_QUIT:
|
||||||
|
// SDL may send us a quit event since we initialize
|
||||||
|
// the video subsystem on startup. If we get one,
|
||||||
|
// forward it on for Qt to take care of.
|
||||||
|
QCoreApplication::instance()->quit();
|
||||||
|
break;
|
||||||
case SDL_CONTROLLERBUTTONDOWN:
|
case SDL_CONTROLLERBUTTONDOWN:
|
||||||
case SDL_CONTROLLERBUTTONUP:
|
case SDL_CONTROLLERBUTTONUP:
|
||||||
{
|
{
|
||||||
@@ -247,12 +253,7 @@ void SdlGamepadKeyNavigation::setSettingsMode(bool settingsMode)
|
|||||||
|
|
||||||
int SdlGamepadKeyNavigation::getConnectedGamepads()
|
int SdlGamepadKeyNavigation::getConnectedGamepads()
|
||||||
{
|
{
|
||||||
if (SDL_InitSubSystem(SDL_INIT_GAMECONTROLLER) != 0) {
|
Q_ASSERT(m_Enabled);
|
||||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
|
||||||
"SDL_InitSubSystem(SDL_INIT_GAMECONTROLLER) failed: %s",
|
|
||||||
SDL_GetError());
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
int count = 0;
|
int count = 0;
|
||||||
for (int i = 0; i < SDL_NumJoysticks(); i++) {
|
for (int i = 0; i < SDL_NumJoysticks(); i++) {
|
||||||
@@ -261,6 +262,5 @@ int SdlGamepadKeyNavigation::getConnectedGamepads()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
SDL_QuitSubSystem(SDL_INIT_GAMECONTROLLER);
|
|
||||||
return count;
|
return count;
|
||||||
}
|
}
|
||||||
@@ -6,6 +6,9 @@
|
|||||||
#include <QMutex>
|
#include <QMutex>
|
||||||
#include <QtDebug>
|
#include <QtDebug>
|
||||||
#include <QNetworkProxyFactory>
|
#include <QNetworkProxyFactory>
|
||||||
|
#include <QPalette>
|
||||||
|
#include <QFont>
|
||||||
|
#include <QCursor>
|
||||||
|
|
||||||
// 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
|
||||||
@@ -28,6 +31,7 @@
|
|||||||
#include "gui/computermodel.h"
|
#include "gui/computermodel.h"
|
||||||
#include "gui/appmodel.h"
|
#include "gui/appmodel.h"
|
||||||
#include "backend/autoupdatechecker.h"
|
#include "backend/autoupdatechecker.h"
|
||||||
|
#include "backend/systemproperties.h"
|
||||||
#include "streaming/session.h"
|
#include "streaming/session.h"
|
||||||
#include "settings/streamingpreferences.h"
|
#include "settings/streamingpreferences.h"
|
||||||
#include "gui/sdlgamepadkeynavigation.h"
|
#include "gui/sdlgamepadkeynavigation.h"
|
||||||
@@ -136,14 +140,6 @@ void qtLogToDiskHandler(QtMsgType type, const QMessageLogContext&, const QString
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
// HACK: Avoid printing this internal Qt warning which seems to print every time we
|
|
||||||
// start our event loop for the QNetworkReply. I have found this warning in many
|
|
||||||
// unrelated bug reports, but none actually tackle this warning itself. It seems to be
|
|
||||||
// new in Qt 5.12.
|
|
||||||
if (msg.startsWith("QNetworkReplyHttpImplPrivate::_q_startOperation was called more than once")) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
QTime logTime = QTime::fromMSecsSinceStartOfDay(s_LoggerTime.elapsed());
|
QTime logTime = QTime::fromMSecsSinceStartOfDay(s_LoggerTime.elapsed());
|
||||||
QString txt = QString("%1 - Qt %2: %3\n").arg(logTime.toString()).arg(typeTxt).arg(msg);
|
QString txt = QString("%1 - Qt %2: %3\n").arg(logTime.toString()).arg(typeTxt).arg(msg);
|
||||||
|
|
||||||
@@ -231,6 +227,8 @@ LONG WINAPI UnhandledExceptionHandler(struct _EXCEPTION_POINTERS *ExceptionInfo)
|
|||||||
|
|
||||||
int main(int argc, char *argv[])
|
int main(int argc, char *argv[])
|
||||||
{
|
{
|
||||||
|
SDL_SetMainReady();
|
||||||
|
|
||||||
// Set the app version for the QCommandLineParser's showVersion() command
|
// Set the app version for the QCommandLineParser's showVersion() command
|
||||||
QCoreApplication::setApplicationVersion(VERSION_STR);
|
QCoreApplication::setApplicationVersion(VERSION_STR);
|
||||||
|
|
||||||
@@ -290,9 +288,12 @@ int main(int argc, char *argv[])
|
|||||||
|
|
||||||
// This avoids using the default keychain for SSL, which may cause
|
// This avoids using the default keychain for SSL, which may cause
|
||||||
// password prompts on macOS.
|
// password prompts on macOS.
|
||||||
qputenv("QT_SSL_USE_TEMPORARY_KEYCHAIN", QByteArray("1"));
|
qputenv("QT_SSL_USE_TEMPORARY_KEYCHAIN", "1");
|
||||||
|
|
||||||
#ifdef Q_OS_WIN32
|
// The debug build of ANGLE on Qt 5.12.2 triggers all sorts of asserts
|
||||||
|
// even simply creating an empty Qt Quick window and then trying to close
|
||||||
|
// the program. Avoid using ANGLE for debug builds until the issue is fixed.
|
||||||
|
#if defined(Q_OS_WIN32) && !defined(QT_DEBUG)
|
||||||
if (!qEnvironmentVariableIsSet("QT_OPENGL")) {
|
if (!qEnvironmentVariableIsSet("QT_OPENGL")) {
|
||||||
// On Windows, use ANGLE so we don't have to load OpenGL
|
// On Windows, use ANGLE so we don't have to load OpenGL
|
||||||
// user-mode drivers into our app. OGL drivers (especially Intel)
|
// user-mode drivers into our app. OGL drivers (especially Intel)
|
||||||
@@ -311,15 +312,57 @@ int main(int argc, char *argv[])
|
|||||||
// Register custom metatypes for use in signals
|
// Register custom metatypes for use in signals
|
||||||
qRegisterMetaType<NvApp>("NvApp");
|
qRegisterMetaType<NvApp>("NvApp");
|
||||||
|
|
||||||
|
// Allow the display to sleep by default. We will manually use SDL_DisableScreenSaver()
|
||||||
|
// and SDL_EnableScreenSaver() when appropriate. This hint must be set before
|
||||||
|
// initializing the SDL video subsystem to have any effect.
|
||||||
|
SDL_SetHint(SDL_HINT_VIDEO_ALLOW_SCREENSAVER, "1");
|
||||||
|
|
||||||
|
// Steam Link requires that we initialize video before creating our
|
||||||
|
// QGuiApplication in order to configure the framebuffer correctly.
|
||||||
|
// It's fine to do on other platforms too, and it can save some time
|
||||||
|
// doing initialization and teardown of the video subsystem after streaming.
|
||||||
|
if (SDL_InitSubSystem(SDL_INIT_VIDEO | SDL_INIT_TIMER) != 0) {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"SDL_InitSubSystem(SDL_INIT_VIDEO | SDL_INIT_TIMER) failed: %s",
|
||||||
|
SDL_GetError());
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use atexit() to ensure SDL_Quit() is called. This avoids
|
||||||
|
// racing with object destruction where SDL may be used.
|
||||||
|
atexit(SDL_Quit);
|
||||||
|
|
||||||
|
// Avoid the default behavior of changing the timer resolution to 1 ms.
|
||||||
|
// We don't want this all the time that Moonlight is open. We will set
|
||||||
|
// it manually when we start streaming.
|
||||||
|
SDL_SetHint(SDL_HINT_TIMER_RESOLUTION, "0");
|
||||||
|
|
||||||
|
// Disable minimize on focus loss by default. Users seem to want this off by default.
|
||||||
|
SDL_SetHint(SDL_HINT_VIDEO_MINIMIZE_ON_FOCUS_LOSS, "0");
|
||||||
|
|
||||||
QGuiApplication app(argc, argv);
|
QGuiApplication app(argc, argv);
|
||||||
|
|
||||||
|
#ifdef STEAM_LINK
|
||||||
|
// Qt 5.9 from the Steam Link SDK is not able to load any fonts
|
||||||
|
// since the Steam Link doesn't include any of the ones it looks
|
||||||
|
// for. We know it has NotoSans so we will explicitly ask for that.
|
||||||
|
if (app.font().family().isEmpty()) {
|
||||||
|
qWarning() << "SL HACK: No default font - using NotoSans";
|
||||||
|
|
||||||
|
QFont fon("NotoSans");
|
||||||
|
app.setFont(fon);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Move the mouse to the bottom right so it's invisible when using
|
||||||
|
// gamepad-only navigation.
|
||||||
|
QCursor().setPos(0xFFFF, 0xFFFF);
|
||||||
|
#endif
|
||||||
|
|
||||||
app.setWindowIcon(QIcon(":/res/moonlight.svg"));
|
app.setWindowIcon(QIcon(":/res/moonlight.svg"));
|
||||||
|
|
||||||
// Register our C++ types for QML
|
// Register our C++ types for QML
|
||||||
qmlRegisterType<ComputerModel>("ComputerModel", 1, 0, "ComputerModel");
|
qmlRegisterType<ComputerModel>("ComputerModel", 1, 0, "ComputerModel");
|
||||||
qmlRegisterType<AppModel>("AppModel", 1, 0, "AppModel");
|
qmlRegisterType<AppModel>("AppModel", 1, 0, "AppModel");
|
||||||
qmlRegisterType<StreamingPreferences>("StreamingPreferences", 1, 0, "StreamingPreferences");
|
|
||||||
qmlRegisterType<SdlGamepadKeyNavigation>("SdlGamepadKeyNavigation", 1, 0, "SdlGamepadKeyNavigation");
|
|
||||||
qmlRegisterUncreatableType<Session>("Session", 1, 0, "Session", "Session cannot be created from QML");
|
qmlRegisterUncreatableType<Session>("Session", 1, 0, "Session", "Session cannot be created from QML");
|
||||||
qmlRegisterSingletonType<ComputerManager>("ComputerManager", 1, 0,
|
qmlRegisterSingletonType<ComputerManager>("ComputerManager", 1, 0,
|
||||||
"ComputerManager",
|
"ComputerManager",
|
||||||
@@ -331,8 +374,48 @@ int main(int argc, char *argv[])
|
|||||||
[](QQmlEngine*, QJSEngine*) -> QObject* {
|
[](QQmlEngine*, QJSEngine*) -> QObject* {
|
||||||
return new AutoUpdateChecker();
|
return new AutoUpdateChecker();
|
||||||
});
|
});
|
||||||
|
qmlRegisterSingletonType<SystemProperties>("SystemProperties", 1, 0,
|
||||||
|
"SystemProperties",
|
||||||
|
[](QQmlEngine*, QJSEngine*) -> QObject* {
|
||||||
|
return new SystemProperties();
|
||||||
|
});
|
||||||
|
qmlRegisterSingletonType<SdlGamepadKeyNavigation>("SdlGamepadKeyNavigation", 1, 0,
|
||||||
|
"SdlGamepadKeyNavigation",
|
||||||
|
[](QQmlEngine*, QJSEngine*) -> QObject* {
|
||||||
|
return new SdlGamepadKeyNavigation();
|
||||||
|
});
|
||||||
|
qmlRegisterSingletonType<StreamingPreferences>("StreamingPreferences", 1, 0,
|
||||||
|
"StreamingPreferences",
|
||||||
|
[](QQmlEngine*, QJSEngine*) -> QObject* {
|
||||||
|
return new StreamingPreferences();
|
||||||
|
});
|
||||||
|
|
||||||
QQuickStyle::setStyle("Material");
|
#ifndef Q_OS_WINRT
|
||||||
|
// Use the dense material dark theme by default
|
||||||
|
if (!qEnvironmentVariableIsSet("QT_QUICK_CONTROLS_STYLE")) {
|
||||||
|
qputenv("QT_QUICK_CONTROLS_STYLE", "Material");
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
// Use universal dark on WinRT
|
||||||
|
if (!qEnvironmentVariableIsSet("QT_QUICK_CONTROLS_STYLE")) {
|
||||||
|
qputenv("QT_QUICK_CONTROLS_STYLE", "Universal");
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
if (!qEnvironmentVariableIsSet("QT_QUICK_CONTROLS_MATERIAL_THEME")) {
|
||||||
|
qputenv("QT_QUICK_CONTROLS_MATERIAL_THEME", "Dark");
|
||||||
|
}
|
||||||
|
if (!qEnvironmentVariableIsSet("QT_QUICK_CONTROLS_MATERIAL_ACCENT")) {
|
||||||
|
qputenv("QT_QUICK_CONTROLS_MATERIAL_ACCENT", "Purple");
|
||||||
|
}
|
||||||
|
if (!qEnvironmentVariableIsSet("QT_QUICK_CONTROLS_MATERIAL_VARIANT")) {
|
||||||
|
qputenv("QT_QUICK_CONTROLS_MATERIAL_VARIANT", "Dense");
|
||||||
|
}
|
||||||
|
if (!qEnvironmentVariableIsSet("QT_QUICK_CONTROLS_UNIVERSAL_THEME")) {
|
||||||
|
qputenv("QT_QUICK_CONTROLS_UNIVERSAL_THEME", "Dark");
|
||||||
|
}
|
||||||
|
if (!qEnvironmentVariableIsSet("QT_QUICK_CONTROLS_1_STYLE")) {
|
||||||
|
qputenv("QT_QUICK_CONTROLS_1_STYLE", "Base");
|
||||||
|
}
|
||||||
|
|
||||||
QQmlApplicationEngine engine;
|
QQmlApplicationEngine engine;
|
||||||
QString initialView;
|
QString initialView;
|
||||||
@@ -340,11 +423,11 @@ int main(int argc, char *argv[])
|
|||||||
GlobalCommandLineParser parser;
|
GlobalCommandLineParser parser;
|
||||||
switch (parser.parse(app.arguments())) {
|
switch (parser.parse(app.arguments())) {
|
||||||
case GlobalCommandLineParser::NormalStartRequested:
|
case GlobalCommandLineParser::NormalStartRequested:
|
||||||
initialView = "PcView.qml";
|
initialView = "qrc:/gui/PcView.qml";
|
||||||
break;
|
break;
|
||||||
case GlobalCommandLineParser::StreamRequested:
|
case GlobalCommandLineParser::StreamRequested:
|
||||||
{
|
{
|
||||||
initialView = "CliStartStreamSegue.qml";
|
initialView = "qrc:/gui/CliStartStreamSegue.qml";
|
||||||
StreamingPreferences* preferences = new StreamingPreferences(&app);
|
StreamingPreferences* preferences = new StreamingPreferences(&app);
|
||||||
StreamCommandLineParser streamParser;
|
StreamCommandLineParser streamParser;
|
||||||
streamParser.parse(app.arguments(), preferences);
|
streamParser.parse(app.arguments(), preferences);
|
||||||
@@ -356,7 +439,7 @@ int main(int argc, char *argv[])
|
|||||||
}
|
}
|
||||||
case GlobalCommandLineParser::QuitRequested:
|
case GlobalCommandLineParser::QuitRequested:
|
||||||
{
|
{
|
||||||
initialView = "CliQuitStreamSegue.qml";
|
initialView = "qrc:/gui/CliQuitStreamSegue.qml";
|
||||||
QuitCommandLineParser quitParser;
|
QuitCommandLineParser quitParser;
|
||||||
quitParser.parse(app.arguments());
|
quitParser.parse(app.arguments());
|
||||||
auto launcher = new CliQuitStream::Launcher(quitParser.getHost(), &app);
|
auto launcher = new CliQuitStream::Launcher(quitParser.getHost(), &app);
|
||||||
@@ -372,22 +455,6 @@ int main(int argc, char *argv[])
|
|||||||
if (engine.rootObjects().isEmpty())
|
if (engine.rootObjects().isEmpty())
|
||||||
return -1;
|
return -1;
|
||||||
|
|
||||||
SDL_SetMainReady();
|
|
||||||
if (SDL_Init(SDL_INIT_TIMER) != 0) {
|
|
||||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
|
||||||
"SDL_Init() failed: %s",
|
|
||||||
SDL_GetError());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Use atexit() to ensure SDL_Quit() is called. This avoids
|
|
||||||
// racing with object destruction where SDL may be used.
|
|
||||||
atexit(SDL_Quit);
|
|
||||||
|
|
||||||
// Avoid the default behavior of changing the timer resolution to 1 ms.
|
|
||||||
// We don't want this all the time that Moonlight is open. We will set
|
|
||||||
// it manually when we start streaming.
|
|
||||||
SDL_SetHint(SDL_HINT_TIMER_RESOLUTION, "0");
|
|
||||||
|
|
||||||
int err = app.exec();
|
int err = app.exec();
|
||||||
|
|
||||||
// Give worker tasks time to properly exit. Fixes PendingQuitTask
|
// Give worker tasks time to properly exit. Fixes PendingQuitTask
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 345 KiB After Width: | Height: | Size: 328 KiB |
|
Before Width: | Height: | Size: 5.4 KiB After Width: | Height: | Size: 2.6 KiB |
@@ -8,7 +8,6 @@
|
|||||||
|
|
||||||
QString Path::s_LogDir;
|
QString Path::s_LogDir;
|
||||||
QString Path::s_BoxArtCacheDir;
|
QString Path::s_BoxArtCacheDir;
|
||||||
const QString Path::k_GameControllerMappingFile("gamecontrollerdb.txt");
|
|
||||||
|
|
||||||
QString Path::getLogDir()
|
QString Path::getLogDir()
|
||||||
{
|
{
|
||||||
@@ -22,40 +21,42 @@ QString Path::getBoxArtCacheDir()
|
|||||||
return s_BoxArtCacheDir;
|
return s_BoxArtCacheDir;
|
||||||
}
|
}
|
||||||
|
|
||||||
QString Path::getGamepadMappingFile()
|
QByteArray Path::readDataFile(QString fileName)
|
||||||
|
{
|
||||||
|
QFile dataFile(getDataFilePath(fileName));
|
||||||
|
dataFile.open(QIODevice::ReadOnly);
|
||||||
|
return dataFile.readAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
QString Path::getDataFilePath(QString fileName)
|
||||||
{
|
{
|
||||||
QString candidatePath;
|
QString candidatePath;
|
||||||
|
|
||||||
// Check the current directory first
|
// Check the current directory first
|
||||||
candidatePath = QDir(QDir::currentPath()).absoluteFilePath(k_GameControllerMappingFile);
|
candidatePath = QDir(QDir::currentPath()).absoluteFilePath(fileName);
|
||||||
if (QFile::exists(candidatePath)) {
|
if (QFile::exists(candidatePath)) {
|
||||||
|
qInfo() << "Found" << fileName << "at" << candidatePath;
|
||||||
return candidatePath;
|
return candidatePath;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Now check the data directories (for Linux, in particular)
|
// Now check the data directories (for Linux, in particular)
|
||||||
candidatePath = QStandardPaths::locate(QStandardPaths::DataLocation, k_GameControllerMappingFile);
|
candidatePath = QStandardPaths::locate(QStandardPaths::DataLocation, fileName);
|
||||||
if (!candidatePath.isEmpty() && QFile::exists(candidatePath)) {
|
if (!candidatePath.isEmpty() && QFile::exists(candidatePath)) {
|
||||||
|
qInfo() << "Found" << fileName << "at" << candidatePath;
|
||||||
return candidatePath;
|
return candidatePath;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Now try the directory of our app installation (for Windows, if current dir doesn't find it)
|
// Now try the directory of our app installation (for Windows, if current dir doesn't find it)
|
||||||
candidatePath = QDir(QCoreApplication::applicationDirPath()).absoluteFilePath(k_GameControllerMappingFile);
|
candidatePath = QDir(QCoreApplication::applicationDirPath()).absoluteFilePath(fileName);
|
||||||
if (QFile::exists(candidatePath)) {
|
if (QFile::exists(candidatePath)) {
|
||||||
|
qInfo() << "Found" << fileName << "at" << candidatePath;
|
||||||
return candidatePath;
|
return candidatePath;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Now try the Resources folder in our app bundle for macOS
|
// Return the QRC embedded copy
|
||||||
QDir dir = QDir(QCoreApplication::applicationDirPath());
|
candidatePath = ":/data/" + fileName;
|
||||||
dir.cdUp();
|
qInfo() << "Found" << fileName << "at" << candidatePath;
|
||||||
dir.cd("Resources");
|
return QString(candidatePath);
|
||||||
dir.filePath(k_GameControllerMappingFile);
|
|
||||||
candidatePath = dir.absoluteFilePath(k_GameControllerMappingFile);
|
|
||||||
if (QFile::exists(candidatePath)) {
|
|
||||||
return candidatePath;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Couldn't find it
|
|
||||||
return QString();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void Path::initialize(bool portable)
|
void Path::initialize(bool portable)
|
||||||
|
|||||||
@@ -9,12 +9,14 @@ public:
|
|||||||
|
|
||||||
static QString getBoxArtCacheDir();
|
static QString getBoxArtCacheDir();
|
||||||
|
|
||||||
static QString getGamepadMappingFile();
|
static QByteArray readDataFile(QString fileName);
|
||||||
|
|
||||||
|
// Only safe to use directly for Qt classes
|
||||||
|
static QString getDataFilePath(QString fileName);
|
||||||
|
|
||||||
static void initialize(bool portable);
|
static void initialize(bool portable);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
static QString s_LogDir;
|
static QString s_LogDir;
|
||||||
static QString s_BoxArtCacheDir;
|
static QString s_BoxArtCacheDir;
|
||||||
static const QString k_GameControllerMappingFile;
|
|
||||||
};
|
};
|
||||||
@@ -13,5 +13,10 @@
|
|||||||
<file>gui/CliQuitStreamSegue.qml</file>
|
<file>gui/CliQuitStreamSegue.qml</file>
|
||||||
<file>gui/CliStartStreamSegue.qml</file>
|
<file>gui/CliStartStreamSegue.qml</file>
|
||||||
<file>gui/AutoResizingComboBox.qml</file>
|
<file>gui/AutoResizingComboBox.qml</file>
|
||||||
|
<file>gui/NavigableMenu.qml</file>
|
||||||
|
<file>gui/ErrorMessageDialog.qml</file>
|
||||||
|
<file>gui/NavigableMessageDialog.qml</file>
|
||||||
|
<file>gui/NavigableDialog.qml</file>
|
||||||
|
<file>gui/CenteredGridView.qml</file>
|
||||||
</qresource>
|
</qresource>
|
||||||
</RCC>
|
</RCC>
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="#fff"><path d="M12 2C6.47 2 2 6.47 2 12s4.47 10 10 10 10-4.47 10-10S17.53 2 12 2zm5 13.59L15.59 17 12 13.41 8.41 17 7 15.59 10.59 12 7 8.41 8.41 7 12 10.59 15.59 7 17 8.41 13.41 12 17 15.59z"/><path d="M0 0h24v24H0z" fill="none"/></svg>
|
||||||
|
After Width: | Height: | Size: 325 B |
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="#FFFFFF"><path fill="none" d="M0 0h24v24H0V0z"/><path d="M11 15h2v2h-2zm0-8h2v6h-2zm.99-5C6.47 2 2 6.48 2 12s4.47 10 9.99 10C17.52 22 22 17.52 22 12S17.52 2 11.99 2zM12 20c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8z"/></svg>
|
||||||
|
After Width: | Height: | Size: 323 B |
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="#FFFFFF"><path fill="none" d="M0 0h24v24H0z"/><path d="M11 18h2v-2h-2v2zm1-16C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8zm0-14c-2.21 0-4 1.79-4 4h2c0-1.1.9-2 2-2s2 .9 2 2c0 2-3 1.75-3 5h2c0-2.25 3-2.5 3-5 0-2.21-1.79-4-4-4z"/></svg>
|
||||||
|
After Width: | Height: | Size: 397 B |
@@ -1,69 +1 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
<?xml version="1.0" standalone="no"?><!-- Generator: Gravit.io --><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" style="isolation:isolate" viewBox="0 0 256 256" width="256" height="256"><g><circle vector-effect="non-scaling-stroke" cx="128" cy="128" r="128" fill="rgb(86,92,100)"/><circle vector-effect="non-scaling-stroke" cx="128.00000000000003" cy="128.00000000000003" r="96" fill="rgb(255,255,255)"/><path d=" M 137.66 132 L 224 132 L 224 124 L 137.66 124 L 198.71 62.95 L 193.05 57.29 L 132 118.34 L 132 32 L 124 32 L 124 118.34 L 62.95 57.29 L 57.29 62.95 L 118.34 124 L 32 124 L 32 132 L 118.34 132 L 57.29 193.05 L 62.95 198.71 L 124 137.66 L 124 224 L 132 224 L 132 137.66 L 193.05 198.71 L 198.71 193.05 L 137.66 132 Z " fill-rule="evenodd" fill="rgb(86,92,100)"/></g></svg>
|
||||||
<svg
|
|
||||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
|
||||||
xmlns:cc="http://creativecommons.org/ns#"
|
|
||||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
|
||||||
xmlns:svg="http://www.w3.org/2000/svg"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
id="svg3761"
|
|
||||||
viewBox="0 0 680 680"
|
|
||||||
height="680"
|
|
||||||
width="680"
|
|
||||||
version="1.0">
|
|
||||||
<metadata
|
|
||||||
id="metadata3767">
|
|
||||||
<rdf:RDF>
|
|
||||||
<cc:Work
|
|
||||||
rdf:about="">
|
|
||||||
<dc:format>image/svg+xml</dc:format>
|
|
||||||
<dc:type
|
|
||||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
|
||||||
<dc:title></dc:title>
|
|
||||||
</cc:Work>
|
|
||||||
</rdf:RDF>
|
|
||||||
</metadata>
|
|
||||||
<defs
|
|
||||||
id="defs3765" />
|
|
||||||
<g
|
|
||||||
id="g3759"
|
|
||||||
transform="translate(-135,491) scale(0.100000,-0.100000)">
|
|
||||||
<circle
|
|
||||||
id="circle3741"
|
|
||||||
fill="#565C64"
|
|
||||||
r="3400"
|
|
||||||
cy="1505"
|
|
||||||
cx="4755" />
|
|
||||||
<path
|
|
||||||
id="path3743"
|
|
||||||
style="fill:#ffffff"
|
|
||||||
d="M4550 4230 c-582 -48 -1123 -271 -1568 -646 l-84 -70 869 -869 c477 -477 871 -868 876 -868 4 0 6 552 5 1228 l-3 1227 -25 1 c-14 0 -45 -1 -70 -3z" />
|
|
||||||
<path
|
|
||||||
id="path3745"
|
|
||||||
style="fill:#ffffff"
|
|
||||||
d="M4867 4230 c-10 -9 -9 -2453 1 -2453 4 0 398 391 875 868 l869 869 -84 70 c-344 290 -749 492 -1178 587 -166 36 -469 74 -483 59z" />
|
|
||||||
<path
|
|
||||||
id="path3747"
|
|
||||||
style="fill:#ffffff"
|
|
||||||
d="M2673 3275 c-241 -285 -412 -595 -526 -953 -63 -196 -111 -458 -119 -640 l-3 -70 1228 -3 c675 -1 1227 1 1227 5 0 5 -391 399 -868 876 l-869 869 -70 -84z" />
|
|
||||||
<path
|
|
||||||
id="path3749"
|
|
||||||
style="fill:#ffffff"
|
|
||||||
d="M5898 2490 c-478 -477 -868 -871 -868 -876 0 -4 552 -6 1228 -5 l1227 3 -3 70 c-14 326 -124 729 -287 1055 -91 182 -259 430 -387 573 l-43 48 -867 -868z" />
|
|
||||||
<path
|
|
||||||
id="path3751"
|
|
||||||
style="fill:#ffffff"
|
|
||||||
d="M2026 1388 c-7 -11 10 -198 30 -321 76 -486 291 -952 617 -1338 l70 -84 869 869 c477 477 868 871 868 875 0 11 -2448 10 -2454 -1z" />
|
|
||||||
<path
|
|
||||||
id="path3753"
|
|
||||||
style="fill:#ffffff"
|
|
||||||
d="M5030 1390 c0 -5 391 -399 868 -876 l869 -869 70 84 c241 285 412 595 526 953 63 196 111 458 119 640 l3 70 -1227 3 c-676 1 -1228 -1 -1228 -5z" />
|
|
||||||
<path
|
|
||||||
id="path3755"
|
|
||||||
style="fill:#ffffff"
|
|
||||||
d="M3767 359 l-869 -869 84 -70 c285 -241 595 -412 953 -526 196 -63 458 -111 640 -119 l70 -3 3 1228 c1 675 -1 1227 -5 1227 -5 0 -399 -391 -876 -868z" />
|
|
||||||
<path
|
|
||||||
id="path3757"
|
|
||||||
style="fill:#ffffff"
|
|
||||||
d="M4862 0 l3 -1228 70 3 c182 8 444 56 640 119 358 114 668 285 953 526 l84 70 -869 869 c-477 477 -871 868 -876 868 -4 0 -6 -552 -5 -1227z" />
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 820 B |
@@ -8,11 +8,18 @@
|
|||||||
<file>res/baseline-lock-24px.svg</file>
|
<file>res/baseline-lock-24px.svg</file>
|
||||||
<file>res/baseline-play_circle_filled_white-48px.svg</file>
|
<file>res/baseline-play_circle_filled_white-48px.svg</file>
|
||||||
<file>res/baseline-warning-24px.svg</file>
|
<file>res/baseline-warning-24px.svg</file>
|
||||||
|
<file>res/baseline-cancel-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>
|
<file>res/update.svg</file>
|
||||||
|
<file>res/baseline-help_outline-24px.svg</file>
|
||||||
|
<file>res/baseline-error_outline-24px.svg</file>
|
||||||
|
</qresource>
|
||||||
|
<qresource prefix="/data">
|
||||||
|
<file alias="gamecontrollerdb.txt">SDL_GameControllerDB/gamecontrollerdb.txt</file>
|
||||||
|
<file alias="ModeSeven.ttf">ModeSeven.ttf</file>
|
||||||
</qresource>
|
</qresource>
|
||||||
</RCC>
|
</RCC>
|
||||||
@@ -54,15 +54,10 @@ void MappingManager::save()
|
|||||||
|
|
||||||
void MappingManager::applyMappings()
|
void MappingManager::applyMappings()
|
||||||
{
|
{
|
||||||
QString mappingFile = Path::getGamepadMappingFile();
|
QByteArray mappingData = Path::readDataFile("gamecontrollerdb.txt");
|
||||||
if (!mappingFile.isEmpty()) {
|
if (!mappingData.isEmpty()) {
|
||||||
std::string mappingFileNative = QDir::toNativeSeparators(mappingFile).toStdString();
|
int newMappings = SDL_GameControllerAddMappingsFromRW(
|
||||||
|
SDL_RWFromConstMem(mappingData.constData(), mappingData.size()), 1);
|
||||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
|
||||||
"Loading gamepad mappings from: %s",
|
|
||||||
mappingFileNative.c_str());
|
|
||||||
|
|
||||||
int newMappings = SDL_GameControllerAddMappingsFromFile(mappingFileNative.c_str());
|
|
||||||
if (newMappings < 0) {
|
if (newMappings < 0) {
|
||||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
"Error loading gamepad mappings: %s",
|
"Error loading gamepad mappings: %s",
|
||||||
@@ -75,8 +70,8 @@ void MappingManager::applyMappings()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
"No gamepad mapping file found");
|
"Unable to load gamepad mapping file");
|
||||||
}
|
}
|
||||||
|
|
||||||
QList<SdlGamepadMapping> mappings = m_Mappings.values();
|
QList<SdlGamepadMapping> mappings = m_Mappings.values();
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
#include "streamingpreferences.h"
|
#include "streamingpreferences.h"
|
||||||
#include "streaming/session.h"
|
|
||||||
#include "streaming/streamutils.h"
|
|
||||||
|
|
||||||
#include <QSettings>
|
#include <QSettings>
|
||||||
|
|
||||||
@@ -24,6 +22,7 @@
|
|||||||
#define SER_MOUSEACCELERATION "mouseacceleration"
|
#define SER_MOUSEACCELERATION "mouseacceleration"
|
||||||
#define SER_STARTWINDOWED "startwindowed"
|
#define SER_STARTWINDOWED "startwindowed"
|
||||||
#define SER_FRAMEPACING "framepacing"
|
#define SER_FRAMEPACING "framepacing"
|
||||||
|
#define SER_CONNWARNINGS "connwarnings"
|
||||||
|
|
||||||
StreamingPreferences::StreamingPreferences(QObject *parent)
|
StreamingPreferences::StreamingPreferences(QObject *parent)
|
||||||
: QObject(parent)
|
: QObject(parent)
|
||||||
@@ -49,6 +48,7 @@ void StreamingPreferences::reload()
|
|||||||
mouseAcceleration = settings.value(SER_MOUSEACCELERATION, false).toBool();
|
mouseAcceleration = settings.value(SER_MOUSEACCELERATION, false).toBool();
|
||||||
startWindowed = settings.value(SER_STARTWINDOWED, false).toBool();
|
startWindowed = settings.value(SER_STARTWINDOWED, false).toBool();
|
||||||
framePacing = settings.value(SER_FRAMEPACING, false).toBool();
|
framePacing = settings.value(SER_FRAMEPACING, false).toBool();
|
||||||
|
connectionWarnings = settings.value(SER_CONNWARNINGS, true).toBool();
|
||||||
audioConfig = static_cast<AudioConfig>(settings.value(SER_AUDIOCFG,
|
audioConfig = static_cast<AudioConfig>(settings.value(SER_AUDIOCFG,
|
||||||
static_cast<int>(AudioConfig::AC_STEREO)).toInt());
|
static_cast<int>(AudioConfig::AC_STEREO)).toInt());
|
||||||
videoCodecConfig = static_cast<VideoCodecConfig>(settings.value(SER_VIDEOCFG,
|
videoCodecConfig = static_cast<VideoCodecConfig>(settings.value(SER_VIDEOCFG,
|
||||||
@@ -79,133 +79,13 @@ void StreamingPreferences::save()
|
|||||||
settings.setValue(SER_MOUSEACCELERATION, mouseAcceleration);
|
settings.setValue(SER_MOUSEACCELERATION, mouseAcceleration);
|
||||||
settings.setValue(SER_STARTWINDOWED, startWindowed);
|
settings.setValue(SER_STARTWINDOWED, startWindowed);
|
||||||
settings.setValue(SER_FRAMEPACING, framePacing);
|
settings.setValue(SER_FRAMEPACING, framePacing);
|
||||||
|
settings.setValue(SER_CONNWARNINGS, connectionWarnings);
|
||||||
settings.setValue(SER_AUDIOCFG, static_cast<int>(audioConfig));
|
settings.setValue(SER_AUDIOCFG, static_cast<int>(audioConfig));
|
||||||
settings.setValue(SER_VIDEOCFG, static_cast<int>(videoCodecConfig));
|
settings.setValue(SER_VIDEOCFG, static_cast<int>(videoCodecConfig));
|
||||||
settings.setValue(SER_VIDEODEC, static_cast<int>(videoDecoderSelection));
|
settings.setValue(SER_VIDEODEC, static_cast<int>(videoDecoderSelection));
|
||||||
settings.setValue(SER_WINDOWMODE, static_cast<int>(windowMode));
|
settings.setValue(SER_WINDOWMODE, static_cast<int>(windowMode));
|
||||||
}
|
}
|
||||||
|
|
||||||
bool StreamingPreferences::hasAnyHardwareAcceleration()
|
|
||||||
{
|
|
||||||
// Always use VDS_AUTO to avoid spamming the user with warnings
|
|
||||||
// if they've forced software decoding.
|
|
||||||
return Session::isHardwareDecodeAvailable(VDS_AUTO,
|
|
||||||
VIDEO_FORMAT_H264,
|
|
||||||
1920, 1080, 60);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool StreamingPreferences::isRunningWayland()
|
|
||||||
{
|
|
||||||
return qgetenv("XDG_SESSION_TYPE") == QByteArray("wayland");
|
|
||||||
}
|
|
||||||
|
|
||||||
bool StreamingPreferences::isWow64()
|
|
||||||
{
|
|
||||||
#ifdef Q_OS_WIN32
|
|
||||||
return QSysInfo::currentCpuArchitecture() != QSysInfo::buildCpuArchitecture();
|
|
||||||
#else
|
|
||||||
return false;
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
|
|
||||||
QString StreamingPreferences::getUnmappedGamepads()
|
|
||||||
{
|
|
||||||
return SdlInputHandler::getUnmappedGamepads();
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
SDL_DisplayMode mode, bestMode, desktopMode;
|
|
||||||
for (int displayIndex = 0; displayIndex < SDL_GetNumVideoDisplays(); displayIndex++) {
|
|
||||||
// Get the native desktop resolution
|
|
||||||
if (StreamUtils::getRealDesktopMode(displayIndex, &desktopMode)) {
|
|
||||||
|
|
||||||
// Start at desktop mode and work our way up
|
|
||||||
bestMode = desktopMode;
|
|
||||||
for (int i = 0; i < SDL_GetNumDisplayModes(displayIndex); i++) {
|
|
||||||
if (SDL_GetDisplayMode(displayIndex, i, &mode) == 0) {
|
|
||||||
if (mode.w == desktopMode.w && mode.h == desktopMode.h) {
|
|
||||||
if (mode.refresh_rate > bestMode.refresh_rate) {
|
|
||||||
bestMode = mode;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
maxFrameRate = qMax(maxFrameRate, bestMode.refresh_rate);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
SDL_QuitSubSystem(SDL_INIT_VIDEO);
|
|
||||||
|
|
||||||
return maxFrameRate;
|
|
||||||
}
|
|
||||||
|
|
||||||
QRect StreamingPreferences::getDesktopResolution(int displayIndex)
|
|
||||||
{
|
|
||||||
if (SDL_InitSubSystem(SDL_INIT_VIDEO) != 0) {
|
|
||||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
|
||||||
"SDL_InitSubSystem(SDL_INIT_VIDEO) failed: %s",
|
|
||||||
SDL_GetError());
|
|
||||||
return QRect();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (displayIndex >= SDL_GetNumVideoDisplays()) {
|
|
||||||
SDL_QuitSubSystem(SDL_INIT_VIDEO);
|
|
||||||
return QRect();
|
|
||||||
}
|
|
||||||
|
|
||||||
SDL_DisplayMode mode;
|
|
||||||
int err = SDL_GetDesktopDisplayMode(displayIndex, &mode);
|
|
||||||
SDL_QuitSubSystem(SDL_INIT_VIDEO);
|
|
||||||
|
|
||||||
if (err == 0) {
|
|
||||||
return QRect(0, 0, mode.w, mode.h);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
|
||||||
"SDL_GetDesktopDisplayMode() failed: %s",
|
|
||||||
SDL_GetError());
|
|
||||||
return QRect();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
QRect StreamingPreferences::getNativeResolution(int displayIndex)
|
|
||||||
{
|
|
||||||
if (SDL_InitSubSystem(SDL_INIT_VIDEO) != 0) {
|
|
||||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
|
||||||
"SDL_InitSubSystem(SDL_INIT_VIDEO) failed: %s",
|
|
||||||
SDL_GetError());
|
|
||||||
return QRect();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (displayIndex >= SDL_GetNumVideoDisplays()) {
|
|
||||||
SDL_QuitSubSystem(SDL_INIT_VIDEO);
|
|
||||||
return QRect();
|
|
||||||
}
|
|
||||||
|
|
||||||
SDL_DisplayMode mode;
|
|
||||||
bool success = StreamUtils::getRealDesktopMode(displayIndex, &mode);
|
|
||||||
SDL_QuitSubSystem(SDL_INIT_VIDEO);
|
|
||||||
|
|
||||||
if (success) {
|
|
||||||
return QRect(0, 0, mode.w, mode.h);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
return QRect();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
int StreamingPreferences::getDefaultBitrate(int width, int height, int fps)
|
int StreamingPreferences::getDefaultBitrate(int width, int height, int fps)
|
||||||
{
|
{
|
||||||
// This table prefers 16:10 resolutions because they are
|
// This table prefers 16:10 resolutions because they are
|
||||||
|
|||||||
@@ -15,20 +15,6 @@ public:
|
|||||||
|
|
||||||
Q_INVOKABLE void save();
|
Q_INVOKABLE void save();
|
||||||
|
|
||||||
Q_INVOKABLE static bool hasAnyHardwareAcceleration();
|
|
||||||
|
|
||||||
Q_INVOKABLE static bool isRunningWayland();
|
|
||||||
|
|
||||||
Q_INVOKABLE static bool isWow64();
|
|
||||||
|
|
||||||
Q_INVOKABLE static int getMaximumStreamingFrameRate();
|
|
||||||
|
|
||||||
Q_INVOKABLE QRect getDesktopResolution(int displayIndex);
|
|
||||||
|
|
||||||
Q_INVOKABLE QRect getNativeResolution(int displayIndex);
|
|
||||||
|
|
||||||
Q_INVOKABLE QString getUnmappedGamepads();
|
|
||||||
|
|
||||||
void reload();
|
void reload();
|
||||||
|
|
||||||
enum AudioConfig
|
enum AudioConfig
|
||||||
@@ -77,6 +63,7 @@ public:
|
|||||||
Q_PROPERTY(bool mouseAcceleration MEMBER mouseAcceleration NOTIFY mouseAccelerationChanged)
|
Q_PROPERTY(bool mouseAcceleration MEMBER mouseAcceleration NOTIFY mouseAccelerationChanged)
|
||||||
Q_PROPERTY(bool startWindowed MEMBER startWindowed NOTIFY startWindowedChanged)
|
Q_PROPERTY(bool startWindowed MEMBER startWindowed NOTIFY startWindowedChanged)
|
||||||
Q_PROPERTY(bool framePacing MEMBER framePacing NOTIFY framePacingChanged)
|
Q_PROPERTY(bool framePacing MEMBER framePacing NOTIFY framePacingChanged)
|
||||||
|
Q_PROPERTY(bool connectionWarnings MEMBER connectionWarnings NOTIFY connectionWarningsChanged)
|
||||||
Q_PROPERTY(AudioConfig audioConfig MEMBER audioConfig NOTIFY audioConfigChanged)
|
Q_PROPERTY(AudioConfig audioConfig MEMBER audioConfig NOTIFY audioConfigChanged)
|
||||||
Q_PROPERTY(VideoCodecConfig videoCodecConfig MEMBER videoCodecConfig NOTIFY videoCodecConfigChanged)
|
Q_PROPERTY(VideoCodecConfig videoCodecConfig MEMBER videoCodecConfig NOTIFY videoCodecConfigChanged)
|
||||||
Q_PROPERTY(VideoDecoderSelection videoDecoderSelection MEMBER videoDecoderSelection NOTIFY videoDecoderSelectionChanged)
|
Q_PROPERTY(VideoDecoderSelection videoDecoderSelection MEMBER videoDecoderSelection NOTIFY videoDecoderSelectionChanged)
|
||||||
@@ -97,6 +84,7 @@ public:
|
|||||||
bool mouseAcceleration;
|
bool mouseAcceleration;
|
||||||
bool startWindowed;
|
bool startWindowed;
|
||||||
bool framePacing;
|
bool framePacing;
|
||||||
|
bool connectionWarnings;
|
||||||
AudioConfig audioConfig;
|
AudioConfig audioConfig;
|
||||||
VideoCodecConfig videoCodecConfig;
|
VideoCodecConfig videoCodecConfig;
|
||||||
VideoDecoderSelection videoDecoderSelection;
|
VideoDecoderSelection videoDecoderSelection;
|
||||||
@@ -119,5 +107,6 @@ signals:
|
|||||||
void windowModeChanged();
|
void windowModeChanged();
|
||||||
void startWindowedChanged();
|
void startWindowedChanged();
|
||||||
void framePacingChanged();
|
void framePacingChanged();
|
||||||
|
void connectionWarningsChanged();
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -3,16 +3,26 @@
|
|||||||
|
|
||||||
#ifdef HAVE_SOUNDIO
|
#ifdef HAVE_SOUNDIO
|
||||||
#include "renderers/soundioaudiorenderer.h"
|
#include "renderers/soundioaudiorenderer.h"
|
||||||
#else
|
|
||||||
#include "renderers/sdl.h"
|
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
#ifdef HAVE_SLAUDIO
|
||||||
|
#include "renderers/slaud.h"
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#include "renderers/sdl.h"
|
||||||
|
|
||||||
#include <Limelight.h>
|
#include <Limelight.h>
|
||||||
|
|
||||||
IAudioRenderer* Session::createAudioRenderer()
|
IAudioRenderer* Session::createAudioRenderer()
|
||||||
{
|
{
|
||||||
#ifdef HAVE_SOUNDIO
|
#if defined(HAVE_SOUNDIO)
|
||||||
|
if (qgetenv("ML_AUDIO") == "SDL") {
|
||||||
|
return new SdlAudioRenderer();
|
||||||
|
}
|
||||||
|
|
||||||
return new SoundIoAudioRenderer();
|
return new SoundIoAudioRenderer();
|
||||||
|
#elif defined(HAVE_SLAUDIO)
|
||||||
|
return new SLAudioRenderer();
|
||||||
#else
|
#else
|
||||||
return new SdlAudioRenderer();
|
return new SdlAudioRenderer();
|
||||||
#endif
|
#endif
|
||||||
@@ -111,6 +121,21 @@ void Session::arDecodeAndPlaySample(char* sampleData, int sampleLength)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// See if we need to drop this sample
|
||||||
|
if (s_ActiveSession->m_DropAudioEndTime != 0) {
|
||||||
|
if (SDL_TICKS_PASSED(SDL_GetTicks(), s_ActiveSession->m_DropAudioEndTime)) {
|
||||||
|
// Avoid calling SDL_GetTicks() now
|
||||||
|
s_ActiveSession->m_DropAudioEndTime = 0;
|
||||||
|
|
||||||
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Audio drop window has ended");
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// We're still in the drop window
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
s_ActiveSession->m_AudioSampleCount++;
|
s_ActiveSession->m_AudioSampleCount++;
|
||||||
|
|
||||||
if (s_ActiveSession->m_AudioRenderer != nullptr) {
|
if (s_ActiveSession->m_AudioRenderer != nullptr) {
|
||||||
@@ -141,10 +166,22 @@ void Session::arDecodeAndPlaySample(char* sampleData, int sampleLength)
|
|||||||
// safe to reinitialize here because we can't be torn down while
|
// safe to reinitialize here because we can't be torn down while
|
||||||
// the audio decoder/playback thread is still alive.
|
// the audio decoder/playback thread is still alive.
|
||||||
if (s_ActiveSession->m_AudioRenderer == nullptr && (s_ActiveSession->m_AudioSampleCount % 200) == 0) {
|
if (s_ActiveSession->m_AudioRenderer == nullptr && (s_ActiveSession->m_AudioSampleCount % 200) == 0) {
|
||||||
|
// Since we're doing this inline and audio initialization takes time, we need
|
||||||
|
// to drop samples to account for the time we've spent blocking audio rendering
|
||||||
|
// so we return to real-time playback and don't accumulate latency.
|
||||||
|
Uint32 audioReinitStartTime = SDL_GetTicks();
|
||||||
|
|
||||||
s_ActiveSession->m_AudioRenderer = s_ActiveSession->createAudioRenderer();
|
s_ActiveSession->m_AudioRenderer = s_ActiveSession->createAudioRenderer();
|
||||||
if (!s_ActiveSession->m_AudioRenderer->prepareForPlayback(&s_ActiveSession->m_AudioConfig)) {
|
if (!s_ActiveSession->m_AudioRenderer->prepareForPlayback(&s_ActiveSession->m_AudioConfig)) {
|
||||||
delete s_ActiveSession->m_AudioRenderer;
|
delete s_ActiveSession->m_AudioRenderer;
|
||||||
s_ActiveSession->m_AudioRenderer = nullptr;
|
s_ActiveSession->m_AudioRenderer = nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Uint32 audioReinitStopTime = SDL_GetTicks();
|
||||||
|
|
||||||
|
s_ActiveSession->m_DropAudioEndTime = audioReinitStopTime + (audioReinitStopTime - audioReinitStartTime);
|
||||||
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Audio reinitialization took %d ms - starting drop window",
|
||||||
|
audioReinitStopTime - audioReinitStartTime);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -16,9 +16,4 @@ public:
|
|||||||
|
|
||||||
private:
|
private:
|
||||||
SDL_AudioDeviceID m_AudioDevice;
|
SDL_AudioDeviceID m_AudioDevice;
|
||||||
Uint32 m_ChannelCount;
|
|
||||||
Uint32 m_PendingDrops;
|
|
||||||
Uint32 m_PendingHardDrops;
|
|
||||||
Uint32 m_SampleIndex;
|
|
||||||
Uint32 m_BaselinePendingData;
|
|
||||||
};
|
};
|
||||||
@@ -5,18 +5,8 @@
|
|||||||
|
|
||||||
#include <QtGlobal>
|
#include <QtGlobal>
|
||||||
|
|
||||||
#define MIN_QUEUED_FRAMES 2
|
|
||||||
#define MAX_QUEUED_FRAMES 4
|
|
||||||
#define STOP_THE_WORLD_LIMIT 20
|
|
||||||
#define DROP_RATIO_DENOM 32
|
|
||||||
|
|
||||||
SdlAudioRenderer::SdlAudioRenderer()
|
SdlAudioRenderer::SdlAudioRenderer()
|
||||||
: m_AudioDevice(0),
|
: m_AudioDevice(0)
|
||||||
m_ChannelCount(0),
|
|
||||||
m_PendingDrops(0),
|
|
||||||
m_PendingHardDrops(0),
|
|
||||||
m_SampleIndex(0),
|
|
||||||
m_BaselinePendingData(0)
|
|
||||||
{
|
{
|
||||||
SDL_assert(!SDL_WasInit(SDL_INIT_AUDIO));
|
SDL_assert(!SDL_WasInit(SDL_INIT_AUDIO));
|
||||||
if (SDL_InitSubSystem(SDL_INIT_AUDIO) != 0) {
|
if (SDL_InitSubSystem(SDL_INIT_AUDIO) != 0) {
|
||||||
@@ -61,27 +51,6 @@ bool SdlAudioRenderer::prepareForPlayback(const OPUS_MULTISTREAM_CONFIGURATION*
|
|||||||
have.samples,
|
have.samples,
|
||||||
have.size);
|
have.size);
|
||||||
|
|
||||||
// SDL counts pending samples in the queued
|
|
||||||
// audio size using the WASAPI backend. This
|
|
||||||
// includes silence, which can throw off our
|
|
||||||
// pending data count. Get a baseline so we
|
|
||||||
// can exclude that data.
|
|
||||||
m_BaselinePendingData = 0;
|
|
||||||
#ifdef Q_OS_WIN32
|
|
||||||
for (int i = 0; i < 100; i++) {
|
|
||||||
m_BaselinePendingData = qMax(m_BaselinePendingData, SDL_GetQueuedAudioSize(m_AudioDevice));
|
|
||||||
SDL_Delay(10);
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
m_BaselinePendingData *= 2;
|
|
||||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
|
||||||
"Baseline pending audio data: %d bytes",
|
|
||||||
m_BaselinePendingData);
|
|
||||||
|
|
||||||
m_ChannelCount = opusConfig->channelCount;
|
|
||||||
m_SampleIndex = 0;
|
|
||||||
m_PendingDrops = m_PendingHardDrops = 0;
|
|
||||||
|
|
||||||
// Start playback
|
// Start playback
|
||||||
SDL_PauseAudioDevice(m_AudioDevice, 0);
|
SDL_PauseAudioDevice(m_AudioDevice, 0);
|
||||||
|
|
||||||
@@ -102,53 +71,6 @@ SdlAudioRenderer::~SdlAudioRenderer()
|
|||||||
|
|
||||||
bool SdlAudioRenderer::submitAudio(short* audioBuffer, int audioSize)
|
bool SdlAudioRenderer::submitAudio(short* audioBuffer, int audioSize)
|
||||||
{
|
{
|
||||||
m_SampleIndex++;
|
|
||||||
|
|
||||||
Uint32 queuedAudio = SDL_GetQueuedAudioSize(m_AudioDevice);
|
|
||||||
if (queuedAudio > m_BaselinePendingData) {
|
|
||||||
queuedAudio -= m_BaselinePendingData;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
queuedAudio = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
Uint32 framesQueued = queuedAudio / (SAMPLES_PER_FRAME * m_ChannelCount * sizeof(short));
|
|
||||||
|
|
||||||
// We must check this prior to the below checks to ensure we don't
|
|
||||||
// underflow if framesQueued - m_PendingHardDrops < 0.
|
|
||||||
if (framesQueued <= MIN_QUEUED_FRAMES) {
|
|
||||||
m_PendingDrops = m_PendingHardDrops = 0;
|
|
||||||
}
|
|
||||||
// Pend enough drops to get us back to MIN_QUEUED_FRAMES, checking first
|
|
||||||
// to ensure we don't underflow.
|
|
||||||
else if (framesQueued > m_PendingHardDrops &&
|
|
||||||
framesQueued - m_PendingHardDrops > STOP_THE_WORLD_LIMIT) {
|
|
||||||
m_PendingHardDrops = framesQueued - MIN_QUEUED_FRAMES;
|
|
||||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
|
||||||
"Pending hard drop of %u audio frames",
|
|
||||||
m_PendingHardDrops);
|
|
||||||
}
|
|
||||||
// If we're under the stop the world limit, we can drop samples
|
|
||||||
// gracefully over the next little while.
|
|
||||||
else if (framesQueued > m_PendingHardDrops + m_PendingDrops &&
|
|
||||||
framesQueued - m_PendingHardDrops - m_PendingDrops > MAX_QUEUED_FRAMES) {
|
|
||||||
m_PendingDrops = framesQueued - MIN_QUEUED_FRAMES;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Determine if this frame should be dropped
|
|
||||||
if (m_PendingHardDrops != 0) {
|
|
||||||
// Hard drops happen all at once to forcefully
|
|
||||||
// resync with the source.
|
|
||||||
m_PendingHardDrops--;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
else if (m_PendingDrops != 0 && m_SampleIndex % DROP_RATIO_DENOM == 0) {
|
|
||||||
// Normal drops are interspersed with the audio data
|
|
||||||
// to hide the glitches.
|
|
||||||
m_PendingDrops--;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (SDL_QueueAudio(m_AudioDevice, audioBuffer, audioSize) < 0) {
|
if (SDL_QueueAudio(m_AudioDevice, audioBuffer, audioSize) < 0) {
|
||||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
"Failed to queue audio sample: %s",
|
"Failed to queue audio sample: %s",
|
||||||
|
|||||||
@@ -0,0 +1,58 @@
|
|||||||
|
#include "slaud.h"
|
||||||
|
|
||||||
|
#include <SDL.h>
|
||||||
|
|
||||||
|
SLAudioRenderer::SLAudioRenderer()
|
||||||
|
: m_AudioContext(nullptr),
|
||||||
|
m_AudioStream(nullptr)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
bool SLAudioRenderer::prepareForPlayback(const OPUS_MULTISTREAM_CONFIGURATION* opusConfig)
|
||||||
|
{
|
||||||
|
m_AudioContext = SLAudio_CreateContext();
|
||||||
|
if (m_AudioContext == nullptr) {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"SLAudio_CreateContext() failed");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_AudioStream = SLAudio_CreateStream(m_AudioContext,
|
||||||
|
opusConfig->sampleRate,
|
||||||
|
opusConfig->channelCount,
|
||||||
|
SAMPLES_PER_FRAME * sizeof(short) * opusConfig->channelCount,
|
||||||
|
1);
|
||||||
|
if (m_AudioStream == nullptr) {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"SLAudio_CreateStream() failed");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Using SLAudio renderer");
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
SLAudioRenderer::~SLAudioRenderer()
|
||||||
|
{
|
||||||
|
if (m_AudioStream != nullptr) {
|
||||||
|
SLAudio_FreeStream(m_AudioStream);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (m_AudioContext != nullptr) {
|
||||||
|
SLAudio_FreeContext(m_AudioContext);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool SLAudioRenderer::submitAudio(short* audioBuffer, int audioSize)
|
||||||
|
{
|
||||||
|
void* outputBuffer = SLAudio_BeginFrame(m_AudioStream);
|
||||||
|
|
||||||
|
if (outputBuffer != nullptr) {
|
||||||
|
memcpy(outputBuffer, audioBuffer, audioSize);
|
||||||
|
SLAudio_SubmitFrame(m_AudioStream);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "renderer.h"
|
||||||
|
#include <SLAudio.h>
|
||||||
|
|
||||||
|
class SLAudioRenderer : public IAudioRenderer
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
SLAudioRenderer();
|
||||||
|
|
||||||
|
virtual ~SLAudioRenderer();
|
||||||
|
|
||||||
|
virtual bool prepareForPlayback(const OPUS_MULTISTREAM_CONFIGURATION* opusConfig);
|
||||||
|
|
||||||
|
virtual bool submitAudio(short* audioBuffer, int audioSize);
|
||||||
|
|
||||||
|
private:
|
||||||
|
CSLAudioContext* m_AudioContext;
|
||||||
|
CSLAudioStream* m_AudioStream;
|
||||||
|
};
|
||||||
@@ -9,9 +9,11 @@ const double SoundIoAudioRenderer::k_RawSampleLengthSec = 0.005;
|
|||||||
|
|
||||||
#ifdef Q_OS_LINUX
|
#ifdef Q_OS_LINUX
|
||||||
// PulseAudio and ALSA require more than just 5 ms samples
|
// PulseAudio and ALSA require more than just 5 ms samples
|
||||||
// for some reason, so write a minimum of 20 ms each time to
|
// for some reason, so write a minimum of 25 ms each time to
|
||||||
// prevent underruns on Bluetooth.
|
// prevent underruns on Bluetooth.
|
||||||
const double SoundIoAudioRenderer::k_MinSampleLengthSec = 0.020;
|
// https://github.com/moonlight-stream/moonlight-qt/issues/147
|
||||||
|
// https://github.com/moonlight-stream/moonlight-qt/issues/157
|
||||||
|
const double SoundIoAudioRenderer::k_MinSampleLengthSec = 0.025;
|
||||||
#else
|
#else
|
||||||
// This determines the size of the buffers we'll
|
// This determines the size of the buffers we'll
|
||||||
// get from CoreAudio. It is also the minimum
|
// get from CoreAudio. It is also the minimum
|
||||||
@@ -369,16 +371,15 @@ void SoundIoAudioRenderer::sioWriteCallback(SoundIoOutStream* stream, int frameC
|
|||||||
(me->m_OpusChannelCount * stream->bytes_per_sample);
|
(me->m_OpusChannelCount * stream->bytes_per_sample);
|
||||||
int bytesRead = 0;
|
int bytesRead = 0;
|
||||||
|
|
||||||
|
// Clamp framesLeft to frameCountMax
|
||||||
|
framesLeft = qMin(framesLeft, frameCountMax);
|
||||||
|
|
||||||
// Ensure we always write at least a buffer, even if it's silence, to avoid
|
// Ensure we always write at least a buffer, even if it's silence, to avoid
|
||||||
// busy looping when no audio data is available while libsoundio tries to keep
|
// busy looping when no audio data is available while libsoundio tries to keep
|
||||||
// us from starving the output device.
|
// us from starving the output device.
|
||||||
frameCountMin = qMax(frameCountMin, (int)(stream->sample_rate * k_MinSampleLengthSec));
|
frameCountMin = qMax(frameCountMin, (int)(stream->sample_rate * k_MinSampleLengthSec));
|
||||||
frameCountMin = qMin(frameCountMin, frameCountMax);
|
frameCountMin = qMin(frameCountMin, frameCountMax);
|
||||||
|
|
||||||
// Clamp framesLeft to frameCountMin to ensure that we never write more than one sample.
|
|
||||||
// This makes sure that we never increase our latency beyond what the sink is consuming.
|
|
||||||
framesLeft = qMin(framesLeft, frameCountMin);
|
|
||||||
|
|
||||||
// Track latency on queueing-based backends
|
// Track latency on queueing-based backends
|
||||||
if (me->m_SoundIo->current_backend != SoundIoBackendCoreAudio && me->m_SoundIo->current_backend != SoundIoBackendJack) {
|
if (me->m_SoundIo->current_backend != SoundIoBackendCoreAudio && me->m_SoundIo->current_backend != SoundIoBackendJack) {
|
||||||
soundio_outstream_get_latency(stream, &me->m_Latency);
|
soundio_outstream_get_latency(stream, &me->m_Latency);
|
||||||
|
|||||||
@@ -54,8 +54,24 @@ SdlInputHandler::SdlInputHandler(StreamingPreferences& prefs, NvComputer*, int s
|
|||||||
// is via normal motion events that are influenced by mouse acceleration).
|
// is via normal motion events that are influenced by mouse acceleration).
|
||||||
// Otherwise, we'll use raw input capture which is straight from the device
|
// Otherwise, we'll use raw input capture which is straight from the device
|
||||||
// without modification by the OS.
|
// without modification by the OS.
|
||||||
SDL_SetHint(SDL_HINT_MOUSE_RELATIVE_MODE_WARP,
|
SDL_SetHintWithPriority(SDL_HINT_MOUSE_RELATIVE_MODE_WARP,
|
||||||
prefs.mouseAcceleration ? "1" : "0");
|
prefs.mouseAcceleration ? "1" : "0",
|
||||||
|
SDL_HINT_OVERRIDE);
|
||||||
|
|
||||||
|
// We must initialize joystick explicitly before gamecontroller in order
|
||||||
|
// to ensure we receive gamecontroller attach events for gamepads where
|
||||||
|
// SDL doesn't have a built-in mapping. By starting joystick first, we
|
||||||
|
// can allow mapping manager to update the mappings before GC attach
|
||||||
|
// events are generated.
|
||||||
|
SDL_assert(!SDL_WasInit(SDL_INIT_JOYSTICK));
|
||||||
|
if (SDL_InitSubSystem(SDL_INIT_JOYSTICK) != 0) {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"SDL_InitSubSystem(SDL_INIT_JOYSTICK) failed: %s",
|
||||||
|
SDL_GetError());
|
||||||
|
}
|
||||||
|
|
||||||
|
MappingManager mappingManager;
|
||||||
|
mappingManager.applyMappings();
|
||||||
|
|
||||||
// We need to reinit this each time, since you only get
|
// We need to reinit this each time, since you only get
|
||||||
// an initial set of gamepad arrival events once per init.
|
// an initial set of gamepad arrival events once per init.
|
||||||
@@ -66,8 +82,12 @@ SdlInputHandler::SdlInputHandler(StreamingPreferences& prefs, NvComputer*, int s
|
|||||||
SDL_GetError());
|
SDL_GetError());
|
||||||
}
|
}
|
||||||
|
|
||||||
MappingManager mappingManager;
|
SDL_assert(!SDL_WasInit(SDL_INIT_HAPTIC));
|
||||||
mappingManager.applyMappings();
|
if (SDL_InitSubSystem(SDL_INIT_HAPTIC) != 0) {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"SDL_InitSubSystem(SDL_INIT_HAPTIC) failed: %s",
|
||||||
|
SDL_GetError());
|
||||||
|
}
|
||||||
|
|
||||||
// Initialize the gamepad mask with currently attached gamepads to avoid
|
// Initialize the gamepad mask with currently attached gamepads to avoid
|
||||||
// causing gamepads to unexpectedly disappear and reappear on the host
|
// causing gamepads to unexpectedly disappear and reappear on the host
|
||||||
@@ -87,6 +107,9 @@ SdlInputHandler::SdlInputHandler(StreamingPreferences& prefs, NvComputer*, int s
|
|||||||
SdlInputHandler::~SdlInputHandler()
|
SdlInputHandler::~SdlInputHandler()
|
||||||
{
|
{
|
||||||
for (int i = 0; i < MAX_GAMEPADS; i++) {
|
for (int i = 0; i < MAX_GAMEPADS; i++) {
|
||||||
|
if (m_GamepadState[i].haptic != nullptr) {
|
||||||
|
SDL_HapticClose(m_GamepadState[i].haptic);
|
||||||
|
}
|
||||||
if (m_GamepadState[i].controller != nullptr) {
|
if (m_GamepadState[i].controller != nullptr) {
|
||||||
SDL_GameControllerClose(m_GamepadState[i].controller);
|
SDL_GameControllerClose(m_GamepadState[i].controller);
|
||||||
}
|
}
|
||||||
@@ -97,8 +120,17 @@ SdlInputHandler::~SdlInputHandler()
|
|||||||
SDL_RemoveTimer(m_RightButtonReleaseTimer);
|
SDL_RemoveTimer(m_RightButtonReleaseTimer);
|
||||||
SDL_RemoveTimer(m_DragTimer);
|
SDL_RemoveTimer(m_DragTimer);
|
||||||
|
|
||||||
|
SDL_QuitSubSystem(SDL_INIT_HAPTIC);
|
||||||
|
SDL_assert(!SDL_WasInit(SDL_INIT_HAPTIC));
|
||||||
|
|
||||||
SDL_QuitSubSystem(SDL_INIT_GAMECONTROLLER);
|
SDL_QuitSubSystem(SDL_INIT_GAMECONTROLLER);
|
||||||
SDL_assert(!SDL_WasInit(SDL_INIT_GAMECONTROLLER));
|
SDL_assert(!SDL_WasInit(SDL_INIT_GAMECONTROLLER));
|
||||||
|
|
||||||
|
SDL_QuitSubSystem(SDL_INIT_JOYSTICK);
|
||||||
|
SDL_assert(!SDL_WasInit(SDL_INIT_JOYSTICK));
|
||||||
|
|
||||||
|
// Return background event handling to off
|
||||||
|
SDL_SetHint(SDL_HINT_JOYSTICK_ALLOW_BACKGROUND_EVENTS, "0");
|
||||||
}
|
}
|
||||||
|
|
||||||
void SdlInputHandler::handleKeyEvent(SDL_KeyboardEvent* event)
|
void SdlInputHandler::handleKeyEvent(SDL_KeyboardEvent* event)
|
||||||
@@ -111,10 +143,21 @@ void SdlInputHandler::handleKeyEvent(SDL_KeyboardEvent* event)
|
|||||||
(event->keysym.mod & KMOD_CTRL) &&
|
(event->keysym.mod & KMOD_CTRL) &&
|
||||||
(event->keysym.mod & KMOD_ALT) &&
|
(event->keysym.mod & KMOD_ALT) &&
|
||||||
(event->keysym.mod & KMOD_SHIFT)) {
|
(event->keysym.mod & KMOD_SHIFT)) {
|
||||||
|
// First we test the SDLK combos for matches,
|
||||||
|
// that way we ensure that latin keyboard users
|
||||||
|
// can match to the key they see on their keyboards.
|
||||||
|
// If nothing matches that, we'll then go on to
|
||||||
|
// checking scancodes so non-latin keyboard users
|
||||||
|
// can have working hotkeys (though possibly in
|
||||||
|
// odd positions). We must do all SDLK tests before
|
||||||
|
// any scancode tests to avoid issues in cases
|
||||||
|
// where the SDLK for one shortcut collides with
|
||||||
|
// the scancode of another.
|
||||||
|
|
||||||
// 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 (SDLK)");
|
||||||
|
|
||||||
// Push a quit event to the main loop
|
// Push a quit event to the main loop
|
||||||
SDL_Event event;
|
SDL_Event event;
|
||||||
@@ -126,7 +169,55 @@ void SdlInputHandler::handleKeyEvent(SDL_KeyboardEvent* event)
|
|||||||
// Check for the unbind combo (Ctrl+Alt+Shift+Z)
|
// Check for the unbind combo (Ctrl+Alt+Shift+Z)
|
||||||
else if (event->keysym.sym == SDLK_z) {
|
else if (event->keysym.sym == SDLK_z) {
|
||||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
"Detected mouse capture toggle combo");
|
"Detected mouse capture toggle combo (SDLK)");
|
||||||
|
|
||||||
|
// Stop handling future input
|
||||||
|
SDL_SetRelativeMouseMode((SDL_bool)!SDL_GetRelativeMouseMode());
|
||||||
|
|
||||||
|
// Force raise all keys to ensure they aren't stuck,
|
||||||
|
// since we won't get their key up events.
|
||||||
|
raiseAllKeys();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
else if (event->keysym.sym == SDLK_x) {
|
||||||
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Detected full-screen toggle combo (SDLK)");
|
||||||
|
Session::s_ActiveSession->toggleFullscreen();
|
||||||
|
|
||||||
|
// Force raise all keys just be safe across this full-screen/windowed
|
||||||
|
// transition just in case key events get lost.
|
||||||
|
raiseAllKeys();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
else if (event->keysym.sym == SDLK_s) {
|
||||||
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Detected stats toggle combo (SDLK)");
|
||||||
|
|
||||||
|
// Toggle the stats overlay
|
||||||
|
Session::get()->getOverlayManager().setOverlayState(Overlay::OverlayDebug,
|
||||||
|
!Session::get()->getOverlayManager().isOverlayEnabled(Overlay::OverlayDebug));
|
||||||
|
|
||||||
|
// Force raise all keys just be safe across this full-screen/windowed
|
||||||
|
// transition just in case key events get lost.
|
||||||
|
raiseAllKeys();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Check for quit combo (Ctrl+Alt+Shift+Q)
|
||||||
|
else if (event->keysym.scancode == SDL_SCANCODE_Q) {
|
||||||
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Detected quit key combo (scancode)");
|
||||||
|
|
||||||
|
// Push a quit event to the main loop
|
||||||
|
SDL_Event event;
|
||||||
|
event.type = SDL_QUIT;
|
||||||
|
event.quit.timestamp = SDL_GetTicks();
|
||||||
|
SDL_PushEvent(&event);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Check for the unbind combo (Ctrl+Alt+Shift+Z)
|
||||||
|
else if (event->keysym.scancode == SDL_SCANCODE_Z) {
|
||||||
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Detected mouse capture toggle combo (scancode)");
|
||||||
|
|
||||||
// Stop handling future input
|
// Stop handling future input
|
||||||
SDL_SetRelativeMouseMode((SDL_bool)!SDL_GetRelativeMouseMode());
|
SDL_SetRelativeMouseMode((SDL_bool)!SDL_GetRelativeMouseMode());
|
||||||
@@ -137,11 +228,24 @@ void SdlInputHandler::handleKeyEvent(SDL_KeyboardEvent* event)
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// Check for the full-screen combo (Ctrl+Alt+Shift+X)
|
// Check for the full-screen combo (Ctrl+Alt+Shift+X)
|
||||||
else if (event->keysym.sym == SDLK_x) {
|
else if (event->keysym.scancode == SDL_SCANCODE_X) {
|
||||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
"Detected full-screen toggle combo");
|
"Detected full-screen toggle combo (scancode)");
|
||||||
Session::s_ActiveSession->toggleFullscreen();
|
Session::s_ActiveSession->toggleFullscreen();
|
||||||
|
|
||||||
|
// Force raise all keys just be safe across this full-screen/windowed
|
||||||
|
// transition just in case key events get lost.
|
||||||
|
raiseAllKeys();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
else if (event->keysym.scancode == SDL_SCANCODE_S) {
|
||||||
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Detected stats toggle combo (scancode)");
|
||||||
|
|
||||||
|
// Toggle the stats overlay
|
||||||
|
Session::get()->getOverlayManager().setOverlayState(Overlay::OverlayDebug,
|
||||||
|
!Session::get()->getOverlayManager().isOverlayEnabled(Overlay::OverlayDebug));
|
||||||
|
|
||||||
// Force raise all keys just be safe across this full-screen/windowed
|
// Force raise all keys just be safe across this full-screen/windowed
|
||||||
// transition just in case key events get lost.
|
// transition just in case key events get lost.
|
||||||
raiseAllKeys();
|
raiseAllKeys();
|
||||||
@@ -641,17 +745,12 @@ void SdlInputHandler::handleControllerDeviceEvent(SDL_ControllerDeviceEvent* eve
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
#if SDL_VERSION_ATLEAST(2, 0, 9)
|
// We used to use SDL_GameControllerGetPlayerIndex() here but that
|
||||||
// Determine this player's preferred index
|
// can lead to strange issues due to bugs in Windows where an Xbox
|
||||||
i = SDL_GameControllerGetPlayerIndex(controller);
|
// controller will join as player 2, even though no player 1 controller
|
||||||
if (i < 0 || i >= MAX_GAMEPADS || m_GamepadState[i].controller != NULL) {
|
// is connected at all. This pretty much screws any attempt to use
|
||||||
// If the player index is unavailable or invalid, start searching from 0
|
// the gamepad in single player games, so just assign them in order from 0.
|
||||||
i = 0;
|
|
||||||
}
|
|
||||||
#else
|
|
||||||
// SDL 2.0.8 and earlier has no player number hints
|
|
||||||
i = 0;
|
i = 0;
|
||||||
#endif
|
|
||||||
|
|
||||||
for (; i < MAX_GAMEPADS; i++) {
|
for (; i < MAX_GAMEPADS; i++) {
|
||||||
if (m_GamepadState[i].controller == NULL) {
|
if (m_GamepadState[i].controller == NULL) {
|
||||||
@@ -678,6 +777,24 @@ void SdlInputHandler::handleControllerDeviceEvent(SDL_ControllerDeviceEvent* eve
|
|||||||
|
|
||||||
state->controller = controller;
|
state->controller = controller;
|
||||||
state->jsId = SDL_JoystickInstanceID(SDL_GameControllerGetJoystick(state->controller));
|
state->jsId = SDL_JoystickInstanceID(SDL_GameControllerGetJoystick(state->controller));
|
||||||
|
state->haptic = SDL_HapticOpenFromJoystick(SDL_GameControllerGetJoystick(state->controller));
|
||||||
|
state->hapticEffectId = -1;
|
||||||
|
state->hapticMethod = GAMEPAD_HAPTIC_METHOD_NONE;
|
||||||
|
if (state->haptic != nullptr) {
|
||||||
|
if ((SDL_HapticQuery(state->haptic) & SDL_HAPTIC_LEFTRIGHT) == 0) {
|
||||||
|
if (SDL_HapticRumbleSupported(state->haptic)) {
|
||||||
|
if (SDL_HapticRumbleInit(state->haptic) == 0) {
|
||||||
|
state->hapticMethod = GAMEPAD_HAPTIC_METHOD_SIMPLERUMBLE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (state->hapticMethod == GAMEPAD_HAPTIC_METHOD_NONE) {
|
||||||
|
SDL_HapticClose(state->haptic);
|
||||||
|
state->haptic = nullptr;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
state->hapticMethod = GAMEPAD_HAPTIC_METHOD_LEFTRIGHT;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
SDL_JoystickGetGUIDString(SDL_JoystickGetGUID(SDL_GameControllerGetJoystick(state->controller)),
|
SDL_JoystickGetGUIDString(SDL_JoystickGetGUID(SDL_GameControllerGetJoystick(state->controller)),
|
||||||
guidStr, sizeof(guidStr));
|
guidStr, sizeof(guidStr));
|
||||||
@@ -712,6 +829,9 @@ void SdlInputHandler::handleControllerDeviceEvent(SDL_ControllerDeviceEvent* eve
|
|||||||
state = findStateForGamepad(event->which);
|
state = findStateForGamepad(event->which);
|
||||||
if (state != NULL) {
|
if (state != NULL) {
|
||||||
SDL_GameControllerClose(state->controller);
|
SDL_GameControllerClose(state->controller);
|
||||||
|
if (state->haptic != nullptr) {
|
||||||
|
SDL_HapticClose(state->haptic);
|
||||||
|
}
|
||||||
|
|
||||||
// Remove this from the gamepad mask in MC-mode
|
// Remove this from the gamepad mask in MC-mode
|
||||||
if (m_MultiController) {
|
if (m_MultiController) {
|
||||||
@@ -765,6 +885,59 @@ void SdlInputHandler::handleJoystickArrivalEvent(SDL_JoyDeviceEvent* event)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void SdlInputHandler::rumble(unsigned short controllerNumber, unsigned short lowFreqMotor, unsigned short highFreqMotor)
|
||||||
|
{
|
||||||
|
// Make sure the controller number is within our supported count
|
||||||
|
if (controllerNumber >= MAX_GAMEPADS) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the controller supports haptics (and if the controller exists at all)
|
||||||
|
SDL_Haptic* haptic = m_GamepadState[controllerNumber].haptic;
|
||||||
|
if (haptic == nullptr) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop the last effect we played
|
||||||
|
if (m_GamepadState[controllerNumber].hapticMethod == GAMEPAD_HAPTIC_METHOD_LEFTRIGHT) {
|
||||||
|
if (m_GamepadState[controllerNumber].hapticEffectId >= 0) {
|
||||||
|
SDL_HapticDestroyEffect(haptic, m_GamepadState[controllerNumber].hapticEffectId);
|
||||||
|
}
|
||||||
|
} else if (m_GamepadState[controllerNumber].hapticMethod == GAMEPAD_HAPTIC_METHOD_SIMPLERUMBLE) {
|
||||||
|
SDL_HapticRumbleStop(haptic);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If this callback is telling us to stop both motors, don't bother queuing a new effect
|
||||||
|
if (lowFreqMotor == 0 && highFreqMotor == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (m_GamepadState[controllerNumber].hapticMethod == GAMEPAD_HAPTIC_METHOD_LEFTRIGHT) {
|
||||||
|
SDL_HapticEffect effect;
|
||||||
|
SDL_memset(&effect, 0, sizeof(effect));
|
||||||
|
effect.type = SDL_HAPTIC_LEFTRIGHT;
|
||||||
|
|
||||||
|
// The effect should last until we are instructed to stop or change it
|
||||||
|
effect.leftright.length = SDL_HAPTIC_INFINITY;
|
||||||
|
|
||||||
|
// SDL haptics range from 0-32767 but XInput uses 0-65535, so divide by 2 to correct for SDL's scaling
|
||||||
|
effect.leftright.large_magnitude = lowFreqMotor / 2;
|
||||||
|
effect.leftright.small_magnitude = highFreqMotor / 2;
|
||||||
|
|
||||||
|
// Play the new effect
|
||||||
|
m_GamepadState[controllerNumber].hapticEffectId = SDL_HapticNewEffect(haptic, &effect);
|
||||||
|
if (m_GamepadState[controllerNumber].hapticEffectId >= 0) {
|
||||||
|
SDL_HapticRunEffect(haptic, m_GamepadState[controllerNumber].hapticEffectId, 1);
|
||||||
|
}
|
||||||
|
} else if (m_GamepadState[controllerNumber].hapticMethod == GAMEPAD_HAPTIC_METHOD_SIMPLERUMBLE) {
|
||||||
|
SDL_HapticRumblePlay(haptic,
|
||||||
|
std::min(1.0, (GAMEPAD_HAPTIC_SIMPLE_HIFREQ_MOTOR_WEIGHT*highFreqMotor +
|
||||||
|
GAMEPAD_HAPTIC_SIMPLE_LOWFREQ_MOTOR_WEIGHT*lowFreqMotor) / 65535.0),
|
||||||
|
SDL_HAPTIC_INFINITY);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
void SdlInputHandler::handleTouchFingerEvent(SDL_TouchFingerEvent* event)
|
void SdlInputHandler::handleTouchFingerEvent(SDL_TouchFingerEvent* event)
|
||||||
{
|
{
|
||||||
int fingerIndex = -1;
|
int fingerIndex = -1;
|
||||||
@@ -910,7 +1083,6 @@ int SdlInputHandler::getAttachedGamepadMask()
|
|||||||
return 0x1;
|
return 0x1;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Use SDL_GameControllerGetPlayerIndex() for accurate indexes?
|
|
||||||
count = mask = 0;
|
count = mask = 0;
|
||||||
for (int i = 0; i < SDL_NumJoysticks(); i++) {
|
for (int i = 0; i < SDL_NumJoysticks(); i++) {
|
||||||
if (SDL_IsGameController(i)) {
|
if (SDL_IsGameController(i)) {
|
||||||
|
|||||||
@@ -8,6 +8,9 @@
|
|||||||
struct GamepadState {
|
struct GamepadState {
|
||||||
SDL_GameController* controller;
|
SDL_GameController* controller;
|
||||||
SDL_JoystickID jsId;
|
SDL_JoystickID jsId;
|
||||||
|
SDL_Haptic* haptic;
|
||||||
|
int hapticMethod;
|
||||||
|
int hapticEffectId;
|
||||||
short index;
|
short index;
|
||||||
|
|
||||||
short buttons;
|
short buttons;
|
||||||
@@ -19,6 +22,13 @@ struct GamepadState {
|
|||||||
#define MAX_GAMEPADS 4
|
#define MAX_GAMEPADS 4
|
||||||
#define MAX_FINGERS 2
|
#define MAX_FINGERS 2
|
||||||
|
|
||||||
|
#define GAMEPAD_HAPTIC_METHOD_NONE 0
|
||||||
|
#define GAMEPAD_HAPTIC_METHOD_LEFTRIGHT 1
|
||||||
|
#define GAMEPAD_HAPTIC_METHOD_SIMPLERUMBLE 2
|
||||||
|
|
||||||
|
#define GAMEPAD_HAPTIC_SIMPLE_HIFREQ_MOTOR_WEIGHT 0.33
|
||||||
|
#define GAMEPAD_HAPTIC_SIMPLE_LOWFREQ_MOTOR_WEIGHT 0.8
|
||||||
|
|
||||||
class SdlInputHandler
|
class SdlInputHandler
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
@@ -43,6 +53,8 @@ public:
|
|||||||
|
|
||||||
void handleJoystickArrivalEvent(SDL_JoyDeviceEvent* event);
|
void handleJoystickArrivalEvent(SDL_JoyDeviceEvent* event);
|
||||||
|
|
||||||
|
void rumble(unsigned short controllerNumber, unsigned short lowFreqMotor, unsigned short highFreqMotor);
|
||||||
|
|
||||||
void handleTouchFingerEvent(SDL_TouchFingerEvent* event);
|
void handleTouchFingerEvent(SDL_TouchFingerEvent* event);
|
||||||
|
|
||||||
int getAttachedGamepadMask();
|
int getAttachedGamepadMask();
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
#ifdef HAVE_SLVIDEO
|
#ifdef HAVE_SLVIDEO
|
||||||
#include "video/sl.h"
|
#include "video/slvid.h"
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#ifdef Q_OS_WIN32
|
#ifdef Q_OS_WIN32
|
||||||
@@ -38,7 +38,9 @@ CONNECTION_LISTENER_CALLBACKS Session::k_ConnCallbacks = {
|
|||||||
Session::clConnectionTerminated,
|
Session::clConnectionTerminated,
|
||||||
nullptr,
|
nullptr,
|
||||||
nullptr,
|
nullptr,
|
||||||
Session::clLogMessage
|
Session::clLogMessage,
|
||||||
|
Session::clRumble,
|
||||||
|
Session::clConnectionStatusUpdate
|
||||||
};
|
};
|
||||||
|
|
||||||
AUDIO_RENDERER_CALLBACKS Session::k_AudioCallbacks = {
|
AUDIO_RENDERER_CALLBACKS Session::k_AudioCallbacks = {
|
||||||
@@ -73,8 +75,11 @@ void Session::clStageFailed(int stage, long errorCode)
|
|||||||
|
|
||||||
void Session::clConnectionTerminated(long errorCode)
|
void Session::clConnectionTerminated(long errorCode)
|
||||||
{
|
{
|
||||||
s_ActiveSession->m_UnexpectedTermination = true;
|
// Display the termination dialog if this was not intended
|
||||||
emit s_ActiveSession->displayLaunchError("Connection terminated");
|
if (errorCode != 0) {
|
||||||
|
s_ActiveSession->m_UnexpectedTermination = true;
|
||||||
|
emit s_ActiveSession->displayLaunchError("Connection terminated");
|
||||||
|
}
|
||||||
|
|
||||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
"Connection terminated: %ld",
|
"Connection terminated: %ld",
|
||||||
@@ -99,18 +104,57 @@ void Session::clLogMessage(const char* format, ...)
|
|||||||
va_end(ap);
|
va_end(ap);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void Session::clRumble(unsigned short controllerNumber, unsigned short lowFreqMotor, unsigned short highFreqMotor)
|
||||||
|
{
|
||||||
|
// The input handler can be closed during the stream if LiStopConnection() hasn't completed yet
|
||||||
|
// but the stream has been stopped by the user. In this case, just discard the rumble.
|
||||||
|
SDL_AtomicLock(&s_ActiveSession->m_InputHandlerLock);
|
||||||
|
if (s_ActiveSession->m_InputHandler != nullptr) {
|
||||||
|
s_ActiveSession->m_InputHandler->rumble(controllerNumber, lowFreqMotor, highFreqMotor);
|
||||||
|
}
|
||||||
|
SDL_AtomicUnlock(&s_ActiveSession->m_InputHandlerLock);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Session::clConnectionStatusUpdate(int connectionStatus)
|
||||||
|
{
|
||||||
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Connection status update: %d",
|
||||||
|
connectionStatus);
|
||||||
|
|
||||||
|
if (!s_ActiveSession->m_Preferences->connectionWarnings) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (connectionStatus)
|
||||||
|
{
|
||||||
|
case CONN_STATUS_POOR:
|
||||||
|
if (s_ActiveSession->m_StreamConfig.bitrate > 5000) {
|
||||||
|
strcpy(s_ActiveSession->m_OverlayManager.getOverlayText(Overlay::OverlayStatusUpdate), "Slow connection to PC\nReduce your bitrate");
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
strcpy(s_ActiveSession->m_OverlayManager.getOverlayText(Overlay::OverlayStatusUpdate), "Poor connection to PC");
|
||||||
|
}
|
||||||
|
s_ActiveSession->m_OverlayManager.setOverlayTextUpdated(Overlay::OverlayStatusUpdate);
|
||||||
|
s_ActiveSession->m_OverlayManager.setOverlayState(Overlay::OverlayStatusUpdate, true);
|
||||||
|
break;
|
||||||
|
case CONN_STATUS_OKAY:
|
||||||
|
s_ActiveSession->m_OverlayManager.setOverlayState(Overlay::OverlayStatusUpdate, false);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#define CALL_INITIALIZE(dec) (dec)->initialize(vds, window, videoFormat, width, height, frameRate, enableVsync, enableFramePacing)
|
#define CALL_INITIALIZE(dec) (dec)->initialize(vds, window, videoFormat, width, height, frameRate, enableVsync, enableFramePacing)
|
||||||
|
|
||||||
bool Session::chooseDecoder(StreamingPreferences::VideoDecoderSelection vds,
|
bool Session::chooseDecoder(StreamingPreferences::VideoDecoderSelection vds,
|
||||||
SDL_Window* window, int videoFormat, int width, int height,
|
SDL_Window* window, int videoFormat, int width, int height,
|
||||||
int frameRate, bool enableVsync, bool enableFramePacing, IVideoDecoder*& chosenDecoder)
|
int frameRate, bool enableVsync, bool enableFramePacing, bool testOnly, IVideoDecoder*& chosenDecoder)
|
||||||
{
|
{
|
||||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
"V-sync %s",
|
"V-sync %s",
|
||||||
enableVsync ? "enabled" : "disabled");
|
enableVsync ? "enabled" : "disabled");
|
||||||
|
|
||||||
#ifdef HAVE_SLVIDEO
|
#ifdef HAVE_SLVIDEO
|
||||||
chosenDecoder = new SLVideoDecoder();
|
chosenDecoder = new SLVideoDecoder(testOnly);
|
||||||
if (CALL_INITIALIZE(chosenDecoder)) {
|
if (CALL_INITIALIZE(chosenDecoder)) {
|
||||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
"SLVideo video decoder chosen");
|
"SLVideo video decoder chosen");
|
||||||
@@ -125,7 +169,7 @@ bool Session::chooseDecoder(StreamingPreferences::VideoDecoderSelection vds,
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
#ifdef HAVE_FFMPEG
|
#ifdef HAVE_FFMPEG
|
||||||
chosenDecoder = new FFmpegVideoDecoder();
|
chosenDecoder = new FFmpegVideoDecoder(testOnly);
|
||||||
if (CALL_INITIALIZE(chosenDecoder)) {
|
if (CALL_INITIALIZE(chosenDecoder)) {
|
||||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
"FFmpeg-based video decoder chosen");
|
"FFmpeg-based video decoder chosen");
|
||||||
@@ -200,30 +244,13 @@ int Session::drSubmitDecodeUnit(PDECODE_UNIT du)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bool Session::isHardwareDecodeAvailable(StreamingPreferences::VideoDecoderSelection vds,
|
bool Session::isHardwareDecodeAvailable(SDL_Window* window,
|
||||||
|
StreamingPreferences::VideoDecoderSelection vds,
|
||||||
int videoFormat, int width, int height, int frameRate)
|
int videoFormat, int width, int height, int frameRate)
|
||||||
{
|
{
|
||||||
IVideoDecoder* decoder;
|
IVideoDecoder* decoder;
|
||||||
|
|
||||||
if (SDL_InitSubSystem(SDL_INIT_VIDEO) != 0) {
|
if (!chooseDecoder(vds, window, videoFormat, width, height, frameRate, true, false, true, decoder)) {
|
||||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
|
||||||
"SDL_InitSubSystem(SDL_INIT_VIDEO) failed: %s",
|
|
||||||
SDL_GetError());
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
SDL_Window* window = SDL_CreateWindow("", 0, 0, width, height, SDL_WINDOW_HIDDEN);
|
|
||||||
if (!window) {
|
|
||||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
|
||||||
"Failed to create window for hardware decode test: %s",
|
|
||||||
SDL_GetError());
|
|
||||||
SDL_QuitSubSystem(SDL_INIT_VIDEO);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!chooseDecoder(vds, window, videoFormat, width, height, frameRate, true, false, decoder)) {
|
|
||||||
SDL_DestroyWindow(window);
|
|
||||||
SDL_QuitSubSystem(SDL_INIT_VIDEO);
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -231,39 +258,16 @@ bool Session::isHardwareDecodeAvailable(StreamingPreferences::VideoDecoderSelect
|
|||||||
|
|
||||||
delete decoder;
|
delete decoder;
|
||||||
|
|
||||||
// This must be called after the decoder is deleted, because
|
|
||||||
// the renderer may want to interact with the window
|
|
||||||
SDL_DestroyWindow(window);
|
|
||||||
|
|
||||||
SDL_QuitSubSystem(SDL_INIT_VIDEO);
|
|
||||||
|
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
int Session::getDecoderCapabilities(StreamingPreferences::VideoDecoderSelection vds,
|
int Session::getDecoderCapabilities(SDL_Window* window,
|
||||||
|
StreamingPreferences::VideoDecoderSelection vds,
|
||||||
int videoFormat, int width, int height, int frameRate)
|
int videoFormat, int width, int height, int frameRate)
|
||||||
{
|
{
|
||||||
IVideoDecoder* decoder;
|
IVideoDecoder* decoder;
|
||||||
|
|
||||||
if (SDL_InitSubSystem(SDL_INIT_VIDEO) != 0) {
|
if (!chooseDecoder(vds, window, videoFormat, width, height, frameRate, true, false, true, decoder)) {
|
||||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
|
||||||
"SDL_InitSubSystem(SDL_INIT_VIDEO) failed: %s",
|
|
||||||
SDL_GetError());
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
SDL_Window* window = SDL_CreateWindow("", 0, 0, width, height, SDL_WINDOW_HIDDEN);
|
|
||||||
if (!window) {
|
|
||||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
|
||||||
"Failed to create window for hardware decode test: %s",
|
|
||||||
SDL_GetError());
|
|
||||||
SDL_QuitSubSystem(SDL_INIT_VIDEO);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!chooseDecoder(vds, window, videoFormat, width, height, frameRate, true, false, decoder)) {
|
|
||||||
SDL_DestroyWindow(window);
|
|
||||||
SDL_QuitSubSystem(SDL_INIT_VIDEO);
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -271,12 +275,6 @@ int Session::getDecoderCapabilities(StreamingPreferences::VideoDecoderSelection
|
|||||||
|
|
||||||
delete decoder;
|
delete decoder;
|
||||||
|
|
||||||
// This must be called after the decoder is deleted, because
|
|
||||||
// the renderer may want to interact with the window
|
|
||||||
SDL_DestroyWindow(window);
|
|
||||||
|
|
||||||
SDL_QuitSubSystem(SDL_INIT_VIDEO);
|
|
||||||
|
|
||||||
return caps;
|
return caps;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -293,12 +291,17 @@ Session::Session(NvComputer* computer, NvApp& app, StreamingPreferences *prefere
|
|||||||
m_DisplayOriginY(0),
|
m_DisplayOriginY(0),
|
||||||
m_PendingWindowedTransition(false),
|
m_PendingWindowedTransition(false),
|
||||||
m_UnexpectedTermination(true), // Failure prior to streaming is unexpected
|
m_UnexpectedTermination(true), // Failure prior to streaming is unexpected
|
||||||
|
m_InputHandler(nullptr),
|
||||||
|
m_InputHandlerLock(0),
|
||||||
m_OpusDecoder(nullptr),
|
m_OpusDecoder(nullptr),
|
||||||
m_AudioRenderer(nullptr),
|
m_AudioRenderer(nullptr),
|
||||||
m_AudioSampleCount(0)
|
m_AudioSampleCount(0),
|
||||||
|
m_DropAudioEndTime(0)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NB: This may not get destroyed for a long time! Don't put any vital cleanup here.
|
||||||
|
// Use Session::exec() or DeferredSessionCleanupTask instead.
|
||||||
Session::~Session()
|
Session::~Session()
|
||||||
{
|
{
|
||||||
// Acquire session semaphore to ensure all cleanup is done before the destructor returns
|
// Acquire session semaphore to ensure all cleanup is done before the destructor returns
|
||||||
@@ -307,8 +310,17 @@ Session::~Session()
|
|||||||
s_ActiveSessionSemaphore.release();
|
s_ActiveSessionSemaphore.release();
|
||||||
}
|
}
|
||||||
|
|
||||||
void Session::initialize()
|
bool Session::initialize()
|
||||||
{
|
{
|
||||||
|
// Create a hidden window to use for decoder initialization tests
|
||||||
|
SDL_Window* testWindow = SDL_CreateWindow("", 0, 0, 1280, 720, SDL_WINDOW_HIDDEN);
|
||||||
|
if (!testWindow) {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Failed to create window for hardware decode test: %s",
|
||||||
|
SDL_GetError());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
qInfo() << "Server GPU:" << m_Computer->gpuModel;
|
qInfo() << "Server GPU:" << m_Computer->gpuModel;
|
||||||
qInfo() << "Server GFE version:" << m_Computer->gfeVersion;
|
qInfo() << "Server GFE version:" << m_Computer->gfeVersion;
|
||||||
|
|
||||||
@@ -361,7 +373,8 @@ void Session::initialize()
|
|||||||
case StreamingPreferences::VCC_AUTO:
|
case StreamingPreferences::VCC_AUTO:
|
||||||
// TODO: Determine if HEVC is better depending on the decoder
|
// TODO: Determine if HEVC is better depending on the decoder
|
||||||
m_StreamConfig.supportsHevc =
|
m_StreamConfig.supportsHevc =
|
||||||
isHardwareDecodeAvailable(m_Preferences->videoDecoderSelection,
|
isHardwareDecodeAvailable(testWindow,
|
||||||
|
m_Preferences->videoDecoderSelection,
|
||||||
VIDEO_FORMAT_H265,
|
VIDEO_FORMAT_H265,
|
||||||
m_StreamConfig.width,
|
m_StreamConfig.width,
|
||||||
m_StreamConfig.height,
|
m_StreamConfig.height,
|
||||||
@@ -398,6 +411,15 @@ void Session::initialize()
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add the capability flags from the chosen decoder/renderer
|
||||||
|
// Requires m_StreamConfig.supportsHevc to be initialized
|
||||||
|
m_VideoCallbacks.capabilities |= getDecoderCapabilities(testWindow,
|
||||||
|
m_Preferences->videoDecoderSelection,
|
||||||
|
m_StreamConfig.supportsHevc ? VIDEO_FORMAT_H265 : VIDEO_FORMAT_H264,
|
||||||
|
m_StreamConfig.width,
|
||||||
|
m_StreamConfig.height,
|
||||||
|
m_StreamConfig.fps);
|
||||||
|
|
||||||
switch (m_Preferences->windowMode)
|
switch (m_Preferences->windowMode)
|
||||||
{
|
{
|
||||||
case StreamingPreferences::WM_FULLSCREEN_DESKTOP:
|
case StreamingPreferences::WM_FULLSCREEN_DESKTOP:
|
||||||
@@ -408,6 +430,14 @@ void Session::initialize()
|
|||||||
m_FullScreenFlag = SDL_WINDOW_FULLSCREEN;
|
m_FullScreenFlag = SDL_WINDOW_FULLSCREEN;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check for validation errors/warnings and emit
|
||||||
|
// signals for them, if appropriate
|
||||||
|
bool ret = validateLaunch(testWindow);
|
||||||
|
|
||||||
|
SDL_DestroyWindow(testWindow);
|
||||||
|
|
||||||
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
void Session::emitLaunchWarning(QString text)
|
void Session::emitLaunchWarning(QString text)
|
||||||
@@ -426,7 +456,7 @@ void Session::emitLaunchWarning(QString text)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bool Session::validateLaunch()
|
bool Session::validateLaunch(SDL_Window* testWindow)
|
||||||
{
|
{
|
||||||
QStringList warningList;
|
QStringList warningList;
|
||||||
|
|
||||||
@@ -446,7 +476,8 @@ bool Session::validateLaunch()
|
|||||||
bool hevcForced = 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 (!isHardwareDecodeAvailable(m_Preferences->videoDecoderSelection,
|
if (!isHardwareDecodeAvailable(testWindow,
|
||||||
|
m_Preferences->videoDecoderSelection,
|
||||||
VIDEO_FORMAT_H265,
|
VIDEO_FORMAT_H265,
|
||||||
m_StreamConfig.width,
|
m_StreamConfig.width,
|
||||||
m_StreamConfig.height,
|
m_StreamConfig.height,
|
||||||
@@ -487,7 +518,8 @@ bool Session::validateLaunch()
|
|||||||
emitLaunchWarning("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(testWindow,
|
||||||
|
m_Preferences->videoDecoderSelection,
|
||||||
VIDEO_FORMAT_H265_MAIN10,
|
VIDEO_FORMAT_H265_MAIN10,
|
||||||
m_StreamConfig.width,
|
m_StreamConfig.width,
|
||||||
m_StreamConfig.height,
|
m_StreamConfig.height,
|
||||||
@@ -536,7 +568,8 @@ bool Session::validateLaunch()
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (m_Preferences->videoDecoderSelection == StreamingPreferences::VDS_FORCE_HARDWARE &&
|
if (m_Preferences->videoDecoderSelection == StreamingPreferences::VDS_FORCE_HARDWARE &&
|
||||||
!isHardwareDecodeAvailable(m_Preferences->videoDecoderSelection,
|
!isHardwareDecodeAvailable(testWindow,
|
||||||
|
m_Preferences->videoDecoderSelection,
|
||||||
m_StreamConfig.supportsHevc ? VIDEO_FORMAT_H265 : VIDEO_FORMAT_H264,
|
m_StreamConfig.supportsHevc ? VIDEO_FORMAT_H265 : VIDEO_FORMAT_H264,
|
||||||
m_StreamConfig.width,
|
m_StreamConfig.width,
|
||||||
m_StreamConfig.height,
|
m_StreamConfig.height,
|
||||||
@@ -552,13 +585,6 @@ bool Session::validateLaunch()
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add the capability flags from the chosen decoder/renderer
|
|
||||||
m_VideoCallbacks.capabilities |= getDecoderCapabilities(m_Preferences->videoDecoderSelection,
|
|
||||||
m_StreamConfig.supportsHevc ? VIDEO_FORMAT_H265 : VIDEO_FORMAT_H264,
|
|
||||||
m_StreamConfig.width,
|
|
||||||
m_StreamConfig.height,
|
|
||||||
m_StreamConfig.fps);
|
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -780,11 +806,7 @@ void Session::exec(int displayOriginX, int displayOriginY)
|
|||||||
// Complete initialization in this deferred context to avoid
|
// Complete initialization in this deferred context to avoid
|
||||||
// calling expensive functions in the constructor (during the
|
// calling expensive functions in the constructor (during the
|
||||||
// process of loading the StreamSegue).
|
// process of loading the StreamSegue).
|
||||||
initialize();
|
if (!initialize()) {
|
||||||
|
|
||||||
// Check for validation errors/warnings and emit
|
|
||||||
// signals for them, if appropriate
|
|
||||||
if (!validateLaunch()) {
|
|
||||||
emit sessionFinished();
|
emit sessionFinished();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -806,15 +828,30 @@ void Session::exec(int displayOriginX, int displayOriginY)
|
|||||||
|
|
||||||
// Initialize the gamepad code with our preferences
|
// Initialize the gamepad code with our preferences
|
||||||
StreamingPreferences prefs;
|
StreamingPreferences prefs;
|
||||||
SdlInputHandler inputHandler(prefs, m_Computer,
|
m_InputHandler = new SdlInputHandler(prefs, m_Computer,
|
||||||
m_StreamConfig.width,
|
m_StreamConfig.width,
|
||||||
m_StreamConfig.height);
|
m_StreamConfig.height);
|
||||||
|
|
||||||
// The UI should have ensured the old game was already quit
|
// The UI should have ensured the old game was already quit
|
||||||
// if we decide to stream a different game.
|
// if we decide to stream a different game.
|
||||||
Q_ASSERT(m_Computer->currentGameId == 0 ||
|
Q_ASSERT(m_Computer->currentGameId == 0 ||
|
||||||
m_Computer->currentGameId == m_App.id);
|
m_Computer->currentGameId == m_App.id);
|
||||||
|
|
||||||
|
// SOPS will set all settings to 720p60 if it doesn't recognize
|
||||||
|
// the chosen resolution. Avoid that by disabling SOPS when it
|
||||||
|
// is not streaming a supported resolution.
|
||||||
|
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, m_Computer->serverCert);
|
NvHTTP http(m_Computer->activeAddress, m_Computer->serverCert);
|
||||||
if (m_Computer->currentGameId != 0) {
|
if (m_Computer->currentGameId != 0) {
|
||||||
@@ -822,30 +859,24 @@ void Session::exec(int displayOriginX, int displayOriginY)
|
|||||||
}
|
}
|
||||||
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());
|
m_InputHandler->getAttachedGamepadMask());
|
||||||
}
|
}
|
||||||
} catch (const GfeHttpResponseException& e) {
|
} catch (const GfeHttpResponseException& e) {
|
||||||
emit displayLaunchError(e.toQString());
|
delete m_InputHandler;
|
||||||
|
m_InputHandler = nullptr;
|
||||||
|
emit displayLaunchError("GeForce Experience returned error: " + e.toQString());
|
||||||
QThreadPool::globalInstance()->start(new DeferredSessionCleanupTask(this));
|
QThreadPool::globalInstance()->start(new DeferredSessionCleanupTask(this));
|
||||||
return;
|
return;
|
||||||
} catch (const QtNetworkReplyException& e) {
|
} catch (const QtNetworkReplyException& e) {
|
||||||
|
delete m_InputHandler;
|
||||||
|
m_InputHandler = nullptr;
|
||||||
emit displayLaunchError(e.toQString());
|
emit displayLaunchError(e.toQString());
|
||||||
QThreadPool::globalInstance()->start(new DeferredSessionCleanupTask(this));
|
QThreadPool::globalInstance()->start(new DeferredSessionCleanupTask(this));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
SDL_assert(!SDL_WasInit(SDL_INIT_VIDEO));
|
|
||||||
if (SDL_InitSubSystem(SDL_INIT_VIDEO) != 0) {
|
|
||||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
|
||||||
"SDL_InitSubSystem(SDL_INIT_VIDEO) failed: %s",
|
|
||||||
SDL_GetError());
|
|
||||||
emit displayLaunchError(QString::fromLocal8Bit(SDL_GetError()));
|
|
||||||
QThreadPool::globalInstance()->start(new DeferredSessionCleanupTask(this));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
QByteArray hostnameStr = m_Computer->activeAddress.toLatin1();
|
QByteArray hostnameStr = m_Computer->activeAddress.toLatin1();
|
||||||
QByteArray siAppVersion = m_Computer->appVersion.toLatin1();
|
QByteArray siAppVersion = m_Computer->appVersion.toLatin1();
|
||||||
|
|
||||||
@@ -869,7 +900,8 @@ void Session::exec(int displayOriginX, int displayOriginY)
|
|||||||
if (err != 0) {
|
if (err != 0) {
|
||||||
// We already displayed an error dialog in the stage failure
|
// We already displayed an error dialog in the stage failure
|
||||||
// listener.
|
// listener.
|
||||||
SDL_QuitSubSystem(SDL_INIT_VIDEO);
|
delete m_InputHandler;
|
||||||
|
m_InputHandler = nullptr;
|
||||||
QThreadPool::globalInstance()->start(new DeferredSessionCleanupTask(this));
|
QThreadPool::globalInstance()->start(new DeferredSessionCleanupTask(this));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -891,7 +923,8 @@ void Session::exec(int displayOriginX, int displayOriginY)
|
|||||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
"SDL_CreateWindow() failed: %s",
|
"SDL_CreateWindow() failed: %s",
|
||||||
SDL_GetError());
|
SDL_GetError());
|
||||||
SDL_QuitSubSystem(SDL_INIT_VIDEO);
|
delete m_InputHandler;
|
||||||
|
m_InputHandler = nullptr;
|
||||||
QThreadPool::globalInstance()->start(new DeferredSessionCleanupTask(this));
|
QThreadPool::globalInstance()->start(new DeferredSessionCleanupTask(this));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -962,14 +995,6 @@ void Session::exec(int displayOriginX, int displayOriginY)
|
|||||||
// sleep precision and more accurate callback timing.
|
// sleep precision and more accurate callback timing.
|
||||||
SDL_SetHint(SDL_HINT_TIMER_RESOLUTION, "1");
|
SDL_SetHint(SDL_HINT_TIMER_RESOLUTION, "1");
|
||||||
|
|
||||||
// Raise the priority of the main thread, since it handles
|
|
||||||
// time-sensitive video rendering
|
|
||||||
if (SDL_SetThreadPriority(SDL_THREAD_PRIORITY_HIGH) < 0) {
|
|
||||||
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
|
||||||
"Unable to set main thread to high priority: %s",
|
|
||||||
SDL_GetError());
|
|
||||||
}
|
|
||||||
|
|
||||||
int currentDisplayIndex = SDL_GetWindowDisplayIndex(m_Window);
|
int currentDisplayIndex = SDL_GetWindowDisplayIndex(m_Window);
|
||||||
|
|
||||||
// Now that we're about to stream, any SDL_QUIT event is expected
|
// Now that we're about to stream, any SDL_QUIT event is expected
|
||||||
@@ -1052,7 +1077,7 @@ void Session::exec(int displayOriginX, int displayOriginY)
|
|||||||
|
|
||||||
// Raise all keys that are currently pressed. If we don't do this, certain keys
|
// Raise all keys that are currently pressed. If we don't do this, certain keys
|
||||||
// used in shortcuts that cause focus loss (such as Alt+Tab) may get stuck down.
|
// used in shortcuts that cause focus loss (such as Alt+Tab) may get stuck down.
|
||||||
inputHandler.raiseAllKeys();
|
m_InputHandler->raiseAllKeys();
|
||||||
}
|
}
|
||||||
|
|
||||||
// We want to recreate the decoder for resizes (full-screen toggles) and the initial shown event.
|
// We want to recreate the decoder for resizes (full-screen toggles) and the initial shown event.
|
||||||
@@ -1125,6 +1150,7 @@ void Session::exec(int displayOriginX, int displayOriginY)
|
|||||||
m_ActiveVideoHeight, m_ActiveVideoFrameRate,
|
m_ActiveVideoHeight, m_ActiveVideoFrameRate,
|
||||||
enableVsync,
|
enableVsync,
|
||||||
enableVsync && m_Preferences->framePacing,
|
enableVsync && m_Preferences->framePacing,
|
||||||
|
false,
|
||||||
s_ActiveSession->m_VideoDecoder)) {
|
s_ActiveSession->m_VideoDecoder)) {
|
||||||
SDL_AtomicUnlock(&m_DecoderLock);
|
SDL_AtomicUnlock(&m_DecoderLock);
|
||||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
@@ -1142,31 +1168,31 @@ void Session::exec(int displayOriginX, int displayOriginY)
|
|||||||
|
|
||||||
case SDL_KEYUP:
|
case SDL_KEYUP:
|
||||||
case SDL_KEYDOWN:
|
case SDL_KEYDOWN:
|
||||||
inputHandler.handleKeyEvent(&event.key);
|
m_InputHandler->handleKeyEvent(&event.key);
|
||||||
break;
|
break;
|
||||||
case SDL_MOUSEBUTTONDOWN:
|
case SDL_MOUSEBUTTONDOWN:
|
||||||
case SDL_MOUSEBUTTONUP:
|
case SDL_MOUSEBUTTONUP:
|
||||||
inputHandler.handleMouseButtonEvent(&event.button);
|
m_InputHandler->handleMouseButtonEvent(&event.button);
|
||||||
break;
|
break;
|
||||||
case SDL_MOUSEMOTION:
|
case SDL_MOUSEMOTION:
|
||||||
inputHandler.handleMouseMotionEvent(&event.motion);
|
m_InputHandler->handleMouseMotionEvent(&event.motion);
|
||||||
break;
|
break;
|
||||||
case SDL_MOUSEWHEEL:
|
case SDL_MOUSEWHEEL:
|
||||||
inputHandler.handleMouseWheelEvent(&event.wheel);
|
m_InputHandler->handleMouseWheelEvent(&event.wheel);
|
||||||
break;
|
break;
|
||||||
case SDL_CONTROLLERAXISMOTION:
|
case SDL_CONTROLLERAXISMOTION:
|
||||||
inputHandler.handleControllerAxisEvent(&event.caxis);
|
m_InputHandler->handleControllerAxisEvent(&event.caxis);
|
||||||
break;
|
break;
|
||||||
case SDL_CONTROLLERBUTTONDOWN:
|
case SDL_CONTROLLERBUTTONDOWN:
|
||||||
case SDL_CONTROLLERBUTTONUP:
|
case SDL_CONTROLLERBUTTONUP:
|
||||||
inputHandler.handleControllerButtonEvent(&event.cbutton);
|
m_InputHandler->handleControllerButtonEvent(&event.cbutton);
|
||||||
break;
|
break;
|
||||||
case SDL_CONTROLLERDEVICEADDED:
|
case SDL_CONTROLLERDEVICEADDED:
|
||||||
case SDL_CONTROLLERDEVICEREMOVED:
|
case SDL_CONTROLLERDEVICEREMOVED:
|
||||||
inputHandler.handleControllerDeviceEvent(&event.cdevice);
|
m_InputHandler->handleControllerDeviceEvent(&event.cdevice);
|
||||||
break;
|
break;
|
||||||
case SDL_JOYDEVICEADDED:
|
case SDL_JOYDEVICEADDED:
|
||||||
inputHandler.handleJoystickArrivalEvent(&event.jdevice);
|
m_InputHandler->handleJoystickArrivalEvent(&event.jdevice);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
// SDL2 sends touch events from trackpads by default on
|
// SDL2 sends touch events from trackpads by default on
|
||||||
@@ -1176,7 +1202,7 @@ void Session::exec(int displayOriginX, int displayOriginY)
|
|||||||
case SDL_FINGERDOWN:
|
case SDL_FINGERDOWN:
|
||||||
case SDL_FINGERMOTION:
|
case SDL_FINGERMOTION:
|
||||||
case SDL_FINGERUP:
|
case SDL_FINGERUP:
|
||||||
inputHandler.handleTouchFingerEvent(&event.tfinger);
|
m_InputHandler->handleTouchFingerEvent(&event.tfinger);
|
||||||
break;
|
break;
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
@@ -1190,7 +1216,16 @@ DispatchDeferredCleanup:
|
|||||||
SDL_SetHint(SDL_HINT_TIMER_RESOLUTION, "0");
|
SDL_SetHint(SDL_HINT_TIMER_RESOLUTION, "0");
|
||||||
|
|
||||||
// Raise any keys that are still down
|
// Raise any keys that are still down
|
||||||
inputHandler.raiseAllKeys();
|
m_InputHandler->raiseAllKeys();
|
||||||
|
|
||||||
|
// Destroy the input handler now. Any rumble callbacks that
|
||||||
|
// occur after this point will be discarded. This must be destroyed
|
||||||
|
// before allow the UI to continue execution or it could interfere
|
||||||
|
// with SDLGamepadKeyNavigation.
|
||||||
|
SDL_AtomicLock(&m_InputHandlerLock);
|
||||||
|
delete m_InputHandler;
|
||||||
|
m_InputHandler = nullptr;
|
||||||
|
SDL_AtomicUnlock(&m_InputHandlerLock);
|
||||||
|
|
||||||
// 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);
|
||||||
@@ -1205,8 +1240,6 @@ DispatchDeferredCleanup:
|
|||||||
if (iconSurface != nullptr) {
|
if (iconSurface != nullptr) {
|
||||||
SDL_FreeSurface(iconSurface);
|
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
|
||||||
|
|||||||
@@ -4,11 +4,11 @@
|
|||||||
|
|
||||||
#include <Limelight.h>
|
#include <Limelight.h>
|
||||||
#include <opus_multistream.h>
|
#include <opus_multistream.h>
|
||||||
#include "backend/computermanager.h"
|
|
||||||
#include "settings/streamingpreferences.h"
|
#include "settings/streamingpreferences.h"
|
||||||
#include "input.h"
|
#include "input.h"
|
||||||
#include "video/decoder.h"
|
#include "video/decoder.h"
|
||||||
#include "audio/renderers/renderer.h"
|
#include "audio/renderers/renderer.h"
|
||||||
|
#include "video/overlaymanager.h"
|
||||||
|
|
||||||
class Session : public QObject
|
class Session : public QObject
|
||||||
{
|
{
|
||||||
@@ -25,12 +25,19 @@ public:
|
|||||||
Q_INVOKABLE void exec(int displayOriginX, int displayOriginY);
|
Q_INVOKABLE void exec(int displayOriginX, int displayOriginY);
|
||||||
|
|
||||||
static
|
static
|
||||||
bool isHardwareDecodeAvailable(StreamingPreferences::VideoDecoderSelection vds,
|
bool isHardwareDecodeAvailable(SDL_Window* window,
|
||||||
|
StreamingPreferences::VideoDecoderSelection vds,
|
||||||
int videoFormat, int width, int height, int frameRate);
|
int videoFormat, int width, int height, int frameRate);
|
||||||
|
|
||||||
static
|
static Session* get()
|
||||||
int getDecoderCapabilities(StreamingPreferences::VideoDecoderSelection vds,
|
{
|
||||||
int videoFormat, int width, int height, int frameRate);
|
return s_ActiveSession;
|
||||||
|
}
|
||||||
|
|
||||||
|
Overlay::OverlayManager& getOverlayManager()
|
||||||
|
{
|
||||||
|
return m_OverlayManager;
|
||||||
|
}
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
void stageStarting(QString stage);
|
void stageStarting(QString stage);
|
||||||
@@ -48,13 +55,16 @@ signals:
|
|||||||
void sessionFinished();
|
void sessionFinished();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void initialize();
|
bool initialize();
|
||||||
|
|
||||||
bool validateLaunch();
|
bool validateLaunch(SDL_Window* testWindow);
|
||||||
|
|
||||||
void emitLaunchWarning(QString text);
|
void emitLaunchWarning(QString text);
|
||||||
|
|
||||||
int getDecoderCapabilities();
|
static
|
||||||
|
int getDecoderCapabilities(SDL_Window* window,
|
||||||
|
StreamingPreferences::VideoDecoderSelection vds,
|
||||||
|
int videoFormat, int width, int height, int frameRate);
|
||||||
|
|
||||||
IAudioRenderer* createAudioRenderer();
|
IAudioRenderer* createAudioRenderer();
|
||||||
|
|
||||||
@@ -73,6 +83,7 @@ private:
|
|||||||
bool chooseDecoder(StreamingPreferences::VideoDecoderSelection vds,
|
bool chooseDecoder(StreamingPreferences::VideoDecoderSelection vds,
|
||||||
SDL_Window* window, int videoFormat, int width, int height,
|
SDL_Window* window, int videoFormat, int width, int height,
|
||||||
int frameRate, bool enableVsync, bool enableFramePacing,
|
int frameRate, bool enableVsync, bool enableFramePacing,
|
||||||
|
bool testOnly,
|
||||||
IVideoDecoder*& chosenDecoder);
|
IVideoDecoder*& chosenDecoder);
|
||||||
|
|
||||||
static
|
static
|
||||||
@@ -87,6 +98,12 @@ private:
|
|||||||
static
|
static
|
||||||
void clLogMessage(const char* format, ...);
|
void clLogMessage(const char* format, ...);
|
||||||
|
|
||||||
|
static
|
||||||
|
void clRumble(unsigned short controllerNumber, unsigned short lowFreqMotor, unsigned short highFreqMotor);
|
||||||
|
|
||||||
|
static
|
||||||
|
void clConnectionStatusUpdate(int connectionStatus);
|
||||||
|
|
||||||
static
|
static
|
||||||
int arInit(int audioConfiguration,
|
int arInit(int audioConfiguration,
|
||||||
const POPUS_MULTISTREAM_CONFIGURATION opusConfig,
|
const POPUS_MULTISTREAM_CONFIGURATION opusConfig,
|
||||||
@@ -122,6 +139,8 @@ private:
|
|||||||
int m_DisplayOriginY;
|
int m_DisplayOriginY;
|
||||||
bool m_PendingWindowedTransition;
|
bool m_PendingWindowedTransition;
|
||||||
bool m_UnexpectedTermination;
|
bool m_UnexpectedTermination;
|
||||||
|
SdlInputHandler* m_InputHandler;
|
||||||
|
SDL_SpinLock m_InputHandlerLock;
|
||||||
|
|
||||||
int m_ActiveVideoFormat;
|
int m_ActiveVideoFormat;
|
||||||
int m_ActiveVideoWidth;
|
int m_ActiveVideoWidth;
|
||||||
@@ -133,6 +152,9 @@ private:
|
|||||||
IAudioRenderer* m_AudioRenderer;
|
IAudioRenderer* m_AudioRenderer;
|
||||||
OPUS_MULTISTREAM_CONFIGURATION m_AudioConfig;
|
OPUS_MULTISTREAM_CONFIGURATION m_AudioConfig;
|
||||||
int m_AudioSampleCount;
|
int m_AudioSampleCount;
|
||||||
|
Uint32 m_DropAudioEndTime;
|
||||||
|
|
||||||
|
Overlay::OverlayManager m_OverlayManager;
|
||||||
|
|
||||||
static AUDIO_RENDERER_CALLBACKS k_AudioCallbacks;
|
static AUDIO_RENDERER_CALLBACKS k_AudioCallbacks;
|
||||||
static CONNECTION_LISTENER_CALLBACKS k_ConnCallbacks;
|
static CONNECTION_LISTENER_CALLBACKS k_ConnCallbacks;
|
||||||
|
|||||||
@@ -12,12 +12,14 @@ typedef struct _VIDEO_STATS {
|
|||||||
uint32_t receivedFrames;
|
uint32_t receivedFrames;
|
||||||
uint32_t decodedFrames;
|
uint32_t decodedFrames;
|
||||||
uint32_t renderedFrames;
|
uint32_t renderedFrames;
|
||||||
|
uint32_t totalFrames;
|
||||||
uint32_t networkDroppedFrames;
|
uint32_t networkDroppedFrames;
|
||||||
uint32_t pacerDroppedFrames;
|
uint32_t pacerDroppedFrames;
|
||||||
uint32_t totalReassemblyTime;
|
uint32_t totalReassemblyTime;
|
||||||
uint32_t totalDecodeTime;
|
uint32_t totalDecodeTime;
|
||||||
uint32_t totalPacerTime;
|
uint32_t totalPacerTime;
|
||||||
uint32_t totalRenderTime;
|
uint32_t totalRenderTime;
|
||||||
|
float totalFps;
|
||||||
float receivedFps;
|
float receivedFps;
|
||||||
float decodedFps;
|
float decodedFps;
|
||||||
float renderedFps;
|
float renderedFps;
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
#include "dxva2.h"
|
#include "dxva2.h"
|
||||||
#include "../ffmpeg.h"
|
#include "../ffmpeg.h"
|
||||||
#include <streaming/streamutils.h>
|
#include <streaming/streamutils.h>
|
||||||
|
#include <streaming/session.h>
|
||||||
|
|
||||||
#include <SDL_syswm.h>
|
#include <SDL_syswm.h>
|
||||||
|
|
||||||
@@ -25,7 +26,10 @@ DXVA2Renderer::DXVA2Renderer() :
|
|||||||
m_RenderTarget(nullptr),
|
m_RenderTarget(nullptr),
|
||||||
m_ProcService(nullptr),
|
m_ProcService(nullptr),
|
||||||
m_Processor(nullptr),
|
m_Processor(nullptr),
|
||||||
m_FrameIndex(0)
|
m_FrameIndex(0),
|
||||||
|
m_DebugOverlayFont(nullptr),
|
||||||
|
m_StatusOverlayFont(nullptr),
|
||||||
|
m_BlockingPresent(false)
|
||||||
{
|
{
|
||||||
RtlZeroMemory(m_DecSurfaces, sizeof(m_DecSurfaces));
|
RtlZeroMemory(m_DecSurfaces, sizeof(m_DecSurfaces));
|
||||||
RtlZeroMemory(&m_DXVAContext, sizeof(m_DXVAContext));
|
RtlZeroMemory(&m_DXVAContext, sizeof(m_DXVAContext));
|
||||||
@@ -44,6 +48,8 @@ DXVA2Renderer::~DXVA2Renderer()
|
|||||||
SAFE_COM_RELEASE(m_RenderTarget);
|
SAFE_COM_RELEASE(m_RenderTarget);
|
||||||
SAFE_COM_RELEASE(m_ProcService);
|
SAFE_COM_RELEASE(m_ProcService);
|
||||||
SAFE_COM_RELEASE(m_Processor);
|
SAFE_COM_RELEASE(m_Processor);
|
||||||
|
SAFE_COM_RELEASE(m_DebugOverlayFont);
|
||||||
|
SAFE_COM_RELEASE(m_StatusOverlayFont);
|
||||||
|
|
||||||
for (int i = 0; i < ARRAYSIZE(m_DecSurfaces); i++) {
|
for (int i = 0; i < ARRAYSIZE(m_DecSurfaces); i++) {
|
||||||
SAFE_COM_RELEASE(m_DecSurfaces[i]);
|
SAFE_COM_RELEASE(m_DecSurfaces[i]);
|
||||||
@@ -256,68 +262,120 @@ bool DXVA2Renderer::initializeRenderer()
|
|||||||
m_DisplayWidth = renderTargetDesc.Width;
|
m_DisplayWidth = renderTargetDesc.Width;
|
||||||
m_DisplayHeight = renderTargetDesc.Height;
|
m_DisplayHeight = renderTargetDesc.Height;
|
||||||
|
|
||||||
hr = DXVA2CreateVideoService(m_Device, IID_IDirectXVideoProcessorService,
|
if (!isDXVideoProcessorAPIBlacklisted()) {
|
||||||
reinterpret_cast<void**>(&m_ProcService));
|
hr = DXVA2CreateVideoService(m_Device, IID_IDirectXVideoProcessorService,
|
||||||
|
reinterpret_cast<void**>(&m_ProcService));
|
||||||
|
|
||||||
if (FAILED(hr)) {
|
if (FAILED(hr)) {
|
||||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
"DXVA2CreateVideoService(IID_IDirectXVideoProcessorService) failed: %x",
|
"DXVA2CreateVideoService(IID_IDirectXVideoProcessorService) failed: %x",
|
||||||
hr);
|
hr);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
DXVA2_VideoProcessorCaps caps;
|
DXVA2_VideoProcessorCaps caps;
|
||||||
hr = m_ProcService->GetVideoProcessorCaps(DXVA2_VideoProcProgressiveDevice, &m_Desc, renderTargetDesc.Format, &caps);
|
hr = m_ProcService->GetVideoProcessorCaps(DXVA2_VideoProcProgressiveDevice, &m_Desc, renderTargetDesc.Format, &caps);
|
||||||
if (FAILED(hr)) {
|
if (FAILED(hr)) {
|
||||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
"GetVideoProcessorCaps() failed for DXVA2_VideoProcProgressiveDevice: %x",
|
"GetVideoProcessorCaps() failed for DXVA2_VideoProcProgressiveDevice: %x",
|
||||||
hr);
|
hr);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!(caps.DeviceCaps & DXVA2_VPDev_HardwareDevice)) {
|
if (!(caps.DeviceCaps & DXVA2_VPDev_HardwareDevice)) {
|
||||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
"DXVA2_VideoProcProgressiveDevice is not hardware: %x",
|
"DXVA2_VideoProcProgressiveDevice is not hardware: %x",
|
||||||
caps.DeviceCaps);
|
caps.DeviceCaps);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
else if (!(caps.VideoProcessorOperations & DXVA2_VideoProcess_YUV2RGB) &&
|
else if (!(caps.VideoProcessorOperations & DXVA2_VideoProcess_YUV2RGB) &&
|
||||||
!(caps.VideoProcessorOperations & DXVA2_VideoProcess_YUV2RGBExtended)) {
|
!(caps.VideoProcessorOperations & DXVA2_VideoProcess_YUV2RGBExtended)) {
|
||||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
"DXVA2_VideoProcProgressiveDevice can't convert YUV2RGB: %x",
|
"DXVA2_VideoProcProgressiveDevice can't convert YUV2RGB: %x",
|
||||||
caps.VideoProcessorOperations);
|
caps.VideoProcessorOperations);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
else if (!(caps.VideoProcessorOperations & DXVA2_VideoProcess_StretchX) ||
|
else if (!(caps.VideoProcessorOperations & DXVA2_VideoProcess_StretchX) ||
|
||||||
!(caps.VideoProcessorOperations & DXVA2_VideoProcess_StretchY)) {
|
!(caps.VideoProcessorOperations & DXVA2_VideoProcess_StretchY)) {
|
||||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
"DXVA2_VideoProcProgressiveDevice can't stretch video: %x",
|
"DXVA2_VideoProcProgressiveDevice can't stretch video: %x",
|
||||||
caps.VideoProcessorOperations);
|
caps.VideoProcessorOperations);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (caps.DeviceCaps & DXVA2_VPDev_EmulatedDXVA1) {
|
if (caps.DeviceCaps & DXVA2_VPDev_EmulatedDXVA1) {
|
||||||
// DXVA2 over DXVA1 may have bad performance
|
// DXVA2 over DXVA1 may have bad performance
|
||||||
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
"DXVA2_VideoProcProgressiveDevice is DXVA1");
|
"DXVA2_VideoProcProgressiveDevice is DXVA1");
|
||||||
}
|
}
|
||||||
|
|
||||||
m_ProcService->GetProcAmpRange(DXVA2_VideoProcProgressiveDevice, &m_Desc, renderTargetDesc.Format, DXVA2_ProcAmp_Brightness, &m_BrightnessRange);
|
m_ProcService->GetProcAmpRange(DXVA2_VideoProcProgressiveDevice, &m_Desc, renderTargetDesc.Format, DXVA2_ProcAmp_Brightness, &m_BrightnessRange);
|
||||||
m_ProcService->GetProcAmpRange(DXVA2_VideoProcProgressiveDevice, &m_Desc, renderTargetDesc.Format, DXVA2_ProcAmp_Contrast, &m_ContrastRange);
|
m_ProcService->GetProcAmpRange(DXVA2_VideoProcProgressiveDevice, &m_Desc, renderTargetDesc.Format, DXVA2_ProcAmp_Contrast, &m_ContrastRange);
|
||||||
m_ProcService->GetProcAmpRange(DXVA2_VideoProcProgressiveDevice, &m_Desc, renderTargetDesc.Format, DXVA2_ProcAmp_Hue, &m_HueRange);
|
m_ProcService->GetProcAmpRange(DXVA2_VideoProcProgressiveDevice, &m_Desc, renderTargetDesc.Format, DXVA2_ProcAmp_Hue, &m_HueRange);
|
||||||
m_ProcService->GetProcAmpRange(DXVA2_VideoProcProgressiveDevice, &m_Desc, renderTargetDesc.Format, DXVA2_ProcAmp_Saturation, &m_SaturationRange);
|
m_ProcService->GetProcAmpRange(DXVA2_VideoProcProgressiveDevice, &m_Desc, renderTargetDesc.Format, DXVA2_ProcAmp_Saturation, &m_SaturationRange);
|
||||||
|
|
||||||
hr = m_ProcService->CreateVideoProcessor(DXVA2_VideoProcProgressiveDevice, &m_Desc, renderTargetDesc.Format, 0, &m_Processor);
|
hr = m_ProcService->CreateVideoProcessor(DXVA2_VideoProcProgressiveDevice, &m_Desc, renderTargetDesc.Format, 0, &m_Processor);
|
||||||
if (FAILED(hr)) {
|
if (FAILED(hr)) {
|
||||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
"CreateVideoProcessor() failed for DXVA2_VideoProcProgressiveDevice: %x",
|
"CreateVideoProcessor() failed for DXVA2_VideoProcProgressiveDevice: %x",
|
||||||
hr);
|
hr);
|
||||||
return false;
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool DXVA2Renderer::isDXVideoProcessorAPIBlacklisted()
|
||||||
|
{
|
||||||
|
IDirect3D9* d3d9;
|
||||||
|
HRESULT hr;
|
||||||
|
bool result = false;
|
||||||
|
|
||||||
|
if (qgetenv("DXVA2_DISABLE_VIDPROC_BLACKLIST") == "1") {
|
||||||
|
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"IDirectXVideoProcessor blacklist is disabled");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
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) && id.VendorId == 0x8086) {
|
||||||
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Avoiding IDirectXVideoProcessor API on Intel GPU");
|
||||||
|
|
||||||
|
// On Intel GPUs, we can get unwanted video "enhancements" due to post-processing
|
||||||
|
// effects that the GPU driver forces on us. In many cases, this makes the video
|
||||||
|
// actually look worse. We can avoid these by using StretchRect() instead on these
|
||||||
|
// platforms.
|
||||||
|
result = true;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
result = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"GetDeviceCaps() failed: %x", hr);
|
||||||
|
}
|
||||||
|
|
||||||
|
d3d9->Release();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"GetDirect3D() failed: %x", hr);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
bool DXVA2Renderer::isDecoderBlacklisted()
|
bool DXVA2Renderer::isDecoderBlacklisted()
|
||||||
{
|
{
|
||||||
IDirect3D9* d3d9;
|
IDirect3D9* d3d9;
|
||||||
@@ -327,6 +385,12 @@ bool DXVA2Renderer::isDecoderBlacklisted()
|
|||||||
// TODO: Update for HEVC Main10
|
// TODO: Update for HEVC Main10
|
||||||
SDL_assert(m_VideoFormat != VIDEO_FORMAT_H265_MAIN10);
|
SDL_assert(m_VideoFormat != VIDEO_FORMAT_H265_MAIN10);
|
||||||
|
|
||||||
|
if (qgetenv("DXVA2_DISABLE_DECODER_BLACKLIST") == "1") {
|
||||||
|
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"DXVA2 decoder blacklist is disabled");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
hr = m_Device->GetDirect3D(&d3d9);
|
hr = m_Device->GetDirect3D(&d3d9);
|
||||||
if (SUCCEEDED(hr)) {
|
if (SUCCEEDED(hr)) {
|
||||||
D3DCAPS9 caps;
|
D3DCAPS9 caps;
|
||||||
@@ -512,6 +576,8 @@ bool DXVA2Renderer::initializeDevice(SDL_Window* window, bool enableVsync)
|
|||||||
d3dpp.BackBufferCount = 1;
|
d3dpp.BackBufferCount = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
m_BlockingPresent = false;
|
||||||
|
|
||||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
"Windowed mode with DWM running");
|
"Windowed mode with DWM running");
|
||||||
}
|
}
|
||||||
@@ -521,6 +587,7 @@ bool DXVA2Renderer::initializeDevice(SDL_Window* window, bool enableVsync)
|
|||||||
d3dpp.PresentationInterval = D3DPRESENT_INTERVAL_ONE;
|
d3dpp.PresentationInterval = D3DPRESENT_INTERVAL_ONE;
|
||||||
d3dpp.SwapEffect = D3DSWAPEFFECT_DISCARD;
|
d3dpp.SwapEffect = D3DSWAPEFFECT_DISCARD;
|
||||||
d3dpp.BackBufferCount = 1;
|
d3dpp.BackBufferCount = 1;
|
||||||
|
m_BlockingPresent = true;
|
||||||
|
|
||||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
"V-Sync enabled");
|
"V-Sync enabled");
|
||||||
@@ -531,6 +598,7 @@ bool DXVA2Renderer::initializeDevice(SDL_Window* window, bool enableVsync)
|
|||||||
d3dpp.PresentationInterval = D3DPRESENT_INTERVAL_IMMEDIATE;
|
d3dpp.PresentationInterval = D3DPRESENT_INTERVAL_IMMEDIATE;
|
||||||
d3dpp.SwapEffect = D3DSWAPEFFECT_DISCARD;
|
d3dpp.SwapEffect = D3DSWAPEFFECT_DISCARD;
|
||||||
d3dpp.BackBufferCount = 1;
|
d3dpp.BackBufferCount = 1;
|
||||||
|
m_BlockingPresent = false;
|
||||||
|
|
||||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
"V-Sync disabled in tearing mode");
|
"V-Sync disabled in tearing mode");
|
||||||
@@ -591,9 +659,13 @@ bool DXVA2Renderer::initialize(SDL_Window* window, int videoFormat, int width, i
|
|||||||
|
|
||||||
int alignment;
|
int alignment;
|
||||||
|
|
||||||
// HEVC using DXVA requires 128B alignment
|
// HEVC using DXVA requires 128 pixel alignment, however Intel GPUs decoding HEVC
|
||||||
|
// using StretchRect() to render draw a translucent green line at the top of
|
||||||
|
// the screen in full-screen mode at 720p/1080p unless we use 32 pixel alignment.
|
||||||
|
// This appears to work without issues on AMD and Nvidia GPUs too, so we will
|
||||||
|
// do it unconditionally for now.
|
||||||
if (m_VideoFormat & VIDEO_FORMAT_MASK_H265) {
|
if (m_VideoFormat & VIDEO_FORMAT_MASK_H265) {
|
||||||
alignment = 128;
|
alignment = 32;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
alignment = 16;
|
alignment = 16;
|
||||||
@@ -627,7 +699,7 @@ bool DXVA2Renderer::initialize(SDL_Window* window, int videoFormat, int width, i
|
|||||||
// input focus. This glitches out the renderer and a bunch of other stuff.
|
// input focus. This glitches out the renderer and a bunch of other stuff.
|
||||||
// Direct3D9Ex itself seems to have this minimize on focus loss behavior on its
|
// Direct3D9Ex itself seems to have this minimize on focus loss behavior on its
|
||||||
// own, so just disable SDL's handling of the focus loss event.
|
// own, so just disable SDL's handling of the focus loss event.
|
||||||
SDL_SetHint(SDL_HINT_VIDEO_MINIMIZE_ON_FOCUS_LOSS, "0");
|
SDL_SetHintWithPriority(SDL_HINT_VIDEO_MINIMIZE_ON_FOCUS_LOSS, "0", SDL_HINT_OVERRIDE);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -649,7 +721,65 @@ IFFmpegRenderer::FramePacingConstraint DXVA2Renderer::getFramePacingConstraint()
|
|||||||
return PACING_ANY;
|
return PACING_ANY;
|
||||||
}
|
}
|
||||||
|
|
||||||
void DXVA2Renderer::renderFrameAtVsync(AVFrame *frame)
|
void DXVA2Renderer::notifyOverlayUpdated(Overlay::OverlayType type)
|
||||||
|
{
|
||||||
|
HRESULT hr;
|
||||||
|
|
||||||
|
switch (type)
|
||||||
|
{
|
||||||
|
case Overlay::OverlayDebug:
|
||||||
|
if (m_DebugOverlayFont == nullptr) {
|
||||||
|
hr = D3DXCreateFontA(m_Device,
|
||||||
|
Session::get()->getOverlayManager().getOverlayFontSize(Overlay::OverlayDebug),
|
||||||
|
0,
|
||||||
|
FW_HEAVY,
|
||||||
|
1,
|
||||||
|
false,
|
||||||
|
ANSI_CHARSET,
|
||||||
|
OUT_DEFAULT_PRECIS,
|
||||||
|
DEFAULT_QUALITY,
|
||||||
|
DEFAULT_PITCH | FF_DONTCARE,
|
||||||
|
"",
|
||||||
|
&m_DebugOverlayFont);
|
||||||
|
if (FAILED(hr)) {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"D3DXCreateFontA() failed: %x",
|
||||||
|
hr);
|
||||||
|
m_DebugOverlayFont = nullptr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case Overlay::OverlayStatusUpdate:
|
||||||
|
if (m_StatusOverlayFont == nullptr) {
|
||||||
|
hr = D3DXCreateFontA(m_Device,
|
||||||
|
Session::get()->getOverlayManager().getOverlayFontSize(Overlay::OverlayStatusUpdate),
|
||||||
|
0,
|
||||||
|
FW_HEAVY,
|
||||||
|
1,
|
||||||
|
false,
|
||||||
|
ANSI_CHARSET,
|
||||||
|
OUT_DEFAULT_PRECIS,
|
||||||
|
DEFAULT_QUALITY,
|
||||||
|
DEFAULT_PITCH | FF_DONTCARE,
|
||||||
|
"",
|
||||||
|
&m_StatusOverlayFont);
|
||||||
|
if (FAILED(hr)) {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"D3DXCreateFontA() failed: %x",
|
||||||
|
hr);
|
||||||
|
m_StatusOverlayFont = nullptr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
SDL_assert(false);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
@@ -785,12 +915,10 @@ void DXVA2Renderer::renderFrameAtVsync(AVFrame *frame)
|
|||||||
|
|
||||||
bltParams.Alpha = DXVA2_Fixed32OpaqueAlpha();
|
bltParams.Alpha = DXVA2_Fixed32OpaqueAlpha();
|
||||||
|
|
||||||
m_Device->Clear(0, nullptr, D3DCLEAR_TARGET, D3DCOLOR_ARGB(255, 0, 0, 0), 0.0f, 0);
|
hr = m_Device->Clear(0, nullptr, D3DCLEAR_TARGET, D3DCOLOR_ARGB(255, 0, 0, 0), 0.0f, 0);
|
||||||
|
|
||||||
hr = m_Processor->VideoProcessBlt(m_RenderTarget, &bltParams, &sample, 1, nullptr);
|
|
||||||
if (FAILED(hr)) {
|
if (FAILED(hr)) {
|
||||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
"VideoProcessBlt() failed: %x",
|
"Clear() failed: %x",
|
||||||
hr);
|
hr);
|
||||||
SDL_Event event;
|
SDL_Event event;
|
||||||
event.type = SDL_RENDER_TARGETS_RESET;
|
event.type = SDL_RENDER_TARGETS_RESET;
|
||||||
@@ -798,7 +926,85 @@ void DXVA2Renderer::renderFrameAtVsync(AVFrame *frame)
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
hr = m_Device->PresentEx(nullptr, nullptr, nullptr, nullptr, 0);
|
hr = m_Device->BeginScene();
|
||||||
|
if (FAILED(hr)) {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"BeginScene() failed: %x",
|
||||||
|
hr);
|
||||||
|
SDL_Event event;
|
||||||
|
event.type = SDL_RENDER_TARGETS_RESET;
|
||||||
|
SDL_PushEvent(&event);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (m_Processor) {
|
||||||
|
hr = m_Processor->VideoProcessBlt(m_RenderTarget, &bltParams, &sample, 1, nullptr);
|
||||||
|
if (FAILED(hr)) {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"VideoProcessBlt() failed, falling back to StretchRect(): %x",
|
||||||
|
hr);
|
||||||
|
m_Processor->Release();
|
||||||
|
m_Processor = nullptr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!m_Processor) {
|
||||||
|
// This function doesn't trigger any of Intel's garbage video "enhancements"
|
||||||
|
hr = m_Device->StretchRect(surface, &sample.SrcRect, m_RenderTarget, &sample.DstRect, D3DTEXF_NONE);
|
||||||
|
if (FAILED(hr)) {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"StretchRect() failed: %x",
|
||||||
|
hr);
|
||||||
|
SDL_Event event;
|
||||||
|
event.type = SDL_RENDER_TARGETS_RESET;
|
||||||
|
SDL_PushEvent(&event);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (m_DebugOverlayFont != nullptr) {
|
||||||
|
if (Session::get()->getOverlayManager().isOverlayEnabled(Overlay::OverlayDebug)) {
|
||||||
|
SDL_Color color = Session::get()->getOverlayManager().getOverlayColor(Overlay::OverlayDebug);
|
||||||
|
m_DebugOverlayFont->DrawTextA(nullptr,
|
||||||
|
Session::get()->getOverlayManager().getOverlayText(Overlay::OverlayDebug),
|
||||||
|
-1,
|
||||||
|
&sample.DstRect,
|
||||||
|
DT_LEFT | DT_NOCLIP,
|
||||||
|
D3DCOLOR_ARGB(color.a, color.r, color.g, color.b));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (m_StatusOverlayFont != nullptr) {
|
||||||
|
if (Session::get()->getOverlayManager().isOverlayEnabled(Overlay::OverlayStatusUpdate)) {
|
||||||
|
SDL_Color color = Session::get()->getOverlayManager().getOverlayColor(Overlay::OverlayStatusUpdate);
|
||||||
|
m_StatusOverlayFont->DrawTextA(nullptr,
|
||||||
|
Session::get()->getOverlayManager().getOverlayText(Overlay::OverlayStatusUpdate),
|
||||||
|
-1,
|
||||||
|
&sample.DstRect,
|
||||||
|
DT_RIGHT | DT_NOCLIP,
|
||||||
|
D3DCOLOR_ARGB(color.a, color.r, color.g, color.b));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
hr = m_Device->EndScene();
|
||||||
|
if (FAILED(hr)) {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"EndScene() failed: %x",
|
||||||
|
hr);
|
||||||
|
SDL_Event event;
|
||||||
|
event.type = SDL_RENDER_TARGETS_RESET;
|
||||||
|
SDL_PushEvent(&event);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
do {
|
||||||
|
// Use D3DPRESENT_DONOTWAIT if present may block in order to avoid holding the giant
|
||||||
|
// lock around this D3D device for excessive lengths of time (blocking concurrent decoding tasks).
|
||||||
|
hr = m_Device->PresentEx(nullptr, nullptr, nullptr, nullptr, m_BlockingPresent ? D3DPRESENT_DONOTWAIT : 0);
|
||||||
|
if (hr == D3DERR_WASSTILLDRAWING) {
|
||||||
|
SDL_Delay(1);
|
||||||
|
}
|
||||||
|
} while (hr == D3DERR_WASSTILLDRAWING);
|
||||||
if (FAILED(hr)) {
|
if (FAILED(hr)) {
|
||||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
"PresentEx() failed: %x",
|
"PresentEx() failed: %x",
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
#include "pacer/pacer.h"
|
#include "pacer/pacer.h"
|
||||||
|
|
||||||
#include <d3d9.h>
|
#include <d3d9.h>
|
||||||
|
#include <d3dx9.h>
|
||||||
#include <dxva2api.h>
|
#include <dxva2api.h>
|
||||||
|
|
||||||
extern "C" {
|
extern "C" {
|
||||||
@@ -20,18 +21,20 @@ public:
|
|||||||
int width,
|
int width,
|
||||||
int height,
|
int height,
|
||||||
int maxFps,
|
int maxFps,
|
||||||
bool enableVsync);
|
bool enableVsync) override;
|
||||||
virtual bool prepareDecoderContext(AVCodecContext* context);
|
virtual bool prepareDecoderContext(AVCodecContext* context) override;
|
||||||
virtual void renderFrameAtVsync(AVFrame* frame);
|
virtual void renderFrame(AVFrame* frame) override;
|
||||||
virtual bool needsTestFrame();
|
virtual bool needsTestFrame() override;
|
||||||
virtual int getDecoderCapabilities();
|
virtual int getDecoderCapabilities() override;
|
||||||
virtual FramePacingConstraint getFramePacingConstraint();
|
virtual FramePacingConstraint getFramePacingConstraint() override;
|
||||||
|
virtual void notifyOverlayUpdated(Overlay::OverlayType) override;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
bool initializeDecoder();
|
bool initializeDecoder();
|
||||||
bool initializeRenderer();
|
bool initializeRenderer();
|
||||||
bool initializeDevice(SDL_Window* window, bool enableVsync);
|
bool initializeDevice(SDL_Window* window, bool enableVsync);
|
||||||
bool isDecoderBlacklisted();
|
bool isDecoderBlacklisted();
|
||||||
|
bool isDXVideoProcessorAPIBlacklisted();
|
||||||
|
|
||||||
static
|
static
|
||||||
AVBufferRef* ffPoolAlloc(void* opaque, int size);
|
AVBufferRef* ffPoolAlloc(void* opaque, int size);
|
||||||
@@ -67,4 +70,7 @@ private:
|
|||||||
DXVA2_ValueRange m_SaturationRange;
|
DXVA2_ValueRange m_SaturationRange;
|
||||||
DXVA2_VideoDesc m_Desc;
|
DXVA2_VideoDesc m_Desc;
|
||||||
REFERENCE_TIME m_FrameIndex;
|
REFERENCE_TIME m_FrameIndex;
|
||||||
|
LPD3DXFONT m_DebugOverlayFont;
|
||||||
|
LPD3DXFONT m_StatusOverlayFont;
|
||||||
|
bool m_BlockingPresent;
|
||||||
};
|
};
|
||||||
@@ -76,7 +76,11 @@ int DxVsyncSource::vsyncThread(void* context)
|
|||||||
{
|
{
|
||||||
DxVsyncSource* me = reinterpret_cast<DxVsyncSource*>(context);
|
DxVsyncSource* me = reinterpret_cast<DxVsyncSource*>(context);
|
||||||
|
|
||||||
|
#if SDL_VERSION_ATLEAST(2, 0, 9)
|
||||||
|
SDL_SetThreadPriority(SDL_THREAD_PRIORITY_TIME_CRITICAL);
|
||||||
|
#else
|
||||||
SDL_SetThreadPriority(SDL_THREAD_PRIORITY_HIGH);
|
SDL_SetThreadPriority(SDL_THREAD_PRIORITY_HIGH);
|
||||||
|
#endif
|
||||||
|
|
||||||
D3DKMT_OPENADAPTERFROMHDC openAdapterParams = {};
|
D3DKMT_OPENADAPTERFROMHDC openAdapterParams = {};
|
||||||
HMONITOR lastMonitor = nullptr;
|
HMONITOR lastMonitor = nullptr;
|
||||||
|
|||||||
@@ -33,7 +33,11 @@ int NullThreadedVsyncSource::vsyncThread(void* context)
|
|||||||
{
|
{
|
||||||
NullThreadedVsyncSource* me = reinterpret_cast<NullThreadedVsyncSource*>(context);
|
NullThreadedVsyncSource* me = reinterpret_cast<NullThreadedVsyncSource*>(context);
|
||||||
|
|
||||||
|
#if SDL_VERSION_ATLEAST(2, 0, 9)
|
||||||
|
SDL_SetThreadPriority(SDL_THREAD_PRIORITY_TIME_CRITICAL);
|
||||||
|
#else
|
||||||
SDL_SetThreadPriority(SDL_THREAD_PRIORITY_HIGH);
|
SDL_SetThreadPriority(SDL_THREAD_PRIORITY_HIGH);
|
||||||
|
#endif
|
||||||
|
|
||||||
while (SDL_AtomicGet(&me->m_Stopping) == 0) {
|
while (SDL_AtomicGet(&me->m_Stopping) == 0) {
|
||||||
me->m_Pacer->vsyncCallback(1000 / me->m_DisplayFps);
|
me->m_Pacer->vsyncCallback(1000 / me->m_DisplayFps);
|
||||||
|
|||||||
@@ -8,10 +8,15 @@
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
#ifdef Q_OS_WIN32
|
#ifdef Q_OS_WIN32
|
||||||
|
#define WIN32_LEAN_AND_MEAN
|
||||||
|
#include <Windows.h>
|
||||||
|
#include <VersionHelpers.h>
|
||||||
#include "dxvsyncsource.h"
|
#include "dxvsyncsource.h"
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#define FRAME_HISTORY_ENTRIES 8
|
// Limit the number of queued frames to prevent excessive memory consumption
|
||||||
|
// if the V-Sync source or renderer is blocked for a while.
|
||||||
|
#define MAX_QUEUED_FRAMES 8
|
||||||
|
|
||||||
// We may be woken up slightly late so don't go all the way
|
// We may be woken up slightly late so don't go all the way
|
||||||
// up to the next V-sync since we may accidentally step into
|
// up to the next V-sync since we may accidentally step into
|
||||||
@@ -21,7 +26,8 @@
|
|||||||
#define TIMER_SLACK_MS 3
|
#define TIMER_SLACK_MS 3
|
||||||
|
|
||||||
Pacer::Pacer(IFFmpegRenderer* renderer, PVIDEO_STATS videoStats) :
|
Pacer::Pacer(IFFmpegRenderer* renderer, PVIDEO_STATS videoStats) :
|
||||||
m_FrameQueueLock(0),
|
m_RenderThread(nullptr),
|
||||||
|
m_Stopping(false),
|
||||||
m_VsyncSource(nullptr),
|
m_VsyncSource(nullptr),
|
||||||
m_VsyncRenderer(renderer),
|
m_VsyncRenderer(renderer),
|
||||||
m_MaxVideoFps(0),
|
m_MaxVideoFps(0),
|
||||||
@@ -37,10 +43,76 @@ Pacer::~Pacer()
|
|||||||
delete m_VsyncSource;
|
delete m_VsyncSource;
|
||||||
m_VsyncSource = nullptr;
|
m_VsyncSource = nullptr;
|
||||||
|
|
||||||
while (!m_FrameQueue.isEmpty()) {
|
// Stop the render thread
|
||||||
AVFrame* frame = m_FrameQueue.dequeue();
|
m_Stopping = true;
|
||||||
|
if (m_RenderThread != nullptr) {
|
||||||
|
m_RenderQueueNotEmpty.wakeAll();
|
||||||
|
SDL_WaitThread(m_RenderThread, nullptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete any remaining unconsumed frames
|
||||||
|
while (!m_RenderQueue.isEmpty()) {
|
||||||
|
AVFrame* frame = m_RenderQueue.dequeue();
|
||||||
av_frame_free(&frame);
|
av_frame_free(&frame);
|
||||||
}
|
}
|
||||||
|
while (!m_PacingQueue.isEmpty()) {
|
||||||
|
AVFrame* frame = m_PacingQueue.dequeue();
|
||||||
|
av_frame_free(&frame);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int Pacer::renderThread(void* context)
|
||||||
|
{
|
||||||
|
Pacer* me = reinterpret_cast<Pacer*>(context);
|
||||||
|
|
||||||
|
if (SDL_SetThreadPriority(SDL_THREAD_PRIORITY_HIGH) < 0) {
|
||||||
|
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Unable to set render thread to high priority: %s",
|
||||||
|
SDL_GetError());
|
||||||
|
}
|
||||||
|
|
||||||
|
while (!me->m_Stopping) {
|
||||||
|
// Acquire the frame queue lock to protect the queue and
|
||||||
|
// the not empty condition
|
||||||
|
me->m_FrameQueueLock.lock();
|
||||||
|
|
||||||
|
// Wait for a frame to be ready to render
|
||||||
|
while (!me->m_Stopping && me->m_RenderQueue.isEmpty()) {
|
||||||
|
me->m_RenderQueueNotEmpty.wait(&me->m_FrameQueueLock);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (me->m_Stopping) {
|
||||||
|
// Exit this thread
|
||||||
|
me->m_FrameQueueLock.unlock();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dequeue the most recent frame for rendering and free the others.
|
||||||
|
// NB: m_FrameQueueLock still held here!
|
||||||
|
AVFrame* lastFrame = nullptr;
|
||||||
|
while (!me->m_RenderQueue.isEmpty()) {
|
||||||
|
if (lastFrame != nullptr) {
|
||||||
|
// Don't hold the frame queue lock across av_frame_free(),
|
||||||
|
// since it could need to talk to the GPU driver. This is safe
|
||||||
|
// because we're guaranteed that the queue will not shrink during
|
||||||
|
// this time (and so dequeue() below will always get something).
|
||||||
|
me->m_FrameQueueLock.unlock();
|
||||||
|
av_frame_free(&lastFrame);
|
||||||
|
me->m_VideoStats->pacerDroppedFrames++;
|
||||||
|
me->m_FrameQueueLock.lock();
|
||||||
|
}
|
||||||
|
|
||||||
|
lastFrame = me->m_RenderQueue.dequeue();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Release the frame queue lock before rendering
|
||||||
|
me->m_FrameQueueLock.unlock();
|
||||||
|
|
||||||
|
// Render and free the mot current frame
|
||||||
|
me->renderFrame(lastFrame);
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Called in an arbitrary thread by the IVsyncSource on V-sync
|
// Called in an arbitrary thread by the IVsyncSource on V-sync
|
||||||
@@ -52,9 +124,7 @@ void Pacer::vsyncCallback(int timeUntilNextVsyncMillis)
|
|||||||
|
|
||||||
SDL_assert(timeUntilNextVsyncMillis >= TIMER_SLACK_MS);
|
SDL_assert(timeUntilNextVsyncMillis >= TIMER_SLACK_MS);
|
||||||
|
|
||||||
Uint32 vsyncCallbackStartTime = SDL_GetTicks();
|
m_FrameQueueLock.lock();
|
||||||
|
|
||||||
SDL_AtomicLock(&m_FrameQueueLock);
|
|
||||||
|
|
||||||
// If the queue length history entries are large, be strict
|
// If the queue length history entries are large, be strict
|
||||||
// about dropping excess frames.
|
// about dropping excess frames.
|
||||||
@@ -64,8 +134,8 @@ void Pacer::vsyncCallback(int timeUntilNextVsyncMillis)
|
|||||||
// frame history to drop frames only if consistently above the
|
// frame history to drop frames only if consistently above the
|
||||||
// one queued frame mark.
|
// one queued frame mark.
|
||||||
if (m_MaxVideoFps >= m_DisplayFps) {
|
if (m_MaxVideoFps >= m_DisplayFps) {
|
||||||
for (int i = 0; i < m_FrameQueueHistory.count(); i++) {
|
for (int queueHistoryEntry : m_PacingQueueHistory) {
|
||||||
if (m_FrameQueueHistory[i] <= 1) {
|
if (queueHistoryEntry <= 1) {
|
||||||
// Be lenient as long as the queue length
|
// Be lenient as long as the queue length
|
||||||
// resolves before the end of frame history
|
// resolves before the end of frame history
|
||||||
frameDropTarget = 3;
|
frameDropTarget = 3;
|
||||||
@@ -73,55 +143,39 @@ void Pacer::vsyncCallback(int timeUntilNextVsyncMillis)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (m_FrameQueueHistory.count() == FRAME_HISTORY_ENTRIES) {
|
// Keep a rolling 500 ms window of pacing queue history
|
||||||
m_FrameQueueHistory.dequeue();
|
if (m_PacingQueueHistory.count() == m_DisplayFps / 2) {
|
||||||
|
m_PacingQueueHistory.dequeue();
|
||||||
}
|
}
|
||||||
|
|
||||||
m_FrameQueueHistory.enqueue(m_FrameQueue.count());
|
m_PacingQueueHistory.enqueue(m_PacingQueue.count());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Catch up if we're several frames ahead
|
// Catch up if we're several frames ahead
|
||||||
while (m_FrameQueue.count() > frameDropTarget) {
|
while (m_PacingQueue.count() > frameDropTarget) {
|
||||||
AVFrame* frame = m_FrameQueue.dequeue();
|
AVFrame* frame = m_PacingQueue.dequeue();
|
||||||
|
|
||||||
|
// Drop the lock while we call av_frame_free()
|
||||||
|
m_FrameQueueLock.unlock();
|
||||||
m_VideoStats->pacerDroppedFrames++;
|
m_VideoStats->pacerDroppedFrames++;
|
||||||
av_frame_free(&frame);
|
av_frame_free(&frame);
|
||||||
|
m_FrameQueueLock.lock();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (m_FrameQueue.isEmpty()) {
|
if (m_PacingQueue.isEmpty()) {
|
||||||
SDL_AtomicUnlock(&m_FrameQueueLock);
|
// Wait for a frame to arrive or our V-sync timeout to expire
|
||||||
|
if (!m_PacingQueueNotEmpty.wait(&m_FrameQueueLock, timeUntilNextVsyncMillis - TIMER_SLACK_MS)) {
|
||||||
while (!SDL_TICKS_PASSED(SDL_GetTicks(),
|
// Wait timed out - unlock and bail
|
||||||
vsyncCallbackStartTime + timeUntilNextVsyncMillis - TIMER_SLACK_MS)) {
|
m_FrameQueueLock.unlock();
|
||||||
SDL_Delay(1);
|
return;
|
||||||
|
|
||||||
SDL_AtomicLock(&m_FrameQueueLock);
|
|
||||||
if (!m_FrameQueue.isEmpty()) {
|
|
||||||
// Don't release the lock
|
|
||||||
goto RenderNextFrame;
|
|
||||||
}
|
|
||||||
SDL_AtomicUnlock(&m_FrameQueueLock);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Nothing to render at this time
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
RenderNextFrame:
|
// Place the first frame on the render queue
|
||||||
// Grab the first frame
|
dropFrameForEnqueue(m_RenderQueue);
|
||||||
AVFrame* frame = m_FrameQueue.dequeue();
|
m_RenderQueue.enqueue(m_PacingQueue.dequeue());
|
||||||
SDL_AtomicUnlock(&m_FrameQueueLock);
|
m_FrameQueueLock.unlock();
|
||||||
|
m_RenderQueueNotEmpty.wakeOne();
|
||||||
// Count time spent in Pacer's queues
|
|
||||||
Uint32 beforeRender = SDL_GetTicks();
|
|
||||||
m_VideoStats->totalPacerTime += beforeRender - frame->pts;
|
|
||||||
|
|
||||||
// Render it
|
|
||||||
m_VsyncRenderer->renderFrameAtVsync(frame);
|
|
||||||
m_VideoStats->totalRenderTime += SDL_GetTicks() - beforeRender;
|
|
||||||
m_VideoStats->renderedFrames++;
|
|
||||||
|
|
||||||
// Free the frame
|
|
||||||
av_frame_free(&frame);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool Pacer::initialize(SDL_Window* window, int maxVideoFps, bool enablePacing)
|
bool Pacer::initialize(SDL_Window* window, int maxVideoFps, bool enablePacing)
|
||||||
@@ -137,7 +191,11 @@ bool Pacer::initialize(SDL_Window* window, int maxVideoFps, bool enablePacing)
|
|||||||
#if defined(Q_OS_DARWIN)
|
#if defined(Q_OS_DARWIN)
|
||||||
m_VsyncSource = DisplayLinkVsyncSourceFactory::createVsyncSource(this);
|
m_VsyncSource = DisplayLinkVsyncSourceFactory::createVsyncSource(this);
|
||||||
#elif defined(Q_OS_WIN32)
|
#elif defined(Q_OS_WIN32)
|
||||||
m_VsyncSource = new DxVsyncSource(this);
|
// Don't use D3DKMTWaitForVerticalBlankEvent() on Windows 7, because
|
||||||
|
// it blocks during other concurrent DX operations (like actually rendering).
|
||||||
|
if (IsWindows8OrGreater()) {
|
||||||
|
m_VsyncSource = new DxVsyncSource(this);
|
||||||
|
}
|
||||||
#else
|
#else
|
||||||
// Platforms without a VsyncSource will just render frames
|
// Platforms without a VsyncSource will just render frames
|
||||||
// immediately like they used to.
|
// immediately like they used to.
|
||||||
@@ -153,32 +211,88 @@ bool Pacer::initialize(SDL_Window* window, int maxVideoFps, bool enablePacing)
|
|||||||
m_DisplayFps, m_MaxVideoFps);
|
m_DisplayFps, m_MaxVideoFps);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
m_RenderThread = SDL_CreateThread(Pacer::renderThread, "Pacer Render Thread", this);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void Pacer::renderFrame(AVFrame* frame)
|
||||||
|
{
|
||||||
|
// Count time spent in Pacer's queues
|
||||||
|
Uint32 beforeRender = SDL_GetTicks();
|
||||||
|
m_VideoStats->totalPacerTime += beforeRender - frame->pts;
|
||||||
|
|
||||||
|
// Render it
|
||||||
|
m_VsyncRenderer->renderFrame(frame);
|
||||||
|
Uint32 afterRender = SDL_GetTicks();
|
||||||
|
|
||||||
|
m_VideoStats->totalRenderTime += afterRender - beforeRender;
|
||||||
|
m_VideoStats->renderedFrames++;
|
||||||
|
av_frame_free(&frame);
|
||||||
|
|
||||||
|
// Drop frames if we have too many queued up for a while
|
||||||
|
m_FrameQueueLock.lock();
|
||||||
|
|
||||||
|
int frameDropTarget = 0;
|
||||||
|
for (int queueHistoryEntry : m_RenderQueueHistory) {
|
||||||
|
if (queueHistoryEntry == 0) {
|
||||||
|
// Be lenient as long as the queue length
|
||||||
|
// resolves before the end of frame history
|
||||||
|
frameDropTarget = 2;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keep a rolling 500 ms window of render queue history
|
||||||
|
if (m_RenderQueueHistory.count() == m_MaxVideoFps / 2) {
|
||||||
|
m_RenderQueueHistory.dequeue();
|
||||||
|
}
|
||||||
|
|
||||||
|
m_RenderQueueHistory.enqueue(m_RenderQueue.count());
|
||||||
|
|
||||||
|
// Catch up if we're several frames ahead
|
||||||
|
while (m_RenderQueue.count() > frameDropTarget) {
|
||||||
|
AVFrame* frame = m_RenderQueue.dequeue();
|
||||||
|
|
||||||
|
// Drop the lock while we call av_frame_free()
|
||||||
|
m_FrameQueueLock.unlock();
|
||||||
|
m_VideoStats->pacerDroppedFrames++;
|
||||||
|
av_frame_free(&frame);
|
||||||
|
m_FrameQueueLock.lock();
|
||||||
|
}
|
||||||
|
|
||||||
|
m_FrameQueueLock.unlock();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Pacer::dropFrameForEnqueue(QQueue<AVFrame*>& queue)
|
||||||
|
{
|
||||||
|
SDL_assert(queue.size() <= MAX_QUEUED_FRAMES);
|
||||||
|
if (queue.size() == MAX_QUEUED_FRAMES) {
|
||||||
|
AVFrame* frame = queue.dequeue();
|
||||||
|
av_frame_free(&frame);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void Pacer::submitFrame(AVFrame* frame)
|
void Pacer::submitFrame(AVFrame* frame)
|
||||||
{
|
{
|
||||||
// Make sure initialize() has been called
|
// Make sure initialize() has been called
|
||||||
SDL_assert(m_MaxVideoFps != 0);
|
SDL_assert(m_MaxVideoFps != 0);
|
||||||
|
|
||||||
// Queue the frame until the V-sync callback if
|
// Queue the frame and possibly wake up the render thread
|
||||||
// we have a V-sync source, otherwise deliver it
|
m_FrameQueueLock.lock();
|
||||||
// immediately and hope for the best.
|
if (m_VsyncSource != nullptr) {
|
||||||
if (isUsingFrameQueue()) {
|
dropFrameForEnqueue(m_PacingQueue);
|
||||||
SDL_AtomicLock(&m_FrameQueueLock);
|
m_PacingQueue.enqueue(frame);
|
||||||
m_FrameQueue.enqueue(frame);
|
|
||||||
SDL_AtomicUnlock(&m_FrameQueueLock);
|
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
Uint32 beforeRender = SDL_GetTicks();
|
dropFrameForEnqueue(m_RenderQueue);
|
||||||
m_VsyncRenderer->renderFrameAtVsync(frame);
|
m_RenderQueue.enqueue(frame);
|
||||||
m_VideoStats->totalRenderTime += SDL_GetTicks() - beforeRender;
|
}
|
||||||
m_VideoStats->renderedFrames++;
|
m_FrameQueueLock.unlock();
|
||||||
av_frame_free(&frame);
|
if (m_VsyncSource != nullptr) {
|
||||||
|
m_PacingQueueNotEmpty.wakeOne();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
m_RenderQueueNotEmpty.wakeOne();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bool Pacer::isUsingFrameQueue()
|
|
||||||
{
|
|
||||||
return m_VsyncSource != nullptr;
|
|
||||||
}
|
|
||||||
@@ -4,6 +4,8 @@
|
|||||||
#include "../renderer.h"
|
#include "../renderer.h"
|
||||||
|
|
||||||
#include <QQueue>
|
#include <QQueue>
|
||||||
|
#include <QMutex>
|
||||||
|
#include <QWaitCondition>
|
||||||
|
|
||||||
class IVsyncSource {
|
class IVsyncSource {
|
||||||
public:
|
public:
|
||||||
@@ -24,12 +26,22 @@ public:
|
|||||||
|
|
||||||
void vsyncCallback(int timeUntilNextVsyncMillis);
|
void vsyncCallback(int timeUntilNextVsyncMillis);
|
||||||
|
|
||||||
bool isUsingFrameQueue();
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
QQueue<AVFrame*> m_FrameQueue;
|
static int renderThread(void* context);
|
||||||
QQueue<int> m_FrameQueueHistory;
|
|
||||||
SDL_SpinLock m_FrameQueueLock;
|
void renderFrame(AVFrame* frame);
|
||||||
|
|
||||||
|
void dropFrameForEnqueue(QQueue<AVFrame*>& queue);
|
||||||
|
|
||||||
|
QQueue<AVFrame*> m_RenderQueue;
|
||||||
|
QQueue<AVFrame*> m_PacingQueue;
|
||||||
|
QQueue<int> m_PacingQueueHistory;
|
||||||
|
QQueue<int> m_RenderQueueHistory;
|
||||||
|
QMutex m_FrameQueueLock;
|
||||||
|
QWaitCondition m_RenderQueueNotEmpty;
|
||||||
|
QWaitCondition m_PacingQueueNotEmpty;
|
||||||
|
SDL_Thread* m_RenderThread;
|
||||||
|
bool m_Stopping;
|
||||||
|
|
||||||
IVsyncSource* m_VsyncSource;
|
IVsyncSource* m_VsyncSource;
|
||||||
IFFmpegRenderer* m_VsyncRenderer;
|
IFFmpegRenderer* m_VsyncRenderer;
|
||||||
|
|||||||
@@ -2,11 +2,13 @@
|
|||||||
|
|
||||||
#include <SDL.h>
|
#include <SDL.h>
|
||||||
|
|
||||||
|
#include "streaming/video/overlaymanager.h"
|
||||||
|
|
||||||
extern "C" {
|
extern "C" {
|
||||||
#include <libavcodec/avcodec.h>
|
#include <libavcodec/avcodec.h>
|
||||||
}
|
}
|
||||||
|
|
||||||
class IFFmpegRenderer {
|
class IFFmpegRenderer : public Overlay::IOverlayRenderer {
|
||||||
public:
|
public:
|
||||||
enum FramePacingConstraint {
|
enum FramePacingConstraint {
|
||||||
PACING_FORCE_OFF,
|
PACING_FORCE_OFF,
|
||||||
@@ -14,7 +16,6 @@ public:
|
|||||||
PACING_ANY
|
PACING_ANY
|
||||||
};
|
};
|
||||||
|
|
||||||
virtual ~IFFmpegRenderer() {}
|
|
||||||
virtual bool initialize(SDL_Window* window,
|
virtual bool initialize(SDL_Window* window,
|
||||||
int videoFormat,
|
int videoFormat,
|
||||||
int width,
|
int width,
|
||||||
@@ -22,29 +23,13 @@ public:
|
|||||||
int maxFps,
|
int maxFps,
|
||||||
bool enableVsync) = 0;
|
bool enableVsync) = 0;
|
||||||
virtual bool prepareDecoderContext(AVCodecContext* context) = 0;
|
virtual bool prepareDecoderContext(AVCodecContext* context) = 0;
|
||||||
virtual void renderFrameAtVsync(AVFrame* frame) = 0;
|
virtual void renderFrame(AVFrame* frame) = 0;
|
||||||
virtual bool needsTestFrame() = 0;
|
virtual bool needsTestFrame() = 0;
|
||||||
virtual int getDecoderCapabilities() = 0;
|
virtual int getDecoderCapabilities() = 0;
|
||||||
virtual FramePacingConstraint getFramePacingConstraint() = 0;
|
virtual FramePacingConstraint getFramePacingConstraint() = 0;
|
||||||
};
|
|
||||||
|
|
||||||
class SdlRenderer : public IFFmpegRenderer {
|
// IOverlayRenderer
|
||||||
public:
|
virtual void notifyOverlayUpdated(Overlay::OverlayType) override {
|
||||||
SdlRenderer();
|
// Nothing
|
||||||
virtual ~SdlRenderer();
|
}
|
||||||
virtual bool initialize(SDL_Window* window,
|
|
||||||
int videoFormat,
|
|
||||||
int width,
|
|
||||||
int height,
|
|
||||||
int maxFps,
|
|
||||||
bool enableVsync);
|
|
||||||
virtual bool prepareDecoderContext(AVCodecContext* context);
|
|
||||||
virtual void renderFrameAtVsync(AVFrame* frame);
|
|
||||||
virtual bool needsTestFrame();
|
|
||||||
virtual int getDecoderCapabilities();
|
|
||||||
virtual FramePacingConstraint getFramePacingConstraint();
|
|
||||||
|
|
||||||
private:
|
|
||||||
SDL_Renderer* m_Renderer;
|
|
||||||
SDL_Texture* m_Texture;
|
|
||||||
};
|
};
|
||||||
@@ -1,4 +1,9 @@
|
|||||||
#include "renderer.h"
|
#include "sdlvid.h"
|
||||||
|
|
||||||
|
#include "streaming/session.h"
|
||||||
|
#include "path.h"
|
||||||
|
|
||||||
|
#include <QDir>
|
||||||
|
|
||||||
#include <Limelight.h>
|
#include <Limelight.h>
|
||||||
|
|
||||||
@@ -6,11 +11,42 @@ SdlRenderer::SdlRenderer()
|
|||||||
: m_Renderer(nullptr),
|
: m_Renderer(nullptr),
|
||||||
m_Texture(nullptr)
|
m_Texture(nullptr)
|
||||||
{
|
{
|
||||||
|
SDL_assert(TTF_WasInit() == 0);
|
||||||
|
if (TTF_Init() != 0) {
|
||||||
|
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"TTF_Init() failed: %s",
|
||||||
|
TTF_GetError());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
SDL_zero(m_OverlayFonts);
|
||||||
|
SDL_zero(m_OverlaySurfaces);
|
||||||
|
SDL_zero(m_OverlayTextures);
|
||||||
}
|
}
|
||||||
|
|
||||||
SdlRenderer::~SdlRenderer()
|
SdlRenderer::~SdlRenderer()
|
||||||
{
|
{
|
||||||
|
for (int i = 0; i < Overlay::OverlayMax; i++) {
|
||||||
|
if (m_OverlayFonts[i] != nullptr) {
|
||||||
|
TTF_CloseFont(m_OverlayFonts[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TTF_Quit();
|
||||||
|
SDL_assert(TTF_WasInit() == 0);
|
||||||
|
|
||||||
|
for (int i = 0; i < Overlay::OverlayMax; i++) {
|
||||||
|
if (m_OverlayTextures[i] != nullptr) {
|
||||||
|
SDL_DestroyTexture(m_OverlayTextures[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < Overlay::OverlayMax; i++) {
|
||||||
|
if (m_OverlaySurfaces[i] != nullptr) {
|
||||||
|
SDL_FreeSurface(m_OverlaySurfaces[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (m_Texture != nullptr) {
|
if (m_Texture != nullptr) {
|
||||||
SDL_DestroyTexture(m_Texture);
|
SDL_DestroyTexture(m_Texture);
|
||||||
}
|
}
|
||||||
@@ -48,6 +84,47 @@ IFFmpegRenderer::FramePacingConstraint SdlRenderer::getFramePacingConstraint()
|
|||||||
return PACING_ANY;
|
return PACING_ANY;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void SdlRenderer::notifyOverlayUpdated(Overlay::OverlayType type)
|
||||||
|
{
|
||||||
|
// Construct the required font to render the overlay
|
||||||
|
if (m_OverlayFonts[type] == nullptr) {
|
||||||
|
QByteArray fontData = Path::readDataFile("ModeSeven.ttf");
|
||||||
|
if (fontData.isEmpty()) {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Unable to load SDL overlay font");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_OverlayFonts[type] = TTF_OpenFontRW(SDL_RWFromConstMem(fontData.constData(), fontData.size()),
|
||||||
|
1,
|
||||||
|
Session::get()->getOverlayManager().getOverlayFontSize(type));
|
||||||
|
if (m_OverlayFonts[type] == nullptr) {
|
||||||
|
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"TTF_OpenFont() failed: %s",
|
||||||
|
TTF_GetError());
|
||||||
|
|
||||||
|
// Can't proceed without a font
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SDL_Surface* oldSurface = (SDL_Surface*)SDL_AtomicGetPtr((void**)&m_OverlaySurfaces[type]);
|
||||||
|
|
||||||
|
// Free the old surface
|
||||||
|
if (oldSurface != nullptr && SDL_AtomicCASPtr((void**)&m_OverlaySurfaces[type], oldSurface, nullptr)) {
|
||||||
|
SDL_FreeSurface(oldSurface);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Session::get()->getOverlayManager().isOverlayEnabled(type)) {
|
||||||
|
// The _Wrapped variant is required for line breaks to work
|
||||||
|
SDL_Surface* surface = TTF_RenderText_Blended_Wrapped(m_OverlayFonts[type],
|
||||||
|
Session::get()->getOverlayManager().getOverlayText(type),
|
||||||
|
Session::get()->getOverlayManager().getOverlayColor(type),
|
||||||
|
1000);
|
||||||
|
SDL_AtomicSetPtr((void**)&m_OverlaySurfaces[type], surface);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
bool SdlRenderer::initialize(SDL_Window* window,
|
bool SdlRenderer::initialize(SDL_Window* window,
|
||||||
int,
|
int,
|
||||||
int width,
|
int width,
|
||||||
@@ -66,6 +143,14 @@ bool SdlRenderer::initialize(SDL_Window* window,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#ifdef Q_OS_WIN32
|
||||||
|
// We render on a different thread than the main thread which is handling window
|
||||||
|
// messages. Without D3DCREATE_MULTITHREADED, this can cause a deadlock by blocking
|
||||||
|
// on a window message being processed while the main thread is blocked waiting for
|
||||||
|
// the render thread to finish.
|
||||||
|
SDL_SetHintWithPriority(SDL_HINT_RENDER_DIRECT3D_THREADSAFE, "1", SDL_HINT_OVERRIDE);
|
||||||
|
#endif
|
||||||
|
|
||||||
m_Renderer = SDL_CreateRenderer(window, -1, rendererFlags);
|
m_Renderer = SDL_CreateRenderer(window, -1, rendererFlags);
|
||||||
if (!m_Renderer) {
|
if (!m_Renderer) {
|
||||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
@@ -95,10 +180,58 @@ bool SdlRenderer::initialize(SDL_Window* window,
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#ifdef Q_OS_WIN32
|
||||||
|
// For some reason, using Direct3D9Ex breaks this with multi-monitor setups.
|
||||||
|
// When focus is lost, the window is minimized then immediately restored without
|
||||||
|
// input focus. This glitches out the renderer and a bunch of other stuff.
|
||||||
|
// Direct3D9Ex itself seems to have this minimize on focus loss behavior on its
|
||||||
|
// own, so just disable SDL's handling of the focus loss event.
|
||||||
|
SDL_SetHintWithPriority(SDL_HINT_VIDEO_MINIMIZE_ON_FOCUS_LOSS, "0", SDL_HINT_OVERRIDE);
|
||||||
|
#endif
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
void SdlRenderer::renderFrameAtVsync(AVFrame* frame)
|
void SdlRenderer::renderOverlay(Overlay::OverlayType type)
|
||||||
|
{
|
||||||
|
if (Session::get()->getOverlayManager().isOverlayEnabled(type)) {
|
||||||
|
// If a new surface has been created for updated overlay data, convert it into a texture.
|
||||||
|
// NB: We have to do this conversion at render-time because we can only interact
|
||||||
|
// with the renderer on a single thread.
|
||||||
|
SDL_Surface* surface = (SDL_Surface*)SDL_AtomicGetPtr((void**)&m_OverlaySurfaces[type]);
|
||||||
|
if (surface != nullptr && SDL_AtomicCASPtr((void**)&m_OverlaySurfaces[type], surface, nullptr)) {
|
||||||
|
if (m_OverlayTextures[type] != nullptr) {
|
||||||
|
SDL_DestroyTexture(m_OverlayTextures[type]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type == Overlay::OverlayStatusUpdate) {
|
||||||
|
// Bottom Left
|
||||||
|
int unused;
|
||||||
|
SDL_RenderGetLogicalSize(m_Renderer, &unused, &m_OverlayRects[type].y);
|
||||||
|
m_OverlayRects[type].x = 0;
|
||||||
|
m_OverlayRects[type].y -= surface->h;
|
||||||
|
}
|
||||||
|
else if (type == Overlay::OverlayDebug) {
|
||||||
|
// Top left
|
||||||
|
m_OverlayRects[type].x = 0;
|
||||||
|
m_OverlayRects[type].y = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_OverlayRects[type].w = surface->w;
|
||||||
|
m_OverlayRects[type].h = surface->h;
|
||||||
|
|
||||||
|
m_OverlayTextures[type] = SDL_CreateTextureFromSurface(m_Renderer, surface);
|
||||||
|
SDL_FreeSurface(surface);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we have an overlay texture, render it too
|
||||||
|
if (m_OverlayTextures[type] != nullptr) {
|
||||||
|
SDL_RenderCopy(m_Renderer, m_OverlayTextures[type], nullptr, &m_OverlayRects[type]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void SdlRenderer::renderFrame(AVFrame* frame)
|
||||||
{
|
{
|
||||||
SDL_UpdateYUVTexture(m_Texture, nullptr,
|
SDL_UpdateYUVTexture(m_Texture, nullptr,
|
||||||
frame->data[0],
|
frame->data[0],
|
||||||
@@ -109,6 +242,14 @@ void SdlRenderer::renderFrameAtVsync(AVFrame* frame)
|
|||||||
frame->linesize[2]);
|
frame->linesize[2]);
|
||||||
|
|
||||||
SDL_RenderClear(m_Renderer);
|
SDL_RenderClear(m_Renderer);
|
||||||
|
|
||||||
|
// Draw the video content itself
|
||||||
SDL_RenderCopy(m_Renderer, m_Texture, nullptr, nullptr);
|
SDL_RenderCopy(m_Renderer, m_Texture, nullptr, nullptr);
|
||||||
|
|
||||||
|
// Draw the overlays
|
||||||
|
for (int i = 0; i < Overlay::OverlayMax; i++) {
|
||||||
|
renderOverlay((Overlay::OverlayType)i);
|
||||||
|
}
|
||||||
|
|
||||||
SDL_RenderPresent(m_Renderer);
|
SDL_RenderPresent(m_Renderer);
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "renderer.h"
|
||||||
|
|
||||||
|
#include <SDL_ttf.h>
|
||||||
|
|
||||||
|
class SdlRenderer : public IFFmpegRenderer {
|
||||||
|
public:
|
||||||
|
SdlRenderer();
|
||||||
|
virtual ~SdlRenderer() override;
|
||||||
|
virtual bool initialize(SDL_Window* window,
|
||||||
|
int videoFormat,
|
||||||
|
int width,
|
||||||
|
int height,
|
||||||
|
int maxFps,
|
||||||
|
bool enableVsync) override;
|
||||||
|
virtual bool prepareDecoderContext(AVCodecContext* context) override;
|
||||||
|
virtual void renderFrame(AVFrame* frame) override;
|
||||||
|
virtual bool needsTestFrame() override;
|
||||||
|
virtual int getDecoderCapabilities() override;
|
||||||
|
virtual FramePacingConstraint getFramePacingConstraint() override;
|
||||||
|
virtual void notifyOverlayUpdated(Overlay::OverlayType) override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
void renderOverlay(Overlay::OverlayType type);
|
||||||
|
|
||||||
|
SDL_Renderer* m_Renderer;
|
||||||
|
SDL_Texture* m_Texture;
|
||||||
|
TTF_Font* m_OverlayFonts[Overlay::OverlayMax];
|
||||||
|
SDL_Surface* m_OverlaySurfaces[Overlay::OverlayMax];
|
||||||
|
SDL_Texture* m_OverlayTextures[Overlay::OverlayMax];
|
||||||
|
SDL_Rect m_OverlayRects[Overlay::OverlayMax];
|
||||||
|
};
|
||||||
|
|
||||||
@@ -183,7 +183,7 @@ IFFmpegRenderer::FramePacingConstraint VAAPIRenderer::getFramePacingConstraint()
|
|||||||
}
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
VAAPIRenderer::renderFrameAtVsync(AVFrame* frame)
|
VAAPIRenderer::renderFrame(AVFrame* frame)
|
||||||
{
|
{
|
||||||
VASurfaceID surface = (VASurfaceID)(uintptr_t)frame->data[3];
|
VASurfaceID surface = (VASurfaceID)(uintptr_t)frame->data[3];
|
||||||
AVHWDeviceContext* deviceContext = (AVHWDeviceContext*)m_HwContext->data;
|
AVHWDeviceContext* deviceContext = (AVHWDeviceContext*)m_HwContext->data;
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ public:
|
|||||||
int maxFps,
|
int maxFps,
|
||||||
bool enableVsync);
|
bool enableVsync);
|
||||||
virtual bool prepareDecoderContext(AVCodecContext* context);
|
virtual bool prepareDecoderContext(AVCodecContext* context);
|
||||||
virtual void renderFrameAtVsync(AVFrame* frame);
|
virtual void renderFrame(AVFrame* frame);
|
||||||
virtual bool needsTestFrame();
|
virtual bool needsTestFrame();
|
||||||
virtual int getDecoderCapabilities();
|
virtual int getDecoderCapabilities();
|
||||||
virtual FramePacingConstraint getFramePacingConstraint();
|
virtual FramePacingConstraint getFramePacingConstraint();
|
||||||
|
|||||||
@@ -251,7 +251,7 @@ IFFmpegRenderer::FramePacingConstraint VDPAURenderer::getFramePacingConstraint()
|
|||||||
return PACING_ANY;
|
return PACING_ANY;
|
||||||
}
|
}
|
||||||
|
|
||||||
void VDPAURenderer::renderFrameAtVsync(AVFrame* frame)
|
void VDPAURenderer::renderFrame(AVFrame* frame)
|
||||||
{
|
{
|
||||||
VdpStatus status;
|
VdpStatus status;
|
||||||
VdpVideoSurface videoSurface = (VdpVideoSurface)(uintptr_t)frame->data[3];
|
VdpVideoSurface videoSurface = (VdpVideoSurface)(uintptr_t)frame->data[3];
|
||||||
@@ -335,8 +335,8 @@ void VDPAURenderer::renderFrameAtVsync(AVFrame* frame)
|
|||||||
0, nullptr,
|
0, nullptr,
|
||||||
&sourceRect,
|
&sourceRect,
|
||||||
chosenSurface,
|
chosenSurface,
|
||||||
&outputRect,
|
|
||||||
nullptr,
|
nullptr,
|
||||||
|
&outputRect,
|
||||||
0,
|
0,
|
||||||
nullptr);
|
nullptr);
|
||||||
if (status != VDP_STATUS_OK) {
|
if (status != VDP_STATUS_OK) {
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ public:
|
|||||||
int maxFps,
|
int maxFps,
|
||||||
bool enableVsync);
|
bool enableVsync);
|
||||||
virtual bool prepareDecoderContext(AVCodecContext* context);
|
virtual bool prepareDecoderContext(AVCodecContext* context);
|
||||||
virtual void renderFrameAtVsync(AVFrame* frame);
|
virtual void renderFrame(AVFrame* frame);
|
||||||
virtual bool needsTestFrame();
|
virtual bool needsTestFrame();
|
||||||
virtual int getDecoderCapabilities();
|
virtual int getDecoderCapabilities();
|
||||||
virtual FramePacingConstraint getFramePacingConstraint();
|
virtual FramePacingConstraint getFramePacingConstraint();
|
||||||
|
|||||||
@@ -7,11 +7,13 @@
|
|||||||
|
|
||||||
#include <SDL_syswm.h>
|
#include <SDL_syswm.h>
|
||||||
#include <Limelight.h>
|
#include <Limelight.h>
|
||||||
|
#include <streaming/session.h>
|
||||||
|
|
||||||
#include <mach/mach_time.h>
|
#include <mach/mach_time.h>
|
||||||
#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 <dispatch/dispatch.h>
|
||||||
|
|
||||||
class VTRenderer : public IFFmpegRenderer
|
class VTRenderer : public IFFmpegRenderer
|
||||||
{
|
{
|
||||||
@@ -20,11 +22,12 @@ public:
|
|||||||
: m_HwContext(nullptr),
|
: m_HwContext(nullptr),
|
||||||
m_DisplayLayer(nullptr),
|
m_DisplayLayer(nullptr),
|
||||||
m_FormatDesc(nullptr),
|
m_FormatDesc(nullptr),
|
||||||
m_View(nullptr)
|
m_StreamView(nullptr)
|
||||||
{
|
{
|
||||||
|
SDL_zero(m_OverlayTextFields);
|
||||||
}
|
}
|
||||||
|
|
||||||
virtual ~VTRenderer()
|
virtual ~VTRenderer() override
|
||||||
{
|
{
|
||||||
if (m_HwContext != nullptr) {
|
if (m_HwContext != nullptr) {
|
||||||
av_buffer_unref(&m_HwContext);
|
av_buffer_unref(&m_HwContext);
|
||||||
@@ -34,13 +37,19 @@ public:
|
|||||||
CFRelease(m_FormatDesc);
|
CFRelease(m_FormatDesc);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (m_View != nullptr) {
|
for (int i = 0; i < Overlay::OverlayMax; i++) {
|
||||||
[m_View removeFromSuperview];
|
if (m_OverlayTextFields[i] != nullptr) {
|
||||||
|
[m_OverlayTextFields[i] removeFromSuperview];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (m_StreamView != nullptr) {
|
||||||
|
[m_StreamView removeFromSuperview];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Caller frees frame after we return
|
// Caller frees frame after we return
|
||||||
virtual void renderFrameAtVsync(AVFrame* frame) override
|
virtual void renderFrame(AVFrame* frame) override
|
||||||
{
|
{
|
||||||
OSStatus status;
|
OSStatus status;
|
||||||
CVPixelBufferRef pixBuf = reinterpret_cast<CVPixelBufferRef>(frame->data[3]);
|
CVPixelBufferRef pixBuf = reinterpret_cast<CVPixelBufferRef>(frame->data[3]);
|
||||||
@@ -159,21 +168,21 @@ public:
|
|||||||
// SDL adds its own content view to listen for events.
|
// SDL adds its own content view to listen for events.
|
||||||
// We need to add a subview for our display layer.
|
// We need to add a subview for our display layer.
|
||||||
NSView* contentView = info.info.cocoa.window.contentView;
|
NSView* contentView = info.info.cocoa.window.contentView;
|
||||||
m_View = [[NSView alloc] initWithFrame:contentView.bounds];
|
m_StreamView = [[NSView alloc] initWithFrame:contentView.bounds];
|
||||||
|
|
||||||
m_DisplayLayer = [[AVSampleBufferDisplayLayer alloc] init];
|
m_DisplayLayer = [[AVSampleBufferDisplayLayer alloc] init];
|
||||||
m_DisplayLayer.bounds = m_View.bounds;
|
m_DisplayLayer.bounds = m_StreamView.bounds;
|
||||||
m_DisplayLayer.position = CGPointMake(CGRectGetMidX(m_View.bounds), CGRectGetMidY(m_View.bounds));
|
m_DisplayLayer.position = CGPointMake(CGRectGetMidX(m_StreamView.bounds), CGRectGetMidY(m_StreamView.bounds));
|
||||||
m_DisplayLayer.videoGravity = AVLayerVideoGravityResizeAspect;
|
m_DisplayLayer.videoGravity = AVLayerVideoGravityResizeAspect;
|
||||||
|
|
||||||
// Create a layer-hosted view by setting the layer before wantsLayer
|
// Create a layer-hosted view by setting the layer before wantsLayer
|
||||||
// This avoids us having to add our AVSampleBufferDisplayLayer as a
|
// This avoids us having to add our AVSampleBufferDisplayLayer as a
|
||||||
// sublayer of a layer-backed view which leaves a useless layer in
|
// sublayer of a layer-backed view which leaves a useless layer in
|
||||||
// the middle.
|
// the middle.
|
||||||
m_View.layer = m_DisplayLayer;
|
m_StreamView.layer = m_DisplayLayer;
|
||||||
m_View.wantsLayer = YES;
|
m_StreamView.wantsLayer = YES;
|
||||||
|
|
||||||
[contentView addSubview: m_View];
|
[contentView addSubview: m_StreamView];
|
||||||
|
|
||||||
err = av_hwdevice_ctx_create(&m_HwContext,
|
err = av_hwdevice_ctx_create(&m_HwContext,
|
||||||
AV_HWDEVICE_TYPE_VIDEOTOOLBOX,
|
AV_HWDEVICE_TYPE_VIDEOTOOLBOX,
|
||||||
@@ -190,6 +199,71 @@ public:
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void updateOverlayOnMainThread(Overlay::OverlayType type)
|
||||||
|
{
|
||||||
|
// Lazy initialization for the overlay
|
||||||
|
if (m_OverlayTextFields[type] == nullptr) {
|
||||||
|
m_OverlayTextFields[type] = [[NSTextField alloc] initWithFrame:m_StreamView.bounds];
|
||||||
|
[m_OverlayTextFields[type] setBezeled:NO];
|
||||||
|
[m_OverlayTextFields[type] setDrawsBackground:NO];
|
||||||
|
[m_OverlayTextFields[type] setEditable:NO];
|
||||||
|
[m_OverlayTextFields[type] setSelectable:NO];
|
||||||
|
|
||||||
|
switch (type) {
|
||||||
|
case Overlay::OverlayDebug:
|
||||||
|
[m_OverlayTextFields[type] setAlignment:NSLeftTextAlignment];
|
||||||
|
break;
|
||||||
|
case Overlay::OverlayStatusUpdate:
|
||||||
|
[m_OverlayTextFields[type] setAlignment:NSRightTextAlignment];
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
SDL_Color color = Session::get()->getOverlayManager().getOverlayColor(type);
|
||||||
|
[m_OverlayTextFields[type] setTextColor:[NSColor colorWithSRGBRed:color.r / 255.0 green:color.g / 255.0 blue:color.b / 255.0 alpha:color.a / 255.0]];
|
||||||
|
[m_OverlayTextFields[type] setFont:[NSFont messageFontOfSize:Session::get()->getOverlayManager().getOverlayFontSize(type)]];
|
||||||
|
|
||||||
|
[m_StreamView addSubview: m_OverlayTextFields[type]];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update text contents
|
||||||
|
[m_OverlayTextFields[type] setStringValue: [NSString stringWithUTF8String:Session::get()->getOverlayManager().getOverlayText(type)]];
|
||||||
|
|
||||||
|
// Unhide if it's enabled
|
||||||
|
[m_OverlayTextFields[type] setHidden: !Session::get()->getOverlayManager().isOverlayEnabled(type)];
|
||||||
|
}
|
||||||
|
|
||||||
|
static void updateDebugOverlayOnMainThread(void* context)
|
||||||
|
{
|
||||||
|
VTRenderer* me = (VTRenderer*)context;
|
||||||
|
|
||||||
|
me->updateOverlayOnMainThread(Overlay::OverlayDebug);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void updateStatusOverlayOnMainThread(void* context)
|
||||||
|
{
|
||||||
|
VTRenderer* me = (VTRenderer*)context;
|
||||||
|
|
||||||
|
me->updateOverlayOnMainThread(Overlay::OverlayStatusUpdate);
|
||||||
|
}
|
||||||
|
|
||||||
|
virtual void notifyOverlayUpdated(Overlay::OverlayType type) override
|
||||||
|
{
|
||||||
|
// We must do the actual UI updates on the main thread, so queue an
|
||||||
|
// async callback on the main thread via GCD to do the UI update.
|
||||||
|
switch (type) {
|
||||||
|
case Overlay::OverlayDebug:
|
||||||
|
dispatch_async_f(dispatch_get_main_queue(), this, updateDebugOverlayOnMainThread);
|
||||||
|
break;
|
||||||
|
case Overlay::OverlayStatusUpdate:
|
||||||
|
dispatch_async_f(dispatch_get_main_queue(), this, updateStatusOverlayOnMainThread);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
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);
|
||||||
@@ -202,8 +276,11 @@ public:
|
|||||||
|
|
||||||
virtual bool needsTestFrame() override
|
virtual bool needsTestFrame() override
|
||||||
{
|
{
|
||||||
// We query VT to determine whether the codec is supported
|
// We used to trust VT to tell us whether decode will work, but
|
||||||
return false;
|
// there are cases where it can lie because the hardware technically
|
||||||
|
// can decode the format but VT is unserviceable for some other reason.
|
||||||
|
// Decoding the test frame will tell us for sure whether it will work.
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
virtual int getDecoderCapabilities() override
|
virtual int getDecoderCapabilities() override
|
||||||
@@ -223,7 +300,8 @@ private:
|
|||||||
AVBufferRef* m_HwContext;
|
AVBufferRef* m_HwContext;
|
||||||
AVSampleBufferDisplayLayer* m_DisplayLayer;
|
AVSampleBufferDisplayLayer* m_DisplayLayer;
|
||||||
CMVideoFormatDescriptionRef m_FormatDesc;
|
CMVideoFormatDescriptionRef m_FormatDesc;
|
||||||
NSView* m_View;
|
NSView* m_StreamView;
|
||||||
|
NSTextField* m_OverlayTextFields[Overlay::OverlayMax];
|
||||||
};
|
};
|
||||||
|
|
||||||
IFFmpegRenderer* VTRendererFactory::createRenderer() {
|
IFFmpegRenderer* VTRendererFactory::createRenderer() {
|
||||||
|
|||||||
@@ -1,8 +1,12 @@
|
|||||||
#include <Limelight.h>
|
#include <Limelight.h>
|
||||||
#include "ffmpeg.h"
|
#include "ffmpeg.h"
|
||||||
#include "streaming/streamutils.h"
|
#include "streaming/streamutils.h"
|
||||||
|
#include "streaming/session.h"
|
||||||
|
|
||||||
#include <h264_stream.h>
|
#include <h264_stream.h>
|
||||||
|
|
||||||
|
#include "ffmpeg-renderers/sdlvid.h"
|
||||||
|
|
||||||
#ifdef Q_OS_WIN32
|
#ifdef Q_OS_WIN32
|
||||||
#include "ffmpeg-renderers/dxva2.h"
|
#include "ffmpeg-renderers/dxva2.h"
|
||||||
#endif
|
#endif
|
||||||
@@ -57,7 +61,7 @@ enum AVPixelFormat FFmpegVideoDecoder::ffGetFormat(AVCodecContext* context,
|
|||||||
return AV_PIX_FMT_NONE;
|
return AV_PIX_FMT_NONE;
|
||||||
}
|
}
|
||||||
|
|
||||||
FFmpegVideoDecoder::FFmpegVideoDecoder()
|
FFmpegVideoDecoder::FFmpegVideoDecoder(bool testOnly)
|
||||||
: m_VideoDecoderCtx(nullptr),
|
: m_VideoDecoderCtx(nullptr),
|
||||||
m_DecodeBuffer(1024 * 1024, 0),
|
m_DecodeBuffer(1024 * 1024, 0),
|
||||||
m_HwDecodeCfg(nullptr),
|
m_HwDecodeCfg(nullptr),
|
||||||
@@ -66,10 +70,10 @@ FFmpegVideoDecoder::FFmpegVideoDecoder()
|
|||||||
m_Pacer(nullptr),
|
m_Pacer(nullptr),
|
||||||
m_LastFrameNumber(0),
|
m_LastFrameNumber(0),
|
||||||
m_StreamFps(0),
|
m_StreamFps(0),
|
||||||
m_NeedsSpsFixup(false)
|
m_NeedsSpsFixup(false),
|
||||||
|
m_TestOnly(testOnly)
|
||||||
{
|
{
|
||||||
av_init_packet(&m_Pkt);
|
av_init_packet(&m_Pkt);
|
||||||
SDL_AtomicSet(&m_QueuedFrames, 0);
|
|
||||||
|
|
||||||
SDL_zero(m_ActiveWndVideoStats);
|
SDL_zero(m_ActiveWndVideoStats);
|
||||||
SDL_zero(m_LastWndVideoStats);
|
SDL_zero(m_LastWndVideoStats);
|
||||||
@@ -82,6 +86,12 @@ FFmpegVideoDecoder::FFmpegVideoDecoder()
|
|||||||
FFmpegVideoDecoder::~FFmpegVideoDecoder()
|
FFmpegVideoDecoder::~FFmpegVideoDecoder()
|
||||||
{
|
{
|
||||||
reset();
|
reset();
|
||||||
|
|
||||||
|
// Set log level back to default.
|
||||||
|
// NB: We don't do this in reset() because we want
|
||||||
|
// to preserve the log level across reset() during
|
||||||
|
// test initialization.
|
||||||
|
av_log_set_level(AV_LOG_INFO);
|
||||||
}
|
}
|
||||||
|
|
||||||
IFFmpegRenderer* FFmpegVideoDecoder::getRenderer()
|
IFFmpegRenderer* FFmpegVideoDecoder::getRenderer()
|
||||||
@@ -91,28 +101,6 @@ IFFmpegRenderer* FFmpegVideoDecoder::getRenderer()
|
|||||||
|
|
||||||
void FFmpegVideoDecoder::reset()
|
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(10);
|
|
||||||
SDL_PumpEvents();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
delete m_Pacer;
|
delete m_Pacer;
|
||||||
m_Pacer = nullptr;
|
m_Pacer = nullptr;
|
||||||
|
|
||||||
@@ -123,16 +111,29 @@ void FFmpegVideoDecoder::reset()
|
|||||||
// need to delete in the renderer destructor.
|
// need to delete in the renderer destructor.
|
||||||
avcodec_free_context(&m_VideoDecoderCtx);
|
avcodec_free_context(&m_VideoDecoderCtx);
|
||||||
|
|
||||||
|
if (!m_TestOnly) {
|
||||||
|
Session::get()->getOverlayManager().setOverlayRenderer(nullptr);
|
||||||
|
}
|
||||||
|
|
||||||
delete m_Renderer;
|
delete m_Renderer;
|
||||||
m_Renderer = nullptr;
|
m_Renderer = nullptr;
|
||||||
|
|
||||||
logVideoStats(m_GlobalVideoStats, "Global video stats");
|
if (!m_TestOnly) {
|
||||||
|
logVideoStats(m_GlobalVideoStats, "Global video stats");
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Test-only decoders can't have any frames submitted
|
||||||
|
SDL_assert(m_GlobalVideoStats.totalFrames == 0);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bool FFmpegVideoDecoder::completeInitialization(AVCodec* decoder, SDL_Window* window,
|
bool FFmpegVideoDecoder::completeInitialization(AVCodec* decoder, SDL_Window* window,
|
||||||
int videoFormat, int width, int height,
|
int videoFormat, int width, int height,
|
||||||
int maxFps, bool enableFramePacing, bool testOnly)
|
int maxFps, bool enableFramePacing, bool testFrame)
|
||||||
{
|
{
|
||||||
|
// In test-only mode, we should only see test frames
|
||||||
|
SDL_assert(!m_TestOnly || testFrame);
|
||||||
|
|
||||||
auto vsyncConstraint = m_Renderer->getFramePacingConstraint();
|
auto vsyncConstraint = m_Renderer->getFramePacingConstraint();
|
||||||
if (vsyncConstraint == IFFmpegRenderer::PACING_FORCE_OFF && enableFramePacing) {
|
if (vsyncConstraint == IFFmpegRenderer::PACING_FORCE_OFF && enableFramePacing) {
|
||||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
@@ -150,9 +151,13 @@ bool FFmpegVideoDecoder::completeInitialization(AVCodec* decoder, SDL_Window* wi
|
|||||||
}
|
}
|
||||||
|
|
||||||
m_StreamFps = maxFps;
|
m_StreamFps = maxFps;
|
||||||
m_Pacer = new Pacer(m_Renderer, &m_ActiveWndVideoStats);
|
|
||||||
if (!m_Pacer->initialize(window, maxFps, enableFramePacing)) {
|
// Don't bother initializing Pacer if we're not actually going to render
|
||||||
return false;
|
if (!testFrame) {
|
||||||
|
m_Pacer = new Pacer(m_Renderer, &m_ActiveWndVideoStats);
|
||||||
|
if (!m_Pacer->initialize(window, maxFps, enableFramePacing)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
m_VideoDecoderCtx = avcodec_alloc_context3(decoder);
|
m_VideoDecoderCtx = avcodec_alloc_context3(decoder);
|
||||||
@@ -210,7 +215,7 @@ bool FFmpegVideoDecoder::completeInitialization(AVCodec* decoder, SDL_Window* wi
|
|||||||
// our minds on the selected video codec, so we'll do a trial run
|
// 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
|
// now to see if things will actually work when the video stream
|
||||||
// comes in.
|
// comes in.
|
||||||
if (testOnly) {
|
if (testFrame) {
|
||||||
if (videoFormat & VIDEO_FORMAT_MASK_H264) {
|
if (videoFormat & VIDEO_FORMAT_MASK_H264) {
|
||||||
m_Pkt.data = (uint8_t*)k_H264TestFrame;
|
m_Pkt.data = (uint8_t*)k_H264TestFrame;
|
||||||
m_Pkt.size = sizeof(k_H264TestFrame);
|
m_Pkt.size = sizeof(k_H264TestFrame);
|
||||||
@@ -239,12 +244,10 @@ bool FFmpegVideoDecoder::completeInitialization(AVCodec* decoder, SDL_Window* wi
|
|||||||
else {
|
else {
|
||||||
m_NeedsSpsFixup = false;
|
m_NeedsSpsFixup = false;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
#ifdef QT_DEBUG
|
// Tell overlay manager to use this renderer
|
||||||
// Restore default log level before streaming
|
Session::get()->getOverlayManager().setOverlayRenderer(m_Renderer);
|
||||||
av_log_set_level(AV_LOG_INFO);
|
}
|
||||||
#endif
|
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -254,6 +257,7 @@ void FFmpegVideoDecoder::addVideoStats(VIDEO_STATS& src, VIDEO_STATS& dst)
|
|||||||
dst.receivedFrames += src.receivedFrames;
|
dst.receivedFrames += src.receivedFrames;
|
||||||
dst.decodedFrames += src.decodedFrames;
|
dst.decodedFrames += src.decodedFrames;
|
||||||
dst.renderedFrames += src.renderedFrames;
|
dst.renderedFrames += src.renderedFrames;
|
||||||
|
dst.totalFrames += src.totalFrames;
|
||||||
dst.networkDroppedFrames += src.networkDroppedFrames;
|
dst.networkDroppedFrames += src.networkDroppedFrames;
|
||||||
dst.pacerDroppedFrames += src.pacerDroppedFrames;
|
dst.pacerDroppedFrames += src.pacerDroppedFrames;
|
||||||
dst.totalReassemblyTime += src.totalReassemblyTime;
|
dst.totalReassemblyTime += src.totalReassemblyTime;
|
||||||
@@ -271,59 +275,59 @@ void FFmpegVideoDecoder::addVideoStats(VIDEO_STATS& src, VIDEO_STATS& dst)
|
|||||||
// The following code assumes the global measure was already started first
|
// The following code assumes the global measure was already started first
|
||||||
SDL_assert(dst.measurementStartTimestamp <= src.measurementStartTimestamp);
|
SDL_assert(dst.measurementStartTimestamp <= src.measurementStartTimestamp);
|
||||||
|
|
||||||
|
dst.totalFps = (float)dst.totalFrames / ((float)(now - dst.measurementStartTimestamp) / 1000);
|
||||||
dst.receivedFps = (float)dst.receivedFrames / ((float)(now - dst.measurementStartTimestamp) / 1000);
|
dst.receivedFps = (float)dst.receivedFrames / ((float)(now - dst.measurementStartTimestamp) / 1000);
|
||||||
dst.decodedFps = (float)dst.decodedFrames / ((float)(now - dst.measurementStartTimestamp) / 1000);
|
dst.decodedFps = (float)dst.decodedFrames / ((float)(now - dst.measurementStartTimestamp) / 1000);
|
||||||
dst.renderedFps = (float)dst.renderedFrames / ((float)(now - dst.measurementStartTimestamp) / 1000);
|
dst.renderedFps = (float)dst.renderedFrames / ((float)(now - dst.measurementStartTimestamp) / 1000);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void FFmpegVideoDecoder::stringifyVideoStats(VIDEO_STATS& stats, char* output)
|
||||||
|
{
|
||||||
|
int offset = 0;
|
||||||
|
|
||||||
|
// Start with an empty string
|
||||||
|
output[offset] = 0;
|
||||||
|
|
||||||
|
if (stats.receivedFps > 0) {
|
||||||
|
offset += sprintf(&output[offset],
|
||||||
|
"Estimated host PC frame rate: %.2f FPS\n"
|
||||||
|
"Incoming frame rate from network: %.2f FPS\n"
|
||||||
|
"Decoding frame rate: %.2f FPS\n"
|
||||||
|
"Rendering frame rate: %.2f FPS\n",
|
||||||
|
stats.totalFps,
|
||||||
|
stats.receivedFps,
|
||||||
|
stats.decodedFps,
|
||||||
|
stats.renderedFps);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stats.renderedFrames != 0) {
|
||||||
|
offset += sprintf(&output[offset],
|
||||||
|
"Frames dropped by your network connection: %.2f%%\n"
|
||||||
|
"Frames dropped due to network jitter: %.2f%%\n"
|
||||||
|
"Average receive time: %.2f ms\n"
|
||||||
|
"Average decoding time: %.2f ms\n"
|
||||||
|
"Average frame queue delay: %.2f ms\n"
|
||||||
|
"Average rendering time (including monitor V-sync latency): %.2f ms\n",
|
||||||
|
(float)stats.networkDroppedFrames / stats.totalFrames * 100,
|
||||||
|
(float)stats.pacerDroppedFrames / stats.decodedFrames * 100,
|
||||||
|
(float)stats.totalReassemblyTime / stats.receivedFrames,
|
||||||
|
(float)stats.totalDecodeTime / stats.decodedFrames,
|
||||||
|
(float)stats.totalPacerTime / stats.renderedFrames,
|
||||||
|
(float)stats.totalRenderTime / stats.renderedFrames);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void FFmpegVideoDecoder::logVideoStats(VIDEO_STATS& stats, const char* title)
|
void FFmpegVideoDecoder::logVideoStats(VIDEO_STATS& stats, const char* title)
|
||||||
{
|
{
|
||||||
if (stats.renderedFps > 0 || stats.renderedFrames != 0) {
|
if (stats.renderedFps > 0 || stats.renderedFrames != 0) {
|
||||||
|
char videoStatsStr[512];
|
||||||
|
stringifyVideoStats(stats, videoStatsStr);
|
||||||
|
|
||||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
"%s", title);
|
"%s", title);
|
||||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
"----------------------------------------------------------");
|
"----------------------------------------------------------\n%s",
|
||||||
}
|
videoStatsStr);
|
||||||
|
|
||||||
if (stats.renderedFps > 0) {
|
|
||||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
|
||||||
"Incoming frame rate from network: %.2f FPS",
|
|
||||||
stats.receivedFps);
|
|
||||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
|
||||||
"Decoding frame rate: %.2f FPS",
|
|
||||||
stats.decodedFps);
|
|
||||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
|
||||||
"Rendering frame rate: %.2f FPS",
|
|
||||||
stats.renderedFps);
|
|
||||||
}
|
|
||||||
if (stats.renderedFrames != 0) {
|
|
||||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
|
||||||
"Total frames received: %u",
|
|
||||||
stats.receivedFrames);
|
|
||||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
|
||||||
"Total frames decoded: %u",
|
|
||||||
stats.decodedFrames);
|
|
||||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
|
||||||
"Total frames rendered: %u",
|
|
||||||
stats.renderedFrames);
|
|
||||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
|
||||||
"Average reassembly time: %.2f ms",
|
|
||||||
(float)stats.totalReassemblyTime / stats.decodedFrames);
|
|
||||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
|
||||||
"Average decode time: %.2f ms",
|
|
||||||
(float)stats.totalDecodeTime / stats.decodedFrames);
|
|
||||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
|
||||||
"Average frame pacing delay: %.2f ms",
|
|
||||||
(float)stats.totalPacerTime / stats.renderedFrames);
|
|
||||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
|
||||||
"Average render time: %.2f ms",
|
|
||||||
(float)stats.totalRenderTime / stats.renderedFrames);
|
|
||||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
|
||||||
"Frames lost during network transmission: %.2f%%",
|
|
||||||
(float)stats.networkDroppedFrames / stats.decodedFrames);
|
|
||||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
|
||||||
"Frames dropped by frame pacing: %.2f%%",
|
|
||||||
(float)stats.pacerDroppedFrames / stats.decodedFrames);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -368,10 +372,8 @@ bool FFmpegVideoDecoder::initialize(
|
|||||||
{
|
{
|
||||||
AVCodec* decoder;
|
AVCodec* decoder;
|
||||||
|
|
||||||
#ifdef QT_DEBUG
|
// Increase log level until the first frame is decoded
|
||||||
// Increase log level during initialization
|
|
||||||
av_log_set_level(AV_LOG_DEBUG);
|
av_log_set_level(AV_LOG_DEBUG);
|
||||||
#endif
|
|
||||||
|
|
||||||
if (videoFormat & VIDEO_FORMAT_MASK_H264) {
|
if (videoFormat & VIDEO_FORMAT_MASK_H264) {
|
||||||
decoder = avcodec_find_decoder(AV_CODEC_ID_H264);
|
decoder = avcodec_find_decoder(AV_CODEC_ID_H264);
|
||||||
@@ -400,7 +402,7 @@ bool FFmpegVideoDecoder::initialize(
|
|||||||
m_Renderer = new SdlRenderer();
|
m_Renderer = new SdlRenderer();
|
||||||
if (vds != StreamingPreferences::VDS_FORCE_HARDWARE &&
|
if (vds != StreamingPreferences::VDS_FORCE_HARDWARE &&
|
||||||
m_Renderer->initialize(window, videoFormat, width, height, maxFps, enableVsync) &&
|
m_Renderer->initialize(window, videoFormat, width, height, maxFps, enableVsync) &&
|
||||||
completeInitialization(decoder, window, videoFormat, width, height, maxFps, enableFramePacing, false)) {
|
completeInitialization(decoder, window, videoFormat, width, height, maxFps, enableFramePacing, m_TestOnly)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
@@ -417,7 +419,13 @@ bool FFmpegVideoDecoder::initialize(
|
|||||||
m_HwDecodeCfg = config;
|
m_HwDecodeCfg = config;
|
||||||
// Initialize the hardware codec and submit a test frame if the renderer needs it
|
// Initialize the hardware codec and submit a test frame if the renderer needs it
|
||||||
if (m_Renderer->initialize(window, videoFormat, width, height, maxFps, enableVsync) &&
|
if (m_Renderer->initialize(window, videoFormat, width, height, maxFps, enableVsync) &&
|
||||||
completeInitialization(decoder, window, videoFormat, width, height, maxFps, enableFramePacing, m_Renderer->needsTestFrame())) {
|
completeInitialization(decoder, window, videoFormat, width, height, maxFps, enableFramePacing, m_TestOnly || m_Renderer->needsTestFrame())) {
|
||||||
|
if (m_TestOnly) {
|
||||||
|
// This decoder is only for testing capabilities, so don't bother
|
||||||
|
// creating a usable renderer
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
if (m_Renderer->needsTestFrame()) {
|
if (m_Renderer->needsTestFrame()) {
|
||||||
// The test worked, so now let's initialize it for real
|
// The test worked, so now let's initialize it for real
|
||||||
reset();
|
reset();
|
||||||
@@ -492,16 +500,30 @@ int FFmpegVideoDecoder::submitDecodeUnit(PDECODE_UNIT du)
|
|||||||
PLENTRY entry = du->bufferList;
|
PLENTRY entry = du->bufferList;
|
||||||
int err;
|
int err;
|
||||||
|
|
||||||
// Flip stats windows roughly every second
|
SDL_assert(!m_TestOnly);
|
||||||
if (m_ActiveWndVideoStats.receivedFrames == m_StreamFps) {
|
|
||||||
#if 0
|
|
||||||
VIDEO_STATS lastTwoWndStats = {};
|
|
||||||
addVideoStats(m_LastWndVideoStats, lastTwoWndStats);
|
|
||||||
addVideoStats(m_ActiveWndVideoStats, lastTwoWndStats);
|
|
||||||
|
|
||||||
// Print stats from the last 2 windows
|
if (!m_LastFrameNumber) {
|
||||||
logVideoStats(lastTwoWndStats, "Periodic video stats");
|
m_ActiveWndVideoStats.measurementStartTimestamp = SDL_GetTicks();
|
||||||
#endif
|
m_LastFrameNumber = du->frameNumber;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Any frame number greater than m_LastFrameNumber + 1 represents a dropped frame
|
||||||
|
m_ActiveWndVideoStats.networkDroppedFrames += du->frameNumber - (m_LastFrameNumber + 1);
|
||||||
|
m_ActiveWndVideoStats.totalFrames += du->frameNumber - (m_LastFrameNumber + 1);
|
||||||
|
m_LastFrameNumber = du->frameNumber;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Flip stats windows roughly every second
|
||||||
|
if (SDL_TICKS_PASSED(SDL_GetTicks(), m_ActiveWndVideoStats.measurementStartTimestamp + 1000)) {
|
||||||
|
// Update overlay stats if it's enabled
|
||||||
|
if (Session::get()->getOverlayManager().isOverlayEnabled(Overlay::OverlayDebug)) {
|
||||||
|
VIDEO_STATS lastTwoWndStats = {};
|
||||||
|
addVideoStats(m_LastWndVideoStats, lastTwoWndStats);
|
||||||
|
addVideoStats(m_ActiveWndVideoStats, lastTwoWndStats);
|
||||||
|
|
||||||
|
stringifyVideoStats(lastTwoWndStats, Session::get()->getOverlayManager().getOverlayText(Overlay::OverlayDebug));
|
||||||
|
Session::get()->getOverlayManager().setOverlayTextUpdated(Overlay::OverlayDebug);
|
||||||
|
}
|
||||||
|
|
||||||
// Accumulate these values into the global stats
|
// Accumulate these values into the global stats
|
||||||
addVideoStats(m_ActiveWndVideoStats, m_GlobalVideoStats);
|
addVideoStats(m_ActiveWndVideoStats, m_GlobalVideoStats);
|
||||||
@@ -513,16 +535,7 @@ int FFmpegVideoDecoder::submitDecodeUnit(PDECODE_UNIT du)
|
|||||||
}
|
}
|
||||||
|
|
||||||
m_ActiveWndVideoStats.receivedFrames++;
|
m_ActiveWndVideoStats.receivedFrames++;
|
||||||
|
m_ActiveWndVideoStats.totalFrames++;
|
||||||
if (!m_LastFrameNumber) {
|
|
||||||
m_ActiveWndVideoStats.measurementStartTimestamp = SDL_GetTicks();
|
|
||||||
m_LastFrameNumber = du->frameNumber;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
// Any frame number greater than m_LastFrameNumber + 1 represents a dropped frame
|
|
||||||
m_ActiveWndVideoStats.networkDroppedFrames += du->frameNumber - (m_LastFrameNumber + 1);
|
|
||||||
m_LastFrameNumber = du->frameNumber;
|
|
||||||
}
|
|
||||||
|
|
||||||
int requiredBufferSize = du->fullLength;
|
int requiredBufferSize = du->fullLength;
|
||||||
if (du->frameType == FRAME_TYPE_IDR) {
|
if (du->frameType == FRAME_TYPE_IDR) {
|
||||||
@@ -582,17 +595,19 @@ int FFmpegVideoDecoder::submitDecodeUnit(PDECODE_UNIT du)
|
|||||||
// Reset failed decodes count if we reached this far
|
// Reset failed decodes count if we reached this far
|
||||||
m_ConsecutiveFailedDecodes = 0;
|
m_ConsecutiveFailedDecodes = 0;
|
||||||
|
|
||||||
|
// Restore default log level after a successful decode
|
||||||
|
av_log_set_level(AV_LOG_INFO);
|
||||||
|
|
||||||
// Capture a frame timestamp to measuring pacing delay
|
// Capture a frame timestamp to measuring pacing delay
|
||||||
frame->pts = SDL_GetTicks();
|
frame->pts = SDL_GetTicks();
|
||||||
|
|
||||||
// Queue the frame for rendering from the main thread
|
|
||||||
SDL_AtomicIncRef(&m_QueuedFrames);
|
|
||||||
queueFrame(frame);
|
|
||||||
|
|
||||||
// Count time in avcodec_send_packet() and avcodec_receive_frame()
|
// Count time in avcodec_send_packet() and avcodec_receive_frame()
|
||||||
// as time spent decoding
|
// as time spent decoding
|
||||||
m_ActiveWndVideoStats.totalDecodeTime += SDL_GetTicks() - beforeDecode;
|
m_ActiveWndVideoStats.totalDecodeTime += SDL_GetTicks() - beforeDecode;
|
||||||
m_ActiveWndVideoStats.decodedFrames++;
|
m_ActiveWndVideoStats.decodedFrames++;
|
||||||
|
|
||||||
|
// Queue the frame for rendering (or render now if pacer is disabled)
|
||||||
|
m_Pacer->submitFrame(frame);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
av_frame_free(&frame);
|
av_frame_free(&frame);
|
||||||
@@ -616,30 +631,13 @@ int FFmpegVideoDecoder::submitDecodeUnit(PDECODE_UNIT du)
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Called on main thread
|
// Called on main thread
|
||||||
void FFmpegVideoDecoder::renderFrame(SDL_UserEvent* event)
|
void FFmpegVideoDecoder::renderFrame(SDL_UserEvent*)
|
||||||
{
|
{
|
||||||
AVFrame* frame = reinterpret_cast<AVFrame*>(event->data1);
|
SDL_assert(false);
|
||||||
m_Pacer->submitFrame(frame);
|
|
||||||
SDL_AtomicDecRef(&m_QueuedFrames);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Called on main thread
|
// Called on main thread
|
||||||
void FFmpegVideoDecoder::dropFrame(SDL_UserEvent* event)
|
void FFmpegVideoDecoder::dropFrame(SDL_UserEvent*)
|
||||||
{
|
{
|
||||||
AVFrame* frame = reinterpret_cast<AVFrame*>(event->data1);
|
SDL_assert(false);
|
||||||
// If Pacer is using a frame queue, we can just queue the
|
|
||||||
// frame and let it decide whether to drop or not. If Pacer
|
|
||||||
// is submitting directly for rendering, we drop the frame
|
|
||||||
// ourselves.
|
|
||||||
if (m_Pacer->isUsingFrameQueue()) {
|
|
||||||
m_Pacer->submitFrame(frame);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
av_frame_free(&frame);
|
|
||||||
|
|
||||||
// Since Pacer won't see this frame, we'll mark it as a drop
|
|
||||||
// on its behalf
|
|
||||||
m_ActiveWndVideoStats.pacerDroppedFrames++;
|
|
||||||
}
|
|
||||||
SDL_AtomicDecRef(&m_QueuedFrames);
|
|
||||||
}
|
}
|
||||||
@@ -10,8 +10,8 @@ extern "C" {
|
|||||||
|
|
||||||
class FFmpegVideoDecoder : public IVideoDecoder {
|
class FFmpegVideoDecoder : public IVideoDecoder {
|
||||||
public:
|
public:
|
||||||
FFmpegVideoDecoder();
|
FFmpegVideoDecoder(bool testOnly);
|
||||||
virtual ~FFmpegVideoDecoder();
|
virtual ~FFmpegVideoDecoder() override;
|
||||||
virtual bool initialize(StreamingPreferences::VideoDecoderSelection vds,
|
virtual bool initialize(StreamingPreferences::VideoDecoderSelection vds,
|
||||||
SDL_Window* window,
|
SDL_Window* window,
|
||||||
int videoFormat,
|
int videoFormat,
|
||||||
@@ -31,7 +31,9 @@ public:
|
|||||||
private:
|
private:
|
||||||
bool completeInitialization(AVCodec* decoder, SDL_Window* window,
|
bool completeInitialization(AVCodec* decoder, SDL_Window* window,
|
||||||
int videoFormat, int width, int height,
|
int videoFormat, int width, int height,
|
||||||
int maxFps, bool enableFramePacing, bool testOnly);
|
int maxFps, bool enableFramePacing, bool testFrame);
|
||||||
|
|
||||||
|
void stringifyVideoStats(VIDEO_STATS& stats, char* output);
|
||||||
|
|
||||||
void logVideoStats(VIDEO_STATS& stats, const char* title);
|
void logVideoStats(VIDEO_STATS& stats, const char* title);
|
||||||
|
|
||||||
@@ -52,15 +54,16 @@ private:
|
|||||||
QByteArray m_DecodeBuffer;
|
QByteArray m_DecodeBuffer;
|
||||||
const AVCodecHWConfig* m_HwDecodeCfg;
|
const AVCodecHWConfig* m_HwDecodeCfg;
|
||||||
IFFmpegRenderer* m_Renderer;
|
IFFmpegRenderer* m_Renderer;
|
||||||
SDL_atomic_t m_QueuedFrames;
|
|
||||||
int m_ConsecutiveFailedDecodes;
|
int m_ConsecutiveFailedDecodes;
|
||||||
Pacer* m_Pacer;
|
Pacer* m_Pacer;
|
||||||
VIDEO_STATS m_ActiveWndVideoStats;
|
VIDEO_STATS m_ActiveWndVideoStats;
|
||||||
VIDEO_STATS m_LastWndVideoStats;
|
VIDEO_STATS m_LastWndVideoStats;
|
||||||
VIDEO_STATS m_GlobalVideoStats;
|
VIDEO_STATS m_GlobalVideoStats;
|
||||||
|
|
||||||
int m_LastFrameNumber;
|
int m_LastFrameNumber;
|
||||||
int m_StreamFps;
|
int m_StreamFps;
|
||||||
bool m_NeedsSpsFixup;
|
bool m_NeedsSpsFixup;
|
||||||
|
bool m_TestOnly;
|
||||||
|
|
||||||
static const uint8_t k_H264TestFrame[];
|
static const uint8_t k_H264TestFrame[];
|
||||||
static const uint8_t k_HEVCTestFrame[];
|
static const uint8_t k_HEVCTestFrame[];
|
||||||
|
|||||||
@@ -0,0 +1,67 @@
|
|||||||
|
#include "overlaymanager.h"
|
||||||
|
|
||||||
|
using namespace Overlay;
|
||||||
|
|
||||||
|
OverlayManager::OverlayManager() :
|
||||||
|
m_Renderer(nullptr)
|
||||||
|
{
|
||||||
|
memset(m_Overlays, 0, sizeof(m_Overlays));
|
||||||
|
|
||||||
|
m_Overlays[OverlayType::OverlayDebug].color = {0xD0, 0xD0, 0x00, 0xFF};
|
||||||
|
m_Overlays[OverlayType::OverlayDebug].fontSize = 20;
|
||||||
|
|
||||||
|
m_Overlays[OverlayType::OverlayStatusUpdate].color = {0xCC, 0x00, 0x00, 0xFF};
|
||||||
|
m_Overlays[OverlayType::OverlayStatusUpdate].fontSize = 36;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool OverlayManager::isOverlayEnabled(OverlayType type)
|
||||||
|
{
|
||||||
|
return m_Overlays[type].enabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
char* OverlayManager::getOverlayText(OverlayType type)
|
||||||
|
{
|
||||||
|
return m_Overlays[type].text;
|
||||||
|
}
|
||||||
|
|
||||||
|
int OverlayManager::getOverlayFontSize(OverlayType type)
|
||||||
|
{
|
||||||
|
return m_Overlays[type].fontSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
void OverlayManager::setOverlayTextUpdated(OverlayType type)
|
||||||
|
{
|
||||||
|
// Only update the overlay state if it's enabled. If it's not enabled,
|
||||||
|
// the renderer has already been notified by setOverlayState().
|
||||||
|
if (m_Overlays[type].enabled && m_Renderer != nullptr) {
|
||||||
|
m_Renderer->notifyOverlayUpdated(type);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void OverlayManager::setOverlayState(OverlayType type, bool enabled)
|
||||||
|
{
|
||||||
|
bool stateChanged = m_Overlays[type].enabled != enabled;
|
||||||
|
|
||||||
|
m_Overlays[type].enabled = enabled;
|
||||||
|
|
||||||
|
if (stateChanged) {
|
||||||
|
if (!enabled) {
|
||||||
|
// Set the text to empty string on disable
|
||||||
|
m_Overlays[type].text[0] = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (m_Renderer != nullptr) {
|
||||||
|
m_Renderer->notifyOverlayUpdated(type);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SDL_Color OverlayManager::getOverlayColor(OverlayType type)
|
||||||
|
{
|
||||||
|
return m_Overlays[type].color;
|
||||||
|
}
|
||||||
|
|
||||||
|
void OverlayManager::setOverlayRenderer(IOverlayRenderer* renderer)
|
||||||
|
{
|
||||||
|
m_Renderer = renderer;
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QString>
|
||||||
|
|
||||||
|
#include <SDL.h>
|
||||||
|
|
||||||
|
namespace Overlay {
|
||||||
|
|
||||||
|
enum OverlayType {
|
||||||
|
OverlayDebug,
|
||||||
|
OverlayStatusUpdate,
|
||||||
|
OverlayMax
|
||||||
|
};
|
||||||
|
|
||||||
|
class IOverlayRenderer
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
virtual ~IOverlayRenderer() = default;
|
||||||
|
|
||||||
|
virtual void notifyOverlayUpdated(OverlayType type) = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
class OverlayManager
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
OverlayManager();
|
||||||
|
|
||||||
|
bool isOverlayEnabled(OverlayType type);
|
||||||
|
char* getOverlayText(OverlayType type);
|
||||||
|
void setOverlayTextUpdated(OverlayType type);
|
||||||
|
void setOverlayState(OverlayType type, bool enabled);
|
||||||
|
SDL_Color getOverlayColor(OverlayType type);
|
||||||
|
int getOverlayFontSize(OverlayType type);
|
||||||
|
|
||||||
|
void setOverlayRenderer(IOverlayRenderer* renderer);
|
||||||
|
|
||||||
|
struct {
|
||||||
|
bool enabled;
|
||||||
|
int fontSize;
|
||||||
|
SDL_Color color;
|
||||||
|
char text[512];
|
||||||
|
} m_Overlays[OverlayMax];
|
||||||
|
IOverlayRenderer* m_Renderer;
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
#include "sl.h"
|
#include "slvid.h"
|
||||||
|
|
||||||
SLVideoDecoder::SLVideoDecoder()
|
SLVideoDecoder::SLVideoDecoder(bool)
|
||||||
: m_VideoContext(nullptr),
|
: m_VideoContext(nullptr),
|
||||||
m_VideoStream(nullptr)
|
m_VideoStream(nullptr)
|
||||||
{
|
{
|
||||||
@@ -7,7 +7,7 @@
|
|||||||
class SLVideoDecoder : public IVideoDecoder
|
class SLVideoDecoder : public IVideoDecoder
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
SLVideoDecoder();
|
SLVideoDecoder(bool testOnly);
|
||||||
virtual ~SLVideoDecoder();
|
virtual ~SLVideoDecoder();
|
||||||
virtual bool initialize(StreamingPreferences::VideoDecoderSelection vds,
|
virtual bool initialize(StreamingPreferences::VideoDecoderSelection vds,
|
||||||
SDL_Window* window,
|
SDL_Window* window,
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
0.10.0
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
SOURCES = main.cpp
|
||||||
|
LIBS += -lSLVideo -lSLAudio
|
||||||
|
|
||||||
@@ -1,7 +1,9 @@
|
|||||||
#include <SLVideo.h>
|
#include <SLVideo.h>
|
||||||
|
#include <SLAudio.h>
|
||||||
|
|
||||||
int main()
|
int main()
|
||||||
{
|
{
|
||||||
SLVideo_CreateContext();
|
SLVideo_CreateContext();
|
||||||
|
SLAudio_CreateContext();
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
SOURCES = main.cpp
|
|
||||||
LIBS += -lSLVideo
|
|
||||||
|
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
Versions/Current/Frameworks
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
Versions/Current/Headers
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
Versions/Current/Resources
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
Versions/Current/SDL2_ttf
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
Versions/Current/FreeType
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
Versions/Current/Headers
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
Versions/Current/Resources
|
||||||
@@ -0,0 +1,603 @@
|
|||||||
|
/* ftconfig.h. Generated from ftconfig.in by configure. */
|
||||||
|
/***************************************************************************/
|
||||||
|
/* */
|
||||||
|
/* ftconfig.in */
|
||||||
|
/* */
|
||||||
|
/* UNIX-specific configuration file (specification only). */
|
||||||
|
/* */
|
||||||
|
/* Copyright 1996-2018 by */
|
||||||
|
/* David Turner, Robert Wilhelm, and Werner Lemberg. */
|
||||||
|
/* */
|
||||||
|
/* This file is part of the FreeType project, and may only be used, */
|
||||||
|
/* modified, and distributed under the terms of the FreeType project */
|
||||||
|
/* license, LICENSE.TXT. By continuing to use, modify, or distribute */
|
||||||
|
/* this file you indicate that you have read the license and */
|
||||||
|
/* understand and accept it fully. */
|
||||||
|
/* */
|
||||||
|
/***************************************************************************/
|
||||||
|
|
||||||
|
|
||||||
|
/*************************************************************************/
|
||||||
|
/* */
|
||||||
|
/* This header file contains a number of macro definitions that are used */
|
||||||
|
/* by the rest of the engine. Most of the macros here are automatically */
|
||||||
|
/* determined at compile time, and you should not need to change it to */
|
||||||
|
/* port FreeType, except to compile the library with a non-ANSI */
|
||||||
|
/* compiler. */
|
||||||
|
/* */
|
||||||
|
/* Note however that if some specific modifications are needed, we */
|
||||||
|
/* advise you to place a modified copy in your build directory. */
|
||||||
|
/* */
|
||||||
|
/* The build directory is usually `builds/<system>', and contains */
|
||||||
|
/* system-specific files that are always included first when building */
|
||||||
|
/* the library. */
|
||||||
|
/* */
|
||||||
|
/*************************************************************************/
|
||||||
|
|
||||||
|
|
||||||
|
#ifndef FTCONFIG_H_
|
||||||
|
#define FTCONFIG_H_
|
||||||
|
|
||||||
|
#include <ft2build.h>
|
||||||
|
#include FT_CONFIG_OPTIONS_H
|
||||||
|
#include FT_CONFIG_STANDARD_LIBRARY_H
|
||||||
|
|
||||||
|
|
||||||
|
FT_BEGIN_HEADER
|
||||||
|
|
||||||
|
|
||||||
|
/*************************************************************************/
|
||||||
|
/* */
|
||||||
|
/* PLATFORM-SPECIFIC CONFIGURATION MACROS */
|
||||||
|
/* */
|
||||||
|
/* These macros can be toggled to suit a specific system. The current */
|
||||||
|
/* ones are defaults used to compile FreeType in an ANSI C environment */
|
||||||
|
/* (16bit compilers are also supported). Copy this file to your own */
|
||||||
|
/* `builds/<system>' directory, and edit it to port the engine. */
|
||||||
|
/* */
|
||||||
|
/*************************************************************************/
|
||||||
|
|
||||||
|
|
||||||
|
#define HAVE_UNISTD_H 1
|
||||||
|
#define HAVE_FCNTL_H 1
|
||||||
|
#define HAVE_STDINT_H 1
|
||||||
|
|
||||||
|
|
||||||
|
/* There are systems (like the Texas Instruments 'C54x) where a `char' */
|
||||||
|
/* has 16 bits. ANSI C says that sizeof(char) is always 1. Since an */
|
||||||
|
/* `int' has 16 bits also for this system, sizeof(int) gives 1 which */
|
||||||
|
/* is probably unexpected. */
|
||||||
|
/* */
|
||||||
|
/* `CHAR_BIT' (defined in limits.h) gives the number of bits in a */
|
||||||
|
/* `char' type. */
|
||||||
|
|
||||||
|
#ifndef FT_CHAR_BIT
|
||||||
|
#define FT_CHAR_BIT CHAR_BIT
|
||||||
|
#endif
|
||||||
|
|
||||||
|
|
||||||
|
/* #undef FT_USE_AUTOCONF_SIZEOF_TYPES */
|
||||||
|
#ifdef FT_USE_AUTOCONF_SIZEOF_TYPES
|
||||||
|
|
||||||
|
#define SIZEOF_INT 4
|
||||||
|
#define SIZEOF_LONG 8
|
||||||
|
#define FT_SIZEOF_INT SIZEOF_INT
|
||||||
|
#define FT_SIZEOF_LONG SIZEOF_LONG
|
||||||
|
|
||||||
|
#else /* !FT_USE_AUTOCONF_SIZEOF_TYPES */
|
||||||
|
|
||||||
|
/* Following cpp computation of the bit length of int and long */
|
||||||
|
/* is copied from default include/freetype/config/ftconfig.h. */
|
||||||
|
/* If any improvement is required for this file, it should be */
|
||||||
|
/* applied to the original header file for the builders that */
|
||||||
|
/* do not use configure script. */
|
||||||
|
|
||||||
|
/* The size of an `int' type. */
|
||||||
|
#if FT_UINT_MAX == 0xFFFFUL
|
||||||
|
#define FT_SIZEOF_INT (16 / FT_CHAR_BIT)
|
||||||
|
#elif FT_UINT_MAX == 0xFFFFFFFFUL
|
||||||
|
#define FT_SIZEOF_INT (32 / FT_CHAR_BIT)
|
||||||
|
#elif FT_UINT_MAX > 0xFFFFFFFFUL && FT_UINT_MAX == 0xFFFFFFFFFFFFFFFFUL
|
||||||
|
#define FT_SIZEOF_INT (64 / FT_CHAR_BIT)
|
||||||
|
#else
|
||||||
|
#error "Unsupported size of `int' type!"
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/* The size of a `long' type. A five-byte `long' (as used e.g. on the */
|
||||||
|
/* DM642) is recognized but avoided. */
|
||||||
|
#if FT_ULONG_MAX == 0xFFFFFFFFUL
|
||||||
|
#define FT_SIZEOF_LONG (32 / FT_CHAR_BIT)
|
||||||
|
#elif FT_ULONG_MAX > 0xFFFFFFFFUL && FT_ULONG_MAX == 0xFFFFFFFFFFUL
|
||||||
|
#define FT_SIZEOF_LONG (32 / FT_CHAR_BIT)
|
||||||
|
#elif FT_ULONG_MAX > 0xFFFFFFFFUL && FT_ULONG_MAX == 0xFFFFFFFFFFFFFFFFUL
|
||||||
|
#define FT_SIZEOF_LONG (64 / FT_CHAR_BIT)
|
||||||
|
#else
|
||||||
|
#error "Unsupported size of `long' type!"
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#endif /* !FT_USE_AUTOCONF_SIZEOF_TYPES */
|
||||||
|
|
||||||
|
|
||||||
|
/* FT_UNUSED is a macro used to indicate that a given parameter is not */
|
||||||
|
/* used -- this is only used to get rid of unpleasant compiler warnings */
|
||||||
|
#ifndef FT_UNUSED
|
||||||
|
#define FT_UNUSED( arg ) ( (arg) = (arg) )
|
||||||
|
#endif
|
||||||
|
|
||||||
|
|
||||||
|
/*************************************************************************/
|
||||||
|
/* */
|
||||||
|
/* AUTOMATIC CONFIGURATION MACROS */
|
||||||
|
/* */
|
||||||
|
/* These macros are computed from the ones defined above. Don't touch */
|
||||||
|
/* their definition, unless you know precisely what you are doing. No */
|
||||||
|
/* porter should need to mess with them. */
|
||||||
|
/* */
|
||||||
|
/*************************************************************************/
|
||||||
|
|
||||||
|
|
||||||
|
/*************************************************************************/
|
||||||
|
/* */
|
||||||
|
/* Mac support */
|
||||||
|
/* */
|
||||||
|
/* This is the only necessary change, so it is defined here instead */
|
||||||
|
/* providing a new configuration file. */
|
||||||
|
/* */
|
||||||
|
#if defined( __APPLE__ ) || ( defined( __MWERKS__ ) && defined( macintosh ) )
|
||||||
|
/* no Carbon frameworks for 64bit 10.4.x */
|
||||||
|
/* AvailabilityMacros.h is available since Mac OS X 10.2, */
|
||||||
|
/* so guess the system version by maximum errno before inclusion */
|
||||||
|
#include <errno.h>
|
||||||
|
#ifdef ECANCELED /* defined since 10.2 */
|
||||||
|
#include "AvailabilityMacros.h"
|
||||||
|
#endif
|
||||||
|
#if defined( __LP64__ ) && \
|
||||||
|
( MAC_OS_X_VERSION_MIN_REQUIRED <= MAC_OS_X_VERSION_10_4 )
|
||||||
|
#undef FT_MACINTOSH
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#elif defined( __SC__ ) || defined( __MRC__ )
|
||||||
|
/* Classic MacOS compilers */
|
||||||
|
#include "ConditionalMacros.h"
|
||||||
|
#if TARGET_OS_MAC
|
||||||
|
#define FT_MACINTOSH 1
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#endif
|
||||||
|
|
||||||
|
|
||||||
|
/* Fix compiler warning with sgi compiler */
|
||||||
|
#if defined( __sgi ) && !defined( __GNUC__ )
|
||||||
|
#if defined( _COMPILER_VERSION ) && ( _COMPILER_VERSION >= 730 )
|
||||||
|
#pragma set woff 3505
|
||||||
|
#endif
|
||||||
|
#endif
|
||||||
|
|
||||||
|
|
||||||
|
/*************************************************************************/
|
||||||
|
/* */
|
||||||
|
/* <Section> */
|
||||||
|
/* basic_types */
|
||||||
|
/* */
|
||||||
|
/*************************************************************************/
|
||||||
|
|
||||||
|
|
||||||
|
/*************************************************************************/
|
||||||
|
/* */
|
||||||
|
/* <Type> */
|
||||||
|
/* FT_Int16 */
|
||||||
|
/* */
|
||||||
|
/* <Description> */
|
||||||
|
/* A typedef for a 16bit signed integer type. */
|
||||||
|
/* */
|
||||||
|
typedef signed short FT_Int16;
|
||||||
|
|
||||||
|
|
||||||
|
/*************************************************************************/
|
||||||
|
/* */
|
||||||
|
/* <Type> */
|
||||||
|
/* FT_UInt16 */
|
||||||
|
/* */
|
||||||
|
/* <Description> */
|
||||||
|
/* A typedef for a 16bit unsigned integer type. */
|
||||||
|
/* */
|
||||||
|
typedef unsigned short FT_UInt16;
|
||||||
|
|
||||||
|
/* */
|
||||||
|
|
||||||
|
|
||||||
|
/* this #if 0 ... #endif clause is for documentation purposes */
|
||||||
|
#if 0
|
||||||
|
|
||||||
|
/*************************************************************************/
|
||||||
|
/* */
|
||||||
|
/* <Type> */
|
||||||
|
/* FT_Int32 */
|
||||||
|
/* */
|
||||||
|
/* <Description> */
|
||||||
|
/* A typedef for a 32bit signed integer type. The size depends on */
|
||||||
|
/* the configuration. */
|
||||||
|
/* */
|
||||||
|
typedef signed XXX FT_Int32;
|
||||||
|
|
||||||
|
|
||||||
|
/*************************************************************************/
|
||||||
|
/* */
|
||||||
|
/* <Type> */
|
||||||
|
/* FT_UInt32 */
|
||||||
|
/* */
|
||||||
|
/* A typedef for a 32bit unsigned integer type. The size depends on */
|
||||||
|
/* the configuration. */
|
||||||
|
/* */
|
||||||
|
typedef unsigned XXX FT_UInt32;
|
||||||
|
|
||||||
|
|
||||||
|
/*************************************************************************/
|
||||||
|
/* */
|
||||||
|
/* <Type> */
|
||||||
|
/* FT_Int64 */
|
||||||
|
/* */
|
||||||
|
/* A typedef for a 64bit signed integer type. The size depends on */
|
||||||
|
/* the configuration. Only defined if there is real 64bit support; */
|
||||||
|
/* otherwise, it gets emulated with a structure (if necessary). */
|
||||||
|
/* */
|
||||||
|
typedef signed XXX FT_Int64;
|
||||||
|
|
||||||
|
|
||||||
|
/*************************************************************************/
|
||||||
|
/* */
|
||||||
|
/* <Type> */
|
||||||
|
/* FT_UInt64 */
|
||||||
|
/* */
|
||||||
|
/* A typedef for a 64bit unsigned integer type. The size depends on */
|
||||||
|
/* the configuration. Only defined if there is real 64bit support; */
|
||||||
|
/* otherwise, it gets emulated with a structure (if necessary). */
|
||||||
|
/* */
|
||||||
|
typedef unsigned XXX FT_UInt64;
|
||||||
|
|
||||||
|
/* */
|
||||||
|
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#if FT_SIZEOF_INT == 4
|
||||||
|
|
||||||
|
typedef signed int FT_Int32;
|
||||||
|
typedef unsigned int FT_UInt32;
|
||||||
|
|
||||||
|
#elif FT_SIZEOF_LONG == 4
|
||||||
|
|
||||||
|
typedef signed long FT_Int32;
|
||||||
|
typedef unsigned long FT_UInt32;
|
||||||
|
|
||||||
|
#else
|
||||||
|
#error "no 32bit type found -- please check your configuration files"
|
||||||
|
#endif
|
||||||
|
|
||||||
|
|
||||||
|
/* look up an integer type that is at least 32 bits */
|
||||||
|
#if FT_SIZEOF_INT >= 4
|
||||||
|
|
||||||
|
typedef int FT_Fast;
|
||||||
|
typedef unsigned int FT_UFast;
|
||||||
|
|
||||||
|
#elif FT_SIZEOF_LONG >= 4
|
||||||
|
|
||||||
|
typedef long FT_Fast;
|
||||||
|
typedef unsigned long FT_UFast;
|
||||||
|
|
||||||
|
#endif
|
||||||
|
|
||||||
|
|
||||||
|
/* determine whether we have a 64-bit int type */
|
||||||
|
/* (mostly for environments without `autoconf') */
|
||||||
|
#if FT_SIZEOF_LONG == 8
|
||||||
|
|
||||||
|
/* FT_LONG64 must be defined if a 64-bit type is available */
|
||||||
|
#define FT_LONG64
|
||||||
|
#define FT_INT64 long
|
||||||
|
#define FT_UINT64 unsigned long
|
||||||
|
|
||||||
|
/* we handle the LLP64 scheme separately for GCC and clang, */
|
||||||
|
/* suppressing the `long long' warning */
|
||||||
|
#elif ( FT_SIZEOF_LONG == 4 ) && \
|
||||||
|
defined( HAVE_LONG_LONG_INT ) && \
|
||||||
|
defined( __GNUC__ )
|
||||||
|
#pragma GCC diagnostic ignored "-Wlong-long"
|
||||||
|
#define FT_LONG64
|
||||||
|
#define FT_INT64 long long int
|
||||||
|
#define FT_UINT64 unsigned long long int
|
||||||
|
|
||||||
|
/*************************************************************************/
|
||||||
|
/* */
|
||||||
|
/* A 64-bit data type may create compilation problems if you compile */
|
||||||
|
/* in strict ANSI mode. To avoid them, we disable other 64-bit data */
|
||||||
|
/* types if __STDC__ is defined. You can however ignore this rule */
|
||||||
|
/* by defining the FT_CONFIG_OPTION_FORCE_INT64 configuration macro. */
|
||||||
|
/* */
|
||||||
|
#elif !defined( __STDC__ ) || defined( FT_CONFIG_OPTION_FORCE_INT64 )
|
||||||
|
|
||||||
|
#if defined( __STDC_VERSION__ ) && __STDC_VERSION__ >= 199901L
|
||||||
|
|
||||||
|
#define FT_LONG64
|
||||||
|
#define FT_INT64 long long int
|
||||||
|
#define FT_UINT64 unsigned long long int
|
||||||
|
|
||||||
|
#elif defined( _MSC_VER ) && _MSC_VER >= 900 /* Visual C++ (and Intel C++) */
|
||||||
|
|
||||||
|
/* this compiler provides the __int64 type */
|
||||||
|
#define FT_LONG64
|
||||||
|
#define FT_INT64 __int64
|
||||||
|
#define FT_UINT64 unsigned __int64
|
||||||
|
|
||||||
|
#elif defined( __BORLANDC__ ) /* Borland C++ */
|
||||||
|
|
||||||
|
/* XXXX: We should probably check the value of __BORLANDC__ in order */
|
||||||
|
/* to test the compiler version. */
|
||||||
|
|
||||||
|
/* this compiler provides the __int64 type */
|
||||||
|
#define FT_LONG64
|
||||||
|
#define FT_INT64 __int64
|
||||||
|
#define FT_UINT64 unsigned __int64
|
||||||
|
|
||||||
|
#elif defined( __WATCOMC__ ) /* Watcom C++ */
|
||||||
|
|
||||||
|
/* Watcom doesn't provide 64-bit data types */
|
||||||
|
|
||||||
|
#elif defined( __MWERKS__ ) /* Metrowerks CodeWarrior */
|
||||||
|
|
||||||
|
#define FT_LONG64
|
||||||
|
#define FT_INT64 long long int
|
||||||
|
#define FT_UINT64 unsigned long long int
|
||||||
|
|
||||||
|
#elif defined( __GNUC__ )
|
||||||
|
|
||||||
|
/* GCC provides the `long long' type */
|
||||||
|
#define FT_LONG64
|
||||||
|
#define FT_INT64 long long int
|
||||||
|
#define FT_UINT64 unsigned long long int
|
||||||
|
|
||||||
|
#endif /* __STDC_VERSION__ >= 199901L */
|
||||||
|
|
||||||
|
#endif /* FT_SIZEOF_LONG == 8 */
|
||||||
|
|
||||||
|
#ifdef FT_LONG64
|
||||||
|
typedef FT_INT64 FT_Int64;
|
||||||
|
typedef FT_UINT64 FT_UInt64;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
|
||||||
|
#ifdef _WIN64
|
||||||
|
/* only 64bit Windows uses the LLP64 data model, i.e., */
|
||||||
|
/* 32bit integers, 64bit pointers */
|
||||||
|
#define FT_UINT_TO_POINTER( x ) (void*)(unsigned __int64)(x)
|
||||||
|
#else
|
||||||
|
#define FT_UINT_TO_POINTER( x ) (void*)(unsigned long)(x)
|
||||||
|
#endif
|
||||||
|
|
||||||
|
|
||||||
|
/*************************************************************************/
|
||||||
|
/* */
|
||||||
|
/* miscellaneous */
|
||||||
|
/* */
|
||||||
|
/*************************************************************************/
|
||||||
|
|
||||||
|
|
||||||
|
#define FT_BEGIN_STMNT do {
|
||||||
|
#define FT_END_STMNT } while ( 0 )
|
||||||
|
#define FT_DUMMY_STMNT FT_BEGIN_STMNT FT_END_STMNT
|
||||||
|
|
||||||
|
|
||||||
|
/* typeof condition taken from gnulib's `intprops.h' header file */
|
||||||
|
#if ( ( defined( __GNUC__ ) && __GNUC__ >= 2 ) || \
|
||||||
|
( defined( __IBMC__ ) && __IBMC__ >= 1210 && \
|
||||||
|
defined( __IBM__TYPEOF__ ) ) || \
|
||||||
|
( defined( __SUNPRO_C ) && __SUNPRO_C >= 0x5110 && !__STDC__ ) )
|
||||||
|
#define FT_TYPEOF( type ) ( __typeof__ ( type ) )
|
||||||
|
#else
|
||||||
|
#define FT_TYPEOF( type ) /* empty */
|
||||||
|
#endif
|
||||||
|
|
||||||
|
|
||||||
|
/* Use FT_LOCAL and FT_LOCAL_DEF to declare and define, respectively, */
|
||||||
|
/* a function that gets used only within the scope of a module. */
|
||||||
|
/* Normally, both the header and source code files for such a */
|
||||||
|
/* function are within a single module directory. */
|
||||||
|
/* */
|
||||||
|
/* Intra-module arrays should be tagged with FT_LOCAL_ARRAY and */
|
||||||
|
/* FT_LOCAL_ARRAY_DEF. */
|
||||||
|
/* */
|
||||||
|
#ifdef FT_MAKE_OPTION_SINGLE_OBJECT
|
||||||
|
|
||||||
|
#define FT_LOCAL( x ) static x
|
||||||
|
#define FT_LOCAL_DEF( x ) static x
|
||||||
|
|
||||||
|
#else
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
#define FT_LOCAL( x ) extern "C" x
|
||||||
|
#define FT_LOCAL_DEF( x ) extern "C" x
|
||||||
|
#else
|
||||||
|
#define FT_LOCAL( x ) extern x
|
||||||
|
#define FT_LOCAL_DEF( x ) x
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#endif /* FT_MAKE_OPTION_SINGLE_OBJECT */
|
||||||
|
|
||||||
|
#define FT_LOCAL_ARRAY( x ) extern const x
|
||||||
|
#define FT_LOCAL_ARRAY_DEF( x ) const x
|
||||||
|
|
||||||
|
|
||||||
|
/* Use FT_BASE and FT_BASE_DEF to declare and define, respectively, */
|
||||||
|
/* functions that are used in more than a single module. In the */
|
||||||
|
/* current setup this implies that the declaration is in a header */
|
||||||
|
/* file in the `include/freetype/internal' directory, and the */
|
||||||
|
/* function body is in a file in `src/base'. */
|
||||||
|
/* */
|
||||||
|
#ifndef FT_BASE
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
#define FT_BASE( x ) extern "C" x
|
||||||
|
#else
|
||||||
|
#define FT_BASE( x ) extern x
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#endif /* !FT_BASE */
|
||||||
|
|
||||||
|
|
||||||
|
#ifndef FT_BASE_DEF
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
#define FT_BASE_DEF( x ) x
|
||||||
|
#else
|
||||||
|
#define FT_BASE_DEF( x ) x
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#endif /* !FT_BASE_DEF */
|
||||||
|
|
||||||
|
|
||||||
|
/* When compiling FreeType as a DLL or DSO with hidden visibility */
|
||||||
|
/* some systems/compilers need a special attribute in front OR after */
|
||||||
|
/* the return type of function declarations. */
|
||||||
|
/* */
|
||||||
|
/* Two macros are used within the FreeType source code to define */
|
||||||
|
/* exported library functions: FT_EXPORT and FT_EXPORT_DEF. */
|
||||||
|
/* */
|
||||||
|
/* FT_EXPORT( return_type ) */
|
||||||
|
/* */
|
||||||
|
/* is used in a function declaration, as in */
|
||||||
|
/* */
|
||||||
|
/* FT_EXPORT( FT_Error ) */
|
||||||
|
/* FT_Init_FreeType( FT_Library* alibrary ); */
|
||||||
|
/* */
|
||||||
|
/* */
|
||||||
|
/* FT_EXPORT_DEF( return_type ) */
|
||||||
|
/* */
|
||||||
|
/* is used in a function definition, as in */
|
||||||
|
/* */
|
||||||
|
/* FT_EXPORT_DEF( FT_Error ) */
|
||||||
|
/* FT_Init_FreeType( FT_Library* alibrary ) */
|
||||||
|
/* { */
|
||||||
|
/* ... some code ... */
|
||||||
|
/* return FT_Err_Ok; */
|
||||||
|
/* } */
|
||||||
|
/* */
|
||||||
|
/* You can provide your own implementation of FT_EXPORT and */
|
||||||
|
/* FT_EXPORT_DEF here if you want. */
|
||||||
|
/* */
|
||||||
|
/* To export a variable, use FT_EXPORT_VAR. */
|
||||||
|
/* */
|
||||||
|
#ifndef FT_EXPORT
|
||||||
|
|
||||||
|
#ifdef FT2_BUILD_LIBRARY
|
||||||
|
|
||||||
|
#if defined( _WIN32 ) && ( defined( _DLL ) || defined( DLL_EXPORT ) )
|
||||||
|
#define FT_EXPORT( x ) __declspec( dllexport ) x
|
||||||
|
#elif defined( __GNUC__ ) && __GNUC__ >= 4
|
||||||
|
#define FT_EXPORT( x ) __attribute__(( visibility( "default" ) )) x
|
||||||
|
#elif defined( __cplusplus )
|
||||||
|
#define FT_EXPORT( x ) extern "C" x
|
||||||
|
#else
|
||||||
|
#define FT_EXPORT( x ) extern x
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#else
|
||||||
|
|
||||||
|
#if defined( FT2_DLLIMPORT )
|
||||||
|
#define FT_EXPORT( x ) __declspec( dllimport ) x
|
||||||
|
#elif defined( __cplusplus )
|
||||||
|
#define FT_EXPORT( x ) extern "C" x
|
||||||
|
#else
|
||||||
|
#define FT_EXPORT( x ) extern x
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#endif /* !FT_EXPORT */
|
||||||
|
|
||||||
|
|
||||||
|
#ifndef FT_EXPORT_DEF
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
#define FT_EXPORT_DEF( x ) extern "C" x
|
||||||
|
#else
|
||||||
|
#define FT_EXPORT_DEF( x ) extern x
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#endif /* !FT_EXPORT_DEF */
|
||||||
|
|
||||||
|
|
||||||
|
#ifndef FT_EXPORT_VAR
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
#define FT_EXPORT_VAR( x ) extern "C" x
|
||||||
|
#else
|
||||||
|
#define FT_EXPORT_VAR( x ) extern x
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#endif /* !FT_EXPORT_VAR */
|
||||||
|
|
||||||
|
/* The following macros are needed to compile the library with a */
|
||||||
|
/* C++ compiler and with 16bit compilers. */
|
||||||
|
/* */
|
||||||
|
|
||||||
|
/* This is special. Within C++, you must specify `extern "C"' for */
|
||||||
|
/* functions which are used via function pointers, and you also */
|
||||||
|
/* must do that for structures which contain function pointers to */
|
||||||
|
/* assure C linkage -- it's not possible to have (local) anonymous */
|
||||||
|
/* functions which are accessed by (global) function pointers. */
|
||||||
|
/* */
|
||||||
|
/* */
|
||||||
|
/* FT_CALLBACK_DEF is used to _define_ a callback function, */
|
||||||
|
/* located in the same source code file as the structure that uses */
|
||||||
|
/* it. */
|
||||||
|
/* */
|
||||||
|
/* FT_BASE_CALLBACK and FT_BASE_CALLBACK_DEF are used to declare */
|
||||||
|
/* and define a callback function, respectively, in a similar way */
|
||||||
|
/* as FT_BASE and FT_BASE_DEF work. */
|
||||||
|
/* */
|
||||||
|
/* FT_CALLBACK_TABLE is used to _declare_ a constant variable that */
|
||||||
|
/* contains pointers to callback functions. */
|
||||||
|
/* */
|
||||||
|
/* FT_CALLBACK_TABLE_DEF is used to _define_ a constant variable */
|
||||||
|
/* that contains pointers to callback functions. */
|
||||||
|
/* */
|
||||||
|
/* */
|
||||||
|
/* Some 16bit compilers have to redefine these macros to insert */
|
||||||
|
/* the infamous `_cdecl' or `__fastcall' declarations. */
|
||||||
|
/* */
|
||||||
|
#ifndef FT_CALLBACK_DEF
|
||||||
|
#ifdef __cplusplus
|
||||||
|
#define FT_CALLBACK_DEF( x ) extern "C" x
|
||||||
|
#else
|
||||||
|
#define FT_CALLBACK_DEF( x ) static x
|
||||||
|
#endif
|
||||||
|
#endif /* FT_CALLBACK_DEF */
|
||||||
|
|
||||||
|
#ifndef FT_BASE_CALLBACK
|
||||||
|
#ifdef __cplusplus
|
||||||
|
#define FT_BASE_CALLBACK( x ) extern "C" x
|
||||||
|
#define FT_BASE_CALLBACK_DEF( x ) extern "C" x
|
||||||
|
#else
|
||||||
|
#define FT_BASE_CALLBACK( x ) extern x
|
||||||
|
#define FT_BASE_CALLBACK_DEF( x ) x
|
||||||
|
#endif
|
||||||
|
#endif /* FT_BASE_CALLBACK */
|
||||||
|
|
||||||
|
#ifndef FT_CALLBACK_TABLE
|
||||||
|
#ifdef __cplusplus
|
||||||
|
#define FT_CALLBACK_TABLE extern "C"
|
||||||
|
#define FT_CALLBACK_TABLE_DEF extern "C"
|
||||||
|
#else
|
||||||
|
#define FT_CALLBACK_TABLE extern
|
||||||
|
#define FT_CALLBACK_TABLE_DEF /* nothing */
|
||||||
|
#endif
|
||||||
|
#endif /* FT_CALLBACK_TABLE */
|
||||||
|
|
||||||
|
|
||||||
|
FT_END_HEADER
|
||||||
|
|
||||||
|
|
||||||
|
#endif /* FTCONFIG_H_ */
|
||||||
|
|
||||||
|
|
||||||
|
/* END */
|
||||||