Compare commits

...
Author SHA1 Message Date
Cameron Gutman 141f6669e8 Version 0.0.4 2018-07-29 21:44:10 -07:00
Cameron Gutman 6300d242cc Update AppStream metadata 2018-07-29 21:40:34 -07:00
Cameron Gutman 186579266c Fix Online -> Offline computer transition 2018-07-29 19:08:29 -07:00
Cameron Gutman 13490af90c Replace OpenSSL dylibs with static build for macOS 10.10+ 2018-07-29 19:07:42 -07:00
R. Aidan Campbell 23a9c2cd9e context menu opened by default for offline hosts 2018-07-29 16:13:42 -07:00
R. Aidan Campbell 88631e4a63 Merge branch 'master' of github.com:cgutman/moonlight-qt 2018-07-29 16:04:53 -07:00
R. Aidan Campbell 9e744ce169 Now with WoL and PC deletion from QML. Deletion needs bugfix, possibly not committed to storage? 2018-07-29 16:04:45 -07:00
Cameron Gutman 77a68db8c1 Code signing for Mac 2018-07-29 15:35:51 -07:00
Cameron Gutman 27247f253c Add Travis CI badge 2018-07-29 13:50:11 -07:00
Cameron Gutman ada7f92c75 Travis CI for Mac 2018-07-29 13:38:05 -07:00
Cameron Gutman 87db4e88a5 Replace autogenerate app bundle strings with real ones and relocate the icon for Mac 2018-07-29 13:05:18 -07:00
R. Aidan Campbell 277594139b Updated macos .app icon to use proper logo 2018-07-29 10:21:53 -07:00
Cameron Gutman 79d5e35306 Mac deployment
- Use the SDL2 framework wholesale instead of manually extracting dylib and headers
- Run install_name_tool on the remaining dylibs so they use the correct load path
- Add generate-dmg.sh for creating packaged Mac builds
2018-07-29 03:38:59 -07:00
Cameron Gutman cda80b319e Publish artifacts from AppVeyor builds 2018-07-28 23:59:20 -07:00
Cameron Gutman ab4b024df4 Travis CI support 2018-07-28 22:49:36 -07:00
Cameron Gutman 7f9cbac232 Fix more AppVeyor bugs 2018-07-28 19:56:08 -07:00
Cameron Gutman 8470ffcb0d Fix some build script bugs uncovered by AppVeyor 2018-07-28 19:44:09 -07:00
Cameron Gutman 92c897762b Fix AppVeyor link 2018-07-28 19:33:11 -07:00
Cameron Gutman 36014cfcfd Add AppVeyor build status 2018-07-28 19:29:54 -07:00
Cameron Gutman df26f731eb Improve debugging messages 2018-07-28 16:06:26 -07:00
Cameron Gutman e39b5a6a72 Block x86 installs on x64 OSes 2018-07-28 13:59:27 -07:00
Cameron Gutman c7138fe36c Fix Discord link 2018-07-28 03:52:24 -07:00
Cameron Gutman b5deab772d Add Discord link 2018-07-28 03:47:34 -07:00
Cameron Gutman 4bfeccec74 Update README with more information 2018-07-28 03:45:45 -07:00
Cameron Gutman 42ed39a67f Version 0.0.3 2018-07-28 03:13:39 -07:00
Cameron Gutman 68612db86d Fix audio glitching on GFE 3.14.1 at low bitrates 2018-07-28 03:13:05 -07:00
Cameron Gutman 3ad5f1478c Comment out HDR option since it's non-working and tweak the remaining option text 2018-07-28 03:06:11 -07:00
Cameron Gutman a81b501d7d Hide the gamepad mapping button since it's unimplemented 2018-07-28 01:27:42 -07:00
Cameron Gutman 871988b2cb Add 1440p and up to 90 FPS streaming options for > 60 Hz monitors 2018-07-28 01:22:31 -07:00
Cameron Gutman 59d087adf5 Add function for querying maximum supported streaming frame rate 2018-07-28 00:25:52 -07:00
Cameron Gutman d9ed3f9694 Fix incorrect key mappings with non-QWERTY keyboards on both host and client 2018-07-27 02:08:54 -07:00
Cameron Gutman e652dbcc79 Version 0.0.2 2018-07-27 00:14:29 -07:00
Cameron Gutman b5965d763c Create desktop shortcut, detect various error conditions to fail installation, and only install VCRedist if it's not already present 2018-07-27 00:13:02 -07:00
Cameron Gutman 717c3903d1 Fix top padding for grid views 2018-07-26 22:47:48 -07:00
Cameron Gutman 5854ce2000 Fix handling of long app names 2018-07-26 22:39:45 -07:00
Cameron Gutman eb1316ce32 Avoid asserting if we get a box art load completion callback for an app that's gone 2018-07-26 22:16:27 -07:00
Cameron Gutman c31e7262b2 Fix decoding of non-ASCII characters 2018-07-26 22:15:52 -07:00
Cameron Gutman 9e6aadffa1 Populate the DXVA2_VideoDesc with format details from the AVFrame 2018-07-26 21:04:05 -07:00
Cameron Gutman 0bde932550 Prevent HEVC streaming with the software decoder for performance reasons 2018-07-26 19:26:22 -07:00
Cameron Gutman f5499be215 Warn when no hardware decoding is available 2018-07-26 12:13:38 -07:00
Cameron Gutman 42988e0dbf Limit log file sizes to 10k lines 2018-07-26 00:21:26 -07:00
Cameron Gutman b16aac0c06 Fix priority and formatting on many debug messages 2018-07-25 23:47:32 -07:00
Cameron Gutman 7c74cdc50b Add enhanced logging support, including log to file for release builds 2018-07-25 23:25:17 -07:00
Cameron Gutman 26ee8564f8 Include VC redistributable in the bundle 2018-07-25 19:52:58 -07:00
Cameron Gutman 718a575478 Reorganize WiX sources 2018-07-25 19:33:09 -07:00
Cameron Gutman 1f314b839e Various installer fixes 2018-07-25 19:16:06 -07:00
Cameron Gutman 34494e4c89 Add initial WiX installer for Windows 2018-07-25 02:53:51 -07:00
Cameron Gutman 03dc563b32 Create Moonlight png for WiX bootstrapper 2018-07-24 21:11:48 -07:00
Cameron Gutman 5e4182f0a0 Add Moonlight.ico for Windows and script to generate it 2018-07-24 20:07:10 -07:00
Cameron Gutman 010475e8a2 Use per-display undefined window positions 2018-07-22 18:40:15 -07:00
Cameron Gutman bd170c4bd6 Always use the current display mode and just let the renderers scale the output 2018-07-22 18:28:17 -07:00
Cameron Gutman 386e463942 Also use the SDL icon for Linux 2018-07-22 17:42:31 -07:00
Cameron Gutman 9d840afdfb Add an SDL window icon for Windows 2018-07-22 17:07:45 -07:00
Cameron Gutman 83b81aad4b Don't capture the mouse initially on debug builds 2018-07-22 16:21:15 -07:00
Cameron Gutman 4caa7ec749 Enable LC_DEBUG for debug builds 2018-07-22 16:05:42 -07:00
Cameron Gutman cf3cbd627d Use full-screen windowed mode to avoid having to enable V-Sync on DXVA2 2018-07-22 15:42:17 -07:00
Cameron Gutman 3bb62a82e1 Avoid doing a display mode change on Linux when entering full-screen 2018-07-22 14:03:47 -07:00
Cameron Gutman a06a625d48 Rework the logo SVG to look better as an app icon, by Jorys Paulin 2018-07-22 03:21:19 -07:00
Cameron Gutman 13d686b6f8 Rename output binary to moonlight 2018-07-22 03:16:53 -07:00
Cameron Gutman 75c7b2828f Remove unneccessary FFmpeg files 2018-07-22 02:47:59 -07:00
Cameron Gutman 18c5e19ac9 Add files for Flatpak deployment 2018-07-22 01:51:05 -07:00
Cameron Gutman 06635d9f9a Update common-c submodule 2018-07-21 20:30:53 -07:00
Cameron Gutman ad57a55d12 Add (untested) Steam Link video decoding/rendering 2018-07-21 20:29:05 -07:00
Cameron Gutman a97e4babf9 Fix build on GCC 4.x and lower 2018-07-21 19:34:13 -07:00
Cameron Gutman 1117c49867 Fix another brown-paper-bag bug in the build system 2018-07-21 19:26:24 -07:00
Cameron Gutman dc723a8da1 Fix strict aliasing violation 2018-07-21 19:16:08 -07:00
Cameron Gutman 27239ff320 Fix typo in ARM configuration 2018-07-21 19:15:16 -07:00
Cameron Gutman b073c90f0f Fix build on OpenSSL 1.0.1 2018-07-21 19:02:12 -07:00
Cameron Gutman 519626a6d5 Fix compilation with Qt 5.9 2018-07-21 18:47:41 -07:00
Cameron Gutman 71c11cacd9 Update opus project for ARM 2018-07-21 18:30:12 -07:00
Cameron Gutman b7b452875d Add a missing HAVE_FFMPEG guard 2018-07-21 17:32:00 -07:00
Cameron Gutman eb536c2273 Overhaul platform and decoder/renderer detection 2018-07-21 17:00:09 -07:00
R. Aidan Campbell c816a6198c fix macos build: for macos, Q_OS_UNIX is also defined 2018-07-21 14:31:37 -07:00
Cameron Gutman 399154f5f5 Implement VAAPI rendering for X11 2018-07-21 00:16:03 -07:00
Cameron Gutman 64a839c2f9 Create the window after starting streaming to avoid a mouse capture bug in SDL on Windows 2018-07-20 19:55:07 -07:00
Cameron Gutman 20288210be Fix VT video centering after transition from windowed to full screen 2018-07-20 19:37:54 -07:00
Cameron Gutman 7b235743cb Count the window chrome size in our placement decision and fix sizing after full-screen toggle 2018-07-20 19:18:55 -07:00
Cameron Gutman 4c507f1179 Move SDL subsystem init into appropriate components to fix gamepad detection issue 2018-07-20 18:15:46 -07:00
Cameron Gutman e516861366 Set window dimensions properly when toggling full-screen 2018-07-20 16:01:22 -07:00
R. Aidan Campbell 48c2a8c5b0 Merge branch 'master' of github.com:cgutman/moonlight-qt 2018-07-20 15:47:57 -07:00
R. Aidan Campbell b41e8c07d2 Switched settings page to two-pane layout 2018-07-20 15:47:50 -07:00
Cameron Gutman 10b5245919 Reset the decoder when D3D state is lost 2018-07-20 15:31:57 -07:00
R. Aidan Campbell 86874a2e1c minor wording change in settings 2018-07-20 14:36:39 -07:00
R. Aidan Campbell f951a01a68 changed audio configuration to a dropdown, allowing auto/stereo/surround 2018-07-20 14:35:47 -07:00
R. Aidan Campbell d12fed3e7f remove unused settings 2018-07-20 14:24:27 -07:00
R. Aidan Campbell 4566139c75 now with video codec selection 2018-07-20 14:22:22 -07:00
R. Aidan Campbell 38df63b962 now with video decoder selection 2018-07-20 14:14:38 -07:00
Cameron Gutman faaf9dfa07 Fix crashes when restarting streaming with DXVA2 renderer due to leaked surface references 2018-07-20 13:10:54 -07:00
Cameron Gutman 9c2befc386 Only require OpenSSL 1.1 on Apple platforms where SecureTransport requires keys from PEM_write_bio_PrivateKey_traditional() 2018-07-19 23:19:00 -07:00
Cameron Gutman e85ca2a6c9 Reduce VideoToolbox renderer video latency 2018-07-18 22:25:46 -07:00
Cameron Gutman e32aace443 Draw a black background behind the rendering 2018-07-18 22:07:43 -07:00
Cameron Gutman bd8e4a4bce Fix DXVA video centering 2018-07-18 21:48:46 -07:00
Cameron Gutman 5ee1358712 Implement a proper destructor for ComputerManager to prevent polling threads from living beyond QGuiApplication 2018-07-18 21:27:43 -07:00
Cameron Gutman 06501bd4b7 Fix video scaling on DXVA2 2018-07-18 19:10:14 -07:00
Cameron Gutman a4ad58d9ec Fix some Clang static analysis warnings 2018-07-18 18:39:33 -07:00
Cameron Gutman 76d99cd60e Raise combo keys for all special combos, not just quit 2018-07-18 00:14:12 -07:00
Cameron Gutman 1b85dcc829 Improve frame pacing on VideoToolbox renderer 2018-07-18 00:11:17 -07:00
Cameron Gutman c97e298b80 Use the queueFrame() helper 2018-07-17 20:05:59 -07:00
188 changed files with 3670 additions and 15393 deletions
+4
View File
@@ -1 +1,5 @@
*pro.user
**/.vs/
build/
+40
View File
@@ -0,0 +1,40 @@
language: cpp
git:
depth: 1
matrix:
include:
- os: osx
compiler: clang
- os: linux
sudo: required
dist: xenial
compiler: gcc
addons:
apt:
sources:
- sourceline: 'ppa:beineri/opt-qt596-xenial'
packages:
- qt59base
- qt59quickcontrols2
- qt59svg
- libesd0-dev
- libgl-dev
install:
- '[ "$TRAVIS_OS_NAME" != osx ] || brew install qt5'
- '[ "$TRAVIS_OS_NAME" != osx ] || brew link --force qt5'
- if [[ "$TRAVIS_OS_NAME" = "linux" ]]; then export SDL2_VER=2.0.8 && wget https://www.libsdl.org/release/SDL2-$SDL2_VER.tar.gz && tar -xvf SDL2-$SDL2_VER.tar.gz && cd SDL2-$SDL2_VER && ./configure && make && sudo make install && cd ..; fi
before_script:
- '[ "$TRAVIS_OS_NAME" != "linux" ] || source /opt/qt59/bin/qt59-env.sh'
- qmake -v
- '[[ "$TRAVIS_OS_NAME" != linux || "$CXX" != clang++ ]] || export QMAKESPEC=linux-clang'
- '[[ "$TRAVIS_OS_NAME" != linux || "$CXX" != g++ ]] || export QMAKESPEC=linux-g++'
- '[ "$TRAVIS_OS_NAME" != osx ] || export QMAKESPEC=macx-clang'
script:
- qmake moonlight-qt.pro -spec $QMAKESPEC
- make debug
- make release
+19 -8
View File
@@ -1,20 +1,31 @@
# Moonlight PC
[Moonlight PC](http://moonlight-stream.com) is an open source implementation of NVIDIA's GameStream, as used by the NVIDIA Shield, but built to run on Windows, Mac, and Linux.
[Moonlight PC](http://moonlight-stream.com) is an open source implementation of NVIDIA's GameStream, as used by the NVIDIA Shield, but built to run on Windows, Mac, and Linux. This client is the successor to [Moonlight Chrome](https://github.com/moonlight-stream/moonlight-chrome) for streaming on PC.
Moonlight also has mobile versions for [Android](https://github.com/moonlight-stream/moonlight-android) and [iOS](https://github.com/moonlight-stream/moonlight-ios).
This is a work-in-progress port and is only suitable for development use at this point.
This client is currently still in development (pre-alpha), but the streaming performance is already much better than the older Moonlight Chrome client on many machines. Test releases may be available for your OS on the [Releases page](https://github.com/moonlight-stream/moonlight-qt/releases).
You can follow development on our [Discord server](https://discord.gg/6ERtzFY).
[![Windows AppVeyor Status](https://ci.appveyor.com/api/projects/status/glj5cxqwy2w3bglv/branch/master?svg=true)](https://ci.appveyor.com/project/cgutman/moonlight-qt/branch/master)
[![Mac and Linux Travis CI Status](https://travis-ci.org/moonlight-stream/moonlight-qt.svg?branch=master)](https://travis-ci.org/moonlight-stream/moonlight-qt)
## Features
- Hardware accelerated video decoding on Windows, Mac, and Linux
- Supports streaming at up to 90 FPS on high refresh rate monitors
- Supports streaming at 720p, 1080p, 1440p, or 4K
- 5.1 surround sound audio
- HEVC support for better image quality at reduced bandwidth
- Keyboard and mouse support
- Gamepad support with SDL gamepad mappings
## Building
1. Install the latest Qt SDK (and optionally, the Qt Creator IDE) from https://www.qt.io/download
3. Run `git submodule update --init --recursive` from within `moonlight-qt/`
4. Open the project in Qt Creator or build from qmake on the command line
1. Install the latest Qt SDK (and optionally, the Qt Creator IDE) from https://www.qt.io/download (select MSVC toolchain on Windows)
2. Run `git submodule update --init --recursive` from within `moonlight-qt/`
3. Open the project in Qt Creator or build from qmake on the command line
## Contribute
This project is being actively developed at [XDA Developers](http://forum.xda-developers.com/showthread.php?t=2505510)
1. Fork us
2. Write code
3. Send Pull Requests
+10 -6
View File
@@ -3,24 +3,28 @@
<plist version="1.0">
<dict>
<key>CFBundleExecutable</key>
<string>moonlight-qt</string>
<string>Moonlight</string>
<key>CFBundleGetInfoString</key>
<string>Created by Qt/QMake</string>
<string>Stream games from your NVIDIA GameStream-enabled PC</string>
<key>CFBundleIconFile</key>
<string>macos</string>
<string>moonlight</string>
<key>CFBundleIdentifier</key>
<string>com.yourcompany.moonlight-qt</string>
<string>com.moonlight-stream.Moonlight</string>
<key>NSHighResolutionCapable</key>
<true/>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>LSMinimumSystemVersion</key>
<string>10.10</string>
<key>NOTE</key>
<string>This file was generated by Qt/QMake.</string>
<key>NSPrincipalClass</key>
<string>NSApplication</string>
<key>NSSupportsAutomaticGraphicsSwitching</key>
<true/>
<key>CFBundleVersion</key>
<string>0.0.4</string>
<key>CFBundleDisplayName</key>
<string>Moonlight</string>
</dict>
</plist>
+93 -28
View File
@@ -1,7 +1,16 @@
QT += core quick network quickcontrols2
QT += core quick network quickcontrols2 svg
CONFIG += c++11
TARGET = moonlight-qt
unix:!macx {
TARGET = moonlight
} else {
# On macOS, this is the name displayed in the global menu bar
TARGET = Moonlight
}
# Support debug and release builds from command line for CI
CONFIG += debug_and_release
TEMPLATE = app
# The following define makes your compiler emit warnings if you use
@@ -28,20 +37,32 @@ win32 {
LIBS += ws2_32.lib winmm.lib dxva2.lib ole32.lib
}
macx {
INCLUDEPATH += $$PWD/../libs/mac/include
LIBS += -L$$PWD/../libs/mac/lib
INCLUDEPATH += $$PWD/../libs/mac/include $$PWD/../libs/mac/Frameworks/SDL2.framework/Versions/A/Headers
LIBS += -L$$PWD/../libs/mac/lib -F$$PWD/../libs/mac/Frameworks
}
unix:!macx {
CONFIG += link_pkgconfig
PKGCONFIG += openssl sdl2 libavcodec libavdevice libavformat libavutil
PKGCONFIG += openssl sdl2
LIBS += -ldl
packagesExist(libavcodec) {
PKGCONFIG += libavcodec libavutil
CONFIG += ffmpeg
packagesExist(libva) {
CONFIG += libva
}
}
}
win32 {
LIBS += -llibssl -llibcrypto -lSDL2 -lavcodec -lavdevice -lavformat -lavutil
LIBS += -llibssl -llibcrypto -lSDL2 -lavcodec -lavutil
CONFIG += ffmpeg
}
macx {
LIBS += -lssl -lcrypto -lSDL2 -lavcodec.58 -lavdevice.58 -lavformat.58 -lavutil.56
LIBS += -lobjc -framework VideoToolbox -framework AVFoundation -framework CoreGraphics -framework CoreMedia -framework AppKit
LIBS += -lssl -lcrypto -lavcodec.58 -lavutil.56 -framework SDL2
LIBS += -lobjc -framework VideoToolbox -framework AVFoundation -framework CoreVideo -framework CoreGraphics -framework CoreMedia -framework AppKit
CONFIG += ffmpeg
}
SOURCES += \
@@ -55,17 +76,8 @@ SOURCES += \
streaming/input.cpp \
streaming/session.cpp \
streaming/audio.cpp \
streaming/video/ffmpeg.cpp \
gui/computermodel.cpp \
gui/appmodel.cpp \
streaming/video/ffmpeg-renderers/sdl.cpp
win32 {
SOURCES += streaming/video/ffmpeg-renderers/dxva2.cpp
}
macx {
SOURCES += streaming/video/ffmpeg-renderers/vt.mm
}
gui/appmodel.cpp
HEADERS += \
utils.h \
@@ -79,14 +91,45 @@ HEADERS += \
streaming/session.hpp \
gui/computermodel.h \
gui/appmodel.h \
streaming/video/decoder.h \
streaming/video/ffmpeg.h \
streaming/video/ffmpeg-renderers/renderer.h
streaming/video/decoder.h
# Platform-specific renderers and decoders
ffmpeg {
message(FFmpeg decoder selected)
DEFINES += HAVE_FFMPEG
SOURCES += \
streaming/video/ffmpeg.cpp \
streaming/video/ffmpeg-renderers/sdl.cpp
HEADERS += \
streaming/video/ffmpeg.h \
streaming/video/ffmpeg-renderers/renderer.h
}
libva {
message(VAAPI renderer selected)
DEFINES += HAVE_LIBVA
SOURCES += streaming/video/ffmpeg-renderers/vaapi.cpp
HEADERS += streaming/video/ffmpeg-renderers/vaapi.h
}
config_SLVideo {
message(SLVideo decoder/renderer selected)
DEFINES += HAVE_SLVIDEO
LIBS += -lSLVideo
SOURCES += streaming/video/sl.cpp
HEADERS += streaming/video/sl.h
}
win32 {
message(DXVA2 renderer selected)
SOURCES += streaming/video/ffmpeg-renderers/dxva2.cpp
HEADERS += streaming/video/ffmpeg-renderers/dxva2.h
}
macx {
message(VideoToolbox renderer selected)
SOURCES += streaming/video/ffmpeg-renderers/vt.mm
HEADERS += streaming/video/ffmpeg-renderers/vt.h
}
@@ -121,14 +164,36 @@ else:unix: LIBS += -L$$OUT_PWD/../qmdnsengine/ -lqmdnsengine
INCLUDEPATH += $$PWD/../qmdnsengine/qmdnsengine/src/include $$PWD/../qmdnsengine
DEPENDPATH += $$PWD/../qmdnsengine/qmdnsengine/src/include $$PWD/../qmdnsengine
# Default rules for deployment.
qnx: target.path = /tmp/$${TARGET}/bin
else: unix:!android: target.path = /opt/$${TARGET}/bin
!isEmpty(target.path): INSTALLS += target
unix:!macx: {
isEmpty(PREFIX) {
PREFIX = /usr/local
}
isEmpty(BINDIR) {
BINDIR = bin
}
target.path = $$PREFIX/$$BINDIR/
desktop.files = deploy/linux/com.moonlight_stream.Moonlight.desktop
desktop.path = $$PREFIX/share/applications/
icons.files = res/moonlight.svg
icons.path = $$PREFIX/share/icons/hicolor/scalable/apps/
appdata.files = deploy/linux/com.moonlight_stream.Moonlight.appdata.xml
appdata.path = $$PREFIX/share/metainfo/
INSTALLS += target desktop icons appdata
}
win32 {
RC_ICONS = moonlight.ico
}
macx {
QMAKE_INFO_PLIST = $$PWD/Info.plist
APP_QML_FILES.files = res/macos.icns
APP_QML_FILES.path = Contents/Resources
QMAKE_BUNDLE_DATA += APP_QML_FILES
APP_BUNDLE_RESOURCES.files = moonlight.icns
APP_BUNDLE_RESOURCES.path = Contents/Resources
QMAKE_BUNDLE_DATA += APP_BUNDLE_RESOURCES
}
VERSION = 0.0.4
+55 -5
View File
@@ -140,12 +140,12 @@ NvComputer::NvComputer(QString address, QString serverInfo)
bool NvComputer::wake()
{
if (state == NvComputer::CS_ONLINE) {
qWarning() << name << " is already online";
qWarning() << name << "is already online";
return true;
}
if (macAddress.isNull()) {
qWarning() << name << " has no MAC address stored";
qWarning() << name << "has no MAC address stored";
return false;
}
@@ -184,7 +184,7 @@ bool NvComputer::wake()
// Send to all ports
for (quint16 port : WOL_PORTS) {
if (sock.writeDatagram(wolPayload, address, port)) {
qDebug() << "Send WoL packet to " << name << " via " << address.toString() << ":" << port;
qInfo().nospace().noquote() << "Send WoL packet to " << name << " via " << address.toString() << ":" << port;
success = true;
}
}
@@ -286,6 +286,56 @@ ComputerManager::ComputerManager(QObject *parent)
settings.endArray();
}
ComputerManager::~ComputerManager()
{
QWriteLocker lock(&m_Lock);
// Delete machines that haven't been resolved yet
while (!m_PendingResolution.isEmpty()) {
MdnsPendingComputer* computer = m_PendingResolution.first();
delete computer;
m_PendingResolution.removeFirst();
}
// Delete the browser to stop discovery
delete m_MdnsBrowser;
m_MdnsBrowser = nullptr;
{
QMutableMapIterator<QString, QThread*> i(m_PollThreads);
// Interrupt all polling threads
while (i.hasNext()) {
i.next();
i.value()->requestInterruption();
}
// Wait for all threads to terminate before destroying
// the NvComputer objects.
i.toFront();
while (i.hasNext()) {
i.next();
QThread* thread = i.value();
thread->wait();
delete thread;
i.remove();
}
}
{
QMutableMapIterator<QString, NvComputer*> i(m_KnownHosts);
// Destroy all NvComputer objects
while (i.hasNext()) {
i.next();
delete i.value();
i.remove();
}
}
}
void ComputerManager::saveHosts()
{
QSettings settings;
@@ -312,7 +362,7 @@ void ComputerManager::startPolling()
m_MdnsBrowser = new QMdnsEngine::Browser(&m_MdnsServer, "_nvstream._tcp.local.", &m_MdnsCache);
connect(m_MdnsBrowser, &QMdnsEngine::Browser::serviceAdded,
this, [this](const QMdnsEngine::Service& service) {
qDebug() << "Discovered mDNS host: " << service.hostname();
qInfo() << "Discovered mDNS host:" << service.hostname();
MdnsPendingComputer* pendingComputer = new MdnsPendingComputer(&m_MdnsServer, &m_MdnsCache, service);
connect(pendingComputer, SIGNAL(resolvedv4(MdnsPendingComputer*,QHostAddress)),
@@ -331,7 +381,7 @@ void ComputerManager::startPolling()
void ComputerManager::handleMdnsServiceResolved(MdnsPendingComputer* computer,
const QHostAddress& address)
{
qDebug() << "Resolved " << computer->hostname() << " to " << address.toString();
qInfo() << "Resolved" << computer->hostname() << "to" << address.toString();
addNewHost(address.toString(), true);
+8 -11
View File
@@ -148,31 +148,26 @@ private:
int pollsSinceLastAppListFetch = POLLS_PER_APPLIST_FETCH;
while (!isInterruptionRequested()) {
bool stateChanged = false;
for (int i = 0; i < TRIES_BEFORE_OFFLINING; i++) {
bool online = false;
for (int i = 0; i < TRIES_BEFORE_OFFLINING && !online; i++) {
for (auto& address : m_Computer->uniqueAddresses()) {
if (isInterruptionRequested()) {
return;
}
if (tryPollComputer(address, stateChanged)) {
online = true;
break;
}
}
// No need to continue retrying if we're online
if (m_Computer->state == NvComputer::CS_ONLINE) {
break;
}
}
// Check if we failed after all retry attempts
// Note: we don't need to acquire the read lock here,
// because we're on the writing thread.
if (m_Computer->state != NvComputer::CS_ONLINE) {
if (m_Computer->state != NvComputer::CS_OFFLINE) {
m_Computer->state = NvComputer::CS_OFFLINE;
stateChanged = true;
}
if (!online && m_Computer->state != NvComputer::CS_OFFLINE) {
m_Computer->state = NvComputer::CS_OFFLINE;
stateChanged = true;
}
// Grab the applist if it's empty or it's been long enough that we need to refresh
@@ -249,6 +244,8 @@ class ComputerManager : public QObject
public:
explicit ComputerManager(QObject *parent = nullptr);
virtual ~ComputerManager();
Q_INVOKABLE void startPolling();
Q_INVOKABLE void stopPollingAsync();
+14 -9
View File
@@ -2,12 +2,12 @@
#include "utils.h"
#include <QDebug>
#include <QRandomGenerator64>
#include <openssl/pem.h>
#include <openssl/rsa.h>
#include <openssl/bn.h>
#include <openssl/x509.h>
#include <openssl/rand.h>
#define SER_UNIQUEID "uniqueid"
#define SER_CERT "certificate"
@@ -101,7 +101,7 @@ void IdentityManager::createCredentials(QSettings& settings)
settings.setValue(SER_CERT, m_CachedPemCert);
settings.setValue(SER_KEY, m_CachedPrivateKey);
qDebug() << "Wrote new identity credentials to settings";
qInfo() << "Wrote new identity credentials to settings";
}
IdentityManager::IdentityManager()
@@ -112,7 +112,7 @@ IdentityManager::IdentityManager()
m_CachedPrivateKey = settings.value(SER_KEY).toByteArray();
if (m_CachedPemCert.isEmpty() || m_CachedPrivateKey.isEmpty()) {
qDebug() << "No existing credentials found";
qInfo() << "No existing credentials found";
createCredentials(settings);
}
else if (getSslCertificate().isNull()) {
@@ -157,10 +157,14 @@ IdentityManager::getSslKey()
// We must write out our PEM in the old PKCS1 format for SecureTransport
// on macOS/iOS to be able to read it.
#ifdef Q_OS_DARWIN
PEM_write_bio_PrivateKey_traditional(bio, pk, nullptr, nullptr, 0, nullptr, 0);
#else
PEM_write_bio_PrivateKey(bio, pk, nullptr, nullptr, 0, nullptr, 0);
#endif
BUF_MEM* mem;
BIO_get_mem_ptr(bio, &mem);
PEM_write_bio_PrivateKey_traditional(bio, pk, nullptr, nullptr, 0, nullptr, 0);
m_CachedSslKey = QSslKey(QByteArray::fromRawData(mem->data, (int)mem->length), QSsl::Rsa);
BIO_free(bio);
@@ -187,14 +191,15 @@ IdentityManager::getUniqueId()
// Load the unique ID from settings
m_CachedUniqueId = settings.value(SER_UNIQUEID).toString();
if (!m_CachedUniqueId.isEmpty() && !m_CachedUniqueId.isNull()) {
qDebug() << "Loaded unique ID from settings: " << m_CachedUniqueId;
qInfo() << "Loaded unique ID from settings:" << m_CachedUniqueId;
}
else {
// Generate a new unique ID in base 16
m_CachedUniqueId = QString::number(
QRandomGenerator64::securelySeeded().generate64(), 16);
uint64_t uid;
RAND_bytes(reinterpret_cast<unsigned char*>(&uid), sizeof(uid));
m_CachedUniqueId = QString::number(uid, 16);
qDebug() << "Generated new unique ID: " << m_CachedUniqueId;
qInfo() << "Generated new unique ID:" << m_CachedUniqueId;
settings.setValue(SER_UNIQUEID, m_CachedUniqueId);
}
+22 -7
View File
@@ -123,6 +123,11 @@ NvHTTP::launchApp(int appId,
bool localAudio,
int gamepadMask)
{
int riKeyId;
memcpy(&riKeyId, streamConfig->remoteInputAesIv, sizeof(riKeyId));
riKeyId = qFromBigEndian(riKeyId);
QString response =
openConnectionToString(m_BaseUrlHttps,
"launch",
@@ -132,7 +137,7 @@ NvHTTP::launchApp(int appId,
QString::number(streamConfig->fps)+
"&additionalStates=1&sops="+QString::number(sops ? 1 : 0)+
"&rikey="+QByteArray(streamConfig->remoteInputAesKey, sizeof(streamConfig->remoteInputAesKey)).toHex()+
"&rikeyid="+QString::number(qFromBigEndian(*(int*)streamConfig->remoteInputAesIv))+
"&rikeyid="+QString::number(riKeyId)+
(streamConfig->enableHdr ?
"&hdrMode=1&clientHdrCapVersion=0&clientHdrCapSupportedFlagsInUint32=0&clientHdrCapMetaDataId=NV_STATIC_METADATA_TYPE_1&clientHdrCapDisplayData=0x0x0x0x0x0x0x0x0x0x0" :
"")+
@@ -149,11 +154,16 @@ NvHTTP::launchApp(int appId,
void
NvHTTP::resumeApp(PSTREAM_CONFIGURATION streamConfig)
{
int riKeyId;
memcpy(&riKeyId, streamConfig->remoteInputAesIv, sizeof(riKeyId));
riKeyId = qFromBigEndian(riKeyId);
QString response =
openConnectionToString(m_BaseUrlHttps,
"resume",
"rikey="+QString(QByteArray(streamConfig->remoteInputAesKey, sizeof(streamConfig->remoteInputAesKey)).toHex())+
"&rikeyid="+QString::number(qFromBigEndian(*(int*)streamConfig->remoteInputAesIv))+
"&rikeyid="+QString::number(riKeyId)+
"&surroundAudioInfo="+getSurroundAudioInfoString(streamConfig->audioConfiguration),
false);
@@ -265,7 +275,10 @@ NvHTTP::verifyResponseStatus(QString xml)
else
{
QString statusMessage = xmlReader.attributes().value("status_message").toString();
qDebug() << "Request failed: " << statusCode << " " << statusMessage;
if (statusCode != 401) {
// 401 is expected for unpaired PCs when we fetch serverinfo over HTTPS
qWarning() << "Request failed:" << statusCode << statusMessage;
}
throw GfeHttpResponseException(statusCode, statusMessage);
}
}
@@ -330,7 +343,9 @@ NvHTTP::openConnectionToString(QUrl baseUrl,
QNetworkReply* reply = openConnection(baseUrl, command, arguments, enableTimeout);
QString ret;
ret = QTextStream(reply).readAll();
QTextStream stream(reply);
stream.setCodec("UTF-8");
ret = stream.readAll();
delete reply;
return ret;
@@ -366,13 +381,13 @@ NvHTTP::openConnection(QUrl baseUrl,
{
QTimer::singleShot(REQUEST_TIMEOUT_MS, &loop, SLOT(quit()));
}
qDebug() << "Executing request: " << url.toString();
qInfo() << "Executing request:" << url.toString();
loop.exec(QEventLoop::ExcludeUserInputEvents);
// Abort the request if it timed out
if (!reply->isFinished())
{
qDebug() << "Aborting timed out request for " << url.toString();
qWarning() << "Aborting timed out request for" << url.toString();
reply->abort();
}
@@ -383,7 +398,7 @@ NvHTTP::openConnection(QUrl baseUrl,
// Handle error
if (reply->error() != QNetworkReply::NoError)
{
qDebug() << command << " request failed with error " << reply->error();
qWarning() << command << " request failed with error " << reply->error();
GfeHttpResponseException exception(reply->error(), reply->errorString());
delete reply;
throw exception;
+26 -24
View File
@@ -1,8 +1,6 @@
#include "nvpairingmanager.h"
#include "utils.h"
#include <QRandomGenerator>
#include <openssl/bio.h>
#include <openssl/aes.h>
#include <openssl/rand.h>
@@ -21,7 +19,7 @@ NvPairingManager::NvPairingManager(QString address) :
BIO_free_all(bio);
if (m_Cert == nullptr)
{
throw new std::runtime_error("Unable to load certificate");
throw std::runtime_error("Unable to load certificate");
}
QByteArray pk = IdentityManager::get()->getPrivateKey();
@@ -32,7 +30,7 @@ NvPairingManager::NvPairingManager(QString address) :
BIO_free_all(bio);
if (m_PrivateKey == nullptr)
{
throw new std::runtime_error("Unable to load private key");
throw std::runtime_error("Unable to load private key");
}
}
@@ -89,13 +87,15 @@ NvPairingManager::getSignatureFromPemCert(QByteArray certificate)
X509* cert = PEM_read_bio_X509(bio, nullptr, nullptr, nullptr);
BIO_free_all(bio);
#if (OPENSSL_VERSION_NUMBER < 0x10100000L)
ASN1_BIT_STRING *asnSignature;
#else
const ASN1_BIT_STRING *asnSignature;
#endif
#if (OPENSSL_VERSION_NUMBER < 0x10002000L)
ASN1_BIT_STRING *asnSignature = cert->signature;
#elif (OPENSSL_VERSION_NUMBER < 0x10100000L)
ASN1_BIT_STRING *asnSignature;
X509_get0_signature(&asnSignature, NULL, cert);
#else
const ASN1_BIT_STRING *asnSignature;
X509_get0_signature(&asnSignature, NULL, cert);
#endif
QByteArray signature(reinterpret_cast<char*>(asnSignature->data), asnSignature->length);
@@ -164,7 +164,7 @@ NvPairingManager::PairState
NvPairingManager::pair(QString appVersion, QString pin)
{
int serverMajorVersion = NvHTTP::parseQuad(appVersion).at(0);
qDebug() << "Pairing with server generation: " << serverMajorVersion;
qInfo() << "Pairing with server generation:" << serverMajorVersion;
QCryptographicHash::Algorithm hashAlgo;
int hashLength;
@@ -196,14 +196,14 @@ NvPairingManager::pair(QString appVersion, QString pin)
NvHTTP::verifyResponseStatus(getCert);
if (NvHTTP::getXmlString(getCert, "paired") != "1")
{
qDebug() << "Failed pairing at stage #1";
qCritical() << "Failed pairing at stage #1";
return PairState::FAILED;
}
QByteArray serverCert = NvHTTP::getXmlStringFromHex(getCert, "plaincert");
if (serverCert == nullptr)
{
qDebug() << "Server likely already pairing";
qCritical() << "Server likely already pairing";
m_Http.openConnectionToString(m_Http.m_BaseUrlHttp, "unpair", nullptr, true);
return PairState::ALREADY_IN_PROGRESS;
}
@@ -218,7 +218,7 @@ NvPairingManager::pair(QString appVersion, QString pin)
NvHTTP::verifyResponseStatus(challengeXml);
if (NvHTTP::getXmlString(challengeXml, "paired") != "1")
{
qDebug() << "Failed pairing at stage #2";
qCritical() << "Failed pairing at stage #2";
m_Http.openConnectionToString(m_Http.m_BaseUrlHttp, "unpair", nullptr, true);
return PairState::FAILED;
}
@@ -228,13 +228,15 @@ NvPairingManager::pair(QString appVersion, QString pin)
QByteArray challengeResponse;
QByteArray serverResponse(challengeResponseData.data(), hashLength);
#if (OPENSSL_VERSION_NUMBER < 0x10100000L)
#if (OPENSSL_VERSION_NUMBER < 0x10002000L)
ASN1_BIT_STRING *asnSignature = m_Cert->signature;
#elif (OPENSSL_VERSION_NUMBER < 0x10100000L)
ASN1_BIT_STRING *asnSignature;
#else
const ASN1_BIT_STRING *asnSignature;
#endif
X509_get0_signature(&asnSignature, NULL, m_Cert);
#else
const ASN1_BIT_STRING *asnSignature;
X509_get0_signature(&asnSignature, NULL, m_Cert);
#endif
challengeResponse.append(challengeResponseData.data() + hashLength, 16);
challengeResponse.append(reinterpret_cast<char*>(asnSignature->data), asnSignature->length);
@@ -249,7 +251,7 @@ NvPairingManager::pair(QString appVersion, QString pin)
NvHTTP::verifyResponseStatus(respXml);
if (NvHTTP::getXmlString(respXml, "paired") != "1")
{
qDebug() << "Failed pairing at stage #3";
qCritical() << "Failed pairing at stage #3";
m_Http.openConnectionToString(m_Http.m_BaseUrlHttp, "unpair", nullptr, true);
return PairState::FAILED;
}
@@ -262,7 +264,7 @@ NvPairingManager::pair(QString appVersion, QString pin)
serverSignature,
serverCert))
{
qDebug() << "MITM detected";
qCritical() << "MITM detected";
m_Http.openConnectionToString(m_Http.m_BaseUrlHttp, "unpair", nullptr, true);
return PairState::FAILED;
}
@@ -273,7 +275,7 @@ NvPairingManager::pair(QString appVersion, QString pin)
expectedResponseData.append(serverSecret);
if (QCryptographicHash::hash(expectedResponseData, hashAlgo) != serverResponse)
{
qDebug() << "Incorrect PIN";
qCritical() << "Incorrect PIN";
m_Http.openConnectionToString(m_Http.m_BaseUrlHttp, "unpair", nullptr, true);
return PairState::PIN_WRONG;
}
@@ -290,7 +292,7 @@ NvPairingManager::pair(QString appVersion, QString pin)
NvHTTP::verifyResponseStatus(secretRespXml);
if (NvHTTP::getXmlString(secretRespXml, "paired") != "1")
{
qDebug() << "Failed pairing at stage #4";
qCritical() << "Failed pairing at stage #4";
m_Http.openConnectionToString(m_Http.m_BaseUrlHttp, "unpair", nullptr, true);
return PairState::FAILED;
}
@@ -302,7 +304,7 @@ NvPairingManager::pair(QString appVersion, QString pin)
NvHTTP::verifyResponseStatus(pairChallengeXml);
if (NvHTTP::getXmlString(pairChallengeXml, "paired") != "1")
{
qDebug() << "Failed pairing at stage #5";
qCritical() << "Failed pairing at stage #5";
m_Http.openConnectionToString(m_Http.m_BaseUrlHttp, "unpair", nullptr, true);
return PairState::FAILED;
}
@@ -0,0 +1,50 @@
<?xml version="1.0" encoding="UTF-8"?>
<component type="desktop">
<id>com.moonlight_stream.Moonlight</id>
<metadata_license>CC0-1.0</metadata_license>
<project_license>GPL-3.0+</project_license>
<name>Moonlight</name>
<summary>Stream games from your NVIDIA GameStream-enabled PC</summary>
<description>
<p>Moonlight can stream games or other applications from a PC with an NVIDIA GeForce GTX 600-series or higher GPU and GeForce Experience installed.</p>
<p>GameStream utilizes the NVENC hardware encoder present on NVIDIA GPUs for low-latency streaming at up 4K 60 FPS. Moonlight can utilize VAAPI hardware acceleration for high-performance H.264 and HEVC video decoding with low CPU usage.</p>
</description>
<url type="homepage">https://moonlight-stream.com</url>
<url type="bugtracker">https://github.com/moonlight-stream/moonlight-qt/issues</url>
<url type="help">https://github.com/moonlight-stream/moonlight-docs/wiki/Setup-Guide</url>
<screenshots>
<screenshot type="default">
<image>https://moonlight-stream.com/appstream_resources/moonlight_screenshot_1.png</image>
</screenshot>
<screenshot>
<image>https://moonlight-stream.com/appstream_resources/moonlight_screenshot_2.png</image>
</screenshot>
</screenshots>
<releases>
<release version="0.0.4" date="2018-07-29">
<description>
<p>Initial pre-alpha release for Linux. Please file bugs!</p>
</description>
</release>
</releases>
<categories>
<category>Game</category>
<category>Qt</category>
</categories>
<developer_name>Moonlight Game Streaming Team</developer_name>
<update_contact>cameron@moonlight-stream.com</update_contact>
<launchable type="desktop-id">com.moonlight_stream.Moonlight.desktop</launchable>
<provides>
<binary>moonlight</binary>
</provides>
<content_rating type="oars-1.0" />
</component>
@@ -0,0 +1,9 @@
[Desktop Entry]
Name=Moonlight
Comment=Stream games from your NVIDIA GameStream-enabled PC
Exec=moonlight
Icon=moonlight
Terminal=false
Type=Application
Categories=Qt;Game;
Keywords=nvidia;gamestream;stream;
+4 -3
View File
@@ -12,7 +12,7 @@ GridView {
id: appGrid
anchors.fill: parent
anchors.leftMargin: (parent.width % (cellWidth + anchors.rightMargin)) / 2
anchors.topMargin: 5
anchors.topMargin: 20
anchors.rightMargin: 5
anchors.bottomMargin: 5
cellWidth: 225; cellHeight: 350;
@@ -72,11 +72,12 @@ GridView {
color: "white"
width: parent.width
height: 100
height: 125
anchors.top: appIcon.bottom
font.pointSize: 26
font.pointSize: 22
horizontalAlignment: Text.AlignHCenter
wrapMode: Text.Wrap
elide: Text.ElideRight
}
MouseArea {
+92 -30
View File
@@ -6,6 +6,7 @@ import QtQuick.Layouts 1.3
import ComputerModel 1.0
import ComputerManager 1.0
import StreamingPreferences 1.0
GridView {
property ComputerModel computerModel : createModel()
@@ -13,7 +14,7 @@ GridView {
id: pcGrid
anchors.fill: parent
anchors.leftMargin: (parent.width % (cellWidth + anchors.rightMargin)) / 2
anchors.topMargin: 5
anchors.topMargin: 20
anchors.rightMargin: 5
anchors.bottomMargin: 5
cellWidth: 350; cellHeight: 350;
@@ -25,9 +26,17 @@ GridView {
// routine to run, but only if we start as invisible
visible: false
StreamingPreferences {
id: prefs
}
Component.onCompleted: {
// Setup signals on CM
ComputerManager.computerAddCompleted.connect(addComplete)
if (!prefs.hasAnyHardwareAcceleration()) {
noHwDecoderDialog.open()
}
}
onVisibleChanged: {
@@ -133,44 +142,68 @@ GridView {
horizontalAlignment: Text.AlignHCenter
}
Menu {
id: pcContextMenu
MenuItem {
text: "Wake PC"
onTriggered: computerModel.wakeComputer(index)
}
MenuItem {
text: "Delete PC"
onTriggered: {
deletePcDialog.pcIndex = index
// get confirmation first, actual closing is called from the dialog
deletePcDialog.open()
}
}
}
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.LeftButton | Qt.RightButton;
onClicked: {
if (model.addPc) {
addPcDialog.open()
}
else if (model.online) {
if (model.paired) {
// go to game view
var component = Qt.createComponent("AppView.qml")
var appView = component.createObject(stackView)
appView.computerIndex = index
appView.objectName = model.name
stackView.push(appView)
if(mouse.button === Qt.LeftButton) {
if (model.addPc) {
addPcDialog.open()
}
else {
if (!model.busy) {
var pin = ("0000" + Math.floor(Math.random() * 10000)).slice(-4)
// Stop polling, since pairing may make GFE unresponsive
ComputerManager.stopPollingAsync()
// Kick off pairing in the background
computerModel.pairComputer(index, pin)
// Display the pairing dialog
pairDialog.pin = pin
pairDialog.open()
else if (model.online) {
if (model.paired) {
// go to game view
var component = Qt.createComponent("AppView.qml")
var appView = component.createObject(stackView)
appView.computerIndex = index
appView.objectName = model.name
stackView.push(appView)
}
else {
// cannot pair while something is streaming or attempting to pair
errorDialog.text = "This PC is currently busy. Make sure to quit any running games and try again."
errorDialog.open()
if (!model.busy) {
var pin = ("0000" + Math.floor(Math.random() * 10000)).slice(-4)
// Stop polling, since pairing may make GFE unresponsive
ComputerManager.stopPollingAsync()
// Kick off pairing in the background
computerModel.pairComputer(index, pin)
// Display the pairing dialog
pairDialog.pin = pin
pairDialog.open()
}
else {
// cannot pair while something is streaming or attempting to pair
errorDialog.text = "This PC is currently busy. Make sure to quit any running games and try again."
errorDialog.open()
}
}
} else if(!model.online) {
pcContextMenu.open()
}
}
else {
// TODO: Wake on LAN and delete PC options
else { // right click
if(!model.addPc) { // but only for actual PCs, not the add-pc option
pcContextMenu.open()
}
}
}
}
@@ -184,6 +217,19 @@ GridView {
standardButtons: StandardButton.Ok
}
MessageDialog {
id: noHwDecoderDialog
modality:Qt.WindowModal
icon: StandardIcon.Warning
standardButtons: StandardButton.Ok | StandardButton.Help
text: "No functioning hardware accelerated H.264 video decoder was detected by Moonlight. " +
"Your streaming performance may be severely degraded in this configuration. " +
"Click the Help button for more information on solving this problem."
onHelp: {
Qt.openUrlExternally("https://github.com/moonlight-stream/moonlight-docs/wiki/Fixing-Hardware-Decoding-Problems");
}
}
MessageDialog {
id: pairDialog
// don't allow edits to the rest of the window while open
@@ -196,6 +242,22 @@ GridView {
}
}
MessageDialog {
id: deletePcDialog
// don't allow edits to the rest of the window while open
modality:Qt.WindowModal
property int pcIndex : -1;
text:"Are you sure you want to unpair from this PC?"
standardButtons: StandardButton.Yes |StandardButton.No
onYes: {
console.log("deleting PC pairing for PC at index: " + pcIndex)
computerModel.deleteComputer(pcIndex);
// hack to remove the child from the gridview
model = createModel()
}
}
Dialog {
id: addPcDialog
property string label: "Enter the IP address of your GameStream PC"
+258 -118
View File
@@ -16,14 +16,13 @@ ScrollView {
}
Column {
x: 10
y: 10
width: settingsPage.width
height: 400
padding: 10
id: settingsColumn1
width: settingsPage.width / 2 - padding
GroupBox {
id: basicSettingsGroupBox
width: (parent.width - 20)
width: (parent.width - 2 * parent.padding)
padding: 12
title: "<font color=\"skyblue\">Basic Settings</font>"
font.pointSize: 12
@@ -35,7 +34,7 @@ ScrollView {
Label {
width: parent.width
id: resFPStitle
text: qsTr("Resolution and FPS target")
text: qsTr("Resolution and FPS")
font.pointSize: 12
wrapMode: Text.Wrap
color: "white"
@@ -44,84 +43,113 @@ ScrollView {
Label {
width: parent.width
id: resFPSdesc
text: qsTr("Setting values too high for your device may cause lag or crashes")
text: qsTr("Setting values too high for your PC may cause lag, stuttering, or errors")
font.pointSize: 9
wrapMode: Text.Wrap
color: "white"
}
ComboBox {
// ignore setting the index at first, and actually set it when the component is loaded
Component.onCompleted: {
// load the saved width/height/fps, and iterate through the ComboBox until a match is found
// set it to that index.
var saved_width = prefs.width
var saved_height = prefs.height
var saved_fps = prefs.fps
currentIndex = 0
for(var i = 0; i < resolutionComboBox.count; i++) {
var el_width = parseInt(resolutionListModel.get(i).video_width);
var el_height = parseInt(resolutionListModel.get(i).video_height);
var el_fps = parseInt(resolutionListModel.get(i).video_fps);
if(saved_width === el_width &&
saved_height === el_height &&
saved_fps === el_fps) {
currentIndex = i
}
Row {
spacing: 5
ComboBox {
// ignore setting the index at first, and actually set it when the component is loaded
Component.onCompleted: {
// load the saved width/height, and iterate through the ComboBox until a match is found
// and set it to that index.
var saved_width = prefs.width
var saved_height = prefs.height
currentIndex = 0
for (var i = 0; i < resolutionComboBox.count; i++) {
var el_width = parseInt(resolutionListModel.get(i).video_width);
var el_height = parseInt(resolutionListModel.get(i).video_height);
if (saved_width === el_width && saved_height === el_height) {
currentIndex = i
}
}
}
id: resolutionComboBox
font.pointSize: 9
textRole: "text"
model: ListModel {
id: resolutionListModel
ListElement {
text: "720p"
video_width: "1280"
video_height: "720"
}
ListElement {
text: "1080p"
video_width: "1920"
video_height: "1080"
}
ListElement {
text: "1440p"
video_width: "2560"
video_height: "1440"
}
ListElement {
text: "4K"
video_width: "3840"
video_height: "2160"
}
}
// ::onActivated must be used, as it only listens for when the index is changed by a human
onActivated : {
prefs.width = parseInt(resolutionListModel.get(currentIndex).video_width)
prefs.height = parseInt(resolutionListModel.get(currentIndex).video_height)
prefs.bitrateKbps = prefs.getDefaultBitrate(prefs.width, prefs.height, prefs.fps);
slider.value = prefs.bitrateKbps
}
}
id: resolutionComboBox
width: Math.min(bitrateDesc.implicitWidth, parent.width)
font.pointSize: 9
textRole: "text"
model: ListModel {
id: resolutionListModel
ListElement {
text: "720p 30 FPS"
video_width: "1280"
video_height: "720"
video_fps: "30"
}
ListElement {
text: "720p 60 FPS"
video_width: "1280"
video_height: "720"
video_fps: "60"
}
ListElement {
text: "1080p 30 FPS"
video_width: "1920"
video_height: "1080"
video_fps: "30"
}
ListElement {
text: "1080p 60 FPS"
video_width: "1920"
video_height: "1080"
video_fps: "60"
}
ListElement {
text: "4K 30 FPS"
video_width: "3840"
video_height: "2160"
video_fps: "30"
}
ListElement {
text: "4K 60 FPS"
video_width: "3840"
video_height: "2160"
video_fps: "60"
}
}
// ::onActivated must be used, as it only listens for when the index is changed by a human
onActivated : {
prefs.width = parseInt(resolutionListModel.get(currentIndex).video_width)
prefs.height = parseInt(resolutionListModel.get(currentIndex).video_height)
prefs.fps = parseInt(resolutionListModel.get(currentIndex).video_fps)
ComboBox {
// ignore setting the index at first, and actually set it when the component is loaded
Component.onCompleted: {
// Get the max supported FPS on this system
var max_fps = prefs.getMaximumStreamingFrameRate();
prefs.bitrateKbps = prefs.getDefaultBitrate(prefs.width, prefs.height, prefs.fps);
slider.value = prefs.bitrateKbps
// Use 64 as the cutoff for adding a separate option to
// handle wonky displays that report just over 60 Hz.
if (max_fps > 64) {
fpsListModel.append({"text": max_fps+" FPS", "video_fps": ""+max_fps})
}
var saved_fps = prefs.fps
currentIndex = 0
for (var i = 0; i < fpsComboBox.count; i++) {
var el_fps = parseInt(fpsListModel.get(i).video_fps);
if (el_fps === saved_fps) {
currentIndex = i
}
}
}
id: fpsComboBox
font.pointSize: 9
textRole: "text"
model: ListModel {
id: fpsListModel
ListElement {
text: "30 FPS"
video_fps: "30"
}
ListElement {
text: "60 FPS"
video_fps: "60"
}
// A higher value may be added at runtime
// based on the attached display refresh rate
}
// ::onActivated must be used, as it only listens for when the index is changed by a human
onActivated : {
prefs.fps = parseInt(fpsListModel.get(currentIndex).video_fps)
prefs.bitrateKbps = prefs.getDefaultBitrate(prefs.width, prefs.height, prefs.fps);
slider.value = prefs.bitrateKbps
}
}
}
@@ -151,7 +179,7 @@ ScrollView {
stepSize: 500
from : 500
to: 100000
to: 150000
snapMode: "SnapOnRelease"
width: Math.min(bitrateDesc.implicitWidth, parent.width)
@@ -177,7 +205,7 @@ ScrollView {
GroupBox {
id: audioSettingsGroupBox
width: (parent.width - 20)
width: (parent.width - 2 * parent.padding)
padding: 12
title: "<font color=\"skyblue\">Audio Settings</font>"
font.pointSize: 12
@@ -186,24 +214,66 @@ ScrollView {
anchors.fill: parent
spacing: 5
CheckBox {
id: surroundSoundCheck
text: "<font color=\"white\">Enable 5.1 surround sound</font>"
font.pointSize: 12
Label {
width: parent.width
id: resAudioTitle
text: qsTr("Audio configuration")
font.pointSize: 12
wrapMode: Text.Wrap
color: "white"
}
// the backend actually supports auto/stereo/5.1. We'll expose stereo/5.1
checked: prefs.audioConfig === StreamingPreferences.AC_FORCE_SURROUND
ComboBox {
// ignore setting the index at first, and actually set it when the component is loaded
Component.onCompleted: {
var saved_audio = prefs.audioConfig
currentIndex = 0
for(var i = 0; i < audioListModel.count; i++) {
var el_audio= audioListModel.get(i).val;
if(saved_audio === el_audio) {
currentIndex = i
}
}
}
onCheckedChanged: {
prefs.audioConfig = checked ? StreamingPreferences.AC_FORCE_SURROUND : StreamingPreferences.AC_FORCE_STEREO
id: audioComboBox
width: Math.min(bitrateDesc.implicitWidth, parent.width)
font.pointSize: 9
textRole: "text"
model: ListModel {
id: audioListModel
ListElement {
text: "Autodetect"
val: StreamingPreferences.AC_AUTO
}
ListElement {
text: "Stereo"
val: StreamingPreferences.AC_FORCE_STEREO
}
ListElement {
text: "5.1 surround sound"
val: StreamingPreferences.AC_FORCE_SURROUND
}
}
// ::onActivated must be used, as it only listens for when the index is changed by a human
onActivated : {
prefs.audioConfig = audioListModel.get(currentIndex).val
}
}
}
}
}
Column {
padding: 10
anchors.left: settingsColumn1.right
id: settingsColumn2
width: settingsPage.width / 2 - padding
GroupBox {
id: gamepadSettingsGroupBox
width: (parent.width - 20)
width: (parent.width - parent.padding)
padding: 12
title: "<font color=\"skyblue\">Gamepad Settings</font>"
font.pointSize: 12
@@ -221,38 +291,12 @@ ScrollView {
prefs.multiController = checked
}
}
CheckBox {
id: mouseEmulationCheck
text: "<font color=\"white\">Mouse emulation via gamepad</font>"
font.pointSize: 12
// TODO: make this actually do anything
}
}
}
GroupBox {
id: onScreenControlsGroupBox
width: (parent.width - 20)
padding: 12
title: "<font color=\"skyblue\">On-screen Controls Settings</font>"
font.pointSize: 12
Column {
anchors.fill: parent
spacing: 5
CheckBox {
id: onScreenControlsCheck
text: "<font color=\"white\">Show on-screen controls</font>"
font.pointSize: 12
// TODO: make this actually do anything
}
}
}
GroupBox {
id: hostSettingsGroupBox
width: (parent.width - 20)
width: (parent.width - parent.padding)
padding: 12
title: "<font color=\"skyblue\">Host Settings</font>"
font.pointSize: 12
@@ -285,7 +329,7 @@ ScrollView {
GroupBox {
id: advancedSettingsGroupBox
width: (parent.width - 20)
width: (parent.width - parent.padding)
padding: 12
title: "<font color=\"skyblue\">Advanced Settings</font>"
font.pointSize: 12
@@ -294,11 +338,107 @@ ScrollView {
anchors.fill: parent
spacing: 5
CheckBox {
id: neverDropFramesCheck
text: "<font color=\"white\">Never drop frames</font>"
font.pointSize: 12
Label {
width: parent.width
id: resVDSTitle
text: qsTr("Video decoder")
font.pointSize: 12
wrapMode: Text.Wrap
color: "white"
}
ComboBox {
// ignore setting the index at first, and actually set it when the component is loaded
Component.onCompleted: {
var saved_vds = prefs.videoDecoderSelection
currentIndex = 0
for(var i = 0; i < decoderListModel.count; i++) {
var el_vds = decoderListModel.get(i).val;
if(saved_vds === el_vds) {
currentIndex = i
}
}
}
id: decoderComboBox
width: Math.min(bitrateDesc.implicitWidth, parent.width)
font.pointSize: 9
textRole: "text"
model: ListModel {
id: decoderListModel
ListElement {
text: "Automatic (Recommended)"
val: StreamingPreferences.VDS_AUTO
}
ListElement {
text: "Force software decoding"
val: StreamingPreferences.VDS_FORCE_SOFTWARE
}
ListElement {
text: "Force hardware decoding"
val: StreamingPreferences.VDS_FORCE_HARDWARE
}
}
// ::onActivated must be used, as it only listens for when the index is changed by a human
onActivated : {
prefs.videoDecoderSelection = decoderListModel.get(currentIndex).val
}
}
Label {
width: parent.width
id: resVCCTitle
text: qsTr("Video codec")
font.pointSize: 12
wrapMode: Text.Wrap
color: "white"
}
ComboBox {
// ignore setting the index at first, and actually set it when the component is loaded
Component.onCompleted: {
var saved_vcc = prefs.videoCodecConfig
currentIndex = 0
for(var i = 0; i < codecListModel.count; i++) {
var el_vcc = codecListModel.get(i).val;
if(saved_vcc === el_vcc) {
currentIndex = i
}
}
}
id: codecComboBox
width: Math.min(bitrateDesc.implicitWidth, parent.width)
font.pointSize: 9
textRole: "text"
model: ListModel {
id: codecListModel
ListElement {
text: "Automatic (Recommended)"
val: StreamingPreferences.VCC_AUTO
}
ListElement {
text: "Force H.264"
val: StreamingPreferences.VCC_FORCE_H264
}
ListElement {
text: "Force HEVC"
val: StreamingPreferences.VCC_FORCE_HEVC
}
// HDR seems to be broken in GFE 3.14.1, and even when that's fixed
// we'll probably need to gate this feature on OS support in our
// renderers.
/* ListElement {
text: "Force HEVC HDR"
val: StreamingPreferences.VCC_FORCE_HEVC_HDR
} */
}
// ::onActivated must be used, as it only listens for when the index is changed by a human
onActivated : {
prefs.videoCodecConfig = codecListModel.get(currentIndex).val
}
}
}
}
}
+10 -5
View File
@@ -123,10 +123,15 @@ void AppModel::handleBoxArtLoaded(NvComputer* computer, NvApp app, QUrl /* image
Q_ASSERT(computer == m_Computer);
int index = m_Apps.indexOf(app);
Q_ASSERT(index >= 0);
// Let our view know the box art data has changed for this app
emit dataChanged(createIndex(index, 0),
createIndex(index, 0),
QVector<int>() << BoxArtRole);
// Make sure we're not delivering a callback to an app that's already been removed
if (index >= 0) {
// Let our view know the box art data has changed for this app
emit dataChanged(createIndex(index, 0),
createIndex(index, 0),
QVector<int>() << BoxArtRole);
}
else {
qWarning() << "App not found for box art callback:" << app.name;
}
}
+3
View File
@@ -80,6 +80,9 @@ ApplicationWindow {
}
ToolButton {
// TODO: Implement gamepad mapping then unhide this button
visible: false
icon.source: "qrc:/res/ic_videogame_asset_white_48px.svg"
onClicked: navigateTo("qrc:/gui/GamepadMapper.qml", "Gamepad Mapping")
+129 -5
View File
@@ -2,6 +2,8 @@
#include <QQmlApplicationEngine>
#include <QIcon>
#include <QQuickStyle>
#include <QMutex>
#include <QtDebug>
// Don't let SDL hook our main function, since Qt is already
// doing the same thing. This needs to be before any headers
@@ -14,8 +16,134 @@
#include "streaming/session.hpp"
#include "settings/streamingpreferences.h"
#if !defined(QT_DEBUG) && defined(Q_OS_WIN32)
// Log to file for release Windows builds
#define USE_CUSTOM_LOGGER
#define LOG_TO_FILE
#elif defined(Q_OS_UNIX) && !defined(Q_OS_DARWIN)
// Use stdout logger on all Linux/BSD builds
#define USE_CUSTOM_LOGGER
#elif !defined(QT_DEBUG) && defined(Q_OS_DARWIN)
// Log to file for release Mac builds
#define USE_CUSTOM_LOGGER
#define LOG_TO_FILE
#else
// For debug Windows and Mac builds, use default logger
#endif
#ifdef USE_CUSTOM_LOGGER
static QTime s_LoggerTime;
static QTextStream s_LoggerStream(stdout);
static QMutex s_LoggerLock;
#ifdef LOG_TO_FILE
#define MAX_LOG_LINES 10000
static int s_LogLinesWritten = 0;
static bool s_LogLimitReached = false;
static QFile* s_LoggerFile;
#endif
void logToLoggerStream(QString& message)
{
QMutexLocker lock(&s_LoggerLock);
#ifdef LOG_TO_FILE
if (s_LogLimitReached) {
return;
}
else if (s_LogLinesWritten == MAX_LOG_LINES) {
s_LoggerStream << "Log size limit reached!" << endl;
s_LogLimitReached = true;
return;
}
else {
s_LogLinesWritten++;
}
#endif
s_LoggerStream << message << endl;
}
void sdlLogToDiskHandler(void*, int category, SDL_LogPriority priority, const char* message)
{
QString priorityTxt;
switch (priority) {
case SDL_LOG_PRIORITY_VERBOSE:
priorityTxt = "Verbose";
break;
case SDL_LOG_PRIORITY_DEBUG:
priorityTxt = "Debug";
break;
case SDL_LOG_PRIORITY_INFO:
priorityTxt = "Info";
break;
case SDL_LOG_PRIORITY_WARN:
priorityTxt = "Warn";
break;
case SDL_LOG_PRIORITY_ERROR:
priorityTxt = "Error";
break;
case SDL_LOG_PRIORITY_CRITICAL:
priorityTxt = "Critical";
break;
default:
priorityTxt = "Unknown";
break;
}
QTime logTime = QTime::fromMSecsSinceStartOfDay(s_LoggerTime.elapsed());
QString txt = QString("%1 - SDL %2 (%3): %4").arg(logTime.toString()).arg(priorityTxt).arg(category).arg(message);
logToLoggerStream(txt);
}
void qtLogToDiskHandler(QtMsgType type, const QMessageLogContext&, const QString& msg)
{
QString typeTxt;
switch (type) {
case QtDebugMsg:
typeTxt = "Debug";
break;
case QtInfoMsg:
typeTxt = "Info";
break;
case QtWarningMsg:
typeTxt = "Warning";
break;
case QtCriticalMsg:
typeTxt = "Critical";
break;
case QtFatalMsg:
typeTxt = "Fatal";
break;
}
QTime logTime = QTime::fromMSecsSinceStartOfDay(s_LoggerTime.elapsed());
QString txt = QString("%1 - Qt %2: %3").arg(logTime.toString()).arg(typeTxt).arg(msg);
logToLoggerStream(txt);
}
#endif
int main(int argc, char *argv[])
{
#ifdef USE_CUSTOM_LOGGER
#ifdef LOG_TO_FILE
QDir tempDir(QDir::tempPath());
s_LoggerFile = new QFile(tempDir.filePath(QString("Moonlight-%1.log").arg(QDateTime::currentSecsSinceEpoch())));
if (s_LoggerFile->open(QIODevice::WriteOnly)) {
qInfo() << "Redirecting log output to " << s_LoggerFile->fileName();
s_LoggerStream.setDevice(s_LoggerFile);
}
#endif
s_LoggerTime.start();
qInstallMessageHandler(qtLogToDiskHandler);
SDL_LogSetOutputFunction(sdlLogToDiskHandler, nullptr);
#endif
QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
// This avoids using the default keychain for SSL, which may cause
@@ -53,12 +181,8 @@ int main(int argc, char *argv[])
if (engine.rootObjects().isEmpty())
return -1;
// Ensure that SDL is always initialized since we may need to use it
// for non-streaming purposes (like checking on audio devices)
SDL_SetMainReady();
if (SDL_Init(SDL_INIT_VIDEO |
SDL_INIT_AUDIO |
SDL_INIT_GAMECONTROLLER) != 0) {
if (SDL_Init(0) != 0) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"SDL_Init() failed: %s",
SDL_GetError());
Binary file not shown.
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 345 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

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

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

+44 -14
View File
@@ -1,4 +1,5 @@
#include "streamingpreferences.h"
#include "streaming/session.hpp"
#include <QSettings>
@@ -57,24 +58,53 @@ void StreamingPreferences::save()
settings.setValue(SER_VIDEODEC, static_cast<int>(videoDecoderSelection));
}
bool StreamingPreferences::hasAnyHardwareAcceleration()
{
// Always use VDS_AUTO to avoid spamming the user with warnings
// if they've forced software decoding.
return Session::isHardwareDecodeAvailable(VDS_AUTO,
VIDEO_FORMAT_H264,
1920, 1080, 60);
}
int StreamingPreferences::getMaximumStreamingFrameRate()
{
// Never let the maximum drop below 60 FPS
int maxFrameRate = 60;
if (SDL_InitSubSystem(SDL_INIT_VIDEO) != 0) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"SDL_InitSubSystem(SDL_INIT_VIDEO) failed: %s",
SDL_GetError());
return maxFrameRate;
}
for (int i = 0; i < SDL_GetNumVideoDisplays(); i++) {
SDL_DisplayMode mode;
if (SDL_GetCurrentDisplayMode(i, &mode) == 0) {
// Cap the frame rate at 90 FPS, since I can't seem to get
// much more out of GFE even with the game rendering at > 300 FPS.
maxFrameRate = qMax(maxFrameRate, qMin(90, mode.refresh_rate));
}
}
SDL_QuitSubSystem(SDL_INIT_VIDEO);
return maxFrameRate;
}
int StreamingPreferences::getDefaultBitrate(int width, int height, int fps)
{
if (width * height * fps <= 1280 * 720 * 30) {
return 5000;
if (width * height <= 1280 * 720) {
return static_cast<int>(5000 * (fps / 30.0));
}
else if (width * height * fps <= 1280 * 720 * 60) {
return 10000;
else if (width * height <= 1920 * 1080) {
return static_cast<int>(10000 * (fps / 30.0));
}
else if (width * height * fps <= 1920 * 1080 * 30) {
return 10000;
else if (width * height <= 2560 * 1440) {
return static_cast<int>(20000 * (fps / 30.0));
}
else if (width * height * fps <= 1920 * 1080 * 60) {
return 20000;
}
else if (width * height * fps <= 3840 * 2160 * 30) {
return 40000;
}
else /* if (width * height * fps <= 3840 * 2160 * 60) */ {
return 80000;
else /* if (width * height <= 3840 * 2160) */ {
return static_cast<int>(40000 * (fps / 30.0));
}
}
+4
View File
@@ -14,6 +14,10 @@ public:
Q_INVOKABLE void save();
Q_INVOKABLE static bool hasAnyHardwareAcceleration();
Q_INVOKABLE static int getMaximumStreamingFrameRate();
void reload();
enum AudioConfig
+35 -5
View File
@@ -23,6 +23,14 @@ int Session::sdlDetermineAudioConfiguration()
{
SDL_AudioSpec want, have;
SDL_AudioDeviceID dev;
int ret;
if (SDL_InitSubSystem(SDL_INIT_AUDIO) != 0) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"SDL_InitSubSystem(SDL_INIT_AUDIO) failed: %s",
SDL_GetError());
return AUDIO_CONFIGURATION_STEREO;
}
SDL_zero(want);
want.freq = 48000;
@@ -38,7 +46,8 @@ int Session::sdlDetermineAudioConfiguration()
"Failed to open audio device");
// We'll probably have issues during audio stream init, but we'll
// try anyway
return AUDIO_CONFIGURATION_STEREO;
ret = AUDIO_CONFIGURATION_STEREO;
goto Exit;
}
SDL_CloseAudioDevice(dev);
@@ -50,11 +59,15 @@ int Session::sdlDetermineAudioConfiguration()
// We don't support quadraphonic or 7.1 surround, but SDL
// should be able to downmix or upmix better from 5.1 than
// from stereo, so use 5.1 in non-stereo cases.
return AUDIO_CONFIGURATION_51_SURROUND;
ret = AUDIO_CONFIGURATION_51_SURROUND;
}
else {
return AUDIO_CONFIGURATION_STEREO;
ret = AUDIO_CONFIGURATION_STEREO;
}
Exit:
SDL_QuitSubSystem(SDL_INIT_AUDIO);
return ret;
}
int Session::sdlAudioInit(int /* audioConfiguration */,
@@ -64,6 +77,14 @@ int Session::sdlAudioInit(int /* audioConfiguration */,
SDL_AudioSpec want, have;
int error;
SDL_assert(!SDL_WasInit(SDL_INIT_AUDIO));
if (SDL_InitSubSystem(SDL_INIT_AUDIO) != 0) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"SDL_InitSubSystem(SDL_INIT_AUDIO) failed: %s",
SDL_GetError());
return AUDIO_CONFIGURATION_STEREO;
}
SDL_zero(want);
want.freq = opusConfig->sampleRate;
want.format = AUDIO_S16;
@@ -80,6 +101,7 @@ int Session::sdlAudioInit(int /* audioConfiguration */,
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Failed to open audio device: %s",
SDL_GetError());
SDL_QuitSubSystem(SDL_INIT_AUDIO);
return -1;
}
@@ -95,6 +117,7 @@ int Session::sdlAudioInit(int /* audioConfiguration */,
error);
SDL_CloseAudioDevice(s_AudioDevice);
s_AudioDevice = 0;
SDL_QuitSubSystem(SDL_INIT_AUDIO);
return -2;
}
@@ -104,7 +127,7 @@ int Session::sdlAudioInit(int /* audioConfiguration */,
// pending data count. Get a baseline so we
// can exclude that data.
s_BaselinePendingData = 0;
#ifdef _WIN32
#ifdef Q_OS_WIN32
for (int i = 0; i < 100; i++) {
s_BaselinePendingData = qMax(s_BaselinePendingData, SDL_GetQueuedAudioSize(s_AudioDevice));
SDL_Delay(10);
@@ -119,6 +142,10 @@ int Session::sdlAudioInit(int /* audioConfiguration */,
s_SampleIndex = 0;
s_PendingDrops = s_PendingHardDrops = 0;
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Audio stream has %d channels",
opusConfig->channelCount);
return 0;
}
@@ -140,7 +167,10 @@ void Session::sdlAudioCleanup()
s_AudioDevice = 0;
opus_multistream_decoder_destroy(s_OpusDecoder);
s_OpusDecoder = NULL;
s_OpusDecoder = nullptr;
SDL_QuitSubSystem(SDL_INIT_AUDIO);
SDL_assert(!SDL_WasInit(SDL_INIT_AUDIO));
}
void Session::sdlAudioDecodeAndPlaySample(char* sampleData, int sampleLength)
+103 -92
View File
@@ -9,7 +9,7 @@
#ifndef VK_F1
#define VK_F1 0x70
#define VK_F13 0x7C
#define VK_NUMPAD1 0x61
#define VK_NUMPAD0 0x60
#endif
const int SdlInputHandler::k_ButtonMap[] = {
@@ -23,6 +23,15 @@ const int SdlInputHandler::k_ButtonMap[] = {
SdlInputHandler::SdlInputHandler(bool multiController)
: m_MultiController(multiController)
{
// We need to reinit this each time, since you only get
// an initial set of gamepad arrival events once per init.
SDL_assert(!SDL_WasInit(SDL_INIT_GAMECONTROLLER));
if (SDL_InitSubSystem(SDL_INIT_GAMECONTROLLER) != 0) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"SDL_InitSubSystem(SDL_INIT_GAMECONTROLLER) failed: %s",
SDL_GetError());
}
if (!m_MultiController) {
// Player 1 is always present in non-MC mode
m_GamepadMask = 0x1;
@@ -38,6 +47,9 @@ SdlInputHandler::~SdlInputHandler()
SDL_GameControllerClose(m_GamepadState[i].controller);
}
}
SDL_QuitSubSystem(SDL_INIT_GAMECONTROLLER);
SDL_assert(!SDL_WasInit(SDL_INIT_GAMECONTROLLER));
}
void SdlInputHandler::handleKeyEvent(SDL_KeyboardEvent* event)
@@ -51,26 +63,26 @@ void SdlInputHandler::handleKeyEvent(SDL_KeyboardEvent* event)
(event->keysym.mod & KMOD_ALT) &&
(event->keysym.mod & KMOD_SHIFT)) {
// Force raise all keys in the combo to avoid
// leaving them down after disconnecting
LiSendKeyboardEvent(0xA0, KEY_ACTION_UP, 0);
LiSendKeyboardEvent(0xA1, KEY_ACTION_UP, 0);
LiSendKeyboardEvent(0xA2, KEY_ACTION_UP, 0);
LiSendKeyboardEvent(0xA3, KEY_ACTION_UP, 0);
LiSendKeyboardEvent(0xA4, KEY_ACTION_UP, 0);
LiSendKeyboardEvent(0xA5, KEY_ACTION_UP, 0);
// Check for quit combo (Ctrl+Alt+Shift+Q)
if (event->keysym.sym == SDLK_q) {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Detected quit key combo");
// Force raise all keys in the combo to avoid
// leaving them down after disconnecting
LiSendKeyboardEvent(0xA0, KEY_ACTION_UP, 0);
LiSendKeyboardEvent(0xA1, KEY_ACTION_UP, 0);
LiSendKeyboardEvent(0xA2, KEY_ACTION_UP, 0);
LiSendKeyboardEvent(0xA3, KEY_ACTION_UP, 0);
LiSendKeyboardEvent(0xA4, KEY_ACTION_UP, 0);
LiSendKeyboardEvent(0xA5, KEY_ACTION_UP, 0);
// Uncapture the mouse to avoid processing any
// further keyboard input
SDL_SetRelativeMouseMode(SDL_FALSE);
SDL_Event event;
// Drain the event queue of any additional input
// that might be processed before our quit message.
while (SDL_PollEvent(&event));
// Push a quit event to the main loop
event.type = SDL_QUIT;
event.quit.timestamp = SDL_GetTicks();
@@ -88,12 +100,7 @@ void SdlInputHandler::handleKeyEvent(SDL_KeyboardEvent* event)
else if (event->keysym.sym == SDLK_x) {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Detected full-screen toggle combo");
if (SDL_GetWindowFlags(Session::s_ActiveSession->m_Window) & SDL_WINDOW_FULLSCREEN) {
SDL_SetWindowFullscreen(Session::s_ActiveSession->m_Window, 0);
}
else {
SDL_SetWindowFullscreen(Session::s_ActiveSession->m_Window, SDL_WINDOW_FULLSCREEN);
}
Session::s_ActiveSession->toggleFullscreen();
return;
}
}
@@ -115,195 +122,199 @@ void SdlInputHandler::handleKeyEvent(SDL_KeyboardEvent* event)
modifiers |= MODIFIER_SHIFT;
}
// Set keycode
if (event->keysym.sym >= SDLK_0 && event->keysym.sym <= SDLK_9) {
keyCode = (event->keysym.sym - SDLK_0) + VK_0;
// Set keycode. We explicitly use scancode here because GFE will try to correct
// for AZERTY layouts on the host but it depends on receiving VK_ values matching
// a QWERTY layout to work.
if (event->keysym.scancode >= SDL_SCANCODE_1 && event->keysym.scancode <= SDL_SCANCODE_9) {
// SDL defines SDL_SCANCODE_0 > SDL_SCANCODE_9, so we need to handle that manually
keyCode = (event->keysym.scancode - SDL_SCANCODE_1) + VK_0 + 1;
}
else if (event->keysym.sym >= SDLK_a && event->keysym.sym <= SDLK_z) {
keyCode = (event->keysym.sym - SDLK_a) + VK_A;
else if (event->keysym.scancode >= SDL_SCANCODE_A && event->keysym.scancode <= SDL_SCANCODE_Z) {
keyCode = (event->keysym.scancode - SDL_SCANCODE_A) + VK_A;
}
else if (event->keysym.sym >= SDLK_F1 && event->keysym.sym <= SDLK_F12) {
keyCode = (event->keysym.sym - SDLK_F1) + VK_F1;
else if (event->keysym.scancode >= SDL_SCANCODE_F1 && event->keysym.scancode <= SDL_SCANCODE_F12) {
keyCode = (event->keysym.scancode - SDL_SCANCODE_F1) + VK_F1;
}
else if (event->keysym.sym >= SDLK_F13 && event->keysym.sym <= SDLK_F24) {
keyCode = (event->keysym.sym - SDLK_F13) + VK_F13;
else if (event->keysym.scancode >= SDL_SCANCODE_F13 && event->keysym.scancode <= SDL_SCANCODE_F24) {
keyCode = (event->keysym.scancode - SDL_SCANCODE_F13) + VK_F13;
}
else if (event->keysym.sym >= SDLK_KP_1 && event->keysym.sym <= SDLK_KP_9) {
// SDL defines SDLK_KP_0 > SDLK_KP_9, so we need to handle that manually
keyCode = (event->keysym.sym - SDLK_KP_1) + VK_NUMPAD1;
else if (event->keysym.scancode >= SDL_SCANCODE_KP_1 && event->keysym.scancode <= SDL_SCANCODE_KP_9) {
// SDL defines SDL_SCANCODE_KP_0 > SDL_SCANCODE_KP_9, so we need to handle that manually
keyCode = (event->keysym.scancode - SDL_SCANCODE_KP_1) + VK_NUMPAD0 + 1;
}
else {
switch (event->keysym.sym) {
case SDLK_BACKSPACE:
switch (event->keysym.scancode) {
case SDL_SCANCODE_BACKSPACE:
keyCode = 0x08;
break;
case SDLK_TAB:
case SDL_SCANCODE_TAB:
keyCode = 0x09;
break;
case SDLK_CLEAR:
case SDL_SCANCODE_CLEAR:
keyCode = 0x0C;
break;
case SDLK_RETURN:
case SDL_SCANCODE_RETURN:
keyCode = 0x0D;
break;
case SDLK_PAUSE:
case SDL_SCANCODE_PAUSE:
keyCode = 0x13;
break;
case SDLK_CAPSLOCK:
case SDL_SCANCODE_CAPSLOCK:
keyCode = 0x14;
break;
case SDLK_ESCAPE:
case SDL_SCANCODE_ESCAPE:
keyCode = 0x1B;
break;
case SDLK_SPACE:
case SDL_SCANCODE_SPACE:
keyCode = 0x20;
break;
case SDLK_PAGEUP:
case SDL_SCANCODE_PAGEUP:
keyCode = 0x21;
break;
case SDLK_PAGEDOWN:
case SDL_SCANCODE_PAGEDOWN:
keyCode = 0x22;
break;
case SDLK_END:
case SDL_SCANCODE_END:
keyCode = 0x23;
break;
case SDLK_HOME:
case SDL_SCANCODE_HOME:
keyCode = 0x24;
break;
case SDLK_LEFT:
case SDL_SCANCODE_LEFT:
keyCode = 0x25;
break;
case SDLK_UP:
case SDL_SCANCODE_UP:
keyCode = 0x26;
break;
case SDLK_RIGHT:
case SDL_SCANCODE_RIGHT:
keyCode = 0x27;
break;
case SDLK_DOWN:
case SDL_SCANCODE_DOWN:
keyCode = 0x28;
break;
case SDLK_SELECT:
case SDL_SCANCODE_SELECT:
keyCode = 0x29;
break;
case SDLK_EXECUTE:
case SDL_SCANCODE_EXECUTE:
keyCode = 0x2B;
break;
case SDLK_PRINTSCREEN:
case SDL_SCANCODE_PRINTSCREEN:
keyCode = 0x2C;
break;
case SDLK_INSERT:
case SDL_SCANCODE_INSERT:
keyCode = 0x2D;
break;
case SDLK_DELETE:
case SDL_SCANCODE_DELETE:
keyCode = 0x2E;
break;
case SDLK_HELP:
case SDL_SCANCODE_HELP:
keyCode = 0x2F;
break;
case SDLK_KP_0:
// See comment above about why we only handle KP_0 here
keyCode = 0x60;
case SDL_SCANCODE_KP_0:
// See comment above about why we only handle SDL_SCANCODE_KP_0 here
keyCode = VK_NUMPAD0;
break;
case SDLK_KP_MULTIPLY:
case SDL_SCANCODE_0:
// See comment above about why we only handle SDL_SCANCODE_0 here
keyCode = VK_0;
break;
case SDL_SCANCODE_KP_MULTIPLY:
keyCode = 0x6A;
break;
case SDLK_KP_PLUS:
case SDL_SCANCODE_KP_PLUS:
keyCode = 0x6B;
break;
case SDLK_KP_COMMA:
case SDL_SCANCODE_KP_COMMA:
keyCode = 0x6C;
break;
case SDLK_KP_MINUS:
case SDL_SCANCODE_KP_MINUS:
keyCode = 0x6D;
break;
case SDLK_KP_DECIMAL:
case SDL_SCANCODE_KP_DECIMAL:
keyCode = 0x6E;
break;
case SDLK_KP_DIVIDE:
case SDL_SCANCODE_KP_DIVIDE:
keyCode = 0x6F;
break;
case SDLK_NUMLOCKCLEAR:
case SDL_SCANCODE_NUMLOCKCLEAR:
keyCode = 0x90;
break;
case SDLK_SCROLLLOCK:
case SDL_SCANCODE_SCROLLLOCK:
keyCode = 0x91;
break;
case SDLK_LSHIFT:
case SDL_SCANCODE_LSHIFT:
keyCode = 0xA0;
break;
case SDLK_RSHIFT:
case SDL_SCANCODE_RSHIFT:
keyCode = 0xA1;
break;
case SDLK_LCTRL:
case SDL_SCANCODE_LCTRL:
keyCode = 0xA2;
break;
case SDLK_RCTRL:
case SDL_SCANCODE_RCTRL:
keyCode = 0xA3;
break;
case SDLK_LALT:
case SDL_SCANCODE_LALT:
keyCode = 0xA4;
break;
case SDLK_RALT:
case SDL_SCANCODE_RALT:
keyCode = 0xA5;
break;
case SDLK_AC_BACK:
case SDL_SCANCODE_AC_BACK:
keyCode = 0xA6;
break;
case SDLK_AC_FORWARD:
case SDL_SCANCODE_AC_FORWARD:
keyCode = 0xA7;
break;
case SDLK_AC_REFRESH:
case SDL_SCANCODE_AC_REFRESH:
keyCode = 0xA8;
break;
case SDLK_AC_STOP:
case SDL_SCANCODE_AC_STOP:
keyCode = 0xA9;
break;
case SDLK_AC_SEARCH:
case SDL_SCANCODE_AC_SEARCH:
keyCode = 0xAA;
break;
case SDLK_AC_BOOKMARKS:
case SDL_SCANCODE_AC_BOOKMARKS:
keyCode = 0xAB;
break;
case SDLK_AC_HOME:
case SDL_SCANCODE_AC_HOME:
keyCode = 0xAC;
break;
case SDLK_SEMICOLON:
case SDL_SCANCODE_SEMICOLON:
keyCode = 0xBA;
break;
case SDLK_PLUS:
keyCode = 0xBB;
break;
case SDLK_COMMA:
case SDL_SCANCODE_COMMA:
keyCode = 0xBC;
break;
case SDLK_MINUS:
case SDL_SCANCODE_MINUS:
keyCode = 0xBD;
break;
case SDLK_PERIOD:
case SDL_SCANCODE_PERIOD:
keyCode = 0xBE;
break;
case SDLK_SLASH:
case SDL_SCANCODE_SLASH:
keyCode = 0xBF;
break;
case SDLK_BACKQUOTE:
case SDL_SCANCODE_GRAVE:
keyCode = 0xC0;
break;
case SDLK_LEFTBRACKET:
case SDL_SCANCODE_LEFTBRACKET:
keyCode = 0xDB;
break;
case SDLK_BACKSLASH:
case SDL_SCANCODE_BACKSLASH:
keyCode = 0xDC;
break;
case SDLK_RIGHTBRACKET:
case SDL_SCANCODE_RIGHTBRACKET:
keyCode = 0xDD;
break;
case SDLK_QUOTE:
case SDL_SCANCODE_APOSTROPHE:
keyCode = 0xDE;
break;
default:
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Unhandled button event: %d",
event->keysym.sym);
event->keysym.scancode);
return;
}
}
+306 -91
View File
@@ -5,12 +5,38 @@
#include <SDL.h>
#include "utils.h"
#ifdef HAVE_FFMPEG
#include "video/ffmpeg.h"
#endif
#ifdef HAVE_SLVIDEO
#include "video/sl.h"
#endif
#ifdef Q_OS_WIN32
// Using full-screen desktop allows us to avoid needing to enable V-sync
// and it also avoids some strange flickering issues on my Win7 test machine
// with Intel HD 5500 graphics.
#define SDL_OS_FULLSCREEN_FLAG SDL_WINDOW_FULLSCREEN_DESKTOP
#else
#define SDL_OS_FULLSCREEN_FLAG SDL_WINDOW_FULLSCREEN
#endif
#ifdef Q_OS_WIN32
// Scaling the icon down on Win32 looks dreadful, so render at lower res
#define ICON_SIZE 32
#else
#define ICON_SIZE 64
#endif
#include <openssl/rand.h>
#include <QRandomGenerator>
#include <QtEndian>
#include <QCoreApplication>
#include <QThreadPool>
#include <QSvgRenderer>
#include <QPainter>
#include <QImage>
CONNECTION_LISTENER_CALLBACKS Session::k_ConnCallbacks = {
Session::clStageStarting,
@@ -82,6 +108,22 @@ bool Session::chooseDecoder(StreamingPreferences::VideoDecoderSelection vds,
SDL_Window* window, int videoFormat, int width, int height,
int frameRate, IVideoDecoder*& chosenDecoder)
{
#ifdef HAVE_SLVIDEO
chosenDecoder = new SLVideoDecoder();
if (chosenDecoder->initialize(vds, window, videoFormat, width, height, frameRate)) {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"SLVideo video decoder chosen");
return true;
}
else {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Unable to load SLVideo decoder");
delete chosenDecoder;
chosenDecoder = nullptr;
}
#endif
#ifdef HAVE_FFMPEG
chosenDecoder = new FFmpegVideoDecoder();
if (chosenDecoder->initialize(vds, window, videoFormat, width, height, frameRate)) {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
@@ -94,6 +136,7 @@ bool Session::chooseDecoder(StreamingPreferences::VideoDecoderSelection vds,
delete chosenDecoder;
chosenDecoder = nullptr;
}
#endif
// If we reach this, we didn't initialize any decoders successfully
return false;
@@ -101,12 +144,17 @@ bool Session::chooseDecoder(StreamingPreferences::VideoDecoderSelection vds,
int Session::drSetup(int videoFormat, int width, int height, int frameRate, void *, int)
{
if (!chooseDecoder(s_ActiveSession->m_Preferences.videoDecoderSelection,
s_ActiveSession->m_Window,
videoFormat, width, height, frameRate,
s_ActiveSession->m_VideoDecoder)) {
return -1;
}
s_ActiveSession->m_ActiveVideoFormat = videoFormat;
s_ActiveSession->m_ActiveVideoWidth = width;
s_ActiveSession->m_ActiveVideoHeight = height;
s_ActiveSession->m_ActiveVideoFrameRate = frameRate;
// Defer decoder setup until we've started streaming so we
// don't have to hide and show the SDL window (which seems to
// cause pointer hiding to break on Windows).
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Video stream is %dx%dx%d (format 0x%x)",
width, height, frameRate, videoFormat);
return 0;
}
@@ -117,7 +165,16 @@ int Session::drSubmitDecodeUnit(PDECODE_UNIT du)
// from underneath the session when we initiate destruction.
// We need to destroy the decoder on the main thread to satisfy
// some API constraints (like DXVA2).
SDL_AtomicLock(&s_ActiveSession->m_DecoderLock);
if (s_ActiveSession->m_NeedsIdr) {
// If we reset our decoder, we'll need to request an IDR frame
s_ActiveSession->m_NeedsIdr = false;
SDL_AtomicUnlock(&s_ActiveSession->m_DecoderLock);
return DR_NEED_IDR;
}
IVideoDecoder* decoder = s_ActiveSession->m_VideoDecoder;
if (decoder != nullptr) {
int ret = decoder->submitDecodeUnit(du);
@@ -135,23 +192,35 @@ bool Session::isHardwareDecodeAvailable(StreamingPreferences::VideoDecoderSelect
{
IVideoDecoder* decoder;
if (SDL_InitSubSystem(SDL_INIT_VIDEO) != 0) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"SDL_InitSubSystem(SDL_INIT_VIDEO) failed: %s",
SDL_GetError());
return false;
}
SDL_Window* window = SDL_CreateWindow("", 0, 0, width, height, SDL_WINDOW_HIDDEN);
if (!window) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Failed to create window for hardware decode test: %s",
SDL_GetError());
SDL_QuitSubSystem(SDL_INIT_VIDEO);
return false;
}
if (!chooseDecoder(vds, window, videoFormat, width, height, frameRate, decoder)) {
SDL_DestroyWindow(window);
SDL_QuitSubSystem(SDL_INIT_VIDEO);
return false;
}
SDL_DestroyWindow(window);
bool ret = decoder->isHardwareAccelerated();
delete decoder;
SDL_QuitSubSystem(SDL_INIT_VIDEO);
return ret;
}
@@ -160,7 +229,8 @@ Session::Session(NvComputer* computer, NvApp& app)
m_App(app),
m_Window(nullptr),
m_VideoDecoder(nullptr),
m_DecoderLock(0)
m_DecoderLock(0),
m_NeedsIdr(false)
{
LiInitializeVideoCallbacks(&m_VideoCallbacks);
m_VideoCallbacks.setup = drSetup;
@@ -178,14 +248,16 @@ Session::Session(NvComputer* computer, NvApp& app)
m_StreamConfig.fps = m_Preferences.fps;
m_StreamConfig.bitrate = m_Preferences.bitrateKbps;
m_StreamConfig.hevcBitratePercentageMultiplier = 75;
for (unsigned int i = 0; i < sizeof(m_StreamConfig.remoteInputAesKey); i++) {
m_StreamConfig.remoteInputAesKey[i] =
(char)(QRandomGenerator::global()->generate() % 256);
}
*(int*)m_StreamConfig.remoteInputAesIv = qToBigEndian(QRandomGenerator::global()->generate());
RAND_bytes(reinterpret_cast<unsigned char*>(m_StreamConfig.remoteInputAesKey),
sizeof(m_StreamConfig.remoteInputAesKey));
// Only the first 4 bytes are populated in the RI key IV
RAND_bytes(reinterpret_cast<unsigned char*>(m_StreamConfig.remoteInputAesIv), 4);
switch (m_Preferences.audioConfig)
{
case StreamingPreferences::AC_AUTO:
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Autodetecting audio configuration");
m_StreamConfig.audioConfiguration = sdlDetermineAudioConfiguration();
break;
case StreamingPreferences::AC_FORCE_STEREO:
@@ -196,6 +268,10 @@ Session::Session(NvComputer* computer, NvApp& app)
break;
}
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Audio configuration: %d",
m_StreamConfig.audioConfiguration);
switch (m_Preferences.videoCodecConfig)
{
case StreamingPreferences::VCC_AUTO:
@@ -242,23 +318,30 @@ bool Session::validateLaunch()
QStringList warningList;
if (m_StreamConfig.supportsHevc) {
if (m_Preferences.videoCodecConfig == StreamingPreferences::VCC_FORCE_HEVC ||
m_Preferences.videoCodecConfig == StreamingPreferences::VCC_FORCE_HEVC_HDR) {
if (m_Computer->maxLumaPixelsHEVC == 0) {
emit displayLaunchWarning("Your host PC GPU doesn't support HEVC. "
"A GeForce GTX 900-series (Maxwell) or later GPU is required for HEVC streaming.");
}
}
else if (!isHardwareDecodeAvailable(m_Preferences.videoDecoderSelection,
VIDEO_FORMAT_H265,
m_StreamConfig.width,
m_StreamConfig.height,
m_StreamConfig.fps)) {
bool hevcForced = m_Preferences.videoCodecConfig == StreamingPreferences::VCC_FORCE_HEVC ||
m_Preferences.videoCodecConfig == StreamingPreferences::VCC_FORCE_HEVC_HDR;
if (!isHardwareDecodeAvailable(m_Preferences.videoDecoderSelection,
VIDEO_FORMAT_H265,
m_StreamConfig.width,
m_StreamConfig.height,
m_StreamConfig.fps)) {
// NOTE: HEVC currently uses only 1 slice regardless of what
// we provide in CAPABILITY_SLICES_PER_FRAME(), so we should
// never use it for software decoding (unless common-c starts
// respecting it for HEVC).
m_StreamConfig.supportsHevc = false;
if (hevcForced) {
emit displayLaunchWarning("This PC's GPU doesn't support HEVC decoding.");
}
}
if (hevcForced) {
if (m_Computer->maxLumaPixelsHEVC == 0) {
emit displayLaunchWarning("Your host PC GPU doesn't support HEVC. "
"A GeForce GTX 900-series (Maxwell) or later GPU is required for HEVC streaming.");
}
}
}
@@ -280,7 +363,7 @@ bool Session::validateLaunch()
m_StreamConfig.width,
m_StreamConfig.height,
m_StreamConfig.fps)) {
emit displayLaunchWarning("Your client PC GPU doesn't support HEVC Main10 decoding for HDR streaming.");
emit displayLaunchWarning("This PC's GPU doesn't support HEVC Main10 decoding for HDR streaming.");
}
else {
// TODO: Also validate display capabilites
@@ -320,9 +403,6 @@ class DeferredSessionCleanupTask : public QRunnable
{
// Finish cleanup of the connection state
LiStopConnection();
if (Session::s_ActiveSession->m_Window != nullptr) {
SDL_DestroyWindow(Session::s_ActiveSession->m_Window);
}
// Allow another session to start now that we're cleaned up
Session::s_ActiveSession = nullptr;
@@ -330,6 +410,103 @@ class DeferredSessionCleanupTask : public QRunnable
}
};
void Session::getWindowDimensions(bool fullScreen,
int& x, int& y,
int& width, int& height)
{
int displayIndex = 0;
if (m_Window != nullptr) {
displayIndex = SDL_GetWindowDisplayIndex(m_Window);
if (displayIndex < 0) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"SDL_GetWindowDisplayIndex() failed: %s",
SDL_GetError());
// Assume display 0
displayIndex = 0;
}
}
if (fullScreen) {
SDL_DisplayMode currentMode;
if (SDL_GetCurrentDisplayMode(displayIndex, &currentMode) == 0) {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Using current display mode: %dx%dx%d",
currentMode.w, currentMode.h, currentMode.refresh_rate);
width = currentMode.w;
height = currentMode.h;
}
else {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Unable to get current display mode: %s",
SDL_GetError());
width = m_StreamConfig.width;
height = m_StreamConfig.height;
}
// Full-screen modes always start at the origin
x = y = SDL_WINDOWPOS_UNDEFINED_DISPLAY(displayIndex);
}
else {
SDL_Rect usableBounds;
if (SDL_GetDisplayUsableBounds(displayIndex, &usableBounds) == 0) {
width = usableBounds.w;
height = usableBounds.h;
x = usableBounds.x;
y = usableBounds.y;
if (m_Window != nullptr) {
int top, left, bottom, right;
if (SDL_GetWindowBordersSize(m_Window, &top, &left, &bottom, &right) < 0) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"Unable to get window border size");
return;
}
x += left;
y += top;
width -= left + right;
height -= top + bottom;
}
}
else {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"SDL_GetDisplayUsableBounds() failed: %s",
SDL_GetError());
width = m_StreamConfig.width;
height = m_StreamConfig.height;
x = y = SDL_WINDOWPOS_UNDEFINED_DISPLAY(displayIndex);
}
}
}
void Session::toggleFullscreen()
{
bool fullScreen = !(SDL_GetWindowFlags(m_Window) & SDL_OS_FULLSCREEN_FLAG);
int x, y, width, height;
// If we're leaving full screen, toggle out before setting our size and position
// that way we have accurate display size metrics (not the size our stream changed it to).
if (!fullScreen) {
SDL_SetWindowFullscreen(m_Window, 0);
}
getWindowDimensions(fullScreen, x, y, width, height);
SDL_SetWindowPosition(m_Window, x, y);
SDL_SetWindowSize(m_Window, width, height);
if (fullScreen) {
SDL_SetWindowFullscreen(m_Window, SDL_OS_FULLSCREEN_FLAG);
}
}
void Session::exec()
{
// Check for validation errors/warnings and emit
@@ -373,64 +550,12 @@ void Session::exec()
return;
}
int flags = SDL_WINDOW_HIDDEN;
int width, height;
if (m_Preferences.fullScreen) {
SDL_DisplayMode desired, closest;
SDL_zero(desired);
desired.w = m_StreamConfig.width;
desired.h = m_StreamConfig.height;
desired.refresh_rate = m_StreamConfig.fps;
if (SDL_GetClosestDisplayMode(0, &desired, &closest)) {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Closest match for %dx%dx%d is %dx%dx%d",
desired.w, desired.h, desired.refresh_rate,
closest.w, closest.h, closest.refresh_rate);
width = closest.w;
height = closest.h;
}
else if (SDL_GetCurrentDisplayMode(0, &closest) == 0) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"Using current display mode: %dx%dx%d",
closest.w, closest.h, closest.refresh_rate);
width = closest.w;
height = closest.h;
}
else {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"Unable to get current or closest display mode");
width = m_StreamConfig.width;
height = m_StreamConfig.height;
}
flags |= SDL_WINDOW_FULLSCREEN;
}
else {
SDL_DisplayMode current;
width = m_StreamConfig.width;
height = m_StreamConfig.height;
if (SDL_GetCurrentDisplayMode(0, &current) == 0) {
if (current.w <= m_StreamConfig.width || current.h <= m_StreamConfig.height) {
// If we match or exceed the dimensions of the display, maximize the window
flags |= SDL_WINDOW_MAXIMIZED;
}
}
}
m_Window = SDL_CreateWindow("Moonlight",
SDL_WINDOWPOS_UNDEFINED,
SDL_WINDOWPOS_UNDEFINED,
width,
height,
flags);
if (!m_Window) {
SDL_assert(!SDL_WasInit(SDL_INIT_VIDEO));
if (SDL_InitSubSystem(SDL_INIT_VIDEO) != 0) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"SDL_CreateWindow() failed: %s",
"SDL_InitSubSystem(SDL_INIT_VIDEO) failed: %s",
SDL_GetError());
emit displayLaunchError(QString::fromLocal8Bit(SDL_GetError()));
s_ActiveSessionSemaphore.release();
return;
}
@@ -458,6 +583,7 @@ void Session::exec()
// We already displayed an error dialog in the stage failure
// listener.
s_ActiveSessionSemaphore.release();
SDL_QuitSubSystem(SDL_INIT_VIDEO);
return;
}
@@ -465,11 +591,61 @@ void Session::exec()
emit connectionStarted();
QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents);
// Show the streaming window
SDL_ShowWindow(m_Window);
int x, y, width, height;
getWindowDimensions(m_Preferences.fullScreen,
x, y, width, height);
// Capture the mouse
m_Window = SDL_CreateWindow("Moonlight",
x,
y,
width,
height,
m_Preferences.fullScreen ?
SDL_OS_FULLSCREEN_FLAG : 0);
if (!m_Window) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"SDL_CreateWindow() failed: %s",
SDL_GetError());
s_ActiveSessionSemaphore.release();
SDL_QuitSubSystem(SDL_INIT_VIDEO);
return;
}
// For non-full screen windows, call getWindowDimensions()
// again after creating a window to allow it to account
// for window chrome size.
if (!m_Preferences.fullScreen) {
getWindowDimensions(false, x, y, width, height);
SDL_SetWindowPosition(m_Window, x, y);
SDL_SetWindowSize(m_Window, width, height);
}
QSvgRenderer svgIconRenderer(QString(":/res/moonlight.svg"));
QImage svgImage(ICON_SIZE, ICON_SIZE, QImage::Format_RGBA8888);
svgImage.fill(0);
QPainter svgPainter(&svgImage);
svgIconRenderer.render(&svgPainter);
SDL_Surface* iconSurface = SDL_CreateRGBSurfaceWithFormatFrom((void*)svgImage.constBits(),
svgImage.width(),
svgImage.height(),
32,
4 * svgImage.width(),
SDL_PIXELFORMAT_RGBA32);
#ifndef Q_OS_DARWIN
// Other platforms seem to preserve our Qt icon when creating a new window
if (iconSurface != nullptr) {
SDL_SetWindowIcon(m_Window, iconSurface);
}
#endif
#ifndef QT_DEBUG
// Capture the mouse by default on release builds only.
// This prevents the mouse from becoming trapped inside
// Moonlight when it's halted at a debug break.
SDL_SetRelativeMouseMode(SDL_TRUE);
#endif
// Disable the screen saver
SDL_DisableScreenSaver();
@@ -510,6 +686,41 @@ void Session::exec()
m_VideoDecoder->renderFrame(&event.user);
break;
}
case SDL_WINDOWEVENT:
// We want to recreate the decoder for resizes (full-screen toggles) and the initial shown event.
// We use SDL_WINDOWEVENT_SIZE_CHANGED rather than SDL_WINDOWEVENT_RESIZED because the latter doesn't
// seem to fire when switching from windowed to full-screen on X11.
if (event.window.event != SDL_WINDOWEVENT_SIZE_CHANGED && event.window.event != SDL_WINDOWEVENT_SHOWN) {
break;
}
// Fall through
case SDL_RENDER_DEVICE_RESET:
case SDL_RENDER_TARGETS_RESET:
SDL_AtomicLock(&m_DecoderLock);
// Destroy the old decoder
delete m_VideoDecoder;
// Chose a new decoder (hopefully the same one, but possibly
// not if a GPU was removed or something).
if (!chooseDecoder(m_Preferences.videoDecoderSelection,
m_Window, m_ActiveVideoFormat, m_ActiveVideoWidth,
m_ActiveVideoHeight, m_ActiveVideoFrameRate,
s_ActiveSession->m_VideoDecoder)) {
SDL_AtomicUnlock(&m_DecoderLock);
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Failed to recreate decoder after reset");
goto DispatchDeferredCleanup;
}
// Request an IDR frame to complete the reset
m_NeedsIdr = true;
SDL_AtomicUnlock(&m_DecoderLock);
break;
case SDL_KEYUP:
case SDL_KEYDOWN:
inputHandler.handleKeyEvent(&event.key);
@@ -547,9 +758,6 @@ DispatchDeferredCleanup:
// so we can return to the Qt GUI ASAP.
SDL_SetRelativeMouseMode(SDL_FALSE);
SDL_EnableScreenSaver();
if (m_Window != nullptr) {
SDL_HideWindow(m_Window);
}
// Destroy the decoder, since this must be done on the main thread
SDL_AtomicLock(&m_DecoderLock);
@@ -557,6 +765,13 @@ DispatchDeferredCleanup:
m_VideoDecoder = nullptr;
SDL_AtomicUnlock(&m_DecoderLock);
SDL_DestroyWindow(m_Window);
if (iconSurface != nullptr) {
SDL_FreeSurface(iconSurface);
}
SDL_QuitSubSystem(SDL_INIT_VIDEO);
SDL_assert(!SDL_WasInit(SDL_INIT_VIDEO));
// Cleanup can take a while, so dispatch it to a worker thread.
// When it is complete, it will release our s_ActiveSessionSemaphore
// reference.
+15 -3
View File
@@ -21,6 +21,10 @@ public:
Q_INVOKABLE void exec();
static
bool isHardwareDecodeAvailable(StreamingPreferences::VideoDecoderSelection vds,
int videoFormat, int width, int height, int frameRate);
signals:
void stageStarting(QString stage);
@@ -39,9 +43,11 @@ private:
int sdlDetermineAudioConfiguration();
static
bool isHardwareDecodeAvailable(StreamingPreferences::VideoDecoderSelection vds,
int videoFormat, int width, int height, int frameRate);
void getWindowDimensions(bool fullScreen,
int& x, int& y,
int& width, int& height);
void toggleFullscreen();
static
bool chooseDecoder(StreamingPreferences::VideoDecoderSelection vds,
@@ -94,6 +100,12 @@ private:
SDL_Window* m_Window;
IVideoDecoder* m_VideoDecoder;
SDL_SpinLock m_DecoderLock;
bool m_NeedsIdr;
int m_ActiveVideoFormat;
int m_ActiveVideoWidth;
int m_ActiveVideoHeight;
int m_ActiveVideoFrameRate;
static SDL_AudioDeviceID s_AudioDevice;
static OpusMSDecoder* s_OpusDecoder;
+168 -47
View File
@@ -1,4 +1,4 @@
#include <Initguid.h>
#include <initguid.h>
#include "dxva2.h"
#include <Limelight.h>
@@ -8,16 +8,16 @@ DEFINE_GUID(DXVADDI_Intel_ModeH264_E, 0x604F8E68,0x4951,0x4C54,0x88,0xFE,0xAB,0x
#define SAFE_COM_RELEASE(x) if (x) { (x)->Release(); }
DXVA2Renderer::DXVA2Renderer() :
m_FrameIndex(0),
m_SurfacesUsed(0),
m_SdlRenderer(nullptr),
m_DecService(nullptr),
m_Decoder(nullptr),
m_SurfacesUsed(0),
m_Pool(nullptr),
m_Device(nullptr),
m_RenderTarget(nullptr),
m_ProcService(nullptr),
m_Processor(nullptr)
m_Processor(nullptr),
m_FrameIndex(0)
{
RtlZeroMemory(m_DecSurfaces, sizeof(m_DecSurfaces));
RtlZeroMemory(&m_DXVAContext, sizeof(m_DXVAContext));
@@ -87,6 +87,9 @@ bool DXVA2Renderer::prepareDecoderContext(AVCodecContext* context)
return false;
}
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Using DXVA2 accelerated renderer");
return true;
}
@@ -101,8 +104,8 @@ int DXVA2Renderer::ffGetBuffer2(AVCodecContext* context, AVFrame* frame, int)
frame->data[3] = frame->buf[0]->data;
frame->format = AV_PIX_FMT_DXVA2_VLD;
frame->width = me->m_Width;
frame->height = me->m_Height;
frame->width = me->m_VideoWidth;
frame->height = me->m_VideoHeight;
return 0;
}
@@ -218,8 +221,8 @@ bool DXVA2Renderer::initializeDecoder()
alignment = 16;
}
hr = m_DecService->CreateSurface(FFALIGN(m_Width, alignment),
FFALIGN(m_Height, alignment),
hr = m_DecService->CreateSurface(FFALIGN(m_VideoWidth, alignment),
FFALIGN(m_VideoHeight, alignment),
ARRAYSIZE(m_DecSurfaces) - 1,
m_Desc.Format,
D3DPOOL_DEFAULT,
@@ -262,6 +265,9 @@ bool DXVA2Renderer::initializeRenderer()
D3DSURFACE_DESC renderTargetDesc;
m_RenderTarget->GetDesc(&renderTargetDesc);
m_DisplayWidth = renderTargetDesc.Width;
m_DisplayHeight = renderTargetDesc.Height;
hr = DXVA2CreateVideoService(m_Device, IID_IDirectXVideoProcessorService,
reinterpret_cast<void**>(&m_ProcService));
@@ -302,6 +308,14 @@ bool DXVA2Renderer::initializeRenderer()
caps.VideoProcessorOperations);
continue;
}
else if (!(caps.VideoProcessorOperations & DXVA2_VideoProcess_StretchX) ||
!(caps.VideoProcessorOperations & DXVA2_VideoProcess_StretchY)) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"Device %d can't stretch video: %x",
i,
caps.VideoProcessorOperations);
continue;
}
m_ProcService->GetProcAmpRange(guids[i], &m_Desc, renderTargetDesc.Format, DXVA2_ProcAmp_Brightness, &m_BrightnessRange);
m_ProcService->GetProcAmpRange(guids[i], &m_Desc, renderTargetDesc.Format, DXVA2_ProcAmp_Contrast, &m_ContrastRange);
@@ -341,21 +355,17 @@ bool DXVA2Renderer::initializeRenderer()
bool DXVA2Renderer::initialize(SDL_Window* window, int videoFormat, int width, int height)
{
m_VideoFormat = videoFormat;
m_Width = width;
m_Height = height;
m_VideoWidth = width;
m_VideoHeight = height;
// FFmpeg will be decoding on different threads than the main thread that we're
// currently running on right now. We must set this hint so SDL will pass
// D3DCREATE_MULTITHREADED to IDirect3D9::CreateDevice().
SDL_SetHint(SDL_HINT_RENDER_DIRECT3D_THREADSAFE, "1");
// We want VSYNC when running in full-screen. DWM will give us
// tear-free video when running composited (windowed) without any
// extra latency waiting for VSYNC.
m_SdlRenderer = SDL_CreateRenderer(window, -1,
SDL_RENDERER_ACCELERATED |
((SDL_GetWindowFlags(window) & SDL_WINDOW_FULLSCREEN) ?
SDL_RENDERER_PRESENTVSYNC : 0));
// We require full-screen desktop mode to avoid having to enable V-sync
// to synchronize frame delivery (which has a much higher latency penalty).
m_SdlRenderer = SDL_CreateRenderer(window, -1, SDL_RENDERER_ACCELERATED);
if (!m_SdlRenderer) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"SDL_CreateRenderer() failed: %s",
@@ -365,15 +375,20 @@ bool DXVA2Renderer::initialize(SDL_Window* window, int videoFormat, int width, i
m_Device = SDL_RenderGetD3D9Device(m_SdlRenderer);
// Draw a black frame until the video stream starts rendering
SDL_SetRenderDrawColor(m_SdlRenderer, 0, 0, 0, SDL_ALPHA_OPAQUE);
SDL_RenderClear(m_SdlRenderer);
SDL_RenderPresent(m_SdlRenderer);
RtlZeroMemory(&m_Desc, sizeof(m_Desc));
m_Desc.SampleWidth = m_Width;
m_Desc.SampleHeight = m_Height;
m_Desc.SampleFormat.VideoChromaSubsampling = DXVA2_VideoChromaSubsampling_ProgressiveChroma;
m_Desc.SampleFormat.NominalRange = DXVA2_NominalRange_0_255;
m_Desc.SampleFormat.VideoTransferMatrix = DXVA2_VideoTransferMatrix_BT709;
m_Desc.SampleFormat.VideoLighting = DXVA2_VideoLighting_dim;
m_Desc.SampleFormat.VideoPrimaries = DXVA2_VideoPrimaries_BT709;
m_Desc.SampleFormat.VideoTransferFunction = DXVA2_VideoTransFunc_709;
m_Desc.SampleWidth = m_VideoWidth;
m_Desc.SampleHeight = m_VideoHeight;
m_Desc.SampleFormat.VideoChromaSubsampling = DXVA2_VideoChromaSubsampling_Unknown;
m_Desc.SampleFormat.NominalRange = DXVA2_NominalRange_Unknown;
m_Desc.SampleFormat.VideoTransferMatrix = DXVA2_VideoTransferMatrix_Unknown;
m_Desc.SampleFormat.VideoLighting = DXVA2_VideoLighting_Unknown;
m_Desc.SampleFormat.VideoPrimaries = DXVA2_VideoPrimaries_Unknown;
m_Desc.SampleFormat.VideoTransferFunction = DXVA2_VideoTransFunc_Unknown;
m_Desc.SampleFormat.SampleFormat = DXVA2_SampleProgressiveFrame;
m_Desc.Format = (D3DFORMAT)MAKEFOURCC('N','V','1','2');
@@ -393,23 +408,95 @@ void DXVA2Renderer::renderFrame(AVFrame* frame)
IDirect3DSurface9* surface = reinterpret_cast<IDirect3DSurface9*>(frame->data[3]);
HRESULT hr;
hr = m_Device->TestCooperativeLevel();
switch (hr) {
case D3D_OK:
switch (frame->color_range) {
case AVCOL_RANGE_JPEG:
m_Desc.SampleFormat.NominalRange = DXVA2_NominalRange_0_255;
break;
case AVCOL_RANGE_MPEG:
m_Desc.SampleFormat.NominalRange = DXVA2_NominalRange_16_235;
break;
case D3DERR_DEVICELOST:
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"D3D device lost!");
return;
case D3DERR_DEVICENOTRESET:
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"D3D device not reset!");
return;
default:
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Unknown D3D error: %x",
hr);
return;
m_Desc.SampleFormat.NominalRange = DXVA2_NominalRange_Unknown;
break;
}
switch (frame->color_primaries) {
case AVCOL_PRI_BT709:
m_Desc.SampleFormat.VideoPrimaries = DXVA2_VideoPrimaries_BT709;
break;
case AVCOL_PRI_BT470M:
m_Desc.SampleFormat.VideoPrimaries = DXVA2_VideoPrimaries_BT470_2_SysM;
break;
case AVCOL_PRI_BT470BG:
m_Desc.SampleFormat.VideoPrimaries = DXVA2_VideoPrimaries_BT470_2_SysBG;
break;
case AVCOL_PRI_SMPTE170M:
m_Desc.SampleFormat.VideoPrimaries = DXVA2_VideoPrimaries_SMPTE170M;
break;
case AVCOL_PRI_SMPTE240M:
m_Desc.SampleFormat.VideoPrimaries = DXVA2_VideoPrimaries_SMPTE240M;
break;
default:
m_Desc.SampleFormat.VideoPrimaries = DXVA2_VideoPrimaries_Unknown;
break;
}
switch (frame->color_trc) {
case AVCOL_TRC_SMPTE170M:
case AVCOL_TRC_BT709:
m_Desc.SampleFormat.VideoTransferFunction = DXVA2_VideoTransFunc_709;
break;
case AVCOL_TRC_LINEAR:
m_Desc.SampleFormat.VideoTransferFunction = DXVA2_VideoTransFunc_10;
break;
case AVCOL_TRC_GAMMA22:
m_Desc.SampleFormat.VideoTransferFunction = DXVA2_VideoTransFunc_22;
break;
case AVCOL_TRC_GAMMA28:
m_Desc.SampleFormat.VideoTransferFunction = DXVA2_VideoTransFunc_28;
break;
case AVCOL_TRC_SMPTE240M:
m_Desc.SampleFormat.VideoTransferFunction = DXVA2_VideoTransFunc_240M;
break;
case AVCOL_TRC_IEC61966_2_1:
m_Desc.SampleFormat.VideoTransferFunction = DXVA2_VideoTransFunc_sRGB;
break;
default:
m_Desc.SampleFormat.VideoTransferFunction = DXVA2_VideoTransFunc_Unknown;
break;
}
switch (frame->colorspace) {
case AVCOL_SPC_BT709:
m_Desc.SampleFormat.VideoTransferMatrix = DXVA2_VideoTransferMatrix_BT709;
break;
case AVCOL_SPC_BT470BG:
case AVCOL_SPC_SMPTE170M:
m_Desc.SampleFormat.VideoTransferMatrix = DXVA2_VideoTransferMatrix_BT601;
break;
case AVCOL_SPC_SMPTE240M:
m_Desc.SampleFormat.VideoTransferMatrix = DXVA2_VideoTransferMatrix_SMPTE240M;
break;
default:
m_Desc.SampleFormat.VideoTransferMatrix = DXVA2_VideoTransferMatrix_Unknown;
break;
}
switch (frame->chroma_location) {
case AVCHROMA_LOC_LEFT:
m_Desc.SampleFormat.VideoChromaSubsampling = DXVA2_VideoChromaSubsampling_Horizontally_Cosited |
DXVA2_VideoChromaSubsampling_Vertically_AlignedChromaPlanes;
break;
case AVCHROMA_LOC_CENTER:
m_Desc.SampleFormat.VideoChromaSubsampling = DXVA2_VideoChromaSubsampling_Vertically_AlignedChromaPlanes;
break;
case AVCHROMA_LOC_TOPLEFT:
m_Desc.SampleFormat.VideoChromaSubsampling = DXVA2_VideoChromaSubsampling_Horizontally_Cosited |
DXVA2_VideoChromaSubsampling_Vertically_Cosited;
break;
default:
m_Desc.SampleFormat.VideoChromaSubsampling = DXVA2_VideoChromaSubsampling_Unknown;
break;
}
DXVA2_VideoSample sample = {};
@@ -418,18 +505,31 @@ void DXVA2Renderer::renderFrame(AVFrame* frame)
sample.SrcSurface = surface;
sample.SrcRect.right = m_Desc.SampleWidth;
sample.SrcRect.bottom = m_Desc.SampleHeight;
sample.DstRect = sample.SrcRect;
sample.SampleFormat = m_Desc.SampleFormat;
sample.PlanarAlpha = DXVA2_Fixed32OpaqueAlpha();
// Center in frame and preserve aspect ratio
double srcAspectRatio = (double)m_Desc.SampleWidth / (double)m_Desc.SampleHeight;
double dstAspectRatio = (double)m_DisplayWidth / (double)m_DisplayHeight;
if (dstAspectRatio < srcAspectRatio) {
// Greater height per width
int drawHeight = (int)(m_DisplayWidth / srcAspectRatio);
sample.DstRect.top = (m_DisplayHeight - drawHeight) / 2;
sample.DstRect.bottom = sample.DstRect.bottom + drawHeight;
sample.DstRect.right = m_DisplayWidth;
}
else {
// Greater width per height
int drawWidth = (int)(m_DisplayHeight * srcAspectRatio);
sample.DstRect.bottom = m_DisplayHeight;
sample.DstRect.left = (m_DisplayWidth - drawWidth) / 2;
sample.DstRect.right = sample.DstRect.left + drawWidth;
}
DXVA2_VideoProcessBltParams bltParams = {};
bltParams.TargetFrame = m_FrameIndex++;
bltParams.TargetRect.right = m_Desc.SampleWidth;
bltParams.TargetRect.bottom = m_Desc.SampleHeight;
bltParams.BackgroundColor.Y = 0x1000;
bltParams.BackgroundColor.Cb = 0x8000;
bltParams.BackgroundColor.Cr = 0x8000;
bltParams.TargetRect = sample.DstRect;
bltParams.BackgroundColor.Alpha = 0xFFFF;
bltParams.DestFormat.SampleFormat = DXVA2_SampleProgressiveFrame;
@@ -441,12 +541,33 @@ void DXVA2Renderer::renderFrame(AVFrame* frame)
bltParams.Alpha = DXVA2_Fixed32OpaqueAlpha();
if (SDL_RenderClear(m_SdlRenderer) != 0) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"SDL_RenderClear() failed: %s",
SDL_GetError());
av_frame_free(&frame);
// We're going to cheat a little bit here. It seems SDL's
// renderer may flake out in scenarios like moving the window
// between monitors, so generate a synthetic reset event for
// the main loop to consume.
SDL_Event event;
event.type = SDL_RENDER_TARGETS_RESET;
SDL_PushEvent(&event);
return;
}
hr = m_Processor->VideoProcessBlt(m_RenderTarget, &bltParams, &sample, 1, nullptr);
av_frame_free(&frame);
if (FAILED(hr)) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"VideoProcessBlt() failed: %x",
hr);
}
m_Device->Present(nullptr, nullptr, nullptr, nullptr);
// We must try to present to trigger SDL's logic to recover the render target,
// even if VideoProcessBlt() fails.
SDL_RenderPresent(m_SdlRenderer);
}
+5 -2
View File
@@ -39,8 +39,11 @@ private:
const enum AVPixelFormat* pixFmts);
int m_VideoFormat;
int m_Width;
int m_Height;
int m_VideoWidth;
int m_VideoHeight;
int m_DisplayWidth;
int m_DisplayHeight;
SDL_Renderer* m_SdlRenderer;
+24 -1
View File
@@ -1,5 +1,7 @@
#include "renderer.h"
#include <Limelight.h>
SdlRenderer::SdlRenderer()
: m_Renderer(nullptr),
m_Texture(nullptr)
@@ -21,14 +23,26 @@ SdlRenderer::~SdlRenderer()
bool SdlRenderer::prepareDecoderContext(AVCodecContext*)
{
/* Nothing to do */
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Using SDL software renderer");
return true;
}
bool SdlRenderer::initialize(SDL_Window* window,
int,
int videoFormat,
int width,
int height)
{
// NOTE: HEVC currently uses only 1 slice regardless of what
// we provide in CAPABILITY_SLICES_PER_FRAME(), so we should
// never use it for software decoding (unless common-c starts
// respecting it for HEVC).
if (videoFormat != VIDEO_FORMAT_H264) {
return false;
}
m_Renderer = SDL_CreateRenderer(window, -1, SDL_RENDERER_ACCELERATED);
if (!m_Renderer) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
@@ -41,6 +55,11 @@ bool SdlRenderer::initialize(SDL_Window* window,
// logical rendering surface size is equal to the stream size
SDL_RenderSetLogicalSize(m_Renderer, width, height);
// Draw a black frame until the video stream starts rendering
SDL_SetRenderDrawColor(m_Renderer, 0, 0, 0, SDL_ALPHA_OPAQUE);
SDL_RenderClear(m_Renderer);
SDL_RenderPresent(m_Renderer);
m_Texture = SDL_CreateTexture(m_Renderer,
SDL_PIXELFORMAT_YV12,
SDL_TEXTUREACCESS_STREAMING,
@@ -65,6 +84,10 @@ void SdlRenderer::renderFrame(AVFrame* frame)
frame->linesize[1],
frame->data[2],
frame->linesize[2]);
// Done with the frame now
av_frame_free(&frame);
SDL_RenderClear(m_Renderer);
SDL_RenderCopy(m_Renderer, m_Texture, nullptr, nullptr);
SDL_RenderPresent(m_Renderer);
@@ -0,0 +1,136 @@
#include "vaapi.h"
#include <dlfcn.h>
#include <SDL_syswm.h>
VAAPIRenderer::VAAPIRenderer()
: m_HwContext(nullptr),
m_X11VaLibHandle(nullptr),
m_vaPutSurface(nullptr)
{
}
VAAPIRenderer::~VAAPIRenderer()
{
if (m_HwContext != nullptr) {
av_buffer_unref(&m_HwContext);
}
if (m_X11VaLibHandle != nullptr) {
dlclose(m_X11VaLibHandle);
}
}
bool
VAAPIRenderer::initialize(SDL_Window* window, int, int width, int height)
{
int err;
SDL_SysWMinfo info;
m_VideoWidth = width;
m_VideoHeight = height;
SDL_GetWindowSize(window, &m_DisplayWidth, &m_DisplayHeight);
SDL_VERSION(&info.version);
if (!SDL_GetWindowWMInfo(window, &info)) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"SDL_GetWindowWMInfo() failed: %s",
SDL_GetError());
return false;
}
SDL_assert(info.subsystem == SDL_SYSWM_X11);
if (info.subsystem == SDL_SYSWM_X11) {
m_XWindow = info.info.x11.window;
m_X11VaLibHandle = dlopen("libva-x11.so", RTLD_LAZY);
if (!m_X11VaLibHandle) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"dlopen(libva.so) failed: %s",
dlerror());
return false;
}
m_vaPutSurface = (vaPutSurface_t)dlsym(m_X11VaLibHandle, "vaPutSurface");
}
else if (info.subsystem == SDL_SYSWM_WAYLAND) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"VAAPI backend does not currently support Wayland");
return false;
}
else {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Unsupported VAAPI rendering subsystem: %d",
info.subsystem);
return false;
}
err = av_hwdevice_ctx_create(&m_HwContext,
AV_HWDEVICE_TYPE_VAAPI,
nullptr, nullptr, 0);
if (err < 0) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Failed to create VAAPI context: %d",
err);
return false;
}
return true;
}
bool
VAAPIRenderer::prepareDecoderContext(AVCodecContext* context)
{
context->hw_device_ctx = av_buffer_ref(m_HwContext);
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Using VAAPI accelerated renderer");
return true;
}
void
VAAPIRenderer::renderFrame(AVFrame* frame)
{
VASurfaceID surface = (VASurfaceID)(uintptr_t)frame->data[3];
AVHWDeviceContext* deviceContext = (AVHWDeviceContext*)m_HwContext->data;
AVVAAPIDeviceContext* vaDeviceContext = (AVVAAPIDeviceContext*)deviceContext->hwctx;
// Center in frame and preserve aspect ratio
int x, y, width, height;
double srcAspectRatio = (double)m_VideoWidth / (double)m_VideoHeight;
double dstAspectRatio = (double)m_DisplayWidth / (double)m_DisplayHeight;
if (dstAspectRatio < srcAspectRatio) {
// Greater height per width
int drawHeight = (int)(m_DisplayWidth / srcAspectRatio);
y = (m_DisplayHeight - drawHeight) / 2;
height = drawHeight;
x = 0;
width = m_DisplayWidth;
}
else {
// Greater width per height
int drawWidth = (int)(m_DisplayHeight * srcAspectRatio);
y = 0;
height = m_DisplayHeight;
x = (m_DisplayWidth - drawWidth) / 2;
width = drawWidth;
}
m_vaPutSurface(vaDeviceContext->display,
surface,
m_XWindow,
0, 0,
m_VideoWidth, m_VideoHeight,
x, y,
width, height,
NULL, 0, 0);
av_frame_free(&frame);
}
@@ -0,0 +1,50 @@
#pragma once
#include "renderer.h"
extern "C" {
#include <va/va.h>
#include <va/va_x11.h>
#include <libavutil/hwcontext_vaapi.h>
}
class VAAPIRenderer : public IFFmpegRenderer
{
typedef VAStatus (*vaPutSurface_t)(
VADisplay dpy,
VASurfaceID surface,
Drawable draw, /* X Drawable */
short srcx,
short srcy,
unsigned short srcw,
unsigned short srch,
short destx,
short desty,
unsigned short destw,
unsigned short desth,
VARectangle *cliprects,
unsigned int number_cliprects,
unsigned int flags
);
public:
VAAPIRenderer();
virtual ~VAAPIRenderer();
virtual bool initialize(SDL_Window* window,
int videoFormat,
int width,
int height);
virtual bool prepareDecoderContext(AVCodecContext* context);
virtual void renderFrame(AVFrame* frame);
private:
Window m_XWindow;
AVBufferRef* m_HwContext;
void* m_X11VaLibHandle;
vaPutSurface_t m_vaPutSurface;
int m_VideoWidth;
int m_VideoHeight;
int m_DisplayWidth;
int m_DisplayHeight;
};
+147 -39
View File
@@ -7,9 +7,14 @@
#include <SDL_syswm.h>
#include <Limelight.h>
#include <QQueue>
#import <Cocoa/Cocoa.h>
#import <VideoToolbox/VideoToolbox.h>
#import <AVFoundation/AVFoundation.h>
#import <CoreVideo/CoreVideo.h>
#define FRAME_HISTORY_ENTRIES 8
class VTRenderer : public IFFmpegRenderer
{
@@ -18,9 +23,10 @@ public:
: m_HwContext(nullptr),
m_DisplayLayer(nullptr),
m_FormatDesc(nullptr),
m_View(nullptr)
m_View(nullptr),
m_DisplayLink(nullptr),
m_FrameQueueLock(0)
{
}
virtual ~VTRenderer()
@@ -32,8 +38,132 @@ public:
if (m_FormatDesc != nullptr) {
CFRelease(m_FormatDesc);
}
if (m_DisplayLink != nullptr) {
CVDisplayLinkStop(m_DisplayLink);
CVDisplayLinkRelease(m_DisplayLink);
}
while (!m_FrameQueue.isEmpty()) {
AVFrame* frame = m_FrameQueue.dequeue();
av_frame_free(&frame);
}
if (m_View != nullptr) {
[m_View removeFromSuperview];
}
}
void drawFrame(uint64_t vsyncTime)
{
OSStatus status;
SDL_AtomicLock(&m_FrameQueueLock);
int frameDropTarget;
// If the queue length history entries are large, be strict
// about dropping excess frames.
frameDropTarget = 1;
for (int i = 0; i < m_FrameQueueHistory.count(); i++) {
if (m_FrameQueueHistory[i] <= 1) {
// Be lenient as long as the queue length
// resolves before the end of frame history
frameDropTarget = 3;
}
}
if (m_FrameQueueHistory.count() == FRAME_HISTORY_ENTRIES) {
m_FrameQueueHistory.dequeue();
}
m_FrameQueueHistory.enqueue(m_FrameQueue.count());
// Catch up if we're several frames ahead
while (m_FrameQueue.count() > frameDropTarget) {
AVFrame* frame = m_FrameQueue.dequeue();
av_frame_free(&frame);
}
if (m_FrameQueue.isEmpty()) {
SDL_AtomicUnlock(&m_FrameQueueLock);
return;
}
// Grab the first frame
AVFrame* frame = m_FrameQueue.dequeue();
SDL_AtomicUnlock(&m_FrameQueueLock);
CVPixelBufferRef pixBuf = reinterpret_cast<CVPixelBufferRef>(frame->data[3]);
// If the format has changed or doesn't exist yet, construct it with the
// pixel buffer data
if (!m_FormatDesc || !CMVideoFormatDescriptionMatchesImageBuffer(m_FormatDesc, pixBuf)) {
if (m_FormatDesc != nullptr) {
CFRelease(m_FormatDesc);
}
status = CMVideoFormatDescriptionCreateForImageBuffer(kCFAllocatorDefault,
pixBuf, &m_FormatDesc);
if (status != noErr) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"CMVideoFormatDescriptionCreateForImageBuffer() failed: %d",
status);
av_frame_free(&frame);
return;
}
}
// Queue this sample for the next v-sync
CMSampleTimingInfo timingInfo = {
.duration = kCMTimeInvalid,
.decodeTimeStamp = kCMTimeInvalid,
.presentationTimeStamp = CMTimeMake(vsyncTime, 1000 * 1000 * 1000)
};
CMSampleBufferRef sampleBuffer;
status = CMSampleBufferCreateReadyWithImageBuffer(kCFAllocatorDefault,
pixBuf,
m_FormatDesc,
&timingInfo,
&sampleBuffer);
if (status != noErr) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"CMSampleBufferCreateReadyWithImageBuffer() failed: %d",
status);
av_frame_free(&frame);
return;
}
[m_DisplayLayer enqueueSampleBuffer:sampleBuffer];
CFRelease(sampleBuffer);
av_frame_free(&frame);
}
static
CVReturn
displayLinkOutputCallback(
CVDisplayLinkRef,
const CVTimeStamp* now,
const CVTimeStamp* /* vsyncTime */,
CVOptionFlags,
CVOptionFlags*,
void *displayLinkContext)
{
VTRenderer* me = reinterpret_cast<VTRenderer*>(displayLinkContext);
// In my testing on macOS 10.13, this callback is invoked about 24 ms
// prior to the specified v-sync time (now - vsyncTime). Since this is
// greater than the standard v-sync interval (16 ms = 60 FPS), we will
// draw using the current host time, rather than the actual v-sync target
// time. Because the CVDisplayLink is in sync with the actual v-sync
// interval, even if many ms prior, we can safely use the current host time
// and get a consistent callback for each v-sync. This reduces video latency
// by at least 1 frame vs. rendering with the actual vsyncTime.
me->drawFrame(now->hostTime);
return kCVReturnSuccess;
}
virtual bool initialize(SDL_Window* window,
int videoFormat,
@@ -113,60 +243,34 @@ public:
return false;
}
CVDisplayLinkCreateWithActiveCGDisplays(&m_DisplayLink);
CVDisplayLinkSetOutputCallback(m_DisplayLink, displayLinkOutputCallback, this);
CVDisplayLinkStart(m_DisplayLink);
return true;
}
virtual bool prepareDecoderContext(AVCodecContext* context) override
{
context->hw_device_ctx = av_buffer_ref(m_HwContext);
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Using VideoToolbox accelerated renderer");
return true;
}
virtual void renderFrame(AVFrame* frame) override
{
CVPixelBufferRef pixBuf = reinterpret_cast<CVPixelBufferRef>(frame->data[3]);
OSStatus status;
if (m_DisplayLayer.status == AVQueuedSampleBufferRenderingStatusFailed) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"Resetting failed AVSampleBufferDisplay layer");
setupDisplayLayer();
}
// If the format has changed or doesn't exist yet, construct it with the
// pixel buffer data
if (!m_FormatDesc || !CMVideoFormatDescriptionMatchesImageBuffer(m_FormatDesc, pixBuf)) {
status = CMVideoFormatDescriptionCreateForImageBuffer(kCFAllocatorDefault,
pixBuf, &m_FormatDesc);
if (status != noErr) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"CMVideoFormatDescriptionCreateForImageBuffer() failed: %d",
status);
return;
}
}
CMSampleBufferRef sampleBuffer;
status = CMSampleBufferCreateReadyWithImageBuffer(kCFAllocatorDefault,
pixBuf,
m_FormatDesc,
&kCMTimingInfoInvalid,
&sampleBuffer);
if (status != noErr) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"CMSampleBufferCreateReadyWithImageBuffer() failed: %d",
status);
return;
}
CFArrayRef attachments = CMSampleBufferGetSampleAttachmentsArray(sampleBuffer, true);
CFMutableDictionaryRef dict = (CFMutableDictionaryRef)CFArrayGetValueAtIndex(attachments, 0);
CFDictionarySetValue(dict, kCMSampleAttachmentKey_DisplayImmediately, kCFBooleanTrue);
[m_DisplayLayer enqueueSampleBuffer:sampleBuffer];
CFRelease(sampleBuffer);
SDL_AtomicLock(&m_FrameQueueLock);
m_FrameQueue.enqueue(frame);
SDL_AtomicUnlock(&m_FrameQueueLock);
}
private:
@@ -192,6 +296,10 @@ private:
AVSampleBufferDisplayLayer* m_DisplayLayer;
CMVideoFormatDescriptionRef m_FormatDesc;
NSView* m_View;
CVDisplayLinkRef m_DisplayLink;
QQueue<AVFrame*> m_FrameQueue;
QQueue<int> m_FrameQueueHistory;
SDL_SpinLock m_FrameQueueLock;
};
IFFmpegRenderer* VTRendererFactory::createRenderer() {
+41 -18
View File
@@ -1,14 +1,18 @@
#include <Limelight.h>
#include "ffmpeg.h"
#ifdef _WIN32
#ifdef Q_OS_WIN32
#include "ffmpeg-renderers/dxva2.h"
#endif
#ifdef __APPLE__
#ifdef Q_OS_DARWIN
#include "ffmpeg-renderers/vt.h"
#endif
#ifdef HAVE_LIBVA
#include "ffmpeg-renderers/vaapi.h"
#endif
bool FFmpegVideoDecoder::chooseDecoder(
StreamingPreferences::VideoDecoderSelection vds,
SDL_Window* window,
@@ -60,15 +64,20 @@ bool FFmpegVideoDecoder::chooseDecoder(
// Look for acceleration types we support
switch (config->device_type) {
#ifdef _WIN32
#ifdef Q_OS_WIN32
case AV_HWDEVICE_TYPE_DXVA2:
newRenderer = new DXVA2Renderer();
break;
#endif
#ifdef __APPLE__
#ifdef Q_OS_DARWIN
case AV_HWDEVICE_TYPE_VIDEOTOOLBOX:
newRenderer = VTRendererFactory::createRenderer();
break;
#endif
#ifdef HAVE_LIBVA
case AV_HWDEVICE_TYPE_VAAPI:
newRenderer = new VAAPIRenderer();
break;
#endif
default:
continue;
@@ -98,6 +107,7 @@ FFmpegVideoDecoder::FFmpegVideoDecoder()
m_Renderer(nullptr)
{
av_init_packet(&m_Pkt);
SDL_AtomicSet(&m_QueuedFrames, 0);
// Use linear filtering when renderer scaling is required
SDL_SetHint(SDL_HINT_RENDER_SCALE_QUALITY, "1");
@@ -105,14 +115,31 @@ FFmpegVideoDecoder::FFmpegVideoDecoder()
FFmpegVideoDecoder::~FFmpegVideoDecoder()
{
avcodec_close(m_VideoDecoderCtx);
av_free(m_VideoDecoderCtx);
m_VideoDecoderCtx = nullptr;
// Drop any frames still queued to ensure
// they are properly freed.
SDL_Event event;
m_HwDecodeCfg = nullptr;
while (SDL_AtomicGet(&m_QueuedFrames) > 0) {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Waiting for %d frames to return",
SDL_AtomicGet(&m_QueuedFrames));
if (SDL_PeepEvents(&event,
1,
SDL_GETEVENT,
SDL_USEREVENT,
SDL_USEREVENT) == 1) {
dropFrame(&event.user);
}
else {
SDL_Delay(100);
SDL_PumpEvents();
}
}
avcodec_free_context(&m_VideoDecoderCtx);
delete m_Renderer;
m_Renderer = nullptr;
}
bool FFmpegVideoDecoder::initialize(
@@ -217,14 +244,9 @@ int FFmpegVideoDecoder::submitDecodeUnit(PDECODE_UNIT du)
err = avcodec_receive_frame(m_VideoDecoderCtx, frame);
if (err == 0) {
SDL_Event event;
event.type = SDL_USEREVENT;
event.user.code = SDL_CODE_FRAME_READY;
event.user.data1 = frame;
// The main thread will handle freeing this
SDL_PushEvent(&event);
// Queue the frame for rendering from the main thread
SDL_AtomicIncRef(&m_QueuedFrames);
queueFrame(frame);
}
else {
av_frame_free(&frame);
@@ -238,7 +260,7 @@ void FFmpegVideoDecoder::renderFrame(SDL_UserEvent* event)
{
AVFrame* frame = reinterpret_cast<AVFrame*>(event->data1);
m_Renderer->renderFrame(frame);
av_frame_free(&frame);
SDL_AtomicDecRef(&m_QueuedFrames);
}
// Called on main thread
@@ -246,4 +268,5 @@ void FFmpegVideoDecoder::dropFrame(SDL_UserEvent* event)
{
AVFrame* frame = reinterpret_cast<AVFrame*>(event->data1);
av_frame_free(&frame);
SDL_AtomicDecRef(&m_QueuedFrames);
}
+1
View File
@@ -38,4 +38,5 @@ private:
QByteArray m_DecodeBuffer;
const AVCodecHWConfig* m_HwDecodeCfg;
IFFmpegRenderer* m_Renderer;
SDL_atomic_t m_QueuedFrames;
};
+106
View File
@@ -0,0 +1,106 @@
#include "sl.h"
SLVideoDecoder::SLVideoDecoder()
: m_VideoContext(nullptr),
m_VideoStream(nullptr)
{
}
SLVideoDecoder::~SLVideoDecoder()
{
if (m_VideoStream != nullptr) {
SLVideo_FreeStream(m_VideoStream);
}
if (m_VideoContext != nullptr) {
SLVideo_FreeContext(m_VideoContext);
}
}
bool
SLVideoDecoder::isHardwareAccelerated()
{
// SLVideo is always hardware accelerated
return true;
}
bool
SLVideoDecoder::initialize(StreamingPreferences::VideoDecoderSelection vds,
SDL_Window*,
int videoFormat, int, int, int frameRate)
{
// SLVideo only supports hardware decoding
if (vds == StreamingPreferences::VDS_FORCE_SOFTWARE) {
return false;
}
// SLVideo only supports H.264
if (videoFormat != VIDEO_FORMAT_H264) {
return false;
}
m_VideoContext = SLVideo_CreateContext();
if (!m_VideoContext) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"SLVideo_CreateContext() failed");
return false;
}
// Create a low latency H.264 stream
m_VideoStream = SLVideo_CreateStream(m_VideoContext, k_ESLVideoFormatH264, 1);
if (!m_VideoStream) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"SLVideo_CreateStream() failed");
return false;
}
SLVideo_SetStreamTargetFramerate(m_VideoStream, frameRate, 1);
return true;
}
int
SLVideoDecoder::submitDecodeUnit(PDECODE_UNIT du)
{
int err;
err = SLVideo_BeginFrame(m_VideoStream, du->fullLength);
if (err < 0) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"SLVideo_BeginFrame() failed: %d",
err);
// Need an IDR frame to resync
return DR_NEED_IDR;
}
PLENTRY entry = du->bufferList;
while (entry != nullptr) {
err = SLVideo_WriteFrameData(m_VideoStream,
entry->data,
entry->length);
if (err < 0) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"SLVideo_WriteFrameData() failed: %d",
err);
// Need an IDR frame to resync
return DR_NEED_IDR;
}
entry = entry->next;
}
err = SLVideo_SubmitFrame(m_VideoStream);
if (err < 0) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"SLVideo_SubmitFrame() failed: %d",
err);
// Need an IDR frame to resync
return DR_NEED_IDR;
}
return DR_OK;
}
+28
View File
@@ -0,0 +1,28 @@
#pragma once
#include "decoder.h"
#include <SLVideo.h>
class SLVideoDecoder : public IVideoDecoder
{
public:
SLVideoDecoder();
virtual ~SLVideoDecoder();
virtual bool initialize(StreamingPreferences::VideoDecoderSelection vds,
SDL_Window* window,
int videoFormat,
int width,
int height,
int frameRate);
virtual bool isHardwareAccelerated();
virtual int submitDecodeUnit(PDECODE_UNIT du);
// Unused since rendering is done directly from the decode thread
virtual void renderFrame(SDL_UserEvent*) {}
virtual void dropFrame(SDL_UserEvent*) {}
private:
CSLVideoContext* m_VideoContext;
CSLVideoStream* m_VideoStream;
};
+1 -1
View File
@@ -1,5 +1,5 @@
#pragma once
#define THROW_BAD_ALLOC_IF_NULL(x) \
if ((x) == nullptr) throw new std::bad_alloc()
if ((x) == nullptr) throw std::bad_alloc()
+3
View File
@@ -0,0 +1,3 @@
SOURCES = main.cpp
LIBS += -lSLVideo
+7
View File
@@ -0,0 +1,7 @@
#include <SLVideo.h>
int main()
{
SLVideo_CreateContext();
return 0;
}
+1
View File
@@ -0,0 +1 @@
Versions/Current/Headers
+1
View File
@@ -0,0 +1 @@
Versions/Current/Resources
+1
View File
@@ -0,0 +1 @@
Versions/Current/SDL2

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