Compare commits

..
263 Commits
Author SHA1 Message Date
Cameron Gutman 227ff4e13a Version 1.2.1 (hotfix for Windows and macOS) 2019-12-21 13:01:44 -08:00
Cameron Gutman 8e6e70683e Add workaround for QTBUG-80947 2019-12-21 12:58:45 -08:00
Cameron Gutman 626f0aec6a Rebuild SDL 2.0.10 for macOS with https://bugzilla.libsdl.org/show_bug.cgi?id=4888 patch 2019-12-21 12:49:40 -08:00
Cameron Gutman 973d174a30 Version 1.2.0 (take 2) 2019-12-17 18:25:37 -08:00
Cameron Gutman 9f35690b95 Update DMG signing to support disambiguating identity shortnames 2019-12-17 18:10:39 -08:00
Cameron Gutman d961f2bc13 Switch to QElapsedTimer since QTime is deprecated for our usecase in Qt 5.14 2019-12-16 23:16:31 -08:00
Cameron Gutman 10e8277044 Version 1.2.0 2019-12-16 22:51:11 -08:00
Cameron Gutman b4c40d9b18 Update SDL_GameControllerDB submodule 2019-12-16 22:38:58 -08:00
Cameron Gutman e1603f0808 Explicitly use Rec 601 for VDPAU 2019-12-16 18:02:28 -08:00
Cameron Gutman d4c2350d63 Fix DXVA2 rendering with the wrong colorspace on AMD GPUs 2019-12-15 13:29:19 -08:00
Cameron Gutman 5e5470ae12 Specify video transfer matrix for SLVideo 2019-12-15 11:27:47 -08:00
Cameron Gutman 6230a99360 Fix colorspace conversion with SDL as frontend renderer 2019-12-14 17:17:29 -08:00
Cameron Gutman 801adb6af7 Add colorspace support to VAAPI renderer 2019-12-14 16:58:04 -08:00
Cameron Gutman 8b9b96f422 Only enable slicing when CPU decoding 2019-12-14 15:34:48 -08:00
Cameron Gutman 160684f23e Allow decoders to choose a desired colorspace 2019-12-14 15:25:56 -08:00
Cameron Gutman 88930a9de4 Optimize decoder capability checking 2019-12-14 15:20:44 -08:00
Cameron Gutman af6b8c9b88 Update common-c with colorspace configuration support 2019-12-14 14:40:02 -08:00
Cameron Gutman bf52d3189d Switch to Bionic build image on Travis CI 2019-12-14 13:25:57 -08:00
Cameron Gutman 5d872e8b5b Add auto update check to Steam Link 2019-12-13 20:21:35 -08:00
Cameron Gutman 4832d8007b Print joystick haptic capabilities to log 2019-12-13 18:43:35 -08:00
Cameron Gutman 879e084383 Enable Qt 5.14's new fractional DPI scaling 2019-12-12 20:36:23 -08:00
Cameron Gutman 1ae16bb657 Add source archival script 2019-12-09 18:27:51 -08:00
Cameron Gutman 2177bc68db Add git-archive-all.sh script from https://github.com/meitar/git-archive-all.sh 2019-12-09 18:27:51 -08:00
Cameron Gutman c8b887fe32 Use the new SDL_GameControllerRumble() API on SDL 2.0.9 to fix rumble on HIDAPI gamepads 2019-12-07 16:17:01 -08:00
Cameron Gutman 7205242f38 Improve VPN detection on Windows 2019-12-07 16:00:03 -08:00
Cameron Gutman be33b5b387 Update default window mode on macOS after upgrading 2019-12-07 13:27:47 -08:00
Cameron Gutman 66a123f4b9 Store binaries on the symbol server too 2019-12-03 19:14:19 -08:00
Cameron Gutman 76a4ee22b3 Update SDL_GameControllerDB submodule 2019-12-03 19:02:29 -08:00
Cameron Gutman 3a6bf4d20b Disable arbitrary audio duration for libsoundio until further testing is done 2019-12-03 19:00:43 -08:00
Cameron Gutman 09cf105cdc Block RTSS hooks due to user reported crashes 2019-12-03 18:31:18 -08:00
Cameron Gutman ce02e086a3 Add missing return when no interface address matches 2019-12-02 17:27:57 -08:00
Cameron Gutman e8d4313454 Fix build on Qt 5.10 and earlier 2019-12-02 17:18:29 -08:00
Cameron Gutman 49046f88cf Ignore unknown MTUs 2019-12-01 22:10:07 -08:00
Cameron Gutman 5e2f32e3eb Force remote streaming mode if connecting through a VPN 2019-12-01 21:50:36 -08:00
Cameron Gutman b4a1393bf6 Update common-c with PMTUD and low bitrate audio fixes 2019-12-01 19:45:15 -08:00
Cameron Gutman 0ccee9d806 Support arbitrary audio duration in libsoundio renderer 2019-12-01 19:43:22 -08:00
Cameron Gutman 5700272dc4 Improve low bandwidth audio performance for SDL and SLAudio and fix RTSP issues with broken PMTUD 2019-11-30 22:24:28 -06:00
Cameron Gutman 7e5a3925b3 Fix typo in channel mapping 2019-11-17 14:35:37 -08:00
Cameron Gutman 1febe32e73 Fix 5.1 channel mapping on Steam Link 2019-11-17 12:22:57 -08:00
Cameron Gutman 6059c982cf Fix auto-comment line breaks 2019-11-16 11:58:07 -08:00
Cameron Gutman de7b973239 Plumb presentation time from server into AVFrame.pts field 2019-11-11 18:03:47 -08:00
Cameron Gutman 741113a607 Add configuration for auto-comment bot 2019-11-09 15:04:28 -08:00
Cameron Gutman c20daa8867 Add compatibility checks for HEVC Main10 in VT backend 2019-11-05 18:36:43 -08:00
Cameron Gutman 5e57a7d5c0 Prevent SDL renderer from trying to render 10-bit output 2019-11-05 17:29:21 -08:00
Cameron Gutman 7c8d27700e Add 10-bit output support to VDPAU renderer 2019-11-05 17:09:44 -08:00
Cameron Gutman f8621be5ee Add an HEVC Main10 test frame 2019-11-05 17:08:25 -08:00
Cameron Gutman 8ed87e10b4 Change PC icon to be consistent with iOS and Android 2019-11-05 17:06:57 -08:00
Cameron Gutman ca26fa2dd0 Add basic HEVC Main10 support in DXVA2 renderer 2019-11-04 22:57:12 -08:00
Cameron Gutman 1669095fb1 Prevent double-clicks from triggering pairing twice. Fixes #286 2019-11-04 19:30:38 -08:00
Cameron Gutman 13eed48877 Update moonlight-common-c submodule 2019-11-02 21:49:00 -07:00
Cameron Gutman 07494e6eff Fix Linux build 2019-10-12 19:44:59 -07:00
Cameron Gutman 41b1d72825 Update moonlight-common-c submodule 2019-10-12 19:22:19 -07:00
Cameron Gutman 4d2965e74f Always install the VC redistributable 2019-09-30 00:11:37 -07:00
Cameron Gutman 12526bfca4 Version 1.1.1 2019-09-29 22:11:39 -07:00
Cameron Gutman 3e2c344140 Display the Moonlight icon in Discord Rich Presence info 2019-09-29 22:06:12 -07:00
Cameron Gutman 33730dd584 Display the update tooltip on launch if an update is available 2019-09-29 21:32:24 -07:00
Cameron Gutman 1cb03b1882 Add padding to app labels 2019-09-29 21:26:07 -07:00
Cameron Gutman 2f0a6db414 Add hacks for SDL+Mutter interaction bugs 2019-09-29 21:05:31 -07:00
Cameron Gutman a42328d971 Use VSWhere to locate Visual Studio and redistributables 2019-09-29 15:01:53 -07:00
Cameron Gutman 96ff51d598 Use SDL audio for platforms that libsoundio doesn't support 2019-09-22 12:03:53 -07:00
Cameron Gutman 7c3d3af681 Fix control stream connection error on multi-homed hosts 2019-09-14 14:08:38 -07:00
Cameron Gutman 6942bca5a7 Link to download page from download badge 2019-08-25 23:24:34 -07:00
Cameron Gutman 9eac9a190e Add Downloads badge 2019-08-25 23:20:55 -07:00
Cameron Gutman 40e4fbd9a6 Update common-c for parallel STUN requests 2019-08-20 19:05:54 -07:00
Cameron Gutman 7b6996ed80 Add a little help to the build scripts 2019-08-13 18:07:42 -07:00
Cameron Gutman ef2827a9d8 Update common-c for STUN fallback 2019-08-07 23:14:07 -07:00
Cameron Gutman dbf98ce3a6 Require keyframes even for software H.264 decoding 2019-08-07 22:38:31 -07:00
Cameron Gutman ee7cb5123d Add NVDEC to list of supported video acceleration APIs 2019-08-05 18:31:38 -07:00
Cameron Gutman 77e1d6ab8a Version 1.1 2019-08-05 18:28:26 -07:00
Cameron Gutman 1640bed7a5 Improve reliability of mDNS discovery 2019-08-04 18:04:33 -07:00
Cameron Gutman c6fd5cbbce Update common-c to fix high quality surround sound over the Internet 2019-08-04 15:15:51 -07:00
Cameron Gutman 5288041da5 Prevent us from consuming more than 4 samples per write callback to bound latency 2019-08-03 12:47:44 -07:00
Cameron Gutman a848fb1dd1 Add Steam Link to Travis CI 2019-08-02 21:31:07 -07:00
Cameron Gutman 172945434c Build the full DMG on Travis CI 2019-08-02 20:52:23 -07:00
Cameron Gutman a0967e0281 Fix building a dmg with no code signing identities 2019-08-02 20:40:20 -07:00
Cameron Gutman deab717c73 Remove AppVeyor OpenSSL hack 2019-08-02 20:21:10 -07:00
Cameron Gutman 5db5a088ea Fix NVDEC artifacts if the renderer is delayed for a bit 2019-08-01 20:25:58 -07:00
Cameron Gutman 5e106d2f19 Only store the local address from mDNS if it actually worked 2019-07-31 22:16:11 -07:00
Cameron Gutman 940da6bc73 Fix discovery of IPv6-only hosts 2019-07-31 22:07:20 -07:00
Cameron Gutman d110bff253 Update qmdnsengine to improve reliability of mDNS on multi-homed systems 2019-07-31 22:03:27 -07:00
Cameron Gutman 4caa329c36 Update Windows to SDL 2.0.10 with USE_D3D9EX 2019-07-28 12:01:12 -07:00
Cameron Gutman 7c7d279337 Update SDL_GameControllerDB to dca1a629d9e094baa17fff8ba1642cabc6125eed 2019-07-26 16:43:20 -04:00
Cameron Gutman 831e6ee76d Update moonlight-common-c to fix termination error code on GFE 3.19.0.107 2019-07-26 16:43:20 -04:00
Cameron Gutman b01842ad02 Update macOS to SDL 2.0.10 2019-07-26 16:43:20 -04:00
Cameron Gutman 7acf91883a Rework audio renderer selection to fall back and treat ML_AUDIO as a hard selection 2019-07-26 12:50:45 -04:00
Cameron Gutman 897f6fdd3e Improve handling of IPv6-only cases 2019-07-15 01:07:31 -07:00
Cameron Gutman 6f9c3fbc38 Add support for zero configuration IPv6 streaming 2019-07-14 17:56:57 -07:00
Cameron Gutman bcbd1a5b4b Fix subsystem print in VAAPI 2019-07-07 15:41:26 -07:00
Cameron Gutman 6468efd7e4 Add VAAPI DRM support 2019-07-07 15:32:38 -07:00
Cameron Gutman 66863789c7 Update common-c for compatibility with legacy servers 2019-07-05 22:52:42 -07:00
Cameron Gutman b30058fc29 Fix pairing to Gen 3-5 servers 2019-07-05 21:04:04 -07:00
Cameron Gutman a41cd0b070 Generate SHA-256 client certificates instead of SHA-1 2019-07-05 20:57:21 -07:00
Cameron Gutman ea8b905f02 Missed a call to SDL_SetRelativeMouseMode 2019-07-02 22:39:09 -07:00
Cameron Gutman 16b301236b Add a mouse capture fallback if SDL_SetRelativeMouseMode() fails 2019-07-02 22:17:38 -07:00
Cameron Gutman 22162dda83 Fix runtime SDL version report 2019-06-30 18:48:11 -07:00
Cameron Gutman 024af89be3 Add an option to disable the gamepad mouse button toggle 2019-06-29 18:24:59 -07:00
Cameron Gutman e1a697658e Add Discord Rich Presence for macOS 2019-06-29 17:55:21 -07:00
Cameron Gutman 0a5051f959 Add Discord Rich Presence integration for Windows 2019-06-29 17:40:30 -07:00
Cameron Gutman 82b6b60b31 Take advantage of SDL 2.0.10 bugfixes and new APIs. Fixes #133 2019-06-29 16:08:04 -07:00
Cameron Gutman 06b3c3636a Print compile-time and run-time SDL versions 2019-06-29 15:43:45 -07:00
Cameron Gutman e5712a9474 Move dylibs into the Frameworks folder in the app bundle 2019-06-29 15:39:20 -07:00
Cameron Gutman 6ff845c53b Use borderless windowed mode as the default on macOS. Fixes #246 2019-06-29 14:58:27 -07:00
Cameron Gutman 1c225ed04d Use CUDA acceleration if other hwaccels are unavailable 2019-06-26 21:54:29 -07:00
Cameron Gutman 612b738968 Use the preferred sw_format when reading frames back 2019-06-26 21:25:41 -07:00
Cameron Gutman 87356ef594 Add CUDA acceleration support 2019-06-26 21:02:33 -07:00
Cameron Gutman 20a4dfa6d6 Update bug report template 2019-06-25 21:41:02 -07:00
Cameron Gutman 524cc97f51 Use borderless windowed mode when toggling to full-screen from an initially windowed stream. Fixes #248 2019-06-23 13:29:57 -07:00
Cameron Gutman fc849fb51a Switch back to SDL for audio on Linux. Fixes #245 2019-06-23 13:16:00 -07:00
Cameron Gutman c88651939a Re-add latency bounding on the SDL audio renderer 2019-06-23 13:14:55 -07:00
Cameron Gutman 01254f67f0 Allow audio renderers to specify capabilities 2019-06-23 12:49:37 -07:00
Cameron Gutman 212ed33b21 Block Optimus from using the dGPU due to driver bugs. Fixes #240. Fixes #235 2019-06-22 19:49:05 -07:00
Cameron Gutman bd3f8aef21 Remove OpenSSL 1.0.2 for Qt 5.12.4/5.13 2019-06-18 19:54:35 -07:00
Cameron Gutman 3785acea29 Update to OpenSSL 1.1.1c in preparation for Qt 5.12.4/5.13 2019-06-16 15:20:04 -07:00
Cameron Gutman 866e2f1762 Version 1.0.1 2019-05-27 09:31:01 -07:00
Cameron Gutman 0a705d6752 Fix video on GFE 3.19 2019-05-27 02:24:55 -07:00
Cameron Gutman 4f74fd5354 Fix uninitialized variables and a race condition with CVDisplayLinkStart() 2019-05-21 09:09:16 -07:00
Cameron Gutman 3df9a5d01c Version 1.0.0 2019-05-21 00:14:12 -07:00
Cameron Gutman 2682829bd3 Fix Steam Link audio latency cap 2019-05-20 23:57:27 -07:00
Cameron Gutman 30cfa2607d Hide mouse cursor after streaming on Steam Link 2019-05-19 17:43:46 -07:00
Cameron Gutman 4467c2e6ad Update SDL_GameControllerDB 2019-05-19 13:46:05 -07:00
Cameron Gutman 2c0e8a0ddf Send WoL packet to 48002 and 48010 to workaround ISP blocking of 7 and 9 2019-05-19 13:36:05 -07:00
Cameron Gutman 4c17f32a2e Fix extra non-working gamepad appearing to the host 2019-05-19 13:24:06 -07:00
Cameron Gutman 674220087f Finish mouse emulation support with overlay and bugfixes 2019-05-19 13:10:42 -07:00
Cameron Gutman fc8d5d5799 Add gamepad mouse emulation support 2019-05-19 12:17:23 -07:00
Cameron Gutman 4e4f04c174 Focus on first item when a gamepad is connected 2019-05-19 11:08:23 -07:00
Cameron Gutman 65c21f3392 Improve gamepad navigation on settings page 2019-05-19 10:16:54 -07:00
Cameron Gutman 97fb30cdf1 Fix a few bugs in CVDisplayLink integration in VTRenderer 2019-05-19 09:52:59 -07:00
Cameron Gutman c975279589 Only enable exception-based thread naming on debug builds 2019-05-13 17:54:03 -07:00
Cameron Gutman b3ee7a635f Cap queued audio at 40 ms on Steam Link 2019-05-11 19:09:59 -07:00
Cameron Gutman c2b12868bb Move DisplayLinkVsyncSource back into VTRenderer to reduce latency 2019-05-11 18:33:12 -07:00
Cameron Gutman bdbb03e16f Request 20 ms audio frames on Steam Link to reduce CPU overhead 2019-05-04 15:46:11 -07:00
Cameron Gutman 53138d7c16 Name threads for easier debugging 2019-05-03 21:18:58 -07:00
Cameron Gutman 18d1d35104 Use QByteArray::reserve() rather than reallocating each time we must resize 2019-05-02 22:54:18 -07:00
Cameron Gutman 99d9de35de Add NV21 pixel format support to SDL renderer 2019-05-02 22:51:28 -07:00
Cameron Gutman e6a48481a5 Restore the bulk submission optimization for Steam Link 2019-05-01 22:31:52 -07:00
Cameron Gutman 21f2b1224a Decode directly into the audio renderer's buffer to avoid a copy 2019-05-01 21:27:41 -07:00
Cameron Gutman 187f47a353 Update common-c to fix receive time corruption 2019-04-30 23:23:02 -07:00
Cameron Gutman cb75b3d6d2 Version 0.10.1 2019-04-28 19:27:30 -07:00
Cameron Gutman 27708b9ecb Disable HIDAPI for Xbox gamepads on macOS 2019-04-28 18:45:56 -07:00
Cameron Gutman 5443165fce Update macOS to SDL 2.0.9 with https://bugzilla.libsdl.org/show_bug.cgi?id=4614 patch 2019-04-28 18:28:06 -07:00
Cameron Gutman dfb006d264 More Steam Link performance optimizations 2019-04-28 15:57:57 -07:00
Cameron Gutman 4f00f62674 Batch controller axis events for reduced CPU usage on Steam Link 2019-04-28 15:16:04 -07:00
Cameron Gutman cec527ffd5 Update common-c with performance optimizations and audio bandwidth reduction 2019-04-28 13:17:22 -07:00
Cameron Gutman 4f2fe5c988 Reduce polling rate on SL due to CPU performance limitations 2019-04-28 13:04:58 -07:00
Cameron Gutman 2bf552f801 Batch audio frames on Steam Link to save CPU cycles 2019-04-28 12:55:04 -07:00
Cameron Gutman 477fa8fedf Add logging callbacks for SLAudio and SLVideo 2019-04-28 11:01:54 -07:00
Cameron Gutman f1c7c5f02c Don't emit boxArtLoadComplete on box art load failure to prevent an infinite loop 2019-04-27 18:48:06 -07:00
Cameron Gutman 4e936193c7 Request a test frame for MMAL decoder 2019-04-24 21:31:52 -07:00
Cameron Gutman f9895c22b6 Update SDL_GameControllerDB to cbebd79740bc1360978bdf6d9d4d13d189e420a3 2019-04-24 20:27:23 -07:00
Cameron Gutman 6e46537112 Fix highlights for toolbar buttons to be more visible 2019-04-24 20:22:39 -07:00
Cameron Gutman 92b3918b26 Correct the path used by forfiles commands 2019-04-23 23:25:10 -07:00
Cameron Gutman 34c615a5fd Only include pre-compiled QML files in the installer and portable zips 2019-04-23 22:28:28 -07:00
Cameron Gutman fa29260e09 Work around Qt 5.12 dialog crash on disconnect 2019-04-23 19:40:21 -07:00
cgutman1 a05816b495 Pre-generate QML cache for our QtQuick dependences. Fixes #217 2019-04-22 21:04:21 -07:00
Cameron Gutman 9c5d847dae Autodetect a Wayland environment and configure SDL correctly. Fixes #211 2019-04-21 18:31:11 -07:00
Cameron Gutman 082e330f9d Partially revert fa4c0e82bd because we'll need Qt initialized first to detect Wayland 2019-04-21 17:43:38 -07:00
Cameron Gutman 8878c4a136 Use VAAPI decoder for AMD on Wayland 2019-04-21 15:33:21 -07:00
Cameron Gutman 00926bac81 Fix build warning in SDL renderer 2019-04-21 11:09:11 -07:00
Cameron Gutman 67b0907e52 Add globaldefs.pri for setting qmake values in each project 2019-04-21 11:08:33 -07:00
Cameron Gutman dba479774b Add DRM renderer for Rockchip devices 2019-04-21 05:22:37 +00:00
Cameron Gutman 5c8a1e632b Add support for rendering NV12 hwframes with SDL 2019-04-19 22:16:06 -07:00
Cameron Gutman d65e29111f Fix some codec selection bugs 2019-04-19 20:57:21 -07:00
Cameron Gutman ed170b8092 Prevent the computer name from overflowing the PC grid item 2019-04-18 21:03:37 -07:00
Cameron Gutman ac36fef968 Fix incorrect placeholder status after loading box art causing app label to be absent 2019-04-18 20:58:40 -07:00
Cameron Gutman 10f6b73cad Revert "Avoid using ANGLE on debug builds due to assert issues on Qt 5.12.2"
Qt 5.12.3 fixes the bug. https://bugreports.qt.io/browse/QTBUG-74467

This reverts commit 26bdc3e1d5.
2019-04-18 20:57:39 -07:00
Cameron Gutman 13d933fc07 Until Qt 5.13, VS 2019 is not supported 2019-04-18 20:08:27 -07:00
Cameron Gutman 3ba98e86f2 Remove QC1 style variable since QC1 dialogs are no longer used 2019-04-18 20:04:24 -07:00
Cameron Gutman 1dfca525cf Refactor renderer initialization to reduce duplication and inconsistency 2019-04-18 20:02:14 -07:00
Cameron Gutman e51ad1a68a Fix use-after-free due to stale m_FrontendRenderer pointer. Fixes #214 2019-04-18 19:26:13 -07:00
Cameron Gutman 9c76700f74 Add MMAL renderer for Raspberry Pi 2019-04-16 01:20:21 -07:00
Cameron Gutman 773b1af6f1 Minor refactoring in preparation for non-hwaccel decoder support 2019-04-15 21:56:01 -07:00
Cameron Gutman b8b633a6cc Use SDL to render on Wayland for VAAPI 2019-04-12 22:54:21 -07:00
Cameron Gutman ada2270bd1 Reduce code duplication in renderers 2019-04-12 22:12:53 -07:00
Cameron Gutman 25e5175c54 Refactor parameter passing to decoders and split backend/decode-only and frontend renderers 2019-04-11 22:27:20 -07:00
Cameron Gutman 9dcd770df2 Fix alloc-dealloc mismatch 2019-04-09 21:51:39 -07:00
Cameron Gutman 00a1948b73 Treat AV_CODEC_CAP_HARDWARE codecs as hardware accelerated too 2019-04-09 21:50:22 -07:00
Cameron Gutman 859a5a5e0c Allow a renderer to opt-out of the render thread and use that for SDL on OGL 2019-04-09 21:46:14 -07:00
Cameron Gutman 6783cf57da TTF_OpenFontRW retains the SDL_RWops on success, so we must keep the font data around to back it 2019-04-09 21:26:47 -07:00
Cameron Gutman a8dc5ac14a Add App Notarization and Hardened Runtime for macOS builds 2019-04-09 18:30:25 -07:00
Cameron Gutman 6182553d79 Version 0.10.0 2019-04-07 13:21:02 -07:00
Cameron Gutman 905a0cdac1 Make GFE-reported errors more clear 2019-04-07 12:21:05 -07:00
Cameron Gutman f20ddcf26d Fix long delay in LiStopConnection() that can cause a hang on quit 2019-04-06 15:28:48 -07:00
Cameron Gutman fe26098c2b Fix quitting via Command-Q on macOS 2019-04-06 15:22:14 -07:00
Cameron Gutman 2354a0a640 Fix unresponsive GUI on quit if a polling operation is in progress 2019-04-06 13:11:36 -07:00
Cameron Gutman 7c3f09c70c Update poor connection behavior 2019-04-06 12:25:35 -07:00
Cameron Gutman caf5350fe6 Update SDL_GameControllerDB to e411d95fcda34c19270c2d12335fc47512a87dec 2019-04-06 12:06:52 -07:00
Cameron Gutman 84084835ce Refactor and fix the GridView centering code to avoid flipping between states 2019-04-06 11:48:58 -07:00
Cameron Gutman 7f38a67ede Fix brown-paper-bag bug in 948910bfa2 2019-04-03 21:13:12 -07:00
Cameron Gutman 54383b84ec Fix random deadlocks on window resize with the software decoder due to processing window messages off the render thread 2019-04-03 20:44:50 -07:00
Cameron Gutman 948910bfa2 Don't open the app context menu for mouse clicks 2019-04-03 20:27:00 -07:00
Cameron Gutman ce5dac07d3 Ensure the anchor hack stays in sync with changing margins prior to Qt 5.12 2019-04-01 22:12:49 -07:00
Cameron Gutman d4b0057423 Minor tweak to the label layout of running apps 2019-04-01 20:55:11 -07:00
Cameron Gutman de8b12f594 Fix grid view margins and scroll bar positioning on Qt 5.9 2019-04-01 20:40:52 -07:00
Cameron Gutman 69e31a0a37 Redesign the app view UI to feature larger game artwork and less text 2019-04-01 19:49:33 -07:00
Cameron Gutman c383cd39f4 Fix text wrapping with giant error messages 2019-04-01 18:31:13 -07:00
Cameron Gutman e88d2f0ab2 Add files and scripts to build for Steam Link 2019-03-31 23:26:17 -07:00
Cameron Gutman 32535578a0 Use newlines for the help text separator on the unmapped gamepad dialog 2019-03-31 20:10:56 -07:00
Cameron Gutman 2e9af8fef0 Fix focus and rejection of Yes/No dialogs on SL 2019-03-31 19:40:30 -07:00
Cameron Gutman 0aa87aacc6 Fix focus issues after dismissal of consecutive dialogs (like Add PC -> Add PC error) 2019-03-31 19:32:17 -07:00
Cameron Gutman bc38c302ed Cap the maximum width of the error dialogs 2019-03-31 19:05:38 -07:00
Cameron Gutman c724902508 Clean up code for new QC2 dialogs and remove some unused imports 2019-03-31 17:44:19 -07:00
Cameron Gutman 73604020d7 Rewrite dialog code to use Quick Controls 2 2019-03-31 17:24:25 -07:00
Cameron Gutman 060a00b9c7 Consolidate help handling into ErrorMessageDialog 2019-03-31 15:16:48 -07:00
Cameron Gutman 2cd32bf25a Move the cursor into the lower right corner on SL 2019-03-31 13:58:27 -07:00
Cameron Gutman 81ad1a18fa Fix error dialog focus issues after dismissal on Steam Link 2019-03-31 13:57:57 -07:00
Cameron Gutman 1d94903eef Add a hack to ensure the layout anchors work properly on Qt 5.11 and earlier 2019-03-30 12:28:18 -07:00
Cameron Gutman 2d7dcb495c Remove CAPABILITY_DIRECT_SUBMIT for the video decoder because it can block for significant time periods when D3D9 is reconfiguring display settings (and holding the D3D global lock)
This can cause false reports of packet loss.
2019-03-29 23:38:23 -07:00
Cameron Gutman bc580c9aaa Ensure SdlGamepadKeyNavigation is only re-enabled after the sessionFinished() callback to avoid conflicts with SdlInputHandler 2019-03-29 23:20:43 -07:00
Cameron Gutman f8e2740532 Add placeholder text when no PCs are present 2019-03-27 18:13:20 -07:00
Cameron Gutman a079ecd20b Improve transitions and eliminate conflicting anchor warnings 2019-03-27 01:28:46 -07:00
Cameron Gutman 89c8725766 Improve Add PC dialog readability on Windows 2019-03-27 00:39:25 -07:00
Cameron Gutman da7d532564 Move Add PC button to toolbar 2019-03-26 21:31:51 -07:00
Cameron Gutman 4752d4966d Improve Add PC dialog to have a proper text field 2019-03-26 21:12:53 -07:00
Cameron Gutman 50f47f1ed5 Fix handling of screen saver after fa4c0e82bd and reset background events hint for UI after streaming 2019-03-26 21:11:24 -07:00
Cameron Gutman 26bdc3e1d5 Avoid using ANGLE on debug builds due to assert issues on Qt 5.12.2 2019-03-24 19:23:02 -07:00
Cameron Gutman 764360c0f3 Avoid doing non-trivial work in onCheckedChanged on settings page load 2019-03-24 19:15:35 -07:00
Cameron Gutman 08482577eb Fix connection termination dialog regression from 03c0be0720 2019-03-24 15:19:38 -07:00
Cameron Gutman cabbba2448 Move status update overlay to bottom left for SDL 2019-03-23 21:14:21 -07:00
Cameron Gutman c48ed7bb0d Fix undefined variable usage after 2703efedef 2019-03-23 20:25:19 -07:00
Cameron Gutman fa4c0e82bd Only initialize the video subsystem once per launch 2019-03-23 17:46:42 -07:00
Cameron Gutman c0bf8b9c25 Remove redundant initializtion of SDL video 2019-03-23 14:26:08 -07:00
Cameron Gutman d2cff86c97 Refactor SdlGamepadKeyNavigation to avoid multiple reinitializations of SDL 2019-03-23 14:15:55 -07:00
Cameron Gutman 03c0be0720 Only display help options on platforms with web browsers 2019-03-23 13:51:34 -07:00
Cameron Gutman a3361363e4 Remove duplicate data files on Linux 2019-03-23 12:20:01 -07:00
Cameron Gutman 40e142c5c7 Add force feedback to feature list 2019-03-23 12:14:43 -07:00
Cameron Gutman 2703efedef Improve performance of looking up static system properties 2019-03-23 12:05:08 -07:00
Cameron Gutman c313f1a20b Embed our data files inside the binary with QRC 2019-03-23 10:45:44 -07:00
Cameron Gutman b7116657d9 Add SLAudio renderer for Steam Link 2019-03-22 23:08:10 -07:00
Cameron Gutman 2bf3f1b94b Refactor Steam Link support files 2019-03-22 22:51:08 -07:00
Cameron Gutman b78a7971d9 Fix the launch warning toasts on Qt 5.9 2019-03-22 21:30:06 -07:00
Cameron Gutman 5079bc4976 Fix SDL video initialization on Steam Link 2019-03-22 21:11:17 -07:00
Cameron Gutman 555032c15c Add fallback font hack for Steam Link 2019-03-22 20:53:02 -07:00
Cameron Gutman bcff327565 Use QRC URLs for the initialView value to fix loading from the QRC blob 2019-03-22 19:21:04 -07:00
Cameron Gutman e95ff989b1 Allow DXVA2 renderer blacklists to be bypassed 2019-03-21 20:44:30 -07:00
Cameron Gutman 018e8999e2 Add connection status overlay for the VT renderer 2019-03-18 20:56:53 -07:00
Cameron Gutman f624a1817e Add preference and tweak connection warning 2019-03-18 20:21:52 -07:00
Cameron Gutman 202a643e5a Fix size of the status update overlay on DXVA2 2019-03-17 15:12:25 -07:00
Cameron Gutman c75b9c9221 Add connection status overlay for DXVA2 and SDL 2019-03-17 15:08:21 -07:00
Cameron Gutman 9a6f5ba1a8 Improve speed of process termination on exit 2019-03-17 12:23:13 -07:00
Cameron Gutman 052194714a Adjust thread priorities based on recent refactoring 2019-03-17 00:45:31 -07:00
Cameron Gutman 5ccb4a8b7f Improve busy PC error text 2019-03-17 00:33:52 -07:00
Cameron Gutman 7c3cec0451 Add EZFRD32.dll and EZFRD64.dll to blacklist based on crash reports 2019-03-17 00:23:32 -07:00
Cameron Gutman b79d3b31b3 Disable minimize on focus loss by default 2019-03-17 00:20:47 -07:00
Cameron Gutman bf5d43d0cf Use SDL_HINT_OVERRIDE for critical hints 2019-03-17 00:18:15 -07:00
Cameron Gutman f2ac570949 Rebuild SDL2 (5dc13016cf) with USE_D3D9EX 2019-03-16 23:52:40 -07:00
Cameron Gutman 37636ef1eb Allow other SSL errors as long as the cert is correct 2019-03-16 19:01:55 -07:00
Cameron Gutman c540bec824 Remove logging hack now that Qt 5.12.2 fixes the bug 2019-03-16 16:59:49 -07:00
Cameron Gutman 9288cfa684 Basic support for building as UWP app 2019-03-13 01:02:04 -07:00
Cameron Gutman b1de8b703b Update bug report template for Snap package 2019-03-06 21:32:51 -08:00
Cameron Gutman ccd0777ce9 Use a Loader to be able to use StackView.onActivated to trigger stream start 2019-03-02 20:55:50 -08:00
Cameron Gutman 49b67b3e18 Adjust QGnomePlatform hack to avoid disabling desktop settings awareness. Fixes #185 2019-03-02 19:50:05 -08:00
Cameron Gutman bc97168dc7 Update moonlight-common-c 2019-03-02 14:14:54 -08:00
Cameron Gutman 0ec04df334 Hide hint text when the connection completes 2019-03-02 14:13:32 -08:00
Cameron Gutman 26d7946b4a Fix toolbar hiding in quit segue 2019-03-02 14:13:14 -08:00
Cameron Gutman 292cc579cf Partially revert 8037fb76fc to fix mouse polling issues on Windows 2019-03-02 14:11:30 -08:00
Cameron Gutman 8492482e55 Add a script to create a zip of Qt PDBs 2019-02-26 19:10:17 -08:00
Cameron Gutman ac8183feee Fix selection loss when going back caused by previous commit 2019-02-22 22:44:09 -08:00
Cameron Gutman 8037fb76fc Use StackView activation callbacks to perform init and cleanup since visibility callbacks aren't guaranteed to run in any order 2019-02-22 22:14:06 -08:00
Cameron Gutman 754a14941b Update Windows build to OpenSSL 1.1.0k to fix App Verifier TLS warning 2019-02-22 21:14:40 -08:00
Konstantin Budnikov e32bc1a0a3 SDL_HapticRumblePlay rumble for gamepad without SDL_HAPTIC_LEFTRIGHT support (#181) 2019-02-20 19:22:38 -08:00
Cameron Gutman e5a8b49c93 Limit the number of queued frames in Pacer to prevent excessive memory consumption if the renderer gets blocked for a while 2019-02-18 12:13:45 -08:00
Cameron Gutman b6872456cd Version 0.9.1 2019-02-17 13:13:57 -08:00
Cameron Gutman 2a12b018da Fix special key combos on non-QWERTY latin keyboards 2019-02-17 13:07:54 -08:00
365 changed files with 14994 additions and 7132 deletions
+11 -5
View File
@@ -1,8 +1,10 @@
---
name: Bug report
about: Create a report to help us address broken functionality
about: Create a report to help us address issues in Moonlight
---
**READ ME FIRST!**
This issue tracker is not the right place to ask for troubleshooting help if you're not able to connect or stream at all. For those requests, please use the #pc-help channel on our Discord server where there are many more people who can help (or direct you back here if it looks like a Moonlight bug after all). https://discord.gg/MySTSdq
**Describe the bug**
A clear and concise description of what the bug is.
@@ -16,24 +18,28 @@ If applicable, add screenshots to help explain your problem. If the issue is rel
**Affected games**
List the games you've tried that exhibit the issue. To see if the issue is game-specific, try streaming Steam Big Picture with Moonlight and see if the issue persists there.
**Other Moonlight clients**
- Does the issue occur when using Moonlight on iOS or Android?
**Moonlight settings (please complete the following information)**
- Have any settings been adjusted from defaults?
- If so, which settings have been changed?
- Does the problem still occur after reverting settings back to default?
**Client PC details (please complete the following information)**
- OS: [e.g. Windows 10 1803]
- Moonlight Version: [e.g. v0.2.0]
- OS: [e.g. Windows 10 1809]
- Moonlight Version: [e.g. v0.9.0]
**Server PC details (please complete the following information)**
- OS: [e.g. Windows 10 1803]
- OS: [e.g. Windows 10 1809]
- GeForce Experience version: [e.g. 3.16.0.140]
- Nvidia GPU driver: [e.g. 417.35]
**Moonlight Logs (please attach)**
- On Windows, `Moonlight-###.log` files can be found in `%TEMP%`. Simply type that into the File Explorer path field to navigate there.
- On macOS, `Moonlight-###.log` files can be found in `/tmp`. In Finder, press Cmd+Shift+G, then type `/tmp` to navigate there.
- On Linux, logs print to the terminal when running via `flatpak run com.moonlight_stream.Moonlight`
- On Linux with the Flatpak, logs print to the terminal when running with the command: `flatpak run com.moonlight_stream.Moonlight`
- On Linux with the Snap, logs print to the terminal when running with the command: `moonlight`
**Additional context**
Anything else you think may be relevant to the issue
+4
View File
@@ -0,0 +1,4 @@
issuesOpened: >
If this is a question about Moonlight or you need help troubleshooting a streaming problem, please use the help channels on our [Discord server](https://discord.gg/MySTSdq) instead of GitHub issues. There are many more people available on Discord to help you and answer your questions.<br /><br />
This issue tracker should only be used for specific bugs or feature requests.<br /><br />
Thank you, and happy streaming!
+43 -24
View File
@@ -3,51 +3,70 @@ language: cpp
git:
depth: 1
env:
- NJOBS=2
matrix:
include:
- os: osx
osx_image: xcode10.2
compiler: clang
env: TRAVIS_TARGET=osx
addons:
homebrew:
packages:
- qt
- os: linux
sudo: required
dist: xenial
compiler: gcc
env: TRAVIS_TARGET=steamlink
addons:
apt:
packages:
- libc6:i386
- libstdc++6:i386
- os: linux
sudo: required
dist: bionic
compiler: gcc
env: TRAVIS_TARGET=linux
addons:
apt:
sources:
- sourceline: 'ppa:beineri/opt-qt597-xenial'
- sourceline: 'ppa:jonathonf/ffmpeg-4'
- sourceline: 'ppa:savoury1/multimedia'
- sourceline: 'ppa:savoury1/ffmpeg4'
packages:
- qt59base
- qt59quickcontrols2
- qt59svg
- libesd0-dev
- libgl-dev
- qt5-default
- qtbase5-dev
- qtquickcontrols2-5-dev
- libqt5svg5-dev
- libgl1-mesa-dev
- libegl1-mesa-dev
- libxkbcommon-dev
- wayland-protocols
- libopus-dev
- libavcodec-dev
- libavutil-dev
- libva-dev
- libvdpau-dev
- libsdl2-dev
- qtdeclarative5-dev
- libssl-dev
- libsdl2-ttf-dev
- libavcodec-dev
- libavutil-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.9 && wget https://www.libsdl.org/release/SDL2-$SDL2_VER.tar.gz && tar -xvf SDL2-$SDL2_VER.tar.gz && cd SDL2-$SDL2_VER && ./configure && make -j$NJOBS && sudo make install && cd ..; fi
- if [[ "$TRAVIS_OS_NAME" = "linux" ]]; then export SDL2_TTF_VER=2.0.15 && wget https://www.libsdl.org/projects/SDL_ttf/release/SDL2_ttf-$SDL2_TTF_VER.tar.gz && tar -xvf SDL2_ttf-$SDL2_TTF_VER.tar.gz && cd SDL2_ttf-$SDL2_TTF_VER && ./configure && make -j$NJOBS && sudo make install && cd ..; fi
- '[ "$TRAVIS_TARGET" != osx ] || brew link --force qt'
- if [[ "$TRAVIS_TARGET" = "osx" ]]; then nvm use node && npm install --global create-dmg; fi
- if [[ "$TRAVIS_TARGET" = "steamlink" ]]; then git clone --depth=1 https://github.com/ValveSoftware/steamlink-sdk.git $HOME/steamlink-sdk; 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'
- '[ "$TRAVIS_TARGET" = "steamlink" ] || qmake -v'
- '[[ "$TRAVIS_TARGET" != linux || "$CXX" != clang++ ]] || export QMAKESPEC=linux-clang'
- '[[ "$TRAVIS_TARGET" != linux || "$CXX" != g++ ]] || export QMAKESPEC=linux-g++'
- '[ "$TRAVIS_TARGET" != osx ] || export QMAKESPEC=macx-clang'
- '[ "$TRAVIS_TARGET" != "steamlink" ] || export STEAMLINK_SDK_PATH=$HOME/steamlink-sdk'
script:
- qmake moonlight-qt.pro -spec $QMAKESPEC
- make -j$NJOBS debug
- make -j$NJOBS release
- '[ "$TRAVIS_TARGET" != "linux" ] || qmake moonlight-qt.pro -spec $QMAKESPEC'
- '[ "$TRAVIS_TARGET" != "linux" ] || make -j$(nproc) debug'
- '[ "$TRAVIS_TARGET" != "linux" ] || make -j$(nproc) release'
- '[ "$TRAVIS_TARGET" != "osx" ] || scripts/generate-dmg.sh Release'
- '[ "$TRAVIS_TARGET" != "steamlink" ] || scripts/build-steamlink-app.sh'
+1 -5
View File
@@ -3,11 +3,7 @@ QT -= core gui
TARGET = AntiHooking
TEMPLATE = lib
# Support debug and release builds from command line for CI
CONFIG += debug_and_release
# Ensure symbols are always generated
CONFIG += force_debug_info
include(../globaldefs.pri)
INCLUDEPATH += $$PWD/../libs/windows/include
contains(QT_ARCH, i386) {
+40 -1
View File
@@ -134,7 +134,46 @@ private:
// This DLL shipped with ASUS Sonic Radar 3 improperly handles
// D3D9 exclusive fullscreen in a way that causes CreateDeviceEx()
// to deadlock. https://github.com/moonlight-stream/moonlight-qt/issues/102
L"NahimicOSD.dll"
L"NahimicOSD.dll",
// This DLL has been seen in several crash reports. Some Googling
// suggests it's highly unstable and causes issues in many games.
L"EZFRD32.dll",
L"EZFRD64.dll",
// These are the dList DLLs for Optimus hybrid graphics DDI.
// https://docs.microsoft.com/en-us/windows-hardware/drivers/display/hybrid-system-ddi
//
// We forcefully block them from loading because Optimus has a bug that
// deadlocks DXVA2 when we present with D3DPRESENT_DONOTWAIT. This will prevent
// Optimus from ever using the dGPU even if the user has requested it.
// https://github.com/moonlight-stream/moonlight-qt/issues/240
// https://github.com/moonlight-stream/moonlight-qt/issues/235
L"nvdlist.dll",
L"nvdlistx.dll",
// In some unknown circumstances, RTSS tries to hook in the middle of an instruction, leaving garbage
// code inside d3d9.dll that causes a crash when executed:
//
// 0:000> u
// d3d9!D3D9GetCurrentOwnershipMode+0x5d:
// 00007ff8`95b95861 9b wait
// 00007ff8`95b95862 a7 cmps dword ptr [rsi],dword ptr [rdi] <--- crash happens here
// 00007ff8`95b95863 ff ???
// 00007ff8`95b95864 bfe8ca8a00 mov edi,8ACAE8h
// 00007ff8`95b95869 00eb add bl,ch
// 00007ff8`95b9586b f1 ???
// 00007ff8`95b9586c b808000000 mov eax,8
// 00007ff8`95b95871 ebe6 jmp d3d9!D3D9GetCurrentOwnershipMode+0x55 (00007ff8`95b95859)
//
// Disassembling starting at the exact address of the attempted hook yields the intended jmp instruction
//
// 0:000> u d3d9!D3D9GetCurrentOwnershipMode+0x5c:
// 00007ff8`95b95860 e99ba7ffbf jmp 00007ff8`55b90000
//
// Since the RTSS OSD doesn't even work with DXVA2, we'll just block the hooks entirely.
L"RTSSHooks.dll",
L"RTSSHooks64.dll",
};
};
+13 -6
View File
@@ -8,6 +8,7 @@ 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)
[![Downloads](https://img.shields.io/github/downloads/moonlight-stream/moonlight-qt/total)](https://github.com/moonlight-stream/moonlight-qt/releases)
## Features
- Hardware accelerated video decoding on Windows, Mac, and Linux
@@ -16,10 +17,10 @@ You can follow development on our [Discord server](https://discord.gg/6ERtzFY).
- 5.1 surround sound audio
- HEVC support for better image quality at reduced bandwidth
- Keyboard and mouse support
- Gamepad support with SDL gamepad mappings
- Gamepad support with force feedback for up to 4 players
## Downloads
- [Windows and macOS](https://github.com/moonlight-stream/moonlight-qt/releases)
- [Windows, macOS, and Steam Link](https://github.com/moonlight-stream/moonlight-qt/releases)
- [Snap (for Ubuntu-based Linux distros)](https://snapcraft.io/moonlight)
- [Flatpak (for other Linux distros)](https://flathub.org/apps/details/com.moonlight_stream.Moonlight)
@@ -27,33 +28,39 @@ You can follow development on our [Discord server](https://discord.gg/6ERtzFY).
### General Build Requirements
* Qt 5.9 SDK or later
### Windows-specific Requirements
### Windows-specific Build Requirements
* Windows 7 or later
* [Visual Studio](https://visualstudio.microsoft.com/downloads/) 2015 or later (Community edition is fine)
* [Visual Studio](https://visualstudio.microsoft.com/downloads/) 2017 (Community edition is fine)
* Select MSVC Desktop toolchain during Qt installation
* [DirectX SDK](https://www.microsoft.com/en-us/download/details.aspx?id=6812)
* [7-Zip](https://www.7-zip.org/) (only if building installers for non-development PCs)
* [WiX Toolset](http://wixtoolset.org/releases/) v3.11 or later (only if building installers for non-development PCs)
### Mac-specific Requirements
### Mac-specific Build Requirements
* macOS Sierra (10.12) or later
* Xcode 10
* [create-dmg](https://github.com/sindresorhus/create-dmg) (only if building DMGs for use on non-development Macs)
### Linux-specific Requirements
### Linux-specific Build Requirements
* GCC or Clang
* Install your distro equivalents of: `openssl-devel qt5-devel SDL2-devel ffmpeg-devel qt5-qtquickcontrols2-devel libva-devel libvdpau-devel opus-devel pulseaudio-libs-devel alsa-lib-devel SDL2_ttf-devel`
* FFmpeg 4.0 is required to build. If your distro doesn't package FFmpeg 4.0, you can build and install it from source on http://ffmpeg.org/
### Steam Link-specific Build Requirements
* [Steam Link SDK](https://github.com/ValveSoftware/steamlink-sdk) cloned on your build system
* STEAMLINK_SDK_PATH environment variable set to the Steam Link SDK path
### Build Setup Steps
1. Install the latest Qt SDK (and optionally, the Qt Creator IDE) from https://www.qt.io/download
* You can install Qt via Homebrew on macOS, but you will need to use `brew install qt --with-debug` to be able to create debug builds of Moonlight.
* You may also use your Linux distro's package manager for the Qt SDK as long as the packages are Qt 5.9 or later.
* This step is not required for building on Steam Link, because the Steam Link SDK includes Qt 5.9.
2. Run `git submodule update --init --recursive` from within `moonlight-qt/`
3. Open the project in Qt Creator or build from qmake on the command line.
* To build a binary for use on non-development machines, use the scripts in the `scripts` folder.
* For Windows builds, use `scripts\generate-installers.bat`. Execute this script from the root of the repository within a Qt command prompt. Ensure WiX and 7-Zip binary directories are in your `%PATH%`.
* For macOS builds, use `scripts/generate-dmg.sh`. Execute this script from the root of the repository and ensure Qt's `bin` folder is in your `$PATH`.
* For Steam Link builds, run `scripts/build-steamlink-app.sh` from the root of the repository.
* To build from the command line for development use, run `qmake moonlight-qt.pro` then `make debug` or `make release`
## Contribute
+95 -63
View File
@@ -8,11 +8,7 @@ unix:!macx {
TARGET = Moonlight
}
# Support debug and release builds from command line for CI
CONFIG += debug_and_release
# Ensure symbols are always generated
CONFIG += force_debug_info
include(../globaldefs.pri)
# Precompile QML files to avoid writing qmlcache on portable versions.
# Since this binds the app against the Qt runtime version, we will only
@@ -60,15 +56,21 @@ macx {
}
unix:!macx {
CONFIG += link_pkgconfig soundio
CONFIG += link_pkgconfig
PKGCONFIG += openssl sdl2 SDL2_ttf opus
# For libsoundio
packagesExist(libpulse) {
PKGCONFIG += libpulse
# SLAudio is used on Steam Link
!config_SL {
CONFIG += soundio
}
packagesExist(alsa) {
PKGCONFIG += alsa
soundio {
packagesExist(libpulse) {
PKGCONFIG += libpulse
}
packagesExist(alsa) {
PKGCONFIG += alsa
}
}
packagesExist(libavcodec) {
@@ -82,26 +84,42 @@ unix:!macx {
packagesExist(libva-wayland) {
CONFIG += libva-wayland
}
packagesExist(libva-drm) {
CONFIG += libva-drm
}
CONFIG += libva
}
packagesExist(vdpau) {
CONFIG += libvdpau
}
packagesExist(mmal) {
PKGCONFIG += mmal
CONFIG += mmal
}
packagesExist(libdrm) {
PKGCONFIG += libdrm
CONFIG += libdrm
}
}
}
win32 {
LIBS += -llibssl -llibcrypto -lSDL2 -lSDL2_ttf -lavcodec -lavutil -lopus -ld3dx9
CONFIG += ffmpeg soundio
CONFIG += ffmpeg
}
win32:!winrt {
CONFIG += soundio discord-rpc
}
macx {
LIBS += -lssl -lcrypto -lavcodec.58 -lavutil.56 -lopus -framework SDL2 -framework SDL2_ttf
LIBS += -lobjc -framework VideoToolbox -framework AVFoundation -framework CoreVideo -framework CoreGraphics -framework CoreMedia -framework AppKit
LIBS += -lobjc -framework VideoToolbox -framework AVFoundation -framework CoreVideo -framework CoreGraphics -framework CoreMedia -framework AppKit -framework Metal
# For libsoundio
LIBS += -framework CoreAudio -framework AudioUnit
CONFIG += ffmpeg soundio
CONFIG += ffmpeg soundio discord-rpc
}
SOURCES += \
@@ -113,6 +131,7 @@ SOURCES += \
backend/nvpairingmanager.cpp \
backend/computermanager.cpp \
backend/boxartmanager.cpp \
backend/richpresencemanager.cpp \
cli/commandlineparser.cpp \
cli/quitstream.cpp \
cli/startstream.cpp \
@@ -128,7 +147,8 @@ SOURCES += \
path.cpp \
settings/mappingmanager.cpp \
gui/sdlgamepadkeynavigation.cpp \
streaming/video/overlaymanager.cpp
streaming/video/overlaymanager.cpp \
backend/systemproperties.cpp
HEADERS += \
utils.h \
@@ -139,6 +159,7 @@ HEADERS += \
backend/nvpairingmanager.h \
backend/computermanager.h \
backend/boxartmanager.h \
backend/richpresencemanager.h \
cli/commandlineparser.h \
cli/quitstream.h \
cli/startstream.h \
@@ -155,7 +176,8 @@ HEADERS += \
path.h \
settings/mappingmanager.h \
gui/sdlgamepadkeynavigation.h \
streaming/video/overlaymanager.h
streaming/video/overlaymanager.h \
backend/systemproperties.h
# Platform-specific renderers and decoders
ffmpeg {
@@ -165,6 +187,7 @@ ffmpeg {
SOURCES += \
streaming/video/ffmpeg.cpp \
streaming/video/ffmpeg-renderers/sdlvid.cpp \
streaming/video/ffmpeg-renderers/cuda.cpp \
streaming/video/ffmpeg-renderers/pacer/pacer.cpp \
streaming/video/ffmpeg-renderers/pacer/nullthreadedvsyncsource.cpp
@@ -172,6 +195,7 @@ ffmpeg {
streaming/video/ffmpeg.h \
streaming/video/ffmpeg-renderers/renderer.h \
streaming/video/ffmpeg-renderers/sdlvid.h \
streaming/video/ffmpeg-renderers/cuda.h \
streaming/video/ffmpeg-renderers/pacer/pacer.h \
streaming/video/ffmpeg-renderers/pacer/nullthreadedvsyncsource.h
}
@@ -195,6 +219,12 @@ libva-wayland {
PKGCONFIG += libva-wayland
DEFINES += HAVE_LIBVA_WAYLAND
}
libva-wayland {
message(VAAPI DRM support enabled)
PKGCONFIG += libva-drm
DEFINES += HAVE_LIBVA_DRM
}
libvdpau {
message(VDPAU renderer selected)
@@ -202,16 +232,34 @@ libvdpau {
SOURCES += streaming/video/ffmpeg-renderers/vdpau.cpp
HEADERS += streaming/video/ffmpeg-renderers/vdpau.h
}
config_SLVideo {
message(SLVideo decoder/renderer selected)
mmal {
message(MMAL renderer selected)
DEFINES += HAVE_SLVIDEO
LIBS += -lSLVideo
SOURCES += streaming/video/sl.cpp
HEADERS += streaming/video/sl.h
DEFINES += HAVE_MMAL
SOURCES += streaming/video/ffmpeg-renderers/mmal.cpp
HEADERS += streaming/video/ffmpeg-renderers/mmal.h
}
win32 {
libdrm {
message(DRM renderer selected)
DEFINES += HAVE_DRM
SOURCES += streaming/video/ffmpeg-renderers/drm.cpp
HEADERS += streaming/video/ffmpeg-renderers/drm.h
}
config_SL {
message(Steam Link build configuration selected)
DEFINES += STEAM_LINK HAVE_SLVIDEO HAVE_SLAUDIO
LIBS += -lSLVideo -lSLAudio
SOURCES += \
streaming/video/slvid.cpp \
streaming/audio/renderers/slaud.cpp
HEADERS += \
streaming/video/slvid.h \
streaming/audio/renderers/slaud.h
}
win32:!winrt {
message(DXVA2 renderer selected)
SOURCES += \
@@ -226,12 +274,10 @@ macx {
message(VideoToolbox renderer selected)
SOURCES += \
streaming/video/ffmpeg-renderers/vt.mm \
streaming/video/ffmpeg-renderers/pacer/displaylinkvsyncsource.mm
streaming/video/ffmpeg-renderers/vt.mm
HEADERS += \
streaming/video/ffmpeg-renderers/vt.h \
streaming/video/ffmpeg-renderers/pacer/displaylinkvsyncsource.h
streaming/video/ffmpeg-renderers/vt.h
}
soundio {
message(libsoundio audio renderer selected)
@@ -240,6 +286,12 @@ soundio {
SOURCES += streaming/audio/renderers/soundioaudiorenderer.cpp
HEADERS += streaming/audio/renderers/soundioaudiorenderer.h
}
discord-rpc {
message(Discord integration enabled)
LIBS += -ldiscord-rpc
DEFINES += HAVE_DISCORD
}
RESOURCES += \
resources.qrc \
@@ -265,12 +317,14 @@ else:unix: LIBS += -L$$OUT_PWD/../qmdnsengine/ -lqmdnsengine
INCLUDEPATH += $$PWD/../qmdnsengine/qmdnsengine/src/include $$PWD/../qmdnsengine
DEPENDPATH += $$PWD/../qmdnsengine/qmdnsengine/src/include $$PWD/../qmdnsengine
win32:CONFIG(release, debug|release): LIBS += -L$$OUT_PWD/../soundio/release/ -lsoundio
else:win32:CONFIG(debug, debug|release): LIBS += -L$$OUT_PWD/../soundio/debug/ -lsoundio
else:unix: LIBS += -L$$OUT_PWD/../soundio/ -lsoundio
soundio {
win32:CONFIG(release, debug|release): LIBS += -L$$OUT_PWD/../soundio/release/ -lsoundio
else:win32:CONFIG(debug, debug|release): LIBS += -L$$OUT_PWD/../soundio/debug/ -lsoundio
else:unix: LIBS += -L$$OUT_PWD/../soundio/ -lsoundio
INCLUDEPATH += $$PWD/../soundio/libsoundio
DEPENDPATH += $$PWD/../soundio/libsoundio
INCLUDEPATH += $$PWD/../soundio/libsoundio
DEPENDPATH += $$PWD/../soundio/libsoundio
}
win32:CONFIG(release, debug|release): LIBS += -L$$OUT_PWD/../h264bitstream/release/ -lh264bitstream
else:win32:CONFIG(debug, debug|release): LIBS += -L$$OUT_PWD/../h264bitstream/debug/ -lh264bitstream
@@ -279,22 +333,12 @@ else:unix: LIBS += -L$$OUT_PWD/../h264bitstream/ -lh264bitstream
INCLUDEPATH += $$PWD/../h264bitstream/h264bitstream
DEPENDPATH += $$PWD/../h264bitstream/h264bitstream
win32:CONFIG(release, debug|release): LIBS += -L$$OUT_PWD/../AntiHooking/release/ -lAntiHooking
else:win32:CONFIG(debug, debug|release): LIBS += -L$$OUT_PWD/../AntiHooking/debug/ -lAntiHooking
!winrt {
win32:CONFIG(release, debug|release): LIBS += -L$$OUT_PWD/../AntiHooking/release/ -lAntiHooking
else:win32:CONFIG(debug, debug|release): LIBS += -L$$OUT_PWD/../AntiHooking/debug/ -lAntiHooking
INCLUDEPATH += $$PWD/../AntiHooking
DEPENDPATH += $$PWD/../AntiHooking
defineTest(copyToDestDir) {
files = $$1
for(FILE, files) {
!equals(PWD, $$OUT_PWD) {
QMAKE_POST_LINK += $$QMAKE_COPY $$shell_quote($$shell_path($$PWD/$$FILE)) $$shell_quote($$shell_path($$OUT_PWD)) $$escape_expand(\\n\\t)
}
}
export(QMAKE_POST_LINK)
INCLUDEPATH += $$PWD/../AntiHooking
DEPENDPATH += $$PWD/../AntiHooking
}
unix:!macx: {
@@ -319,16 +363,7 @@ unix:!macx: {
appstream.files = deploy/linux/com.moonlight_stream.Moonlight.appdata.xml
appstream.path = $$PREFIX/$$DATADIR/metainfo/
appdata.files = SDL_GameControllerDB/gamecontrollerdb.txt ModeSeven.ttf
appdata.path = "$$PREFIX/$$DATADIR/Moonlight Game Streaming Project/Moonlight/"
CONFIG(debug, debug|release) {
# Copy the required data files into the application directory only for debug builds.
# Release builds will use the INSTALLS variable to place these in the correct folders.
copyToDestDir(SDL_GameControllerDB/gamecontrollerdb.txt ModeSeven.ttf)
}
INSTALLS += target appdata desktop icons appstream
INSTALLS += target desktop icons appstream
}
win32 {
RC_ICONS = moonlight.ico
@@ -336,9 +371,6 @@ win32 {
QMAKE_TARGET_DESCRIPTION = Moonlight Game Streaming Client
QMAKE_TARGET_PRODUCT = Moonlight
# Copy the required data files into the application directory
copyToDestDir(SDL_GameControllerDB/gamecontrollerdb.txt ModeSeven.ttf)
CONFIG -= embed_manifest_exe
QMAKE_LFLAGS += /MANIFEST:embed /MANIFESTINPUT:$${PWD}/Moonlight.exe.manifest
}
@@ -349,10 +381,10 @@ macx {
QMAKE_INFO_PLIST = $$OUT_PWD/Info.plist
APP_BUNDLE_RESOURCES.files = moonlight.icns SDL_GameControllerDB/gamecontrollerdb.txt ModeSeven.ttf
APP_BUNDLE_RESOURCES.files = moonlight.icns
APP_BUNDLE_RESOURCES.path = Contents/Resources
APP_BUNDLE_FRAMEWORKS.files = $$files(../libs/mac/Frameworks/*.framework, true)
APP_BUNDLE_FRAMEWORKS.files = $$files(../libs/mac/Frameworks/*.framework, true) $$files(../libs/mac/lib/*.dylib, true)
APP_BUNDLE_FRAMEWORKS.path = Contents/Frameworks
QMAKE_BUNDLE_DATA += APP_BUNDLE_RESOURCES APP_BUNDLE_FRAMEWORKS
+15 -3
View File
@@ -28,7 +28,10 @@ AutoUpdateChecker::AutoUpdateChecker(QObject *parent) :
void AutoUpdateChecker::start()
{
#if defined(Q_OS_WIN32) || defined(Q_OS_DARWIN) // Only run update checker on platforms without auto-update
#if defined(Q_OS_WIN32) || defined(Q_OS_DARWIN) || defined(STEAM_LINK) // Only run update checker on platforms without auto-update
// HACK: Set network accessibility to work around QTBUG-80947
m_Nam.setNetworkAccessible(QNetworkAccessManager::Accessible);
// We'll get a callback when this is finished
QUrl url("https://moonlight-stream.org/updates/qt.json");
m_Nam.get(QNetworkRequest(url));
@@ -43,6 +46,15 @@ void AutoUpdateChecker::parseStringToVersionQuad(QString& string, QVector<int>&
}
}
QString AutoUpdateChecker::getPlatform()
{
#ifdef STEAM_LINK
return QStringLiteral("steamlink");
#else
return QSysInfo::productType();
#endif
}
void AutoUpdateChecker::handleUpdateCheckRequestFinished(QNetworkReply* reply)
{
Q_ASSERT(reply->isFinished());
@@ -88,7 +100,7 @@ void AutoUpdateChecker::handleUpdateCheckRequestFinished(QNetworkReply* reply)
}
if (updateObj["arch"] == QSysInfo::buildCpuArchitecture() &&
updateObj["platform"] == QSysInfo::productType()) {
updateObj["platform"] == getPlatform()) {
qDebug() << "Found update manifest match for current platform";
QString latestVersion = updateObj["version"].toString();
@@ -137,7 +149,7 @@ void AutoUpdateChecker::handleUpdateCheckRequestFinished(QNetworkReply* reply)
}
qWarning() << "No entry in update manifest found for current platform: "
<< QSysInfo::buildCpuArchitecture() << QSysInfo::productType();
<< QSysInfo::buildCpuArchitecture() << getPlatform();
}
else {
qWarning() << "Update checking failed with error: " << reply->error();
+2
View File
@@ -20,6 +20,8 @@ private slots:
private:
void parseStringToVersionQuad(QString& string, QVector<int>& version);
QString getPlatform();
QVector<int> m_CurrentVersionQuad;
QNetworkAccessManager m_Nam;
};
+2 -3
View File
@@ -89,10 +89,9 @@ QUrl BoxArtManager::loadBoxArt(NvComputer* computer, NvApp& app)
void BoxArtManager::handleBoxArtLoadComplete(NvComputer* computer, NvApp app, QUrl image)
{
if (image.isEmpty()) {
image = QUrl("qrc:/res/no_app_image.png");
if (!image.isEmpty()) {
emit boxArtLoadComplete(computer, app, image);
}
emit boxArtLoadComplete(computer, app, image);
}
QUrl BoxArtManager::loadBoxArtFromNetwork(NvComputer* computer, int appId)
+130 -24
View File
@@ -7,6 +7,7 @@
#include <QThread>
#include <QThreadPool>
#include <QCoreApplication>
#define SER_HOSTS "hosts"
@@ -130,8 +131,12 @@ private:
emit computerStateChanged(m_Computer);
}
// Wait a bit to poll again
QThread::sleep(3);
// Wait a bit to poll again, but do it in 100 ms chunks
// so we can be interrupted reasonably quickly.
// FIXME: QWaitCondition would be better.
for (int i = 0; i < 30 && !isInterruptionRequested(); i++) {
QThread::msleep(100);
}
}
}
@@ -157,6 +162,13 @@ ComputerManager::ComputerManager(QObject *parent)
m_KnownHosts[computer->uuid] = computer;
}
settings.endArray();
// To quit in a timely manner, we must block additional requests
// after we receive the aboutToQuit() signal. This is neccessary
// because NvHTTP uses aboutToQuit() to abort requests in progres
// while quitting, however this is a one time signal - additional
// requests would not be aborted and block termination.
connect(QCoreApplication::instance(), SIGNAL(aboutToQuit()), this, SLOT(handleAboutToQuit()));
}
ComputerManager::~ComputerManager()
@@ -204,6 +216,42 @@ void ComputerManager::saveHosts()
settings.endArray();
}
QHostAddress ComputerManager::getBestGlobalAddressV6(QVector<QHostAddress> &addresses)
{
for (const QHostAddress& address : addresses) {
if (address.protocol() == QAbstractSocket::IPv6Protocol) {
if (address.isInSubnet(QHostAddress("fe80::"), 10)) {
// Link-local
continue;
}
if (address.isInSubnet(QHostAddress("fec0::"), 10)) {
qInfo() << "Ignoring site-local address:" << address;
continue;
}
if (address.isInSubnet(QHostAddress("fc00::"), 7)) {
qInfo() << "Ignoring ULA:" << address;
continue;
}
if (address.isInSubnet(QHostAddress("2002::"), 16)) {
qInfo() << "Ignoring 6to4 address:" << address;
continue;
}
if (address.isInSubnet(QHostAddress("2001::"), 32)) {
qInfo() << "Ignoring Teredo address:" << address;
continue;
}
return address;
}
}
return QHostAddress();
}
void ComputerManager::startPolling()
{
QWriteLocker lock(&m_Lock);
@@ -221,9 +269,9 @@ void ComputerManager::startPolling()
this, [this](const QMdnsEngine::Service& service) {
qInfo() << "Discovered mDNS host:" << service.hostname();
MdnsPendingComputer* pendingComputer = new MdnsPendingComputer(&m_MdnsServer, &m_MdnsCache, service);
connect(pendingComputer, SIGNAL(resolvedv4(MdnsPendingComputer*,QHostAddress)),
this, SLOT(handleMdnsServiceResolved(MdnsPendingComputer*,QHostAddress)));
MdnsPendingComputer* pendingComputer = new MdnsPendingComputer(&m_MdnsServer, service);
connect(pendingComputer, &MdnsPendingComputer::resolvedHost,
this, &ComputerManager::handleMdnsServiceResolved);
m_PendingResolution.append(pendingComputer);
});
}
@@ -265,11 +313,39 @@ void ComputerManager::startPollingComputer(NvComputer* computer)
}
void ComputerManager::handleMdnsServiceResolved(MdnsPendingComputer* computer,
const QHostAddress& address)
QVector<QHostAddress>& addresses)
{
qInfo() << "Resolved" << computer->hostname() << "to" << address.toString();
QHostAddress v6Global = getBestGlobalAddressV6(addresses);
bool added = false;
addNewHost(address.toString(), true);
// Add the host using the IPv4 address
for (const QHostAddress& address : addresses) {
if (address.protocol() == QAbstractSocket::IPv4Protocol) {
// NB: We don't just call addNewHost() here with v6Global because the IPv6
// address may not be reachable (if the user hasn't installed the IPv6 helper yet
// or if this host lacks outbound IPv6 capability). We want to add IPv6 even if
// it's not currently reachable.
addNewHost(address.toString(), true, v6Global);
added = true;
break;
}
}
if (!added) {
// If we get here, there wasn't an IPv4 address so we'll do it v6-only
for (const QHostAddress& address : addresses) {
if (address.protocol() == QAbstractSocket::IPv6Protocol) {
// Use a link-local or site-local address for the "local address"
if (address.isInSubnet(QHostAddress("fe80::"), 10) ||
address.isInSubnet(QHostAddress("fec0::"), 10) ||
address.isInSubnet(QHostAddress("fc00::"), 7)) {
addNewHost(address.toString(), true, v6Global);
added = true;
break;
}
}
}
}
m_PendingResolution.removeOne(computer);
computer->deleteLater();
@@ -339,6 +415,17 @@ void ComputerManager::deleteHost(NvComputer* computer)
QThreadPool::globalInstance()->start(new DeferredHostDeletionTask(this, computer));
}
void ComputerManager::handleAboutToQuit()
{
QWriteLocker lock(&m_Lock);
// Interrupt polling threads immediately, so they
// avoid making additional requests while quitting
for (ComputerPollingEntry* entry : m_PollEntries) {
entry->interrupt();
}
}
class PendingPairingTask : public QObject, public QRunnable
{
Q_OBJECT
@@ -378,7 +465,7 @@ private:
break;
}
} catch (const GfeHttpResponseException& e) {
emit pairingCompleted(m_Computer, e.toQString());
emit pairingCompleted(m_Computer, "GeForce Experience returned error: " + e.toQString());
} catch (const QtNetworkReplyException& e) {
emit pairingCompleted(m_Computer, e.toQString());
}
@@ -485,9 +572,10 @@ class PendingAddTask : public QObject, public QRunnable
Q_OBJECT
public:
PendingAddTask(ComputerManager* computerManager, QString address, bool mdns)
PendingAddTask(ComputerManager* computerManager, QString address, QHostAddress mdnsIpv6Address, bool mdns)
: m_ComputerManager(computerManager),
m_Address(address),
m_MdnsIpv6Address(mdnsIpv6Address),
m_Mdns(mdns)
{
connect(this, &PendingAddTask::computerAddCompleted,
@@ -537,16 +625,21 @@ private:
{
NvHTTP http(m_Address, QSslCertificate());
qInfo() << "Processing new PC at" << m_Address << "from" << (m_Mdns ? "mDNS" : "user");
qInfo() << "Processing new PC at" << m_Address << "from" << (m_Mdns ? "mDNS" : "user") << m_MdnsIpv6Address;
// Perform initial serverinfo fetch over HTTP since we don't know which cert to use
QString serverInfo = fetchServerInfo(http);
if (serverInfo.isEmpty() && !m_MdnsIpv6Address.isNull()) {
// Retry using the global IPv6 address if the IPv4 or link-local IPv6 address fails
http.setAddress(m_MdnsIpv6Address.toString());
serverInfo = fetchServerInfo(http);
}
if (serverInfo.isEmpty()) {
return;
}
// Create initial newComputer using HTTP serverinfo with no pinned cert
NvComputer* newComputer = new NvComputer(m_Address, serverInfo, QSslCertificate());
NvComputer* newComputer = new NvComputer(http.address(), serverInfo, QSslCertificate());
// Check if we have a record of this host UUID to pull the pinned cert
NvComputer* existingComputer;
@@ -566,22 +659,34 @@ private:
}
// Update the polled computer with the HTTPS information
NvComputer httpsComputer(m_Address, serverInfo, QSslCertificate());
NvComputer httpsComputer(http.address(), serverInfo, QSslCertificate());
newComputer->update(httpsComputer);
}
// Update addresses depending on the context
if (m_Mdns) {
newComputer->localAddress = m_Address;
// Get the WAN IP address using STUN if we're on mDNS
quint32 addr;
int err = LiFindExternalAddressIP4("stun.moonlight-stream.org", 3478, &addr);
if (err == 0) {
newComputer->remoteAddress = QHostAddress(qFromBigEndian(addr)).toString();
// Only update local address if we actually reached the PC via this address.
// If we reached it via the IPv6 address after the local address failed,
// don't store the non-working local address.
if (http.address() == m_Address) {
newComputer->localAddress = m_Address;
}
else {
qWarning() << "STUN failed to get WAN address:" << err;
// Get the WAN IP address using STUN if we're on mDNS over IPv4
if (QHostAddress(newComputer->localAddress).protocol() == QAbstractSocket::IPv4Protocol) {
quint32 addr;
int err = LiFindExternalAddressIP4("stun.moonlight-stream.org", 3478, &addr);
if (err == 0) {
newComputer->remoteAddress = QHostAddress(qFromBigEndian(addr)).toString();
}
else {
qWarning() << "STUN failed to get WAN address:" << err;
}
}
if (!m_MdnsIpv6Address.isNull()) {
Q_ASSERT(m_MdnsIpv6Address.protocol() == QAbstractSocket::IPv6Protocol);
newComputer->ipv6Address = m_MdnsIpv6Address.toString();
}
}
else {
@@ -634,14 +739,15 @@ private:
ComputerManager* m_ComputerManager;
QString m_Address;
QHostAddress m_MdnsIpv6Address;
bool m_Mdns;
};
void ComputerManager::addNewHost(QString address, bool mdns)
void ComputerManager::addNewHost(QString address, bool mdns, QHostAddress mdnsIpv6Address)
{
// Punt to a worker thread to avoid stalling the
// UI while waiting for serverinfo query to complete
PendingAddTask* addTask = new PendingAddTask(this, address, mdns);
PendingAddTask* addTask = new PendingAddTask(this, address, mdnsIpv6Address, mdns);
QThreadPool::globalInstance()->start(addTask);
}
+44 -12
View File
@@ -13,6 +13,7 @@
#include <QReadWriteLock>
#include <QSettings>
#include <QRunnable>
#include <QTimer>
class MdnsPendingComputer : public QObject
{
@@ -20,13 +21,18 @@ class MdnsPendingComputer : public QObject
public:
explicit MdnsPendingComputer(QMdnsEngine::Server* server,
QMdnsEngine::Cache* cache,
const QMdnsEngine::Service& service)
: m_Hostname(service.hostname()),
m_Resolver(server, m_Hostname, cache)
m_Server(server),
m_Resolver(nullptr)
{
connect(&m_Resolver, SIGNAL(resolved(QHostAddress)),
this, SLOT(handleResolved(QHostAddress)));
// Start resolving
resolve();
}
virtual ~MdnsPendingComputer()
{
delete m_Resolver;
}
QString hostname()
@@ -35,20 +41,42 @@ public:
}
private slots:
void handleResolved(const QHostAddress& address)
void handleResolvedTimeout()
{
if (address.protocol() == QAbstractSocket::IPv4Protocol) {
m_Resolver.disconnect();
emit resolvedv4(this, address);
if (m_Addresses.isEmpty()) {
// Try again
qInfo() << "Resolving" << hostname() << "timed out. Retrying...";
resolve();
}
else {
Q_ASSERT(!m_Addresses.isEmpty());
emit resolvedHost(this, m_Addresses);
}
}
void handleResolvedAddress(const QHostAddress& address)
{
qInfo() << "Resolved" << hostname() << "to" << address;
m_Addresses.push_back(address);
}
signals:
void resolvedv4(MdnsPendingComputer*, const QHostAddress&);
void resolvedHost(MdnsPendingComputer*,QVector<QHostAddress>&);
private:
void resolve()
{
delete m_Resolver;
m_Resolver = new QMdnsEngine::Resolver(m_Server, m_Hostname);
connect(m_Resolver, &QMdnsEngine::Resolver::resolved,
this, &MdnsPendingComputer::handleResolvedAddress);
QTimer::singleShot(2000, this, SLOT(handleResolvedTimeout()));
}
QByteArray m_Hostname;
QMdnsEngine::Resolver m_Resolver;
QMdnsEngine::Server* m_Server;
QMdnsEngine::Resolver* m_Resolver;
QVector<QHostAddress> m_Addresses;
};
class ComputerPollingEntry
@@ -140,7 +168,7 @@ public:
Q_INVOKABLE void stopPollingAsync();
Q_INVOKABLE void addNewHost(QString address, bool mdns);
Q_INVOKABLE void addNewHost(QString address, bool mdns, QHostAddress mdnsIpv6Address = QHostAddress());
void pairHost(NvComputer* computer, QString pin);
@@ -161,13 +189,17 @@ signals:
void quitAppCompleted(QVariant error);
private slots:
void handleAboutToQuit();
void handleComputerStateChanged(NvComputer* computer);
void handleMdnsServiceResolved(MdnsPendingComputer* computer, const QHostAddress& address);
void handleMdnsServiceResolved(MdnsPendingComputer* computer, QVector<QHostAddress>& addresses);
private:
void saveHosts();
QHostAddress getBestGlobalAddressV6(QVector<QHostAddress>& addresses);
void startPollingComputer(NvComputer* computer);
int m_PollingRef;
+12 -5
View File
@@ -39,11 +39,18 @@ void ComputerSeeker::onComputerUpdated(NvComputer *computer)
bool ComputerSeeker::matchComputer(NvComputer *computer) const
{
QString value = m_ComputerName.toLower();
return computer->name.toLower() == value ||
computer->localAddress.toLower() == value ||
computer->remoteAddress.toLower() == value ||
computer->manualAddress.toLower() == value ||
computer->uuid.toLower() == value;
if (computer->name.toLower() == value || computer->uuid.toLower() == value) {
return true;
}
for (const QString& addr : computer->uniqueAddresses()) {
if (addr.toLower() == value) {
return true;
}
}
return false;
}
bool ComputerSeeker::isOnline(NvComputer *computer) const
+1 -1
View File
@@ -75,7 +75,7 @@ void IdentityManager::createCredentials(QSettings& settings)
-1, -1, 0);
X509_set_issuer_name(cert, name);
X509_sign(cert, pk, EVP_sha1());
X509_sign(cert, pk, EVP_sha256());
BIO* biokey = BIO_new(BIO_s_mem());
THROW_BAD_ALLOC_IF_NULL(biokey);
+87 -1
View File
@@ -3,6 +3,7 @@
#include <QUdpSocket>
#include <QHostInfo>
#include <QNetworkInterface>
#include <QNetworkProxy>
#define SER_NAME "hostname"
#define SER_UUID "uuid"
@@ -10,6 +11,7 @@
#define SER_LOCALADDR "localaddress"
#define SER_REMOTEADDR "remoteaddress"
#define SER_MANUALADDR "manualaddress"
#define SER_IPV6ADDR "ipv6address"
#define SER_APPLIST "apps"
#define SER_SRVCERT "srvcert"
@@ -24,6 +26,7 @@ NvComputer::NvComputer(QSettings& settings)
this->macAddress = settings.value(SER_MAC).toByteArray();
this->localAddress = settings.value(SER_LOCALADDR).toString();
this->remoteAddress = settings.value(SER_REMOTEADDR).toString();
this->ipv6Address = settings.value(SER_IPV6ADDR).toString();
this->manualAddress = settings.value(SER_MANUALADDR).toString();
this->serverCert = QSslCertificate(settings.value(SER_SRVCERT).toByteArray());
@@ -63,6 +66,7 @@ void NvComputer::serialize(QSettings& settings)
settings.setValue(SER_MAC, macAddress);
settings.setValue(SER_LOCALADDR, localAddress);
settings.setValue(SER_REMOTEADDR, remoteAddress);
settings.setValue(SER_IPV6ADDR, ipv6Address);
settings.setValue(SER_MANUALADDR, manualAddress);
settings.setValue(SER_SRVCERT, serverCert.toPem());
@@ -129,7 +133,12 @@ NvComputer::NvComputer(QString address, QString serverInfo, QSslCertificate serv
mode2.width * mode2.height * mode2.refreshRate;
});
// We can get an IPv4 loopback address if we're using the GS IPv6 Forwarder
this->localAddress = NvHTTP::getXmlString(serverInfo, "LocalIP");
if (this->localAddress.startsWith("127.")) {
this->localAddress = QString();
}
this->remoteAddress = NvHTTP::getXmlString(serverInfo, "ExternalIP");
this->pairState = NvHTTP::getXmlString(serverInfo, "PairStatus") == "1" ?
PS_PAIRED : PS_NOT_PAIRED;
@@ -156,7 +165,7 @@ bool NvComputer::wake()
const quint16 WOL_PORTS[] = {
7, 9, // Standard WOL ports
47998, 47999, 48000, // Ports opened by GFE
47998, 47999, 48000, 48002, 48010, // Ports opened by GFE
};
// Create the WoL payload
@@ -229,6 +238,81 @@ bool NvComputer::wake()
return success;
}
bool NvComputer::isReachableOverVpn()
{
if (activeAddress.isEmpty()) {
return false;
}
QTcpSocket s;
s.setProxy(QNetworkProxy::NoProxy);
s.connectToHost(activeAddress, 47984);
if (s.waitForConnected(3000)) {
Q_ASSERT(!s.localAddress().isNull());
for (const QNetworkInterface& nic : QNetworkInterface::allInterfaces()) {
// Ensure the interface is up
if ((nic.flags() & QNetworkInterface::IsUp) == 0) {
continue;
}
for (const QNetworkAddressEntry& addr : nic.addressEntries()) {
if (addr.ip() == s.localAddress()) {
qInfo() << "Found matching interface:" << nic.humanReadableName() << nic.hardwareAddress() << nic.flags();
#if QT_VERSION >= QT_VERSION_CHECK(5, 11, 0)
qInfo() << "Interface Type:" << nic.type();
qInfo() << "Interface MTU:" << nic.maximumTransmissionUnit();
if (nic.type() == QNetworkInterface::Virtual ||
nic.type() == QNetworkInterface::Ppp) {
// Treat PPP and virtual interfaces as likely VPNs
return true;
}
if (nic.maximumTransmissionUnit() != 0 && nic.maximumTransmissionUnit() < 1500) {
// Treat MTUs under 1500 as likely VPNs
return true;
}
#endif
if (nic.flags() & QNetworkInterface::IsPointToPoint) {
// Treat point-to-point links as likely VPNs.
// This check detects OpenVPN on Unix-like OSes.
return true;
}
if (nic.hardwareAddress().startsWith("00:FF", Qt::CaseInsensitive)) {
// OpenVPN TAP interfaces have a MAC address starting with 00:FF on Windows
return true;
}
if (nic.humanReadableName().startsWith("ZeroTier")) {
// ZeroTier interfaces always start with "ZeroTier"
return true;
}
if (nic.humanReadableName().contains("VPN")) {
// This one is just a final heuristic if all else fails
return true;
}
// Didn't meet any of our VPN heuristics
return false;
}
}
}
qWarning() << "No match found for address:" << s.localAddress();
return false;
}
else {
// If we fail to connect, just pretend that it's not a VPN
return false;
}
}
QVector<QString> NvComputer::uniqueAddresses()
{
QVector<QString> uniqueAddressList;
@@ -237,6 +321,7 @@ QVector<QString> NvComputer::uniqueAddresses()
uniqueAddressList.append(activeAddress);
uniqueAddressList.append(localAddress);
uniqueAddressList.append(remoteAddress);
uniqueAddressList.append(ipv6Address);
uniqueAddressList.append(manualAddress);
// Prune duplicates (always giving precedence to the first)
@@ -296,6 +381,7 @@ bool NvComputer::update(NvComputer& that)
ASSIGN_IF_CHANGED_AND_NONEMPTY(macAddress);
ASSIGN_IF_CHANGED_AND_NONEMPTY(localAddress);
ASSIGN_IF_CHANGED_AND_NONEMPTY(remoteAddress);
ASSIGN_IF_CHANGED_AND_NONEMPTY(ipv6Address);
ASSIGN_IF_CHANGED_AND_NONEMPTY(manualAddress);
ASSIGN_IF_CHANGED(pairState);
ASSIGN_IF_CHANGED(serverCodecModeSupport);
+4
View File
@@ -29,6 +29,9 @@ public:
bool
wake();
bool
isReachableOverVpn();
QVector<QString>
uniqueAddresses();
@@ -64,6 +67,7 @@ public:
// Persisted traits
QString localAddress;
QString remoteAddress;
QString ipv6Address;
QString manualAddress;
QByteArray macAddress;
QString name;
+47 -24
View File
@@ -18,21 +18,20 @@
#define QUIT_TIMEOUT_MS 30000
NvHTTP::NvHTTP(QString address, QSslCertificate serverCert) :
m_Address(address),
m_ServerCert(serverCert)
{
Q_ASSERT(!address.isEmpty());
m_BaseUrlHttp.setScheme("http");
m_BaseUrlHttps.setScheme("https");
m_BaseUrlHttp.setHost(address);
m_BaseUrlHttps.setHost(address);
m_BaseUrlHttp.setPort(47989);
m_BaseUrlHttps.setPort(47984);
setAddress(address);
// Never use a proxy server
QNetworkProxy noProxy(QNetworkProxy::NoProxy);
m_Nam.setProxy(noProxy);
connect(&m_Nam, &QNetworkAccessManager::sslErrors, this, &NvHTTP::handleSslErrors);
}
void NvHTTP::setServerCert(QSslCertificate serverCert)
@@ -40,6 +39,21 @@ void NvHTTP::setServerCert(QSslCertificate serverCert)
m_ServerCert = serverCert;
}
void NvHTTP::setAddress(QString address)
{
Q_ASSERT(!address.isEmpty());
m_Address = address;
m_BaseUrlHttp.setHost(address);
m_BaseUrlHttps.setHost(address);
}
QString NvHTTP::address()
{
return m_Address;
}
QVector<int>
NvHTTP::parseQuad(QString quad)
{
@@ -377,6 +391,28 @@ NvHTTP::getXmlString(QString xml,
return nullptr;
}
void NvHTTP::handleSslErrors(QNetworkReply* reply, const QList<QSslError>& errors)
{
bool ignoreErrors = true;
if (m_ServerCert.isNull()) {
// We should never make an HTTPS request without a cert
Q_ASSERT(!m_ServerCert.isNull());
return;
}
for (auto error : errors) {
if (m_ServerCert != error.certificate()) {
ignoreErrors = false;
break;
}
}
if (ignoreErrors) {
reply->ignoreSslErrors(errors);
}
}
QString
NvHTTP::openConnectionToString(QUrl baseUrl,
QString command,
@@ -413,33 +449,20 @@ NvHTTP::openConnection(QUrl baseUrl,
QUuid::createUuid().toRfc4122().toHex() +
((arguments != nullptr) ? ("&" + arguments) : ""));
QNetworkRequest request = QNetworkRequest(url);
QNetworkRequest request(url);
// Add our client certificate
request.setSslConfiguration(IdentityManager::get()->getSslConfig());
// HACK: Set reachability to work around QTBUG-80947
m_Nam.setNetworkAccessible(QNetworkAccessManager::Accessible);
QNetworkReply* reply = m_Nam.get(request);
// Assert that we always have a server cert for HTTPS, since
// the request will fail anyway if we do not.
Q_ASSERT(!m_ServerCert.isNull() || baseUrl == m_BaseUrlHttp);
if (!m_ServerCert.isNull()) {
// Pin the server certificate received during pairing
QList<QSslError> expectedSslErrors;
expectedSslErrors.append(QSslError(QSslError::HostNameMismatch, m_ServerCert));
expectedSslErrors.append(QSslError(QSslError::SelfSignedCertificate, m_ServerCert));
// The SecureTransport backend for Qt TLS on macOS throws CertificateUntrusted
// instead of SelfSignedCertificate, so we will need to allow that error too.
expectedSslErrors.append(QSslError(QSslError::CertificateUntrusted, m_ServerCert));
reply->ignoreSslErrors(expectedSslErrors);
}
// Run the request with a timeout if requested
QEventLoop loop;
QObject::connect(reply, SIGNAL(finished()), &loop, SLOT(quit()));
connect(reply, SIGNAL(finished()), &loop, SLOT(quit()));
connect(QCoreApplication::instance(), SIGNAL(aboutToQuit()), &loop, SLOT(quit()));
if (timeoutMs) {
QTimer::singleShot(timeoutMs, &loop, SLOT(quit()));
}
+10 -1
View File
@@ -113,8 +113,10 @@ private:
QString m_ErrorText;
};
class NvHTTP
class NvHTTP : public QObject
{
Q_OBJECT
public:
enum NvLogLevel {
NVLL_NONE,
@@ -154,6 +156,10 @@ public:
void setServerCert(QSslCertificate serverCert);
void setAddress(QString address);
QString address();
static
QVector<int>
parseQuad(QString quad);
@@ -184,6 +190,9 @@ public:
QUrl m_BaseUrlHttp;
QUrl m_BaseUrlHttps;
private:
void
handleSslErrors(QNetworkReply* reply, const QList<QSslError>& errors);
QNetworkReply*
openConnection(QUrl baseUrl,
QString command,
+3 -1
View File
@@ -256,7 +256,9 @@ NvPairingManager::pair(QString appVersion, QString pin, QSslCertificate& serverC
challengeResponse.append(reinterpret_cast<char*>(asnSignature->data), asnSignature->length);
challengeResponse.append(clientSecretData);
QByteArray encryptedChallengeResponseHash = encrypt(QCryptographicHash::hash(challengeResponse, hashAlgo), &encKey);
QByteArray paddedHash = QCryptographicHash::hash(challengeResponse, hashAlgo);
paddedHash.resize(32);
QByteArray encryptedChallengeResponseHash = encrypt(paddedHash, &encKey);
QString respXml = m_Http.openConnectionToString(m_Http.m_BaseUrlHttp,
"pair",
"devicename=roth&updateState=1&serverchallengeresp=" +
+64
View File
@@ -0,0 +1,64 @@
#include "richpresencemanager.h"
#include <QDebug>
RichPresenceManager::RichPresenceManager(StreamingPreferences& prefs, QString gameName)
: m_DiscordActive(false)
{
#ifdef HAVE_DISCORD
if (prefs.richPresence) {
DiscordEventHandlers handlers = {};
handlers.ready = discordReady;
handlers.disconnected = discordDisconnected;
handlers.errored = discordErrored;
Discord_Initialize("594668102021677159", &handlers, 0, nullptr);
m_DiscordActive = true;
}
if (m_DiscordActive) {
QByteArray stateStr = (QString("Streaming ") + gameName).toUtf8();
DiscordRichPresence discordPresence = {};
discordPresence.state = stateStr.data();
discordPresence.startTimestamp = time(nullptr);
discordPresence.largeImageKey = "icon";
Discord_UpdatePresence(&discordPresence);
}
#endif
}
RichPresenceManager::~RichPresenceManager()
{
#ifdef HAVE_DISCORD
if (m_DiscordActive) {
Discord_ClearPresence();
Discord_Shutdown();
}
#endif
}
void RichPresenceManager::runCallbacks()
{
#ifdef HAVE_DISCORD
if (m_DiscordActive) {
Discord_RunCallbacks();
}
#endif
}
#ifdef HAVE_DISCORD
void RichPresenceManager::discordReady(const DiscordUser* request)
{
qInfo() << "Discord integration ready for user:" << request->username;
}
void RichPresenceManager::discordDisconnected(int errorCode, const char *message)
{
qInfo() << "Discord integration disconnected:" << errorCode << message;
}
void RichPresenceManager::discordErrored(int errorCode, const char *message)
{
qWarning() << "Discord integration error:" << errorCode << message;
}
#endif
+26
View File
@@ -0,0 +1,26 @@
#pragma once
#include "settings/streamingpreferences.h"
#ifdef HAVE_DISCORD
#include <discord_rpc.h>
#endif
class RichPresenceManager
{
public:
RichPresenceManager(StreamingPreferences& prefs, QString gameName);
~RichPresenceManager();
void runCallbacks();
private:
#ifdef HAVE_DISCORD
static void discordReady(const DiscordUser* request);
static void discordDisconnected(int errorCode, const char* message);
static void discordErrored(int errorCode, const char* message);
#endif
bool m_DiscordActive;
};
+123
View File
@@ -0,0 +1,123 @@
#include "systemproperties.h"
#include <QGuiApplication>
#include "streaming/session.h"
#include "streaming/streamutils.h"
SystemProperties::SystemProperties()
{
isRunningWayland = qgetenv("XDG_SESSION_TYPE") == "wayland";
isRunningXWayland = qgetenv("XDG_SESSION_TYPE") == "wayland" && QGuiApplication::platformName() == "xcb";
#ifdef Q_OS_WIN32
isWow64 = QSysInfo::currentCpuArchitecture() != QSysInfo::buildCpuArchitecture();
#else
isWow64 = false;
#endif
#ifndef STEAM_LINK
hasBrowser = true;
#else
hasBrowser = false;
#endif
#ifdef HAVE_DISCORD
hasDiscordIntegration = true;
#else
hasDiscordIntegration = false;
#endif
unmappedGamepads = SdlInputHandler::getUnmappedGamepads();
// Populate data that requires talking to SDL. We do it all in one shot
// and cache the results to speed up future queries on this data.
querySdlVideoInfo();
Q_ASSERT(maximumStreamingFrameRate >= 60);
Q_ASSERT(!monitorDesktopResolutions.isEmpty());
Q_ASSERT(!monitorNativeResolutions.isEmpty());
}
QRect SystemProperties::getDesktopResolution(int displayIndex)
{
// Returns default constructed QRect if out of bounds
return monitorDesktopResolutions.value(displayIndex);
}
QRect SystemProperties::getNativeResolution(int displayIndex)
{
// Returns default constructed QRect if out of bounds
return monitorNativeResolutions.value(displayIndex);
}
void SystemProperties::querySdlVideoInfo()
{
monitorDesktopResolutions.clear();
monitorNativeResolutions.clear();
hasHardwareAcceleration = false;
// Never let the maximum drop below 60 FPS
maximumStreamingFrameRate = 60;
if (SDL_InitSubSystem(SDL_INIT_VIDEO) != 0) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"SDL_InitSubSystem(SDL_INIT_VIDEO) failed: %s",
SDL_GetError());
return;
}
SDL_DisplayMode bestMode;
for (int displayIndex = 0; displayIndex < SDL_GetNumVideoDisplays(); displayIndex++) {
SDL_DisplayMode desktopMode;
int err;
err = SDL_GetDesktopDisplayMode(displayIndex, &desktopMode);
if (err == 0) {
monitorDesktopResolutions.insert(displayIndex, QRect(0, 0, desktopMode.w, desktopMode.h));
}
else {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"SDL_GetDesktopDisplayMode() failed: %s",
SDL_GetError());
}
if (StreamUtils::getRealDesktopMode(displayIndex, &desktopMode)) {
monitorNativeResolutions.insert(displayIndex, QRect(0, 0, desktopMode.w, desktopMode.h));
// Start at desktop mode and work our way up
bestMode = desktopMode;
for (int i = 0; i < SDL_GetNumDisplayModes(displayIndex); i++) {
SDL_DisplayMode mode;
if (SDL_GetDisplayMode(displayIndex, i, &mode) == 0) {
if (mode.w == desktopMode.w && mode.h == desktopMode.h) {
if (mode.refresh_rate > bestMode.refresh_rate) {
bestMode = mode;
}
}
}
}
maximumStreamingFrameRate = qMax(maximumStreamingFrameRate, bestMode.refresh_rate);
}
}
SDL_Window* testWindow = SDL_CreateWindow("", 0, 0, 1280, 720, SDL_WINDOW_HIDDEN);
if (!testWindow) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Failed to create window for hardware decode test: %s",
SDL_GetError());
SDL_QuitSubSystem(SDL_INIT_VIDEO);
return;
}
hasHardwareAcceleration =
Session::isHardwareDecodeAvailable(testWindow,
StreamingPreferences::VDS_AUTO,
VIDEO_FORMAT_H264,
1920, 1080, 60);
SDL_DestroyWindow(testWindow);
SDL_QuitSubSystem(SDL_INIT_VIDEO);
}
+42
View File
@@ -0,0 +1,42 @@
#pragma once
#include <QObject>
#include <QRect>
class SystemProperties : public QObject
{
Q_OBJECT
public:
SystemProperties();
Q_PROPERTY(bool hasHardwareAcceleration MEMBER hasHardwareAcceleration CONSTANT)
Q_PROPERTY(bool isRunningWayland MEMBER isRunningWayland CONSTANT)
Q_PROPERTY(bool isRunningXWayland MEMBER isRunningXWayland CONSTANT)
Q_PROPERTY(bool isWow64 MEMBER isWow64 CONSTANT)
Q_PROPERTY(bool hasBrowser MEMBER hasBrowser CONSTANT)
Q_PROPERTY(bool hasDiscordIntegration MEMBER hasDiscordIntegration CONSTANT)
Q_PROPERTY(QString unmappedGamepads MEMBER unmappedGamepads NOTIFY unmappedGamepadsChanged)
Q_PROPERTY(int maximumStreamingFrameRate MEMBER maximumStreamingFrameRate CONSTANT)
Q_INVOKABLE QRect getDesktopResolution(int displayIndex);
Q_INVOKABLE QRect getNativeResolution(int displayIndex);
signals:
void unmappedGamepadsChanged();
private:
void querySdlVideoInfo();
bool hasHardwareAcceleration;
bool isRunningWayland;
bool isRunningXWayland;
bool isWow64;
bool hasBrowser;
bool hasDiscordIntegration;
QString unmappedGamepads;
int maximumStreamingFrameRate;
QList<QRect> monitorDesktopResolutions;
QList<QRect> monitorNativeResolutions;
};
+1 -1
View File
@@ -59,7 +59,7 @@ public:
void showMessage(QString message, MessageType type) const
{
#if defined(Q_OS_WIN)
#if defined(Q_OS_WIN32)
UINT flags = MB_OK | MB_TOPMOST | MB_SETFOREGROUND;
flags |= (type == Info ? MB_ICONINFORMATION : MB_ICONERROR);
QString title = "Moonlight";
@@ -12,10 +12,10 @@
<ul>
<li>Streaming at up to 4K resolution</li>
<li>Support for up to 120 FPS streaming (high refresh rate monitor recommended)</li>
<li>Hardware accelerated video decoding with VAAPI and VDPAU support</li>
<li>Hardware accelerated video decoding with VAAPI, VDPAU, and NVDEC support</li>
<li>5.1 surround sound audio support</li>
<li>HEVC support for better video compression efficiency</li>
<li>Gamepad support with SDL mapping compatibility</li>
<li>Gamepad support with force feedback for up to 4 players</li>
</ul>
</description>
@@ -33,7 +33,117 @@
</screenshots>
<releases>
<release version="0.9.0" date="2019-02-17">
<release version="1.2.0" date="2019-12-17">
<description>
<p>Bugfixes:</p>
<ul>
<li>Improved video encoding efficiency</li>
<li>Improved performance when streaming over a VPN</li>
<li>Fixed RTSP handshake error on certain networks</li>
<li>Fixed rumble not working on certain gamepads</li>
<li>Fixed pairing error when double-clicking on an unpaired PC</li>
<li>Updated included SDL gamepad mappings</li>
</ul>
</description>
</release>
<release version="1.1.1" date="2019-09-29">
<description>
<p>Bugfixes:</p>
<ul>
<li>Improved reliability of public IP address detection</li>
<li>Fixed connecting to a host PC with multiple network connections</li>
<li>Fixed a hang after exiting a full-screen stream with GNOME on Wayland</li>
<li>Fixed gamepad hotplugging</li>
<li>Updated to SDL 2.0.10</li>
<li>Updated to Qt 5.13.1</li>
</ul>
</description>
</release>
<release version="1.1.0" date="2019-08-05">
<description>
<p>New features:</p>
<ul>
<li>Added support for NVDEC for NVIDIA GPUs on Wayland</li>
<li>Added an option to disable gamepad mouse mode for games that require holding the Start button</li>
<li>Added automatic detection of IPv6 addresses for Internet streaming over IPv6 networks</li>
</ul>
<p>Bugfixes:</p>
<ul>
<li>Improved speed and reliability of automatic PC detection on the local network</li>
<li>Fixed audio latency increasing under certain network conditions</li>
<li>Fixed 5.1 surround sound sometimes causing no audio to play when streaming over the Internet</li>
<li>Fixed graceful termination of games not being detected properly on GFE 3.19</li>
<li>Fixed several audio issues on Linux by switching back to SDL for audio</li>
<li>Changed client certificate signature algorithm from SHA-1 to SHA-256 for Debian Buster compatibility</li>
<li>Updated included gamepad mappings</li>
</ul>
</description>
</release>
<release version="1.0.1" date="2019-05-27">
<description>
<p>Bugfixes:</p>
<ul>
<li>Fixed broken video on GeForce Experience 3.19</li>
</ul>
</description>
</release>
<release version="1.0.0" date="2019-05-21">
<description>
<p>New features:</p>
<ul>
<li>Mouse mode for gamepads (Press and hold Start to toggle)</li>
<li>Improved gamepad UI navigation</li>
</ul>
<p>Bugfixes:</p>
<ul>
<li>Fixed duplicate non-working gamepads appearing on the host</li>
<li>Fixed excessive frame queue delay on macOS</li>
<li>Fixed excessive audio latency on Steam Link</li>
<li>Fixed hiding mouse cursor after streaming on Steam Link</li>
<li>Fixed incorrect receive time stats on 32-bit platforms</li>
<li>Added UDP ports 48002 and 48010 for Wake-on-LAN</li>
<li>Updated included gamepad mappings</li>
</ul>
</description>
</release>
<release version="0.10.1" date="2019-04-28">
<description>
<p>New features:</p>
<ul>
<li>Video performance optimizations</li>
<li>VAAPI support on Wayland</li>
</ul>
<p>Bugfixes:</p>
<ul>
<li>Fixed a crash when attempting to dismiss the connection terminated dialog</li>
<li>Fixed a crash when using the overlay on the software decoder</li>
<li>Fixed software decoder not displaying output in some cases</li>
<li>Fixed some minor UI issues on the PC grid</li>
<li>Improved visibility of toolbar highlights</li>
</ul>
</description>
</release>
<release version="0.10.0" date="2019-04-07">
<description>
<p>New features:</p>
<ul>
<li>Redesigned App Grid with larger game artwork and less blank space</li>
<li>Redesigned UI dialogs using Material Design</li>
<li>Added a connection warning overlay (which can be disabled in settings)</li>
<li>Streaming window no longer minimizes when it loses focus in full-screen mode</li>
<li>The Steam Link device is now a supported platform</li>
</ul>
<p>Bugfixes:</p>
<ul>
<li>Fixed several bugs that could cause UI hangs or lag</li>
<li>Fixed rumble support on gamepads with limited rumble motor control</li>
<li>Fixed increased memory usage if the software renderer gets delayed</li>
<li>Fixed crashes on certain desktop configurations when loading the UI</li>
<li>Fixed handling of pairing certificates with a bogus date</li>
</ul>
</description>
</release>
<release version="0.9.1" date="2019-02-17">
<description>
<p>New features:</p>
<ul>
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

+13
View File
@@ -0,0 +1,13 @@
#!/bin/sh
# The default HOME is not persistent, so override
# it to a path on the onboard flash. Otherwise our
# pairing data will be lost each reboot.
HOME=/usr/local/moonlight
# Renice PE_Single_CPU which seems to host A/V stuff
renice -10 -p $(pidof PE_Single_CPU)
# Renice Moonlight itself to avoid preemption by background tasks
# Write output to a logfile in /tmp
exec nice -n -10 ./bin/moonlight > /tmp/moonlight.log
+3
View File
@@ -0,0 +1,3 @@
name=Moonlight
icon=moonlight.png
run=moonlight.sh
+84 -64
View File
@@ -1,29 +1,21 @@
import QtQuick 2.9
import QtQuick.Dialogs 1.2
import QtQuick.Controls 2.2
import AppModel 1.0
import ComputerManager 1.0
import SdlGamepadKeyNavigation 1.0
GridView {
CenteredGridView {
property int computerIndex
property AppModel appModel : createModel()
property bool activated
id: appGrid
focus: true
activeFocusOnTab: true
anchors.fill: parent
anchors.leftMargin: (parent.width % (cellWidth + anchors.rightMargin)) / 2
anchors.topMargin: 20
anchors.rightMargin: 5
anchors.bottomMargin: 5
cellWidth: 225; cellHeight: 385;
// The StackView will trigger a visibility change when
// we're pushed onto it, causing our onVisibleChanged
// routine to run, but only if we start as invisible
visible: false
topMargin: 20
bottomMargin: 5
cellWidth: 230; cellHeight: 297;
function computerLost()
{
@@ -32,23 +24,25 @@ GridView {
}
Component.onCompleted: {
// Don't show any highlighted item until interacting with them
// Don't show any highlighted item until interacting with them.
// We do this here instead of onActivated to avoid losing the user's
// selection when backing out of a different page of the app.
currentIndex = -1
}
SdlGamepadKeyNavigation {
id: gamepadKeyNav
StackView.onActivated: {
appModel.computerLost.connect(computerLost)
activated = true
// Highlight the first item if a gamepad is connected
if (currentIndex == -1 && SdlGamepadKeyNavigation.getConnectedGamepads() > 0) {
currentIndex = 0
}
}
onVisibleChanged: {
if (visible) {
appModel.computerLost.connect(computerLost)
gamepadKeyNav.enable()
}
else {
appModel.computerLost.disconnect(computerLost)
gamepadKeyNav.disable()
}
StackView.onDeactivating: {
appModel.computerLost.disconnect(computerLost)
activated = false
}
function createModel()
@@ -61,26 +55,44 @@ GridView {
model: appModel
delegate: NavigableItemDelegate {
width: 200; height: 335;
width: 220; height: 287;
grid: appGrid
Image {
property bool isPlaceholder: false
id: appIcon
anchors.horizontalCenter: parent.horizontalCenter
y: 20
y: 10
source: model.boxart
sourceSize {
width: 150
height: 200
onSourceSizeChanged: {
if ((sourceSize.width == 130 && sourceSize.height == 180) || // GFE 2.0 placeholder image
(sourceSize.width == 628 && sourceSize.height == 888) || // GFE 3.0 placeholder image
(sourceSize.width == 200 && sourceSize.height == 266)) // Our no_app_image.png
{
isPlaceholder = true
}
else
{
isPlaceholder = false
}
width = 200
height = 267
}
width: sourceSize.width
height: sourceSize.height
fillMode: Image.Pad
// Display a tooltip with the full name if it's truncated
ToolTip.text: model.name
ToolTip.delay: 1000
ToolTip.timeout: 5000
ToolTip.visible: (parent.hovered || parent.highlighted) && (!appNameText.visible || appNameText.truncated)
}
ToolButton {
id: resumeButton
anchors.verticalCenterOffset: -50
anchors.horizontalCenterOffset: appIcon.isPlaceholder ? -47 : 0
anchors.verticalCenterOffset: appIcon.isPlaceholder ? -75 : -60
anchors.centerIn: appIcon
visible: model.running
implicitWidth: 125
@@ -107,7 +119,8 @@ GridView {
ToolButton {
id: quitButton
anchors.verticalCenterOffset: 50
anchors.horizontalCenterOffset: appIcon.isPlaceholder ? 47 : 0
anchors.verticalCenterOffset: appIcon.isPlaceholder ? -75 : 60
anchors.centerIn: appIcon
visible: model.running
implicitWidth: 125
@@ -134,20 +147,20 @@ GridView {
Label {
id: appNameText
visible: appIcon.isPlaceholder
text: model.name
width: parent.width
height: 125
anchors.top: appIcon.bottom
width: appIcon.width
height: model.running ? 175 : appIcon.height
leftPadding: 20
rightPadding: 20
anchors.left: appIcon.left
anchors.right: appIcon.right
anchors.bottom: appIcon.bottom
font.pointSize: 22
verticalAlignment: Text.AlignVCenter
horizontalAlignment: Text.AlignHCenter
wrapMode: Text.Wrap
elide: Text.ElideRight
// Display a tooltip with the full name if it's truncated
ToolTip.text: model.name
ToolTip.delay: 1000
ToolTip.timeout: 5000
ToolTip.visible: (parent.hovered || parent.highlighted) && truncated
}
function launchOrResumeSelectedApp()
@@ -168,19 +181,29 @@ GridView {
}
onClicked: {
// Only allow clicking on the box art for non-running games.
// For running games, buttons will appear to resume or quit which
// will handle starting the game and clicks on the box art will
// be ignored.
if (!model.running) {
launchOrResumeSelectedApp()
}
}
Keys.onReturnPressed: {
// Open the app context menu if activated via the gamepad or keyboard
// for running games. If the game isn't running, the above onClicked
// method will handle the launch.
if (model.running) {
// This will primarily be keyboard/gamepad driven so use
// This will be keyboard/gamepad driven so use
// open() instead of popup()
appContextMenu.open()
}
else {
launchOrResumeSelectedApp()
}
}
Keys.onMenuPressed: {
if (model.running) {
// This will primarily be keyboard/gamepad driven so use
// This will be keyboard/gamepad driven so use
// open() instead of popup()
appContextMenu.open()
}
@@ -197,10 +220,7 @@ GridView {
NavigableMenuItem {
parentMenu: appContextMenu
text: model.running ? "Resume Game" : "Launch Game"
onTriggered: {
appContextMenu.close()
launchOrResumeSelectedApp()
}
onTriggered: launchOrResumeSelectedApp()
}
NavigableMenuItem {
parentMenu: appContextMenu
@@ -211,15 +231,14 @@ GridView {
}
}
MessageDialog {
NavigableMessageDialog {
id: quitAppDialog
modality:Qt.WindowModal
property string appName : ""
property bool segueToStream : false
property string nextAppName: ""
property int nextAppIndex: 0
text:"Are you sure you want to quit " + appName +"? Any unsaved progress will be lost."
standardButtons: StandardButton.Yes | StandardButton.No
standardButtons: Dialog.Yes | Dialog.No
function quitApp() {
var component = Qt.createComponent("QuitSegue.qml")
@@ -241,20 +260,21 @@ GridView {
appModel.quitRunningApp()
}
onYes: quitApp()
// For keyboard/gamepad navigation
onAccepted: quitApp()
}
ScrollBar.vertical: ScrollBar {
// Manually hide the scrollbar to prevent it from being drawn on top
// of the StreamSegue during the transition. It can sometimes get stuck
// there since we're not consistently pumping the event loop while
// starting the stream.
visible: activated
parent: appGrid.parent
anchors {
top: appGrid.top
left: appGrid.right
bottom: appGrid.bottom
leftMargin: -10
top: parent.top
right: parent.right
bottom: parent.bottom
}
}
}
+11
View File
@@ -1,6 +1,8 @@
import QtQuick 2.9
import QtQuick.Controls 2.2
import SdlGamepadKeyNavigation 1.0
// https://stackoverflow.com/questions/45029968/how-do-i-set-the-combobox-width-to-fit-the-largest-item
ComboBox {
property int textWidth
@@ -28,4 +30,13 @@ ComboBox {
textWidth = Math.max(popupMetrics.width, textWidth)
}
}
popup.onAboutToShow: {
// Switch to normal navigation for combo boxes
SdlGamepadKeyNavigation.setUiNavMode(false)
}
popup.onAboutToHide: {
SdlGamepadKeyNavigation.setUiNavMode(true)
}
}
+41
View File
@@ -0,0 +1,41 @@
import QtQuick 2.9
import QtQuick.Controls 2.2
GridView {
// Detect Qt 5.11 or earlier using presence of synchronousDrag.
// Prior to 5.12, the leftMargin and rightMargin values did not work.
property bool hasBrokenMargins: this.synchronousDrag === undefined
property int minMargin: 10
property real availableWidth: (parent.width - 2 * minMargin)
property int itemsPerRow: availableWidth / cellWidth
property real horizontalMargin: itemsPerRow < count && availableWidth >= cellWidth ?
(availableWidth % cellWidth) / 2 : minMargin
function updateMargins() {
leftMargin = horizontalMargin
rightMargin = horizontalMargin
if (hasBrokenMargins) {
anchors.leftMargin = leftMargin
anchors.rightMargin = rightMargin
}
}
onHorizontalMarginChanged: {
updateMargins()
}
Component.onCompleted: {
if (hasBrokenMargins) {
// This will cause an anchor conflict with the parent StackView
// which breaks animation, but otherwise the grid will not be
// centered in the window.
anchors.fill = parent
anchors.topMargin = topMargin
anchors.bottomMargin = bottomMargin
}
updateMargins()
}
}
+5 -18
View File
@@ -1,13 +1,10 @@
import QtQuick 2.0
import QtQuick.Controls 2.2
import QtQuick.Dialogs 1.2
import ComputerManager 1.0
import Session 1.0
Item {
anchors.fill: parent
function onSearchingComputer() {
stageLabel.text = "Establishing connection to PC..."
}
@@ -21,13 +18,8 @@ Item {
errorDialog.open()
}
// The StackView will trigger a visibility change when
// we're pushed onto it, causing our onVisibleChanged
// routine to run, but only if we start as invisible
visible: false
onVisibleChanged: {
if (visible && !launcher.isExecuted()) {
StackView.onActivated: {
if (!launcher.isExecuted()) {
toolBar.visible = false
launcher.searchingComputer.connect(onSearchingComputer)
launcher.quittingApp.connect(onQuittingApp)
@@ -55,16 +47,11 @@ Item {
}
}
MessageDialog {
ErrorMessageDialog {
id: errorDialog
modality:Qt.WindowModal
icon: StandardIcon.Critical
standardButtons: StandardButton.Ok
onVisibleChanged: {
if (!visible) {
Qt.quit()
}
onClosed: {
Qt.quit()
}
}
}
+9 -43
View File
@@ -1,13 +1,9 @@
import QtQml 2.2
import QtQuick 2.0
import QtQuick.Controls 2.2
import QtQuick.Dialogs 1.2
import ComputerManager 1.0
Item {
visible: false
function onSearchingComputer() {
stageLabel.text = "Establishing connection to PC..."
}
@@ -36,8 +32,8 @@ Item {
quitAppDialog.open()
}
onVisibleChanged: {
if (visible && !launcher.isExecuted()) {
StackView.onActivated: {
if (!launcher.isExecuted()) {
toolBar.visible = false
launcher.searchingComputer.connect(onSearchingComputer)
launcher.searchingApp.connect(onSearchingApp)
@@ -66,26 +62,18 @@ Item {
}
}
MessageDialog {
ErrorMessageDialog {
id: errorDialog
modality:Qt.WindowModal
icon: StandardIcon.Critical
standardButtons: StandardButton.Ok | StandardButton.Help
onHelp: {
Qt.openUrlExternally("https://github.com/moonlight-stream/moonlight-docs/wiki/Troubleshooting");
}
onVisibleChanged: {
if (!visible) {
Qt.quit();
}
onClosed: {
Qt.quit();
}
}
MessageDialog {
NavigableMessageDialog {
id: quitAppDialog
modality:Qt.WindowModal
text:"Are you sure you want to quit " + appName +"? Any unsaved progress will be lost."
standardButtons: StandardButton.Yes | StandardButton.No
standardButtons: Dialog.Yes | Dialog.No
property string appName : ""
function quitApp() {
@@ -96,29 +84,7 @@ Item {
launcher.quitRunningApp()
}
onYes: quitApp()
// For keyboard/gamepad navigation
onAccepted: quitApp()
// Exit process if app quit is rejected (reacts also to closing of the
// dialog from title bar's close button).
// Note: this depends on undocumented behavior of visibleChanged()
// signal being emitted before yes() or accepted() has been emitted.
onVisibleChanged: {
if (!visible) {
quitTimer.start()
}
}
Component.onCompleted: {
yes.connect(quitTimer.stop)
accepted.connect(quitTimer.stop)
}
}
Timer {
id: quitTimer
interval: 100
onTriggered: Qt.quit()
onRejected: Qt.quit()
}
}
+8
View File
@@ -0,0 +1,8 @@
import QtQuick 2.0
import QtQuick.Controls 2.2
import SystemProperties 1.0
NavigableMessageDialog {
standardButtons: Dialog.Ok | (SystemProperties.hasBrowser ? Dialog.Help : 0)
}
+17
View File
@@ -0,0 +1,17 @@
import QtQuick 2.0
import QtQuick.Controls 2.2
Dialog {
parent: overlay
x: Math.round((parent.width - width) / 2)
y: Math.round((parent.height - height) / 2)
onAboutToHide: {
// We must force focus back to the last item for platforms without
// support for more than one active window like Steam Link. If
// we don't, gamepad and keyboard navigation will break after a
// dialog appears.
stackView.forceActiveFocus()
}
}
+7
View File
@@ -11,6 +11,13 @@ MenuItem {
height: visible ? implicitHeight : 0
focusPolicy: visible ? Qt.TabFocus : Qt.NoFocus
onTriggered: {
// We must close the context menu first or
// it can steal focus from any dialogs that
// onTriggered may spawn.
parentMenu.close()
}
Keys.onReturnPressed: {
triggered()
}
+65
View File
@@ -0,0 +1,65 @@
import QtQuick 2.0
import QtQuick.Controls 2.2
import QtQuick.Layouts 1.2
NavigableDialog {
id: dialog
property alias text: dialogLabel.dialogText
property string helpText
property string helpUrl : "https://github.com/moonlight-stream/moonlight-docs/wiki/Troubleshooting"
property string helpTextSeparator : " "
onOpened: {
// Force keyboard focus on the label so keyboard navigation works
dialogLabel.forceActiveFocus()
}
RowLayout {
spacing: 10
Image {
source: (standardButtons & Dialog.Yes) ?
"qrc:/res/baseline-help_outline-24px.svg" :
"qrc:/res/baseline-error_outline-24px.svg"
sourceSize {
// The icon should be square so use the height as the width too
width: 50
height: 50
}
}
Label {
property string dialogText
id: dialogLabel
text: dialogText + ((helpText && (standardButtons & Dialog.Help)) ? (helpTextSeparator + helpText) : "")
wrapMode: Text.Wrap
elide: Label.ElideRight
// Cap the width so the dialog doesn't grow horizontally forever. This
// will cause word wrap to kick in.
Layout.maximumWidth: 400
Layout.maximumHeight: 400
Keys.onReturnPressed: {
accept()
}
Keys.onEscapePressed: {
reject()
}
}
}
footer: DialogButtonBox {
id: dialogButtonBox
standardButtons: dialog.standardButtons
onHelpRequested: {
Qt.openUrlExternally(helpUrl)
close()
}
}
}
+19
View File
@@ -2,8 +2,27 @@ import QtQuick 2.0
import QtQuick.Controls 2.2
ToolButton {
property string iconSource
activeFocusOnTab: true
// FIXME: We're using an Image here rather than icon.source because
// icons don't work on Qt 5.9 LTS.
Image {
id: image
source: iconSource
anchors.centerIn: parent.background
sourceSize {
width: parent.background.width * 1.10
height: parent.background.height * 1.10
}
}
// This determines the size of the Material highlight. We increase it
// from the default because we use larger than normal icons for TV readability.
background.width: (parent.height - parent.anchors.bottomMargin - parent.anchors.topMargin) * 0.60
background.height: (parent.height - parent.anchors.bottomMargin - parent.anchors.topMargin) * 0.60
Keys.onReturnPressed: {
clicked()
}
+88 -120
View File
@@ -1,8 +1,5 @@
import QtQuick 2.9
import QtQuick.Controls 2.2
import QtQuick.Dialogs 1.2
import QtQuick.Layouts 1.3
import QtQuick.Window 2.2
import ComputerModel 1.0
@@ -10,48 +7,41 @@ import ComputerManager 1.0
import StreamingPreferences 1.0
import SdlGamepadKeyNavigation 1.0
GridView {
CenteredGridView {
property ComputerModel computerModel : createModel()
id: pcGrid
focus: true
activeFocusOnTab: true
anchors.fill: parent
anchors.leftMargin: (parent.width % (cellWidth + anchors.rightMargin)) / 2
anchors.topMargin: 20
anchors.rightMargin: 5
anchors.bottomMargin: 5
cellWidth: 350; cellHeight: 350;
topMargin: 20
bottomMargin: 5
cellWidth: 310; cellHeight: 330;
objectName: "Computers"
// The StackView will trigger a visibility change when
// we're pushed onto it, causing our onVisibleChanged
// routine to run, but only if we start as invisible
visible: false
StreamingPreferences {
id: prefs
}
SdlGamepadKeyNavigation {
id: gamepadKeyNav
}
onVisibleChanged: {
if (visible) {
gamepadKeyNav.enable()
}
else {
gamepadKeyNav.disable()
}
}
Component.onCompleted: {
// Don't show any highlighted item until interacting with them.
// We do this here instead of onActivated to avoid losing the user's
// selection when backing out of a different page of the app.
currentIndex = -1
}
StackView.onActivated: {
// Setup signals on CM
ComputerManager.computerAddCompleted.connect(addComplete)
// Don't show any highlighted item until interacting with them
currentIndex = -1
// This is a bit of a hack to do this here as opposed to main.qml, but
// we need it enabled before calling getConnectedGamepads() and PcView
// is never destroyed, so it should be okay.
SdlGamepadKeyNavigation.enable()
// Highlight the first item if a gamepad is connected
if (currentIndex == -1 && SdlGamepadKeyNavigation.getConnectedGamepads() > 0) {
currentIndex = 0
}
}
StackView.onDeactivating: {
ComputerManager.computerAddCompleted.disconnect(addComplete)
}
function pairingComplete(error)
@@ -62,6 +52,7 @@ GridView {
// Display a failed dialog if we got an error
if (error !== undefined) {
errorDialog.text = error
errorDialog.helpText = ""
errorDialog.open()
}
}
@@ -69,7 +60,8 @@ GridView {
function addComplete(success)
{
if (!success) {
errorDialog.text = "Unable to connect to the specified PC. Click the Help button for possible solutions."
errorDialog.text = "Unable to connect to the specified PC."
errorDialog.helpText = "Click the Help button for possible solutions."
errorDialog.open()
}
}
@@ -82,18 +74,37 @@ GridView {
return model
}
Row {
anchors.centerIn: parent
spacing: 5
visible: pcGrid.count === 0
BusyIndicator {
id: searchSpinner
visible: StreamingPreferences.enableMdns
}
Label {
height: searchSpinner.height
elide: Label.ElideRight
text: StreamingPreferences.enableMdns ? "Searching for PCs with NVIDIA GameStream enabled..."
: "Automatic PC discovery is disabled. Add your PC manually."
font.pointSize: 20
verticalAlignment: Text.AlignVCenter
wrapMode: Text.Wrap
}
}
model: computerModel
delegate: NavigableItemDelegate {
width: 300; height: 300;
width: 300; height: 320;
grid: pcGrid
Image {
id: pcIcon
anchors.horizontalCenter: parent.horizontalCenter
source: {
model.addPc ? "qrc:/res/ic_add_to_queue_white_48px.svg" : "qrc:/res/ic_tv_white_48px.svg"
}
source: "qrc:/res/desktop_windows-48px.svg"
sourceSize {
width: 200
height: 200
@@ -105,8 +116,8 @@ GridView {
id: stateIcon
anchors.horizontalCenter: pcIcon.horizontalCenter
anchors.verticalCenter: pcIcon.verticalCenter
anchors.verticalCenterOffset: -10
visible: !model.addPc && !model.statusUnknown && (!model.online || !model.paired)
anchors.verticalCenterOffset: -15
visible: !model.statusUnknown && (!model.online || !model.paired)
source: !model.online ? "qrc:/res/baseline-warning-24px.svg" : "qrc:/res/baseline-lock-24px.svg"
sourceSize {
width: 75
@@ -118,10 +129,10 @@ GridView {
id: statusUnknownSpinner
anchors.horizontalCenter: pcIcon.horizontalCenter
anchors.verticalCenter: pcIcon.verticalCenter
anchors.verticalCenterOffset: -10
anchors.verticalCenterOffset: -15
width: 75
height: 75
visible: !model.addPc && model.statusUnknown
visible: model.statusUnknown
}
Label {
@@ -130,9 +141,11 @@ GridView {
width: parent.width
anchors.top: pcIcon.bottom
anchors.bottom: parent.bottom
font.pointSize: 36
horizontalAlignment: Text.AlignHCenter
wrapMode: Text.Wrap
elide: Text.ElideRight
}
NavigableMenu {
@@ -150,15 +163,12 @@ GridView {
parentMenu: pcContextMenu
text: "Wake PC"
onTriggered: computerModel.wakeComputer(index)
visible: !model.addPc && !model.online && model.wakeable
visible: !model.online && model.wakeable
}
}
onClicked: {
if (model.addPc) {
addPcDialog.open()
}
else if (model.online) {
if (model.online) {
if (model.paired) {
// go to game view
var component = Qt.createComponent("AppView.qml")
@@ -178,7 +188,8 @@ GridView {
}
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.text = "You cannot pair while a previous session is still running on the host PC. Quit any running games or reboot the host PC, then try pairing again."
errorDialog.helpText = ""
errorDialog.open()
}
}
@@ -189,15 +200,13 @@ GridView {
}
onPressAndHold: {
if (!model.addPc) {
// popup() ensures the menu appears under the mouse cursor
if (pcContextMenu.popup) {
pcContextMenu.popup()
}
else {
// Qt 5.9 doesn't have popup()
pcContextMenu.open()
}
// popup() ensures the menu appears under the mouse cursor
if (pcContextMenu.popup) {
pcContextMenu.popup()
}
else {
// Qt 5.9 doesn't have popup()
pcContextMenu.open()
}
}
@@ -210,99 +219,58 @@ GridView {
}
Keys.onMenuPressed: {
if (!model.addPc) {
// We must use open() here so the menu is positioned on
// the ItemDelegate and not where the mouse cursor is
pcContextMenu.open()
}
// We must use open() here so the menu is positioned on
// the ItemDelegate and not where the mouse cursor is
pcContextMenu.open()
}
}
MessageDialog {
ErrorMessageDialog {
id: errorDialog
// don't allow edits to the rest of the window while open
modality:Qt.WindowModal
icon: StandardIcon.Critical
standardButtons: StandardButton.Ok | StandardButton.Help
onHelp: {
// Using Setup-Guide here instead of Troubleshooting because it's likely that users
// will arrive here by forgetting to enable GameStream or not forwarding ports.
Qt.openUrlExternally("https://github.com/moonlight-stream/moonlight-docs/wiki/Setup-Guide");
}
// Using Setup-Guide here instead of Troubleshooting because it's likely that users
// will arrive here by forgetting to enable GameStream or not forwarding ports.
helpUrl: "https://github.com/moonlight-stream/moonlight-docs/wiki/Setup-Guide"
}
MessageDialog {
NavigableMessageDialog {
id: pairDialog
// Pairing dialog must be modal to prevent double-clicks from triggering
// pairing twice
modal: true
closePolicy: Popup.CloseOnEscape
// don't allow edits to the rest of the window while open
modality:Qt.WindowModal
property string pin : "0000"
text:"Please enter " + pin + " on your GameStream PC. This dialog will close when pairing is completed."
standardButtons: StandardButton.Cancel
standardButtons: Dialog.Cancel
onRejected: {
// FIXME: We should interrupt pairing here
}
}
MessageDialog {
NavigableMessageDialog {
id: deletePcDialog
// don't allow edits to the rest of the window while open
modality:Qt.WindowModal
property int pcIndex : -1;
text:"Are you sure you want to remove this PC?"
standardButtons: StandardButton.Yes | StandardButton.No
standardButtons: Dialog.Yes | Dialog.No
function deletePc() {
console.log("deleting PC pairing for PC at index: " + pcIndex)
computerModel.deleteComputer(pcIndex);
}
onYes: deletePc()
// For keyboard/gamepad activation
onAccepted: deletePc()
}
Dialog {
id: addPcDialog
property string label: "Enter the IP address of your GameStream PC"
property string hint: "192.168.1.100"
property alias editText : editTextItem
standardButtons: StandardButton.Ok | StandardButton.Cancel
onVisibleChanged: {
editTextItem.focus = true
editTextItem.selectAll()
}
onAccepted: {
ComputerManager.addNewHost(editText.text, false)
}
ColumnLayout {
Text {
id: addPcDialogLabel
text: addPcDialog.label
}
Rectangle {
implicitWidth: parent.parent.width
height: editTextItem.height
TextInput {
id: editTextItem
inputMethodHints: Qt.ImhPreferUppercase
text: addPcDialog.hint
}
}
}
}
ScrollBar.vertical: ScrollBar {
parent: pcGrid.parent
anchors {
top: pcGrid.top
left: pcGrid.right
bottom: pcGrid.bottom
leftMargin: -10
top: parent.top
right: parent.right
bottom: parent.bottom
}
}
}
+14 -26
View File
@@ -1,6 +1,5 @@
import QtQuick 2.0
import QtQuick.Controls 2.2
import QtQuick.Dialogs 1.2
import ComputerManager 1.0
import Session 1.0
@@ -12,13 +11,6 @@ Item {
property string stageText : "Quitting " + appName + "..."
anchors.fill: parent
// The StackView will trigger a visibility change when
// we're pushed onto it, causing our onVisibleChanged
// routine to run, but only if we start as invisible
visible: false
function quitAppCompleted(error)
{
// Display a failed dialog if we got an error
@@ -34,26 +26,25 @@ Item {
stackView.replace(segue)
}
else {
// Show the toolbar again
toolBar.visible = true
// Exit this view
stackView.pop()
}
}
onVisibleChanged: {
if (visible) {
// Hide the toolbar before we start loading
toolBar.visible = false
StackView.onActivated: {
// Hide the toolbar before we start loading
toolBar.visible = false
// Connect the quit completion signal
ComputerManager.quitAppCompleted.connect(quitAppCompleted)
}
else {
// Disconnect the signal
ComputerManager.quitAppCompleted.disconnect(quitAppCompleted)
}
// Connect the quit completion signal
ComputerManager.quitAppCompleted.connect(quitAppCompleted)
}
StackView.onDeactivating: {
// Show the toolbar again
toolBar.visible = true
// Disconnect the signal
ComputerManager.quitAppCompleted.disconnect(quitAppCompleted)
}
Row {
@@ -75,10 +66,7 @@ Item {
}
}
MessageDialog {
ErrorMessageDialog {
id: errorDialog
modality:Qt.WindowModal
icon: StandardIcon.Critical
standardButtons: StandardButton.Ok
}
}
+137 -83
View File
@@ -4,11 +4,11 @@ import QtQuick.Controls 2.2
import StreamingPreferences 1.0
import ComputerManager 1.0
import SdlGamepadKeyNavigation 1.0
import SystemProperties 1.0
Flickable {
id: settingsPage
objectName: "Settings"
anchors.fill: parent
contentWidth: settingsColumn1.width > settingsColumn2.width ? settingsColumn1.width : settingsColumn2.width
contentHeight: settingsColumn1.height > settingsColumn2.height ? settingsColumn1.height : settingsColumn2.height
@@ -24,31 +24,22 @@ Flickable {
}
}
StreamingPreferences {
id: prefs
}
StackView.onActivated: {
// This enables Tab and BackTab based navigation rather than arrow keys.
// It is required to shift focus between controls on the settings page.
SdlGamepadKeyNavigation.setUiNavMode(true)
// The StackView will trigger a visibility change when
// we're pushed onto it, causing our onVisibleChanged
// routine to run, but only if we start as invisible
visible: false
SdlGamepadKeyNavigation {
id: gamepadKeyNav
}
onVisibleChanged: {
if (visible) {
gamepadKeyNav.setSettingsMode(true)
gamepadKeyNav.enable()
}
else {
gamepadKeyNav.disable()
// Highlight the first item if a gamepad is connected
if (SdlGamepadKeyNavigation.getConnectedGamepads() > 0) {
resolutionComboBox.forceActiveFocus(Qt.TabFocus)
}
}
Component.onDestruction: {
prefs.save()
StackView.onDeactivating: {
SdlGamepadKeyNavigation.setUiNavMode(false)
// Save the prefs so the Session can observe the changes
StreamingPreferences.save()
}
Column {
@@ -98,10 +89,10 @@ Flickable {
// Some platforms have different desktop resolutions
// and native resolutions (like macOS with Retina displays)
if (displayResIndex == 0) {
screenRect = prefs.getDesktopResolution(displayIndex)
screenRect = SystemProperties.getDesktopResolution(displayIndex)
}
else {
screenRect = prefs.getNativeResolution(displayIndex)
screenRect = SystemProperties.getNativeResolution(displayIndex)
}
if (screenRect.width === 0) {
@@ -140,8 +131,8 @@ Flickable {
// 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
var saved_width = StreamingPreferences.width
var saved_height = StreamingPreferences.height
currentIndex = 0
for (var i = 0; i < resolutionListModel.count; i++) {
var el_width = parseInt(resolutionListModel.get(i).video_width);
@@ -190,12 +181,14 @@ Flickable {
var selectedHeight = parseInt(resolutionListModel.get(currentIndex).video_height)
// Only modify the bitrate if the values actually changed
if (prefs.width !== selectedWidth || prefs.height !== selectedHeight) {
prefs.width = selectedWidth
prefs.height = selectedHeight
if (StreamingPreferences.width !== selectedWidth || StreamingPreferences.height !== selectedHeight) {
StreamingPreferences.width = selectedWidth
StreamingPreferences.height = selectedHeight
prefs.bitrateKbps = prefs.getDefaultBitrate(prefs.width, prefs.height, prefs.fps);
slider.value = prefs.bitrateKbps
StreamingPreferences.bitrateKbps = StreamingPreferences.getDefaultBitrate(StreamingPreferences.width,
StreamingPreferences.height,
StreamingPreferences.fps);
slider.value = StreamingPreferences.bitrateKbps
}
}
}
@@ -204,15 +197,14 @@ Flickable {
function createModel() {
var fpsListModel = Qt.createQmlObject('import QtQuick 2.0; ListModel {}', parent, '')
// Get the max supported FPS on this system
var max_fps = prefs.getMaximumStreamingFrameRate();
var max_fps = SystemProperties.maximumStreamingFrameRate
// Default entries
fpsListModel.append({"text": "30 FPS", "video_fps": "30"})
fpsListModel.append({"text": "60 FPS", "video_fps": "60"})
// Add unsupported FPS values that come before the display max FPS
if (prefs.unsupportedFps) {
if (StreamingPreferences.unsupportedFps) {
if (max_fps > 90) {
fpsListModel.append({"text": "90 FPS (Unsupported)", "video_fps": "90"})
}
@@ -225,7 +217,7 @@ Flickable {
// handle wonky displays that report just over 60 Hz.
if (max_fps > 64) {
// Mark any FPS value greater than 120 as unsupported
if (prefs.unsupportedFps && max_fps > 120) {
if (StreamingPreferences.unsupportedFps && max_fps > 120) {
fpsListModel.append({"text": max_fps+" FPS (Unsupported)", "video_fps": ""+max_fps})
}
else if (max_fps > 120) {
@@ -237,7 +229,7 @@ Flickable {
}
// Add unsupported FPS values that come after the display max FPS
if (prefs.unsupportedFps) {
if (StreamingPreferences.unsupportedFps) {
if (max_fps < 90) {
fpsListModel.append({"text": "90 FPS (Unsupported)", "video_fps": "90"})
}
@@ -252,7 +244,7 @@ Flickable {
function reinitialize() {
model = createModel()
var saved_fps = prefs.fps
var saved_fps = StreamingPreferences.fps
currentIndex = 0
for (var i = 0; i < model.count; i++) {
var el_fps = parseInt(model.get(i).video_fps);
@@ -279,11 +271,13 @@ Flickable {
var selectedFps = parseInt(model.get(currentIndex).video_fps)
// Only modify the bitrate if the values actually changed
if (prefs.fps !== selectedFps) {
prefs.fps = selectedFps
if (StreamingPreferences.fps !== selectedFps) {
StreamingPreferences.fps = selectedFps
prefs.bitrateKbps = prefs.getDefaultBitrate(prefs.width, prefs.height, prefs.fps);
slider.value = prefs.bitrateKbps
StreamingPreferences.bitrateKbps = StreamingPreferences.getDefaultBitrate(StreamingPreferences.width,
StreamingPreferences.height,
StreamingPreferences.fps);
slider.value = StreamingPreferences.bitrateKbps
}
}
}
@@ -308,7 +302,7 @@ Flickable {
Slider {
id: slider
value: prefs.bitrateKbps
value: StreamingPreferences.bitrateKbps
stepSize: 500
from : 500
@@ -319,7 +313,7 @@ Flickable {
onValueChanged: {
bitrateTitle.text = "Video bitrate: " + (value / 1000.0) + " Mbps"
prefs.bitrateKbps = value
StreamingPreferences.bitrateKbps = value
}
}
@@ -334,7 +328,17 @@ Flickable {
AutoResizingComboBox {
// ignore setting the index at first, and actually set it when the component is loaded
Component.onCompleted: {
var savedWm = prefs.windowMode
// Set the recommended option based on the OS
for (var i = 0; i < windowModeListModel.count; i++) {
var thisWm = windowModeListModel.get(i).val;
if (thisWm === StreamingPreferences.recommendedFullScreenMode) {
windowModeListModel.get(i).text += " (Recommended)"
windowModeListModel.move(i, 0, 1);
break
}
}
var savedWm = StreamingPreferences.windowMode
currentIndex = 0
for (var i = 0; i < windowModeListModel.count; i++) {
var thisWm = windowModeListModel.get(i).val;
@@ -352,7 +356,7 @@ Flickable {
model: ListModel {
id: windowModeListModel
ListElement {
text: "Full-screen (Recommended)"
text: "Full-screen"
val: StreamingPreferences.WM_FULLSCREEN
}
ListElement {
@@ -365,7 +369,7 @@ Flickable {
}
}
onActivated: {
prefs.windowMode = windowModeListModel.get(currentIndex).val
StreamingPreferences.windowMode = windowModeListModel.get(currentIndex).val
}
ToolTip.delay: 1000
@@ -379,9 +383,9 @@ Flickable {
hoverEnabled: true
text: "V-Sync"
font.pointSize: 12
checked: prefs.enableVsync
checked: StreamingPreferences.enableVsync
onCheckedChanged: {
prefs.enableVsync = checked
StreamingPreferences.enableVsync = checked
}
ToolTip.delay: 1000
@@ -395,10 +399,10 @@ Flickable {
hoverEnabled: true
text: "Frame pacing"
font.pointSize: 12
enabled: prefs.enableVsync
checked: prefs.enableVsync && prefs.framePacing
enabled: StreamingPreferences.enableVsync
checked: StreamingPreferences.enableVsync && StreamingPreferences.framePacing
onCheckedChanged: {
prefs.framePacing = checked
StreamingPreferences.framePacing = checked
}
ToolTip.delay: 1000
ToolTip.timeout: 5000
@@ -431,7 +435,7 @@ Flickable {
AutoResizingComboBox {
// ignore setting the index at first, and actually set it when the component is loaded
Component.onCompleted: {
var saved_audio = prefs.audioConfig
var saved_audio = StreamingPreferences.audioConfig
currentIndex = 0
for (var i = 0; i < audioListModel.count; i++) {
var el_audio = audioListModel.get(i).val;
@@ -458,7 +462,7 @@ Flickable {
}
// ::onActivated must be used, as it only listens for when the index is changed by a human
onActivated : {
prefs.audioConfig = audioListModel.get(currentIndex).val
StreamingPreferences.audioConfig = audioListModel.get(currentIndex).val
}
}
@@ -480,11 +484,37 @@ Flickable {
id: startMaximizedCheck
text: "Maximize Moonlight window on startup"
font.pointSize: 12
checked: !prefs.startWindowed
checked: !StreamingPreferences.startWindowed
onCheckedChanged: {
prefs.startWindowed = !checked
StreamingPreferences.startWindowed = !checked
}
}
CheckBox {
id: connectionWarningsCheck
text: "Show connection quality warnings"
font.pointSize: 12
checked: StreamingPreferences.connectionWarnings
onCheckedChanged: {
StreamingPreferences.connectionWarnings = checked
}
}
CheckBox {
visible: SystemProperties.hasDiscordIntegration
id: discordPresenceCheck
text: "Discord Rich Presence integration"
font.pointSize: 12
checked: StreamingPreferences.richPresence
onCheckedChanged: {
StreamingPreferences.richPresence = checked
}
ToolTip.delay: 1000
ToolTip.timeout: 5000
ToolTip.visible: hovered
ToolTip.text: "Updates your Discord status to display the name of the game you're streaming."
}
}
}
}
@@ -510,9 +540,9 @@ Flickable {
id: singleControllerCheck
text: "Force gamepad #1 always present"
font.pointSize: 12
checked: !prefs.multiController
checked: !StreamingPreferences.multiController
onCheckedChanged: {
prefs.multiController = !checked
StreamingPreferences.multiController = !checked
}
ToolTip.delay: 1000
@@ -527,9 +557,9 @@ Flickable {
hoverEnabled: true
text: "Raw mouse input"
font.pointSize: 12
checked: !prefs.mouseAcceleration
checked: !StreamingPreferences.mouseAcceleration
onCheckedChanged: {
prefs.mouseAcceleration = !checked
StreamingPreferences.mouseAcceleration = !checked
}
ToolTip.delay: 1000
@@ -537,6 +567,22 @@ Flickable {
ToolTip.visible: hovered
ToolTip.text: "When checked, mouse input is not accelerated or scaled by the OS before passing to Moonlight"
}
CheckBox {
id: gamepadMouseCheck
hoverEnabled: true
text: "Gamepad mouse mode support"
font.pointSize: 12
checked: StreamingPreferences.gamepadMouse
onCheckedChanged: {
StreamingPreferences.gamepadMouse = checked
}
ToolTip.delay: 1000
ToolTip.timeout: 3000
ToolTip.visible: hovered
ToolTip.text: "When enabled, holding the Start button will toggle mouse mode"
}
}
}
@@ -555,9 +601,9 @@ Flickable {
id: optimizeGameSettingsCheck
text: "Optimize game settings"
font.pointSize: 12
checked: prefs.gameOptimizations
checked: StreamingPreferences.gameOptimizations
onCheckedChanged: {
prefs.gameOptimizations = checked
StreamingPreferences.gameOptimizations = checked
}
}
@@ -565,9 +611,9 @@ Flickable {
id: audioPcCheck
text: "Play audio on host PC"
font.pointSize: 12
checked: prefs.playAudioOnHost
checked: StreamingPreferences.playAudioOnHost
onCheckedChanged: {
prefs.playAudioOnHost = checked
StreamingPreferences.playAudioOnHost = checked
}
}
}
@@ -595,7 +641,7 @@ Flickable {
AutoResizingComboBox {
// ignore setting the index at first, and actually set it when the component is loaded
Component.onCompleted: {
var saved_vds = prefs.videoDecoderSelection
var saved_vds = StreamingPreferences.videoDecoderSelection
currentIndex = 0
for (var i = 0; i < decoderListModel.count; i++) {
var el_vds = decoderListModel.get(i).val;
@@ -626,7 +672,7 @@ Flickable {
}
// ::onActivated must be used, as it only listens for when the index is changed by a human
onActivated : {
prefs.videoDecoderSelection = decoderListModel.get(currentIndex).val
StreamingPreferences.videoDecoderSelection = decoderListModel.get(currentIndex).val
}
}
@@ -641,7 +687,7 @@ Flickable {
AutoResizingComboBox {
// ignore setting the index at first, and actually set it when the component is loaded
Component.onCompleted: {
var saved_vcc = prefs.videoCodecConfig
var saved_vcc = StreamingPreferences.videoCodecConfig
currentIndex = 0
for(var i = 0; i < codecListModel.count; i++) {
var el_vcc = codecListModel.get(i).val;
@@ -679,7 +725,7 @@ Flickable {
}
// ::onActivated must be used, as it only listens for when the index is changed by a human
onActivated : {
prefs.videoCodecConfig = codecListModel.get(currentIndex).val
StreamingPreferences.videoCodecConfig = codecListModel.get(currentIndex).val
}
}
@@ -687,13 +733,17 @@ Flickable {
id: unlockUnsupportedFps
text: "Unlock unsupported FPS options"
font.pointSize: 12
checked: prefs.unsupportedFps
checked: StreamingPreferences.unsupportedFps
onCheckedChanged: {
prefs.unsupportedFps = checked
// This is called on init, so only do the work if we've
// actually changed the value.
if (StreamingPreferences.unsupportedFps != checked) {
StreamingPreferences.unsupportedFps = checked
// The selectable FPS values depend on whether
// this option is enabled or not
fpsComboBox.reinitialize()
// The selectable FPS values depend on whether
// this option is enabled or not
fpsComboBox.reinitialize()
}
}
}
@@ -701,18 +751,22 @@ Flickable {
id: enableMdns
text: "Automatically find PCs on the local network (Recommended)"
font.pointSize: 12
checked: prefs.enableMdns
checked: StreamingPreferences.enableMdns
onCheckedChanged: {
prefs.enableMdns = checked
// This is called on init, so only do the work if we've
// actually changed the value.
if (StreamingPreferences.enableMdns != checked) {
StreamingPreferences.enableMdns = checked
// We must save the updated preference to ensure
// ComputerManager can observe the change internally.
prefs.save()
// We must save the updated preference to ensure
// ComputerManager can observe the change internally.
StreamingPreferences.save()
// Restart polling so the mDNS change takes effect
if (window.pollingActive) {
ComputerManager.stopPollingAsync()
ComputerManager.startPolling()
// Restart polling so the mDNS change takes effect
if (window.pollingActive) {
ComputerManager.stopPollingAsync()
ComputerManager.startPolling()
}
}
}
}
@@ -721,9 +775,9 @@ Flickable {
id: quitAppAfter
text: "Quit app after quitting session"
font.pointSize: 12
checked: prefs.quitAppAfter
checked: StreamingPreferences.quitAppAfter
onCheckedChanged: {
prefs.quitAppAfter = checked
StreamingPreferences.quitAppAfter = checked
}
}
}
+47 -60
View File
@@ -1,9 +1,7 @@
import QtQuick 2.0
import QtQuick.Controls 2.2
import QtQuick.Dialogs 1.2
import QtQuick.Window 2.2
import ComputerManager 1.0
import SdlGamepadKeyNavigation 1.0
import Session 1.0
@@ -13,13 +11,6 @@ Item {
property string stageText : "Starting " + appName + "..."
property bool quitAfter : false
anchors.fill: parent
// The StackView will trigger a visibility change when
// we're pushed onto it, causing our onVisibleChanged
// routine to run, but only if we start as invisible
visible: false
function stageStarting(stage)
{
// Update the spinner text
@@ -28,8 +19,8 @@ Item {
function stageFailed(stage, errorCode)
{
errorDialog.text = "Starting " + stage + " failed: Error " + errorCode
errorDialog.open()
// Display the error dialog after Session::exec() returns
streamSegueErrorDialog.text = "Starting " + stage + " failed: Error " + errorCode
}
function connectionStarted()
@@ -38,6 +29,7 @@ Item {
// see them briefly when we pop off the StackView
stageSpinner.visible = false
stageLabel.visible = false
hintText.visible = false
// Hide the window now that streaming has begun
window.visible = false
@@ -46,7 +38,7 @@ Item {
function displayLaunchError(text)
{
// Display the error dialog after Session::exec() returns
errorDialog.text = text
streamSegueErrorDialog.text = text
}
function displayLaunchWarning(text)
@@ -54,7 +46,7 @@ Item {
// This toast appears for 3 seconds, just shorter than how long
// Session will wait for it to be displayed. This gives it time
// to transition to invisible before continuing.
var toast = Qt.createQmlObject('import QtQuick.Controls 2.3; ToolTip {}', parent, '')
var toast = Qt.createQmlObject('import QtQuick.Controls 2.2; ToolTip {}', parent, '')
toast.text = text
toast.timeout = 3000
toast.visible = true
@@ -72,10 +64,14 @@ Item {
function sessionFinished()
{
// Enable GUI gamepad usage now
SdlGamepadKeyNavigation.enable()
if (quitAfter) {
if (errorDialog.text) {
if (streamSegueErrorDialog.text) {
// Quit when the error dialog is acknowledged
errorDialog.open()
streamSegueErrorDialog.quitAfter = quitAfter
streamSegueErrorDialog.open()
}
else {
// Quit immediately
@@ -92,48 +88,60 @@ Item {
// the Qt UI is visible again to prevent losing
// focus on the dialog which would impact gamepad
// users.
if (errorDialog.text) {
errorDialog.open()
if (streamSegueErrorDialog.text) {
streamSegueErrorDialog.quitAfter = quitAfter
streamSegueErrorDialog.open()
}
}
}
// It's important that we don't call enable() here
// or it may interfere with the Session instance
// getting notified of initial connected gamepads.
SdlGamepadKeyNavigation {
id: gamepadKeyNav
StackView.onDeactivating: {
// Show the toolbar again when popped off the stack
toolBar.visible = true
// Enable GUI gamepad usage now
SdlGamepadKeyNavigation.enable()
}
onVisibleChanged: {
if (visible) {
// Hide the toolbar before we start loading
toolBar.visible = false
StackView.onActivated: {
// Hide the toolbar before we start loading
toolBar.visible = false
// Hook up our signals
session.stageStarting.connect(stageStarting)
session.stageFailed.connect(stageFailed)
session.connectionStarted.connect(connectionStarted)
session.displayLaunchError.connect(displayLaunchError)
session.displayLaunchWarning.connect(displayLaunchWarning)
session.quitStarting.connect(quitStarting)
session.sessionFinished.connect(sessionFinished)
// Kick off the stream
streamLoader.active = true
}
Loader {
id: streamLoader
active: false
asynchronous: true
onLoaded: {
// Set the hint text. We do this here rather than
// in the hintText control itself to synchronize
// with Session.exec() which requires no concurrent
// gamepad usage.
hintText.text = gamepadKeyNav.getConnectedGamepads() > 0 ?
hintText.text = SdlGamepadKeyNavigation.getConnectedGamepads() > 0 ?
"Tip: Press Start+Select+L1+R1 to disconnect your session" :
"Tip: Press Ctrl+Alt+Shift+Q to disconnect your session"
// Hook up our signals
session.stageStarting.connect(stageStarting)
session.stageFailed.connect(stageFailed)
session.connectionStarted.connect(connectionStarted)
session.displayLaunchError.connect(displayLaunchError)
session.displayLaunchWarning.connect(displayLaunchWarning)
session.quitStarting.connect(quitStarting)
session.sessionFinished.connect(sessionFinished)
// Stop GUI gamepad usage now
SdlGamepadKeyNavigation.disable()
// Run the streaming session to completion
session.exec(Screen.virtualX, Screen.virtualY)
}
else if (!quitAfter) {
// Show the toolbar again when we become hidden
toolBar.visible = true
}
sourceComponent: Item {}
}
Row {
@@ -165,25 +173,4 @@ Item {
wrapMode: Text.Wrap
}
MessageDialog {
id: errorDialog
modality:Qt.WindowModal
icon: StandardIcon.Critical
standardButtons: StandardButton.Ok | StandardButton.Help
onAccepted: {
if (quitAfter) {
Qt.quit()
}
}
onHelp: {
Qt.openUrlExternally("https://github.com/moonlight-stream/moonlight-docs/wiki/Troubleshooting");
if (quitAfter) {
Qt.quit()
}
}
}
}
+1 -17
View File
@@ -22,18 +22,6 @@ QVariant ComputerModel::data(const QModelIndex& index, int role) const
return QVariant();
}
if (index.row() == m_Computers.count()) {
// We insert a synthetic item at the end for the Add PC option
switch (role) {
case NameRole:
return "Add PC";
case AddPcRole:
return true;
default:
return QVariant();
}
}
Q_ASSERT(index.row() < m_Computers.count());
NvComputer* computer = m_Computers[index.row()];
@@ -50,8 +38,6 @@ QVariant ComputerModel::data(const QModelIndex& index, int role) const
return computer->currentGameId != 0;
case WakeableRole:
return !computer->macAddress.isEmpty();
case AddPcRole:
return false;
case StatusUnknownRole:
return computer->state == NvComputer::CS_UNKNOWN;
default:
@@ -67,8 +53,7 @@ int ComputerModel::rowCount(const QModelIndex& parent) const
return 0;
}
// Add PC placeholder counts as 1
return m_Computers.count() + 1;
return m_Computers.count();
}
QHash<int, QByteArray> ComputerModel::roleNames() const
@@ -79,7 +64,6 @@ QHash<int, QByteArray> ComputerModel::roleNames() const
names[OnlineRole] = "online";
names[PairedRole] = "paired";
names[BusyRole] = "busy";
names[AddPcRole] = "addPc";
names[WakeableRole] = "wakeable";
names[StatusUnknownRole] = "statusUnknown";
+1 -2
View File
@@ -14,8 +14,7 @@ class ComputerModel : public QAbstractListModel
PairedRole,
BusyRole,
WakeableRole,
StatusUnknownRole,
AddPcRole
StatusUnknownRole
};
public:
+133 -102
View File
@@ -1,12 +1,13 @@
import QtQuick 2.9
import QtQuick.Controls 2.2
import QtQuick.Dialogs 1.2
import QtQuick.Layouts 1.3
import QtQuick.Window 2.2
import ComputerManager 1.0
import AutoUpdateChecker 1.0
import StreamingPreferences 1.0
import SystemProperties 1.0
import SdlGamepadKeyNavigation 1.0
ApplicationWindow {
property bool pollingActive: false
@@ -16,11 +17,7 @@ ApplicationWindow {
width: 1280
height: 600
visibility: prefs.startWindowed ? "Windowed" : "Maximized"
StreamingPreferences {
id: prefs
}
visibility: StreamingPreferences.startWindowed ? "Windowed" : "Maximized"
StackView {
id: stackView
@@ -137,19 +134,20 @@ ApplicationWindow {
// onAfterRendering call and potentially reenter this code.
initialized = true;
if (prefs.isRunningWayland()) {
waylandDialog.open()
}
else if (prefs.isWow64()) {
if (SystemProperties.isWow64) {
wow64Dialog.open()
}
else if (!prefs.hasAnyHardwareAcceleration()) {
noHwDecoderDialog.open()
else if (!SystemProperties.hasHardwareAcceleration) {
if (SystemProperties.isRunningXWayland) {
xWaylandDialog.open()
}
else {
noHwDecoderDialog.open()
}
}
var unmappedGamepads = prefs.getUnmappedGamepads()
if (unmappedGamepads) {
unmappedGamepadDialog.unmappedGamepads = unmappedGamepads
if (SystemProperties.unmappedGamepads) {
unmappedGamepadDialog.unmappedGamepads = SystemProperties.unmappedGamepads
unmappedGamepadDialog.open()
}
}
@@ -173,28 +171,21 @@ ApplicationWindow {
header: ToolBar {
id: toolBar
height: 60
anchors.topMargin: 5
anchors.bottomMargin: 5
RowLayout {
spacing: 20
anchors.leftMargin: 10
anchors.rightMargin: 10
anchors.fill: parent
NavigableToolButton {
// Only make the button visible if the user has navigated somewhere.
visible: stackView.depth > 1
// FIXME: We're using an Image here rather than icon.source because
// icons don't work on Qt 5.9 LTS.
Image {
source: "qrc:/res/arrow_left.svg"
anchors.centerIn: parent
sourceSize {
// The icon should be square so use the height as the width too
width: toolBar.height - toolBar.anchors.bottomMargin - toolBar.anchors.topMargin
height: toolBar.height - toolBar.anchors.bottomMargin - toolBar.anchors.topMargin
}
}
iconSource: "qrc:/res/arrow_left.svg"
onClicked: stackView.pop()
@@ -214,28 +205,51 @@ ApplicationWindow {
}
NavigableToolButton {
property string browserUrl: ""
id: addPcButton
visible: stackView.currentItem.objectName === "Computers"
id: updateButton
Image {
source: "qrc:/res/update.svg"
anchors.centerIn: parent
sourceSize {
width: toolBar.height - toolBar.anchors.bottomMargin - toolBar.anchors.topMargin
height: toolBar.height - toolBar.anchors.bottomMargin - toolBar.anchors.topMargin
}
}
iconSource: "qrc:/res/ic_add_to_queue_white_48px.svg"
ToolTip.delay: 1000
ToolTip.timeout: 3000
ToolTip.visible: hovered
ToolTip.text: "Add PC manually" + (newPcShortcut.nativeText ? (" ("+newPcShortcut.nativeText+")") : "")
Shortcut {
id: newPcShortcut
sequence: StandardKey.New
onActivated: addPcButton.clicked()
}
onClicked: {
addPcDialog.open()
}
Keys.onDownPressed: {
stackView.currentItem.forceActiveFocus(Qt.TabFocus)
}
}
NavigableToolButton {
property string browserUrl: ""
id: updateButton
iconSource: "qrc:/res/update.svg"
ToolTip.delay: 1000
ToolTip.timeout: 3000
ToolTip.visible: hovered || visible
// Invisible until we get a callback notifying us that
// an update is available
visible: false
onClicked: Qt.openUrlExternally(browserUrl);
onClicked: {
if (SystemProperties.hasBrowser) {
Qt.openUrlExternally(browserUrl);
}
}
function updateAvailable(version, url)
{
@@ -256,15 +270,9 @@ ApplicationWindow {
NavigableToolButton {
id: helpButton
visible: SystemProperties.hasBrowser
Image {
source: "qrc:/res/question_mark.svg"
anchors.centerIn: parent
sourceSize {
width: toolBar.height - toolBar.anchors.bottomMargin - toolBar.anchors.topMargin
height: toolBar.height - toolBar.anchors.bottomMargin - toolBar.anchors.topMargin
}
}
iconSource: "qrc:/res/question_mark.svg"
ToolTip.delay: 1000
ToolTip.timeout: 3000
@@ -294,14 +302,7 @@ ApplicationWindow {
ToolTip.visible: hovered
ToolTip.text: "Gamepad Mapper"
Image {
source: "qrc:/res/ic_videogame_asset_white_48px.svg"
anchors.centerIn: parent
sourceSize {
width: toolBar.height - toolBar.anchors.bottomMargin - toolBar.anchors.topMargin
height: toolBar.height - toolBar.anchors.bottomMargin - toolBar.anchors.topMargin
}
}
iconSource: "qrc:/res/ic_videogame_asset_white_48px.svg"
onClicked: navigateTo("qrc:/gui/GamepadMapper.qml", "Gamepad Mapping")
@@ -313,14 +314,7 @@ ApplicationWindow {
NavigableToolButton {
id: settingsButton
Image {
source: "qrc:/res/settings.svg"
anchors.centerIn: parent
sourceSize {
width: toolBar.height - toolBar.anchors.bottomMargin - toolBar.anchors.topMargin
height: toolBar.height - toolBar.anchors.bottomMargin - toolBar.anchors.topMargin
}
}
iconSource: "qrc:/res/settings.svg"
onClicked: navigateTo("qrc:/gui/SettingsView.qml", "Settings")
@@ -342,67 +336,104 @@ ApplicationWindow {
}
}
MessageDialog {
ErrorMessageDialog {
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");
}
"Your streaming performance may be severely degraded in this configuration."
helpText: "Click the Help button for more information on solving this problem."
helpUrl: "https://github.com/moonlight-stream/moonlight-docs/wiki/Fixing-Hardware-Decoding-Problems"
}
MessageDialog {
id: waylandDialog
modality:Qt.WindowModal
icon: StandardIcon.Warning
standardButtons: StandardButton.Ok | StandardButton.Help
text: "Moonlight does not support hardware acceleration on Wayland. Continuing on Wayland may result in poor streaming performance. " +
"Please switch to an X session for optimal performance."
onHelp: {
Qt.openUrlExternally("https://github.com/moonlight-stream/moonlight-docs/wiki/Fixing-Hardware-Decoding-Problems");
}
ErrorMessageDialog {
id: xWaylandDialog
text: "Hardware acceleration doesn't work on XWayland. Continuing on XWayland may result in poor streaming performance. " +
"Try running with QT_QPA_PLATFORM=wayland or switch to X11."
helpText: "Click the Help button for more information."
helpUrl: "https://github.com/moonlight-stream/moonlight-docs/wiki/Fixing-Hardware-Decoding-Problems"
}
MessageDialog {
NavigableMessageDialog {
id: wow64Dialog
modality:Qt.WindowModal
icon: StandardIcon.Warning
standardButtons: StandardButton.Ok | StandardButton.Cancel
standardButtons: Dialog.Ok | Dialog.Cancel
text: "This PC is running a 64-bit version of Windows. Please download the x64 version of Moonlight for the best streaming performance."
onAccepted: {
Qt.openUrlExternally("https://github.com/moonlight-stream/moonlight-qt/releases");
}
}
MessageDialog {
ErrorMessageDialog {
id: unmappedGamepadDialog
property string unmappedGamepads : ""
modality:Qt.WindowModal
icon: StandardIcon.Warning
standardButtons: StandardButton.Ok | StandardButton.Help
text: "Moonlight detected gamepads without a proper mapping. " +
"The following gamepads will not function until this is resolved: " + unmappedGamepads + "\n\n" +
"Click the Help button for information on how to map your gamepads."
onHelp: {
Qt.openUrlExternally("https://github.com/moonlight-stream/moonlight-docs/wiki/Gamepad-Mapping");
}
text: "Moonlight detected gamepads without a mapping:\n" + unmappedGamepads
helpTextSeparator: "\n\n"
helpText: "Click the Help button for information on how to map your gamepads."
helpUrl: "https://github.com/moonlight-stream/moonlight-docs/wiki/Gamepad-Mapping"
}
// This dialog appears when quitting via keyboard or gamepad button
MessageDialog {
NavigableMessageDialog {
id: quitConfirmationDialog
modality:Qt.WindowModal
icon: StandardIcon.Warning
standardButtons: StandardButton.Yes | StandardButton.No
standardButtons: Dialog.Yes | Dialog.No
text: "Are you sure you want to quit?"
onYes: Qt.quit()
// For keyboard/gamepad navigation
onAccepted: Qt.quit()
}
// HACK: This belongs in StreamSegue but keeping a dialog around after the parent
// dies can trigger bugs in Qt 5.12 that cause the app to crash. For now, we will
// host this dialog in a QML component that is never destroyed.
//
// To repro: Start a stream, cut the network connection to trigger the "Connection
// terminated" dialog, wait until the app grid times out back to the PC grid, then
// try to dismiss the dialog.
ErrorMessageDialog {
id: streamSegueErrorDialog
property bool quitAfter: false
onClosed: {
if (quitAfter) {
Qt.quit()
}
// StreamSegue assumes its dialog will be re-created each time we
// start streaming, so fake it by wiping out the text each time.
text = ""
}
}
NavigableDialog {
id: addPcDialog
property string label: "Enter the IP address of your GameStream PC:"
standardButtons: Dialog.Ok | Dialog.Cancel
onOpened: {
// Force keyboard focus on the textbox so keyboard navigation works
editText.forceActiveFocus()
}
onAccepted: {
if (editText.text) {
ComputerManager.addNewHost(editText.text, false)
}
}
ColumnLayout {
Label {
text: addPcDialog.label
font.bold: true
}
TextField {
id: editText
Layout.fillWidth: true
focus: true
Keys.onReturnPressed: {
addPcDialog.accept()
}
}
}
}
}
+15 -48
View File
@@ -10,7 +10,7 @@
SdlGamepadKeyNavigation::SdlGamepadKeyNavigation()
: m_Enabled(false),
m_SettingsMode(false),
m_UiNavMode(false),
m_LastAxisNavigationEventTime(0)
{
m_PollingTimer = new QTimer(this);
@@ -91,6 +91,12 @@ void SdlGamepadKeyNavigation::onPollingTimerFired()
while (SDL_PollEvent(&event)) {
switch (event.type) {
case SDL_QUIT:
// SDL may send us a quit event since we initialize
// the video subsystem on startup. If we get one,
// forward it on for Qt to take care of.
QCoreApplication::instance()->quit();
break;
case SDL_CONTROLLERBUTTONDOWN:
case SDL_CONTROLLERBUTTONUP:
{
@@ -100,7 +106,7 @@ void SdlGamepadKeyNavigation::onPollingTimerFired()
switch (event.cbutton.button) {
case SDL_CONTROLLER_BUTTON_DPAD_UP:
if (m_SettingsMode) {
if (m_UiNavMode) {
// Back-tab
sendKey(type, Qt::Key_Tab, Qt::ShiftModifier);
}
@@ -109,7 +115,7 @@ void SdlGamepadKeyNavigation::onPollingTimerFired()
}
break;
case SDL_CONTROLLER_BUTTON_DPAD_DOWN:
if (m_SettingsMode) {
if (m_UiNavMode) {
sendKey(type, Qt::Key_Tab);
}
else {
@@ -118,24 +124,12 @@ void SdlGamepadKeyNavigation::onPollingTimerFired()
break;
case SDL_CONTROLLER_BUTTON_DPAD_LEFT:
sendKey(type, Qt::Key_Left);
if (m_SettingsMode) {
// Some settings controls respond to left/right (like the slider)
// and others respond to up/down (like combo boxes). They seem to
// be mutually exclusive though so let's just send both.
sendKey(type, Qt::Key_Up);
}
break;
case SDL_CONTROLLER_BUTTON_DPAD_RIGHT:
sendKey(type, Qt::Key_Right);
if (m_SettingsMode) {
// Some settings controls respond to left/right (like the slider)
// and others respond to up/down (like combo boxes). They seem to
// be mutually exclusive though so let's just send both.
sendKey(type, Qt::Key_Down);
}
break;
case SDL_CONTROLLER_BUTTON_A:
if (m_SettingsMode) {
if (m_UiNavMode) {
sendKey(type, Qt::Key_Space);
}
else {
@@ -177,7 +171,7 @@ void SdlGamepadKeyNavigation::onPollingTimerFired()
// Do nothing
}
else if (leftY < -30000) {
if (m_SettingsMode) {
if (m_UiNavMode) {
// Back-tab
sendKey(QEvent::Type::KeyPress, Qt::Key_Tab, Qt::ShiftModifier);
sendKey(QEvent::Type::KeyRelease, Qt::Key_Tab, Qt::ShiftModifier);
@@ -190,7 +184,7 @@ void SdlGamepadKeyNavigation::onPollingTimerFired()
m_LastAxisNavigationEventTime = SDL_GetTicks();
}
else if (leftY > 30000) {
if (m_SettingsMode) {
if (m_UiNavMode) {
sendKey(QEvent::Type::KeyPress, Qt::Key_Tab);
sendKey(QEvent::Type::KeyRelease, Qt::Key_Tab);
}
@@ -204,27 +198,11 @@ void SdlGamepadKeyNavigation::onPollingTimerFired()
else if (leftX < -30000) {
sendKey(QEvent::Type::KeyPress, Qt::Key_Left);
sendKey(QEvent::Type::KeyRelease, Qt::Key_Left);
if (m_SettingsMode) {
// Some settings controls respond to left/right (like the slider)
// and others respond to up/down (like combo boxes). They seem to
// be mutually exclusive though so let's just send both.
sendKey(QEvent::Type::KeyPress, Qt::Key_Up);
sendKey(QEvent::Type::KeyRelease, Qt::Key_Up);
}
m_LastAxisNavigationEventTime = SDL_GetTicks();
}
else if (leftX > 30000) {
sendKey(QEvent::Type::KeyPress, Qt::Key_Right);
sendKey(QEvent::Type::KeyRelease, Qt::Key_Right);
if (m_SettingsMode) {
// Some settings controls respond to left/right (like the slider)
// and others respond to up/down (like combo boxes). They seem to
// be mutually exclusive though so let's just send both.
sendKey(QEvent::Type::KeyPress, Qt::Key_Down);
sendKey(QEvent::Type::KeyRelease, Qt::Key_Down);
}
m_LastAxisNavigationEventTime = SDL_GetTicks();
}
}
@@ -240,24 +218,14 @@ void SdlGamepadKeyNavigation::sendKey(QEvent::Type type, Qt::Key key, Qt::Keyboa
}
}
void SdlGamepadKeyNavigation::setSettingsMode(bool settingsMode)
void SdlGamepadKeyNavigation::setUiNavMode(bool uiNavMode)
{
m_SettingsMode = settingsMode;
m_UiNavMode = uiNavMode;
}
int SdlGamepadKeyNavigation::getConnectedGamepads()
{
if (SDL_InitSubSystem(SDL_INIT_GAMECONTROLLER) != 0) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"SDL_InitSubSystem(SDL_INIT_GAMECONTROLLER) failed: %s",
SDL_GetError());
return 0;
}
// Applying mappings is necessary to ensure gamepad without
// a built-in mapping are properly counted.
MappingManager mappingManager;
mappingManager.applyMappings();
Q_ASSERT(m_Enabled);
int count = 0;
for (int i = 0; i < SDL_NumJoysticks(); i++) {
@@ -266,6 +234,5 @@ int SdlGamepadKeyNavigation::getConnectedGamepads()
}
}
SDL_QuitSubSystem(SDL_INIT_GAMECONTROLLER);
return count;
}
+2 -2
View File
@@ -18,7 +18,7 @@ public:
Q_INVOKABLE void disable();
Q_INVOKABLE void setSettingsMode(bool settingsMode);
Q_INVOKABLE void setUiNavMode(bool settingsMode);
Q_INVOKABLE int getConnectedGamepads();
@@ -32,6 +32,6 @@ private:
QTimer* m_PollingTimer;
QList<SDL_GameController*> m_Gamepads;
bool m_Enabled;
bool m_SettingsMode;
bool m_UiNavMode;
Uint32 m_LastAxisNavigationEventTime;
};
+119 -36
View File
@@ -6,6 +6,10 @@
#include <QMutex>
#include <QtDebug>
#include <QNetworkProxyFactory>
#include <QPalette>
#include <QFont>
#include <QCursor>
#include <QElapsedTimer>
// Don't let SDL hook our main function, since Qt is already
// doing the same thing. This needs to be before any headers
@@ -28,6 +32,7 @@
#include "gui/computermodel.h"
#include "gui/appmodel.h"
#include "backend/autoupdatechecker.h"
#include "backend/systemproperties.h"
#include "streaming/session.h"
#include "settings/streamingpreferences.h"
#include "gui/sdlgamepadkeynavigation.h"
@@ -48,7 +53,7 @@
#endif
#ifdef USE_CUSTOM_LOGGER
static QTime s_LoggerTime;
static QElapsedTimer s_LoggerTime;
static QTextStream s_LoggerStream(stdout);
static QMutex s_LoggerLock;
#ifdef LOG_TO_FILE
@@ -136,14 +141,6 @@ void qtLogToDiskHandler(QtMsgType type, const QMessageLogContext&, const QString
break;
}
// HACK: Avoid printing this internal Qt warning which seems to print every time we
// start our event loop for the QNetworkReply. I have found this warning in many
// unrelated bug reports, but none actually tackle this warning itself. It seems to be
// new in Qt 5.12.
if (msg.startsWith("QNetworkReplyHttpImplPrivate::_q_startOperation was called more than once")) {
return;
}
QTime logTime = QTime::fromMSecsSinceStartOfDay(s_LoggerTime.elapsed());
QString txt = QString("%1 - Qt %2: %3\n").arg(logTime.toString()).arg(typeTxt).arg(msg);
@@ -231,6 +228,8 @@ LONG WINAPI UnhandledExceptionHandler(struct _EXCEPTION_POINTERS *ExceptionInfo)
int main(int argc, char *argv[])
{
SDL_SetMainReady();
// Set the app version for the QCommandLineParser's showVersion() command
QCoreApplication::setApplicationVersion(VERSION_STR);
@@ -286,8 +285,14 @@ int main(int argc, char *argv[])
AntiHookingDummyImport();
#endif
// Enable High DPI support
QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)
// Enable fractional High DPI scaling on Qt 5.14 and later
QGuiApplication::setHighDpiScaleFactorRoundingPolicy(Qt::HighDpiScaleFactorRoundingPolicy::PassThrough);
#endif
// This avoids using the default keychain for SSL, which may cause
// password prompts on macOS.
qputenv("QT_SSL_USE_TEMPORARY_KEYCHAIN", "1");
@@ -311,19 +316,94 @@ int main(int argc, char *argv[])
// Register custom metatypes for use in signals
qRegisterMetaType<NvApp>("NvApp");
// Disable desktop settings awareness due to issues with QGnomePlatform
// breaking our dialog rendering on Linux.
QGuiApplication::setDesktopSettingsAware(false);
SDL_version compileVersion;
SDL_VERSION(&compileVersion);
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Compiled with SDL %d.%d.%d",
compileVersion.major, compileVersion.minor, compileVersion.patch);
SDL_version runtimeVersion;
SDL_GetVersion(&runtimeVersion);
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Running with SDL %d.%d.%d",
runtimeVersion.major, runtimeVersion.minor, runtimeVersion.patch);
// Allow the display to sleep by default. We will manually use SDL_DisableScreenSaver()
// and SDL_EnableScreenSaver() when appropriate. This hint must be set before
// initializing the SDL video subsystem to have any effect.
SDL_SetHint(SDL_HINT_VIDEO_ALLOW_SCREENSAVER, "1");
if (SDL_InitSubSystem(SDL_INIT_TIMER) != 0) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"SDL_InitSubSystem(SDL_INIT_TIMER) failed: %s",
SDL_GetError());
return -1;
}
#ifdef STEAM_LINK
// Steam Link requires that we initialize video before creating our
// QGuiApplication in order to configure the framebuffer correctly.
if (SDL_InitSubSystem(SDL_INIT_VIDEO) != 0) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"SDL_InitSubSystem(SDL_INIT_VIDEO) failed: %s",
SDL_GetError());
return -1;
}
#endif
// Use atexit() to ensure SDL_Quit() is called. This avoids
// racing with object destruction where SDL may be used.
atexit(SDL_Quit);
// Avoid the default behavior of changing the timer resolution to 1 ms.
// We don't want this all the time that Moonlight is open. We will set
// it manually when we start streaming.
SDL_SetHint(SDL_HINT_TIMER_RESOLUTION, "0");
// Disable minimize on focus loss by default. Users seem to want this off by default.
SDL_SetHint(SDL_HINT_VIDEO_MINIMIZE_ON_FOCUS_LOSS, "0");
#ifdef QT_DEBUG
// Allow thread naming using exceptions on debug builds. SDL doesn't use SEH
// when throwing the exceptions, so we don't enable it for release builds out
// of caution.
SDL_SetHint(SDL_HINT_WINDOWS_DISABLE_THREAD_NAMING, "0");
#endif
QGuiApplication app(argc, argv);
// After the QGuiApplication is created, the platform stuff will be initialized
// and we can set the SDL video driver to match Qt.
if (qgetenv("XDG_SESSION_TYPE") == "wayland" && QGuiApplication::platformName() == "xcb") {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"Detected XWayland. This will probably break hardware decoding! Try running with QT_QPA_PLATFORM=wayland or switch to X11.");
}
else if (QGuiApplication::platformName().startsWith("wayland")) {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Detected Wayland. Performance may be worse than X11.");
qputenv("SDL_VIDEODRIVER", "wayland");
}
#ifdef STEAM_LINK
// Qt 5.9 from the Steam Link SDK is not able to load any fonts
// since the Steam Link doesn't include any of the ones it looks
// for. We know it has NotoSans so we will explicitly ask for that.
if (app.font().family().isEmpty()) {
qWarning() << "SL HACK: No default font - using NotoSans";
QFont fon("NotoSans");
app.setFont(fon);
}
// Move the mouse to the bottom right so it's invisible when using
// gamepad-only navigation.
QCursor().setPos(0xFFFF, 0xFFFF);
#endif
app.setWindowIcon(QIcon(":/res/moonlight.svg"));
// Register our C++ types for QML
qmlRegisterType<ComputerModel>("ComputerModel", 1, 0, "ComputerModel");
qmlRegisterType<AppModel>("AppModel", 1, 0, "AppModel");
qmlRegisterType<StreamingPreferences>("StreamingPreferences", 1, 0, "StreamingPreferences");
qmlRegisterType<SdlGamepadKeyNavigation>("SdlGamepadKeyNavigation", 1, 0, "SdlGamepadKeyNavigation");
qmlRegisterUncreatableType<Session>("Session", 1, 0, "Session", "Session cannot be created from QML");
qmlRegisterSingletonType<ComputerManager>("ComputerManager", 1, 0,
"ComputerManager",
@@ -335,11 +415,33 @@ int main(int argc, char *argv[])
[](QQmlEngine*, QJSEngine*) -> QObject* {
return new AutoUpdateChecker();
});
qmlRegisterSingletonType<SystemProperties>("SystemProperties", 1, 0,
"SystemProperties",
[](QQmlEngine*, QJSEngine*) -> QObject* {
return new SystemProperties();
});
qmlRegisterSingletonType<SdlGamepadKeyNavigation>("SdlGamepadKeyNavigation", 1, 0,
"SdlGamepadKeyNavigation",
[](QQmlEngine*, QJSEngine*) -> QObject* {
return new SdlGamepadKeyNavigation();
});
qmlRegisterSingletonType<StreamingPreferences>("StreamingPreferences", 1, 0,
"StreamingPreferences",
[](QQmlEngine*, QJSEngine*) -> QObject* {
return new StreamingPreferences();
});
#ifndef Q_OS_WINRT
// Use the dense material dark theme by default
if (!qEnvironmentVariableIsSet("QT_QUICK_CONTROLS_STYLE")) {
qputenv("QT_QUICK_CONTROLS_STYLE", "Material");
}
#else
// Use universal dark on WinRT
if (!qEnvironmentVariableIsSet("QT_QUICK_CONTROLS_STYLE")) {
qputenv("QT_QUICK_CONTROLS_STYLE", "Universal");
}
#endif
if (!qEnvironmentVariableIsSet("QT_QUICK_CONTROLS_MATERIAL_THEME")) {
qputenv("QT_QUICK_CONTROLS_MATERIAL_THEME", "Dark");
}
@@ -352,9 +454,6 @@ int main(int argc, char *argv[])
if (!qEnvironmentVariableIsSet("QT_QUICK_CONTROLS_UNIVERSAL_THEME")) {
qputenv("QT_QUICK_CONTROLS_UNIVERSAL_THEME", "Dark");
}
if (!qEnvironmentVariableIsSet("QT_QUICK_CONTROLS_1_STYLE")) {
qputenv("QT_QUICK_CONTROLS_1_STYLE", "Base");
}
QQmlApplicationEngine engine;
QString initialView;
@@ -362,11 +461,11 @@ int main(int argc, char *argv[])
GlobalCommandLineParser parser;
switch (parser.parse(app.arguments())) {
case GlobalCommandLineParser::NormalStartRequested:
initialView = "PcView.qml";
initialView = "qrc:/gui/PcView.qml";
break;
case GlobalCommandLineParser::StreamRequested:
{
initialView = "CliStartStreamSegue.qml";
initialView = "qrc:/gui/CliStartStreamSegue.qml";
StreamingPreferences* preferences = new StreamingPreferences(&app);
StreamCommandLineParser streamParser;
streamParser.parse(app.arguments(), preferences);
@@ -378,7 +477,7 @@ int main(int argc, char *argv[])
}
case GlobalCommandLineParser::QuitRequested:
{
initialView = "CliQuitStreamSegue.qml";
initialView = "qrc:/gui/CliQuitStreamSegue.qml";
QuitCommandLineParser quitParser;
quitParser.parse(app.arguments());
auto launcher = new CliQuitStream::Launcher(quitParser.getHost(), &app);
@@ -394,22 +493,6 @@ int main(int argc, char *argv[])
if (engine.rootObjects().isEmpty())
return -1;
SDL_SetMainReady();
if (SDL_Init(SDL_INIT_TIMER) != 0) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"SDL_Init() failed: %s",
SDL_GetError());
}
// Use atexit() to ensure SDL_Quit() is called. This avoids
// racing with object destruction where SDL may be used.
atexit(SDL_Quit);
// Avoid the default behavior of changing the timer resolution to 1 ms.
// We don't want this all the time that Moonlight is open. We will set
// it manually when we start streaming.
SDL_SetHint(SDL_HINT_TIMER_RESOLUTION, "0");
int err = app.exec();
// Give worker tasks time to properly exit. Fixes PendingQuitTask
+14 -12
View File
@@ -21,6 +21,13 @@ QString Path::getBoxArtCacheDir()
return s_BoxArtCacheDir;
}
QByteArray Path::readDataFile(QString fileName)
{
QFile dataFile(getDataFilePath(fileName));
dataFile.open(QIODevice::ReadOnly);
return dataFile.readAll();
}
QString Path::getDataFilePath(QString fileName)
{
QString candidatePath;
@@ -28,33 +35,28 @@ QString Path::getDataFilePath(QString fileName)
// Check the current directory first
candidatePath = QDir(QDir::currentPath()).absoluteFilePath(fileName);
if (QFile::exists(candidatePath)) {
qInfo() << "Found" << fileName << "at" << candidatePath;
return candidatePath;
}
// Now check the data directories (for Linux, in particular)
candidatePath = QStandardPaths::locate(QStandardPaths::DataLocation, fileName);
if (!candidatePath.isEmpty() && QFile::exists(candidatePath)) {
qInfo() << "Found" << fileName << "at" << candidatePath;
return candidatePath;
}
// Now try the directory of our app installation (for Windows, if current dir doesn't find it)
candidatePath = QDir(QCoreApplication::applicationDirPath()).absoluteFilePath(fileName);
if (QFile::exists(candidatePath)) {
qInfo() << "Found" << fileName << "at" << candidatePath;
return candidatePath;
}
// Now try the Resources folder in our app bundle for macOS
QDir dir = QDir(QCoreApplication::applicationDirPath());
dir.cdUp();
dir.cd("Resources");
dir.filePath(fileName);
candidatePath = dir.absoluteFilePath(fileName);
if (QFile::exists(candidatePath)) {
return candidatePath;
}
// Couldn't find it
return QString();
// Return the QRC embedded copy
candidatePath = ":/data/" + fileName;
qInfo() << "Found" << fileName << "at" << candidatePath;
return QString(candidatePath);
}
void Path::initialize(bool portable)
+3
View File
@@ -9,6 +9,9 @@ public:
static QString getBoxArtCacheDir();
static QByteArray readDataFile(QString fileName);
// Only safe to use directly for Qt classes
static QString getDataFilePath(QString fileName);
static void initialize(bool portable);
+4
View File
@@ -14,5 +14,9 @@
<file>gui/CliStartStreamSegue.qml</file>
<file>gui/AutoResizingComboBox.qml</file>
<file>gui/NavigableMenu.qml</file>
<file>gui/ErrorMessageDialog.qml</file>
<file>gui/NavigableMessageDialog.qml</file>
<file>gui/NavigableDialog.qml</file>
<file>gui/CenteredGridView.qml</file>
</qresource>
</RCC>
+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="#FFFFFF"><path fill="none" d="M0 0h24v24H0V0z"/><path d="M11 15h2v2h-2zm0-8h2v6h-2zm.99-5C6.47 2 2 6.48 2 12s4.47 10 9.99 10C17.52 22 22 17.52 22 12S17.52 2 11.99 2zM12 20c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8z"/></svg>

After

Width:  |  Height:  |  Size: 323 B

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="#FFFFFF"><path fill="none" d="M0 0h24v24H0z"/><path d="M11 18h2v-2h-2v2zm1-16C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8zm0-14c-2.21 0-4 1.79-4 4h2c0-1.1.9-2 2-2s2 .9 2 2c0 2-3 1.75-3 5h2c0-2.25 3-2.5 3-5 0-2.21-1.79-4-4-4z"/></svg>

After

Width:  |  Height:  |  Size: 397 B

+4
View File
@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="#FFFFFF" width="48" height="48" viewBox="0 0 24 24">
<path fill="none" d="M0 0h24v24H0V0z"/>
<path d="M21 2H3c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h7v2H8v2h8v-2h-2v-2h7c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2zm0 14H3V4h18v12z"/>
</svg>

After

Width:  |  Height:  |  Size: 276 B

@@ -1,4 +0,0 @@
<svg fill="#FFFFFF" height="48" viewBox="0 0 24 24" width="48" xmlns="http://www.w3.org/2000/svg">
<path d="M0 0h24v24H0z" fill="none"/>
<path d="M7 11v2h10v-2H7zm5-9C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8z"/>
</svg>

Before

Width:  |  Height:  |  Size: 303 B

@@ -1,9 +0,0 @@
<svg fill="#FFFFFF" height="48" viewBox="0 0 24 24" width="48" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<defs>
<path d="M0 0h24v24H0V0z" id="a"/>
</defs>
<clipPath id="b">
<use overflow="visible" xlink:href="#a"/>
</clipPath>
<path clip-path="url(#b)" d="M21 3H3c-1.11 0-2 .89-2 2v12c0 1.1.89 2 2 2h5v2h8v-2h5c1.1 0 1.99-.9 1.99-2L23 5c0-1.11-.9-2-2-2zm0 14H3V5h18v12zm-5-7v2H8v-2h8z"/>
</svg>

Before

Width:  |  Height:  |  Size: 467 B

-4
View File
@@ -1,4 +0,0 @@
<svg fill="#FFFFFF" height="48" viewBox="0 0 24 24" width="48" xmlns="http://www.w3.org/2000/svg">
<path d="M0 0h24v24H0z" fill="none"/>
<path d="M21 3H3c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h5v2h8v-2h5c1.1 0 1.99-.9 1.99-2L23 5c0-1.1-.9-2-2-2zm0 14H3V5h18v12z"/>
</svg>

Before

Width:  |  Height:  |  Size: 273 B

+7 -3
View File
@@ -1,9 +1,7 @@
<RCC>
<qresource prefix="/">
<file>res/ic_remove_circle_outline_white_48px.svg</file>
<file>res/ic_remove_from_queue_white_48px.svg</file>
<file>res/ic_videogame_asset_white_48px.svg</file>
<file>res/ic_tv_white_48px.svg</file>
<file>res/desktop_windows-48px.svg</file>
<file>res/ic_add_to_queue_white_48px.svg</file>
<file>res/baseline-lock-24px.svg</file>
<file>res/baseline-play_circle_filled_white-48px.svg</file>
@@ -15,5 +13,11 @@
<file>res/question_mark.svg</file>
<file>res/moonlight.svg</file>
<file>res/update.svg</file>
<file>res/baseline-help_outline-24px.svg</file>
<file>res/baseline-error_outline-24px.svg</file>
</qresource>
<qresource prefix="/data">
<file alias="gamecontrollerdb.txt">SDL_GameControllerDB/gamecontrollerdb.txt</file>
<file alias="ModeSeven.ttf">ModeSeven.ttf</file>
</qresource>
</RCC>
+6 -11
View File
@@ -54,15 +54,10 @@ void MappingManager::save()
void MappingManager::applyMappings()
{
QString mappingFile = Path::getDataFilePath("gamecontrollerdb.txt");
if (!mappingFile.isEmpty()) {
std::string mappingFileNative = QDir::toNativeSeparators(mappingFile).toStdString();
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Loading gamepad mappings from: %s",
mappingFileNative.c_str());
int newMappings = SDL_GameControllerAddMappingsFromFile(mappingFileNative.c_str());
QByteArray mappingData = Path::readDataFile("gamecontrollerdb.txt");
if (!mappingData.isEmpty()) {
int newMappings = SDL_GameControllerAddMappingsFromRW(
SDL_RWFromConstMem(mappingData.constData(), mappingData.size()), 1);
if (newMappings < 0) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Error loading gamepad mappings: %s",
@@ -75,8 +70,8 @@ void MappingManager::applyMappings()
}
}
else {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"No gamepad mapping file found");
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Unable to load gamepad mapping file");
}
QList<SdlGamepadMapping> mappings = m_Mappings.values();
+32 -124
View File
@@ -1,6 +1,4 @@
#include "streamingpreferences.h"
#include "streaming/session.h"
#include "streaming/streamutils.h"
#include <QSettings>
@@ -24,6 +22,12 @@
#define SER_MOUSEACCELERATION "mouseacceleration"
#define SER_STARTWINDOWED "startwindowed"
#define SER_FRAMEPACING "framepacing"
#define SER_CONNWARNINGS "connwarnings"
#define SER_RICHPRESENCE "richpresence"
#define SER_GAMEPADMOUSE "gamepadmouse"
#define SER_DEFAULTVER "defaultver"
#define CURRENT_DEFAULT_VER 1
StreamingPreferences::StreamingPreferences(QObject *parent)
: QObject(parent)
@@ -35,6 +39,14 @@ void StreamingPreferences::reload()
{
QSettings settings;
int defaultVer = settings.value(SER_DEFAULTVER, 0).toInt();
#ifdef Q_OS_DARWIN
recommendedFullScreenMode = WindowMode::WM_FULLSCREEN_DESKTOP;
#else
recommendedFullScreenMode = WindowMode::WM_FULLSCREEN;
#endif
width = settings.value(SER_WIDTH, 1280).toInt();
height = settings.value(SER_HEIGHT, 720).toInt();
fps = settings.value(SER_FPS, 60).toInt();
@@ -49,6 +61,9 @@ void StreamingPreferences::reload()
mouseAcceleration = settings.value(SER_MOUSEACCELERATION, false).toBool();
startWindowed = settings.value(SER_STARTWINDOWED, false).toBool();
framePacing = settings.value(SER_FRAMEPACING, false).toBool();
connectionWarnings = settings.value(SER_CONNWARNINGS, true).toBool();
richPresence = settings.value(SER_RICHPRESENCE, true).toBool();
gamepadMouse = settings.value(SER_GAMEPADMOUSE, true).toBool();
audioConfig = static_cast<AudioConfig>(settings.value(SER_AUDIOCFG,
static_cast<int>(AudioConfig::AC_STEREO)).toInt());
videoCodecConfig = static_cast<VideoCodecConfig>(settings.value(SER_VIDEOCFG,
@@ -58,7 +73,17 @@ void StreamingPreferences::reload()
windowMode = static_cast<WindowMode>(settings.value(SER_WINDOWMODE,
// Try to load from the old preference value too
static_cast<int>(settings.value(SER_FULLSCREEN, true).toBool() ?
WindowMode::WM_FULLSCREEN : WindowMode::WM_WINDOWED)).toInt());
recommendedFullScreenMode : WindowMode::WM_WINDOWED)).toInt());
// Perform default settings updates as required based on last default version
if (defaultVer == 0) {
#ifdef Q_OS_DARWIN
// Update window mode setting on macOS from full-screen (old default) to borderless windowed (new default)
if (windowMode == WindowMode::WM_FULLSCREEN) {
windowMode = WindowMode::WM_FULLSCREEN_DESKTOP;
}
#endif
}
}
void StreamingPreferences::save()
@@ -79,131 +104,14 @@ void StreamingPreferences::save()
settings.setValue(SER_MOUSEACCELERATION, mouseAcceleration);
settings.setValue(SER_STARTWINDOWED, startWindowed);
settings.setValue(SER_FRAMEPACING, framePacing);
settings.setValue(SER_CONNWARNINGS, connectionWarnings);
settings.setValue(SER_RICHPRESENCE, richPresence);
settings.setValue(SER_GAMEPADMOUSE, gamepadMouse);
settings.setValue(SER_AUDIOCFG, static_cast<int>(audioConfig));
settings.setValue(SER_VIDEOCFG, static_cast<int>(videoCodecConfig));
settings.setValue(SER_VIDEODEC, static_cast<int>(videoDecoderSelection));
settings.setValue(SER_WINDOWMODE, static_cast<int>(windowMode));
}
bool StreamingPreferences::hasAnyHardwareAcceleration()
{
// Always use VDS_AUTO to avoid spamming the user with warnings
// if they've forced software decoding.
return Session::isHardwareDecodeAvailable(VDS_AUTO,
VIDEO_FORMAT_H264,
1920, 1080, 60);
}
bool StreamingPreferences::isRunningWayland()
{
return qgetenv("XDG_SESSION_TYPE") == QByteArray("wayland");
}
bool StreamingPreferences::isWow64()
{
#ifdef Q_OS_WIN32
return QSysInfo::currentCpuArchitecture() != QSysInfo::buildCpuArchitecture();
#else
return false;
#endif
}
QString StreamingPreferences::getUnmappedGamepads()
{
return SdlInputHandler::getUnmappedGamepads();
}
int StreamingPreferences::getMaximumStreamingFrameRate()
{
// Never let the maximum drop below 60 FPS
int maxFrameRate = 60;
if (SDL_InitSubSystem(SDL_INIT_VIDEO) != 0) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"SDL_InitSubSystem(SDL_INIT_VIDEO) failed: %s",
SDL_GetError());
return maxFrameRate;
}
SDL_DisplayMode mode, bestMode, desktopMode;
for (int displayIndex = 0; displayIndex < SDL_GetNumVideoDisplays(); displayIndex++) {
// Get the native desktop resolution
if (StreamUtils::getRealDesktopMode(displayIndex, &desktopMode)) {
// Start at desktop mode and work our way up
bestMode = desktopMode;
for (int i = 0; i < SDL_GetNumDisplayModes(displayIndex); i++) {
if (SDL_GetDisplayMode(displayIndex, i, &mode) == 0) {
if (mode.w == desktopMode.w && mode.h == desktopMode.h) {
if (mode.refresh_rate > bestMode.refresh_rate) {
bestMode = mode;
}
}
}
}
maxFrameRate = qMax(maxFrameRate, bestMode.refresh_rate);
}
}
SDL_QuitSubSystem(SDL_INIT_VIDEO);
return maxFrameRate;
}
QRect StreamingPreferences::getDesktopResolution(int displayIndex)
{
if (SDL_InitSubSystem(SDL_INIT_VIDEO) != 0) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"SDL_InitSubSystem(SDL_INIT_VIDEO) failed: %s",
SDL_GetError());
return QRect();
}
if (displayIndex >= SDL_GetNumVideoDisplays()) {
SDL_QuitSubSystem(SDL_INIT_VIDEO);
return QRect();
}
SDL_DisplayMode mode;
int err = SDL_GetDesktopDisplayMode(displayIndex, &mode);
SDL_QuitSubSystem(SDL_INIT_VIDEO);
if (err == 0) {
return QRect(0, 0, mode.w, mode.h);
}
else {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"SDL_GetDesktopDisplayMode() failed: %s",
SDL_GetError());
return QRect();
}
}
QRect StreamingPreferences::getNativeResolution(int displayIndex)
{
if (SDL_InitSubSystem(SDL_INIT_VIDEO) != 0) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"SDL_InitSubSystem(SDL_INIT_VIDEO) failed: %s",
SDL_GetError());
return QRect();
}
if (displayIndex >= SDL_GetNumVideoDisplays()) {
SDL_QuitSubSystem(SDL_INIT_VIDEO);
return QRect();
}
SDL_DisplayMode mode;
bool success = StreamUtils::getRealDesktopMode(displayIndex, &mode);
SDL_QuitSubSystem(SDL_INIT_VIDEO);
if (success) {
return QRect(0, 0, mode.w, mode.h);
}
else {
return QRect();
}
settings.setValue(SER_DEFAULTVER, CURRENT_DEFAULT_VER);
}
int StreamingPreferences::getDefaultBitrate(int width, int height, int fps)
+11 -14
View File
@@ -15,20 +15,6 @@ public:
Q_INVOKABLE void save();
Q_INVOKABLE static bool hasAnyHardwareAcceleration();
Q_INVOKABLE static bool isRunningWayland();
Q_INVOKABLE static bool isWow64();
Q_INVOKABLE static int getMaximumStreamingFrameRate();
Q_INVOKABLE QRect getDesktopResolution(int displayIndex);
Q_INVOKABLE QRect getNativeResolution(int displayIndex);
Q_INVOKABLE QString getUnmappedGamepads();
void reload();
enum AudioConfig
@@ -77,10 +63,14 @@ public:
Q_PROPERTY(bool mouseAcceleration MEMBER mouseAcceleration NOTIFY mouseAccelerationChanged)
Q_PROPERTY(bool startWindowed MEMBER startWindowed NOTIFY startWindowedChanged)
Q_PROPERTY(bool framePacing MEMBER framePacing NOTIFY framePacingChanged)
Q_PROPERTY(bool connectionWarnings MEMBER connectionWarnings NOTIFY connectionWarningsChanged)
Q_PROPERTY(bool richPresence MEMBER richPresence NOTIFY richPresenceChanged)
Q_PROPERTY(bool gamepadMouse MEMBER gamepadMouse NOTIFY gamepadMouseChanged)
Q_PROPERTY(AudioConfig audioConfig MEMBER audioConfig NOTIFY audioConfigChanged)
Q_PROPERTY(VideoCodecConfig videoCodecConfig MEMBER videoCodecConfig NOTIFY videoCodecConfigChanged)
Q_PROPERTY(VideoDecoderSelection videoDecoderSelection MEMBER videoDecoderSelection NOTIFY videoDecoderSelectionChanged)
Q_PROPERTY(WindowMode windowMode MEMBER windowMode NOTIFY windowModeChanged)
Q_PROPERTY(WindowMode recommendedFullScreenMode MEMBER recommendedFullScreenMode CONSTANT)
// Directly accessible members for preferences
int width;
@@ -97,10 +87,14 @@ public:
bool mouseAcceleration;
bool startWindowed;
bool framePacing;
bool connectionWarnings;
bool richPresence;
bool gamepadMouse;
AudioConfig audioConfig;
VideoCodecConfig videoCodecConfig;
VideoDecoderSelection videoDecoderSelection;
WindowMode windowMode;
WindowMode recommendedFullScreenMode;
signals:
void displayModeChanged();
@@ -119,5 +113,8 @@ signals:
void windowModeChanged();
void startWindowedChanged();
void framePacingChanged();
void connectionWarningsChanged();
void richPresenceChanged();
void gamepadMouseChanged();
};
+145 -51
View File
@@ -5,36 +5,114 @@
#include "renderers/soundioaudiorenderer.h"
#endif
#ifdef HAVE_SLAUDIO
#include "renderers/slaud.h"
#endif
#include "renderers/sdl.h"
#include <Limelight.h>
IAudioRenderer* Session::createAudioRenderer()
{
#ifdef HAVE_SOUNDIO
if (qgetenv("ML_AUDIO") == "SDL") {
return new SdlAudioRenderer();
}
return new SoundIoAudioRenderer();
#else
return new SdlAudioRenderer();
#endif
#define TRY_INIT_RENDERER(renderer, opusConfig) \
{ \
IAudioRenderer* __renderer = new renderer(); \
if (__renderer->prepareForPlayback(opusConfig)) \
return __renderer; \
delete __renderer; \
}
bool Session::testAudio(int audioConfiguration)
IAudioRenderer* Session::createAudioRenderer(const POPUS_MULTISTREAM_CONFIGURATION opusConfig)
{
IAudioRenderer* audioRenderer;
audioRenderer = createAudioRenderer();
if (audioRenderer == nullptr) {
return false;
// Handle explicit ML_AUDIO setting and fail if the requested backend fails
QString mlAudio = qgetenv("ML_AUDIO").toLower();
if (mlAudio == "sdl") {
TRY_INIT_RENDERER(SdlAudioRenderer, opusConfig)
return nullptr;
}
#ifdef HAVE_SOUNDIO
else if (mlAudio == "libsoundio") {
TRY_INIT_RENDERER(SoundIoAudioRenderer, opusConfig)
return nullptr;
}
#endif
#if defined(HAVE_SLAUDIO)
else if (mlAudio == "slaudio") {
TRY_INIT_RENDERER(SLAudioRenderer, opusConfig)
return nullptr;
}
#endif
else if (!mlAudio.isEmpty()) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Unknown audio backend: %s",
SDL_getenv("ML_AUDIO"));
return nullptr;
}
// -------------- Automatic backend selection below this line ---------------
#if defined(HAVE_SLAUDIO)
// Steam Link should always have SLAudio
TRY_INIT_RENDERER(SLAudioRenderer, opusConfig)
#endif
#if !defined(Q_OS_WIN32) && !defined(Q_OS_DARWIN)
// Linux defaults to SDL due to persistent glitching issues under libsoundio.
// Platforms that libsoundio doesn't support also default to SDL.
TRY_INIT_RENDERER(SdlAudioRenderer, opusConfig)
#ifdef HAVE_SOUNDIO
TRY_INIT_RENDERER(SoundIoAudioRenderer, opusConfig)
#endif
#else
// Windows and macOS default to libsoundio and fall back to SDL
#ifdef HAVE_SOUNDIO
TRY_INIT_RENDERER(SoundIoAudioRenderer, opusConfig)
#endif
TRY_INIT_RENDERER(SdlAudioRenderer, opusConfig)
#endif
return nullptr;
}
int Session::getAudioRendererCapabilities(int audioConfiguration)
{
// Build a fake OPUS_MULTISTREAM_CONFIGURATION to give
// the renderer the channel count and sample rate.
OPUS_MULTISTREAM_CONFIGURATION opusConfig = {};
opusConfig.sampleRate = 48000;
opusConfig.samplesPerFrame = 240;
switch (audioConfiguration)
{
case AUDIO_CONFIGURATION_STEREO:
opusConfig.channelCount = 2;
break;
case AUDIO_CONFIGURATION_51_SURROUND:
opusConfig.channelCount = 6;
break;
default:
SDL_assert(false);
return 0;
}
IAudioRenderer* audioRenderer = createAudioRenderer(&opusConfig);
if (audioRenderer == nullptr) {
return 0;
}
int caps = audioRenderer->getCapabilities();
delete audioRenderer;
return caps;
}
bool Session::testAudio(int audioConfiguration)
{
// Build a fake OPUS_MULTISTREAM_CONFIGURATION to give
// the renderer the channel count and sample rate.
OPUS_MULTISTREAM_CONFIGURATION opusConfig = {};
opusConfig.sampleRate = 48000;
opusConfig.samplesPerFrame = 240;
switch (audioConfiguration)
{
@@ -49,11 +127,14 @@ bool Session::testAudio(int audioConfiguration)
return false;
}
bool ret = audioRenderer->prepareForPlayback(&opusConfig);
IAudioRenderer* audioRenderer = createAudioRenderer(&opusConfig);
if (audioRenderer == nullptr) {
return false;
}
delete audioRenderer;
return ret;
return true;
}
int Session::arInit(int /* audioConfiguration */,
@@ -64,30 +145,34 @@ int Session::arInit(int /* audioConfiguration */,
SDL_memcpy(&s_ActiveSession->m_AudioConfig, opusConfig, sizeof(*opusConfig));
s_ActiveSession->m_AudioRenderer = s_ActiveSession->createAudioRenderer(&s_ActiveSession->m_AudioConfig);
if (s_ActiveSession->m_AudioRenderer == nullptr) {
return -2;
}
// Allow the chosen renderer to remap Opus channels as needed to ensure proper output
s_ActiveSession->m_AudioRenderer->remapChannels(&s_ActiveSession->m_AudioConfig);
// Create the Opus decoder with the renderer's preferred channel mapping
s_ActiveSession->m_OpusDecoder =
opus_multistream_decoder_create(opusConfig->sampleRate,
opusConfig->channelCount,
opusConfig->streams,
opusConfig->coupledStreams,
opusConfig->mapping,
opus_multistream_decoder_create(s_ActiveSession->m_AudioConfig.sampleRate,
s_ActiveSession->m_AudioConfig.channelCount,
s_ActiveSession->m_AudioConfig.streams,
s_ActiveSession->m_AudioConfig.coupledStreams,
s_ActiveSession->m_AudioConfig.mapping,
&error);
if (s_ActiveSession->m_OpusDecoder == NULL) {
delete s_ActiveSession->m_AudioRenderer;
s_ActiveSession->m_AudioRenderer = nullptr;
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Failed to create decoder: %d",
error);
return -1;
}
s_ActiveSession->m_AudioRenderer = s_ActiveSession->createAudioRenderer();
if (!s_ActiveSession->m_AudioRenderer->prepareForPlayback(opusConfig)) {
delete s_ActiveSession->m_AudioRenderer;
opus_multistream_decoder_destroy(s_ActiveSession->m_OpusDecoder);
return -2;
}
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Audio stream has %d channels",
opusConfig->channelCount);
s_ActiveSession->m_AudioConfig.channelCount);
return 0;
}
@@ -105,8 +190,11 @@ void Session::arDecodeAndPlaySample(char* sampleData, int sampleLength)
{
int samplesDecoded;
// Set this thread to high priority to reduce
// the chance of missing our sample delivery time
#ifndef STEAM_LINK
// Set this thread to high priority to reduce the chance of missing
// our sample delivery time. On Steam Link, this causes starvation
// of other threads due to severely restricted CPU time available,
// so we will skip it on that platform.
if (s_ActiveSession->m_AudioSampleCount == 0) {
if (SDL_SetThreadPriority(SDL_THREAD_PRIORITY_HIGH) < 0) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
@@ -114,6 +202,7 @@ void Session::arDecodeAndPlaySample(char* sampleData, int sampleLength)
SDL_GetError());
}
}
#endif
// See if we need to drop this sample
if (s_ActiveSession->m_DropAudioEndTime != 0) {
@@ -133,25 +222,34 @@ void Session::arDecodeAndPlaySample(char* sampleData, int sampleLength)
s_ActiveSession->m_AudioSampleCount++;
if (s_ActiveSession->m_AudioRenderer != nullptr) {
int desiredSize = sizeof(short) * s_ActiveSession->m_AudioConfig.samplesPerFrame * s_ActiveSession->m_AudioConfig.channelCount;
void* buffer = s_ActiveSession->m_AudioRenderer->getAudioBuffer(&desiredSize);
if (buffer == nullptr) {
return;
}
samplesDecoded = opus_multistream_decode(s_ActiveSession->m_OpusDecoder,
(unsigned char*)sampleData,
sampleLength,
s_ActiveSession->m_OpusDecodeBuffer,
SAMPLES_PER_FRAME,
(short*)buffer,
desiredSize / sizeof(short) / s_ActiveSession->m_AudioConfig.channelCount,
0);
// Update desiredSize with the number of bytes actually populated by the decoding operation
if (samplesDecoded > 0) {
if (!s_ActiveSession->m_AudioRenderer->submitAudio(s_ActiveSession->m_OpusDecodeBuffer,
static_cast<int>(
sizeof(short) *
samplesDecoded *
s_ActiveSession->m_AudioConfig.channelCount))) {
SDL_assert(desiredSize >= sizeof(short) * samplesDecoded * s_ActiveSession->m_AudioConfig.channelCount);
desiredSize = sizeof(short) * samplesDecoded * s_ActiveSession->m_AudioConfig.channelCount;
}
else {
desiredSize = 0;
}
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"Reinitializing audio renderer after failure");
if (!s_ActiveSession->m_AudioRenderer->submitAudio(desiredSize)) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"Reinitializing audio renderer after failure");
delete s_ActiveSession->m_AudioRenderer;
s_ActiveSession->m_AudioRenderer = nullptr;
}
delete s_ActiveSession->m_AudioRenderer;
s_ActiveSession->m_AudioRenderer = nullptr;
}
}
@@ -165,11 +263,7 @@ void Session::arDecodeAndPlaySample(char* sampleData, int sampleLength)
// so we return to real-time playback and don't accumulate latency.
Uint32 audioReinitStartTime = SDL_GetTicks();
s_ActiveSession->m_AudioRenderer = s_ActiveSession->createAudioRenderer();
if (!s_ActiveSession->m_AudioRenderer->prepareForPlayback(&s_ActiveSession->m_AudioConfig)) {
delete s_ActiveSession->m_AudioRenderer;
s_ActiveSession->m_AudioRenderer = nullptr;
}
s_ActiveSession->m_AudioRenderer = s_ActiveSession->createAudioRenderer(&s_ActiveSession->m_AudioConfig);
Uint32 audioReinitStopTime = SDL_GetTicks();
+15 -4
View File
@@ -2,9 +2,6 @@
#include <Limelight.h>
#define MAX_CHANNELS 6
#define SAMPLES_PER_FRAME 240
class IAudioRenderer
{
public:
@@ -12,6 +9,20 @@ public:
virtual bool prepareForPlayback(const OPUS_MULTISTREAM_CONFIGURATION* opusConfig) = 0;
virtual void* getAudioBuffer(int* size) = 0;
// Return false if an unrecoverable error has occurred and the renderer must be reinitialized
virtual bool submitAudio(short* audioBuffer, int audioSize) = 0;
virtual bool submitAudio(int bytesWritten) = 0;
virtual int getCapabilities() = 0;
virtual void remapChannels(POPUS_MULTISTREAM_CONFIGURATION) {
// Use default channel mapping:
// 0 - Front Left
// 1 - Front Right
// 2 - Center
// 3 - LFE
// 4 - Surround Left
// 5 - Surround Right
}
};
+7 -1
View File
@@ -12,8 +12,14 @@ public:
virtual bool prepareForPlayback(const OPUS_MULTISTREAM_CONFIGURATION* opusConfig);
virtual bool submitAudio(short* audioBuffer, int audioSize);
virtual void* getAudioBuffer(int* size);
virtual bool submitAudio(int bytesWritten);
virtual int getCapabilities();
private:
SDL_AudioDeviceID m_AudioDevice;
void* m_AudioBuffer;
int m_FrameSize;
};
+46 -5
View File
@@ -6,7 +6,8 @@
#include <QtGlobal>
SdlAudioRenderer::SdlAudioRenderer()
: m_AudioDevice(0)
: m_AudioDevice(0),
m_AudioBuffer(nullptr)
{
SDL_assert(!SDL_WasInit(SDL_INIT_AUDIO));
if (SDL_InitSubSystem(SDL_INIT_AUDIO) != 0) {
@@ -30,14 +31,22 @@ bool SdlAudioRenderer::prepareForPlayback(const OPUS_MULTISTREAM_CONFIGURATION*
// frames contain a non-power of 2 number of samples,
// so the slop would require buffering another full frame.
// Specifying non-Po2 seems to work for our supported platforms.
want.samples = SAMPLES_PER_FRAME;
want.samples = opusConfig->samplesPerFrame;
m_FrameSize = opusConfig->samplesPerFrame * sizeof(short) * opusConfig->channelCount;
m_AudioDevice = SDL_OpenAudioDevice(NULL, 0, &want, &have, 0);
if (m_AudioDevice == 0) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Failed to open audio device: %s",
SDL_GetError());
SDL_QuitSubSystem(SDL_INIT_AUDIO);
return false;
}
m_AudioBuffer = malloc(m_FrameSize);
if (m_AudioBuffer == nullptr) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Failed to allocate audio buffer");
return false;
}
@@ -65,13 +74,39 @@ SdlAudioRenderer::~SdlAudioRenderer()
SDL_CloseAudioDevice(m_AudioDevice);
}
if (m_AudioBuffer != nullptr) {
free(m_AudioBuffer);
}
SDL_QuitSubSystem(SDL_INIT_AUDIO);
SDL_assert(!SDL_WasInit(SDL_INIT_AUDIO));
}
bool SdlAudioRenderer::submitAudio(short* audioBuffer, int audioSize)
void* SdlAudioRenderer::getAudioBuffer(int*)
{
if (SDL_QueueAudio(m_AudioDevice, audioBuffer, audioSize) < 0) {
return m_AudioBuffer;
}
bool SdlAudioRenderer::submitAudio(int bytesWritten)
{
if (bytesWritten == 0) {
// Nothing to do
return true;
}
// Don't queue if there's already more than 30 ms of audio data waiting
// in Moonlight's audio queue.
if (LiGetPendingAudioDuration() > 30) {
return true;
}
// Provide backpressure on the queue to ensure too many frames don't build up
// in SDL's audio queue.
while (SDL_GetQueuedAudioSize(m_AudioDevice) / m_FrameSize > 10) {
SDL_Delay(1);
}
if (SDL_QueueAudio(m_AudioDevice, m_AudioBuffer, bytesWritten) < 0) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Failed to queue audio sample: %s",
SDL_GetError());
@@ -79,3 +114,9 @@ bool SdlAudioRenderer::submitAudio(short* audioBuffer, int audioSize)
return true;
}
int SdlAudioRenderer::getCapabilities()
{
// Direct submit can't be used because we use LiGetPendingAudioDuration()
return CAPABILITY_SUPPORTS_ARBITRARY_AUDIO_DURATION;
}
+145
View File
@@ -0,0 +1,145 @@
#include "slaud.h"
#include <SDL.h>
SLAudioRenderer::SLAudioRenderer()
: m_AudioContext(nullptr),
m_AudioStream(nullptr),
m_AudioBuffer(nullptr)
{
SLAudio_SetLogFunction(SLAudioRenderer::slLogCallback, nullptr);
}
bool SLAudioRenderer::prepareForPlayback(const OPUS_MULTISTREAM_CONFIGURATION* opusConfig)
{
m_AudioContext = SLAudio_CreateContext();
if (m_AudioContext == nullptr) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"SLAudio_CreateContext() failed");
return false;
}
// This number is pretty conservative (especially for surround), but
// it's hard to avoid since we get crushed by CPU limitations.
m_MaxQueuedAudioMs = 40 * opusConfig->channelCount / 2;
m_AudioBufferSize = opusConfig->samplesPerFrame * sizeof(short) * opusConfig->channelCount;
m_AudioStream = SLAudio_CreateStream(m_AudioContext,
opusConfig->sampleRate,
opusConfig->channelCount,
m_AudioBufferSize,
1);
if (m_AudioStream == nullptr) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"SLAudio_CreateStream() failed");
return false;
}
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Using SLAudio renderer with %d samples per frame",
opusConfig->samplesPerFrame);
return true;
}
#define SWAP_CHANNEL(i, j) \
tmp = opusConfig->mapping[i]; \
opusConfig->mapping[i] = opusConfig->mapping[j]; \
opusConfig->mapping[j] = tmp
void SLAudioRenderer::remapChannels(POPUS_MULTISTREAM_CONFIGURATION opusConfig) {
unsigned char tmp;
if (opusConfig->channelCount == 6) {
// The Moonlight's default channel order is FL,FR,C,LFE,RL,RR
// SLAudio expects FL,C,FR,RL,RR,LFE so we swap the channels around to match
// Swap FR and C - now FL,C,FR,LFE,RL,RR
SWAP_CHANNEL(1, 2);
// Swap LFE and RR - now FL,C,FR,RR,RL,LFE
SWAP_CHANNEL(3, 5);
// Swap RR and RL - now FL,C,FR,RL,RR,LFE
SWAP_CHANNEL(3, 4);
}
}
void* SLAudioRenderer::getAudioBuffer(int* size)
{
SDL_assert(*size == m_AudioBufferSize);
if (m_AudioBuffer == nullptr) {
m_AudioBuffer = SLAudio_BeginFrame(m_AudioStream);
}
return m_AudioBuffer;
}
SLAudioRenderer::~SLAudioRenderer()
{
if (m_AudioBuffer != nullptr) {
memset(m_AudioBuffer, 0, m_AudioBufferSize);
SLAudio_SubmitFrame(m_AudioStream);
}
if (m_AudioStream != nullptr) {
SLAudio_FreeStream(m_AudioStream);
}
if (m_AudioContext != nullptr) {
SLAudio_FreeContext(m_AudioContext);
}
}
bool SLAudioRenderer::submitAudio(int bytesWritten)
{
if (bytesWritten == 0) {
// This buffer will be reused next time
return true;
}
if (LiGetPendingAudioDuration() < m_MaxQueuedAudioMs) {
SLAudio_SubmitFrame(m_AudioStream);
m_AudioBuffer = nullptr;
}
else {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Too many queued audio frames: %d",
LiGetPendingAudioFrames());
}
return true;
}
int SLAudioRenderer::getCapabilities()
{
return CAPABILITY_SLOW_OPUS_DECODER | CAPABILITY_SUPPORTS_ARBITRARY_AUDIO_DURATION;
}
void SLAudioRenderer::slLogCallback(void*, ESLAudioLog logLevel, const char *message)
{
SDL_LogPriority priority;
switch (logLevel)
{
case k_ESLAudioLogError:
priority = SDL_LOG_PRIORITY_ERROR;
break;
case k_ESLAudioLogWarning:
priority = SDL_LOG_PRIORITY_WARN;
break;
case k_ESLAudioLogInfo:
priority = SDL_LOG_PRIORITY_INFO;
break;
default:
case k_ESLAudioLogDebug:
priority = SDL_LOG_PRIORITY_DEBUG;
break;
}
SDL_LogMessage(SDL_LOG_CATEGORY_APPLICATION,
priority,
"SLAudio: %s",
message);
}
+32
View File
@@ -0,0 +1,32 @@
#pragma once
#include "renderer.h"
#include <SLAudio.h>
class SLAudioRenderer : public IAudioRenderer
{
public:
SLAudioRenderer();
virtual ~SLAudioRenderer();
virtual bool prepareForPlayback(const OPUS_MULTISTREAM_CONFIGURATION* opusConfig);
virtual void* getAudioBuffer(int* size);
virtual bool submitAudio(int bytesWritten);
virtual int getCapabilities();
virtual void remapChannels(POPUS_MULTISTREAM_CONFIGURATION opusConfig);
private:
static void slLogCallback(void* context, ESLAudioLog logLevel, const char* message);
CSLAudioContext* m_AudioContext;
CSLAudioStream* m_AudioStream;
void* m_AudioBuffer;
int m_AudioBufferSize;
int m_MaxQueuedAudioMs;
};
@@ -4,29 +4,13 @@
#include <QtGlobal>
// GFE sends us packets in 5 ms chunks
const double SoundIoAudioRenderer::k_RawSampleLengthSec = 0.005;
#ifdef Q_OS_LINUX
// PulseAudio and ALSA require more than just 5 ms samples
// for some reason, so write a minimum of 25 ms each time to
// prevent underruns on Bluetooth.
// https://github.com/moonlight-stream/moonlight-qt/issues/147
// https://github.com/moonlight-stream/moonlight-qt/issues/157
const double SoundIoAudioRenderer::k_MinSampleLengthSec = 0.025;
#else
// This determines the size of the buffers we'll
// get from CoreAudio. It is also the minimum
// size that we will write when called to fill a buffer.
const double SoundIoAudioRenderer::k_MinSampleLengthSec = k_RawSampleLengthSec;
#endif
SoundIoAudioRenderer::SoundIoAudioRenderer()
: m_OpusChannelCount(0),
m_SoundIo(nullptr),
m_Device(nullptr),
m_OutputStream(nullptr),
m_RingBuffer(nullptr),
m_AudioPacketDuration(0),
m_Latency(0),
m_Errored(false)
{
@@ -172,9 +156,11 @@ bool SoundIoAudioRenderer::prepareForPlayback(const OPUS_MULTISTREAM_CONFIGURATI
return false;
}
m_AudioPacketDuration = (opusConfig->samplesPerFrame / (opusConfig->sampleRate / 1000)) / 1000.0;
m_OutputStream->format = SoundIoFormatS16NE;
m_OutputStream->sample_rate = opusConfig->sampleRate;
m_OutputStream->software_latency = k_MinSampleLengthSec;
m_OutputStream->software_latency = m_AudioPacketDuration;
m_OutputStream->name = "Moonlight";
m_OutputStream->userdata = this;
m_OutputStream->error_callback = sioErrorCallback;
@@ -237,30 +223,26 @@ bool SoundIoAudioRenderer::prepareForPlayback(const OPUS_MULTISTREAM_CONFIGURATI
int packetsToBuffer;
#ifdef Q_OS_LINUX
// PulseAudio and ALSA need the large buffer (see comment on k_MinSampleLengthSec),
// so we need a buffer at least double that size to allow packets to arrive
// while we're writing to the sink.
packetsToBuffer = (int)(k_MinSampleLengthSec / k_RawSampleLengthSec) * 2;
#else
if (m_SoundIo->current_backend == SoundIoBackendWasapi) {
// 15 ms buffer seems to be fine for WASAPI
packetsToBuffer = 3;
packetsToBuffer = (int)ceil(0.015 / m_AudioPacketDuration);
}
else {
// 30 ms buffer on CoreAudio to avoid glitching on macOS
packetsToBuffer = 6;
packetsToBuffer = (int)ceil(0.030 / m_AudioPacketDuration);
}
#endif
// Always buffer at least 2 packets
packetsToBuffer = qMax(2, packetsToBuffer);
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Audio buffer size: %d packets",
packetsToBuffer);
"Audio buffer size: %f seconds",
packetsToBuffer * m_AudioPacketDuration);
m_RingBuffer = soundio_ring_buffer_create(m_SoundIo,
m_OutputStream->bytes_per_sample *
m_OpusChannelCount *
SAMPLES_PER_FRAME *
opusConfig->samplesPerFrame *
packetsToBuffer);
if (m_RingBuffer == nullptr) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
@@ -284,15 +266,8 @@ bool SoundIoAudioRenderer::prepareForPlayback(const OPUS_MULTISTREAM_CONFIGURATI
return true;
}
bool SoundIoAudioRenderer::submitAudio(short* audioBuffer, int audioSize)
void* SoundIoAudioRenderer::getAudioBuffer(int* size)
{
if (m_Errored) {
return false;
}
// Flush events to update with new device arrivals
soundio_flush_events(m_SoundIo);
// We must always write a full frame of audio. If we don't,
// the reader will get out of sync with the writer and our
// channels will get all mixed up. To ensure this is always
@@ -300,13 +275,36 @@ bool SoundIoAudioRenderer::submitAudio(short* audioBuffer, int audioSize)
// of our frame size.
int bytesFree = soundio_ring_buffer_free_count(m_RingBuffer);
int bytesPerFrame = m_OpusChannelCount * m_OutputStream->bytes_per_sample;
int bytesToWrite = qMin(audioSize, (bytesFree / bytesPerFrame) * bytesPerFrame);
memcpy(soundio_ring_buffer_write_ptr(m_RingBuffer), audioBuffer, bytesToWrite);
soundio_ring_buffer_advance_write_ptr(m_RingBuffer, bytesToWrite);
*size = qMin(*size, (bytesFree / bytesPerFrame) * bytesPerFrame);
return soundio_ring_buffer_write_ptr(m_RingBuffer);
}
bool SoundIoAudioRenderer::submitAudio(int bytesWritten)
{
if (m_Errored) {
return false;
}
if (bytesWritten == 0) {
// Nothing to do
return true;
}
// Flush events to update with new device arrivals
soundio_flush_events(m_SoundIo);
// Advance the write pointer
soundio_ring_buffer_advance_write_ptr(m_RingBuffer, bytesWritten);
return true;
}
int SoundIoAudioRenderer::getCapabilities()
{
// TODO: Tweak buffer sizes then re-enable arbitrary audio duration
return CAPABILITY_DIRECT_SUBMIT /* | CAPABILITY_SUPPORTS_ARBITRARY_AUDIO_DURATION */;
}
void SoundIoAudioRenderer::sioErrorCallback(SoundIoOutStream* stream, int err)
{
auto me = reinterpret_cast<SoundIoAudioRenderer*>(stream->userdata);
@@ -371,15 +369,19 @@ void SoundIoAudioRenderer::sioWriteCallback(SoundIoOutStream* stream, int frameC
(me->m_OpusChannelCount * stream->bytes_per_sample);
int bytesRead = 0;
// Clamp framesLeft to frameCountMax
framesLeft = qMin(framesLeft, frameCountMax);
// Ensure we always write at least a buffer, even if it's silence, to avoid
// busy looping when no audio data is available while libsoundio tries to keep
// us from starving the output device.
frameCountMin = qMax(frameCountMin, (int)(stream->sample_rate * k_MinSampleLengthSec));
frameCountMin = qMax(frameCountMin, (int)(stream->sample_rate * me->m_AudioPacketDuration));
// Clamp frameCountMax to at least 2 packets or 20 ms to stop our latency from growing if audio packets lag.
// This makes sure that we never increase our latency far beyond what the sink is consuming.
frameCountMax = qMin(frameCountMax, (int)(stream->sample_rate * qMax(me->m_AudioPacketDuration * 2, 0.020)));
frameCountMin = qMin(frameCountMin, frameCountMax);
// Clamp framesLeft to frameCountMax
framesLeft = qMin(framesLeft, frameCountMax);
// Track latency on queueing-based backends
if (me->m_SoundIo->current_backend != SoundIoBackendCoreAudio && me->m_SoundIo->current_backend != SoundIoBackendJack) {
soundio_outstream_get_latency(stream, &me->m_Latency);
@@ -13,7 +13,11 @@ public:
virtual bool prepareForPlayback(const OPUS_MULTISTREAM_CONFIGURATION* opusConfig);
virtual bool submitAudio(short* audioBuffer, int audioSize);
virtual void* getAudioBuffer(int* size);
virtual bool submitAudio(int bytesWritten);
virtual int getCapabilities();
private:
int scoreChannelLayout(const struct SoundIoChannelLayout* layout, const OPUS_MULTISTREAM_CONFIGURATION* opusConfig);
@@ -32,9 +36,7 @@ private:
struct SoundIoOutStream* m_OutputStream;
struct SoundIoRingBuffer* m_RingBuffer;
struct SoundIoChannelLayout m_EffectiveLayout;
double m_AudioPacketDuration;
double m_Latency;
bool m_Errored;
static const double k_RawSampleLengthSec;
static const double k_MinSampleLengthSec;
};
+404 -63
View File
@@ -5,6 +5,7 @@
#include "path.h"
#include <QtGlobal>
#include <QtMath>
#include <QDir>
#define VK_0 0x30
@@ -28,6 +29,22 @@
// How far the finger can move before it cancels a drag or tap
#define DEAD_ZONE_DELTA 0.1f
// How long the Start button must be pressed to toggle mouse emulation
#define MOUSE_EMULATION_LONG_PRESS_TIME 750
// How long between polling the gamepad to send virtual mouse input
#define MOUSE_EMULATION_POLLING_INTERVAL 50
// Determines how fast the mouse will move each interval
#define MOUSE_EMULATION_MOTION_MULTIPLIER 4
// Determines the maximum motion amount before allowing movement
#define MOUSE_EMULATION_DEADZONE 2
// Haptic capabilities (in addition to those from SDL_HapticQuery())
#define ML_HAPTIC_GC_RUMBLE (1U << 16)
#define ML_HAPTIC_SIMPLE_RUMBLE (1U << 17)
const int SdlInputHandler::k_ButtonMap[] = {
A_FLAG, B_FLAG, X_FLAG, Y_FLAG,
BACK_FLAG, SPECIAL_FLAG, PLAY_FLAG,
@@ -38,7 +55,9 @@ const int SdlInputHandler::k_ButtonMap[] = {
SdlInputHandler::SdlInputHandler(StreamingPreferences& prefs, NvComputer*, int streamWidth, int streamHeight)
: m_MultiController(prefs.multiController),
m_GamepadMouse(prefs.gamepadMouse),
m_MouseMoveTimer(0),
m_FakeCaptureActive(false),
m_LeftButtonReleaseTimer(0),
m_RightButtonReleaseTimer(0),
m_DragTimer(0),
@@ -54,8 +73,18 @@ SdlInputHandler::SdlInputHandler(StreamingPreferences& prefs, NvComputer*, int s
// is via normal motion events that are influenced by mouse acceleration).
// Otherwise, we'll use raw input capture which is straight from the device
// without modification by the OS.
SDL_SetHint(SDL_HINT_MOUSE_RELATIVE_MODE_WARP,
prefs.mouseAcceleration ? "1" : "0");
SDL_SetHintWithPriority(SDL_HINT_MOUSE_RELATIVE_MODE_WARP,
prefs.mouseAcceleration ? "1" : "0",
SDL_HINT_OVERRIDE);
#if defined(Q_OS_DARWIN) && !SDL_VERSION_ATLEAST(2, 0, 10)
// SDL 2.0.9 on macOS has a broken HIDAPI mapping for the older Xbox One S
// firmware, so we have to disable HIDAPI for Xbox gamepads on macOS until
// SDL 2.0.10 where the bug is fixed.
// https://github.com/moonlight-stream/moonlight-qt/issues/133
// https://bugzilla.libsdl.org/show_bug.cgi?id=4395
SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI_XBOX, "0");
#endif
// We must initialize joystick explicitly before gamecontroller in order
// to ensure we receive gamecontroller attach events for gamepads where
@@ -72,6 +101,12 @@ SdlInputHandler::SdlInputHandler(StreamingPreferences& prefs, NvComputer*, int s
MappingManager mappingManager;
mappingManager.applyMappings();
// Flush gamepad arrival and departure events which may be queued before
// starting the gamecontroller subsystem again. This prevents us from
// receiving duplicate arrival and departure events for the same gamepad.
SDL_FlushEvent(SDL_CONTROLLERDEVICEADDED);
SDL_FlushEvent(SDL_CONTROLLERDEVICEREMOVED);
// 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));
@@ -81,12 +116,14 @@ SdlInputHandler::SdlInputHandler(StreamingPreferences& prefs, NvComputer*, int s
SDL_GetError());
}
#if !SDL_VERSION_ATLEAST(2, 0, 9)
SDL_assert(!SDL_WasInit(SDL_INIT_HAPTIC));
if (SDL_InitSubSystem(SDL_INIT_HAPTIC) != 0) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"SDL_InitSubSystem(SDL_INIT_HAPTIC) failed: %s",
SDL_GetError());
}
#endif
// Initialize the gamepad mask with currently attached gamepads to avoid
// causing gamepads to unexpectedly disappear and reappear on the host
@@ -106,9 +143,15 @@ SdlInputHandler::SdlInputHandler(StreamingPreferences& prefs, NvComputer*, int s
SdlInputHandler::~SdlInputHandler()
{
for (int i = 0; i < MAX_GAMEPADS; i++) {
if (m_GamepadState[i].mouseEmulationTimer != 0) {
Session::get()->notifyMouseEmulationMode(false);
SDL_RemoveTimer(m_GamepadState[i].mouseEmulationTimer);
}
#if !SDL_VERSION_ATLEAST(2, 0, 9)
if (m_GamepadState[i].haptic != nullptr) {
SDL_HapticClose(m_GamepadState[i].haptic);
}
#endif
if (m_GamepadState[i].controller != nullptr) {
SDL_GameControllerClose(m_GamepadState[i].controller);
}
@@ -119,14 +162,27 @@ SdlInputHandler::~SdlInputHandler()
SDL_RemoveTimer(m_RightButtonReleaseTimer);
SDL_RemoveTimer(m_DragTimer);
#if !SDL_VERSION_ATLEAST(2, 0, 9)
SDL_QuitSubSystem(SDL_INIT_HAPTIC);
SDL_assert(!SDL_WasInit(SDL_INIT_HAPTIC));
#endif
SDL_QuitSubSystem(SDL_INIT_GAMECONTROLLER);
SDL_assert(!SDL_WasInit(SDL_INIT_GAMECONTROLLER));
SDL_QuitSubSystem(SDL_INIT_JOYSTICK);
SDL_assert(!SDL_WasInit(SDL_INIT_JOYSTICK));
// Return background event handling to off
SDL_SetHint(SDL_HINT_JOYSTICK_ALLOW_BACKGROUND_EVENTS, "0");
#ifdef STEAM_LINK
// Hide SDL's cursor on Steam Link after quitting the stream.
// FIXME: We should also do this for other situations where SDL
// and Qt will draw their own mouse cursors like KMSDRM or RPi
// video backends.
SDL_ShowCursor(SDL_DISABLE);
#endif
}
void SdlInputHandler::handleKeyEvent(SDL_KeyboardEvent* event)
@@ -139,10 +195,69 @@ void SdlInputHandler::handleKeyEvent(SDL_KeyboardEvent* event)
(event->keysym.mod & KMOD_CTRL) &&
(event->keysym.mod & KMOD_ALT) &&
(event->keysym.mod & KMOD_SHIFT)) {
// First we test the SDLK combos for matches,
// that way we ensure that latin keyboard users
// can match to the key they see on their keyboards.
// If nothing matches that, we'll then go on to
// checking scancodes so non-latin keyboard users
// can have working hotkeys (though possibly in
// odd positions). We must do all SDLK tests before
// any scancode tests to avoid issues in cases
// where the SDLK for one shortcut collides with
// the scancode of another.
// Check for quit combo (Ctrl+Alt+Shift+Q)
if (event->keysym.scancode == SDL_SCANCODE_Q) {
if (event->keysym.sym == SDLK_q) {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Detected quit key combo");
"Detected quit key combo (SDLK)");
// Push a quit event to the main loop
SDL_Event event;
event.type = SDL_QUIT;
event.quit.timestamp = SDL_GetTicks();
SDL_PushEvent(&event);
return;
}
// Check for the unbind combo (Ctrl+Alt+Shift+Z)
else if (event->keysym.sym == SDLK_z) {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Detected mouse capture toggle combo (SDLK)");
// Stop handling future input
setCaptureActive(!isCaptureActive());
// Force raise all keys to ensure they aren't stuck,
// since we won't get their key up events.
raiseAllKeys();
return;
}
else if (event->keysym.sym == SDLK_x) {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Detected full-screen toggle combo (SDLK)");
Session::s_ActiveSession->toggleFullscreen();
// Force raise all keys just be safe across this full-screen/windowed
// transition just in case key events get lost.
raiseAllKeys();
return;
}
else if (event->keysym.sym == SDLK_s) {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Detected stats toggle combo (SDLK)");
// Toggle the stats overlay
Session::get()->getOverlayManager().setOverlayState(Overlay::OverlayDebug,
!Session::get()->getOverlayManager().isOverlayEnabled(Overlay::OverlayDebug));
// Force raise all keys just be safe across this full-screen/windowed
// transition just in case key events get lost.
raiseAllKeys();
return;
}
// Check for quit combo (Ctrl+Alt+Shift+Q)
else if (event->keysym.scancode == SDL_SCANCODE_Q) {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Detected quit key combo (scancode)");
// Push a quit event to the main loop
SDL_Event event;
@@ -154,10 +269,10 @@ void SdlInputHandler::handleKeyEvent(SDL_KeyboardEvent* event)
// Check for the unbind combo (Ctrl+Alt+Shift+Z)
else if (event->keysym.scancode == SDL_SCANCODE_Z) {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Detected mouse capture toggle combo");
"Detected mouse capture toggle combo (scancode)");
// Stop handling future input
SDL_SetRelativeMouseMode((SDL_bool)!SDL_GetRelativeMouseMode());
setCaptureActive(!isCaptureActive());
// Force raise all keys to ensure they aren't stuck,
// since we won't get their key up events.
@@ -167,7 +282,7 @@ void SdlInputHandler::handleKeyEvent(SDL_KeyboardEvent* event)
// Check for the full-screen combo (Ctrl+Alt+Shift+X)
else if (event->keysym.scancode == SDL_SCANCODE_X) {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Detected full-screen toggle combo");
"Detected full-screen toggle combo (scancode)");
Session::s_ActiveSession->toggleFullscreen();
// Force raise all keys just be safe across this full-screen/windowed
@@ -177,7 +292,7 @@ void SdlInputHandler::handleKeyEvent(SDL_KeyboardEvent* event)
}
else if (event->keysym.scancode == SDL_SCANCODE_S) {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Detected stats toggle combo");
"Detected stats toggle combo (scancode)");
// Toggle the stats overlay
Session::get()->getOverlayManager().setOverlayState(Overlay::OverlayDebug,
@@ -190,7 +305,7 @@ void SdlInputHandler::handleKeyEvent(SDL_KeyboardEvent* event)
}
}
if (!SDL_GetRelativeMouseMode()) {
if (!isCaptureActive()) {
// Not capturing
return;
}
@@ -436,11 +551,11 @@ void SdlInputHandler::handleMouseButtonEvent(SDL_MouseButtonEvent* event)
// the pressed event was consumed by this code).
if (event->button == SDL_BUTTON_LEFT &&
event->state == SDL_RELEASED &&
!SDL_GetRelativeMouseMode()) {
SDL_SetRelativeMouseMode(SDL_TRUE);
!isCaptureActive()) {
setCaptureActive(true);
return;
}
else if (!SDL_GetRelativeMouseMode()) {
else if (!isCaptureActive()) {
// Not capturing
return;
}
@@ -481,7 +596,7 @@ void SdlInputHandler::handleMouseButtonEvent(SDL_MouseButtonEvent* event)
void SdlInputHandler::handleMouseMotionEvent(SDL_MouseMotionEvent* event)
{
if (!SDL_GetRelativeMouseMode()) {
if (!isCaptureActive()) {
// Not capturing
return;
}
@@ -498,7 +613,7 @@ void SdlInputHandler::handleMouseMotionEvent(SDL_MouseMotionEvent* event)
void SdlInputHandler::handleMouseWheelEvent(SDL_MouseWheelEvent* event)
{
if (!SDL_GetRelativeMouseMode()) {
if (!isCaptureActive()) {
// Not capturing
return;
}
@@ -587,46 +702,103 @@ Uint32 SdlInputHandler::mouseMoveTimerCallback(Uint32 interval, void *param)
return interval;
}
Uint32 SdlInputHandler::mouseEmulationTimerCallback(Uint32 interval, void *param)
{
auto gamepad = reinterpret_cast<GamepadState*>(param);
short rawX;
short rawY;
// Determine which analog stick is currently receiving the strongest input
if ((uint32_t)qAbs(gamepad->lsX) + qAbs(gamepad->lsY) > (uint32_t)qAbs(gamepad->rsX) + qAbs(gamepad->rsY)) {
rawX = gamepad->lsX;
rawY = -gamepad->lsY;
}
else {
rawX = gamepad->rsX;
rawY = -gamepad->rsY;
}
float deltaX;
float deltaY;
// Produce a base vector for mouse movement with increased speed as we deviate further from center
deltaX = qPow(rawX / 32766.0f * MOUSE_EMULATION_MOTION_MULTIPLIER, 3);
deltaY = qPow(rawY / 32766.0f * MOUSE_EMULATION_MOTION_MULTIPLIER, 3);
// Enforce deadzones
deltaX = qAbs(deltaX) > MOUSE_EMULATION_DEADZONE ? deltaX - MOUSE_EMULATION_DEADZONE : 0;
deltaY = qAbs(deltaY) > MOUSE_EMULATION_DEADZONE ? deltaY - MOUSE_EMULATION_DEADZONE : 0;
if (deltaX != 0 || deltaY != 0) {
LiSendMouseMoveEvent((short)deltaX, (short)deltaY);
}
return interval;
}
void SdlInputHandler::handleControllerAxisEvent(SDL_ControllerAxisEvent* event)
{
GamepadState* state = findStateForGamepad(event->which);
SDL_JoystickID gameControllerId = event->which;
GamepadState* state = findStateForGamepad(gameControllerId);
if (state == NULL) {
return;
}
switch (event->axis)
{
case SDL_CONTROLLER_AXIS_LEFTX:
state->lsX = event->value;
// Batch all pending axis motion events for this gamepad to save CPU time
SDL_Event nextEvent;
for (;;) {
switch (event->axis)
{
case SDL_CONTROLLER_AXIS_LEFTX:
state->lsX = event->value;
break;
case SDL_CONTROLLER_AXIS_LEFTY:
// Signed values have one more negative value than
// positive value, so inverting the sign on -32768
// could actually cause the value to overflow and
// wrap around to be negative again. Avoid that by
// capping the value at 32767.
state->lsY = -qMax(event->value, (short)-32767);
break;
case SDL_CONTROLLER_AXIS_RIGHTX:
state->rsX = event->value;
break;
case SDL_CONTROLLER_AXIS_RIGHTY:
state->rsY = -qMax(event->value, (short)-32767);
break;
case SDL_CONTROLLER_AXIS_TRIGGERLEFT:
state->lt = (unsigned char)(event->value * 255UL / 32767);
break;
case SDL_CONTROLLER_AXIS_TRIGGERRIGHT:
state->rt = (unsigned char)(event->value * 255UL / 32767);
break;
default:
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Unhandled controller axis: %d",
event->axis);
return;
}
// Check for another event to batch with
if (SDL_PeepEvents(&nextEvent, 1, SDL_PEEKEVENT, SDL_CONTROLLERAXISMOTION, SDL_CONTROLLERAXISMOTION) <= 0) {
break;
case SDL_CONTROLLER_AXIS_LEFTY:
// Signed values have one more negative value than
// positive value, so inverting the sign on -32768
// could actually cause the value to overflow and
// wrap around to be negative again. Avoid that by
// capping the value at 32767.
state->lsY = -qMax(event->value, (short)-32767);
}
event = &nextEvent.caxis;
if (event->which != gameControllerId) {
// Stop batching if a different gamepad interrupts us
break;
case SDL_CONTROLLER_AXIS_RIGHTX:
state->rsX = event->value;
break;
case SDL_CONTROLLER_AXIS_RIGHTY:
state->rsY = -qMax(event->value, (short)-32767);
break;
case SDL_CONTROLLER_AXIS_TRIGGERLEFT:
state->lt = (unsigned char)(event->value * 255UL / 32767);
break;
case SDL_CONTROLLER_AXIS_TRIGGERRIGHT:
state->rt = (unsigned char)(event->value * 255UL / 32767);
break;
default:
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Unhandled controller axis: %d",
event->axis);
return;
}
// Remove the next event to batch
SDL_PeepEvents(&nextEvent, 1, SDL_GETEVENT, SDL_CONTROLLERAXISMOTION, SDL_CONTROLLERAXISMOTION);
}
sendGamepadState(state);
// Only send the gamepad state to the host if it's not in mouse emulation mode
if (state->mouseEmulationTimer == 0) {
sendGamepadState(state);
}
}
void SdlInputHandler::handleControllerButtonEvent(SDL_ControllerButtonEvent* event)
@@ -638,9 +810,76 @@ void SdlInputHandler::handleControllerButtonEvent(SDL_ControllerButtonEvent* eve
if (event->state == SDL_PRESSED) {
state->buttons |= k_ButtonMap[event->button];
if (event->button == SDL_CONTROLLER_BUTTON_START) {
state->lastStartDownTime = SDL_GetTicks();
}
else if (state->mouseEmulationTimer != 0) {
if (event->button == SDL_CONTROLLER_BUTTON_A) {
LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, BUTTON_LEFT);
}
else if (event->button == SDL_CONTROLLER_BUTTON_B) {
LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, BUTTON_RIGHT);
}
else if (event->button == SDL_CONTROLLER_BUTTON_X) {
LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, BUTTON_MIDDLE);
}
else if (event->button == SDL_CONTROLLER_BUTTON_LEFTSHOULDER) {
LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, BUTTON_X1);
}
else if (event->button == SDL_CONTROLLER_BUTTON_RIGHTSHOULDER) {
LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, BUTTON_X2);
}
else if (event->button == SDL_CONTROLLER_BUTTON_DPAD_UP) {
LiSendScrollEvent(1);
}
else if (event->button == SDL_CONTROLLER_BUTTON_DPAD_DOWN) {
LiSendScrollEvent(-1);
}
}
}
else {
state->buttons &= ~k_ButtonMap[event->button];
if (event->button == SDL_CONTROLLER_BUTTON_START) {
if (SDL_GetTicks() - state->lastStartDownTime > MOUSE_EMULATION_LONG_PRESS_TIME) {
if (state->mouseEmulationTimer != 0) {
SDL_RemoveTimer(state->mouseEmulationTimer);
state->mouseEmulationTimer = 0;
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Mouse emulation deactivated");
Session::get()->notifyMouseEmulationMode(false);
}
else if (m_GamepadMouse) {
// Send the start button up event to the host, since we won't do it below
sendGamepadState(state);
state->mouseEmulationTimer = SDL_AddTimer(MOUSE_EMULATION_POLLING_INTERVAL, SdlInputHandler::mouseEmulationTimerCallback, state);
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Mouse emulation active");
Session::get()->notifyMouseEmulationMode(true);
}
}
}
else if (state->mouseEmulationTimer != 0) {
if (event->button == SDL_CONTROLLER_BUTTON_A) {
LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_LEFT);
}
else if (event->button == SDL_CONTROLLER_BUTTON_B) {
LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_RIGHT);
}
else if (event->button == SDL_CONTROLLER_BUTTON_X) {
LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_MIDDLE);
}
else if (event->button == SDL_CONTROLLER_BUTTON_LEFTSHOULDER) {
LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_X1);
}
else if (event->button == SDL_CONTROLLER_BUTTON_RIGHTSHOULDER) {
LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_X2);
}
}
}
// Handle Start+Select+L1+R1 as a gamepad quit combo
@@ -660,7 +899,10 @@ void SdlInputHandler::handleControllerButtonEvent(SDL_ControllerButtonEvent* eve
return;
}
sendGamepadState(state);
// Only send the gamepad state to the host if it's not in mouse emulation mode
if (state->mouseEmulationTimer == 0) {
sendGamepadState(state);
}
}
void SdlInputHandler::handleControllerDeviceEvent(SDL_ControllerDeviceEvent* event)
@@ -673,6 +915,7 @@ void SdlInputHandler::handleControllerDeviceEvent(SDL_ControllerDeviceEvent* eve
SDL_GameController* controller;
const char* mapping;
char guidStr[33];
uint32_t hapticCaps;
controller = SDL_GameControllerOpen(event->which);
if (controller == NULL) {
@@ -690,6 +933,7 @@ void SdlInputHandler::handleControllerDeviceEvent(SDL_ControllerDeviceEvent* eve
i = 0;
for (; i < MAX_GAMEPADS; i++) {
SDL_assert(m_GamepadState[i].controller != controller);
if (m_GamepadState[i].controller == NULL) {
// Found an empty slot
break;
@@ -714,22 +958,50 @@ void SdlInputHandler::handleControllerDeviceEvent(SDL_ControllerDeviceEvent* eve
state->controller = controller;
state->jsId = SDL_JoystickInstanceID(SDL_GameControllerGetJoystick(state->controller));
#if SDL_VERSION_ATLEAST(2, 0, 9)
// Perform a no-op rumble to see if haptics are supported
hapticCaps = SDL_GameControllerRumble(controller, 0, 0, 0) == 0 ?
ML_HAPTIC_GC_RUMBLE : 0;
#else
state->haptic = SDL_HapticOpenFromJoystick(SDL_GameControllerGetJoystick(state->controller));
state->hapticEffectId = -1;
if (state->haptic != nullptr && (SDL_HapticQuery(state->haptic) & SDL_HAPTIC_LEFTRIGHT) == 0) {
SDL_HapticClose(state->haptic);
state->haptic = nullptr;
state->hapticMethod = GAMEPAD_HAPTIC_METHOD_NONE;
if (state->haptic != nullptr) {
// Query for supported haptic effects
hapticCaps = SDL_HapticQuery(state->haptic);
hapticCaps |= SDL_HapticRumbleSupported(state->haptic) ?
ML_HAPTIC_SIMPLE_RUMBLE : 0;
if ((SDL_HapticQuery(state->haptic) & SDL_HAPTIC_LEFTRIGHT) == 0) {
if (SDL_HapticRumbleSupported(state->haptic)) {
if (SDL_HapticRumbleInit(state->haptic) == 0) {
state->hapticMethod = GAMEPAD_HAPTIC_METHOD_SIMPLERUMBLE;
}
}
if (state->hapticMethod == GAMEPAD_HAPTIC_METHOD_NONE) {
SDL_HapticClose(state->haptic);
state->haptic = nullptr;
}
} else {
state->hapticMethod = GAMEPAD_HAPTIC_METHOD_LEFTRIGHT;
}
}
else {
hapticCaps = 0;
}
#endif
SDL_JoystickGetGUIDString(SDL_JoystickGetGUID(SDL_GameControllerGetJoystick(state->controller)),
guidStr, sizeof(guidStr));
mapping = SDL_GameControllerMapping(state->controller);
name = SDL_GameControllerName(state->controller);
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Gamepad %d (player %d) is: %s (%s -> %s)",
"Gamepad %d (player %d) is: %s (haptic capabilities: 0x%x) (mapping: %s -> %s)",
i,
state->index,
name != nullptr ? name : "<null>",
hapticCaps,
guidStr,
mapping != nullptr ? mapping : "<null>");
if (mapping != nullptr) {
@@ -753,10 +1025,18 @@ void SdlInputHandler::handleControllerDeviceEvent(SDL_ControllerDeviceEvent* eve
else if (event->type == SDL_CONTROLLERDEVICEREMOVED) {
state = findStateForGamepad(event->which);
if (state != NULL) {
if (state->mouseEmulationTimer != 0) {
Session::get()->notifyMouseEmulationMode(false);
SDL_RemoveTimer(state->mouseEmulationTimer);
}
SDL_GameControllerClose(state->controller);
#if !SDL_VERSION_ATLEAST(2, 0, 9)
if (state->haptic != nullptr) {
SDL_HapticClose(state->haptic);
}
#endif
// Remove this from the gamepad mask in MC-mode
if (m_MultiController) {
@@ -817,6 +1097,11 @@ void SdlInputHandler::rumble(unsigned short controllerNumber, unsigned short low
return;
}
#if SDL_VERSION_ATLEAST(2, 0, 9)
if (m_GamepadState[controllerNumber].controller != nullptr) {
SDL_GameControllerRumble(m_GamepadState[controllerNumber].controller, lowFreqMotor, highFreqMotor, 30000);
}
#else
// Check if the controller supports haptics (and if the controller exists at all)
SDL_Haptic* haptic = m_GamepadState[controllerNumber].haptic;
if (haptic == nullptr) {
@@ -824,8 +1109,12 @@ void SdlInputHandler::rumble(unsigned short controllerNumber, unsigned short low
}
// Stop the last effect we played
if (m_GamepadState[controllerNumber].hapticEffectId >= 0) {
SDL_HapticDestroyEffect(haptic, m_GamepadState[controllerNumber].hapticEffectId);
if (m_GamepadState[controllerNumber].hapticMethod == GAMEPAD_HAPTIC_METHOD_LEFTRIGHT) {
if (m_GamepadState[controllerNumber].hapticEffectId >= 0) {
SDL_HapticDestroyEffect(haptic, m_GamepadState[controllerNumber].hapticEffectId);
}
} else if (m_GamepadState[controllerNumber].hapticMethod == GAMEPAD_HAPTIC_METHOD_SIMPLERUMBLE) {
SDL_HapticRumbleStop(haptic);
}
// If this callback is telling us to stop both motors, don't bother queuing a new effect
@@ -833,28 +1122,51 @@ void SdlInputHandler::rumble(unsigned short controllerNumber, unsigned short low
return;
}
SDL_HapticEffect effect;
SDL_memset(&effect, 0, sizeof(effect));
effect.type = SDL_HAPTIC_LEFTRIGHT;
if (m_GamepadState[controllerNumber].hapticMethod == GAMEPAD_HAPTIC_METHOD_LEFTRIGHT) {
SDL_HapticEffect effect;
SDL_memset(&effect, 0, sizeof(effect));
effect.type = SDL_HAPTIC_LEFTRIGHT;
// The effect should last until we are instructed to stop or change it
effect.leftright.length = SDL_HAPTIC_INFINITY;
// The effect should last until we are instructed to stop or change it
effect.leftright.length = SDL_HAPTIC_INFINITY;
// SDL haptics range from 0-32767 but XInput uses 0-65535, so divide by 2 to correct for SDL's scaling
effect.leftright.large_magnitude = lowFreqMotor / 2;
effect.leftright.small_magnitude = highFreqMotor / 2;
// SDL haptics range from 0-32767 but XInput uses 0-65535, so divide by 2 to correct for SDL's scaling
effect.leftright.large_magnitude = lowFreqMotor / 2;
effect.leftright.small_magnitude = highFreqMotor / 2;
// Play the new effect
m_GamepadState[controllerNumber].hapticEffectId = SDL_HapticNewEffect(haptic, &effect);
if (m_GamepadState[controllerNumber].hapticEffectId >= 0) {
SDL_HapticRunEffect(haptic, m_GamepadState[controllerNumber].hapticEffectId, 1);
// Play the new effect
m_GamepadState[controllerNumber].hapticEffectId = SDL_HapticNewEffect(haptic, &effect);
if (m_GamepadState[controllerNumber].hapticEffectId >= 0) {
SDL_HapticRunEffect(haptic, m_GamepadState[controllerNumber].hapticEffectId, 1);
}
} else if (m_GamepadState[controllerNumber].hapticMethod == GAMEPAD_HAPTIC_METHOD_SIMPLERUMBLE) {
SDL_HapticRumblePlay(haptic,
std::min(1.0, (GAMEPAD_HAPTIC_SIMPLE_HIFREQ_MOTOR_WEIGHT*highFreqMotor +
GAMEPAD_HAPTIC_SIMPLE_LOWFREQ_MOTOR_WEIGHT*lowFreqMotor) / 65535.0),
SDL_HAPTIC_INFINITY);
}
#endif
}
void SdlInputHandler::handleTouchFingerEvent(SDL_TouchFingerEvent* event)
{
int fingerIndex = -1;
#if SDL_VERSION_ATLEAST(2, 0, 10)
if (SDL_GetTouchDeviceType(event->touchId) != SDL_TOUCH_DEVICE_DIRECT) {
// Ignore anything that isn't a touchscreen. We may get callbacks
// for trackpads, but we want to handle those in the mouse path.
return;
}
#elif defined(Q_OS_DARWIN)
// SDL2 sends touch events from trackpads by default on
// macOS. This totally screws our actual mouse handling,
// so we must explicitly ignore touch events on macOS
// until SDL 2.0.10 where we have SDL_GetTouchDeviceType()
// to tell them apart.
return;
#endif
// Observations on Windows 10: x and y appear to be relative to 0,0 of the window client area.
// Although SDL documentation states they are 0.0 - 1.0 float values, they can actually be higher
// or lower than those values as touch events continue for touches started within the client area that
@@ -1023,6 +1335,35 @@ void SdlInputHandler::raiseAllKeys()
m_KeysDown.clear();
}
bool SdlInputHandler::isCaptureActive()
{
if (SDL_GetRelativeMouseMode()) {
return true;
}
// Some platforms don't support SDL_SetRelativeMouseMode
return m_FakeCaptureActive;
}
void SdlInputHandler::setCaptureActive(bool active)
{
if (active) {
// Try to activate SDL's relative mouse mode
if (SDL_SetRelativeMouseMode(SDL_TRUE) < 0) {
// Relative mouse mode didn't work, so we'll use fake capture
SDL_ShowCursor(SDL_DISABLE);
m_FakeCaptureActive = true;
}
}
else if (m_FakeCaptureActive) {
SDL_ShowCursor(SDL_ENABLE);
m_FakeCaptureActive = false;
}
else {
SDL_SetRelativeMouseMode(SDL_FALSE);
}
}
QString SdlInputHandler::getUnmappedGamepads()
{
QString ret;
+25 -2
View File
@@ -8,10 +8,17 @@
struct GamepadState {
SDL_GameController* controller;
SDL_JoystickID jsId;
SDL_Haptic* haptic;
int hapticEffectId;
short index;
#if !SDL_VERSION_ATLEAST(2, 0, 9)
SDL_Haptic* haptic;
int hapticMethod;
int hapticEffectId;
#endif
SDL_TimerID mouseEmulationTimer;
uint32_t lastStartDownTime;
short buttons;
short lsX, lsY;
short rsX, rsY;
@@ -21,6 +28,13 @@ struct GamepadState {
#define MAX_GAMEPADS 4
#define MAX_FINGERS 2
#define GAMEPAD_HAPTIC_METHOD_NONE 0
#define GAMEPAD_HAPTIC_METHOD_LEFTRIGHT 1
#define GAMEPAD_HAPTIC_METHOD_SIMPLERUMBLE 2
#define GAMEPAD_HAPTIC_SIMPLE_HIFREQ_MOTOR_WEIGHT 0.33
#define GAMEPAD_HAPTIC_SIMPLE_LOWFREQ_MOTOR_WEIGHT 0.8
class SdlInputHandler
{
public:
@@ -53,6 +67,10 @@ public:
void raiseAllKeys();
bool isCaptureActive();
void setCaptureActive(bool active);
static
QString getUnmappedGamepads();
@@ -74,13 +92,18 @@ private:
static
Uint32 mouseMoveTimerCallback(Uint32 interval, void* param);
static
Uint32 mouseEmulationTimerCallback(Uint32 interval, void* param);
bool m_MultiController;
bool m_GamepadMouse;
SDL_TimerID m_MouseMoveTimer;
SDL_atomic_t m_MouseDeltaX;
SDL_atomic_t m_MouseDeltaY;
int m_GamepadMask;
GamepadState m_GamepadState[MAX_GAMEPADS];
QSet<short> m_KeysDown;
bool m_FakeCaptureActive;
SDL_TouchFingerEvent m_TouchDownEvent[MAX_FINGERS];
float m_CumulativeDelta[MAX_FINGERS];
+198 -150
View File
@@ -1,6 +1,7 @@
#include "session.h"
#include "settings/streamingpreferences.h"
#include "streaming/streamutils.h"
#include "backend/richpresencemanager.h"
#include <Limelight.h>
#include <SDL.h>
@@ -11,7 +12,7 @@
#endif
#ifdef HAVE_SLVIDEO
#include "video/sl.h"
#include "video/slvid.h"
#endif
#ifdef Q_OS_WIN32
@@ -39,16 +40,8 @@ CONNECTION_LISTENER_CALLBACKS Session::k_ConnCallbacks = {
nullptr,
nullptr,
Session::clLogMessage,
Session::clRumble
};
AUDIO_RENDERER_CALLBACKS Session::k_AudioCallbacks = {
Session::arInit,
nullptr,
nullptr,
Session::arCleanup,
Session::arDecodeAndPlaySample,
CAPABILITY_DIRECT_SUBMIT
Session::clRumble,
Session::clConnectionStatusUpdate
};
Session* Session::s_ActiveSession;
@@ -114,19 +107,61 @@ void Session::clRumble(unsigned short controllerNumber, unsigned short lowFreqMo
SDL_AtomicUnlock(&s_ActiveSession->m_InputHandlerLock);
}
#define CALL_INITIALIZE(dec) (dec)->initialize(vds, window, videoFormat, width, height, frameRate, enableVsync, enableFramePacing)
void Session::clConnectionStatusUpdate(int connectionStatus)
{
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Connection status update: %d",
connectionStatus);
if (!s_ActiveSession->m_Preferences->connectionWarnings) {
return;
}
if (s_ActiveSession->m_MouseEmulationRefCount > 0) {
// Don't display the overlay if mouse emulation is already using it
return;
}
switch (connectionStatus)
{
case CONN_STATUS_POOR:
if (s_ActiveSession->m_StreamConfig.bitrate > 5000) {
strcpy(s_ActiveSession->m_OverlayManager.getOverlayText(Overlay::OverlayStatusUpdate), "Slow connection to PC\nReduce your bitrate");
}
else {
strcpy(s_ActiveSession->m_OverlayManager.getOverlayText(Overlay::OverlayStatusUpdate), "Poor connection to PC");
}
s_ActiveSession->m_OverlayManager.setOverlayTextUpdated(Overlay::OverlayStatusUpdate);
s_ActiveSession->m_OverlayManager.setOverlayState(Overlay::OverlayStatusUpdate, true);
break;
case CONN_STATUS_OKAY:
s_ActiveSession->m_OverlayManager.setOverlayState(Overlay::OverlayStatusUpdate, false);
break;
}
}
bool Session::chooseDecoder(StreamingPreferences::VideoDecoderSelection vds,
SDL_Window* window, int videoFormat, int width, int height,
int frameRate, bool enableVsync, bool enableFramePacing, bool testOnly, IVideoDecoder*& chosenDecoder)
{
DECODER_PARAMETERS params;
params.width = width;
params.height = height;
params.frameRate = frameRate;
params.videoFormat = videoFormat;
params.window = window;
params.enableVsync = enableVsync;
params.enableFramePacing = enableFramePacing;
params.vds = vds;
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"V-sync %s",
enableVsync ? "enabled" : "disabled");
#ifdef HAVE_SLVIDEO
chosenDecoder = new SLVideoDecoder(testOnly);
if (CALL_INITIALIZE(chosenDecoder)) {
if (chosenDecoder->initialize(&params)) {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"SLVideo video decoder chosen");
return true;
@@ -141,7 +176,7 @@ bool Session::chooseDecoder(StreamingPreferences::VideoDecoderSelection vds,
#ifdef HAVE_FFMPEG
chosenDecoder = new FFmpegVideoDecoder(testOnly);
if (CALL_INITIALIZE(chosenDecoder)) {
if (chosenDecoder->initialize(&params)) {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"FFmpeg-based video decoder chosen");
return true;
@@ -215,30 +250,13 @@ int Session::drSubmitDecodeUnit(PDECODE_UNIT du)
}
}
bool Session::isHardwareDecodeAvailable(StreamingPreferences::VideoDecoderSelection vds,
bool Session::isHardwareDecodeAvailable(SDL_Window* window,
StreamingPreferences::VideoDecoderSelection vds,
int videoFormat, int width, int height, int frameRate)
{
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, true, false, true, decoder)) {
SDL_DestroyWindow(window);
SDL_QuitSubSystem(SDL_INIT_VIDEO);
return false;
}
@@ -246,53 +264,31 @@ bool Session::isHardwareDecodeAvailable(StreamingPreferences::VideoDecoderSelect
delete decoder;
// This must be called after the decoder is deleted, because
// the renderer may want to interact with the window
SDL_DestroyWindow(window);
SDL_QuitSubSystem(SDL_INIT_VIDEO);
return ret;
}
int Session::getDecoderCapabilities(StreamingPreferences::VideoDecoderSelection vds,
int videoFormat, int width, int height, int frameRate)
bool Session::populateDecoderProperties(SDL_Window* window)
{
IVideoDecoder* decoder;
if (SDL_InitSubSystem(SDL_INIT_VIDEO) != 0) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"SDL_InitSubSystem(SDL_INIT_VIDEO) failed: %s",
SDL_GetError());
if (!chooseDecoder(m_Preferences->videoDecoderSelection,
window,
m_StreamConfig.enableHdr ? VIDEO_FORMAT_H265_MAIN10 :
(m_StreamConfig.supportsHevc ? VIDEO_FORMAT_H265 : VIDEO_FORMAT_H264),
m_StreamConfig.width,
m_StreamConfig.height,
m_StreamConfig.fps,
true, false, true, decoder)) {
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;
}
m_VideoCallbacks.capabilities = decoder->getDecoderCapabilities();
if (!chooseDecoder(vds, window, videoFormat, width, height, frameRate, true, false, true, decoder)) {
SDL_DestroyWindow(window);
SDL_QuitSubSystem(SDL_INIT_VIDEO);
return false;
}
int caps = decoder->getDecoderCapabilities();
m_StreamConfig.colorSpace = decoder->getDecoderColorspace();
delete decoder;
// This must be called after the decoder is deleted, because
// the renderer may want to interact with the window
SDL_DestroyWindow(window);
SDL_QuitSubSystem(SDL_INIT_VIDEO);
return caps;
return true;
}
Session::Session(NvComputer* computer, NvApp& app, StreamingPreferences *preferences)
@@ -310,6 +306,7 @@ Session::Session(NvComputer* computer, NvApp& app, StreamingPreferences *prefere
m_UnexpectedTermination(true), // Failure prior to streaming is unexpected
m_InputHandler(nullptr),
m_InputHandlerLock(0),
m_MouseEmulationRefCount(0),
m_OpusDecoder(nullptr),
m_AudioRenderer(nullptr),
m_AudioSampleCount(0),
@@ -327,8 +324,25 @@ Session::~Session()
s_ActiveSessionSemaphore.release();
}
void Session::initialize()
bool Session::initialize()
{
if (SDL_InitSubSystem(SDL_INIT_VIDEO) != 0) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"SDL_InitSubSystem(SDL_INIT_VIDEO) failed: %s",
SDL_GetError());
return false;
}
// Create a hidden window to use for decoder initialization tests
SDL_Window* testWindow = SDL_CreateWindow("", 0, 0, 1280, 720, SDL_WINDOW_HIDDEN);
if (!testWindow) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Failed to create window for hardware decode test: %s",
SDL_GetError());
SDL_QuitSubSystem(SDL_INIT_VIDEO);
return false;
}
qInfo() << "Server GPU:" << m_Computer->gpuModel;
qInfo() << "Server GFE version:" << m_Computer->gfeVersion;
@@ -336,21 +350,12 @@ void Session::initialize()
m_VideoCallbacks.setup = drSetup;
m_VideoCallbacks.submitDecodeUnit = drSubmitDecodeUnit;
// Slice up to 4 times for parallel decode, once slice per core
int slices = qMin(MAX_SLICES, SDL_GetCPUCount());
m_VideoCallbacks.capabilities |= CAPABILITY_SLICES_PER_FRAME(slices);
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Encoder configured for %d slices per frame",
slices);
LiInitializeStreamConfiguration(&m_StreamConfig);
m_StreamConfig.width = m_Preferences->width;
m_StreamConfig.height = m_Preferences->height;
m_StreamConfig.fps = m_Preferences->fps;
m_StreamConfig.bitrate = m_Preferences->bitrateKbps;
m_StreamConfig.hevcBitratePercentageMultiplier = 75;
m_StreamConfig.streamingRemotely = STREAM_CFG_AUTO;
m_StreamConfig.packetSize = 1392;
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Video bitrate: %d kbps",
@@ -372,6 +377,12 @@ void Session::initialize()
break;
}
LiInitializeAudioCallbacks(&m_AudioCallbacks);
m_AudioCallbacks.init = arInit;
m_AudioCallbacks.cleanup = arCleanup;
m_AudioCallbacks.decodeAndPlaySample = arDecodeAndPlaySample;
m_AudioCallbacks.capabilities = getAudioRendererCapabilities(m_StreamConfig.audioConfiguration);
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Audio configuration: %d",
m_StreamConfig.audioConfiguration);
@@ -381,7 +392,8 @@ void Session::initialize()
case StreamingPreferences::VCC_AUTO:
// TODO: Determine if HEVC is better depending on the decoder
m_StreamConfig.supportsHevc =
isHardwareDecodeAvailable(m_Preferences->videoDecoderSelection,
isHardwareDecodeAvailable(testWindow,
m_Preferences->videoDecoderSelection,
VIDEO_FORMAT_H265,
m_StreamConfig.width,
m_StreamConfig.height,
@@ -420,14 +432,33 @@ void Session::initialize()
switch (m_Preferences->windowMode)
{
default:
case StreamingPreferences::WM_FULLSCREEN_DESKTOP:
m_FullScreenFlag = SDL_WINDOW_FULLSCREEN_DESKTOP;
break;
case StreamingPreferences::WM_FULLSCREEN:
default:
m_FullScreenFlag = SDL_WINDOW_FULLSCREEN;
break;
}
// Check for validation errors/warnings and emit
// signals for them, if appropriate
bool ret = validateLaunch(testWindow);
if (ret) {
// Populate decoder-dependent properties.
// Must be done after validateLaunch() since m_StreamConfig is finalized.
ret = populateDecoderProperties(testWindow);
}
SDL_DestroyWindow(testWindow);
if (!ret) {
SDL_QuitSubSystem(SDL_INIT_VIDEO);
return false;
}
return true;
}
void Session::emitLaunchWarning(QString text)
@@ -446,7 +477,7 @@ void Session::emitLaunchWarning(QString text)
}
}
bool Session::validateLaunch()
bool Session::validateLaunch(SDL_Window* testWindow)
{
QStringList warningList;
@@ -466,12 +497,14 @@ bool Session::validateLaunch()
bool hevcForced = m_Preferences->videoCodecConfig == StreamingPreferences::VCC_FORCE_HEVC ||
m_Preferences->videoCodecConfig == StreamingPreferences::VCC_FORCE_HEVC_HDR;
if (!isHardwareDecodeAvailable(m_Preferences->videoDecoderSelection,
VIDEO_FORMAT_H265,
m_StreamConfig.width,
m_StreamConfig.height,
m_StreamConfig.fps) &&
m_Preferences->videoDecoderSelection == StreamingPreferences::VDS_AUTO) {
if (m_Preferences->videoDecoderSelection == StreamingPreferences::VDS_AUTO && // Force hardware decoding checked below
m_Preferences->videoCodecConfig != StreamingPreferences::VCC_AUTO && // Already checked in initialize()
!isHardwareDecodeAvailable(testWindow,
m_Preferences->videoDecoderSelection,
VIDEO_FORMAT_H265,
m_StreamConfig.width,
m_StreamConfig.height,
m_StreamConfig.fps)) {
if (hevcForced) {
emitLaunchWarning("Using software decoding due to your selection to force HEVC without GPU support. This may cause poor streaming performance.");
}
@@ -507,7 +540,8 @@ bool Session::validateLaunch()
emitLaunchWarning("Your host PC GPU doesn't support HDR streaming. "
"A GeForce GTX 1000-series (Pascal) or later GPU is required for HDR streaming.");
}
else if (!isHardwareDecodeAvailable(m_Preferences->videoDecoderSelection,
else if (!isHardwareDecodeAvailable(testWindow,
m_Preferences->videoDecoderSelection,
VIDEO_FORMAT_H265_MAIN10,
m_StreamConfig.width,
m_StreamConfig.height,
@@ -556,7 +590,9 @@ bool Session::validateLaunch()
}
if (m_Preferences->videoDecoderSelection == StreamingPreferences::VDS_FORCE_HARDWARE &&
!isHardwareDecodeAvailable(m_Preferences->videoDecoderSelection,
!m_StreamConfig.enableHdr && // HEVC Main10 was already checked for hardware decode support above
!isHardwareDecodeAvailable(testWindow,
m_Preferences->videoDecoderSelection,
m_StreamConfig.supportsHevc ? VIDEO_FORMAT_H265 : VIDEO_FORMAT_H264,
m_StreamConfig.width,
m_StreamConfig.height,
@@ -572,13 +608,6 @@ bool Session::validateLaunch()
return false;
}
// Add the capability flags from the chosen decoder/renderer
m_VideoCallbacks.capabilities |= getDecoderCapabilities(m_Preferences->videoDecoderSelection,
m_StreamConfig.supportsHevc ? VIDEO_FORMAT_H265 : VIDEO_FORMAT_H264,
m_StreamConfig.width,
m_StreamConfig.height,
m_StreamConfig.fps);
return true;
}
@@ -792,6 +821,22 @@ void Session::toggleFullscreen()
}
}
void Session::notifyMouseEmulationMode(bool enabled)
{
m_MouseEmulationRefCount += enabled ? 1 : -1;
SDL_assert(m_MouseEmulationRefCount >= 0);
// We re-use the status update overlay for mouse mode notification
if (m_MouseEmulationRefCount > 0) {
strcpy(m_OverlayManager.getOverlayText(Overlay::OverlayStatusUpdate), "Gamepad mouse mode active\nLong press Start to deactivate");
m_OverlayManager.setOverlayTextUpdated(Overlay::OverlayStatusUpdate);
m_OverlayManager.setOverlayState(Overlay::OverlayStatusUpdate, true);
}
else {
m_OverlayManager.setOverlayState(Overlay::OverlayStatusUpdate, false);
}
}
void Session::exec(int displayOriginX, int displayOriginY)
{
m_DisplayOriginX = displayOriginX;
@@ -800,11 +845,7 @@ void Session::exec(int displayOriginX, int displayOriginY)
// Complete initialization in this deferred context to avoid
// calling expensive functions in the constructor (during the
// process of loading the StreamSegue).
initialize();
// Check for validation errors/warnings and emit
// signals for them, if appropriate
if (!validateLaunch()) {
if (!initialize()) {
emit sessionFinished();
return;
}
@@ -864,29 +905,19 @@ void Session::exec(int displayOriginX, int displayOriginY)
} catch (const GfeHttpResponseException& e) {
delete m_InputHandler;
m_InputHandler = nullptr;
emit displayLaunchError(e.toQString());
SDL_QuitSubSystem(SDL_INIT_VIDEO);
emit displayLaunchError("GeForce Experience returned error: " + e.toQString());
QThreadPool::globalInstance()->start(new DeferredSessionCleanupTask(this));
return;
} catch (const QtNetworkReplyException& e) {
delete m_InputHandler;
m_InputHandler = nullptr;
SDL_QuitSubSystem(SDL_INIT_VIDEO);
emit displayLaunchError(e.toQString());
QThreadPool::globalInstance()->start(new DeferredSessionCleanupTask(this));
return;
}
SDL_assert(!SDL_WasInit(SDL_INIT_VIDEO));
if (SDL_InitSubSystem(SDL_INIT_VIDEO) != 0) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"SDL_InitSubSystem(SDL_INIT_VIDEO) failed: %s",
SDL_GetError());
delete m_InputHandler;
m_InputHandler = nullptr;
emit displayLaunchError(QString::fromLocal8Bit(SDL_GetError()));
QThreadPool::globalInstance()->start(new DeferredSessionCleanupTask(this));
return;
}
QByteArray hostnameStr = m_Computer->activeAddress.toLatin1();
QByteArray siAppVersion = m_Computer->appVersion.toLatin1();
@@ -903,9 +934,22 @@ void Session::exec(int displayOriginX, int displayOriginY)
hostInfo.serverInfoGfeVersion = siGfeVersion.data();
}
// isReachableOverVpn() does network I/O, so we only attempt to check
// VPN reachability if we've already contacted the PC successfully
if (m_Computer->isReachableOverVpn()) {
// It looks like our route to this PC is over a VPN.
// Treat it as remote even if the target address is in RFC 1918 address space.
m_StreamConfig.streamingRemotely = STREAM_CFG_REMOTE;
m_StreamConfig.packetSize = 1024;
}
else {
m_StreamConfig.streamingRemotely = STREAM_CFG_AUTO;
m_StreamConfig.packetSize = 1392;
}
int err = LiStartConnection(&hostInfo, &m_StreamConfig, &k_ConnCallbacks,
&m_VideoCallbacks,
m_AudioDisabled ? nullptr : &k_AudioCallbacks,
m_AudioDisabled ? nullptr : &m_AudioCallbacks,
NULL, 0, NULL, 0);
if (err != 0) {
// We already displayed an error dialog in the stage failure
@@ -990,7 +1034,11 @@ void Session::exec(int displayOriginX, int displayOriginY)
// This prevents the mouse from becoming trapped inside
// Moonlight when it's halted at a debug break.
if (m_Preferences->windowMode != StreamingPreferences::WM_WINDOWED) {
SDL_SetRelativeMouseMode(SDL_TRUE);
// HACK: This doesn't work on Wayland until we render a frame, so
// just don't do it for now.
if (strcmp(SDL_GetCurrentVideoDriver(), "wayland") != 0) {
m_InputHandler->setCaptureActive(true);
}
}
#endif
@@ -1007,14 +1055,6 @@ void Session::exec(int displayOriginX, int displayOriginY)
// sleep precision and more accurate callback timing.
SDL_SetHint(SDL_HINT_TIMER_RESOLUTION, "1");
// Raise the priority of the main thread, since it handles
// time-sensitive video rendering
if (SDL_SetThreadPriority(SDL_THREAD_PRIORITY_HIGH) < 0) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"Unable to set main thread to high priority: %s",
SDL_GetError());
}
int currentDisplayIndex = SDL_GetWindowDisplayIndex(m_Window);
// Now that we're about to stream, any SDL_QUIT event is expected
@@ -1022,6 +1062,9 @@ void Session::exec(int displayOriginX, int displayOriginY)
// (m_UnexpectedTermination is set back to true).
m_UnexpectedTermination = false;
// Start rich presence to indicate we're in game
RichPresenceManager presence(prefs, m_App.name);
// Hijack this thread to be the SDL main thread. We have to do this
// because we want to suspend all Qt processing until the stream is over.
SDL_Event event;
@@ -1031,7 +1074,14 @@ void Session::exec(int displayOriginX, int displayOriginY)
// blocks this thread too long for high polling rate mice and high
// refresh rate displays.
if (!SDL_PollEvent(&event)) {
#ifndef STEAM_LINK
SDL_Delay(1);
#else
// Waking every 1 ms to process input is too much for the low performance
// ARM core in the Steam Link, so we will wait 10 ms instead.
SDL_Delay(10);
#endif
presence.runCallbacks();
continue;
}
switch (event.type) {
@@ -1039,25 +1089,11 @@ void Session::exec(int displayOriginX, int displayOriginY)
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Quit event received");
goto DispatchDeferredCleanup;
case SDL_USEREVENT: {
SDL_Event nextEvent;
case SDL_USEREVENT:
SDL_assert(event.user.code == SDL_CODE_FRAME_READY);
// Drop any earlier frames
while (SDL_PeepEvents(&nextEvent,
1,
SDL_GETEVENT,
SDL_USEREVENT,
SDL_USEREVENT) == 1) {
m_VideoDecoder->dropFrame(&event.user);
event = nextEvent;
}
// Render the last frame
m_VideoDecoder->renderFrame(&event.user);
m_VideoDecoder->renderFrameOnMainThread();
break;
}
case SDL_WINDOWEVENT:
// Capture mouse cursor when user actives the window by clicking on
@@ -1079,20 +1115,19 @@ void Session::exec(int displayOriginX, int displayOriginY)
SDL_GetWindowPosition(m_Window, &x, &y);
SDL_GetWindowSize(m_Window, &width, &height);
if (mouseX > x && mouseX < x+width && mouseY > y && mouseY < y+height) {
SDL_SetRelativeMouseMode(SDL_TRUE);
m_InputHandler->setCaptureActive(true);
}
}
}
#endif
if (event.window.event == SDL_WINDOWEVENT_FOCUS_LOST) {
// Release mouse cursor when another window is activated (e.g. by using ALT+TAB).
// This lets user to interact with our window's title bar and with the buttons in it.
// Doing this while the window is full-screen breaks the transition out of FS
// (desktop and exclusive), so we must check for that before releasing mouse capture.
if (!(SDL_GetWindowFlags(m_Window) & SDL_WINDOW_FULLSCREEN)) {
SDL_SetRelativeMouseMode(SDL_FALSE);
m_InputHandler->setCaptureActive(false);
}
// Raise all keys that are currently pressed. If we don't do this, certain keys
@@ -1214,24 +1249,18 @@ void Session::exec(int displayOriginX, int displayOriginY)
case SDL_JOYDEVICEADDED:
m_InputHandler->handleJoystickArrivalEvent(&event.jdevice);
break;
// SDL2 sends touch events from trackpads by default on
// macOS. This totally screws our actual mouse handling,
// so we must explicitly ignore touch events on macOS.
#ifndef Q_OS_DARWIN
case SDL_FINGERDOWN:
case SDL_FINGERMOTION:
case SDL_FINGERUP:
m_InputHandler->handleTouchFingerEvent(&event.tfinger);
break;
#endif
}
}
DispatchDeferredCleanup:
// Uncapture the mouse and hide the window immediately,
// so we can return to the Qt GUI ASAP.
SDL_SetRelativeMouseMode(SDL_FALSE);
m_InputHandler->setCaptureActive(false);
SDL_EnableScreenSaver();
SDL_SetHint(SDL_HINT_TIMER_RESOLUTION, "0");
@@ -1253,6 +1282,25 @@ DispatchDeferredCleanup:
m_VideoDecoder = nullptr;
SDL_AtomicUnlock(&m_DecoderLock);
if (strcmp(SDL_GetCurrentVideoDriver(), "wayland") == 0) {
// HACK: SDL (as of 2.0.10) has a bug that causes Mutter not to destroy the window
// surface when in full-screen unless we render more frames after we request
// to exit full-screen. The amount of frames required is variable but 500 ms
// of frames seems sufficient in my testing.
SDL_SetWindowFullscreen(m_Window, 0);
SDL_Renderer* renderer = SDL_CreateRenderer(m_Window, -1, SDL_RENDERER_PRESENTVSYNC);
if (renderer != nullptr) {
SDL_SetRenderDrawColor(renderer, 0, 0, 0, SDL_ALPHA_OPAQUE);
for (int i = 0; i < 10; i++)
{
SDL_RenderClear(renderer);
SDL_RenderPresent(renderer);
SDL_Delay(50);
}
SDL_DestroyRenderer(renderer);
}
}
// This must be called after the decoder is deleted, because
// the renderer may want to interact with the window
SDL_DestroyWindow(m_Window);
@@ -1260,8 +1308,8 @@ DispatchDeferredCleanup:
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
+15 -13
View File
@@ -25,13 +25,10 @@ public:
Q_INVOKABLE void exec(int displayOriginX, int displayOriginY);
static
bool isHardwareDecodeAvailable(StreamingPreferences::VideoDecoderSelection vds,
bool isHardwareDecodeAvailable(SDL_Window* window,
StreamingPreferences::VideoDecoderSelection vds,
int videoFormat, int width, int height, int frameRate);
static
int getDecoderCapabilities(StreamingPreferences::VideoDecoderSelection vds,
int videoFormat, int width, int height, int frameRate);
static Session* get()
{
return s_ActiveSession;
@@ -58,25 +55,27 @@ signals:
void sessionFinished();
private:
void initialize();
bool initialize();
bool validateLaunch();
bool validateLaunch(SDL_Window* testWindow);
void emitLaunchWarning(QString text);
int getDecoderCapabilities();
bool populateDecoderProperties(SDL_Window* window);
IAudioRenderer* createAudioRenderer();
int detectAudioConfiguration();
IAudioRenderer* createAudioRenderer(const POPUS_MULTISTREAM_CONFIGURATION opusConfig);
bool testAudio(int audioConfiguration);
int getAudioRendererCapabilities(int audioConfiguration);
void getWindowDimensions(int& x, int& y,
int& width, int& height);
void toggleFullscreen();
void notifyMouseEmulationMode(bool enabled);
void updateOptimalWindowDisplayMode();
static
@@ -101,6 +100,9 @@ private:
static
void clRumble(unsigned short controllerNumber, unsigned short lowFreqMotor, unsigned short highFreqMotor);
static
void clConnectionStatusUpdate(int connectionStatus);
static
int arInit(int audioConfiguration,
const POPUS_MULTISTREAM_CONFIGURATION opusConfig,
@@ -124,6 +126,7 @@ private:
StreamingPreferences* m_Preferences;
STREAM_CONFIGURATION m_StreamConfig;
DECODER_RENDERER_CALLBACKS m_VideoCallbacks;
AUDIO_RENDERER_CALLBACKS m_AudioCallbacks;
NvComputer* m_Computer;
NvApp m_App;
SDL_Window* m_Window;
@@ -138,6 +141,7 @@ private:
bool m_UnexpectedTermination;
SdlInputHandler* m_InputHandler;
SDL_SpinLock m_InputHandlerLock;
int m_MouseEmulationRefCount;
int m_ActiveVideoFormat;
int m_ActiveVideoWidth;
@@ -145,7 +149,6 @@ private:
int m_ActiveVideoFrameRate;
OpusMSDecoder* m_OpusDecoder;
short m_OpusDecodeBuffer[MAX_CHANNELS * SAMPLES_PER_FRAME];
IAudioRenderer* m_AudioRenderer;
OPUS_MULTISTREAM_CONFIGURATION m_AudioConfig;
int m_AudioSampleCount;
@@ -153,7 +156,6 @@ private:
Overlay::OverlayManager m_OverlayManager;
static AUDIO_RENDERER_CALLBACKS k_AudioCallbacks;
static CONNECTION_LISTENER_CALLBACKS k_ConnCallbacks;
static Session* s_ActiveSession;
static QSemaphore s_ActiveSessionSemaphore;
+15 -23
View File
@@ -26,33 +26,25 @@ typedef struct _VIDEO_STATS {
uint32_t measurementStartTimestamp;
} VIDEO_STATS, *PVIDEO_STATS;
typedef struct _DECODER_PARAMETERS {
SDL_Window* window;
StreamingPreferences::VideoDecoderSelection vds;
int videoFormat;
int width;
int height;
int frameRate;
bool enableVsync;
bool enableFramePacing;
} DECODER_PARAMETERS, *PDECODER_PARAMETERS;
class IVideoDecoder {
public:
virtual ~IVideoDecoder() {}
virtual bool initialize(StreamingPreferences::VideoDecoderSelection vds,
SDL_Window* window,
int videoFormat,
int width,
int height,
int frameRate,
bool enableVsync,
bool enableFramePacing) = 0;
virtual bool initialize(PDECODER_PARAMETERS params) = 0;
virtual bool isHardwareAccelerated() = 0;
virtual int getDecoderCapabilities() = 0;
virtual int getDecoderColorspace() = 0;
virtual int submitDecodeUnit(PDECODE_UNIT du) = 0;
virtual void renderFrame(SDL_UserEvent* event) = 0;
virtual void dropFrame(SDL_UserEvent* event) = 0;
virtual void queueFrame(void* data1 = nullptr,
void* data2 = nullptr)
{
SDL_Event event;
event.type = SDL_USEREVENT;
event.user.code = SDL_CODE_FRAME_READY;
event.user.data1 = data1;
event.user.data2 = data2;
SDL_PushEvent(&event);
}
virtual void renderFrameOnMainThread() = 0;
};
@@ -0,0 +1,57 @@
#include "cuda.h"
CUDARenderer::CUDARenderer()
: m_HwContext(nullptr)
{
}
CUDARenderer::~CUDARenderer()
{
if (m_HwContext != nullptr) {
av_buffer_unref(&m_HwContext);
}
}
bool CUDARenderer::initialize(PDECODER_PARAMETERS)
{
int err;
err = av_hwdevice_ctx_create(&m_HwContext, AV_HWDEVICE_TYPE_CUDA, nullptr, nullptr, 0);
if (err != 0) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"av_hwdevice_ctx_create(CUDA) failed: %d",
err);
return false;
}
return true;
}
bool CUDARenderer::prepareDecoderContext(AVCodecContext* context)
{
context->hw_device_ctx = av_buffer_ref(m_HwContext);
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Using CUDA accelerated decoder");
return true;
}
void CUDARenderer::renderFrame(AVFrame*)
{
// We only support indirect rendering
SDL_assert(false);
}
bool CUDARenderer::needsTestFrame()
{
return true;
}
bool CUDARenderer::isDirectRenderingSupported()
{
// We only support rendering via SDL read-back
return false;
}
@@ -0,0 +1,18 @@
#pragma once
#include "renderer.h"
class CUDARenderer : public IFFmpegRenderer {
public:
CUDARenderer();
virtual ~CUDARenderer() override;
virtual bool initialize(PDECODER_PARAMETERS) override;
virtual bool prepareDecoderContext(AVCodecContext* context) override;
virtual void renderFrame(AVFrame* frame) override;
virtual bool needsTestFrame() override;
virtual bool isDirectRenderingSupported() override;
private:
AVBufferRef* m_HwContext;
};
@@ -0,0 +1,268 @@
#include "drm.h"
extern "C" {
#include <libavutil/hwcontext_drm.h>
}
#include <libdrm/drm_fourcc.h>
#include <unistd.h>
#include <fcntl.h>
#include "streaming/streamutils.h"
#include "streaming/session.h"
#include <Limelight.h>
DrmRenderer::DrmRenderer()
: m_DrmFd(-1),
m_CrtcId(0),
m_PlaneId(0),
m_CurrentFbId(0)
{
}
DrmRenderer::~DrmRenderer()
{
if (m_CurrentFbId != 0) {
drmModeRmFB(m_DrmFd, m_CurrentFbId);
}
if (m_DrmFd != -1) {
close(m_DrmFd);
}
}
bool DrmRenderer::prepareDecoderContext(AVCodecContext*)
{
/* Nothing to do */
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Using DRM renderer");
return true;
}
bool DrmRenderer::initialize(PDECODER_PARAMETERS)
{
const char* device = SDL_getenv("DRM_DEV");
int i;
if (device == nullptr) {
device = "/dev/dri/card0";
}
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Opening DRM device: %s",
device);
m_DrmFd = open(device, O_RDWR | O_CLOEXEC);
if (m_DrmFd < 0) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Failed to open DRM device: %d",
errno);
return false;
}
drmModeRes* resources = drmModeGetResources(m_DrmFd);
if (resources == nullptr) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"drmModeGetResources() failed: %d",
errno);
return false;
}
// Look for a connected connector and get the associated encoder
uint32_t encoderId = 0;
for (i = 0; i < resources->count_connectors && encoderId == 0; i++) {
drmModeConnector* connector = drmModeGetConnector(m_DrmFd, resources->connectors[i]);
if (connector != nullptr) {
if (connector->connection == DRM_MODE_CONNECTED && connector->count_modes > 0) {
encoderId = connector->encoder_id;
}
drmModeFreeConnector(connector);
}
}
if (encoderId == 0) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"No connected displays found!");
drmModeFreeResources(resources);
return false;
}
// Now find the CRTC from the encoder
m_CrtcId = 0;
for (i = 0; i < resources->count_encoders && m_CrtcId == 0; i++) {
drmModeEncoder* encoder = drmModeGetEncoder(m_DrmFd, resources->encoders[i]);
if (encoder != nullptr) {
if (encoder->encoder_id == encoderId) {
m_CrtcId = encoder->crtc_id;
}
drmModeFreeEncoder(encoder);
}
}
if (m_CrtcId == 0) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"DRM encoder not found!");
drmModeFreeResources(resources);
return false;
}
int crtcIndex = -1;
for (int i = 0; i < resources->count_crtcs; i++) {
if (resources->crtcs[i] == m_CrtcId) {
drmModeCrtc* crtc = drmModeGetCrtc(m_DrmFd, resources->crtcs[i]);
crtcIndex = i;
m_OutputRect.x = m_OutputRect.y = 0;
m_OutputRect.w = crtc->width;
m_OutputRect.h = crtc->height;
drmModeFreeCrtc(crtc);
break;
}
}
drmModeFreeResources(resources);
if (crtcIndex == -1) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Failed to get CRTC!");
return false;
}
drmSetClientCap(m_DrmFd, DRM_CLIENT_CAP_UNIVERSAL_PLANES, 1);
drmModePlaneRes* planeRes = drmModeGetPlaneResources(m_DrmFd);
if (planeRes == nullptr) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"drmGetPlaneResources() failed: %d",
errno);
return false;
}
// Find an NV12 overlay plane to render on
m_PlaneId = 0;
for (uint32_t i = 0; i < planeRes->count_planes && m_PlaneId == 0; i++) {
drmModePlane* plane = drmModeGetPlane(m_DrmFd, planeRes->planes[i]);
if (plane != nullptr) {
bool matchingFormat = false;
for (uint32_t j = 0; j < plane->count_formats; j++) {
if (plane->formats[j] == DRM_FORMAT_NV12) {
matchingFormat = true;
break;
}
}
if (matchingFormat == false) {
drmModeFreePlane(plane);
continue;
}
if ((plane->possible_crtcs & (1 << crtcIndex)) && plane->crtc_id == 0) {
drmModeObjectPropertiesPtr props = drmModeObjectGetProperties(m_DrmFd, planeRes->planes[i], DRM_MODE_OBJECT_PLANE);
if (props != nullptr) {
for (uint32_t j = 0; j < props->count_props && m_PlaneId == 0; j++) {
drmModePropertyPtr prop = drmModeGetProperty(m_DrmFd, props->props[j]);
if (prop != nullptr) {
if (!strcmp(prop->name, "type") && props->prop_values[j] == DRM_PLANE_TYPE_OVERLAY) {
m_PlaneId = plane->plane_id;
}
drmModeFreeProperty(prop);
}
}
drmModeFreeObjectProperties(props);
}
}
drmModeFreePlane(plane);
}
}
drmModeFreePlaneResources(planeRes);
return true;
}
enum AVPixelFormat DrmRenderer::getPreferredPixelFormat(int)
{
// DRM PRIME buffers
return AV_PIX_FMT_DRM_PRIME;
}
void DrmRenderer::renderFrame(AVFrame* frame)
{
AVDRMFrameDescriptor* drmFrame = (AVDRMFrameDescriptor*)frame->data[0];
int err;
uint32_t primeHandle;
uint32_t handles[4] = {};
uint32_t pitches[4] = {};
uint32_t offsets[4] = {};
SDL_Rect src, dst;
src.x = src.y = 0;
src.w = frame->width;
src.h = frame->height;
dst = m_OutputRect;
StreamUtils::scaleSourceToDestinationSurface(&src, &dst);
// Convert the FD in the AVDRMFrameDescriptor to a PRIME handle
// that can be used in drmModeAddFB2()
SDL_assert(drmFrame->nb_objects == 1);
err = drmPrimeFDToHandle(m_DrmFd, drmFrame->objects[0].fd, &primeHandle);
if (err < 0) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"drmPrimeFDToHandle() failed: %d",
errno);
return;
}
SDL_assert(drmFrame->nb_layers == 1);
SDL_assert(drmFrame->layers[0].nb_planes == 2);
for (int i = 0; i < drmFrame->layers[0].nb_planes; i++) {
handles[i] = primeHandle;
pitches[i] = drmFrame->layers[0].planes[i].pitch;
offsets[i] = drmFrame->layers[0].planes[i].offset;
}
// Remember the last FB object we created so we can free it
// when we are finished rendering this one (if successful).
uint32_t lastFbId = m_CurrentFbId;
// Create a frame buffer object from the PRIME buffer
err = drmModeAddFB2(m_DrmFd, frame->width, frame->height,
drmFrame->layers[0].format,
handles, pitches, offsets, &m_CurrentFbId, 0);
if (err < 0) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"drmModeAddFB2() failed: %d",
errno);
m_CurrentFbId = lastFbId;
return;
}
// Update the overlay
err = drmModeSetPlane(m_DrmFd, m_PlaneId, m_CrtcId, m_CurrentFbId, 0,
dst.x, dst.y,
dst.w, dst.h,
0, 0,
frame->width << 16,
frame->height << 16);
if (err < 0) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"drmModeSetPlane() failed: %d",
errno);
drmModeRmFB(m_DrmFd, m_CurrentFbId);
m_CurrentFbId = lastFbId;
return;
}
// Free the previous FB object which has now been superseded
drmModeRmFB(m_DrmFd, lastFbId);
}
@@ -0,0 +1,24 @@
#pragma once
#include "renderer.h"
#include <xf86drm.h>
#include <xf86drmMode.h>
class DrmRenderer : public IFFmpegRenderer {
public:
DrmRenderer();
virtual ~DrmRenderer() override;
virtual bool initialize(PDECODER_PARAMETERS params) override;
virtual bool prepareDecoderContext(AVCodecContext* context) override;
virtual void renderFrame(AVFrame* frame) override;
virtual enum AVPixelFormat getPreferredPixelFormat(int videoFormat) override;
private:
int m_DrmFd;
uint32_t m_CrtcId;
uint32_t m_PlaneId;
uint32_t m_CurrentFbId;
SDL_Rect m_OutputRect;
};
+115 -47
View File
@@ -28,6 +28,7 @@ DXVA2Renderer::DXVA2Renderer() :
m_Processor(nullptr),
m_FrameIndex(0),
m_DebugOverlayFont(nullptr),
m_StatusOverlayFont(nullptr),
m_BlockingPresent(false)
{
RtlZeroMemory(m_DecSurfaces, sizeof(m_DecSurfaces));
@@ -48,6 +49,7 @@ DXVA2Renderer::~DXVA2Renderer()
SAFE_COM_RELEASE(m_ProcService);
SAFE_COM_RELEASE(m_Processor);
SAFE_COM_RELEASE(m_DebugOverlayFont);
SAFE_COM_RELEASE(m_StatusOverlayFont);
for (int i = 0; i < ARRAYSIZE(m_DecSurfaces); i++) {
SAFE_COM_RELEASE(m_DecSurfaces[i]);
@@ -105,7 +107,7 @@ bool DXVA2Renderer::prepareDecoderContext(AVCodecContext* context)
int DXVA2Renderer::ffGetBuffer2(AVCodecContext* context, AVFrame* frame, int)
{
DXVA2Renderer* me = (DXVA2Renderer*)((FFmpegVideoDecoder*)context->opaque)->getRenderer();
DXVA2Renderer* me = (DXVA2Renderer*)((FFmpegVideoDecoder*)context->opaque)->getBackendRenderer();
frame->buf[0] = av_buffer_pool_get(me->m_Pool);
if (!frame->buf[0]) {
@@ -330,6 +332,12 @@ bool DXVA2Renderer::isDXVideoProcessorAPIBlacklisted()
HRESULT hr;
bool result = false;
if (qgetenv("DXVA2_DISABLE_VIDPROC_BLACKLIST") == "1") {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"IDirectXVideoProcessor blacklist is disabled");
return false;
}
hr = m_Device->GetDirect3D(&d3d9);
if (SUCCEEDED(hr)) {
D3DCAPS9 caps;
@@ -374,8 +382,11 @@ bool DXVA2Renderer::isDecoderBlacklisted()
HRESULT hr;
bool result = false;
// TODO: Update for HEVC Main10
SDL_assert(m_VideoFormat != VIDEO_FORMAT_H265_MAIN10);
if (qgetenv("DXVA2_DISABLE_DECODER_BLACKLIST") == "1") {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"DXVA2 decoder blacklist is disabled");
return false;
}
hr = m_Device->GetDirect3D(&d3d9);
if (SUCCEEDED(hr)) {
@@ -418,6 +429,15 @@ bool DXVA2Renderer::isDecoderBlacklisted()
"GPU blacklisted for HEVC due to hybrid decode");
result = (m_VideoFormat & VIDEO_FORMAT_MASK_H265) != 0;
break;
case 0x1900: // Skylake
// Blacklist these for HEVC Main10 to avoid hybrid decode.
// Regular HEVC Main is fine though.
if (m_VideoFormat == VIDEO_FORMAT_H265_MAIN10) {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"GPU blacklisted for HEVC Main10 due to hybrid decode");
result = false;
break;
}
default:
// Intel drivers from before late-2017 had a bug that caused some strange artifacts
// when decoding HEVC. Avoid HEVC on drivers prior to build 4836 which I confirmed
@@ -531,6 +551,8 @@ bool DXVA2Renderer::initializeDevice(SDL_Window* window, bool enableVsync)
d3dpp.BackBufferWidth = currentMode.Width;
d3dpp.BackBufferHeight = currentMode.Height;
d3dpp.FullScreen_RefreshRateInHz = currentMode.RefreshRate;
// TODO: Need to select 10-bit color format for HDR
d3dpp.BackBufferFormat = currentMode.Format;
}
else {
@@ -635,11 +657,11 @@ bool DXVA2Renderer::initializeDevice(SDL_Window* window, bool enableVsync)
return true;
}
bool DXVA2Renderer::initialize(SDL_Window* window, int videoFormat, int width, int height, int, bool enableVsync)
bool DXVA2Renderer::initialize(PDECODER_PARAMETERS params)
{
m_VideoFormat = videoFormat;
m_VideoWidth = width;
m_VideoHeight = height;
m_VideoFormat = params->videoFormat;
m_VideoWidth = params->width;
m_VideoHeight = params->height;
RtlZeroMemory(&m_Desc, sizeof(m_Desc));
@@ -666,9 +688,15 @@ bool DXVA2Renderer::initialize(SDL_Window* window, int videoFormat, int width, i
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');
if (!initializeDevice(window, enableVsync)) {
if (m_VideoFormat == VIDEO_FORMAT_H265_MAIN10) {
m_Desc.Format = (D3DFORMAT)MAKEFOURCC('P','0','1','0');
}
else {
m_Desc.Format = (D3DFORMAT)MAKEFOURCC('N','V','1','2');
}
if (!initializeDevice(params->window, params->enableVsync)) {
return false;
}
@@ -685,52 +713,80 @@ bool DXVA2Renderer::initialize(SDL_Window* window, int videoFormat, int width, i
// input focus. This glitches out the renderer and a bunch of other stuff.
// Direct3D9Ex itself seems to have this minimize on focus loss behavior on its
// own, so just disable SDL's handling of the focus loss event.
SDL_SetHint(SDL_HINT_VIDEO_MINIMIZE_ON_FOCUS_LOSS, "0");
SDL_SetHintWithPriority(SDL_HINT_VIDEO_MINIMIZE_ON_FOCUS_LOSS, "0", SDL_HINT_OVERRIDE);
return true;
}
bool DXVA2Renderer::needsTestFrame()
{
// We validate the DXVA2 profiles are supported
// in initialize() so no test frame is required
return false;
}
int DXVA2Renderer::getDecoderCapabilities()
{
return 0;
}
IFFmpegRenderer::FramePacingConstraint DXVA2Renderer::getFramePacingConstraint()
{
return PACING_ANY;
}
void DXVA2Renderer::notifyOverlayUpdated(Overlay::OverlayType)
void DXVA2Renderer::notifyOverlayUpdated(Overlay::OverlayType type)
{
HRESULT hr;
// Initialize the overlay font if it's not already created
if (m_DebugOverlayFont == nullptr) {
hr = D3DXCreateFontA(m_Device,
Session::get()->getOverlayManager().getOverlayFontSize(Overlay::OverlayDebug),
0,
FW_HEAVY,
1,
false,
ANSI_CHARSET,
OUT_DEFAULT_PRECIS,
DEFAULT_QUALITY,
DEFAULT_PITCH | FF_DONTCARE,
"",
&m_DebugOverlayFont);
if (FAILED(hr)) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"D3DXCreateFontA() failed: %x",
hr);
m_DebugOverlayFont = nullptr;
switch (type)
{
case Overlay::OverlayDebug:
if (m_DebugOverlayFont == nullptr) {
hr = D3DXCreateFontA(m_Device,
Session::get()->getOverlayManager().getOverlayFontSize(Overlay::OverlayDebug),
0,
FW_HEAVY,
1,
false,
ANSI_CHARSET,
OUT_DEFAULT_PRECIS,
DEFAULT_QUALITY,
DEFAULT_PITCH | FF_DONTCARE,
"",
&m_DebugOverlayFont);
if (FAILED(hr)) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"D3DXCreateFontA() failed: %x",
hr);
m_DebugOverlayFont = nullptr;
}
}
break;
case Overlay::OverlayStatusUpdate:
if (m_StatusOverlayFont == nullptr) {
hr = D3DXCreateFontA(m_Device,
Session::get()->getOverlayManager().getOverlayFontSize(Overlay::OverlayStatusUpdate),
0,
FW_HEAVY,
1,
false,
ANSI_CHARSET,
OUT_DEFAULT_PRECIS,
DEFAULT_QUALITY,
DEFAULT_PITCH | FF_DONTCARE,
"",
&m_StatusOverlayFont);
if (FAILED(hr)) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"D3DXCreateFontA() failed: %x",
hr);
m_StatusOverlayFont = nullptr;
}
}
break;
default:
SDL_assert(false);
break;
}
}
int DXVA2Renderer::getDecoderColorspace()
{
if (isDXVideoProcessorAPIBlacklisted()) {
// StretchRect() assumes Rec 601 on Intel GPUs
return COLORSPACE_REC_601;
}
else {
// VideoProcessBlt() *should* properly handle whatever, since
// we provide colorspace information. However, AMD GPUs seem to
// always assume Rec 709, so we'll use that as our default.
return COLORSPACE_REC_709;
}
}
@@ -929,6 +985,18 @@ void DXVA2Renderer::renderFrame(AVFrame *frame)
}
}
if (m_StatusOverlayFont != nullptr) {
if (Session::get()->getOverlayManager().isOverlayEnabled(Overlay::OverlayStatusUpdate)) {
SDL_Color color = Session::get()->getOverlayManager().getOverlayColor(Overlay::OverlayStatusUpdate);
m_StatusOverlayFont->DrawTextA(nullptr,
Session::get()->getOverlayManager().getOverlayText(Overlay::OverlayStatusUpdate),
-1,
&sample.DstRect,
DT_RIGHT | DT_NOCLIP,
D3DCOLOR_ARGB(color.a, color.r, color.g, color.b));
}
}
hr = m_Device->EndScene();
if (FAILED(hr)) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
+4 -10
View File
@@ -15,19 +15,12 @@ class DXVA2Renderer : public IFFmpegRenderer
{
public:
DXVA2Renderer();
virtual ~DXVA2Renderer();
virtual bool initialize(SDL_Window* window,
int videoFormat,
int width,
int height,
int maxFps,
bool enableVsync) override;
virtual ~DXVA2Renderer() override;
virtual bool initialize(PDECODER_PARAMETERS params) override;
virtual bool prepareDecoderContext(AVCodecContext* context) override;
virtual void renderFrame(AVFrame* frame) override;
virtual bool needsTestFrame() override;
virtual int getDecoderCapabilities() override;
virtual FramePacingConstraint getFramePacingConstraint() override;
virtual void notifyOverlayUpdated(Overlay::OverlayType) override;
virtual int getDecoderColorspace() override;
private:
bool initializeDecoder();
@@ -71,5 +64,6 @@ private:
DXVA2_VideoDesc m_Desc;
REFERENCE_TIME m_FrameIndex;
LPD3DXFONT m_DebugOverlayFont;
LPD3DXFONT m_StatusOverlayFont;
bool m_BlockingPresent;
};
@@ -0,0 +1,152 @@
#include "mmal.h"
#include "streaming/streamutils.h"
#include "streaming/session.h"
#include <Limelight.h>
MmalRenderer::MmalRenderer()
: m_Renderer(nullptr),
m_InputPort(nullptr)
{
}
MmalRenderer::~MmalRenderer()
{
if (m_InputPort != nullptr) {
mmal_port_disable(m_InputPort);
}
if (m_Renderer != nullptr) {
mmal_component_destroy(m_Renderer);
}
}
bool MmalRenderer::prepareDecoderContext(AVCodecContext*)
{
/* Nothing to do */
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Using MMAL renderer");
return true;
}
bool MmalRenderer::initialize(PDECODER_PARAMETERS params)
{
MMAL_STATUS_T status;
status = mmal_component_create(MMAL_COMPONENT_DEFAULT_VIDEO_RENDERER, &m_Renderer);
if (status != MMAL_SUCCESS) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"mmal_component_create() failed: %x (%s)",
status, mmal_status_to_string(status));
return false;
}
m_InputPort = m_Renderer->input[0];
m_InputPort->format->encoding = MMAL_ENCODING_OPAQUE;
m_InputPort->format->es->video.width = params->width;
m_InputPort->format->es->video.height = params->height;
m_InputPort->format->es->video.crop.x = 0;
m_InputPort->format->es->video.crop.y = 0;
m_InputPort->format->es->video.crop.width = params->width;
m_InputPort->format->es->video.crop.height = params->height;
status = mmal_port_format_commit(m_InputPort);
if (status != MMAL_SUCCESS) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"mmal_port_format_commit() failed: %x (%s)",
status, mmal_status_to_string(status));
return false;
}
status = mmal_component_enable(m_Renderer);
if (status != MMAL_SUCCESS) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"mmal_component_enable() failed: %x (%s)",
status, mmal_status_to_string(status));
return false;
}
{
MMAL_DISPLAYREGION_T dr;
dr.hdr.id = MMAL_PARAMETER_DISPLAYREGION;
dr.hdr.size = sizeof(MMAL_DISPLAYREGION_T);
dr.set = MMAL_DISPLAY_SET_LAYER;
dr.layer = 128;
dr.set |= MMAL_DISPLAY_SET_ALPHA;
dr.alpha = 255;
dr.set |= MMAL_DISPLAY_SET_FULLSCREEN;
dr.fullscreen = (SDL_GetWindowFlags(params->window) & SDL_WINDOW_FULLSCREEN) != 0;
{
SDL_Rect src, dst;
src.x = src.y = 0;
src.w = params->width;
src.h = params->height;
dst.x = dst.y = 0;
SDL_GetWindowSize(params->window, &dst.w, &dst.h);
StreamUtils::scaleSourceToDestinationSurface(&src, &dst);
dr.set |= MMAL_DISPLAY_SET_DEST_RECT;
dr.dest_rect.x = dst.x;
dr.dest_rect.y = dst.y;
dr.dest_rect.width = dst.w;
dr.dest_rect.height = dst.h;
}
}
status = mmal_port_enable(m_InputPort, InputPortCallback);
if (status != MMAL_SUCCESS) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"mmal_port_enable() failed: %x (%s)",
status, mmal_status_to_string(status));
return false;
}
return true;
}
void MmalRenderer::InputPortCallback(MMAL_PORT_T*, MMAL_BUFFER_HEADER_T* buffer)
{
mmal_buffer_header_release(buffer);
}
enum AVPixelFormat MmalRenderer::getPreferredPixelFormat(int videoFormat)
{
// Opaque MMAL buffers
SDL_assert(videoFormat == VIDEO_FORMAT_H264);
return AV_PIX_FMT_MMAL;
}
bool MmalRenderer::needsTestFrame()
{
// We won't be able to decode if the GPU memory is 64 MB or lower,
// so we must test before allowing the decoder to be used.
return true;
}
void MmalRenderer::renderFrame(AVFrame* frame)
{
MMAL_BUFFER_HEADER_T* buffer = (MMAL_BUFFER_HEADER_T*)frame->data[3];
MMAL_STATUS_T status;
status = mmal_port_send_buffer(m_InputPort, buffer);
if (status != MMAL_SUCCESS) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"mmal_port_send_buffer() failed: %x (%s)",
status, mmal_status_to_string(status));
}
else {
// Prevent the buffer from being freed during av_frame_free()
// until rendering is complete. The reference is dropped in
// InputPortCallback().
mmal_buffer_header_acquire(buffer);
}
}
@@ -0,0 +1,26 @@
#pragma once
#include "renderer.h"
#include <interface/mmal/mmal.h>
#include <interface/mmal/util/mmal_util.h>
#include <interface/mmal/util/mmal_util_params.h>
#include <interface/mmal/util/mmal_default_components.h>
class MmalRenderer : public IFFmpegRenderer {
public:
MmalRenderer();
virtual ~MmalRenderer() override;
virtual bool initialize(PDECODER_PARAMETERS params) override;
virtual bool prepareDecoderContext(AVCodecContext* context) override;
virtual void renderFrame(AVFrame* frame) override;
virtual enum AVPixelFormat getPreferredPixelFormat(int videoFormat) override;
virtual bool needsTestFrame() override;
private:
static void InputPortCallback(MMAL_PORT_T* port, MMAL_BUFFER_HEADER_T* buffer);
MMAL_COMPONENT_T* m_Renderer;
MMAL_PORT_T* m_InputPort;
};
@@ -1,11 +0,0 @@
#pragma once
#include "pacer.h"
class DisplayLinkVsyncSourceFactory
{
public:
static
IVsyncSource* createVsyncSource(Pacer* pacer);
};
@@ -1,132 +0,0 @@
#include "displaylinkvsyncsource.h"
#include <SDL_syswm.h>
#include <CoreVideo/CoreVideo.h>
#import <Cocoa/Cocoa.h>
class DisplayLinkVsyncSource : public IVsyncSource
{
public:
DisplayLinkVsyncSource(Pacer* pacer)
: m_Pacer(pacer),
m_DisplayLink(nullptr)
{
}
virtual ~DisplayLinkVsyncSource() override
{
if (m_DisplayLink != nullptr) {
CVDisplayLinkStop(m_DisplayLink);
CVDisplayLinkRelease(m_DisplayLink);
}
}
static
CGDirectDisplayID
getDisplayID(NSScreen* screen)
{
NSNumber* screenNumber = [screen deviceDescription][@"NSScreenNumber"];
return [screenNumber unsignedIntValue];
}
static
CVReturn
displayLinkOutputCallback(
CVDisplayLinkRef displayLink,
const CVTimeStamp* /* now */,
const CVTimeStamp* /* vsyncTime */,
CVOptionFlags,
CVOptionFlags*,
void *displayLinkContext)
{
auto me = reinterpret_cast<DisplayLinkVsyncSource*>(displayLinkContext);
SDL_assert(displayLink == me->m_DisplayLink);
// 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->m_Pacer->vsyncCallback(500 / me->m_DisplayFps);
return kCVReturnSuccess;
}
virtual bool initialize(SDL_Window* window, int displayFps) override
{
SDL_SysWMinfo info;
SDL_VERSION(&info.version);
if (!SDL_GetWindowWMInfo(window, &info)) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"SDL_GetWindowWMInfo() failed: %s",
SDL_GetError());
return false;
}
SDL_assert(info.subsystem == SDL_SYSWM_COCOA);
m_DisplayFps = displayFps;
NSScreen* screen = [info.info.cocoa.window screen];
CVReturn status;
if (screen == nullptr) {
// Window not visible on any display, so use a
// CVDisplayLink that can work with all active displays.
// When we become visible, we'll recreate ourselves
// and associate with the new screen.
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"NSWindow is not visible on any display");
status = CVDisplayLinkCreateWithActiveCGDisplays(&m_DisplayLink);
}
else {
CGDirectDisplayID displayId;
displayId = getDisplayID(screen);
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"NSWindow on display: %x",
displayId);
status = CVDisplayLinkCreateWithCGDisplay(displayId, &m_DisplayLink);
}
if (status != kCVReturnSuccess) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Failed to create CVDisplayLink: %d",
status);
return false;
}
status = CVDisplayLinkSetOutputCallback(m_DisplayLink, displayLinkOutputCallback, this);
if (status != kCVReturnSuccess) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"CVDisplayLinkSetOutputCallback() failed: %d",
status);
return false;
}
status = CVDisplayLinkStart(m_DisplayLink);
if (status != kCVReturnSuccess) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"CVDisplayLinkStart() failed: %d",
status);
return false;
}
return true;
}
private:
Pacer* m_Pacer;
CVDisplayLinkRef m_DisplayLink;
int m_DisplayFps;
};
IVsyncSource* DisplayLinkVsyncSourceFactory::createVsyncSource(Pacer* pacer) {
return new DisplayLinkVsyncSource(pacer);
}
@@ -61,7 +61,7 @@ bool DxVsyncSource::initialize(SDL_Window* window, int displayFps)
m_Window = info.info.win.window;
m_Thread = SDL_CreateThread(vsyncThread, "DX Vsync Thread", this);
m_Thread = SDL_CreateThread(vsyncThread, "DXVsync", this);
if (m_Thread == nullptr) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Unable to create DX V-sync thread: %s",
@@ -76,7 +76,11 @@ int DxVsyncSource::vsyncThread(void* context)
{
DxVsyncSource* me = reinterpret_cast<DxVsyncSource*>(context);
#if SDL_VERSION_ATLEAST(2, 0, 9)
SDL_SetThreadPriority(SDL_THREAD_PRIORITY_TIME_CRITICAL);
#else
SDL_SetThreadPriority(SDL_THREAD_PRIORITY_HIGH);
#endif
D3DKMT_OPENADAPTERFROMHDC openAdapterParams = {};
HMONITOR lastMonitor = nullptr;
@@ -18,7 +18,7 @@ NullThreadedVsyncSource::~NullThreadedVsyncSource()
bool NullThreadedVsyncSource::initialize(SDL_Window*, int displayFps)
{
m_DisplayFps = displayFps;
m_Thread = SDL_CreateThread(vsyncThread, "Null Vsync Thread", this);
m_Thread = SDL_CreateThread(vsyncThread, "NullVsync", this);
if (m_Thread == nullptr) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Unable to create DX V-sync thread: %s",
@@ -33,7 +33,11 @@ int NullThreadedVsyncSource::vsyncThread(void* context)
{
NullThreadedVsyncSource* me = reinterpret_cast<NullThreadedVsyncSource*>(context);
#if SDL_VERSION_ATLEAST(2, 0, 9)
SDL_SetThreadPriority(SDL_THREAD_PRIORITY_TIME_CRITICAL);
#else
SDL_SetThreadPriority(SDL_THREAD_PRIORITY_HIGH);
#endif
while (SDL_AtomicGet(&me->m_Stopping) == 0) {
me->m_Pacer->vsyncCallback(1000 / me->m_DisplayFps);
@@ -3,10 +3,6 @@
#include "nullthreadedvsyncsource.h"
#ifdef Q_OS_DARWIN
#include "displaylinkvsyncsource.h"
#endif
#ifdef Q_OS_WIN32
#define WIN32_LEAN_AND_MEAN
#include <Windows.h>
@@ -14,6 +10,10 @@
#include "dxvsyncsource.h"
#endif
// Limit the number of queued frames to prevent excessive memory consumption
// if the V-Sync source or renderer is blocked for a while.
#define MAX_QUEUED_FRAMES 8
// We may be woken up slightly late so don't go all the way
// up to the next V-sync since we may accidentally step into
// the next V-sync period. It also takes some amount of time
@@ -57,10 +57,34 @@ Pacer::~Pacer()
}
}
void Pacer::renderOnMainThread()
{
// Ignore this call for renderers that work on a dedicated render thread
if (m_RenderThread != nullptr) {
return;
}
m_FrameQueueLock.lock();
if (!m_RenderQueue.isEmpty()) {
// Releases m_FrameQueueLock
renderLastFrameAndUnlock();
}
else {
m_FrameQueueLock.unlock();
}
}
int Pacer::renderThread(void* context)
{
Pacer* me = reinterpret_cast<Pacer*>(context);
if (SDL_SetThreadPriority(SDL_THREAD_PRIORITY_HIGH) < 0) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"Unable to set render thread to high priority: %s",
SDL_GetError());
}
while (!me->m_Stopping) {
// Acquire the frame queue lock to protect the queue and
// the not empty condition
@@ -77,34 +101,61 @@ int Pacer::renderThread(void* context)
break;
}
// Dequeue the most recent frame for rendering and free the others.
// Render the latest frame and discard the others
// NB: m_FrameQueueLock still held here!
AVFrame* lastFrame = nullptr;
while (!me->m_RenderQueue.isEmpty()) {
if (lastFrame != nullptr) {
// Don't hold the frame queue lock across av_frame_free(),
// since it could need to talk to the GPU driver. This is safe
// because we're guaranteed that the queue will not shrink during
// this time (and so dequeue() below will always get something).
me->m_FrameQueueLock.unlock();
av_frame_free(&lastFrame);
me->m_VideoStats->pacerDroppedFrames++;
me->m_FrameQueueLock.lock();
}
lastFrame = me->m_RenderQueue.dequeue();
}
// Release the frame queue lock before rendering
me->m_FrameQueueLock.unlock();
// Render and free the mot current frame
me->renderFrame(lastFrame);
me->renderLastFrameAndUnlock();
}
return 0;
}
void Pacer::enqueueFrameForRenderingAndUnlock(AVFrame *frame)
{
dropFrameForEnqueue(m_RenderQueue);
m_RenderQueue.enqueue(frame);
m_FrameQueueLock.unlock();
if (m_RenderThread != nullptr) {
m_RenderQueueNotEmpty.wakeOne();
}
else {
SDL_Event event;
// For main thread rendering, we'll push an event to trigger a callback
event.type = SDL_USEREVENT;
event.user.code = SDL_CODE_FRAME_READY;
SDL_PushEvent(&event);
}
}
// Caller must hold m_FrameQueueLock
void Pacer::renderLastFrameAndUnlock()
{
// Dequeue the most recent frame for rendering and free the others.
AVFrame* lastFrame = nullptr;
while (!m_RenderQueue.isEmpty()) {
if (lastFrame != nullptr) {
// Don't hold the frame queue lock across av_frame_free(),
// since it could need to talk to the GPU driver. This is safe
// because we're guaranteed that the queue will not shrink during
// this time (and so dequeue() below will always get something).
m_FrameQueueLock.unlock();
av_frame_free(&lastFrame);
m_VideoStats->pacerDroppedFrames++;
m_FrameQueueLock.lock();
}
lastFrame = m_RenderQueue.dequeue();
}
// Release the frame queue lock before rendering
m_FrameQueueLock.unlock();
// Render and free the mot current frame
renderFrame(lastFrame);
}
// Called in an arbitrary thread by the IVsyncSource on V-sync
// or an event synchronized with V-sync
void Pacer::vsyncCallback(int timeUntilNextVsyncMillis)
@@ -162,9 +213,7 @@ void Pacer::vsyncCallback(int timeUntilNextVsyncMillis)
}
// Place the first frame on the render queue
m_RenderQueue.enqueue(m_PacingQueue.dequeue());
m_FrameQueueLock.unlock();
m_RenderQueueNotEmpty.wakeOne();
enqueueFrameForRenderingAndUnlock(m_PacingQueue.dequeue());
}
bool Pacer::initialize(SDL_Window* window, int maxVideoFps, bool enablePacing)
@@ -177,9 +226,7 @@ bool Pacer::initialize(SDL_Window* window, int maxVideoFps, bool enablePacing)
"Frame pacing active: target %d Hz with %d FPS stream",
m_DisplayFps, m_MaxVideoFps);
#if defined(Q_OS_DARWIN)
m_VsyncSource = DisplayLinkVsyncSourceFactory::createVsyncSource(this);
#elif defined(Q_OS_WIN32)
#if defined(Q_OS_WIN32)
// Don't use D3DKMTWaitForVerticalBlankEvent() on Windows 7, because
// it blocks during other concurrent DX operations (like actually rendering).
if (IsWindows8OrGreater()) {
@@ -200,7 +247,9 @@ bool Pacer::initialize(SDL_Window* window, int maxVideoFps, bool enablePacing)
m_DisplayFps, m_MaxVideoFps);
}
m_RenderThread = SDL_CreateThread(Pacer::renderThread, "Pacer Render Thread", this);
if (m_VsyncRenderer->isRenderThreadSupported()) {
m_RenderThread = SDL_CreateThread(Pacer::renderThread, "PacerRender", this);
}
return true;
}
@@ -209,7 +258,7 @@ void Pacer::renderFrame(AVFrame* frame)
{
// Count time spent in Pacer's queues
Uint32 beforeRender = SDL_GetTicks();
m_VideoStats->totalPacerTime += beforeRender - frame->pts;
m_VideoStats->totalPacerTime += beforeRender - frame->pkt_dts;
// Render it
m_VsyncRenderer->renderFrame(frame);
@@ -253,6 +302,15 @@ void Pacer::renderFrame(AVFrame* frame)
m_FrameQueueLock.unlock();
}
void Pacer::dropFrameForEnqueue(QQueue<AVFrame*>& queue)
{
SDL_assert(queue.size() <= MAX_QUEUED_FRAMES);
if (queue.size() == MAX_QUEUED_FRAMES) {
AVFrame* frame = queue.dequeue();
av_frame_free(&frame);
}
}
void Pacer::submitFrame(AVFrame* frame)
{
// Make sure initialize() has been called
@@ -261,16 +319,12 @@ void Pacer::submitFrame(AVFrame* frame)
// Queue the frame and possibly wake up the render thread
m_FrameQueueLock.lock();
if (m_VsyncSource != nullptr) {
dropFrameForEnqueue(m_PacingQueue);
m_PacingQueue.enqueue(frame);
}
else {
m_RenderQueue.enqueue(frame);
}
m_FrameQueueLock.unlock();
if (m_VsyncSource != nullptr) {
m_FrameQueueLock.unlock();
m_PacingQueueNotEmpty.wakeOne();
}
else {
m_RenderQueueNotEmpty.wakeOne();
enqueueFrameForRenderingAndUnlock(frame);
}
}
@@ -26,11 +26,19 @@ public:
void vsyncCallback(int timeUntilNextVsyncMillis);
void renderOnMainThread();
private:
static int renderThread(void* context);
void enqueueFrameForRenderingAndUnlock(AVFrame* frame);
void renderLastFrameAndUnlock();
void renderFrame(AVFrame* frame);
void dropFrameForEnqueue(QQueue<AVFrame*>& queue);
QQueue<AVFrame*> m_RenderQueue;
QQueue<AVFrame*> m_PacingQueue;
QQueue<int> m_PacingQueueHistory;
@@ -2,6 +2,7 @@
#include <SDL.h>
#include "streaming/video/decoder.h"
#include "streaming/video/overlaymanager.h"
extern "C" {
@@ -16,17 +17,50 @@ public:
PACING_ANY
};
virtual bool initialize(SDL_Window* window,
int videoFormat,
int width,
int height,
int maxFps,
bool enableVsync) = 0;
virtual bool initialize(PDECODER_PARAMETERS params) = 0;
virtual bool prepareDecoderContext(AVCodecContext* context) = 0;
virtual void renderFrame(AVFrame* frame) = 0;
virtual bool needsTestFrame() = 0;
virtual int getDecoderCapabilities() = 0;
virtual FramePacingConstraint getFramePacingConstraint() = 0;
virtual bool needsTestFrame() {
// No test frame required by default
return false;
}
virtual int getDecoderCapabilities() {
// No special capabilities by default
return 0;
}
virtual int getDecoderColorspace() {
// Rec 601 is default
return COLORSPACE_REC_601;
}
virtual FramePacingConstraint getFramePacingConstraint() {
// No pacing preference
return PACING_ANY;
}
virtual bool isRenderThreadSupported() {
// Render thread is supported by default
return true;
}
virtual bool isDirectRenderingSupported() {
// The renderer can render directly to the display
return true;
}
virtual enum AVPixelFormat getPreferredPixelFormat(int videoFormat) {
if (videoFormat == VIDEO_FORMAT_H265_MAIN10) {
// 10-bit YUV 4:2:0
return AV_PIX_FMT_P010;
}
else {
// Planar YUV 4:2:0
return AV_PIX_FMT_YUV420P;
}
}
// IOverlayRenderer
virtual void notifyOverlayUpdated(Overlay::OverlayType) override {
+272 -116
View File
@@ -10,9 +10,8 @@
SdlRenderer::SdlRenderer()
: m_Renderer(nullptr),
m_Texture(nullptr),
m_DebugOverlayFont(nullptr),
m_DebugOverlaySurface(nullptr),
m_DebugOverlayTexture(nullptr)
m_SwPixelFormat(AV_PIX_FMT_NONE),
m_FontData(Path::readDataFile("ModeSeven.ttf"))
{
SDL_assert(TTF_WasInit() == 0);
if (TTF_Init() != 0) {
@@ -21,23 +20,33 @@ SdlRenderer::SdlRenderer()
TTF_GetError());
return;
}
SDL_zero(m_OverlayFonts);
SDL_zero(m_OverlaySurfaces);
SDL_zero(m_OverlayTextures);
}
SdlRenderer::~SdlRenderer()
{
if (m_DebugOverlayFont != nullptr) {
TTF_CloseFont(m_DebugOverlayFont);
for (int i = 0; i < Overlay::OverlayMax; i++) {
if (m_OverlayFonts[i] != nullptr) {
TTF_CloseFont(m_OverlayFonts[i]);
}
}
TTF_Quit();
SDL_assert(TTF_WasInit() == 0);
if (m_DebugOverlayTexture != nullptr) {
SDL_DestroyTexture(m_DebugOverlayTexture);
for (int i = 0; i < Overlay::OverlayMax; i++) {
if (m_OverlayTextures[i] != nullptr) {
SDL_DestroyTexture(m_OverlayTextures[i]);
}
}
if (m_DebugOverlaySurface != nullptr) {
SDL_FreeSurface(m_DebugOverlaySurface);
for (int i = 0; i < Overlay::OverlayMax; i++) {
if (m_OverlaySurfaces[i] != nullptr) {
SDL_FreeSurface(m_OverlaySurfaces[i]);
}
}
if (m_Texture != nullptr) {
@@ -54,92 +63,107 @@ bool SdlRenderer::prepareDecoderContext(AVCodecContext*)
/* Nothing to do */
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Using SDL software renderer");
"Using SDL renderer");
return true;
}
bool SdlRenderer::needsTestFrame()
void SdlRenderer::notifyOverlayUpdated(Overlay::OverlayType type)
{
// This renderer should always work
return false;
// Construct the required font to render the overlay
if (m_OverlayFonts[type] == nullptr) {
if (m_FontData.isEmpty()) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"SDL overlay font failed to load");
return;
}
// m_FontData must stay around until the font is closed
m_OverlayFonts[type] = TTF_OpenFontRW(SDL_RWFromConstMem(m_FontData.constData(), m_FontData.size()),
1,
Session::get()->getOverlayManager().getOverlayFontSize(type));
if (m_OverlayFonts[type] == nullptr) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"TTF_OpenFont() failed: %s",
TTF_GetError());
// Can't proceed without a font
return;
}
}
SDL_Surface* oldSurface = (SDL_Surface*)SDL_AtomicGetPtr((void**)&m_OverlaySurfaces[type]);
// Free the old surface
if (oldSurface != nullptr && SDL_AtomicCASPtr((void**)&m_OverlaySurfaces[type], oldSurface, nullptr)) {
SDL_FreeSurface(oldSurface);
}
if (Session::get()->getOverlayManager().isOverlayEnabled(type)) {
// The _Wrapped variant is required for line breaks to work
SDL_Surface* surface = TTF_RenderText_Blended_Wrapped(m_OverlayFonts[type],
Session::get()->getOverlayManager().getOverlayText(type),
Session::get()->getOverlayManager().getOverlayColor(type),
1000);
SDL_AtomicSetPtr((void**)&m_OverlaySurfaces[type], surface);
}
}
bool SdlRenderer::isRenderThreadSupported()
{
SDL_RendererInfo info;
SDL_GetRendererInfo(m_Renderer, &info);
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"SDL renderer backend: %s",
info.name);
if (info.name != QString("direct3d")) {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"SDL renderer backend requires main thread rendering");
return false;
}
return true;
}
int SdlRenderer::getDecoderCapabilities()
{
// The FFmpeg CPU decoder can handle reference frame invalidation,
// but only for H.264.
return CAPABILITY_REFERENCE_FRAME_INVALIDATION_AVC;
// Slice up to 4 times for parallel decode, once slice per core
int slices = qMin(MAX_SLICES, SDL_GetCPUCount());
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Encoder configured for %d slices per frame",
slices);
return CAPABILITY_SLICES_PER_FRAME(slices);
}
IFFmpegRenderer::FramePacingConstraint SdlRenderer::getFramePacingConstraint()
{
return PACING_ANY;
}
void SdlRenderer::notifyOverlayUpdated(Overlay::OverlayType type)
{
if (type == Overlay::OverlayDebug) {
if (m_DebugOverlayFont == nullptr) {
QByteArray fontPath = QDir::toNativeSeparators(Path::getDataFilePath("ModeSeven.ttf")).toUtf8();
if (fontPath.isEmpty()) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"Unable to locate SDL overlay font");
return;
}
m_DebugOverlayFont = TTF_OpenFont(fontPath.data(),
Session::get()->getOverlayManager().getOverlayFontSize(Overlay::OverlayDebug));
if (m_DebugOverlayFont == nullptr) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"TTF_OpenFont() failed: %s",
TTF_GetError());
// Can't proceed without a font
return;
}
}
SDL_Surface* oldSurface = (SDL_Surface*)SDL_AtomicGetPtr((void**)&m_DebugOverlaySurface);
// Free the old surface
if (oldSurface != nullptr && SDL_AtomicCASPtr((void**)&m_DebugOverlaySurface, oldSurface, nullptr)) {
SDL_FreeSurface(oldSurface);
}
if (Session::get()->getOverlayManager().isOverlayEnabled(type)) {
// The _Wrapped variant is required for line breaks to work
SDL_Surface* surface = TTF_RenderText_Blended_Wrapped(m_DebugOverlayFont,
Session::get()->getOverlayManager().getOverlayText(type),
Session::get()->getOverlayManager().getOverlayColor(type),
1000);
SDL_AtomicSetPtr((void**)&m_DebugOverlaySurface, surface);
}
}
else {
SDL_assert(false);
}
}
bool SdlRenderer::initialize(SDL_Window* window,
int,
int width,
int height,
int,
bool enableVsync)
bool SdlRenderer::initialize(PDECODER_PARAMETERS params)
{
Uint32 rendererFlags = SDL_RENDERER_ACCELERATED;
if ((SDL_GetWindowFlags(window) & SDL_WINDOW_FULLSCREEN_DESKTOP) == SDL_WINDOW_FULLSCREEN) {
if (params->videoFormat == VIDEO_FORMAT_H265_MAIN10) {
// SDL doesn't support rendering YUV 10-bit textures yet
return false;
}
if ((SDL_GetWindowFlags(params->window) & SDL_WINDOW_FULLSCREEN_DESKTOP) == SDL_WINDOW_FULLSCREEN) {
// In full-screen exclusive mode, we enable V-sync if requested. For other modes, Windows and Mac
// have compositors that make rendering tear-free. Linux compositor varies by distro and user
// configuration but doesn't seem feasible to detect here.
if (enableVsync) {
if (params->enableVsync) {
rendererFlags |= SDL_RENDERER_PRESENTVSYNC;
}
}
m_Renderer = SDL_CreateRenderer(window, -1, rendererFlags);
#ifdef Q_OS_WIN32
// We render on a different thread than the main thread which is handling window
// messages. Without D3DCREATE_MULTITHREADED, this can cause a deadlock by blocking
// on a window message being processed while the main thread is blocked waiting for
// the render thread to finish.
SDL_SetHintWithPriority(SDL_HINT_RENDER_DIRECT3D_THREADSAFE, "1", SDL_HINT_OVERRIDE);
#endif
m_Renderer = SDL_CreateRenderer(params->window, -1, rendererFlags);
if (!m_Renderer) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"SDL_CreateRenderer() failed: %s",
@@ -149,37 +173,185 @@ bool SdlRenderer::initialize(SDL_Window* window,
// The window may be smaller than the stream size, so ensure our
// logical rendering surface size is equal to the stream size
SDL_RenderSetLogicalSize(m_Renderer, width, height);
SDL_RenderSetLogicalSize(m_Renderer, params->width, params->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,
width,
height);
if (!m_Texture) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"SDL_CreateRenderer() failed: %s",
SDL_GetError());
return false;
}
#ifdef Q_OS_WIN32
// For some reason, using Direct3D9Ex breaks this with multi-monitor setups.
// When focus is lost, the window is minimized then immediately restored without
// input focus. This glitches out the renderer and a bunch of other stuff.
// Direct3D9Ex itself seems to have this minimize on focus loss behavior on its
// own, so just disable SDL's handling of the focus loss event.
SDL_SetHintWithPriority(SDL_HINT_VIDEO_MINIMIZE_ON_FOCUS_LOSS, "0", SDL_HINT_OVERRIDE);
#endif
return true;
}
void SdlRenderer::renderOverlay(Overlay::OverlayType type)
{
if (Session::get()->getOverlayManager().isOverlayEnabled(type)) {
// If a new surface has been created for updated overlay data, convert it into a texture.
// NB: We have to do this conversion at render-time because we can only interact
// with the renderer on a single thread.
SDL_Surface* surface = (SDL_Surface*)SDL_AtomicGetPtr((void**)&m_OverlaySurfaces[type]);
if (surface != nullptr && SDL_AtomicCASPtr((void**)&m_OverlaySurfaces[type], surface, nullptr)) {
if (m_OverlayTextures[type] != nullptr) {
SDL_DestroyTexture(m_OverlayTextures[type]);
}
if (type == Overlay::OverlayStatusUpdate) {
// Bottom Left
int unused;
SDL_RenderGetLogicalSize(m_Renderer, &unused, &m_OverlayRects[type].y);
m_OverlayRects[type].x = 0;
m_OverlayRects[type].y -= surface->h;
}
else if (type == Overlay::OverlayDebug) {
// Top left
m_OverlayRects[type].x = 0;
m_OverlayRects[type].y = 0;
}
m_OverlayRects[type].w = surface->w;
m_OverlayRects[type].h = surface->h;
m_OverlayTextures[type] = SDL_CreateTextureFromSurface(m_Renderer, surface);
SDL_FreeSurface(surface);
}
// If we have an overlay texture, render it too
if (m_OverlayTextures[type] != nullptr) {
SDL_RenderCopy(m_Renderer, m_OverlayTextures[type], nullptr, &m_OverlayRects[type]);
}
}
}
void SdlRenderer::renderFrame(AVFrame* frame)
{
SDL_UpdateYUVTexture(m_Texture, nullptr,
frame->data[0],
frame->linesize[0],
frame->data[1],
frame->linesize[1],
frame->data[2],
frame->linesize[2]);
int err;
AVFrame* swFrame = nullptr;
if (frame->hw_frames_ctx != nullptr) {
// If we are acting as the frontend for a hardware
// accelerated decoder, we'll need to read the frame
// back to render it.
// Find the native read-back format
if (m_SwPixelFormat == AV_PIX_FMT_NONE) {
auto hwFrameCtx = (AVHWFramesContext*)frame->hw_frames_ctx->data;
m_SwPixelFormat = hwFrameCtx->sw_format;
SDL_assert(m_SwPixelFormat != AV_PIX_FMT_NONE);
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Selected read-back format: %d",
m_SwPixelFormat);
}
swFrame = av_frame_alloc();
if (swFrame == nullptr) {
return;
}
swFrame->width = frame->width;
swFrame->height = frame->height;
swFrame->format = m_SwPixelFormat;
err = av_hwframe_transfer_data(swFrame, frame, 0);
if (err != 0) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"av_hwframe_transfer_data() failed: %d",
err);
goto Exit;
}
// av_hwframe_transfer_data() can nuke frame metadata,
// so anything other than width, height, and format must
// be set *after* calling av_hwframe_transfer_data().
swFrame->colorspace = frame->colorspace;
frame = swFrame;
}
if (m_Texture == nullptr) {
Uint32 sdlFormat;
switch (frame->format)
{
case AV_PIX_FMT_YUV420P:
sdlFormat = SDL_PIXELFORMAT_YV12;
break;
case AV_PIX_FMT_NV12:
sdlFormat = SDL_PIXELFORMAT_NV12;
break;
case AV_PIX_FMT_NV21:
sdlFormat = SDL_PIXELFORMAT_NV21;
break;
default:
SDL_assert(false);
goto Exit;
}
switch (frame->colorspace)
{
case AVCOL_SPC_BT709:
SDL_SetYUVConversionMode(SDL_YUV_CONVERSION_BT709);
break;
case AVCOL_SPC_BT470BG:
case AVCOL_SPC_SMPTE170M:
default:
SDL_SetYUVConversionMode(SDL_YUV_CONVERSION_BT601);
break;
}
m_Texture = SDL_CreateTexture(m_Renderer,
sdlFormat,
SDL_TEXTUREACCESS_STREAMING,
frame->width,
frame->height);
if (!m_Texture) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"SDL_CreateTexture() failed: %s",
SDL_GetError());
goto Exit;
}
}
if (frame->format == AV_PIX_FMT_YUV420P) {
SDL_UpdateYUVTexture(m_Texture, nullptr,
frame->data[0],
frame->linesize[0],
frame->data[1],
frame->linesize[1],
frame->data[2],
frame->linesize[2]);
}
else {
char* pixels;
int pitch;
err = SDL_LockTexture(m_Texture, nullptr, (void**)&pixels, &pitch);
if (err < 0) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"SDL_LockTexture() failed: %s",
SDL_GetError());
goto Exit;
}
memcpy(pixels,
frame->data[0],
frame->linesize[0] * frame->height);
memcpy(pixels + (frame->linesize[0] * frame->height),
frame->data[1],
frame->linesize[1] * frame->height / 2);
SDL_UnlockTexture(m_Texture);
}
SDL_RenderClear(m_Renderer);
@@ -187,30 +359,14 @@ void SdlRenderer::renderFrame(AVFrame* frame)
SDL_RenderCopy(m_Renderer, m_Texture, nullptr, nullptr);
// Draw the overlays
if (Session::get()->getOverlayManager().isOverlayEnabled(Overlay::OverlayDebug)) {
// If a new surface has been created for updated overlay data, convert it into a texture.
// NB: We have to do this conversion at render-time because we can only interact
// with the renderer on a single thread.
SDL_Surface* surface = (SDL_Surface*)SDL_AtomicGetPtr((void**)&m_DebugOverlaySurface);
if (surface != nullptr && SDL_AtomicCASPtr((void**)&m_DebugOverlaySurface, surface, nullptr)) {
if (m_DebugOverlayTexture != nullptr) {
SDL_DestroyTexture(m_DebugOverlayTexture);
}
m_DebugOverlayRect.x = 0;
m_DebugOverlayRect.y = 0;
m_DebugOverlayRect.w = surface->w;
m_DebugOverlayRect.h = surface->h;
m_DebugOverlayTexture = SDL_CreateTextureFromSurface(m_Renderer, surface);
SDL_FreeSurface(surface);
}
// If we have a debug overlay texture, render it too
if (m_DebugOverlayTexture != nullptr) {
SDL_RenderCopy(m_Renderer, m_DebugOverlayTexture, nullptr, &m_DebugOverlayRect);
}
for (int i = 0; i < Overlay::OverlayMax; i++) {
renderOverlay((Overlay::OverlayType)i);
}
SDL_RenderPresent(m_Renderer);
Exit:
if (swFrame != nullptr) {
av_frame_free(&swFrame);
}
}
+11 -13
View File
@@ -8,25 +8,23 @@ class SdlRenderer : public IFFmpegRenderer {
public:
SdlRenderer();
virtual ~SdlRenderer() override;
virtual bool initialize(SDL_Window* window,
int videoFormat,
int width,
int height,
int maxFps,
bool enableVsync) override;
virtual bool initialize(PDECODER_PARAMETERS params) override;
virtual bool prepareDecoderContext(AVCodecContext* context) override;
virtual void renderFrame(AVFrame* frame) override;
virtual bool needsTestFrame() override;
virtual int getDecoderCapabilities() override;
virtual FramePacingConstraint getFramePacingConstraint() override;
virtual void notifyOverlayUpdated(Overlay::OverlayType) override;
virtual bool isRenderThreadSupported() override;
virtual int getDecoderCapabilities() override;
private:
void renderOverlay(Overlay::OverlayType type);
SDL_Renderer* m_Renderer;
SDL_Texture* m_Texture;
TTF_Font* m_DebugOverlayFont;
SDL_Surface* m_DebugOverlaySurface;
SDL_Texture* m_DebugOverlayTexture;
SDL_Rect m_DebugOverlayRect;
int m_SwPixelFormat;
QByteArray m_FontData;
TTF_Font* m_OverlayFonts[Overlay::OverlayMax];
SDL_Surface* m_OverlaySurfaces[Overlay::OverlayMax];
SDL_Texture* m_OverlayTextures[Overlay::OverlayMax];
SDL_Rect m_OverlayRects[Overlay::OverlayMax];
};
+89 -41
View File
@@ -5,8 +5,12 @@
#include <SDL_syswm.h>
#include <unistd.h>
#include <fcntl.h>
VAAPIRenderer::VAAPIRenderer()
: m_HwContext(nullptr)
: m_HwContext(nullptr),
m_DrmFd(-1)
{
}
@@ -26,22 +30,26 @@ VAAPIRenderer::~VAAPIRenderer()
vaTerminate(display);
}
}
if (m_DrmFd != -1) {
close(m_DrmFd);
}
}
bool
VAAPIRenderer::initialize(SDL_Window* window, int, int width, int height, int, bool)
VAAPIRenderer::initialize(PDECODER_PARAMETERS params)
{
int err;
SDL_SysWMinfo info;
m_VideoWidth = width;
m_VideoHeight = height;
m_VideoWidth = params->width;
m_VideoHeight = params->height;
SDL_GetWindowSize(window, &m_DisplayWidth, &m_DisplayHeight);
SDL_GetWindowSize(params->window, &m_DisplayWidth, &m_DisplayHeight);
SDL_VERSION(&info.version);
if (!SDL_GetWindowWMInfo(window, &info)) {
if (!SDL_GetWindowWMInfo(params->window, &info)) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"SDL_GetWindowWMInfo() failed: %s",
SDL_GetError());
@@ -76,8 +84,6 @@ VAAPIRenderer::initialize(SDL_Window* window, int, int width, int height, int, b
}
else if (info.subsystem == SDL_SYSWM_WAYLAND) {
#ifdef HAVE_LIBVA_WAYLAND
m_WaylandSurface = info.info.wl.surface;
m_WaylandDisplay = info.info.wl.display;
vaDeviceContext->display = vaGetDisplayWl(info.info.wl.display);
if (!vaDeviceContext->display) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
@@ -88,6 +94,39 @@ VAAPIRenderer::initialize(SDL_Window* window, int, int width, int height, int, b
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Moonlight not compiled with VAAPI Wayland support!");
return false;
#endif
}
// TODO: Upstream a better solution for SDL_GetWindowWMInfo on KMSDRM
else if (strcmp(SDL_GetCurrentVideoDriver(), "KMSDRM") == 0) {
#ifdef HAVE_LIBVA_DRM
const char* device = SDL_getenv("DRM_DEV");
if (device == nullptr) {
device = "/dev/dri/card0";
}
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Opening DRM device: %s",
device);
m_DrmFd = open(device, O_RDWR | O_CLOEXEC);
if (m_DrmFd < 0) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Failed to open DRM device: %d",
errno);
return false;
}
vaDeviceContext->display = vaGetDisplayDRM(m_DrmFd);
if (!vaDeviceContext->display) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Unable to open DRM display for VAAPI");
return false;
}
#else
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Moonlight not compiled with VAAPI DRM support!");
return false;
#endif
}
else {
@@ -116,11 +155,12 @@ VAAPIRenderer::initialize(SDL_Window* window, int, int width, int height, int, b
"Driver: %s",
vendorString ? vendorString : "<unknown>");
// AMD's Gallium VAAPI driver has a nasty memory leak
// that causes memory to be leaked for each submitted frame.
// The Flatpak runtime has a VDPAU driver in place that works
// well, so use that instead on AMD systems.
if (vendorString && qgetenv("FORCE_VAAPI") != "1") {
// AMD's Gallium VAAPI driver has a nasty memory leak that causes memory
// to be leaked for each submitted frame. The Flatpak runtime has a VDPAU
// driver in place that works well, so use that instead on AMD systems. If
// we're using Wayland, we have no choice but to use VAAPI because VDPAU
// is only supported under X11.
if (vendorString && qgetenv("FORCE_VAAPI") != "1" && m_WindowSystem == SDL_SYSWM_X11) {
QString vendorStr(vendorString);
if (vendorStr.contains("AMD", Qt::CaseInsensitive) ||
vendorStr.contains("Radeon", Qt::CaseInsensitive)) {
@@ -158,7 +198,7 @@ VAAPIRenderer::prepareDecoderContext(AVCodecContext* context)
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Using VAAPI accelerated renderer on %s",
m_WindowSystem == SDL_SYSWM_X11 ? "X11" : "Wayland");
SDL_GetCurrentVideoDriver());
return true;
}
@@ -171,15 +211,19 @@ VAAPIRenderer::needsTestFrame()
return true;
}
int
VAAPIRenderer::getDecoderCapabilities()
bool
VAAPIRenderer::isDirectRenderingSupported()
{
return 0;
// Many Wayland renderers don't support YUV surfaces, so use
// another frontend renderer to draw our frames.
return m_WindowSystem == SDL_SYSWM_X11;
}
IFFmpegRenderer::FramePacingConstraint VAAPIRenderer::getFramePacingConstraint()
int VAAPIRenderer::getDecoderColorspace()
{
return PACING_ANY;
// Gallium drivers don't support Rec 709 yet - https://gitlab.freedesktop.org/mesa/mesa/issues/1915
// Intel-vaapi-driver defaults to Rec 601 - https://github.com/intel/intel-vaapi-driver/blob/021bcb79d1bd873bbd9fbca55f40320344bab866/src/i965_output_dri.c#L186
return COLORSPACE_REC_601;
}
void
@@ -201,6 +245,27 @@ VAAPIRenderer::renderFrame(AVFrame* frame)
if (m_WindowSystem == SDL_SYSWM_X11) {
#ifdef HAVE_LIBVA_X11
unsigned int flags = 0;
// NB: Not all VAAPI drivers respect these flags. Many drivers
// just ignore them and do the color conversion as Rec 601.
switch (frame->colorspace) {
case AVCOL_SPC_BT709:
flags |= VA_SRC_BT709;
break;
case AVCOL_SPC_BT470BG:
case AVCOL_SPC_SMPTE170M:
flags |= VA_SRC_BT601;
break;
case AVCOL_SPC_SMPTE240M:
flags |= VA_SRC_SMPTE_240;
break;
default:
// Unknown colorspace
SDL_assert(false);
break;
}
vaPutSurface(vaDeviceContext->display,
surface,
m_XWindow,
@@ -208,31 +273,14 @@ VAAPIRenderer::renderFrame(AVFrame* frame)
m_VideoWidth, m_VideoHeight,
dst.x, dst.y,
dst.w, dst.h,
NULL, 0, 0);
NULL, 0, flags);
#endif
}
else if (m_WindowSystem == SDL_SYSWM_WAYLAND) {
#ifdef HAVE_LIBVA_WAYLAND
struct wl_buffer* buffer;
VAStatus status;
status = vaGetSurfaceBufferWl(vaDeviceContext->display,
surface,
VA_FRAME_PICTURE,
&buffer);
if (status == VA_STATUS_SUCCESS) {
wl_surface_attach(m_WaylandSurface, buffer, 0, 0);
wl_surface_damage(m_WaylandSurface, dst.x, dst.y, dst.w, dst.h);
wl_display_flush(m_WaylandDisplay);
wl_surface_commit(m_WaylandSurface);
}
else {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"vaGetSurfaceBufferWl failed(): %d",
status);
}
#endif
// We don't support direct rendering on Wayland, so we should
// never get called there. Many common Wayland compositors don't
// support YUV surfaces, so direct rendering would fail.
SDL_assert(false);
}
else {
// We don't accept anything else in initialize().
+11 -17
View File
@@ -22,6 +22,9 @@ extern "C" {
#ifdef HAVE_LIBVA_WAYLAND
#include <va/va_wayland.h>
#endif
#ifdef HAVE_LIBVA_DRM
#include <va/va_drm.h>
#endif
#include <libavutil/hwcontext_vaapi.h>
}
@@ -29,32 +32,23 @@ class VAAPIRenderer : public IFFmpegRenderer
{
public:
VAAPIRenderer();
virtual ~VAAPIRenderer();
virtual bool initialize(SDL_Window* window,
int videoFormat,
int width,
int height,
int maxFps,
bool enableVsync);
virtual bool prepareDecoderContext(AVCodecContext* context);
virtual void renderFrame(AVFrame* frame);
virtual bool needsTestFrame();
virtual int getDecoderCapabilities();
virtual FramePacingConstraint getFramePacingConstraint();
virtual ~VAAPIRenderer() override;
virtual bool initialize(PDECODER_PARAMETERS params) override;
virtual bool prepareDecoderContext(AVCodecContext* context) override;
virtual void renderFrame(AVFrame* frame) override;
virtual bool needsTestFrame() override;
virtual bool isDirectRenderingSupported() override;
virtual int getDecoderColorspace() override;
private:
int m_WindowSystem;
AVBufferRef* m_HwContext;
int m_DrmFd;
#ifdef HAVE_LIBVA_X11
Window m_XWindow;
#endif
#ifdef HAVE_LIBVA_WAYLAND
struct wl_surface* m_WaylandSurface;
struct wl_display* m_WaylandDisplay;
#endif
int m_VideoWidth;
int m_VideoHeight;
int m_DisplayWidth;
+23 -15
View File
@@ -12,11 +12,16 @@
#define GET_PROC_ADDRESS(id, func) status = vdpauCtx->get_proc_address(m_Device, id, (void**)func); \
BAIL_ON_FAIL(status, id)
const VdpRGBAFormat VDPAURenderer::k_OutputFormats[] = {
const VdpRGBAFormat VDPAURenderer::k_OutputFormats8Bit[] = {
VDP_RGBA_FORMAT_B8G8R8A8,
VDP_RGBA_FORMAT_R8G8B8A8
};
const VdpRGBAFormat VDPAURenderer::k_OutputFormats10Bit[] = {
VDP_RGBA_FORMAT_B10G10R10A2,
VDP_RGBA_FORMAT_R10G10B10A2
};
VDPAURenderer::VDPAURenderer()
: m_HwContext(nullptr),
m_PresentationQueueTarget(0),
@@ -54,14 +59,14 @@ VDPAURenderer::~VDPAURenderer()
}
}
bool VDPAURenderer::initialize(SDL_Window* window, int, int width, int height, int, bool)
bool VDPAURenderer::initialize(PDECODER_PARAMETERS params)
{
int err;
VdpStatus status;
SDL_SysWMinfo info;
m_VideoWidth = width;
m_VideoHeight = height;
m_VideoWidth = params->width;
m_VideoHeight = params->height;
err = av_hwdevice_ctx_create(&m_HwContext,
AV_HWDEVICE_TYPE_VDPAU,
@@ -93,11 +98,11 @@ bool VDPAURenderer::initialize(SDL_Window* window, int, int width, int height, i
GET_PROC_ADDRESS(VDP_FUNC_ID_VIDEO_SURFACE_GET_PARAMETERS, &m_VdpVideoSurfaceGetParameters);
GET_PROC_ADDRESS(VDP_FUNC_ID_GET_INFORMATION_STRING, &m_VdpGetInformationString);
SDL_GetWindowSize(window, (int*)&m_DisplayWidth, (int*)&m_DisplayHeight);
SDL_GetWindowSize(params->window, (int*)&m_DisplayWidth, (int*)&m_DisplayHeight);
SDL_VERSION(&info.version);
if (!SDL_GetWindowWMInfo(window, &info)) {
if (!SDL_GetWindowWMInfo(params->window, &info)) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"SDL_GetWindowWMInfo() failed: %s",
SDL_GetError());
@@ -143,7 +148,11 @@ bool VDPAURenderer::initialize(SDL_Window* window, int, int width, int height, i
for (int i = 0; i < OUTPUT_SURFACE_FORMAT_COUNT; i++) {
VdpBool supported;
uint32_t maxWidth, maxHeight;
status = m_VdpOutputSurfaceQueryCapabilities(m_Device, k_OutputFormats[i],
VdpRGBAFormat candidateFormat =
params->videoFormat == VIDEO_FORMAT_H265_MAIN10 ?
k_OutputFormats10Bit[i] : k_OutputFormats8Bit[i];
status = m_VdpOutputSurfaceQueryCapabilities(m_Device, candidateFormat,
&supported, &maxWidth, &maxHeight);
if (status != VDP_STATUS_OK) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
@@ -154,7 +163,7 @@ bool VDPAURenderer::initialize(SDL_Window* window, int, int width, int height, i
if (supported) {
if (m_DisplayWidth <= maxWidth && m_DisplayHeight <= maxHeight) {
m_OutputSurfaceFormat = k_OutputFormats[i];
m_OutputSurfaceFormat = candidateFormat;
foundFormat = true;
break;
}
@@ -241,14 +250,13 @@ bool VDPAURenderer::needsTestFrame()
return true;
}
int VDPAURenderer::getDecoderCapabilities()
int VDPAURenderer::getDecoderColorspace()
{
return 0;
}
IFFmpegRenderer::FramePacingConstraint VDPAURenderer::getFramePacingConstraint()
{
return PACING_ANY;
// VDPAU defaults to Rec 601.
// https://http.download.nvidia.com/XFree86/vdpau/doxygen/html/group___vdp_video_mixer.html#ga65580813e9045d94b739ed2bb8b62b46
//
// AMD and Nvidia GPUs both correctly process Rec 601, so let's not try our luck using a non-default colorspace.
return COLORSPACE_REC_601;
}
void VDPAURenderer::renderFrame(AVFrame* frame)
+8 -13
View File
@@ -12,18 +12,12 @@ class VDPAURenderer : public IFFmpegRenderer
{
public:
VDPAURenderer();
virtual ~VDPAURenderer();
virtual bool initialize(SDL_Window* window,
int videoFormat,
int width,
int height,
int maxFps,
bool enableVsync);
virtual bool prepareDecoderContext(AVCodecContext* context);
virtual void renderFrame(AVFrame* frame);
virtual bool needsTestFrame();
virtual int getDecoderCapabilities();
virtual FramePacingConstraint getFramePacingConstraint();
virtual ~VDPAURenderer() override;
virtual bool initialize(PDECODER_PARAMETERS params) override;
virtual bool prepareDecoderContext(AVCodecContext* context) override;
virtual void renderFrame(AVFrame* frame) override;
virtual bool needsTestFrame() override;
virtual int getDecoderColorspace() override;
private:
uint32_t m_VideoWidth, m_VideoHeight;
@@ -40,7 +34,8 @@ private:
int m_NextSurfaceIndex;
#define OUTPUT_SURFACE_FORMAT_COUNT 2
static const VdpRGBAFormat k_OutputFormats[OUTPUT_SURFACE_FORMAT_COUNT];
static const VdpRGBAFormat k_OutputFormats8Bit[OUTPUT_SURFACE_FORMAT_COUNT];
static const VdpRGBAFormat k_OutputFormats10Bit[OUTPUT_SURFACE_FORMAT_COUNT];
VdpGetErrorString* m_VdpGetErrorString;
VdpPresentationQueueTargetDestroy* m_VdpPresentationQueueTargetDestroy;
+214 -46
View File
@@ -14,6 +14,7 @@
#import <VideoToolbox/VideoToolbox.h>
#import <AVFoundation/AVFoundation.h>
#import <dispatch/dispatch.h>
#import <Metal/Metal.h>
class VTRenderer : public IFFmpegRenderer
{
@@ -23,12 +24,28 @@ public:
m_DisplayLayer(nullptr),
m_FormatDesc(nullptr),
m_StreamView(nullptr),
m_DebugOverlayTextField(nullptr)
m_DisplayLink(nullptr),
m_VsyncMutex(nullptr),
m_VsyncPassed(nullptr)
{
SDL_zero(m_OverlayTextFields);
}
virtual ~VTRenderer()
virtual ~VTRenderer() override
{
if (m_DisplayLink != nullptr) {
CVDisplayLinkStop(m_DisplayLink);
CVDisplayLinkRelease(m_DisplayLink);
}
if (m_VsyncPassed != nullptr) {
SDL_DestroyCond(m_VsyncPassed);
}
if (m_VsyncMutex != nullptr) {
SDL_DestroyMutex(m_VsyncMutex);
}
if (m_HwContext != nullptr) {
av_buffer_unref(&m_HwContext);
}
@@ -37,8 +54,10 @@ public:
CFRelease(m_FormatDesc);
}
if (m_DebugOverlayTextField != nullptr) {
[m_DebugOverlayTextField removeFromSuperview];
for (int i = 0; i < Overlay::OverlayMax; i++) {
if (m_OverlayTextFields[i] != nullptr) {
[m_OverlayTextFields[i] removeFromSuperview];
}
}
if (m_StreamView != nullptr) {
@@ -46,6 +65,78 @@ public:
}
}
static
CVReturn
displayLinkOutputCallback(
CVDisplayLinkRef displayLink,
const CVTimeStamp* /* now */,
const CVTimeStamp* /* vsyncTime */,
CVOptionFlags,
CVOptionFlags*,
void *displayLinkContext)
{
auto me = reinterpret_cast<VTRenderer*>(displayLinkContext);
SDL_assert(displayLink == me->m_DisplayLink);
SDL_LockMutex(me->m_VsyncMutex);
SDL_CondSignal(me->m_VsyncPassed);
SDL_UnlockMutex(me->m_VsyncMutex);
return kCVReturnSuccess;
}
bool initializeVsyncCallback(SDL_SysWMinfo* info)
{
NSScreen* screen = [info->info.cocoa.window screen];
CVReturn status;
if (screen == nullptr) {
// Window not visible on any display, so use a
// CVDisplayLink that can work with all active displays.
// When we become visible, we'll recreate ourselves
// and associate with the new screen.
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"NSWindow is not visible on any display");
status = CVDisplayLinkCreateWithActiveCGDisplays(&m_DisplayLink);
}
else {
CGDirectDisplayID displayId = [[screen deviceDescription][@"NSScreenNumber"] unsignedIntValue];
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"NSWindow on display: %x",
displayId);
status = CVDisplayLinkCreateWithCGDisplay(displayId, &m_DisplayLink);
}
if (status != kCVReturnSuccess) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Failed to create CVDisplayLink: %d",
status);
return false;
}
status = CVDisplayLinkSetOutputCallback(m_DisplayLink, displayLinkOutputCallback, this);
if (status != kCVReturnSuccess) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"CVDisplayLinkSetOutputCallback() failed: %d",
status);
return false;
}
// The CVDisplayLink callback uses these, so we must initialize them before
// starting the callbacks.
m_VsyncMutex = SDL_CreateMutex();
m_VsyncPassed = SDL_CreateCond();
status = CVDisplayLinkStart(m_DisplayLink);
if (status != kCVReturnSuccess) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"CVDisplayLinkStart() failed: %d",
status);
return false;
}
return true;
}
// Caller frees frame after we return
virtual void renderFrame(AVFrame* frame) override
{
@@ -102,18 +193,23 @@ public:
[m_DisplayLayer enqueueSampleBuffer:sampleBuffer];
CFRelease(sampleBuffer);
if (m_DisplayLink != nullptr) {
// Vsync is enabled, so wait for a swap before returning
SDL_LockMutex(m_VsyncMutex);
if (SDL_CondWaitTimeout(m_VsyncPassed, m_VsyncMutex, 100) == SDL_MUTEX_TIMEDOUT) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"V-sync wait timed out after 100 ms");
}
SDL_UnlockMutex(m_VsyncMutex);
}
}
virtual bool initialize(SDL_Window* window,
int videoFormat,
int,
int,
int,
bool) override
virtual bool initialize(PDECODER_PARAMETERS params) override
{
int err;
if (videoFormat & VIDEO_FORMAT_MASK_H264) {
if (params->videoFormat & VIDEO_FORMAT_MASK_H264) {
// Prior to 10.13, we'll just assume everything has
// H.264 support and fail open to allow VT decode.
#if __MAC_OS_X_VERSION_MAX_ALLOWED >= 101300
@@ -131,7 +227,7 @@ public:
"Assuming H.264 HW decode on < macOS 10.13");
}
}
else if (videoFormat & VIDEO_FORMAT_MASK_H265) {
else if (params->videoFormat & VIDEO_FORMAT_MASK_H265) {
#if __MAC_OS_X_VERSION_MAX_ALLOWED >= 101300
if (__builtin_available(macOS 10.13, *)) {
if (!VTIsHardwareDecodeSupported(kCMVideoCodecType_HEVC)) {
@@ -139,6 +235,52 @@ public:
"No HW accelerated HEVC decode via VT");
return false;
}
// HEVC Main10 requires more extensive checks because there's no
// simple API to check for Main10 hardware decoding, and if we don't
// have it, we'll silently get software decoding with horrible performance.
if (params->videoFormat == VIDEO_FORMAT_H265_MAIN10) {
#if __MAC_OS_X_VERSION_MAX_ALLOWED >= 101400
if (__builtin_available(macOS 10.14, *)) {
id<MTLDevice> device = MTLCreateSystemDefaultDevice();
if (device == nullptr) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"Unable to get default Metal device");
return false;
}
// Exclude all GPUs earlier than macOSGPUFamily2
// https://developer.apple.com/documentation/metal/mtlfeatureset/mtlfeatureset_macos_gpufamily2_v1
if ([device supportsFeatureSet:MTLFeatureSet_macOS_GPUFamily2_v1]) {
if ([device.name containsString:@"Intel"]) {
// 500-series Intel GPUs are Skylake and don't support Main10 hardware decoding
if ([device.name containsString:@" 5"]) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"No HEVC Main10 support on Skylake iGPU");
return false;
}
}
else if ([device.name containsString:@"AMD"]) {
// FirePro D, M200, and M300 series GPUs don't support Main10 hardware decoding
if ([device.name containsString:@"FirePro D"] ||
[device.name containsString:@" M2"] ||
[device.name containsString:@" M3"]) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"No HEVC Main10 support on AMD GPUs until Polaris");
return false;
}
}
}
}
else
#endif
{
// Fail closed for HEVC Main10 if we're not on 10.14+
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"No HEVC Main10 support on < macOS 10.14");
return false;
}
}
}
else
#endif
@@ -154,7 +296,7 @@ public:
SDL_VERSION(&info.version);
if (!SDL_GetWindowWMInfo(window, &info)) {
if (!SDL_GetWindowWMInfo(params->window, &info)) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"SDL_GetWindowWMInfo() failed: %s",
SDL_GetError());
@@ -194,41 +336,77 @@ public:
return false;
}
if (params->enableVsync) {
if (!initializeVsyncCallback(&info)) {
return false;
}
}
return true;
}
void updateOverlayOnMainThread(Overlay::OverlayType type)
{
// Lazy initialization for the overlay
if (m_OverlayTextFields[type] == nullptr) {
m_OverlayTextFields[type] = [[NSTextField alloc] initWithFrame:m_StreamView.bounds];
[m_OverlayTextFields[type] setBezeled:NO];
[m_OverlayTextFields[type] setDrawsBackground:NO];
[m_OverlayTextFields[type] setEditable:NO];
[m_OverlayTextFields[type] setSelectable:NO];
switch (type) {
case Overlay::OverlayDebug:
[m_OverlayTextFields[type] setAlignment:NSLeftTextAlignment];
break;
case Overlay::OverlayStatusUpdate:
[m_OverlayTextFields[type] setAlignment:NSRightTextAlignment];
break;
default:
break;
}
SDL_Color color = Session::get()->getOverlayManager().getOverlayColor(type);
[m_OverlayTextFields[type] setTextColor:[NSColor colorWithSRGBRed:color.r / 255.0 green:color.g / 255.0 blue:color.b / 255.0 alpha:color.a / 255.0]];
[m_OverlayTextFields[type] setFont:[NSFont messageFontOfSize:Session::get()->getOverlayManager().getOverlayFontSize(type)]];
[m_StreamView addSubview: m_OverlayTextFields[type]];
}
// Update text contents
[m_OverlayTextFields[type] setStringValue: [NSString stringWithUTF8String:Session::get()->getOverlayManager().getOverlayText(type)]];
// Unhide if it's enabled
[m_OverlayTextFields[type] setHidden: !Session::get()->getOverlayManager().isOverlayEnabled(type)];
}
static void updateDebugOverlayOnMainThread(void* context)
{
VTRenderer* me = (VTRenderer*)context;
// Lazy initialization for the overlay
if (me->m_DebugOverlayTextField == nullptr) {
me->m_DebugOverlayTextField = [[NSTextField alloc] initWithFrame:me->m_StreamView.bounds];
[me->m_DebugOverlayTextField setBezeled:NO];
[me->m_DebugOverlayTextField setDrawsBackground:NO];
[me->m_DebugOverlayTextField setEditable:NO];
[me->m_DebugOverlayTextField setSelectable:NO];
me->updateOverlayOnMainThread(Overlay::OverlayDebug);
}
SDL_Color color = Session::get()->getOverlayManager().getOverlayColor(Overlay::OverlayDebug);
[me->m_DebugOverlayTextField setTextColor:[NSColor colorWithSRGBRed:color.r / 255.0 green:color.g / 255.0 blue:color.b / 255.0 alpha:color.a / 255.0]];
[me->m_DebugOverlayTextField setFont:[NSFont messageFontOfSize:Session::get()->getOverlayManager().getOverlayFontSize(Overlay::OverlayDebug)]];
static void updateStatusOverlayOnMainThread(void* context)
{
VTRenderer* me = (VTRenderer*)context;
[me->m_StreamView addSubview: me->m_DebugOverlayTextField];
}
// Update text contents
[me->m_DebugOverlayTextField setStringValue: [NSString stringWithUTF8String:Session::get()->getOverlayManager().getOverlayText(Overlay::OverlayDebug)]];
// Unhide if it's enabled
[me->m_DebugOverlayTextField setHidden: !Session::get()->getOverlayManager().isOverlayEnabled(Overlay::OverlayDebug)];
me->updateOverlayOnMainThread(Overlay::OverlayStatusUpdate);
}
virtual void notifyOverlayUpdated(Overlay::OverlayType type) override
{
// We must do the actual UI updates on the main thread, so queue an
// async callback on the main thread via GCD to do the UI update.
if (type == Overlay::OverlayDebug) {
switch (type) {
case Overlay::OverlayDebug:
dispatch_async_f(dispatch_get_main_queue(), this, updateDebugOverlayOnMainThread);
break;
case Overlay::OverlayStatusUpdate:
dispatch_async_f(dispatch_get_main_queue(), this, updateStatusOverlayOnMainThread);
break;
default:
break;
}
}
@@ -251,25 +429,15 @@ public:
return true;
}
virtual int getDecoderCapabilities() override
{
return 0;
}
virtual IFFmpegRenderer::FramePacingConstraint getFramePacingConstraint() override
{
// This renderer is inherently tied to V-sync due how we're
// rendering with AVSampleBufferDisplay layer. Running without
// the V-Sync source leads to massive stuttering.
return PACING_FORCE_ON;
}
private:
AVBufferRef* m_HwContext;
AVSampleBufferDisplayLayer* m_DisplayLayer;
CMVideoFormatDescriptionRef m_FormatDesc;
NSView* m_StreamView;
NSTextField* m_DebugOverlayTextField;
NSTextField* m_OverlayTextFields[Overlay::OverlayMax];
CVDisplayLinkRef m_DisplayLink;
SDL_mutex* m_VsyncMutex;
SDL_cond* m_VsyncPassed;
};
IFFmpegRenderer* VTRendererFactory::createRenderer() {
+260 -125
View File
@@ -6,6 +6,7 @@
#include <h264_stream.h>
#include "ffmpeg-renderers/sdlvid.h"
#include "ffmpeg-renderers/cuda.h"
#ifdef Q_OS_WIN32
#include "ffmpeg-renderers/dxva2.h"
@@ -23,6 +24,14 @@
#include "ffmpeg-renderers/vdpau.h"
#endif
#ifdef HAVE_MMAL
#include "ffmpeg-renderers/mmal.h"
#endif
#ifdef HAVE_DRM
#include "ffmpeg-renderers/drm.h"
#endif
// This is gross but it allows us to use sizeof()
#include "ffmpeg_videosamples.cpp"
@@ -32,12 +41,18 @@
bool FFmpegVideoDecoder::isHardwareAccelerated()
{
return m_HwDecodeCfg != nullptr;
return m_HwDecodeCfg != nullptr ||
(m_VideoDecoderCtx->codec->capabilities & AV_CODEC_CAP_HARDWARE) != 0;
}
int FFmpegVideoDecoder::getDecoderCapabilities()
{
return CAPABILITY_DIRECT_SUBMIT | m_Renderer->getDecoderCapabilities();
return m_BackendRenderer->getDecoderCapabilities();
}
int FFmpegVideoDecoder::getDecoderColorspace()
{
return m_FrontendRenderer->getDecoderColorspace();
}
enum AVPixelFormat FFmpegVideoDecoder::ffGetFormat(AVCodecContext* context,
@@ -65,7 +80,8 @@ FFmpegVideoDecoder::FFmpegVideoDecoder(bool testOnly)
: m_VideoDecoderCtx(nullptr),
m_DecodeBuffer(1024 * 1024, 0),
m_HwDecodeCfg(nullptr),
m_Renderer(nullptr),
m_BackendRenderer(nullptr),
m_FrontendRenderer(nullptr),
m_ConsecutiveFailedDecodes(0),
m_Pacer(nullptr),
m_LastFrameNumber(0),
@@ -94,9 +110,9 @@ FFmpegVideoDecoder::~FFmpegVideoDecoder()
av_log_set_level(AV_LOG_INFO);
}
IFFmpegRenderer* FFmpegVideoDecoder::getRenderer()
IFFmpegRenderer* FFmpegVideoDecoder::getBackendRenderer()
{
return m_Renderer;
return m_BackendRenderer;
}
void FFmpegVideoDecoder::reset()
@@ -115,8 +131,14 @@ void FFmpegVideoDecoder::reset()
Session::get()->getOverlayManager().setOverlayRenderer(nullptr);
}
delete m_Renderer;
m_Renderer = nullptr;
// If we have a separate frontend renderer, free that first
if (m_FrontendRenderer != m_BackendRenderer) {
delete m_FrontendRenderer;
}
delete m_BackendRenderer;
m_FrontendRenderer = m_BackendRenderer = nullptr;
if (!m_TestOnly) {
logVideoStats(m_GlobalVideoStats, "Global video stats");
@@ -127,35 +149,57 @@ void FFmpegVideoDecoder::reset()
}
}
bool FFmpegVideoDecoder::completeInitialization(AVCodec* decoder, SDL_Window* window,
int videoFormat, int width, int height,
int maxFps, bool enableFramePacing, bool testFrame)
bool FFmpegVideoDecoder::createFrontendRenderer(PDECODER_PARAMETERS params)
{
if (m_BackendRenderer->isDirectRenderingSupported()) {
// The backend renderer can render to the display
m_FrontendRenderer = m_BackendRenderer;
}
else {
// The backend renderer cannot directly render to the display, so
// we will create an SDL renderer to draw the frames.
m_FrontendRenderer = new SdlRenderer();
if (!m_FrontendRenderer->initialize(params)) {
return false;
}
}
// Determine whether the frontend renderer prefers frame pacing
auto vsyncConstraint = m_FrontendRenderer->getFramePacingConstraint();
if (vsyncConstraint == IFFmpegRenderer::PACING_FORCE_OFF && params->enableFramePacing) {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Frame pacing is forcefully disabled by the frontend renderer");
params->enableFramePacing = false;
}
else if (vsyncConstraint == IFFmpegRenderer::PACING_FORCE_ON && !params->enableFramePacing) {
// FIXME: This duplicates logic in Session.cpp
int displayHz = StreamUtils::getDisplayRefreshRate(params->window);
if (displayHz + 5 >= params->frameRate) {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Frame pacing is forcefully enabled by the frontend renderer");
params->enableFramePacing = true;
}
}
return true;
}
bool FFmpegVideoDecoder::completeInitialization(AVCodec* decoder, PDECODER_PARAMETERS params, bool testFrame)
{
// In test-only mode, we should only see test frames
SDL_assert(!m_TestOnly || testFrame);
auto vsyncConstraint = m_Renderer->getFramePacingConstraint();
if (vsyncConstraint == IFFmpegRenderer::PACING_FORCE_OFF && enableFramePacing) {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Frame pacing is forcefully disabled by the active renderer");
enableFramePacing = false;
}
else if (vsyncConstraint == IFFmpegRenderer::PACING_FORCE_ON && !enableFramePacing) {
// FIXME: This duplicates logic in Session.cpp
int displayHz = StreamUtils::getDisplayRefreshRate(window);
if (displayHz + 5 >= maxFps) {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Frame pacing is forcefully enabled by the active renderer");
enableFramePacing = true;
}
// Create the frontend renderer based on the capabilities of the backend renderer
if (!createFrontendRenderer(params)) {
return false;
}
m_StreamFps = maxFps;
m_StreamFps = params->frameRate;
// Don't bother initializing Pacer if we're not actually going to render
if (!testFrame) {
m_Pacer = new Pacer(m_Renderer, &m_ActiveWndVideoStats);
if (!m_Pacer->initialize(window, maxFps, enableFramePacing)) {
m_Pacer = new Pacer(m_FrontendRenderer, &m_ActiveWndVideoStats);
if (!m_Pacer->initialize(params->window, params->frameRate, params->enableFramePacing)) {
return false;
}
}
@@ -174,8 +218,17 @@ bool FFmpegVideoDecoder::completeInitialization(AVCodec* decoder, SDL_Window* wi
m_VideoDecoderCtx->flags |= AV_CODEC_FLAG_OUTPUT_CORRUPT;
m_VideoDecoderCtx->flags2 |= AV_CODEC_FLAG2_SHOW_ALL;
// Report decoding errors to allow us to request a key frame
//
// With HEVC streams, FFmpeg can drop a frame (hwaccel->start_frame() fails)
// without telling us. Since we have an infinite GOP length, this causes artifacts
// on screen that persist for a long time. It's easy to cause this condition
// by using NVDEC and delaying 100 ms randomly in the render path so the decoder
// runs out of output buffers.
m_VideoDecoderCtx->err_recognition = AV_EF_EXPLODE;
// Enable slice multi-threading for software decoding
if (!m_HwDecodeCfg) {
if (!isHardwareAccelerated()) {
m_VideoDecoderCtx->thread_type = FF_THREAD_SLICE;
m_VideoDecoderCtx->thread_count = qMin(MAX_SLICES, SDL_GetCPUCount());
}
@@ -185,13 +238,13 @@ bool FFmpegVideoDecoder::completeInitialization(AVCodec* decoder, SDL_Window* wi
}
// Setup decoding parameters
m_VideoDecoderCtx->width = width;
m_VideoDecoderCtx->height = height;
m_VideoDecoderCtx->pix_fmt = AV_PIX_FMT_YUV420P; // FIXME: HDR
m_VideoDecoderCtx->width = params->width;
m_VideoDecoderCtx->height = params->height;
m_VideoDecoderCtx->pix_fmt = m_FrontendRenderer->getPreferredPixelFormat(params->videoFormat);
m_VideoDecoderCtx->get_format = ffGetFormat;
// Allow the renderer to attach data to this decoder
if (!m_Renderer->prepareDecoderContext(m_VideoDecoderCtx)) {
// Allow the backend renderer to attach data to this decoder
if (!m_BackendRenderer->prepareDecoderContext(m_VideoDecoderCtx)) {
return false;
}
@@ -206,7 +259,7 @@ bool FFmpegVideoDecoder::completeInitialization(AVCodec* decoder, SDL_Window* wi
if (err < 0) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Unable to open decoder for format: %x",
videoFormat);
params->videoFormat);
return false;
}
@@ -216,13 +269,24 @@ bool FFmpegVideoDecoder::completeInitialization(AVCodec* decoder, SDL_Window* wi
// now to see if things will actually work when the video stream
// comes in.
if (testFrame) {
if (videoFormat & VIDEO_FORMAT_MASK_H264) {
switch (params->videoFormat) {
case VIDEO_FORMAT_H264:
m_Pkt.data = (uint8_t*)k_H264TestFrame;
m_Pkt.size = sizeof(k_H264TestFrame);
}
else {
m_Pkt.data = (uint8_t*)k_HEVCTestFrame;
m_Pkt.size = sizeof(k_HEVCTestFrame);
break;
case VIDEO_FORMAT_H265:
m_Pkt.data = (uint8_t*)k_HEVCMainTestFrame;
m_Pkt.size = sizeof(k_HEVCMainTestFrame);
break;
case VIDEO_FORMAT_H265_MAIN10:
m_Pkt.data = (uint8_t*)k_HEVCMain10TestFrame;
m_Pkt.size = sizeof(k_HEVCMain10TestFrame);
break;
default:
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"No test frame for format: %x",
params->videoFormat);
return false;
}
err = avcodec_send_packet(m_VideoDecoderCtx, &m_Pkt);
@@ -235,8 +299,8 @@ bool FFmpegVideoDecoder::completeInitialization(AVCodec* decoder, SDL_Window* wi
}
}
else {
if ((videoFormat & VIDEO_FORMAT_MASK_H264) &&
!(m_Renderer->getDecoderCapabilities() & CAPABILITY_REFERENCE_FRAME_INVALIDATION_AVC)) {
if ((params->videoFormat & VIDEO_FORMAT_MASK_H264) &&
!(m_BackendRenderer->getDecoderCapabilities() & CAPABILITY_REFERENCE_FRAME_INVALIDATION_AVC)) {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Using H.264 SPS fixup");
m_NeedsSpsFixup = true;
@@ -245,8 +309,8 @@ bool FFmpegVideoDecoder::completeInitialization(AVCodec* decoder, SDL_Window* wi
m_NeedsSpsFixup = false;
}
// Tell overlay manager to use this renderer
Session::get()->getOverlayManager().setOverlayRenderer(m_Renderer);
// Tell overlay manager to use this frontend renderer
Session::get()->getOverlayManager().setOverlayRenderer(m_FrontendRenderer);
}
return true;
@@ -331,54 +395,108 @@ void FFmpegVideoDecoder::logVideoStats(VIDEO_STATS& stats, const char* title)
}
}
IFFmpegRenderer* FFmpegVideoDecoder::createAcceleratedRenderer(const AVCodecHWConfig* hwDecodeCfg)
IFFmpegRenderer* FFmpegVideoDecoder::createHwAccelRenderer(const AVCodecHWConfig* hwDecodeCfg, int pass)
{
if (!(hwDecodeCfg->methods & AV_CODEC_HW_CONFIG_METHOD_HW_DEVICE_CTX)) {
return nullptr;
}
// Look for acceleration types we support
switch (hwDecodeCfg->device_type) {
// First pass using our top-tier hwaccel implementations
if (pass == 0) {
switch (hwDecodeCfg->device_type) {
#ifdef Q_OS_WIN32
case AV_HWDEVICE_TYPE_DXVA2:
return new DXVA2Renderer();
case AV_HWDEVICE_TYPE_DXVA2:
return new DXVA2Renderer();
#endif
#ifdef Q_OS_DARWIN
case AV_HWDEVICE_TYPE_VIDEOTOOLBOX:
return VTRendererFactory::createRenderer();
case AV_HWDEVICE_TYPE_VIDEOTOOLBOX:
return VTRendererFactory::createRenderer();
#endif
#ifdef HAVE_LIBVA
case AV_HWDEVICE_TYPE_VAAPI:
return new VAAPIRenderer();
case AV_HWDEVICE_TYPE_VAAPI:
return new VAAPIRenderer();
#endif
#ifdef HAVE_LIBVDPAU
case AV_HWDEVICE_TYPE_VDPAU:
return new VDPAURenderer();
case AV_HWDEVICE_TYPE_VDPAU:
return new VDPAURenderer();
#endif
default:
default:
return nullptr;
}
}
// Second pass for our second-tier hwaccel implementations
else if (pass == 1) {
switch (hwDecodeCfg->device_type) {
case AV_HWDEVICE_TYPE_CUDA:
// CUDA should only be used if all other options fail, since it requires
// read-back of frames. This should only be used for the NVIDIA+Wayland case
// with VDPAU covering the NVIDIA+X11 scenario.
return new CUDARenderer();
default:
return nullptr;
}
}
else {
SDL_assert(false);
return nullptr;
}
}
bool FFmpegVideoDecoder::initialize(
StreamingPreferences::VideoDecoderSelection vds,
SDL_Window* window,
int videoFormat,
int width,
int height,
int maxFps,
bool enableVsync,
bool enableFramePacing)
bool FFmpegVideoDecoder::tryInitializeRenderer(AVCodec* decoder,
PDECODER_PARAMETERS params,
const AVCodecHWConfig* hwConfig,
std::function<IFFmpegRenderer*()> createRendererFunc)
{
m_BackendRenderer = createRendererFunc();
m_HwDecodeCfg = hwConfig;
if (m_BackendRenderer != nullptr &&
m_BackendRenderer->initialize(params) &&
completeInitialization(decoder, params, m_TestOnly || m_BackendRenderer->needsTestFrame())) {
if (m_TestOnly) {
// This decoder is only for testing capabilities, so don't bother
// creating a usable renderer
return true;
}
if (m_BackendRenderer->needsTestFrame()) {
// The test worked, so now let's initialize it for real
reset();
if ((m_BackendRenderer = createRendererFunc()) != nullptr &&
m_BackendRenderer->initialize(params) &&
completeInitialization(decoder, params, false)) {
return true;
}
else {
SDL_LogCritical(SDL_LOG_CATEGORY_APPLICATION,
"Decoder failed to initialize after successful test");
reset();
}
}
else {
// No test required. Good to go now.
return true;
}
}
else {
// Failed to initialize or test frame failed, so keep looking
reset();
}
return false;
}
bool FFmpegVideoDecoder::initialize(PDECODER_PARAMETERS params)
{
AVCodec* decoder;
// Increase log level until the first frame is decoded
av_log_set_level(AV_LOG_DEBUG);
if (videoFormat & VIDEO_FORMAT_MASK_H264) {
if (params->videoFormat & VIDEO_FORMAT_MASK_H264) {
decoder = avcodec_find_decoder(AV_CODEC_ID_H264);
}
else if (videoFormat & VIDEO_FORMAT_MASK_H265) {
else if (params->videoFormat & VIDEO_FORMAT_MASK_H265) {
decoder = avcodec_find_decoder(AV_CODEC_ID_HEVC);
}
else {
@@ -389,67 +507,87 @@ bool FFmpegVideoDecoder::initialize(
if (!decoder) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Unable to find decoder for format: %x",
videoFormat);
params->videoFormat);
return false;
}
for (int i = 0;; i++) {
const AVCodecHWConfig *config = avcodec_get_hw_config(decoder, i);
if (!config || vds == StreamingPreferences::VDS_FORCE_SOFTWARE) {
// No matching hardware acceleration support.
// This is not an error.
m_HwDecodeCfg = nullptr;
m_Renderer = new SdlRenderer();
if (vds != StreamingPreferences::VDS_FORCE_HARDWARE &&
m_Renderer->initialize(window, videoFormat, width, height, maxFps, enableVsync) &&
completeInitialization(decoder, window, videoFormat, width, height, maxFps, enableFramePacing, m_TestOnly)) {
return true;
// Look for a hardware decoder first unless software-only
if (params->vds != StreamingPreferences::VDS_FORCE_SOFTWARE) {
// Look for the first matching hwaccel hardware decoder (pass 0)
for (int i = 0;; i++) {
const AVCodecHWConfig *config = avcodec_get_hw_config(decoder, i);
if (!config) {
// No remaing hwaccel options
break;
}
else {
reset();
return false;
// Initialize the hardware codec and submit a test frame if the renderer needs it
if (tryInitializeRenderer(decoder, params, config,
[config]() -> IFFmpegRenderer* { return createHwAccelRenderer(config, 0); })) {
return true;
}
}
m_Renderer = createAcceleratedRenderer(config);
if (!m_Renderer) {
continue;
// Continue with special non-hwaccel hardware decoders
#ifdef HAVE_MMAL
// MMAL is the decoder for the Raspberry Pi
if (params->videoFormat & VIDEO_FORMAT_MASK_H264) {
AVCodec* mmalDecoder = avcodec_find_decoder_by_name("h264_mmal");
if (mmalDecoder != nullptr &&
tryInitializeRenderer(mmalDecoder, params, nullptr,
[]() -> IFFmpegRenderer* { return new MmalRenderer(); })) {
return true;
}
}
#endif
m_HwDecodeCfg = config;
// Initialize the hardware codec and submit a test frame if the renderer needs it
if (m_Renderer->initialize(window, videoFormat, width, height, maxFps, enableVsync) &&
completeInitialization(decoder, window, videoFormat, width, height, maxFps, enableFramePacing, m_TestOnly || m_Renderer->needsTestFrame())) {
if (m_TestOnly) {
// This decoder is only for testing capabilities, so don't bother
// creating a usable renderer
return true;
}
#ifdef HAVE_DRM
// RKMPP is a hardware accelerated decoder that outputs DRI PRIME buffers
AVCodec* rkmppDecoder;
if (m_Renderer->needsTestFrame()) {
// The test worked, so now let's initialize it for real
reset();
if ((m_Renderer = createAcceleratedRenderer(config)) != nullptr &&
m_Renderer->initialize(window, videoFormat, width, height, maxFps, enableVsync) &&
completeInitialization(decoder, window, videoFormat, width, height, maxFps, enableFramePacing, false)) {
return true;
}
else {
SDL_LogCritical(SDL_LOG_CATEGORY_APPLICATION,
"Decoder failed to initialize after successful test");
reset();
}
}
else {
// No test required. Good to go now.
return true;
}
if (params->videoFormat & VIDEO_FORMAT_MASK_H264) {
rkmppDecoder = avcodec_find_decoder_by_name("h264_rkmpp");
}
else {
// Failed to initialize or test frame failed, so keep looking
reset();
rkmppDecoder = avcodec_find_decoder_by_name("hevc_rkmpp");
}
if (rkmppDecoder != nullptr &&
tryInitializeRenderer(rkmppDecoder, params, nullptr,
[]() -> IFFmpegRenderer* { return new DrmRenderer(); })) {
return true;
}
#endif
// Look for the first matching hwaccel hardware decoder (pass 1)
// This picks up "second-tier" hwaccels like CUDA.
for (int i = 0;; i++) {
const AVCodecHWConfig *config = avcodec_get_hw_config(decoder, i);
if (!config) {
// No remaing hwaccel options
break;
}
// Initialize the hardware codec and submit a test frame if the renderer needs it
if (tryInitializeRenderer(decoder, params, config,
[config]() -> IFFmpegRenderer* { return createHwAccelRenderer(config, 1); })) {
return true;
}
}
}
// Fallback to software if no matching hardware decoder was found
// and if software fallback is allowed
if (params->vds != StreamingPreferences::VDS_FORCE_HARDWARE) {
if (tryInitializeRenderer(decoder, params, nullptr,
[]() -> IFFmpegRenderer* { return new SdlRenderer(); })) {
return true;
}
}
// No decoder worked
return false;
}
void FFmpegVideoDecoder::writeBuffer(PLENTRY entry, int& offset)
@@ -542,9 +680,9 @@ int FFmpegVideoDecoder::submitDecodeUnit(PDECODE_UNIT du)
// Add some extra space in case we need to do an SPS fixup
requiredBufferSize += MAX_SPS_EXTRA_SIZE;
}
if (requiredBufferSize + AV_INPUT_BUFFER_PADDING_SIZE > m_DecodeBuffer.length()) {
m_DecodeBuffer = QByteArray(requiredBufferSize + AV_INPUT_BUFFER_PADDING_SIZE, 0);
}
// Ensure the decoder buffer is large enough
m_DecodeBuffer.reserve(requiredBufferSize + AV_INPUT_BUFFER_PADDING_SIZE);
int offset = 0;
while (entry != nullptr) {
@@ -598,8 +736,11 @@ int FFmpegVideoDecoder::submitDecodeUnit(PDECODE_UNIT du)
// Restore default log level after a successful decode
av_log_set_level(AV_LOG_INFO);
// Store the presentation time
frame->pts = du->presentationTimeMs;
// Capture a frame timestamp to measuring pacing delay
frame->pts = SDL_GetTicks();
frame->pkt_dts = SDL_GetTicks();
// Count time in avcodec_send_packet() and avcodec_receive_frame()
// as time spent decoding
@@ -630,14 +771,8 @@ int FFmpegVideoDecoder::submitDecodeUnit(PDECODE_UNIT du)
return DR_OK;
}
// Called on main thread
void FFmpegVideoDecoder::renderFrame(SDL_UserEvent*)
void FFmpegVideoDecoder::renderFrameOnMainThread()
{
SDL_assert(false);
m_Pacer->renderOnMainThread();
}
// Called on main thread
void FFmpegVideoDecoder::dropFrame(SDL_UserEvent*)
{
SDL_assert(false);
}
+19 -17
View File
@@ -1,5 +1,7 @@
#pragma once
#include <functional>
#include "decoder.h"
#include "ffmpeg-renderers/renderer.h"
#include "ffmpeg-renderers/pacer/pacer.h"
@@ -12,26 +14,17 @@ class FFmpegVideoDecoder : public IVideoDecoder {
public:
FFmpegVideoDecoder(bool testOnly);
virtual ~FFmpegVideoDecoder() override;
virtual bool initialize(StreamingPreferences::VideoDecoderSelection vds,
SDL_Window* window,
int videoFormat,
int width,
int height,
int maxFps,
bool enableVsync,
bool enableFramePacing) override;
virtual bool initialize(PDECODER_PARAMETERS params) override;
virtual bool isHardwareAccelerated() override;
virtual int getDecoderCapabilities() override;
virtual int getDecoderColorspace() override;
virtual int submitDecodeUnit(PDECODE_UNIT du) override;
virtual void renderFrame(SDL_UserEvent* event) override;
virtual void dropFrame(SDL_UserEvent* event) override;
virtual void renderFrameOnMainThread() override;
virtual IFFmpegRenderer* getRenderer();
virtual IFFmpegRenderer* getBackendRenderer();
private:
bool completeInitialization(AVCodec* decoder, SDL_Window* window,
int videoFormat, int width, int height,
int maxFps, bool enableFramePacing, bool testFrame);
bool completeInitialization(AVCodec* decoder, PDECODER_PARAMETERS params, bool testFrame);
void stringifyVideoStats(VIDEO_STATS& stats, char* output);
@@ -39,7 +32,14 @@ private:
void addVideoStats(VIDEO_STATS& src, VIDEO_STATS& dst);
IFFmpegRenderer* createAcceleratedRenderer(const AVCodecHWConfig* hwDecodeCfg);
bool createFrontendRenderer(PDECODER_PARAMETERS params);
bool tryInitializeRenderer(AVCodec* decoder,
PDECODER_PARAMETERS params,
const AVCodecHWConfig* hwConfig,
std::function<IFFmpegRenderer*()> createRendererFunc);
static IFFmpegRenderer* createHwAccelRenderer(const AVCodecHWConfig* hwDecodeCfg, int pass);
void reset();
@@ -53,7 +53,8 @@ private:
AVCodecContext* m_VideoDecoderCtx;
QByteArray m_DecodeBuffer;
const AVCodecHWConfig* m_HwDecodeCfg;
IFFmpegRenderer* m_Renderer;
IFFmpegRenderer* m_BackendRenderer;
IFFmpegRenderer* m_FrontendRenderer;
int m_ConsecutiveFailedDecodes;
Pacer* m_Pacer;
VIDEO_STATS m_ActiveWndVideoStats;
@@ -66,5 +67,6 @@ private:
bool m_TestOnly;
static const uint8_t k_H264TestFrame[];
static const uint8_t k_HEVCTestFrame[];
static const uint8_t k_HEVCMainTestFrame[];
static const uint8_t k_HEVCMain10TestFrame[];
};
+13 -2
View File
@@ -10,10 +10,21 @@ const uint8_t FFmpegVideoDecoder::k_H264TestFrame[] = {
0x00, 0x00, 0x01, 0x65, 0x00, 0x14, 0xa2, 0xe0, 0x08, 0x5f, 0xe3, 0x85, 0x1c, 0xd9, 0xff, 0xe1, 0x1b, 0x01, 0xfa, 0xc0, 0x3e, 0x11, 0x7e, 0xfe, 0x45, 0x5c, 0x43, 0x69, 0x31, 0x4b, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x02, 0x24, 0xb9, 0x7d, 0xb4, 0x70, 0x4d, 0x15, 0xc5, 0x0a, 0x4e, 0x60, 0x00, 0x00, 0x03, 0x00, 0x00, 0x26, 0xa8, 0xb0, 0x00, 0x13, 0xcd, 0xcc, 0x00, 0x07, 0x48, 0x88, 0x00, 0x06, 0x29, 0x69, 0x00, 0x01, 0x16, 0xac, 0x80, 0x00, 0x9e, 0x4e, 0x80, 0x00, 0x3f, 0x84, 0x20, 0x00, 0x6f, 0x41, 0xa0, 0x00, 0x20, 0x00, 0x02, 0x16, 0xb8, 0x00, 0x08, 0x08
};
// 720p 60 FPS HEVC with 1 reference frame
const uint8_t FFmpegVideoDecoder::k_HEVCTestFrame[] = {
// 720p 60 FPS HEVC Main with 1 reference frame
const uint8_t FFmpegVideoDecoder::k_HEVCMainTestFrame[] = {
0x00, 0x00, 0x00, 0x01, 0x40, 0x01, 0x0c, 0x01, 0xff, 0xff, 0x01, 0x40, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x78, 0xac, 0x09,
0x00, 0x00, 0x00, 0x01, 0x42, 0x01, 0x01, 0x01, 0x40, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x78, 0xa0, 0x02, 0x80, 0x80, 0x2e, 0x1f, 0x13, 0x96, 0xb4, 0xa4, 0x25, 0x92, 0xe3, 0x01, 0x6a, 0x0c, 0x0c, 0x0d, 0x48, 0x20, 0x00, 0x00, 0x03, 0x00, 0x20, 0x00, 0x00, 0x07, 0x85, 0xf1, 0xa2, 0xd0,
0x00, 0x00, 0x00, 0x01, 0x44, 0x01, 0xc0, 0xf7, 0xc0, 0xcc, 0x90,
0x00, 0x00, 0x00, 0x01, 0x2a, 0x01, 0xac, 0x0c, 0xf0, 0xe6, 0xf6, 0x5e, 0xff, 0xd9, 0x97, 0xeb, 0x44, 0x7d, 0x1f, 0xa4, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x03, 0x52, 0x7c, 0x4e, 0x5b, 0xfe, 0xbe, 0xc2, 0x87, 0x20, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x2b, 0x60,
};
// 720p 60 FPS HEVC Main10 with 1 reference frame
const uint8_t FFmpegVideoDecoder::k_HEVCMain10TestFrame[] = {
0x00, 0x00, 0x00, 0x01, 0x40, 0x01, 0x0c, 0x01, 0xff, 0xff, 0x02, 0x20, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x78, 0xac, 0x09,
0x00, 0x00, 0x00, 0x01, 0x42, 0x01, 0x01, 0x02, 0x20, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x78, 0xa0, 0x02, 0x80, 0x80, 0x2e, 0x1f, 0x12, 0xd9, 0x6b, 0x4a, 0x42, 0x59, 0x2e, 0x30, 0x16, 0xa0, 0xc0, 0xc0, 0xd4, 0x82, 0x00, 0x00, 0x03, 0x00, 0x02, 0x00, 0x00, 0x03, 0x00, 0x78, 0x5f, 0x1a, 0x2d,
0x00, 0x00, 0x00, 0x01, 0x44, 0x01, 0xc0, 0xf3, 0xc0, 0x4c, 0x90,
0x00, 0x00, 0x00, 0x01, 0x2a, 0x01, 0xac, 0x0c, 0xf0, 0xbc, 0x48, 0x02, 0x6f, 0xff, 0xd3, 0xee, 0x76, 0xf2, 0x4e, 0x53, 0x5f, 0x1e, 0xbb, 0x79, 0x03, 0x0e, 0xd5, 0x68, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x07, 0xb4,
0x00, 0x00, 0x00, 0x01, 0x2a, 0x01, 0x26, 0x43, 0x03, 0x3c, 0x2f, 0x48, 0x02, 0x6f, 0xff, 0xd3, 0xee, 0x76, 0xf2, 0x4e, 0x53, 0x5f, 0x1e, 0xbb, 0x79, 0x03, 0x0e, 0xd5, 0x68, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x05, 0xb4,
0x00, 0x00, 0x00, 0x01, 0x2a, 0x01, 0x2d, 0xc3, 0x03, 0x3c, 0x2f, 0x48, 0x02, 0x6f, 0xff, 0xd3, 0xee, 0x76, 0xf2, 0x4e, 0x53, 0x5f, 0x1e, 0xbb, 0x79, 0x03, 0x0e, 0xd5, 0x68, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x04, 0x24,
0x00, 0x00, 0x00, 0x01, 0x2a, 0x01, 0x36, 0x83, 0x03, 0x3c, 0x2f, 0x48, 0x02, 0x6f, 0xff, 0xd3, 0xee, 0x76, 0xf2, 0x4e, 0x53, 0x5f, 0x1e, 0xbb, 0x79, 0x03, 0x0e, 0xd5, 0x68, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x07, 0xb4
};

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