Compare commits

..
204 Commits
Author SHA1 Message Date
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
Cameron Gutman f64486c88f Version 0.9.0 2019-02-17 00:18:42 -08:00
Cameron Gutman 80a40ac5f9 Increase frame drop threshold 2019-02-16 23:13:35 -08:00
Cameron Gutman a1956350b1 Rename pacing drop to jitter drop to be less confusing for users 2019-02-16 23:04:25 -08:00
Cameron Gutman 2c09875556 Fix input handler cleanup on startup failure 2019-02-16 01:39:31 -08:00
Cameron Gutman 561eed4cbd Use 2 concurrent jobs on Travis CI 2019-02-16 00:59:00 -08:00
Cameron Gutman a8f72df7f8 Fix in-tree builds 2019-02-16 00:42:09 -08:00
Cameron Gutman ab4127f940 Use presence of a frame after rendering rather than render time for frame dropping 2019-02-15 22:44:52 -08:00
Cameron Gutman f03d12b677 Use a separate render thread even with a V-Sync source 2019-02-15 22:09:50 -08:00
Cameron Gutman 81c6202582 Rename renderFrameAtVsync() to renderFrame() in preparation for further refactoring 2019-02-15 21:07:02 -08:00
Cameron Gutman 6cab2bec61 Don't trust VT to tell us whether decode is supported 2019-02-15 19:10:21 -08:00
Cameron Gutman 94ffda653c Update SDL_GameControllerDB to add new mappings 2019-02-15 19:00:30 -08:00
Cameron Gutman c2268b45f6 Merge pull request #177 from konst2/master
Fix hotkeys for non-latin keyboard layouts
2019-02-15 18:57:24 -08:00
Cameron Gutman 38b8a59fe5 Adjust overlay color for more contrast 2019-02-15 18:53:40 -08:00
Cameron Gutman 640e3213ee Copy the required data files into place for building in the IDE 2019-02-15 18:47:42 -08:00
Cameron Gutman b1799009b3 Use the Path class to lookup ModeSeven.ttf 2019-02-15 18:13:36 -08:00
Cameron Gutman f8e693a060 Drop samples to account for lost time reinitializing audio. Fixes #176 2019-02-14 22:32:54 -08:00
Cameron Gutman aba806bc32 Implement debug overlay for VideoToolbox renderer 2019-02-13 20:55:52 -08:00
Cameron Gutman b1fb3f2391 Set FFmpeg log level at debug until first frame is rendered 2019-02-13 18:34:59 -08:00
konst2 041e901c61 Fix hotkeys for non-latin keyboard layouts 2019-02-13 19:56:44 +03:00
Cameron Gutman bb72134992 Attempt to fix Travis CI Linux build 2019-02-12 23:25:23 -08:00
Cameron Gutman f83853c2fb OverlayManager now manages font size for the overlays 2019-02-12 22:36:56 -08:00
Cameron Gutman d5f7b20be3 Update common-c with sign extension fix 2019-02-12 22:24:47 -08:00
Cameron Gutman b878d64460 Check rumble controller number is within the supported range 2019-02-12 22:05:55 -08:00
Cameron Gutman 5397e8ae1a Don't use SDL_GameControllerGetPlayerIndex() because the indexes may be non-contiguous (and wrong) on XInput 2019-02-12 22:04:05 -08:00
Cameron Gutman 46bd8ad83d Add debug overlay for SDL renderer 2019-02-12 21:55:15 -08:00
Cameron Gutman 4dac57cf8c Add SDL2_ttf dependency for Linux 2019-02-12 21:38:51 -08:00
Cameron Gutman 95cb026446 Add SDL2_ttf for macOS 2019-02-12 21:32:11 -08:00
Cameron Gutman 76ea714257 Add SDL2_ttf for Windows 2019-02-12 21:18:55 -08:00
Cameron Gutman 5788fa7204 Make OverlayManager own the overlay color setting 2019-02-12 20:39:10 -08:00
Cameron Gutman d077c6450f Split SdlRenderer from renderer.h 2019-02-12 19:58:36 -08:00
Cameron Gutman 4499ed3878 Work around dialog rendering issue with QGnomePlatform. Fixes #161 2019-02-12 19:45:14 -08:00
Cameron Gutman feb5e3bc78 Fix SDL_WasInit asserts firing due to lifetime management issues with SdlInputHandler 2019-02-12 19:30:02 -08:00
Cameron Gutman 23f1060756 Polish the resume/quit buttons 2019-02-12 19:07:48 -08:00
Cameron Gutman 6ed512e762 Provide a renderer callback when the overlay data changes 2019-02-12 18:43:38 -08:00
Cameron Gutman 947970e07b Shortcut decoder initialization when only using it for capability testing 2019-02-12 18:42:53 -08:00
Cameron Gutman df567b405a Minor bugfixes to haptics 2019-02-11 21:55:56 -08:00
MINICOM\mrb11 7b0596d9df Add controller rumble support. 2019-02-11 21:39:55 -08:00
Cameron Gutman 4b7200ae69 Don't display the connection terminated dialog for intended disconnects 2019-02-10 02:19:37 -08:00
Cameron Gutman 710648f3f4 Fix QuitSegue -> StreamSegue transition to be direct via replacement and disallow toolbar interactions 2019-02-09 21:15:17 -08:00
Cameron Gutman d2002b6a62 Fix SOPS issues causing 720p60 settings on non-standard resolutions and FPS values 2019-02-09 20:37:11 -08:00
Cameron Gutman 8de42b3199 Fix gamepad menu focus and dismissal issues 2019-02-09 19:59:01 -08:00
Cameron Gutman 48e78d2cd3 Don't allow focus on invisible menu items 2019-02-09 19:21:43 -08:00
Cameron Gutman 1fd099db70 Log when StretchRect() fails 2019-02-09 18:47:57 -08:00
Cameron Gutman 2986a40c5a Fix some gamepads working for UI navigation but not in game 2019-01-29 22:01:05 -08:00
Cameron Gutman 8bb6d4d0d9 Fix VDPAU scaling on AMD cards. Fixes #171 2019-01-29 20:52:41 -08:00
Cameron Gutman 00f08578ee Update common-c to fix IDR issue on GFE 3.17 2019-01-29 20:18:03 -08:00
Cameron Gutman 6a514c7f51 Update readme to point to Snap package 2019-01-29 18:30:56 -08:00
Cameron Gutman 21aad643f3 Use 32 pixel alignment for HEVC to fix Intel green line issue 2019-01-28 21:26:27 -08:00
Cameron Gutman 8687448966 Use StretchRect() on Intel GPUs or if VideoProcessBlt() fails 2019-01-28 19:18:43 -08:00
Cameron Gutman 6fcc8b721b Add a button for quitting the running app 2019-01-26 23:57:02 -08:00
Cameron Gutman b4bbd58c77 Add long press action on PC view for touchscreens 2019-01-26 23:11:09 -08:00
Cameron Gutman 1a45b8f07a Fix dialog box styles 2019-01-26 21:45:32 -08:00
Cameron Gutman 4471a858db Fix miscounted pacer dropped frames in non-pacing mode 2019-01-22 21:21:29 -08:00
Cameron Gutman 22e712af5a Add v-sync text to stats overlay 2019-01-22 20:55:58 -08:00
Cameron Gutman 9799b82d53 Improve long render time logic 2019-01-22 20:55:35 -08:00
Cameron Gutman 78915e0b99 Use a separate render thread when not using frame pacing 2019-01-22 20:31:31 -08:00
Cameron Gutman c9cc65aae7 Fix appstream metadata release date for v0.8.1 2019-01-22 19:07:14 -08:00
Cameron Gutman a4edd9d202 Submit frame directly from the decoder thread to avoid interference from the main thread 2019-01-21 20:27:37 -08:00
Cameron Gutman 7747624c80 Revert "Wake up main thread immediately after new frame is pushed"
This reverts commit aab7191a44.
2019-01-21 20:10:13 -08:00
Cameron Gutman aab7191a44 Wake up main thread immediately after new frame is pushed 2019-01-21 18:57:14 -08:00
Cameron Gutman 1f972b1828 Require several consecutive long render times to drop a frame 2019-01-21 17:58:42 -08:00
Cameron Gutman 58b77cf589 Fix inaccurate pacer drop and network drop percentages and add total FPS stat 2019-01-21 17:43:15 -08:00
Cameron Gutman c064f87889 Drop a frame if the rendering time exceeds the v-sync period 2019-01-20 19:59:29 -08:00
Cameron Gutman d6b4c8e7da Avoid blocking decoding operations during Present calls 2019-01-20 14:08:53 -08:00
Cameron Gutman 49f0b45fa8 Update build requirements for Windows 2019-01-20 11:58:30 -08:00
Cameron Gutman b19c4bfc5d Add performance overlay for DXVA2 renderer 2019-01-19 23:05:56 -08:00
Cameron Gutman e944c819d9 Add prefix for NvLogLevel enum values 2019-01-19 21:32:35 -08:00
Cameron Gutman 75f631599c Ensure all polling threads are dead before destroying hosts, even ones that have been detached by stopPollingAsync() 2019-01-19 19:18:50 -08:00
Cameron Gutman e0bebeae25 Allow theme overrides via environment variables 2019-01-19 17:06:35 -08:00
Cameron Gutman 32e7ddf272 Version 0.8.1 2019-01-08 00:48:49 -08:00
Cameron Gutman 7091e0324e Update macOS builds to FFmpeg 4.1 2019-01-07 21:43:18 -08:00
Cameron Gutman 8f8532921c Update FFmpeg build script for Mac to avoid manual steps 2019-01-07 21:39:59 -08:00
Cameron Gutman e9936992ec Update Windows builds to FFmpeg 4.1 2019-01-07 21:08:08 -08:00
Cameron Gutman 6375055b3e Set scripts executable 2019-01-07 20:34:56 -08:00
Cameron Gutman 88b1ea296c Remove video bitrate tooltip since audio is accounted for during remote streaming 2019-01-06 15:41:00 -08:00
Cameron Gutman 2a13a869f3 Don't allow the scroll wheel to move the bitrate slider 2019-01-06 15:39:54 -08:00
Cameron Gutman 89c342bb6f Introduce more granular HTTP timeouts 2019-01-06 14:35:33 -08:00
Cameron Gutman 067f5e33aa Increase audio buffer on Linux to 25 ms. Fixes #147. Fixes #157. 2019-01-06 13:49:20 -08:00
Cameron Gutman 33695f1c6b Update common-c to reduce audio latency 2019-01-06 13:29:06 -08:00
Cameron Gutman 52ef84873e Remove excess latency control logic from SDL renderer, since it doesn't appear to work very well anyway 2019-01-06 13:28:43 -08:00
Cameron Gutman c21ba5c808 Allow the audio renderer to be overriden by the ML_AUDIO environment variable 2019-01-06 13:28:05 -08:00
Cameron Gutman 6cf408f12b Update macOS build steps 2019-01-05 19:23:01 -08:00
Cameron Gutman 8bdc2d285d Rename macOS build output to match final release name 2019-01-05 19:06:00 -08:00
Cameron Gutman a66fbcd6be Use version.txt for Info.plist too 2019-01-05 18:53:17 -08:00
Cameron Gutman 93402a62fc Archive symbol ZIP to ML_SYMBOL_ARCHIVE folder 2019-01-05 18:20:53 -08:00
Cameron Gutman 2a56fb6c70 Add config.test generated files to gitignore 2019-01-05 18:10:20 -08:00
Cameron Gutman 8276c031e9 Name the Windows output binaries to match their final names on GitHub 2019-01-05 18:09:52 -08:00
Cameron Gutman 102e5f6de9 Move the version string into version.txt 2019-01-05 18:08:34 -08:00
Cameron Gutman 1f7bdbfbcb Fail signed release builds if unstaged changes exist 2019-01-05 17:25:40 -08:00
Cameron Gutman f2fdd048c7 Use stun.moonlight-stream.org for STUN 2019-01-04 20:16:27 -08:00
Cameron Gutman 5a2cfc15a6 Ensure WoL works properly on multi-homed hosts. Fixes #135 2019-01-04 20:05:31 -08:00
Cameron Gutman 11c7131875 Replace forgotten clamp to frameCountMax removed by e75dce2543 2019-01-03 18:49:18 -08:00
Cameron Gutman 34aacc90d7 Include version string in update tooltip 2019-01-03 18:00:54 -08:00
Cameron Gutman e75dce2543 Remove one-sample-per-callback write logic to fix crackling at 44.1 KHz. Fixes #152 2019-01-03 17:23:29 -08:00
Cameron Gutman e2774309fb Fix context menus on Qt 5.9 2019-01-02 19:49:06 -08:00
Cameron Gutman dfefdc93c5 Update macOS icns 2019-01-01 19:33:08 -08:00
Cameron Gutman d46ea28bba Merge pull request #151 from Jorys-Paulin/feature/icon-tweaks
Updated Moonlight icon
2019-01-01 19:32:21 -08:00
Cameron Gutman d35c5bcaba Print socket errors when sending WoL packets 2019-01-01 19:20:09 -08:00
Cameron Gutman 2b14856d7f Update common-c for strict remote bitrate logic 2019-01-01 19:03:52 -08:00
Jorys Paulin 3316fdb80e Updated Moonlight icon 2018-12-26 13:29:49 +01:00
Cameron Gutman c5d70c48c9 Fix QMap usage where insertion on read of missing value was not intended 2018-12-26 00:59:30 -08:00
Cameron Gutman 82230ef2f5 Version 0.8.0 2018-12-25 22:29:27 -08:00
Cameron Gutman dcfd3d3e48 Add a debug print for V-sync 2018-12-25 22:19:23 -08:00
Cameron Gutman cb1e2ac32e Avoid filling our client logs with QNetworkReplyHttpImplPrivate warnings 2018-12-25 21:54:20 -08:00
Cameron Gutman ec2c15946a Update audio buffering parameters based on local testing 2018-12-25 21:45:15 -08:00
Cameron Gutman 8be73142f8 Fix cert pinning on macOS 2018-12-25 21:15:59 -08:00
Cameron Gutman d1640e3bb8 Audio latency tweaks and fix for PulseAudio with A2DP 2018-12-25 17:54:18 -08:00
Cameron Gutman 8612e6726d Remove accidentally enabled debug prints 2018-12-25 14:05:55 -08:00
Cameron Gutman 3b4694e9f8 Add libraries required by libsoundio 2018-12-25 14:05:35 -08:00
Cameron Gutman 14cb2e1999 Remove latency drop logic and replace by single frame writes 2018-12-25 14:05:17 -08:00
Cameron Gutman cfabaf334e Add frame pacing toggle 2018-12-25 12:57:00 -08:00
Cameron Gutman c054536fab Count Pacer delay in frame stats 2018-12-25 12:09:45 -08:00
Cameron Gutman 35aac18b4e Improve bitrate slider label and add tooltip 2018-12-25 11:48:34 -08:00
Cameron Gutman de3928cb4d Reduce FPs on the unmapped gamepad dialog 2018-12-25 11:41:48 -08:00
Cameron Gutman 9bf612b800 Update common-c with bitrate fix 2018-12-24 20:10:37 -08:00
Cameron Gutman d9a8d67e3d Use a shared UID for all Moonlight clients 2018-12-22 21:04:44 -08:00
Cameron Gutman 99b0a56a6a Add additional cert pinning assert 2018-12-22 20:05:45 -08:00
Cameron Gutman 61d7aa0400 Require cert pinning for HTTPS 2018-12-22 19:55:28 -08:00
Cameron Gutman 1e98594972 Handle server cert change by pairing again 2018-12-21 19:17:32 -08:00
Cameron Gutman c6383f042c Pin server cert to host during pairing 2018-12-21 18:08:07 -08:00
Cameron Gutman b4d8e0e551 Update bug_report.md 2018-12-19 23:11:15 +04:00
Cameron Gutman d696536486 Version 0.7.0 2018-12-05 22:49:42 -08:00
Cameron Gutman b46a3cf759 Show stream error dialog when launching from the CLI 2018-12-05 22:22:30 -08:00
Cameron Gutman 8a5e3cdb76 Fix returning to app page when start fails 2018-12-05 22:17:26 -08:00
Cameron Gutman 4fb39226ef Fix 'moonlight -v' output on Linux 2018-12-05 22:01:22 -08:00
Cameron Gutman c40174034c Don't kill the app on the host when the session terminates unexpectedly 2018-12-05 20:17:00 -08:00
Cameron Gutman 5029c855f0 Simplify app quitting implementation a bit 2018-12-05 19:49:06 -08:00
Cameron Gutman 2eee839eaa Update moonlight-common-c 2018-12-05 18:47:56 -08:00
Cameron Gutman f42c7fec1e Update SDL GC DB 2018-12-05 18:47:44 -08:00
Janne Hakonen 0ab07303c9 Add quit cli command and app quit option after stream session. Fixes #92 (#138)
* Add quit cli command and app quit option after stream session. Fixes #92

* Code review fixes.
2018-12-05 18:45:28 -08:00
Cameron Gutman ad47990a87 Make update icon more obnoxious so people actually update 2018-12-05 18:40:21 -08:00
Cameron Gutman 1968461ce9 Allow HTTP redirects when checking for updates 2018-12-03 23:29:49 -08:00
Cameron Gutman 8aa3b5d3da Switch to new hostname 2018-12-03 21:48:49 -08:00
Cameron Gutman 23e77eefb0 Rename multi-controller checkbox to be more descriptive 2018-11-29 20:10:47 -08:00
Cameron Gutman 094443cf26 Rename start in windowed mode option to be more accurate 2018-11-28 21:46:14 -08:00
Cameron Gutman b97758ef46 Switch to libsoundio on Linux now that PA works reliably after d1fe752028 2018-11-28 00:13:04 -08:00
Cameron Gutman d1fe752028 Fix busy looping in libsoundio audio renderer when no audio is playing 2018-11-27 23:43:31 -08:00
Cameron Gutman b26df787cd Update to upstream libsoundio 2018-11-27 23:26:15 -08:00
Cameron Gutman 4c5b254730 Add larger downloads section 2018-11-23 02:09:54 -08:00
Cameron Gutman 9a153b7170 Remove unneccessary hardcoded text color for better style support 2018-11-22 02:35:25 -08:00
Cameron Gutman 065a6caee5 Use automatic remote streaming detection 2018-11-21 20:55:25 -08:00
Cameron Gutman 9c7df02941 Avoid duplicate framework copy now that it's handle in the project file 2018-11-20 22:43:10 -08:00
Cameron Gutman c63c5100e2 Version 0.6.4 2018-11-20 22:23:03 -08:00
Cameron Gutman bf05088d70 Remove checks that rely on display modes because they can be inaccurate on machines when the monitor is off 2018-11-20 22:02:43 -08:00
Cameron Gutman e7bb146f92 Allow streaming to match the refresh rate in unsupported mode 2018-11-20 21:47:08 -08:00
Cameron Gutman 688c4a90d9 Use the raw display bounds not the usable display bounds for full-screen calculations 2018-11-18 23:10:30 -08:00
Cameron Gutman 165ad96be1 Fix stack overflow if SDL gamepad initialization fails 2018-11-18 22:04:44 -08:00
Cameron Gutman 115ced7ab4 Only precompile QML files on release builds 2018-11-18 22:04:11 -08:00
Cameron Gutman ef15b5641a Fix accidental dependency on system SDL2.framework when building in the IDE 2018-11-18 21:23:46 -08:00
Cameron Gutman be50464619 Revert "Update SDL2.framework to 2.0.9"
This reverts commit 7254224347.
2018-11-18 16:04:52 -08:00
Cameron Gutman d847b71c86 Fix host virtual gamepads being reconnected during stream startup and causing issues with multiple controllers. Fixes #108 2018-11-18 12:05:17 -08:00
Cameron Gutman dfcc1ff899 Also print gamepad mapping on gamepad attach 2018-11-17 17:23:20 -08:00
Cameron Gutman 65562937f3 Print gamepad GUID string in logs 2018-11-17 17:10:52 -08:00
Cameron Gutman 7b5dc33a21 Version 0.6.3 2018-11-16 01:17:57 -08:00
Cameron Gutman f64c13b9ec Ujpdate common-c with NAT64 fixes 2018-11-16 00:40:42 -08:00
Cameron Gutman 25f70e570e Add timer-based input batching for GFE 3.16 2018-11-15 20:52:07 -08:00
Cameron Gutman b27b64c25c Initialize the SDL timer subsystem 2018-11-15 20:45:32 -08:00
Cameron Gutman bfc64c926a Reorganize settings page now that vertical scrolling works 2018-11-15 16:41:02 -08:00
Cameron Gutman 88ad983aa4 Update common-c for GFE 3.16 2018-11-15 13:11:01 -08:00
Cameron Gutman a95d2e76f5 Fix settings page scrolling. Fixes #106 2018-11-13 18:03:53 -08:00
Cameron Gutman 037886ba5b Add tooltips and update help text 2018-11-04 14:36:12 -08:00
Cameron Gutman 011edfe2a0 Disable any usage of proxy servers 2018-11-04 14:15:53 -08:00
Cameron Gutman 1a5a6773ce Add option to determine if Moonlight launches in full-screen. Fixes #103 2018-11-04 13:57:33 -08:00
Cameron Gutman dda22fd387 Rename mouse acceleration toggle to raw mouse input 2018-11-04 13:31:28 -08:00
Cameron Gutman 7254224347 Update SDL2.framework to 2.0.9 2018-11-03 14:18:17 -07:00
Cameron Gutman 789e52af57 Use SDL_GameControllerGetPlayerIndex() as a hint for picking player indexes 2018-11-03 13:04:34 -07:00
Cameron Gutman b2fcd6c084 Update Windows SDL headers to 2.0.9 2018-11-03 13:01:35 -07:00
Cameron Gutman 76e5794203 Remove SetThreadExecutionState() call now that SDL_DisableScreenSaver() works on Windows 2018-11-03 12:44:14 -07:00
Cameron Gutman a81332dd23 Update SDL2 for Windows to MSVC build of 5dc13016cf34b2c503a7599fabdd4f62d1811fdb 2018-11-03 12:25:38 -07:00
Cameron Gutman e1fab22d98 Fix accidental removal of decoder deletion in previous commit 2018-10-31 18:49:37 -07:00
Cameron Gutman cb5e58d465 Fix use-after-free caught by debug build of SDL2 2018-10-31 18:20:39 -07:00
Cameron Gutman eaeb9ec6f7 Version 0.6.2 2018-10-28 19:57:38 -07:00
Cameron Gutman 88bb17fca8 Change Manufacturer string to "Moonlight Game Streaming Project" 2018-10-28 18:11:07 -07:00
Cameron Gutman f672b8534f Change quit tip based on whether gamepads are attached 2018-10-28 17:59:07 -07:00
Cameron Gutman 839c0a45a0 Avoid polling server status while streaming 2018-10-28 17:53:49 -07:00
Cameron Gutman 974e44ba4c Update common-c for more unique STUN error codes 2018-10-28 17:20:34 -07:00
Cameron Gutman 9461ebec2d Log an error if STUN fails 2018-10-26 20:54:17 -07:00
Cameron Gutman 48d8a53cd6 Resolve WAN address using STUN if the PC is discovered over mDNS 2018-10-26 20:44:51 -07:00
Cameron Gutman 2a8dfd63da Update common-c to include STUN code 2018-10-26 20:22:38 -07:00
Cameron Gutman 49ac431792 Fix calling convention of LoadLibrary hooks which caused x86 builds to crash 2018-10-15 07:41:50 -07:00
Cameron Gutman 8a7c463c52 Version 0.6.1 2018-10-14 23:08:58 -07:00
Cameron Gutman eab77fba7a Fix inability to play audio on sound cards set to 192 KHz sample rate 2018-10-14 22:52:24 -07:00
Cameron Gutman 61d90a5a88 Also display a tooltip when the item is highlighted by gamepad/keyboard 2018-10-14 22:23:08 -07:00
Cameron Gutman ef99f02bc7 Display the tooltip over the text itself and only if it's truncated 2018-10-14 21:39:37 -07:00
Cameron Gutman 9166a604d6 Rename NktHookLib.pdb back to its original name to avoid a link-time warning 2018-10-14 21:14:32 -07:00
Cameron Gutman d5a1189053 Avoid VAAPI on AMD drivers due to a bad memory leak 2018-10-14 20:37:13 -07:00
Cameron Gutman cb52a53922 Remove accidentally committed debugging code 2018-10-14 12:59:13 -07:00
Cameron Gutman 1aff2ff0c3 Fix CoreAudio failure to open device when it contains certain UTF-8 characters. Fixes #104 2018-10-14 11:33:13 -07:00
Cameron Gutman fd63aad3b8 Only avoid HEVC on GFE 3.10 and lower on macOS 2018-10-14 11:28:52 -07:00
Cameron Gutman 9a807a0685 Prevent asserts from being compiled into moonlight-common-c and soundio in release builds 2018-10-14 11:26:29 -07:00
Cameron Gutman 40fc9fa26f Fix Qt 5.9 incompatibility in AutoResizingComboBox. Fixes #107 2018-10-13 20:16:18 -07:00
Cameron Gutman cf0d29d452 Add AntiHooking helper DLL to prevent NahimicOSD.dll from loading into Moonlight and causing us to hang in full-screen mode. Fixes #102 2018-10-13 19:35:21 -07:00
Cameron Gutman e411c207ae Implement quitting Moonlight via gamepad or keyboard. Fixes #100 2018-10-13 00:41:22 -07:00
Cameron Gutman 5df2704ccc Compile h264bitstream as GNU99 on GCC 2018-10-12 21:45:41 -07:00
Cameron Gutman 36f468a599 Fix window icon disappearing when toggling out of full-screen mode on Windows 2018-10-12 20:34:58 -07:00
Cameron Gutman dfae69834a Fix misleading debug print 2018-10-12 20:02:54 -07:00
Cameron Gutman e53b32fa57 Stop polling machines after 5 minutes of inactivity to conserve power 2018-10-12 19:58:29 -07:00
Cameron Gutman 969afac696 Add tool tips to app grid 2018-10-12 19:41:20 -07:00
Cameron Gutman 50d4f267ba Fix font setting in TextMetrics. Fixes #99 2018-10-12 19:19:33 -07:00
Cameron Gutman 33b9caaca9 Compile libsoundio as GNU99 on GCC 2018-10-12 18:02:12 -07:00
Cameron Gutman 4f84843b00 Fixup H.264 SPS for VideoToolbox compatibility. Fixes #98 2018-10-12 17:59:53 -07:00
Cameron Gutman 44f415f94d Sign all deployed binaries in one signtool invocation 2018-10-11 18:45:29 -07:00
244 changed files with 30030 additions and 1267 deletions

No files matched your search

+10 -2
View File
@@ -13,6 +13,9 @@ Any special steps that are required for the bug to appear.
**Screenshots** **Screenshots**
If applicable, add screenshots to help explain your problem. If the issue is related to video glitching or poor quality, please include screenshots. If applicable, add screenshots to help explain your problem. If the issue is related to video glitching or poor quality, please include screenshots.
**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.
**Moonlight settings (please complete the following information)** **Moonlight settings (please complete the following information)**
- Have any settings been adjusted from defaults? - Have any settings been adjusted from defaults?
- If so, which settings have been changed? - If so, which settings have been changed?
@@ -22,10 +25,15 @@ If applicable, add screenshots to help explain your problem. If the issue is rel
- OS: [e.g. Windows 10 1803] - OS: [e.g. Windows 10 1803]
- Moonlight Version: [e.g. v0.2.0] - Moonlight Version: [e.g. v0.2.0]
**Logs (please attach)** **Server PC details (please complete the following information)**
- OS: [e.g. Windows 10 1803]
- 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 Windows, `Moonlight-###.log` files can be found in `%TEMP%`. Simply type that into the File Explorer path field to navigate there.
- On macOS, `Moonlight-###.log` files can be found in `/tmp`. In Finder, press Cmd+Shift+G, then type `/tmp` to navigate there. - On macOS, `Moonlight-###.log` files can be found in `/tmp`. In Finder, press Cmd+Shift+G, then type `/tmp` to navigate there.
- On Linux, logs print to the terminal when running via `flatpak run com.moonlight_stream.Moonlight` - On Linux, logs print to the terminal when running via `flatpak run com.moonlight_stream.Moonlight`
**Additional context** **Additional context**
Add any other context about the problem here. Anything else you think may be relevant to the issue
+2
View File
@@ -3,3 +3,5 @@
**/.vs/ **/.vs/
build/ build/
config.tests/*/.qmake.stash
config.tests/*/Makefile
+3
View File
@@ -10,3 +10,6 @@
[submodule "soundio/libsoundio"] [submodule "soundio/libsoundio"]
path = soundio/libsoundio path = soundio/libsoundio
url = https://github.com/andrewrk/libsoundio.git url = https://github.com/andrewrk/libsoundio.git
[submodule "h264bitstream/h264bitstream"]
path = h264bitstream/h264bitstream
url = https://github.com/aizvorski/h264bitstream.git
+8 -5
View File
@@ -3,6 +3,9 @@ language: cpp
git: git:
depth: 1 depth: 1
env:
- NJOBS=2
matrix: matrix:
include: include:
- os: osx - os: osx
@@ -14,13 +17,12 @@ matrix:
addons: addons:
apt: apt:
sources: sources:
- sourceline: 'ppa:beineri/opt-qt596-xenial' - sourceline: 'ppa:beineri/opt-qt597-xenial'
- sourceline: 'ppa:jonathonf/ffmpeg-4' - sourceline: 'ppa:jonathonf/ffmpeg-4'
packages: packages:
- qt59base - qt59base
- qt59quickcontrols2 - qt59quickcontrols2
- qt59svg - qt59svg
- qt59multimedia
- libesd0-dev - libesd0-dev
- libgl-dev - libgl-dev
- libegl1-mesa-dev - libegl1-mesa-dev
@@ -35,7 +37,8 @@ matrix:
install: install:
- '[ "$TRAVIS_OS_NAME" != osx ] || brew install qt5' - '[ "$TRAVIS_OS_NAME" != osx ] || brew install qt5'
- '[ "$TRAVIS_OS_NAME" != osx ] || brew link --force qt5' - '[ "$TRAVIS_OS_NAME" != osx ] || brew link --force qt5'
- if [[ "$TRAVIS_OS_NAME" = "linux" ]]; then export SDL2_VER=2.0.8 && wget https://www.libsdl.org/release/SDL2-$SDL2_VER.tar.gz && tar -xvf SDL2-$SDL2_VER.tar.gz && cd SDL2-$SDL2_VER && ./configure && make && sudo make install && cd ..; fi - if [[ "$TRAVIS_OS_NAME" = "linux" ]]; then export SDL2_VER=2.0.9 && wget https://www.libsdl.org/release/SDL2-$SDL2_VER.tar.gz && tar -xvf SDL2-$SDL2_VER.tar.gz && cd SDL2-$SDL2_VER && ./configure && make -j$NJOBS && sudo make install && cd ..; fi
- if [[ "$TRAVIS_OS_NAME" = "linux" ]]; then export SDL2_TTF_VER=2.0.15 && wget https://www.libsdl.org/projects/SDL_ttf/release/SDL2_ttf-$SDL2_TTF_VER.tar.gz && tar -xvf SDL2_ttf-$SDL2_TTF_VER.tar.gz && cd SDL2_ttf-$SDL2_TTF_VER && ./configure && make -j$NJOBS && sudo make install && cd ..; fi
before_script: before_script:
- '[ "$TRAVIS_OS_NAME" != "linux" ] || source /opt/qt59/bin/qt59-env.sh' - '[ "$TRAVIS_OS_NAME" != "linux" ] || source /opt/qt59/bin/qt59-env.sh'
@@ -46,5 +49,5 @@ before_script:
script: script:
- qmake moonlight-qt.pro -spec $QMAKESPEC - qmake moonlight-qt.pro -spec $QMAKESPEC
- make debug - make -j$NJOBS debug
- make release - make -j$NJOBS release
+23
View File
@@ -0,0 +1,23 @@
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
INCLUDEPATH += $$PWD/../libs/windows/include
contains(QT_ARCH, i386) {
LIBS += -L$$PWD/../libs/windows/lib/x86
}
contains(QT_ARCH, x86_64) {
LIBS += -L$$PWD/../libs/windows/lib/x64
}
LIBS += -lNktHookLib
DEFINES += ANTIHOOKING_LIBRARY
SOURCES += antihookingprotection.cpp
HEADERS += antihookingprotection.h
+161
View File
@@ -0,0 +1,161 @@
#include "antihookingprotection.h"
#include <NktHookLib.h>
typedef HMODULE (WINAPI *LoadLibraryAFunc)(LPCSTR lpLibFileName);
typedef HMODULE (WINAPI *LoadLibraryWFunc)(LPCWSTR lpLibFileName);
typedef HMODULE (WINAPI *LoadLibraryExAFunc)(LPCSTR lpLibFileName, HANDLE hFile, DWORD dwFlags);
typedef HMODULE (WINAPI *LoadLibraryExWFunc)(LPCWSTR lpLibFileName, HANDLE hFile, DWORD dwFlags);
class AntiHookingProtection
{
public:
static void enable()
{
#ifdef QT_DEBUG
s_HookManager.SetEnableDebugOutput(true);
#endif
HINSTANCE kernel32Handle = NktHookLibHelpers::GetModuleBaseAddress(L"kernel32.dll");
SIZE_T hookId;
s_HookManager.Hook(&hookId, (LPVOID*)&s_RealLoadLibraryA,
NktHookLibHelpers::GetProcedureAddress(kernel32Handle, "LoadLibraryA"),
(LPVOID)AntiHookingProtection::LoadLibraryAHook);
s_HookManager.Hook(&hookId, (LPVOID*)&s_RealLoadLibraryW,
NktHookLibHelpers::GetProcedureAddress(kernel32Handle, "LoadLibraryW"),
(LPVOID)AntiHookingProtection::LoadLibraryWHook);
s_HookManager.Hook(&hookId, (LPVOID*)&s_RealLoadLibraryExA,
NktHookLibHelpers::GetProcedureAddress(kernel32Handle, "LoadLibraryExA"),
(LPVOID)AntiHookingProtection::LoadLibraryExAHook);
s_HookManager.Hook(&hookId, (LPVOID*)&s_RealLoadLibraryExW,
NktHookLibHelpers::GetProcedureAddress(kernel32Handle, "LoadLibraryExW"),
(LPVOID)AntiHookingProtection::LoadLibraryExWHook);
}
private:
static bool isImageBlacklistedW(LPCWSTR lpLibFileName)
{
LPCWSTR dllName;
// If the library has a path prefixed, remove it
dllName = wcsrchr(lpLibFileName, '\\');
if (!dllName) {
// No prefix, so use the full name
dllName = lpLibFileName;
}
else {
// Advance past the backslash
dllName++;
}
// FIXME: We don't currently handle LoadLibrary calls where the
// library name does not include a file extension and the loader
// automatically assumes .dll.
for (int i = 0; i < ARRAYSIZE(k_BlacklistedDlls); i++) {
if (_wcsicmp(dllName, k_BlacklistedDlls[i]) == 0) {
return true;
}
}
return false;
}
static bool isImageBlacklistedA(LPCSTR lpLibFileName)
{
int uniChars = MultiByteToWideChar(CP_THREAD_ACP, 0, lpLibFileName, -1, nullptr, 0);
if (uniChars > 0) {
PWCHAR wideBuffer = new WCHAR[uniChars];
uniChars = MultiByteToWideChar(CP_THREAD_ACP, 0,
lpLibFileName, -1,
wideBuffer, uniChars * sizeof(WCHAR));
if (uniChars > 0) {
bool ret = isImageBlacklistedW(wideBuffer);
delete[] wideBuffer;
return ret;
}
else {
delete[] wideBuffer;
}
}
// Error path
return false;
}
static HMODULE WINAPI LoadLibraryAHook(LPCSTR lpLibFileName)
{
if (lpLibFileName && isImageBlacklistedA(lpLibFileName)) {
SetLastError(ERROR_ACCESS_DISABLED_BY_POLICY);
return nullptr;
}
return s_RealLoadLibraryA(lpLibFileName);
}
static HMODULE WINAPI LoadLibraryWHook(LPCWSTR lpLibFileName)
{
if (lpLibFileName && isImageBlacklistedW(lpLibFileName)) {
SetLastError(ERROR_ACCESS_DISABLED_BY_POLICY);
return nullptr;
}
return s_RealLoadLibraryW(lpLibFileName);
}
static HMODULE WINAPI LoadLibraryExAHook(LPCSTR lpLibFileName, HANDLE hFile, DWORD dwFlags)
{
if (lpLibFileName && isImageBlacklistedA(lpLibFileName)) {
SetLastError(ERROR_ACCESS_DISABLED_BY_POLICY);
return nullptr;
}
return s_RealLoadLibraryExA(lpLibFileName, hFile, dwFlags);
}
static HMODULE WINAPI LoadLibraryExWHook(LPCWSTR lpLibFileName, HANDLE hFile, DWORD dwFlags)
{
if (lpLibFileName && isImageBlacklistedW(lpLibFileName)) {
SetLastError(ERROR_ACCESS_DISABLED_BY_POLICY);
return nullptr;
}
return s_RealLoadLibraryExW(lpLibFileName, hFile, dwFlags);
}
static CNktHookLib s_HookManager;
static LoadLibraryAFunc s_RealLoadLibraryA;
static LoadLibraryWFunc s_RealLoadLibraryW;
static LoadLibraryExAFunc s_RealLoadLibraryExA;
static LoadLibraryExWFunc s_RealLoadLibraryExW;
static constexpr LPCWSTR k_BlacklistedDlls[] = {
// 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"
};
};
CNktHookLib AntiHookingProtection::s_HookManager;
LoadLibraryAFunc AntiHookingProtection::s_RealLoadLibraryA;
LoadLibraryWFunc AntiHookingProtection::s_RealLoadLibraryW;
LoadLibraryExAFunc AntiHookingProtection::s_RealLoadLibraryExA;
LoadLibraryExWFunc AntiHookingProtection::s_RealLoadLibraryExW;
AH_EXPORT void AntiHookingDummyImport() {}
extern "C"
BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID)
{
switch (fdwReason)
{
case DLL_PROCESS_ATTACH:
AntiHookingProtection::enable();
DisableThreadLibraryCalls(hinstDLL);
break;
}
return TRUE;
};
+10
View File
@@ -0,0 +1,10 @@
#pragma once
#ifdef ANTIHOOKING_LIBRARY
#define AH_EXPORT extern "C" __declspec(dllexport)
#else
#define AH_EXPORT extern "C" __declspec(dllimport)
#endif
AH_EXPORT void AntiHookingDummyImport();
+17 -11
View File
@@ -1,11 +1,9 @@
# Moonlight PC # Moonlight PC
[Moonlight PC](http://moonlight-stream.com) is an open source implementation of NVIDIA's GameStream, as used by the NVIDIA Shield, but built to run on Windows, Mac, and Linux. This client is the successor to [Moonlight Chrome](https://github.com/moonlight-stream/moonlight-chrome) for streaming on PC. [Moonlight PC](https://moonlight-stream.org) is an open source implementation of NVIDIA's GameStream, as used by the NVIDIA Shield, but built to run on Windows, Mac, and Linux. This client is the successor to [Moonlight Chrome](https://github.com/moonlight-stream/moonlight-chrome) for streaming on PC.
Moonlight also has mobile versions for [Android](https://github.com/moonlight-stream/moonlight-android) and [iOS](https://github.com/moonlight-stream/moonlight-ios). Moonlight also has mobile versions for [Android](https://github.com/moonlight-stream/moonlight-android) and [iOS](https://github.com/moonlight-stream/moonlight-ios).
See the [releases page](https://github.com/moonlight-stream/moonlight-qt/releases) for download links for Windows and Mac. For Linux, we're on [Flathub](https://flathub.org/apps/details/com.moonlight_stream.Moonlight).
You can follow development on our [Discord server](https://discord.gg/6ERtzFY). 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) [![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)
@@ -20,39 +18,47 @@ You can follow development on our [Discord server](https://discord.gg/6ERtzFY).
- Keyboard and mouse support - Keyboard and mouse support
- Gamepad support with SDL gamepad mappings - Gamepad support with SDL gamepad mappings
## Downloads
- [Windows and macOS](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)
## Building ## Building
### General Build Requirements ### General Build Requirements
* Qt 5.9 SDK or later * Qt 5.9 SDK or later
### Windows-specific Requirements ### Windows-specific Requirements
* Windows 7 or later * Windows 7 or later
* Visual Studio 2015 or later * [Visual Studio](https://visualstudio.microsoft.com/downloads/) 2015 or later (Community edition is fine)
* Select MSVC Desktop toolchain during Qt installation * Select MSVC Desktop toolchain during Qt installation
* 7-Zip (only if building installers for non-development PCs) * [DirectX SDK](https://www.microsoft.com/en-us/download/details.aspx?id=6812)
* WiX Toolset v3.11 or later (only if building installers for non-development PCs) * [7-Zip](https://www.7-zip.org/) (only if building installers for non-development PCs)
* [WiX Toolset](http://wixtoolset.org/releases/) v3.11 or later (only if building installers for non-development PCs)
### Mac-specific Requirements ### Mac-specific Requirements
* macOS El Capitan (10.11) or later * macOS Sierra (10.12) or later
* Xcode with High Sierra (10.13) SDK (Mojave SDK not yet supported until Qt 5.12 later this year) * Xcode 10
* [create-dmg](https://github.com/sindresorhus/create-dmg) (only if building DMGs for use on non-development Macs) * [create-dmg](https://github.com/sindresorhus/create-dmg) (only if building DMGs for use on non-development Macs)
### Linux-specific Requirements ### Linux-specific Requirements
* GCC or Clang * GCC or Clang
* Install your distro equivalents of: `openssl-devel qt5-devel SDL2-devel ffmpeg-devel qt5-qtquickcontrols2-devel libva-devel libvdpau-devel opus-devel` * Install your distro equivalents of: `openssl-devel qt5-devel SDL2-devel ffmpeg-devel qt5-qtquickcontrols2-devel libva-devel libvdpau-devel opus-devel pulseaudio-libs-devel alsa-lib-devel SDL2_ttf-devel`
* FFmpeg 4.0 is required to build. If your distro doesn't package FFmpeg 4.0, you can build and install it from source on http://ffmpeg.org/ * FFmpeg 4.0 is required to build. If your distro doesn't package FFmpeg 4.0, you can build and install it from source on http://ffmpeg.org/
### Build Setup Steps ### Build Setup Steps
1. Install the latest Qt SDK (and optionally, the Qt Creator IDE) from https://www.qt.io/download 1. Install the latest Qt SDK (and optionally, the Qt Creator IDE) from https://www.qt.io/download
* You may also use Homebrew on macOS or your Linux distro's package manager for the Qt SDK as long as the packages are Qt 5.9 or later. * You can install Qt via Homebrew on macOS, but you will need to use `brew install qt --with-debug` to be able to create debug builds of Moonlight.
* You may also use your Linux distro's package manager for the Qt SDK as long as the packages are Qt 5.9 or later.
2. Run `git submodule update --init --recursive` from within `moonlight-qt/` 2. Run `git submodule update --init --recursive` from within `moonlight-qt/`
3. Open the project in Qt Creator or build from qmake on the command line. 3. Open the project in Qt Creator or build from qmake on the command line.
* To build a binary for use on non-development machines, use the scripts in the `scripts` folder. * To build a binary for use on non-development machines, use the scripts in the `scripts` folder.
* For Windows builds, use `scripts\generate-installers.bat`. Execute this script from the root of the repository within a Qt command prompt. Ensure WiX and 7-Zip binary directories are in your `%PATH%`. * For Windows builds, use `scripts\generate-installers.bat`. Execute this script from the root of the repository within a Qt command prompt. Ensure WiX and 7-Zip binary directories are in your `%PATH%`.
* For macOS builds, use `scripts/generate-dmg.sh`. Execute this script from the root of the repository and ensure Qt's `bin` folder is in your `$PATH`. * For macOS builds, use `scripts/generate-dmg.sh`. Execute this script from the root of the repository and ensure Qt's `bin` folder is in your `$PATH`.
* To build from the command line for development use, run `qmake moonlight-qt.pro` then `make debug` or `make release`
## Contribute ## Contribute
1. Fork us 1. Fork us
2. Write code 2. Write code
3. Send Pull Requests 3. Send Pull Requests
Check out our [website](http://moonlight-stream.com) for project links and information. Check out our [website](https://moonlight-stream.org) for project links and information.
+2 -2
View File
@@ -23,9 +23,9 @@
<key>NSSupportsAutomaticGraphicsSwitching</key> <key>NSSupportsAutomaticGraphicsSwitching</key>
<true/> <true/>
<key>CFBundleVersion</key> <key>CFBundleVersion</key>
<string>0.6.0</string> <string>VERSION</string>
<key>CFBundleShortVersionString</key> <key>CFBundleShortVersionString</key>
<string>0.6.0</string> <string>VERSION</string>
<key>CFBundleDisplayName</key> <key>CFBundleDisplayName</key>
<string>Moonlight</string> <string>Moonlight</string>
</dict> </dict>
BIN
View File
Binary file not shown.
+74 -16
View File
@@ -18,7 +18,9 @@ CONFIG += force_debug_info
# Since this binds the app against the Qt runtime version, we will only # Since this binds the app against the Qt runtime version, we will only
# do this for Windows and Mac, since they ship with the Qt runtime. # do this for Windows and Mac, since they ship with the Qt runtime.
win32|macx { win32|macx {
CONFIG += qtquickcompiler CONFIG(release, debug|release) {
CONFIG += qtquickcompiler
}
} }
TEMPLATE = app TEMPLATE = app
@@ -35,25 +37,31 @@ DEFINES += QT_DEPRECATED_WARNINGS
DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000 # disables all the APIs deprecated before Qt 6.0.0 DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000 # disables all the APIs deprecated before Qt 6.0.0
win32 { win32 {
INCLUDEPATH += $$PWD/../libs/windows/include INCLUDEPATH += \
$$PWD/../libs/windows/include \
$$(DXSDK_DIR)/Include
contains(QT_ARCH, i386) { contains(QT_ARCH, i386) {
LIBS += -L$$PWD/../libs/windows/lib/x86 LIBS += -L$$PWD/../libs/windows/lib/x86
LIBS += -L$$(DXSDK_DIR)/Lib/x86
} }
contains(QT_ARCH, x86_64) { contains(QT_ARCH, x86_64) {
LIBS += -L$$PWD/../libs/windows/lib/x64 LIBS += -L$$PWD/../libs/windows/lib/x64
LIBS += -L$$(DXSDK_DIR)/Lib/x64
} }
LIBS += ws2_32.lib winmm.lib dxva2.lib ole32.lib gdi32.lib user32.lib d3d9.lib dwmapi.lib dbghelp.lib LIBS += ws2_32.lib winmm.lib dxva2.lib ole32.lib gdi32.lib user32.lib d3d9.lib dwmapi.lib dbghelp.lib
} }
macx { macx {
INCLUDEPATH += $$PWD/../libs/mac/include $$PWD/../libs/mac/Frameworks/SDL2.framework/Versions/A/Headers INCLUDEPATH += $$PWD/../libs/mac/include
INCLUDEPATH += $$PWD/../libs/mac/Frameworks/SDL2.framework/Versions/A/Headers
INCLUDEPATH += $$PWD/../libs/mac/Frameworks/SDL2_ttf.framework/Versions/A/Headers
LIBS += -L$$PWD/../libs/mac/lib -F$$PWD/../libs/mac/Frameworks LIBS += -L$$PWD/../libs/mac/lib -F$$PWD/../libs/mac/Frameworks
} }
unix:!macx { unix:!macx {
CONFIG += link_pkgconfig CONFIG += link_pkgconfig soundio
PKGCONFIG += openssl sdl2 opus PKGCONFIG += openssl sdl2 SDL2_ttf opus
# For libsoundio # For libsoundio
packagesExist(libpulse) { packagesExist(libpulse) {
@@ -83,11 +91,11 @@ unix:!macx {
} }
} }
win32 { win32 {
LIBS += -llibssl -llibcrypto -lSDL2 -lavcodec -lavutil -lopus LIBS += -llibssl -llibcrypto -lSDL2 -lSDL2_ttf -lavcodec -lavutil -lopus -ld3dx9
CONFIG += ffmpeg soundio CONFIG += ffmpeg soundio
} }
macx { macx {
LIBS += -lssl -lcrypto -lavcodec.58 -lavutil.56 -lopus -framework SDL2 LIBS += -lssl -lcrypto -lavcodec.58 -lavutil.56 -lopus -framework SDL2 -framework SDL2_ttf
LIBS += -lobjc -framework VideoToolbox -framework AVFoundation -framework CoreVideo -framework CoreGraphics -framework CoreMedia -framework AppKit LIBS += -lobjc -framework VideoToolbox -framework AVFoundation -framework CoreVideo -framework CoreGraphics -framework CoreMedia -framework AppKit
# For libsoundio # For libsoundio
@@ -98,6 +106,7 @@ macx {
SOURCES += \ SOURCES += \
main.cpp \ main.cpp \
backend/computerseeker.cpp \
backend/identitymanager.cpp \ backend/identitymanager.cpp \
backend/nvcomputer.cpp \ backend/nvcomputer.cpp \
backend/nvhttp.cpp \ backend/nvhttp.cpp \
@@ -105,6 +114,7 @@ SOURCES += \
backend/computermanager.cpp \ backend/computermanager.cpp \
backend/boxartmanager.cpp \ backend/boxartmanager.cpp \
cli/commandlineparser.cpp \ cli/commandlineparser.cpp \
cli/quitstream.cpp \
cli/startstream.cpp \ cli/startstream.cpp \
settings/streamingpreferences.cpp \ settings/streamingpreferences.cpp \
streaming/input.cpp \ streaming/input.cpp \
@@ -117,10 +127,12 @@ SOURCES += \
backend/autoupdatechecker.cpp \ backend/autoupdatechecker.cpp \
path.cpp \ path.cpp \
settings/mappingmanager.cpp \ settings/mappingmanager.cpp \
gui/sdlgamepadkeynavigation.cpp gui/sdlgamepadkeynavigation.cpp \
streaming/video/overlaymanager.cpp
HEADERS += \ HEADERS += \
utils.h \ utils.h \
backend/computerseeker.h \
backend/identitymanager.h \ backend/identitymanager.h \
backend/nvcomputer.h \ backend/nvcomputer.h \
backend/nvhttp.h \ backend/nvhttp.h \
@@ -128,6 +140,7 @@ HEADERS += \
backend/computermanager.h \ backend/computermanager.h \
backend/boxartmanager.h \ backend/boxartmanager.h \
cli/commandlineparser.h \ cli/commandlineparser.h \
cli/quitstream.h \
cli/startstream.h \ cli/startstream.h \
settings/streamingpreferences.h \ settings/streamingpreferences.h \
streaming/input.h \ streaming/input.h \
@@ -141,7 +154,8 @@ HEADERS += \
backend/autoupdatechecker.h \ backend/autoupdatechecker.h \
path.h \ path.h \
settings/mappingmanager.h \ settings/mappingmanager.h \
gui/sdlgamepadkeynavigation.h gui/sdlgamepadkeynavigation.h \
streaming/video/overlaymanager.h
# Platform-specific renderers and decoders # Platform-specific renderers and decoders
ffmpeg { ffmpeg {
@@ -154,10 +168,10 @@ ffmpeg {
streaming/video/ffmpeg-renderers/pacer/pacer.cpp \ streaming/video/ffmpeg-renderers/pacer/pacer.cpp \
streaming/video/ffmpeg-renderers/pacer/nullthreadedvsyncsource.cpp streaming/video/ffmpeg-renderers/pacer/nullthreadedvsyncsource.cpp
HEADERS += \ HEADERS += \
streaming/video/ffmpeg.h \ streaming/video/ffmpeg.h \
streaming/video/ffmpeg-renderers/renderer.h \ streaming/video/ffmpeg-renderers/renderer.h \
streaming/video/ffmpeg-renderers/sdlvid.h \
streaming/video/ffmpeg-renderers/pacer/pacer.h \ streaming/video/ffmpeg-renderers/pacer/pacer.h \
streaming/video/ffmpeg-renderers/pacer/nullthreadedvsyncsource.h streaming/video/ffmpeg-renderers/pacer/nullthreadedvsyncsource.h
} }
@@ -258,6 +272,31 @@ else:unix: LIBS += -L$$OUT_PWD/../soundio/ -lsoundio
INCLUDEPATH += $$PWD/../soundio/libsoundio INCLUDEPATH += $$PWD/../soundio/libsoundio
DEPENDPATH += $$PWD/../soundio/libsoundio DEPENDPATH += $$PWD/../soundio/libsoundio
win32:CONFIG(release, debug|release): LIBS += -L$$OUT_PWD/../h264bitstream/release/ -lh264bitstream
else:win32:CONFIG(debug, debug|release): LIBS += -L$$OUT_PWD/../h264bitstream/debug/ -lh264bitstream
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
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)
}
unix:!macx: { unix:!macx: {
isEmpty(PREFIX) { isEmpty(PREFIX) {
PREFIX = /usr/local PREFIX = /usr/local
@@ -280,9 +319,15 @@ unix:!macx: {
appstream.files = deploy/linux/com.moonlight_stream.Moonlight.appdata.xml appstream.files = deploy/linux/com.moonlight_stream.Moonlight.appdata.xml
appstream.path = $$PREFIX/$$DATADIR/metainfo/ appstream.path = $$PREFIX/$$DATADIR/metainfo/
appdata.files = SDL_GameControllerDB/gamecontrollerdb.txt appdata.files = SDL_GameControllerDB/gamecontrollerdb.txt ModeSeven.ttf
appdata.path = "$$PREFIX/$$DATADIR/Moonlight Game Streaming Project/Moonlight/" 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 appdata desktop icons appstream
} }
win32 { win32 {
@@ -291,16 +336,29 @@ win32 {
QMAKE_TARGET_DESCRIPTION = Moonlight Game Streaming Client QMAKE_TARGET_DESCRIPTION = Moonlight Game Streaming Client
QMAKE_TARGET_PRODUCT = Moonlight QMAKE_TARGET_PRODUCT = Moonlight
# Copy the required data files into the application directory
copyToDestDir(SDL_GameControllerDB/gamecontrollerdb.txt ModeSeven.ttf)
CONFIG -= embed_manifest_exe CONFIG -= embed_manifest_exe
QMAKE_LFLAGS += /MANIFEST:embed /MANIFESTINPUT:$${PWD}/Moonlight.exe.manifest QMAKE_LFLAGS += /MANIFEST:embed /MANIFESTINPUT:$${PWD}/Moonlight.exe.manifest
} }
macx { macx {
QMAKE_INFO_PLIST = $$PWD/Info.plist # Create Info.plist in object dir with the correct version string
system(cp $$PWD/Info.plist $$OUT_PWD/Info.plist)
system(sed -i -e 's/VERSION/$$cat(version.txt)/g' $$OUT_PWD/Info.plist)
APP_BUNDLE_RESOURCES.files = moonlight.icns SDL_GameControllerDB/gamecontrollerdb.txt QMAKE_INFO_PLIST = $$OUT_PWD/Info.plist
APP_BUNDLE_RESOURCES.files = moonlight.icns SDL_GameControllerDB/gamecontrollerdb.txt ModeSeven.ttf
APP_BUNDLE_RESOURCES.path = Contents/Resources APP_BUNDLE_RESOURCES.path = Contents/Resources
QMAKE_BUNDLE_DATA += APP_BUNDLE_RESOURCES
APP_BUNDLE_FRAMEWORKS.files = $$files(../libs/mac/Frameworks/*.framework, true)
APP_BUNDLE_FRAMEWORKS.path = Contents/Frameworks
QMAKE_BUNDLE_DATA += APP_BUNDLE_RESOURCES APP_BUNDLE_FRAMEWORKS
QMAKE_RPATHDIR += @executable_path/../Frameworks
} }
VERSION = 0.6.0 VERSION = "$$cat(version.txt)"
DEFINES += VERSION_STR=\\\"0.6.0\\\" DEFINES += VERSION_STR=\\\"$$cat(version.txt)\\\"
+6 -2
View File
@@ -12,6 +12,9 @@ AutoUpdateChecker::AutoUpdateChecker(QObject *parent) :
// Never communicate over HTTP // Never communicate over HTTP
m_Nam.setStrictTransportSecurityEnabled(true); m_Nam.setStrictTransportSecurityEnabled(true);
// Allow HTTP redirects
m_Nam.setRedirectPolicy(QNetworkRequest::NoLessSafeRedirectPolicy);
connect(&m_Nam, SIGNAL(finished(QNetworkReply*)), connect(&m_Nam, SIGNAL(finished(QNetworkReply*)),
this, SLOT(handleUpdateCheckRequestFinished(QNetworkReply*))); this, SLOT(handleUpdateCheckRequestFinished(QNetworkReply*)));
@@ -27,7 +30,7 @@ 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) // Only run update checker on platforms without auto-update
// We'll get a callback when this is finished // We'll get a callback when this is finished
QUrl url("https://moonlight-stream.com/updates/qt.json"); QUrl url("https://moonlight-stream.org/updates/qt.json");
m_Nam.get(QNetworkRequest(url)); m_Nam.get(QNetworkRequest(url));
#endif #endif
} }
@@ -113,7 +116,8 @@ void AutoUpdateChecker::handleUpdateCheckRequestFinished(QNetworkReply* reply)
if (currentVer < latestVer) { if (currentVer < latestVer) {
qDebug() << "Update available"; qDebug() << "Update available";
emit onUpdateAvailable(updateObj["browser_url"].toString()); emit onUpdateAvailable(updateObj["version"].toString(),
updateObj["browser_url"].toString());
return; return;
} }
else if (currentVer > latestVer) { else if (currentVer > latestVer) {
+1 -1
View File
@@ -12,7 +12,7 @@ public:
Q_INVOKABLE void start(); Q_INVOKABLE void start();
signals: signals:
void onUpdateAvailable(QString url); void onUpdateAvailable(QString newVersion, QString url);
private slots: private slots:
void handleUpdateCheckRequestFinished(QNetworkReply* reply); void handleUpdateCheckRequestFinished(QNetworkReply* reply);
+1 -1
View File
@@ -97,7 +97,7 @@ void BoxArtManager::handleBoxArtLoadComplete(NvComputer* computer, NvApp app, QU
QUrl BoxArtManager::loadBoxArtFromNetwork(NvComputer* computer, int appId) QUrl BoxArtManager::loadBoxArtFromNetwork(NvComputer* computer, int appId)
{ {
NvHTTP http(computer->activeAddress); NvHTTP http(computer->activeAddress, computer->serverCert);
QString cachePath = getFilePathForBoxArt(computer, appId); QString cachePath = getFilePathForBoxArt(computer, appId);
QImage image; QImage image;
+127 -115
View File
@@ -2,6 +2,9 @@
#include "nvhttp.h" #include "nvhttp.h"
#include "settings/streamingpreferences.h" #include "settings/streamingpreferences.h"
#include <Limelight.h>
#include <QtEndian>
#include <QThread> #include <QThread>
#include <QThreadPool> #include <QThreadPool>
@@ -24,16 +27,16 @@ public:
private: private:
bool tryPollComputer(QString address, bool& changed) bool tryPollComputer(QString address, bool& changed)
{ {
NvHTTP http(address); NvHTTP http(address, m_Computer->serverCert);
QString serverInfo; QString serverInfo;
try { try {
serverInfo = http.getServerInfo(NvHTTP::NvLogLevel::NONE); serverInfo = http.getServerInfo(NvHTTP::NvLogLevel::NVLL_NONE);
} catch (...) { } catch (...) {
return false; return false;
} }
NvComputer newState(address, serverInfo); NvComputer newState(address, serverInfo, QSslCertificate());
// Ensure the machine that responded is the one we intended to contact // Ensure the machine that responded is the one we intended to contact
if (m_Computer->uuid != newState.uuid) { if (m_Computer->uuid != newState.uuid) {
@@ -49,7 +52,7 @@ private:
{ {
Q_ASSERT(m_Computer->activeAddress != nullptr); Q_ASSERT(m_Computer->activeAddress != nullptr);
NvHTTP http(m_Computer->activeAddress); NvHTTP http(m_Computer->activeAddress, m_Computer->serverCert);
QVector<NvApp> appList; QVector<NvApp> appList;
@@ -171,38 +174,19 @@ ComputerManager::~ComputerManager()
delete m_MdnsBrowser; delete m_MdnsBrowser;
m_MdnsBrowser = nullptr; m_MdnsBrowser = nullptr;
{ // Interrupt polling
QMutableMapIterator<QString, QThread*> i(m_PollThreads); for (ComputerPollingEntry* entry : m_PollEntries) {
entry->interrupt();
// Interrupt all polling threads
while (i.hasNext()) {
i.next();
i.value()->requestInterruption();
}
// Wait for all threads to terminate before destroying
// the NvComputer objects.
i.toFront();
while (i.hasNext()) {
i.next();
QThread* thread = i.value();
thread->wait();
delete thread;
i.remove();
}
} }
{ // Delete all polling entries (and associated threads)
QMutableMapIterator<QString, NvComputer*> i(m_KnownHosts); for (ComputerPollingEntry* entry : m_PollEntries) {
delete entry;
}
// Destroy all NvComputer objects // Destroy all NvComputer objects now that polling is halted
while (i.hasNext()) { for (NvComputer* computer : m_KnownHosts) {
i.next(); delete computer;
delete i.value();
i.remove();
}
} }
} }
@@ -215,7 +199,7 @@ void ComputerManager::saveHosts()
settings.beginWriteArray(SER_HOSTS); settings.beginWriteArray(SER_HOSTS);
for (int i = 0; i < m_KnownHosts.count(); i++) { for (int i = 0; i < m_KnownHosts.count(); i++) {
settings.setArrayIndex(i); settings.setArrayIndex(i);
m_KnownHosts[m_KnownHosts.keys()[i]]->serialize(settings); m_KnownHosts.value(m_KnownHosts.keys()[i])->serialize(settings);
} }
settings.endArray(); settings.endArray();
} }
@@ -262,16 +246,22 @@ void ComputerManager::startPollingComputer(NvComputer* computer)
return; return;
} }
if (m_PollThreads.contains(computer->uuid)) { ComputerPollingEntry* pollingEntry;
Q_ASSERT(m_PollThreads[computer->uuid]->isRunning());
return; if (!m_PollEntries.contains(computer->uuid)) {
pollingEntry = m_PollEntries[computer->uuid] = new ComputerPollingEntry();
}
else {
pollingEntry = m_PollEntries[computer->uuid];
} }
PcMonitorThread* thread = new PcMonitorThread(computer); if (!pollingEntry->isActive()) {
connect(thread, SIGNAL(computerStateChanged(NvComputer*)), PcMonitorThread* thread = new PcMonitorThread(computer);
this, SLOT(handleComputerStateChanged(NvComputer*))); connect(thread, SIGNAL(computerStateChanged(NvComputer*)),
m_PollThreads[computer->uuid] = thread; this, SLOT(handleComputerStateChanged(NvComputer*)));
thread->start(); pollingEntry->setActiveThread(thread);
thread->start();
}
} }
void ComputerManager::handleMdnsServiceResolved(MdnsPendingComputer* computer, void ComputerManager::handleMdnsServiceResolved(MdnsPendingComputer* computer,
@@ -314,34 +304,23 @@ public:
void run() void run()
{ {
QThread* pollingThread; ComputerPollingEntry* pollingEntry;
// Only do the minimum amount of work while holding the writer lock. // Only do the minimum amount of work while holding the writer lock.
// We must release it before calling saveHosts(). // We must release it before calling saveHosts().
{ {
QWriteLocker lock(&m_ComputerManager->m_Lock); QWriteLocker lock(&m_ComputerManager->m_Lock);
pollingThread = m_ComputerManager->m_PollThreads[m_Computer->uuid]; pollingEntry = m_ComputerManager->m_PollEntries.take(m_Computer->uuid);
m_ComputerManager->m_PollThreads.remove(m_Computer->uuid);
m_ComputerManager->m_KnownHosts.remove(m_Computer->uuid); m_ComputerManager->m_KnownHosts.remove(m_Computer->uuid);
} }
// Persist the new host list // Persist the new host list
m_ComputerManager->saveHosts(); m_ComputerManager->saveHosts();
// Delete the polling thread // Delete the polling entry first. This will stop all polling threads too.
if (pollingThread != nullptr) { delete pollingEntry;
pollingThread->requestInterruption();
// We must wait here because we're going to delete computer
// and we can't do that out from underneath the poller.
pollingThread->wait();
// The thread is safe to delete now
Q_ASSERT(pollingThread->isFinished());
delete pollingThread;
}
// Finally, delete the computer itself. This must be done // Finally, delete the computer itself. This must be done
// last because the polling thread might be using it. // last because the polling thread might be using it.
@@ -382,7 +361,7 @@ private:
NvPairingManager pairingManager(m_Computer->activeAddress); NvPairingManager pairingManager(m_Computer->activeAddress);
try { try {
NvPairingManager::PairState result = pairingManager.pair(m_Computer->appVersion, m_Pin); NvPairingManager::PairState result = pairingManager.pair(m_Computer->appVersion, m_Pin, m_Computer->serverCert);
switch (result) switch (result)
{ {
case NvPairingManager::PairState::PIN_WRONG: case NvPairingManager::PairState::PIN_WRONG:
@@ -435,7 +414,7 @@ signals:
private: private:
void run() void run()
{ {
NvHTTP http(m_Computer->activeAddress); NvHTTP http(m_Computer->activeAddress, m_Computer->serverCert);
try { try {
if (m_Computer->currentGameId != 0) { if (m_Computer->currentGameId != 0) {
@@ -496,20 +475,8 @@ void ComputerManager::stopPollingAsync()
m_MdnsBrowser = nullptr; m_MdnsBrowser = nullptr;
// Interrupt all threads, but don't wait for them to terminate // Interrupt all threads, but don't wait for them to terminate
QMutableMapIterator<QString, QThread*> i(m_PollThreads); for (ComputerPollingEntry* entry : m_PollEntries) {
while (i.hasNext()) { entry->interrupt();
i.next();
QThread* thread = i.value();
// Let this thread delete itself when it's finished
connect(thread, SIGNAL(finished()),
thread, SLOT(deleteLater()));
// The threads will delete themselves when they terminate,
// but we remove them from the polling threads map here.
i.value()->requestInterruption();
i.remove();
} }
} }
@@ -535,12 +502,8 @@ signals:
void computerStateChanged(NvComputer* computer); void computerStateChanged(NvComputer* computer);
private: private:
void run() QString fetchServerInfo(NvHTTP& http)
{ {
NvHTTP http(m_Address);
qInfo() << "Processing new PC at" << m_Address << "from" << (m_Mdns ? "mDNS" : "user");
QString serverInfo; QString serverInfo;
try { try {
// There's a race condition between GameStream servers reporting presence over // There's a race condition between GameStream servers reporting presence over
@@ -548,12 +511,12 @@ private:
// around this issue, we will issue the request again after a few seconds if // around this issue, we will issue the request again after a few seconds if
// we see a ServiceUnavailableError error. // we see a ServiceUnavailableError error.
try { try {
serverInfo = http.getServerInfo(NvHTTP::NvLogLevel::VERBOSE); serverInfo = http.getServerInfo(NvHTTP::NVLL_VERBOSE);
} catch (const QtNetworkReplyException& e) { } catch (const QtNetworkReplyException& e) {
if (e.getError() == QNetworkReply::ServiceUnavailableError) { if (e.getError() == QNetworkReply::ServiceUnavailableError) {
qWarning() << "Retrying request in 5 seconds after ServiceUnavailableError"; qWarning() << "Retrying request in 5 seconds after ServiceUnavailableError";
QThread::sleep(5); QThread::sleep(5);
serverInfo = http.getServerInfo(NvHTTP::NvLogLevel::VERBOSE); serverInfo = http.getServerInfo(NvHTTP::NVLL_VERBOSE);
qInfo() << "Retry successful"; qInfo() << "Retry successful";
} }
else { else {
@@ -561,63 +524,112 @@ private:
throw e; throw e;
} }
} }
return serverInfo;
} catch (...) { } catch (...) {
if (!m_Mdns) { if (!m_Mdns) {
emit computerAddCompleted(false); emit computerAddCompleted(false);
} }
return QString();
}
}
void run()
{
NvHTTP http(m_Address, QSslCertificate());
qInfo() << "Processing new PC at" << m_Address << "from" << (m_Mdns ? "mDNS" : "user");
// Perform initial serverinfo fetch over HTTP since we don't know which cert to use
QString serverInfo = fetchServerInfo(http);
if (serverInfo.isEmpty()) {
return; return;
} }
NvComputer* newComputer = new NvComputer(m_Address, serverInfo); // Create initial newComputer using HTTP serverinfo with no pinned cert
NvComputer* newComputer = new NvComputer(m_Address, serverInfo, QSslCertificate());
// Check if we have a record of this host UUID to pull the pinned cert
NvComputer* existingComputer;
{
QReadLocker lock(&m_ComputerManager->m_Lock);
existingComputer = m_ComputerManager->m_KnownHosts.value(newComputer->uuid);
if (existingComputer != nullptr) {
http.setServerCert(existingComputer->serverCert);
}
}
// Fetch serverinfo again over HTTPS with the pinned cert
if (existingComputer != nullptr) {
serverInfo = fetchServerInfo(http);
if (serverInfo.isEmpty()) {
return;
}
// Update the polled computer with the HTTPS information
NvComputer httpsComputer(m_Address, serverInfo, QSslCertificate());
newComputer->update(httpsComputer);
}
// Update addresses depending on the context // Update addresses depending on the context
if (m_Mdns) { if (m_Mdns) {
newComputer->localAddress = m_Address; 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();
}
else {
qWarning() << "STUN failed to get WAN address:" << err;
}
} }
else { else {
newComputer->manualAddress = m_Address; newComputer->manualAddress = m_Address;
} }
// Check if this PC already exists {
QWriteLocker lock(&m_ComputerManager->m_Lock); // Check if this PC already exists
NvComputer* existingComputer = m_ComputerManager->m_KnownHosts[newComputer->uuid]; QWriteLocker lock(&m_ComputerManager->m_Lock);
if (existingComputer != nullptr) { NvComputer* existingComputer = m_ComputerManager->m_KnownHosts.value(newComputer->uuid);
// Fold it into the existing PC if (existingComputer != nullptr) {
bool changed = existingComputer->update(*newComputer); // Fold it into the existing PC
delete newComputer; bool changed = existingComputer->update(*newComputer);
delete newComputer;
// Drop the lock before notifying // Drop the lock before notifying
lock.unlock(); lock.unlock();
// For non-mDNS clients, let them know it succeeded // For non-mDNS clients, let them know it succeeded
if (!m_Mdns) { if (!m_Mdns) {
emit computerAddCompleted(true); emit computerAddCompleted(true);
}
// Tell our client if something changed
if (changed) {
qInfo() << existingComputer->name << "is now at" << existingComputer->activeAddress;
emit computerStateChanged(existingComputer);
}
} }
else {
// Store this in our active sets
m_ComputerManager->m_KnownHosts[newComputer->uuid] = newComputer;
// Tell our client if something changed // Start polling if enabled (write lock required)
if (changed) { m_ComputerManager->startPollingComputer(newComputer);
qInfo() << existingComputer->name << "is now at" << existingComputer->activeAddress;
emit computerStateChanged(existingComputer); // Drop the lock before notifying
lock.unlock();
// For non-mDNS clients, let them know it succeeded
if (!m_Mdns) {
emit computerAddCompleted(true);
}
// Tell our client about this new PC
emit computerStateChanged(newComputer);
} }
} }
else {
// Store this in our active sets
m_ComputerManager->m_KnownHosts[newComputer->uuid] = newComputer;
// Start polling if enabled (write lock required)
m_ComputerManager->startPollingComputer(newComputer);
// Drop the lock before notifying
lock.unlock();
// For non-mDNS clients, let them know it succeeded
if (!m_Mdns) {
emit computerAddCompleted(true);
}
// Tell our client about this new PC
emit computerStateChanged(newComputer);
}
} }
ComputerManager* m_ComputerManager; ComputerManager* m_ComputerManager;
+74 -1
View File
@@ -51,6 +51,79 @@ private:
QMdnsEngine::Resolver m_Resolver; QMdnsEngine::Resolver m_Resolver;
}; };
class ComputerPollingEntry
{
public:
ComputerPollingEntry()
: m_ActiveThread(nullptr)
{
}
virtual ~ComputerPollingEntry()
{
interrupt();
// interrupt() should have taken care of this
Q_ASSERT(m_ActiveThread == nullptr);
for (QThread* thread : m_InactiveList) {
thread->wait();
delete thread;
}
}
bool isActive()
{
cleanInactiveList();
return m_ActiveThread != nullptr;
}
void setActiveThread(QThread* thread)
{
cleanInactiveList();
Q_ASSERT(!isActive());
m_ActiveThread = thread;
}
void interrupt()
{
cleanInactiveList();
if (m_ActiveThread != nullptr) {
// Interrupt the active thread
m_ActiveThread->requestInterruption();
// Place it on the inactive list awaiting death
m_InactiveList.append(m_ActiveThread);
m_ActiveThread = nullptr;
}
}
private:
void cleanInactiveList()
{
QMutableListIterator<QThread*> i(m_InactiveList);
// Reap any threads that have finished
while (i.hasNext()) {
i.next();
QThread* thread = i.value();
if (thread->isFinished()) {
delete thread;
i.remove();
}
}
}
QThread* m_ActiveThread;
QList<QThread*> m_InactiveList;
};
class ComputerManager : public QObject class ComputerManager : public QObject
{ {
Q_OBJECT Q_OBJECT
@@ -100,7 +173,7 @@ private:
int m_PollingRef; int m_PollingRef;
QReadWriteLock m_Lock; QReadWriteLock m_Lock;
QMap<QString, NvComputer*> m_KnownHosts; QMap<QString, NvComputer*> m_KnownHosts;
QMap<QString, QThread*> m_PollThreads; QMap<QString, ComputerPollingEntry*> m_PollEntries;
QMdnsEngine::Server m_MdnsServer; QMdnsEngine::Server m_MdnsServer;
QMdnsEngine::Browser* m_MdnsBrowser; QMdnsEngine::Browser* m_MdnsBrowser;
QMdnsEngine::Cache m_MdnsCache; QMdnsEngine::Cache m_MdnsCache;
+59
View File
@@ -0,0 +1,59 @@
#include "computerseeker.h"
#include "computermanager.h"
#include <QTimer>
ComputerSeeker::ComputerSeeker(ComputerManager *manager, QString computerName, QObject *parent)
: QObject(parent), m_ComputerManager(manager), m_ComputerName(computerName),
m_TimeoutTimer(new QTimer(this))
{
m_TimeoutTimer->setSingleShot(true);
connect(m_TimeoutTimer, &QTimer::timeout,
this, &ComputerSeeker::onTimeout);
connect(m_ComputerManager, &ComputerManager::computerStateChanged,
this, &ComputerSeeker::onComputerUpdated);
}
void ComputerSeeker::start(int timeout)
{
m_TimeoutTimer->start(timeout);
// Seek desired computer by both connecting to it directly (this may fail
// if m_ComputerName is UUID, or the name that doesn't resolve to an IP
// address) and by polling it using mDNS, hopefully one of these methods
// would find the host
m_ComputerManager->addNewHost(m_ComputerName, false);
m_ComputerManager->startPolling();
}
void ComputerSeeker::onComputerUpdated(NvComputer *computer)
{
if (!m_TimeoutTimer->isActive()) {
return;
}
if (matchComputer(computer) && isOnline(computer)) {
m_ComputerManager->stopPollingAsync();
m_TimeoutTimer->stop();
emit computerFound(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;
}
bool ComputerSeeker::isOnline(NvComputer *computer) const
{
return computer->state == NvComputer::CS_ONLINE;
}
void ComputerSeeker::onTimeout()
{
m_TimeoutTimer->stop();
m_ComputerManager->stopPollingAsync();
emit errorTimeout();
}
+33
View File
@@ -0,0 +1,33 @@
#pragma once
#include <QObject>
class ComputerManager;
class NvComputer;
class QTimer;
class ComputerSeeker : public QObject
{
Q_OBJECT
public:
explicit ComputerSeeker(ComputerManager *manager, QString computerName, QObject *parent = nullptr);
void start(int timeout);
signals:
void computerFound(NvComputer *computer);
void errorTimeout();
private slots:
void onComputerUpdated(NvComputer *computer);
void onTimeout();
private:
bool matchComputer(NvComputer *computer) const;
bool isOnline(NvComputer *computer) const;
private:
ComputerManager *m_ComputerManager;
QString m_ComputerName;
QTimer *m_TimeoutTimer;
};
+49 -11
View File
@@ -2,6 +2,7 @@
#include <QUdpSocket> #include <QUdpSocket>
#include <QHostInfo> #include <QHostInfo>
#include <QNetworkInterface>
#define SER_NAME "hostname" #define SER_NAME "hostname"
#define SER_UUID "uuid" #define SER_UUID "uuid"
@@ -10,6 +11,7 @@
#define SER_REMOTEADDR "remoteaddress" #define SER_REMOTEADDR "remoteaddress"
#define SER_MANUALADDR "manualaddress" #define SER_MANUALADDR "manualaddress"
#define SER_APPLIST "apps" #define SER_APPLIST "apps"
#define SER_SRVCERT "srvcert"
#define SER_APPNAME "name" #define SER_APPNAME "name"
#define SER_APPID "id" #define SER_APPID "id"
@@ -23,6 +25,7 @@ NvComputer::NvComputer(QSettings& settings)
this->localAddress = settings.value(SER_LOCALADDR).toString(); this->localAddress = settings.value(SER_LOCALADDR).toString();
this->remoteAddress = settings.value(SER_REMOTEADDR).toString(); this->remoteAddress = settings.value(SER_REMOTEADDR).toString();
this->manualAddress = settings.value(SER_MANUALADDR).toString(); this->manualAddress = settings.value(SER_MANUALADDR).toString();
this->serverCert = QSslCertificate(settings.value(SER_SRVCERT).toByteArray());
int appCount = settings.beginReadArray(SER_APPLIST); int appCount = settings.beginReadArray(SER_APPLIST);
for (int i = 0; i < appCount; i++) { for (int i = 0; i < appCount; i++) {
@@ -61,6 +64,7 @@ void NvComputer::serialize(QSettings& settings)
settings.setValue(SER_LOCALADDR, localAddress); settings.setValue(SER_LOCALADDR, localAddress);
settings.setValue(SER_REMOTEADDR, remoteAddress); settings.setValue(SER_REMOTEADDR, remoteAddress);
settings.setValue(SER_MANUALADDR, manualAddress); settings.setValue(SER_MANUALADDR, manualAddress);
settings.setValue(SER_SRVCERT, serverCert.toPem());
// Avoid deleting an existing applist if we couldn't get one // Avoid deleting an existing applist if we couldn't get one
if (!appList.isEmpty()) { if (!appList.isEmpty()) {
@@ -84,8 +88,10 @@ void NvComputer::sortAppList()
}); });
} }
NvComputer::NvComputer(QString address, QString serverInfo) NvComputer::NvComputer(QString address, QString serverInfo, QSslCertificate serverCert)
{ {
this->serverCert = serverCert;
this->name = NvHTTP::getXmlString(serverInfo, "hostname"); this->name = NvHTTP::getXmlString(serverInfo, "hostname");
if (this->name.isEmpty()) { if (this->name.isEmpty()) {
this->name = "UNKNOWN"; this->name = "UNKNOWN";
@@ -167,6 +173,32 @@ bool NvComputer::wake()
QVector<QString> addressList = uniqueAddresses(); QVector<QString> addressList = uniqueAddresses();
addressList.append("255.255.255.255"); addressList.append("255.255.255.255");
// Try to broadcast on all available NICs
for (const QNetworkInterface& nic : QNetworkInterface::allInterfaces()) {
// Ensure the interface is up and skip the loopback adapter
if ((nic.flags() & QNetworkInterface::IsUp) == 0 ||
(nic.flags() & QNetworkInterface::IsLoopBack) != 0) {
continue;
}
QHostAddress allNodesMulticast("FF02::1");
for (const QNetworkAddressEntry& addr : nic.addressEntries()) {
// Store the scope ID for this NIC if IPv6 is enabled
if (!addr.ip().scopeId().isEmpty()) {
allNodesMulticast.setScopeId(addr.ip().scopeId());
}
// Skip IPv6 which doesn't support broadcast
if (!addr.broadcast().isNull()) {
addressList.append(addr.broadcast().toString());
}
}
if (!allNodesMulticast.scopeId().isEmpty()) {
addressList.append(allNodesMulticast.toString());
}
}
// Try all unique address strings or host names // Try all unique address strings or host names
bool success = false; bool success = false;
for (QString& addressString : addressList) { for (QString& addressString : addressList) {
@@ -181,16 +213,14 @@ bool NvComputer::wake()
for (QHostAddress& address : hostInfo.addresses()) { for (QHostAddress& address : hostInfo.addresses()) {
QUdpSocket sock; QUdpSocket sock;
// Bind to any address on the correct protocol // Send to all ports
if (sock.bind(address.protocol() == QUdpSocket::IPv4Protocol ? for (quint16 port : WOL_PORTS) {
QHostAddress::AnyIPv4 : QHostAddress::AnyIPv6)) { if (sock.writeDatagram(wolPayload, address, port)) {
qInfo().nospace().noquote() << "Sent WoL packet to " << name << " via " << address.toString() << ":" << port;
// Send to all ports success = true;
for (quint16 port : WOL_PORTS) { }
if (sock.writeDatagram(wolPayload, address, port)) { else {
qInfo().nospace().noquote() << "Send WoL packet to " << name << " via " << address.toString() << ":" << port; qWarning() << "Send failed:" << sock.error();
success = true;
}
} }
} }
} }
@@ -255,6 +285,13 @@ bool NvComputer::update(NvComputer& that)
changed = true; \ changed = true; \
} }
#define ASSIGN_IF_CHANGED_AND_NONNULL(field) \
if (!that.field.isNull() && \
this->field != that.field) { \
this->field = that.field; \
changed = true; \
}
ASSIGN_IF_CHANGED(name); ASSIGN_IF_CHANGED(name);
ASSIGN_IF_CHANGED_AND_NONEMPTY(macAddress); ASSIGN_IF_CHANGED_AND_NONEMPTY(macAddress);
ASSIGN_IF_CHANGED_AND_NONEMPTY(localAddress); ASSIGN_IF_CHANGED_AND_NONEMPTY(localAddress);
@@ -269,6 +306,7 @@ bool NvComputer::update(NvComputer& that)
ASSIGN_IF_CHANGED(appVersion); ASSIGN_IF_CHANGED(appVersion);
ASSIGN_IF_CHANGED(maxLumaPixelsHEVC); ASSIGN_IF_CHANGED(maxLumaPixelsHEVC);
ASSIGN_IF_CHANGED(gpuModel); ASSIGN_IF_CHANGED(gpuModel);
ASSIGN_IF_CHANGED_AND_NONNULL(serverCert);
ASSIGN_IF_CHANGED_AND_NONEMPTY(appList); ASSIGN_IF_CHANGED_AND_NONEMPTY(appList);
ASSIGN_IF_CHANGED_AND_NONEMPTY(displayModes); ASSIGN_IF_CHANGED_AND_NONEMPTY(displayModes);
return changed; return changed;
+2 -1
View File
@@ -19,7 +19,7 @@ private:
bool pendingQuit; bool pendingQuit;
public: public:
explicit NvComputer(QString address, QString serverInfo); explicit NvComputer(QString address, QString serverInfo, QSslCertificate serverCert);
explicit NvComputer(QSettings& settings); explicit NvComputer(QSettings& settings);
@@ -68,6 +68,7 @@ public:
QByteArray macAddress; QByteArray macAddress;
QString name; QString name;
QString uuid; QString uuid;
QSslCertificate serverCert;
QVector<NvApp> appList; QVector<NvApp> appList;
// Synchronization // Synchronization
+110 -47
View File
@@ -10,11 +10,16 @@
#include <QSslKey> #include <QSslKey>
#include <QImageReader> #include <QImageReader>
#include <QtEndian> #include <QtEndian>
#include <QNetworkProxy>
#define REQUEST_TIMEOUT_MS 5000 #define REQUEST_TIMEOUT_MS 5000
#define LAUNCH_TIMEOUT_MS 120000
#define RESUME_TIMEOUT_MS 30000
#define QUIT_TIMEOUT_MS 30000
NvHTTP::NvHTTP(QString address) : NvHTTP::NvHTTP(QString address, QSslCertificate serverCert) :
m_Address(address) m_Address(address),
m_ServerCert(serverCert)
{ {
Q_ASSERT(!address.isEmpty()); Q_ASSERT(!address.isEmpty());
@@ -24,6 +29,15 @@ NvHTTP::NvHTTP(QString address) :
m_BaseUrlHttps.setHost(address); m_BaseUrlHttps.setHost(address);
m_BaseUrlHttp.setPort(47989); m_BaseUrlHttp.setPort(47989);
m_BaseUrlHttps.setPort(47984); m_BaseUrlHttps.setPort(47984);
// Never use a proxy server
QNetworkProxy noProxy(QNetworkProxy::NoProxy);
m_Nam.setProxy(noProxy);
}
void NvHTTP::setServerCert(QSslCertificate serverCert)
{
m_ServerCert = serverCert;
} }
QVector<int> QVector<int>
@@ -68,36 +82,50 @@ NvHTTP::getServerInfo(NvLogLevel logLevel)
{ {
QString serverInfo; QString serverInfo;
try // Check if we have a pinned cert for this host yet
if (!m_ServerCert.isNull())
{ {
// Always try HTTPS first, since it properly reports try
// pairing status (and a few other attributes).
serverInfo = openConnectionToString(m_BaseUrlHttps,
"serverinfo",
nullptr,
true,
logLevel);
// Throws if the request failed
verifyResponseStatus(serverInfo);
}
catch (const GfeHttpResponseException& e)
{
if (e.getStatusCode() == 401)
{ {
// Certificate validation error, fallback to HTTP // Always try HTTPS first, since it properly reports
serverInfo = openConnectionToString(m_BaseUrlHttp, // pairing status (and a few other attributes).
serverInfo = openConnectionToString(m_BaseUrlHttps,
"serverinfo", "serverinfo",
nullptr, nullptr,
true, REQUEST_TIMEOUT_MS,
logLevel); logLevel);
// Throws if the request failed
verifyResponseStatus(serverInfo); verifyResponseStatus(serverInfo);
} }
else catch (const GfeHttpResponseException& e)
{ {
// Rethrow real errors if (e.getStatusCode() == 401)
throw e; {
// Certificate validation error, fallback to HTTP
serverInfo = openConnectionToString(m_BaseUrlHttp,
"serverinfo",
nullptr,
REQUEST_TIMEOUT_MS,
logLevel);
verifyResponseStatus(serverInfo);
}
else
{
// Rethrow real errors
throw e;
}
} }
} }
else
{
// Only use HTTP prior to pairing
serverInfo = openConnectionToString(m_BaseUrlHttp,
"serverinfo",
nullptr,
REQUEST_TIMEOUT_MS,
logLevel);
verifyResponseStatus(serverInfo);
}
return serverInfo; return serverInfo;
} }
@@ -144,7 +172,10 @@ NvHTTP::launchApp(int appId,
"appid="+QString::number(appId)+ "appid="+QString::number(appId)+
"&mode="+QString::number(streamConfig->width)+"x"+ "&mode="+QString::number(streamConfig->width)+"x"+
QString::number(streamConfig->height)+"x"+ QString::number(streamConfig->height)+"x"+
QString::number(streamConfig->fps)+ // Using an FPS value over 60 causes SOPS to default to 720p60,
// so force it to 60 when starting. This won't impact our ability
// to get > 60 FPS while actually streaming though.
QString::number(streamConfig->fps > 60 ? 60 : streamConfig->fps)+
"&additionalStates=1&sops="+QString::number(sops ? 1 : 0)+ "&additionalStates=1&sops="+QString::number(sops ? 1 : 0)+
"&rikey="+QByteArray(streamConfig->remoteInputAesKey, sizeof(streamConfig->remoteInputAesKey)).toHex()+ "&rikey="+QByteArray(streamConfig->remoteInputAesKey, sizeof(streamConfig->remoteInputAesKey)).toHex()+
"&rikeyid="+QString::number(riKeyId)+ "&rikeyid="+QString::number(riKeyId)+
@@ -155,7 +186,7 @@ NvHTTP::launchApp(int appId,
"&surroundAudioInfo="+getSurroundAudioInfoString(streamConfig->audioConfiguration)+ "&surroundAudioInfo="+getSurroundAudioInfoString(streamConfig->audioConfiguration)+
"&remoteControllersBitmap="+QString::number(gamepadMask)+ "&remoteControllersBitmap="+QString::number(gamepadMask)+
"&gcmap="+QString::number(gamepadMask), "&gcmap="+QString::number(gamepadMask),
false); LAUNCH_TIMEOUT_MS);
// Throws if the request failed // Throws if the request failed
verifyResponseStatus(response); verifyResponseStatus(response);
@@ -175,7 +206,7 @@ NvHTTP::resumeApp(PSTREAM_CONFIGURATION streamConfig)
"rikey="+QString(QByteArray(streamConfig->remoteInputAesKey, sizeof(streamConfig->remoteInputAesKey)).toHex())+ "rikey="+QString(QByteArray(streamConfig->remoteInputAesKey, sizeof(streamConfig->remoteInputAesKey)).toHex())+
"&rikeyid="+QString::number(riKeyId)+ "&rikeyid="+QString::number(riKeyId)+
"&surroundAudioInfo="+getSurroundAudioInfoString(streamConfig->audioConfiguration), "&surroundAudioInfo="+getSurroundAudioInfoString(streamConfig->audioConfiguration),
false); RESUME_TIMEOUT_MS);
// Throws if the request failed // Throws if the request failed
verifyResponseStatus(response); verifyResponseStatus(response);
@@ -188,14 +219,14 @@ NvHTTP::quitApp()
openConnectionToString(m_BaseUrlHttps, openConnectionToString(m_BaseUrlHttps,
"cancel", "cancel",
nullptr, nullptr,
false); QUIT_TIMEOUT_MS);
// Throws if the request failed // Throws if the request failed
verifyResponseStatus(response); verifyResponseStatus(response);
// Newer GFE versions will just return success even if quitting fails // Newer GFE versions will just return success even if quitting fails
// if we're not the original requestor. // if we're not the original requestor.
if (getCurrentGame(getServerInfo(NvHTTP::ERROR)) != 0) { if (getCurrentGame(getServerInfo(NvHTTP::NVLL_ERROR)) != 0) {
// Generate a synthetic GfeResponseException letting the caller know // Generate a synthetic GfeResponseException letting the caller know
// that they can't kill someone else's stream. // that they can't kill someone else's stream.
throw GfeHttpResponseException(599, ""); throw GfeHttpResponseException(599, "");
@@ -235,8 +266,8 @@ NvHTTP::getAppList()
QString appxml = openConnectionToString(m_BaseUrlHttps, QString appxml = openConnectionToString(m_BaseUrlHttps,
"applist", "applist",
nullptr, nullptr,
true, REQUEST_TIMEOUT_MS,
NvLogLevel::ERROR); NvLogLevel::NVLL_ERROR);
verifyResponseStatus(appxml); verifyResponseStatus(appxml);
QXmlStreamReader xmlReader(appxml); QXmlStreamReader xmlReader(appxml);
@@ -303,8 +334,8 @@ NvHTTP::getBoxArt(int appId)
"appasset", "appasset",
"appid="+QString::number(appId)+ "appid="+QString::number(appId)+
"&AssetType=2&AssetIdx=0", "&AssetType=2&AssetIdx=0",
true, REQUEST_TIMEOUT_MS,
NvLogLevel::VERBOSE); NvLogLevel::NVLL_VERBOSE);
QImage image = QImageReader(reply).read(); QImage image = QImageReader(reply).read();
delete reply; delete reply;
@@ -350,10 +381,10 @@ QString
NvHTTP::openConnectionToString(QUrl baseUrl, NvHTTP::openConnectionToString(QUrl baseUrl,
QString command, QString command,
QString arguments, QString arguments,
bool enableTimeout, int timeoutMs,
NvLogLevel logLevel) NvLogLevel logLevel)
{ {
QNetworkReply* reply = openConnection(baseUrl, command, arguments, enableTimeout, logLevel); QNetworkReply* reply = openConnection(baseUrl, command, arguments, timeoutMs, logLevel);
QString ret; QString ret;
QTextStream stream(reply); QTextStream stream(reply);
@@ -368,14 +399,18 @@ QNetworkReply*
NvHTTP::openConnection(QUrl baseUrl, NvHTTP::openConnection(QUrl baseUrl,
QString command, QString command,
QString arguments, QString arguments,
bool enableTimeout, int timeoutMs,
NvLogLevel logLevel) NvLogLevel logLevel)
{ {
// Build a URL for the request // Build a URL for the request
QUrl url(baseUrl); QUrl url(baseUrl);
url.setPath("/" + command); url.setPath("/" + command);
url.setQuery("uniqueid=" + IdentityManager::get()->getUniqueId() +
"&uuid=" + QUuid::createUuid().toRfc4122().toHex() + // Use a common UID for Moonlight clients to allow them to quit
// games for each other (otherwise GFE gets screwed up and it requires
// manual intervention to solve).
url.setQuery("uniqueid=0123456789ABCDEF&uuid=" +
QUuid::createUuid().toRfc4122().toHex() +
((arguments != nullptr) ? ("&" + arguments) : "")); ((arguments != nullptr) ? ("&" + arguments) : ""));
QNetworkRequest request = QNetworkRequest(url); QNetworkRequest request = QNetworkRequest(url);
@@ -385,17 +420,30 @@ NvHTTP::openConnection(QUrl baseUrl,
QNetworkReply* reply = m_Nam.get(request); QNetworkReply* reply = m_Nam.get(request);
// Ignore self-signed certificate errors (since GFE uses them) // Assert that we always have a server cert for HTTPS, since
reply->ignoreSslErrors(); // the request will fail anyway if we do not.
Q_ASSERT(!m_ServerCert.isNull() || baseUrl == m_BaseUrlHttp);
if (!m_ServerCert.isNull()) {
// Pin the server certificate received during pairing
QList<QSslError> expectedSslErrors;
expectedSslErrors.append(QSslError(QSslError::HostNameMismatch, m_ServerCert));
expectedSslErrors.append(QSslError(QSslError::SelfSignedCertificate, m_ServerCert));
// The SecureTransport backend for Qt TLS on macOS throws CertificateUntrusted
// instead of SelfSignedCertificate, so we will need to allow that error too.
expectedSslErrors.append(QSslError(QSslError::CertificateUntrusted, m_ServerCert));
reply->ignoreSslErrors(expectedSslErrors);
}
// Run the request with a timeout if requested // Run the request with a timeout if requested
QEventLoop loop; QEventLoop loop;
QObject::connect(reply, SIGNAL(finished()), &loop, SLOT(quit())); QObject::connect(reply, SIGNAL(finished()), &loop, SLOT(quit()));
if (enableTimeout) if (timeoutMs) {
{ QTimer::singleShot(timeoutMs, &loop, SLOT(quit()));
QTimer::singleShot(REQUEST_TIMEOUT_MS, &loop, SLOT(quit()));
} }
if (logLevel >= NvLogLevel::VERBOSE) { if (logLevel >= NvLogLevel::NVLL_VERBOSE) {
qInfo() << "Executing request:" << url.toString(); qInfo() << "Executing request:" << url.toString();
} }
loop.exec(QEventLoop::ExcludeUserInputEvents); loop.exec(QEventLoop::ExcludeUserInputEvents);
@@ -403,7 +451,7 @@ NvHTTP::openConnection(QUrl baseUrl,
// Abort the request if it timed out // Abort the request if it timed out
if (!reply->isFinished()) if (!reply->isFinished())
{ {
if (logLevel >= NvLogLevel::ERROR) { if (logLevel >= NvLogLevel::NVLL_ERROR) {
qWarning() << "Aborting timed out request for" << url.toString(); qWarning() << "Aborting timed out request for" << url.toString();
} }
reply->abort(); reply->abort();
@@ -416,12 +464,27 @@ NvHTTP::openConnection(QUrl baseUrl,
// Handle error // Handle error
if (reply->error() != QNetworkReply::NoError) if (reply->error() != QNetworkReply::NoError)
{ {
if (logLevel >= NvLogLevel::ERROR) { if (logLevel >= NvLogLevel::NVLL_ERROR) {
qWarning() << command << " request failed with error " << reply->error(); qWarning() << command << " request failed with error " << reply->error();
} }
QtNetworkReplyException exception(reply->error(), reply->errorString());
delete reply; if (reply->error() == QNetworkReply::SslHandshakeFailedError) {
throw exception; // This will trigger falling back to HTTP for the serverinfo query
// then pairing again to get the updated certificate.
GfeHttpResponseException exception(401, "Server certificate mismatch");
delete reply;
throw exception;
}
else if (reply->error() == QNetworkReply::OperationCanceledError) {
QtNetworkReplyException exception(QNetworkReply::TimeoutError, "Request timed out");
delete reply;
throw exception;
}
else {
QtNetworkReplyException exception(reply->error(), reply->errorString());
delete reply;
throw exception;
}
} }
return reply; return reply;
+10 -7
View File
@@ -117,12 +117,12 @@ class NvHTTP
{ {
public: public:
enum NvLogLevel { enum NvLogLevel {
NONE, NVLL_NONE,
ERROR, NVLL_ERROR,
VERBOSE NVLL_VERBOSE
}; };
explicit NvHTTP(QString address); explicit NvHTTP(QString address, QSslCertificate serverCert);
static static
int int
@@ -149,8 +149,10 @@ public:
openConnectionToString(QUrl baseUrl, openConnectionToString(QUrl baseUrl,
QString command, QString command,
QString arguments, QString arguments,
bool enableTimeout, int timeoutMs,
NvLogLevel logLevel = NvLogLevel::VERBOSE); NvLogLevel logLevel = NvLogLevel::NVLL_VERBOSE);
void setServerCert(QSslCertificate serverCert);
static static
QVector<int> QVector<int>
@@ -186,9 +188,10 @@ private:
openConnection(QUrl baseUrl, openConnection(QUrl baseUrl,
QString command, QString command,
QString arguments, QString arguments,
bool enableTimeout, int timeoutMs,
NvLogLevel logLevel); NvLogLevel logLevel);
QString m_Address; QString m_Address;
QNetworkAccessManager m_Nam; QNetworkAccessManager m_Nam;
QSslCertificate m_ServerCert;
}; };
+32 -18
View File
@@ -8,8 +8,10 @@
#include <openssl/x509.h> #include <openssl/x509.h>
#include <openssl/evp.h> #include <openssl/evp.h>
#define REQUEST_TIMEOUT_MS 5000
NvPairingManager::NvPairingManager(QString address) : NvPairingManager::NvPairingManager(QString address) :
m_Http(address) m_Http(address, QSslCertificate())
{ {
QByteArray cert = IdentityManager::get()->getCertificate(); QByteArray cert = IdentityManager::get()->getCertificate();
BIO *bio = BIO_new_mem_buf(cert.data(), -1); BIO *bio = BIO_new_mem_buf(cert.data(), -1);
@@ -161,7 +163,7 @@ NvPairingManager::saltPin(QByteArray salt, QString pin)
} }
NvPairingManager::PairState NvPairingManager::PairState
NvPairingManager::pair(QString appVersion, QString pin) NvPairingManager::pair(QString appVersion, QString pin, QSslCertificate& serverCert)
{ {
int serverMajorVersion = NvHTTP::parseQuad(appVersion).at(0); int serverMajorVersion = NvHTTP::parseQuad(appVersion).at(0);
qInfo() << "Pairing with server generation:" << serverMajorVersion; qInfo() << "Pairing with server generation:" << serverMajorVersion;
@@ -192,7 +194,7 @@ NvPairingManager::pair(QString appVersion, QString pin)
"pair", "pair",
"devicename=roth&updateState=1&phrase=getservercert&salt=" + "devicename=roth&updateState=1&phrase=getservercert&salt=" +
salt.toHex() + "&clientcert=" + IdentityManager::get()->getCertificate().toHex(), salt.toHex() + "&clientcert=" + IdentityManager::get()->getCertificate().toHex(),
false); 0);
NvHTTP::verifyResponseStatus(getCert); NvHTTP::verifyResponseStatus(getCert);
if (NvHTTP::getXmlString(getCert, "paired") != "1") if (NvHTTP::getXmlString(getCert, "paired") != "1")
{ {
@@ -200,26 +202,38 @@ NvPairingManager::pair(QString appVersion, QString pin)
return PairState::FAILED; return PairState::FAILED;
} }
QByteArray serverCert = NvHTTP::getXmlStringFromHex(getCert, "plaincert"); QByteArray serverCertStr = NvHTTP::getXmlStringFromHex(getCert, "plaincert");
if (serverCert == nullptr) if (serverCertStr == nullptr)
{ {
qCritical() << "Server likely already pairing"; qCritical() << "Server likely already pairing";
m_Http.openConnectionToString(m_Http.m_BaseUrlHttp, "unpair", nullptr, true); m_Http.openConnectionToString(m_Http.m_BaseUrlHttp, "unpair", nullptr, REQUEST_TIMEOUT_MS);
return PairState::ALREADY_IN_PROGRESS; return PairState::ALREADY_IN_PROGRESS;
} }
serverCert = QSslCertificate(serverCertStr);
if (serverCert.isNull()) {
Q_ASSERT(!serverCert.isNull());
qCritical() << "Failed to parse plaincert";
m_Http.openConnectionToString(m_Http.m_BaseUrlHttp, "unpair", nullptr, REQUEST_TIMEOUT_MS);
return PairState::FAILED;
}
// Pin this cert for TLS
m_Http.setServerCert(serverCert);
QByteArray randomChallenge = generateRandomBytes(16); QByteArray randomChallenge = generateRandomBytes(16);
QByteArray encryptedChallenge = encrypt(randomChallenge, &encKey); QByteArray encryptedChallenge = encrypt(randomChallenge, &encKey);
QString challengeXml = m_Http.openConnectionToString(m_Http.m_BaseUrlHttp, QString challengeXml = m_Http.openConnectionToString(m_Http.m_BaseUrlHttp,
"pair", "pair",
"devicename=roth&updateState=1&clientchallenge=" + "devicename=roth&updateState=1&clientchallenge=" +
encryptedChallenge.toHex(), encryptedChallenge.toHex(),
true); REQUEST_TIMEOUT_MS);
NvHTTP::verifyResponseStatus(challengeXml); NvHTTP::verifyResponseStatus(challengeXml);
if (NvHTTP::getXmlString(challengeXml, "paired") != "1") if (NvHTTP::getXmlString(challengeXml, "paired") != "1")
{ {
qCritical() << "Failed pairing at stage #2"; qCritical() << "Failed pairing at stage #2";
m_Http.openConnectionToString(m_Http.m_BaseUrlHttp, "unpair", nullptr, true); m_Http.openConnectionToString(m_Http.m_BaseUrlHttp, "unpair", nullptr, REQUEST_TIMEOUT_MS);
return PairState::FAILED; return PairState::FAILED;
} }
@@ -247,12 +261,12 @@ NvPairingManager::pair(QString appVersion, QString pin)
"pair", "pair",
"devicename=roth&updateState=1&serverchallengeresp=" + "devicename=roth&updateState=1&serverchallengeresp=" +
encryptedChallengeResponseHash.toHex(), encryptedChallengeResponseHash.toHex(),
true); REQUEST_TIMEOUT_MS);
NvHTTP::verifyResponseStatus(respXml); NvHTTP::verifyResponseStatus(respXml);
if (NvHTTP::getXmlString(respXml, "paired") != "1") if (NvHTTP::getXmlString(respXml, "paired") != "1")
{ {
qCritical() << "Failed pairing at stage #3"; qCritical() << "Failed pairing at stage #3";
m_Http.openConnectionToString(m_Http.m_BaseUrlHttp, "unpair", nullptr, true); m_Http.openConnectionToString(m_Http.m_BaseUrlHttp, "unpair", nullptr, REQUEST_TIMEOUT_MS);
return PairState::FAILED; return PairState::FAILED;
} }
@@ -262,21 +276,21 @@ NvPairingManager::pair(QString appVersion, QString pin)
if (!verifySignature(serverSecret, if (!verifySignature(serverSecret,
serverSignature, serverSignature,
serverCert)) serverCertStr))
{ {
qCritical() << "MITM detected"; qCritical() << "MITM detected";
m_Http.openConnectionToString(m_Http.m_BaseUrlHttp, "unpair", nullptr, true); m_Http.openConnectionToString(m_Http.m_BaseUrlHttp, "unpair", nullptr, REQUEST_TIMEOUT_MS);
return PairState::FAILED; return PairState::FAILED;
} }
QByteArray expectedResponseData; QByteArray expectedResponseData;
expectedResponseData.append(randomChallenge); expectedResponseData.append(randomChallenge);
expectedResponseData.append(getSignatureFromPemCert(serverCert)); expectedResponseData.append(getSignatureFromPemCert(serverCertStr));
expectedResponseData.append(serverSecret); expectedResponseData.append(serverSecret);
if (QCryptographicHash::hash(expectedResponseData, hashAlgo) != serverResponse) if (QCryptographicHash::hash(expectedResponseData, hashAlgo) != serverResponse)
{ {
qCritical() << "Incorrect PIN"; qCritical() << "Incorrect PIN";
m_Http.openConnectionToString(m_Http.m_BaseUrlHttp, "unpair", nullptr, true); m_Http.openConnectionToString(m_Http.m_BaseUrlHttp, "unpair", nullptr, REQUEST_TIMEOUT_MS);
return PairState::PIN_WRONG; return PairState::PIN_WRONG;
} }
@@ -288,24 +302,24 @@ NvPairingManager::pair(QString appVersion, QString pin)
"pair", "pair",
"devicename=roth&updateState=1&clientpairingsecret=" + "devicename=roth&updateState=1&clientpairingsecret=" +
clientPairingSecret.toHex(), clientPairingSecret.toHex(),
true); REQUEST_TIMEOUT_MS);
NvHTTP::verifyResponseStatus(secretRespXml); NvHTTP::verifyResponseStatus(secretRespXml);
if (NvHTTP::getXmlString(secretRespXml, "paired") != "1") if (NvHTTP::getXmlString(secretRespXml, "paired") != "1")
{ {
qCritical() << "Failed pairing at stage #4"; qCritical() << "Failed pairing at stage #4";
m_Http.openConnectionToString(m_Http.m_BaseUrlHttp, "unpair", nullptr, true); m_Http.openConnectionToString(m_Http.m_BaseUrlHttp, "unpair", nullptr, REQUEST_TIMEOUT_MS);
return PairState::FAILED; return PairState::FAILED;
} }
QString pairChallengeXml = m_Http.openConnectionToString(m_Http.m_BaseUrlHttps, QString pairChallengeXml = m_Http.openConnectionToString(m_Http.m_BaseUrlHttps,
"pair", "pair",
"devicename=roth&updateState=1&phrase=pairchallenge", "devicename=roth&updateState=1&phrase=pairchallenge",
true); REQUEST_TIMEOUT_MS);
NvHTTP::verifyResponseStatus(pairChallengeXml); NvHTTP::verifyResponseStatus(pairChallengeXml);
if (NvHTTP::getXmlString(pairChallengeXml, "paired") != "1") if (NvHTTP::getXmlString(pairChallengeXml, "paired") != "1")
{ {
qCritical() << "Failed pairing at stage #5"; qCritical() << "Failed pairing at stage #5";
m_Http.openConnectionToString(m_Http.m_BaseUrlHttp, "unpair", nullptr, true); m_Http.openConnectionToString(m_Http.m_BaseUrlHttp, "unpair", nullptr, REQUEST_TIMEOUT_MS);
return PairState::FAILED; return PairState::FAILED;
} }
+1 -1
View File
@@ -23,7 +23,7 @@ public:
~NvPairingManager(); ~NvPairingManager();
PairState PairState
pair(QString appVersion, QString pin); pair(QString appVersion, QString pin, QSslCertificate& serverCert);
private: private:
QByteArray QByteArray
+53 -1
View File
@@ -164,6 +164,7 @@ GlobalCommandLineParser::ParseResult GlobalCommandLineParser::parse(const QStrin
"Starts Moonlight normally if no arguments are given.\n" "Starts Moonlight normally if no arguments are given.\n"
"\n" "\n"
"Available actions:\n" "Available actions:\n"
" quit Quit the currently running app\n"
" stream Start streaming an app\n" " stream Start streaming an app\n"
"\n" "\n"
"See 'moonlight <action> --help' for help of specific action." "See 'moonlight <action> --help' for help of specific action."
@@ -179,6 +180,8 @@ GlobalCommandLineParser::ParseResult GlobalCommandLineParser::parse(const QStrin
parser.handleHelpAndVersionOptions(); parser.handleHelpAndVersionOptions();
parser.handleUnknownOptions(); parser.handleUnknownOptions();
return NormalStartRequested; return NormalStartRequested;
} else if (action == "quit") {
return QuitRequested;
} else if (action == "stream") { } else if (action == "stream") {
return StreamRequested; return StreamRequested;
} else { } else {
@@ -186,6 +189,48 @@ GlobalCommandLineParser::ParseResult GlobalCommandLineParser::parse(const QStrin
} }
} }
QuitCommandLineParser::QuitCommandLineParser()
{
}
QuitCommandLineParser::~QuitCommandLineParser()
{
}
void QuitCommandLineParser::parse(const QStringList &args)
{
CommandLineParser parser;
parser.setupCommonOptions();
parser.setApplicationDescription(
"\n"
"Quit the currently running app on the given host."
);
parser.addPositionalArgument("quit", "quit running app");
parser.addPositionalArgument("host", "Host computer name, UUID, or IP address", "<host>");
if (!parser.parse(args)) {
parser.showError(parser.errorText());
}
parser.handleUnknownOptions();
// This method will not return and terminates the process if --version or
// --help is specified
parser.handleHelpAndVersionOptions();
// Verify that host has been provided
auto posArgs = parser.positionalArguments();
if (posArgs.length() < 2) {
parser.showError("Host not provided");
}
m_Host = parser.positionalArguments().at(1);
}
QString QuitCommandLineParser::getHost() const
{
return m_Host;
}
StreamCommandLineParser::StreamCommandLineParser() StreamCommandLineParser::StreamCommandLineParser()
{ {
m_WindowModeMap = { m_WindowModeMap = {
@@ -238,9 +283,11 @@ void StreamCommandLineParser::parse(const QStringList &args, StreamingPreference
parser.addChoiceOption("display-mode", "display mode", m_WindowModeMap.keys()); parser.addChoiceOption("display-mode", "display mode", m_WindowModeMap.keys());
parser.addChoiceOption("audio-config", "audio config", m_AudioConfigMap.keys()); parser.addChoiceOption("audio-config", "audio config", m_AudioConfigMap.keys());
parser.addToggleOption("multi-controller", "multiple controller support"); parser.addToggleOption("multi-controller", "multiple controller support");
parser.addToggleOption("quit-after", "quit app after session");
parser.addToggleOption("mouse-acceleration", "mouse acceleration"); parser.addToggleOption("mouse-acceleration", "mouse acceleration");
parser.addToggleOption("game-optimization", "game optimizations"); parser.addToggleOption("game-optimization", "game optimizations");
parser.addToggleOption("audio-on-host", "audio on host PC"); parser.addToggleOption("audio-on-host", "audio on host PC");
parser.addToggleOption("frame-pacing", "frame pacing");
parser.addChoiceOption("video-codec", "video codec", m_VideoCodecMap.keys()); parser.addChoiceOption("video-codec", "video codec", m_VideoCodecMap.keys());
parser.addChoiceOption("video-decoder", "video decoder", m_VideoDecoderMap.keys()); parser.addChoiceOption("video-decoder", "video decoder", m_VideoDecoderMap.keys());
@@ -314,6 +361,9 @@ void StreamCommandLineParser::parse(const QStringList &args, StreamingPreference
// Resolve --multi-controller and --no-multi-controller options // Resolve --multi-controller and --no-multi-controller options
preferences->multiController = parser.getToggleOptionValue("multi-controller", preferences->multiController); preferences->multiController = parser.getToggleOptionValue("multi-controller", preferences->multiController);
// Resolve --quit-after and --no-quit-after options
preferences->quitAppAfter = parser.getToggleOptionValue("quit-after", preferences->quitAppAfter);
// Resolve --mouse-acceleration and --no-mouse-acceleration options // Resolve --mouse-acceleration and --no-mouse-acceleration options
preferences->mouseAcceleration = parser.getToggleOptionValue("mouse-acceleration", preferences->mouseAcceleration); preferences->mouseAcceleration = parser.getToggleOptionValue("mouse-acceleration", preferences->mouseAcceleration);
@@ -323,6 +373,9 @@ void StreamCommandLineParser::parse(const QStringList &args, StreamingPreference
// Resolve --audio-on-host and --no-audio-on-host options // Resolve --audio-on-host and --no-audio-on-host options
preferences->playAudioOnHost = parser.getToggleOptionValue("audio-on-host", preferences->playAudioOnHost); preferences->playAudioOnHost = parser.getToggleOptionValue("audio-on-host", preferences->playAudioOnHost);
// Resolve --frame-pacing and --no-frame-pacing options
preferences->framePacing = parser.getToggleOptionValue("frame-pacing", preferences->framePacing);
// Resolve --video-codec option // Resolve --video-codec option
if (parser.isSet("video-codec")) { if (parser.isSet("video-codec")) {
preferences->videoCodecConfig = mapValue(m_VideoCodecMap, parser.getChoiceOptionValue("video-codec")); preferences->videoCodecConfig = mapValue(m_VideoCodecMap, parser.getChoiceOptionValue("video-codec"));
@@ -359,4 +412,3 @@ QString StreamCommandLineParser::getAppName() const
{ {
return m_AppName; return m_AppName;
} }
+15
View File
@@ -11,6 +11,7 @@ public:
enum ParseResult { enum ParseResult {
NormalStartRequested, NormalStartRequested,
StreamRequested, StreamRequested,
QuitRequested,
}; };
GlobalCommandLineParser(); GlobalCommandLineParser();
@@ -20,6 +21,20 @@ public:
}; };
class QuitCommandLineParser
{
public:
QuitCommandLineParser();
virtual ~QuitCommandLineParser();
void parse(const QStringList &args);
QString getHost() const;
private:
QString m_Host;
};
class StreamCommandLineParser class StreamCommandLineParser
{ {
public: public:
+171
View File
@@ -0,0 +1,171 @@
#include "quitstream.h"
#include "backend/computermanager.h"
#include "backend/computerseeker.h"
#include "streaming/session.h"
#include <QCoreApplication>
#include <QTimer>
#define COMPUTER_SEEK_TIMEOUT 10000
namespace CliQuitStream
{
enum State {
StateInit,
StateSeekComputer,
StateQuitApp,
StateFailure,
};
class Event
{
public:
enum Type {
AppQuitCompleted,
ComputerFound,
ComputerSeekTimedout,
Executed,
};
Event(Type type)
: type(type), computerManager(nullptr), computer(nullptr) {}
Type type;
ComputerManager *computerManager;
NvComputer *computer;
QString errorMessage;
};
class LauncherPrivate
{
Q_DECLARE_PUBLIC(Launcher)
public:
LauncherPrivate(Launcher *q) : q_ptr(q) {}
void handleEvent(Event event)
{
Q_Q(Launcher);
NvApp app;
switch (event.type) {
// Occurs when CliQuitStreamSegue becomes visible and the UI calls launcher's execute()
case Event::Executed:
if (m_State == StateInit) {
m_State = StateSeekComputer;
m_ComputerManager = event.computerManager;
q->connect(m_ComputerManager, &ComputerManager::quitAppCompleted,
q, &Launcher::onQuitAppCompleted);
m_ComputerSeeker = new ComputerSeeker(m_ComputerManager, m_ComputerName, q);
q->connect(m_ComputerSeeker, &ComputerSeeker::computerFound,
q, &Launcher::onComputerFound);
q->connect(m_ComputerSeeker, &ComputerSeeker::errorTimeout,
q, &Launcher::onComputerSeekTimeout);
m_ComputerSeeker->start(COMPUTER_SEEK_TIMEOUT);
emit q->searchingComputer();
}
break;
// Occurs when computer search timed out
case Event::ComputerSeekTimedout:
if (m_State == StateSeekComputer) {
m_State = StateFailure;
emit q->failed(QString("Failed to connect to %1").arg(m_ComputerName));
}
break;
// Occurs when searched computer is found
case Event::ComputerFound:
if (m_State == StateSeekComputer) {
if (event.computer->pairState == NvComputer::PS_PAIRED) {
m_State = StateQuitApp;
emit q->quittingApp();
m_ComputerManager->quitRunningApp(event.computer);
} else {
m_State = StateFailure;
QString msg = QString("Computer %1 has not been paired. "
"Please open Moonlight to pair before streaming.")
.arg(event.computer->name);
emit q->failed(msg);
}
}
break;
// Occurs when app quit completed (error message is set if failed)
case Event::AppQuitCompleted:
if (m_State == StateQuitApp) {
if (event.errorMessage.isEmpty()) {
QCoreApplication::exit(0);
} else {
m_State = StateFailure;
emit q->failed(QString("Quitting app failed, reason: %1").arg(event.errorMessage));
}
}
break;
}
}
Launcher *q_ptr;
ComputerManager *m_ComputerManager;
QString m_ComputerName;
ComputerSeeker *m_ComputerSeeker;
State m_State;
QTimer *m_TimeoutTimer;
};
Launcher::Launcher(QString computer, QObject *parent)
: QObject(parent),
m_DPtr(new LauncherPrivate(this))
{
Q_D(Launcher);
d->m_ComputerName = computer;
d->m_State = StateInit;
d->m_TimeoutTimer = new QTimer(this);
d->m_TimeoutTimer->setSingleShot(true);
connect(d->m_TimeoutTimer, &QTimer::timeout,
this, &Launcher::onComputerSeekTimeout);
}
Launcher::~Launcher()
{
}
void Launcher::execute(ComputerManager *manager)
{
Q_D(Launcher);
Event event(Event::Executed);
event.computerManager = manager;
d->handleEvent(event);
}
bool Launcher::isExecuted() const
{
Q_D(const Launcher);
return d->m_State != StateInit;
}
void Launcher::onComputerFound(NvComputer *computer)
{
Q_D(Launcher);
Event event(Event::ComputerFound);
event.computer = computer;
d->handleEvent(event);
}
void Launcher::onComputerSeekTimeout()
{
Q_D(Launcher);
Event event(Event::ComputerSeekTimedout);
d->handleEvent(event);
}
void Launcher::onQuitAppCompleted(QVariant error)
{
Q_D(Launcher);
Event event(Event::AppQuitCompleted);
event.errorMessage = error.toString();
d->handleEvent(event);
}
}
+40
View File
@@ -0,0 +1,40 @@
#pragma once
#include <QObject>
#include <QVariant>
class ComputerManager;
class NvComputer;
namespace CliQuitStream
{
class LauncherPrivate;
class Launcher : public QObject
{
Q_OBJECT
Q_DECLARE_PRIVATE_D(m_DPtr, Launcher)
public:
explicit Launcher(QString computer, QObject *parent = nullptr);
~Launcher();
Q_INVOKABLE void execute(ComputerManager *manager);
Q_INVOKABLE bool isExecuted() const;
signals:
void searchingComputer();
void quittingApp();
void failed(QString text);
private slots:
void onComputerFound(NvComputer *computer);
void onComputerSeekTimeout();
void onQuitAppCompleted(QVariant error);
private:
QScopedPointer<LauncherPrivate> m_DPtr;
};
}
+83 -47
View File
@@ -1,7 +1,9 @@
#include "startstream.h" #include "startstream.h"
#include "backend/computermanager.h" #include "backend/computermanager.h"
#include "backend/computerseeker.h"
#include "streaming/session.h" #include "streaming/session.h"
#include <QCoreApplication>
#include <QTimer> #include <QTimer>
#define COMPUTER_SEEK_TIMEOUT 10000 #define COMPUTER_SEEK_TIMEOUT 10000
@@ -22,6 +24,9 @@ class Event
{ {
public: public:
enum Type { enum Type {
AppQuitCompleted,
AppQuitRequested,
ComputerFound,
ComputerUpdated, ComputerUpdated,
Executed, Executed,
Timedout, Timedout,
@@ -33,6 +38,7 @@ public:
Type type; Type type;
ComputerManager *computerManager; ComputerManager *computerManager;
NvComputer *computer; NvComputer *computer;
QString errorMessage;
}; };
class LauncherPrivate class LauncherPrivate
@@ -49,58 +55,78 @@ public:
NvApp app; NvApp app;
switch (event.type) { switch (event.type) {
// Occurs when CliStartStreamSegue becomes visible and the UI calls launcher's execute()
case Event::Executed: case Event::Executed:
if (m_State == StateInit) { if (m_State == StateInit) {
m_State = StateSeekComputer; m_State = StateSeekComputer;
m_TimeoutTimer->start(COMPUTER_SEEK_TIMEOUT);
m_ComputerManager = event.computerManager; m_ComputerManager = event.computerManager;
m_ComputerSeeker = new ComputerSeeker(m_ComputerManager, m_ComputerName, q);
q->connect(m_ComputerSeeker, &ComputerSeeker::computerFound,
q, &Launcher::onComputerFound);
q->connect(m_ComputerSeeker, &ComputerSeeker::errorTimeout,
q, &Launcher::onTimeout);
m_ComputerSeeker->start(COMPUTER_SEEK_TIMEOUT);
q->connect(m_ComputerManager, &ComputerManager::computerStateChanged, q->connect(m_ComputerManager, &ComputerManager::computerStateChanged,
q, &Launcher::onComputerUpdated); q, &Launcher::onComputerUpdated);
// Seek desired computer by both connecting to it directly (this may fail q->connect(m_ComputerManager, &ComputerManager::quitAppCompleted,
// if m_ComputerName is UUID, or the name that doesn't resolve to an IP q, &Launcher::onQuitAppCompleted);
// address) and by polling it using mDNS, hopefully one of these methods
// would find the host
m_ComputerManager->addNewHost(m_ComputerName, false);
m_ComputerManager->startPolling();
emit q->searchingComputer(); emit q->searchingComputer();
} }
break; break;
case Event::ComputerUpdated: // Occurs when searched computer is found
case Event::ComputerFound:
if (m_State == StateSeekComputer) { if (m_State == StateSeekComputer) {
if (matchComputer(event.computer) && isOnline(event.computer)) { if (event.computer->pairState == NvComputer::PS_PAIRED) {
if (isPaired(event.computer)) { m_State = StateSeekApp;
m_State = StateSeekApp; m_Computer = event.computer;
m_TimeoutTimer->start(APP_SEEK_TIMEOUT); m_TimeoutTimer->start(APP_SEEK_TIMEOUT);
m_Computer = event.computer; emit q->searchingApp();
m_ComputerManager->stopPollingAsync(); } else {
emit q->searchingApp(); m_State = StateFailure;
} else { QString msg = QString("Computer %1 has not been paired. "
m_State = StateFailure; "Please open Moonlight to pair before streaming.")
QString msg = QString("Computer %1 has not been paired. " .arg(event.computer->name);
"Please open Moonlight to pair before streaming.") emit q->failed(msg);
.arg(event.computer->name);
emit q->failed(msg);
}
} }
} }
break;
// Occurs when a computer is updated
case Event::ComputerUpdated:
if (m_State == StateSeekApp) { if (m_State == StateSeekApp) {
int index = getAppIndex(); int index = getAppIndex();
if (-1 != index) { if (-1 != index) {
app = m_Computer->appList[index]; app = m_Computer->appList[index];
m_TimeoutTimer->stop();
if (isNotStreaming() || isStreamingApp(app)) { if (isNotStreaming() || isStreamingApp(app)) {
m_State = StateStartSession; m_State = StateStartSession;
m_TimeoutTimer->stop();
session = new Session(m_Computer, app, m_Preferences); session = new Session(m_Computer, app, m_Preferences);
emit q->sessionCreated(app.name, session); emit q->sessionCreated(app.name, session);
} else { } else {
m_State = StateFailure; emit q->appQuitRequired(getCurrentAppName());
QString msg = QString("%1 is already running. Please quit %1 to stream %2.")
.arg(getCurrentAppName(), app.name);
emit q->failed(msg);
} }
} }
} }
break; break;
// Occurs when there was another app running on computer and user accepted quit
// confirmation dialog
case Event::AppQuitRequested:
if (m_State == StateSeekApp) {
m_ComputerManager->quitRunningApp(m_Computer);
}
break;
// Occurs when the previous app quit has been completed, handles quitting errors if any
// happended. ComputerUpdated event's handler handles session start when previous app has
// quit.
case Event::AppQuitCompleted:
if (m_State == StateSeekApp && !event.errorMessage.isEmpty()) {
m_State = StateFailure;
emit q->failed(QString("Quitting app failed, reason: %1").arg(event.errorMessage));
}
break;
// Occurs when computer or app search timed out
case Event::Timedout: case Event::Timedout:
if (m_State == StateSeekComputer) { if (m_State == StateSeekComputer) {
m_State = StateFailure; m_State = StateFailure;
@@ -114,26 +140,6 @@ public:
} }
} }
bool 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;
}
bool isOnline(NvComputer *computer) const
{
return computer->state == NvComputer::CS_ONLINE;
}
bool isPaired(NvComputer *computer) const
{
return computer->pairState == NvComputer::PS_PAIRED;
}
int getAppIndex() const int getAppIndex() const
{ {
for (int i = 0; i < m_Computer->appList.length(); i++) { for (int i = 0; i < m_Computer->appList.length(); i++) {
@@ -169,6 +175,7 @@ public:
QString m_AppName; QString m_AppName;
StreamingPreferences *m_Preferences; StreamingPreferences *m_Preferences;
ComputerManager *m_ComputerManager; ComputerManager *m_ComputerManager;
ComputerSeeker *m_ComputerSeeker;
NvComputer *m_Computer; NvComputer *m_Computer;
State m_State; State m_State;
QTimer *m_TimeoutTimer; QTimer *m_TimeoutTimer;
@@ -202,6 +209,27 @@ void Launcher::execute(ComputerManager *manager)
d->handleEvent(event); d->handleEvent(event);
} }
void Launcher::quitRunningApp()
{
Q_D(Launcher);
Event event(Event::AppQuitRequested);
d->handleEvent(event);
}
bool Launcher::isExecuted() const
{
Q_D(const Launcher);
return d->m_State != StateInit;
}
void Launcher::onComputerFound(NvComputer *computer)
{
Q_D(Launcher);
Event event(Event::ComputerFound);
event.computer = computer;
d->handleEvent(event);
}
void Launcher::onComputerUpdated(NvComputer *computer) void Launcher::onComputerUpdated(NvComputer *computer)
{ {
Q_D(Launcher); Q_D(Launcher);
@@ -217,4 +245,12 @@ void Launcher::onTimeout()
d->handleEvent(event); d->handleEvent(event);
} }
void Launcher::onQuitAppCompleted(QVariant error)
{
Q_D(Launcher);
Event event(Event::AppQuitCompleted);
event.errorMessage = error.toString();
d->handleEvent(event);
}
} }
+6
View File
@@ -1,6 +1,7 @@
#pragma once #pragma once
#include <QObject> #include <QObject>
#include <QVariant>
class ComputerManager; class ComputerManager;
class NvComputer; class NvComputer;
@@ -24,16 +25,21 @@ public:
QObject *parent = nullptr); QObject *parent = nullptr);
~Launcher(); ~Launcher();
Q_INVOKABLE void execute(ComputerManager *manager); Q_INVOKABLE void execute(ComputerManager *manager);
Q_INVOKABLE void quitRunningApp();
Q_INVOKABLE bool isExecuted() const;
signals: signals:
void searchingComputer(); void searchingComputer();
void searchingApp(); void searchingApp();
void sessionCreated(QString appName, Session *session); void sessionCreated(QString appName, Session *session);
void failed(QString text); void failed(QString text);
void appQuitRequired(QString appName);
private slots: private slots:
void onComputerFound(NvComputer *computer);
void onComputerUpdated(NvComputer *computer); void onComputerUpdated(NvComputer *computer);
void onTimeout(); void onTimeout();
void onQuitAppCompleted(QVariant error);
private: private:
QScopedPointer<LauncherPrivate> m_DPtr; QScopedPointer<LauncherPrivate> m_DPtr;
@@ -4,7 +4,7 @@
<metadata_license>CC0-1.0</metadata_license> <metadata_license>CC0-1.0</metadata_license>
<project_license>GPL-3.0+</project_license> <project_license>GPL-3.0+</project_license>
<name>Moonlight</name> <name>Moonlight</name>
<summary>Stream games from your NVIDIA GameStream-enabled PC</summary> <summary>Play games remotely from your NVIDIA GameStream-enabled PC</summary>
<description> <description>
<p>Moonlight can stream games and other applications from a PC with an NVIDIA GeForce GTX 600-series or higher GPU and GeForce Experience installed.</p> <p>Moonlight can stream games and other applications from a PC with an NVIDIA GeForce GTX 600-series or higher GPU and GeForce Experience installed.</p>
@@ -19,20 +19,154 @@
</ul> </ul>
</description> </description>
<url type="homepage">https://moonlight-stream.com</url> <url type="homepage">https://moonlight-stream.org</url>
<url type="bugtracker">https://github.com/moonlight-stream/moonlight-qt/issues</url> <url type="bugtracker">https://github.com/moonlight-stream/moonlight-qt/issues</url>
<url type="help">https://github.com/moonlight-stream/moonlight-docs/wiki/Setup-Guide</url> <url type="help">https://github.com/moonlight-stream/moonlight-docs/wiki/Setup-Guide</url>
<screenshots> <screenshots>
<screenshot type="default"> <screenshot type="default">
<image>https://moonlight-stream.com/appstream_resources/moonlight_screenshot_1.png</image> <image>https://moonlight-stream.org/appstream_resources/moonlight_screenshot_1.png</image>
</screenshot> </screenshot>
<screenshot> <screenshot>
<image>https://moonlight-stream.com/appstream_resources/moonlight_screenshot_2.png</image> <image>https://moonlight-stream.org/appstream_resources/moonlight_screenshot_2.png</image>
</screenshot> </screenshot>
</screenshots> </screenshots>
<releases> <releases>
<release version="0.9.1" date="2019-02-17">
<description>
<p>New features:</p>
<ul>
<li>Added gamepad rumble support</li>
<li>Added a dedicated quit button to the running app tile</li>
<li>Reduced video latency on Windows and Linux</li>
<li>Added a performance overlay for software decoding</li>
</ul>
<p>Bugfixes:</p>
<ul>
<li>Fixed delayed first video frame with GFE 3.17</li>
<li>Fixed erroneously setting host resolution to 720p in certain configurations</li>
<li>The connection termination dialog is no longer displayed for intentional termination</li>
<li>Fixed some gamepads only functioning for UI navigation and not in game</li>
<li>Fixed audio latency increasing when switching audio devices</li>
<li>Fixed VDPAU scaling behavior on AMD cards</li>
<li>Fixed several bugs in gamepad navigation behavior</li>
<li>Work around a bug in QGnomePlatform that breaks dialog rendering on Linux</li>
<li>Updated included SDL gamepad mappings</li>
<li>Fixed hotkey behavior on non-latin keyboards</li>
<li>Fixed incorrect frame drop percentages in debug stats</li>
</ul>
</description>
</release>
<release version="0.8.1" date="2019-01-08">
<description>
<p>New features:</p>
<ul>
<li>Stricter adherence to the specified bitrate when streaming over the Internet</li>
</ul>
<p>Bugfixes:</p>
<ul>
<li>Fixed audio crackling that impacted certain systems</li>
<li>Fixed Wake-on-LAN packets failing to send on some systems</li>
<li>Reduced audio latency when resuming a session where audio is already playing</li>
<li>Added option to enable the old SDL audio renderer by setting ML_AUDIO=SDL</li>
<li>Updated to use the new Qt 5.12 Flatpak runtime</li>
<li>Updated to FFmpeg 4.1</li>
<li>Minor UI tweaks</li>
</ul>
</description>
</release>
<release version="0.8.0" date="2018-12-25">
<description>
<p>New features:</p>
<ul>
<li>Improved security of pairing - After this update, you must re-pair with your PC!</li>
<li>Enabled quitting games started by other Moonlight clients also running this update</li>
</ul>
<p>Bugfixes:</p>
<ul>
<li>Fixed the video bitrate slider not working above certain values on GFE 3.16</li>
<li>Fixed broken Bluetooth audio with PulseAudio</li>
<li>Reduced audio latency</li>
<li>Fixed some non-gamepad devices triggering the unmapped gamepad dialog box</li>
<li>Minor UI tweaks</li>
</ul>
</description>
</release>
<release version="0.7.0" date="2018-12-05">
<description>
<p>New features:</p>
<ul>
<li>Added option to quit the app after stopping the stream</li>
<li>Added CLI option to quit the running app</li>
</ul>
<p>Bugfixes:</p>
<ul>
<li>Switched to libsoundio backend to address audio glitches and reduce latency</li>
<li>Fixed CLI version output</li>
<li>Fixed streaming error messages not showing up when launching from CLI</li>
<li>Updated included SDL gamepad mappings</li>
<li>Various minor UI tweaks</li>
</ul>
</description>
</release>
<release version="0.6.4" date="2018-11-20">
<description>
<p>New features:</p>
<ul>
<li>Added an option to match client display refresh rate when FPS is unlocked</li>
</ul>
<p>Bugfixes:</p>
<ul>
<li>Fixed multiple gamepads not being detected on the host in some scenarios</li>
<li>Fixed an incorrect warning when trying to stream at 4K</li>
</ul>
</description>
</release>
<release version="0.6.3" date="2018-11-16">
<description>
<p>New features:</p>
<ul>
<li>Added support for GeForce Experience 3.16</li>
<li>Added an option to force Moonlight to start in windowed mode</li>
</ul>
<p>Bugfixes:</p>
<ul>
<li>Fixed scrolling not working on the settings page</li>
<li>Skipped using proxy servers when attempting to stream</li>
<li>Fixed a couple possible crashes</li>
<li>Renamed the mouse acceleration option to be more clear</li>
</ul>
</description>
</release>
<release version="0.6.2" date="2018-10-28">
<description>
<p>New features:</p>
<ul>
<li>Added automatic IP address detection for Internet streaming</li>
<li>Added a quit shortcut tip for gamepad users</li>
</ul>
<p>Bugfixes:</p>
<ul>
<li>Fixed server state polling not being stopped while streaming</li>
</ul>
</description>
</release>
<release version="0.6.1" date="2018-10-14">
<description>
<p>New features:</p>
<ul>
<li>Added support for quitting Moonlight via gamepad</li>
<li>Added tooltips for games with very long names</li>
</ul>
<p>Bugfixes:</p>
<ul>
<li>Added a workaround for a memory leak in the VAAPI driver for AMD GPUs</li>
<li>Fixed combo boxes on the settings page being too small for certain DPI scaling</li>
<li>Reduced power usage when Moonlight is idling in the background for a while</li>
</ul>
</description>
</release>
<release version="0.6.0" date="2018-10-06"> <release version="0.6.0" date="2018-10-06">
<description> <description>
<p>New features:</p> <p>New features:</p>
@@ -157,7 +291,7 @@
</categories> </categories>
<developer_name>Moonlight Game Streaming Team</developer_name> <developer_name>Moonlight Game Streaming Team</developer_name>
<update_contact>cameron@moonlight-stream.com</update_contact> <update_contact>cameron@moonlight-stream.org</update_contact>
<launchable type="desktop-id">com.moonlight_stream.Moonlight.desktop</launchable> <launchable type="desktop-id">com.moonlight_stream.Moonlight.desktop</launchable>
+79 -28
View File
@@ -66,7 +66,7 @@ GridView {
Image { Image {
id: appIcon id: appIcon
anchors.horizontalCenter: parent.horizontalCenter; anchors.horizontalCenter: parent.horizontalCenter
y: 20 y: 20
source: model.boxart source: model.boxart
sourceSize { sourceSize {
@@ -78,21 +78,63 @@ GridView {
fillMode: Image.Pad fillMode: Image.Pad
} }
Image { ToolButton {
id: runningIcon id: resumeButton
anchors.verticalCenterOffset: -50
anchors.centerIn: appIcon anchors.centerIn: appIcon
visible: model.running visible: model.running
source: "qrc:/res/baseline-play_circle_filled_white-48px.svg" implicitWidth: 125
sourceSize { implicitHeight: 125
width: 75
height: 75 Image {
source: "qrc:/res/baseline-play_circle_filled_white-48px.svg"
anchors.centerIn: parent
sourceSize {
width: 75
height: 75
}
} }
onClicked: {
launchOrResumeSelectedApp()
}
ToolTip.text: "Resume Game"
ToolTip.delay: 1000
ToolTip.timeout: 3000
ToolTip.visible: hovered
} }
Text { ToolButton {
id: quitButton
anchors.verticalCenterOffset: 50
anchors.centerIn: appIcon
visible: model.running
implicitWidth: 125
implicitHeight: 125
Image {
source: "qrc:/res/baseline-cancel-24px.svg"
anchors.centerIn: parent
sourceSize {
width: 75
height: 75
}
}
onClicked: {
doQuitGame()
}
ToolTip.text: "Quit Game"
ToolTip.delay: 1000
ToolTip.timeout: 3000
ToolTip.visible: hovered
}
Label {
id: appNameText id: appNameText
text: model.name text: model.name
color: "white"
width: parent.width width: parent.width
height: 125 height: 125
anchors.top: appIcon.bottom anchors.top: appIcon.bottom
@@ -100,6 +142,12 @@ GridView {
horizontalAlignment: Text.AlignHCenter horizontalAlignment: Text.AlignHCenter
wrapMode: Text.Wrap wrapMode: Text.Wrap
elide: Text.ElideRight 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() function launchOrResumeSelectedApp()
@@ -120,28 +168,34 @@ GridView {
} }
onClicked: { onClicked: {
// Nothing is running or this app is running if (model.running) {
launchOrResumeSelectedApp() // This will primarily be keyboard/gamepad driven so use
} // open() instead of popup()
appContextMenu.open()
MouseArea { }
anchors.fill: parent else {
acceptedButtons: Qt.RightButton launchOrResumeSelectedApp()
onClicked: {
// popup() ensures the menu appears under the mouse cursor
appContextMenu.popup()
} }
} }
Keys.onMenuPressed: { Keys.onMenuPressed: {
// We must use open() here so the menu is positioned on if (model.running) {
// the ItemDelegate and not where the mouse cursor is // This will primarily be keyboard/gamepad driven so use
appContextMenu.open() // open() instead of popup()
appContextMenu.open()
}
} }
Menu { function doQuitGame() {
quitAppDialog.appName = appModel.getRunningAppName()
quitAppDialog.segueToStream = false
quitAppDialog.open()
}
NavigableMenu {
id: appContextMenu id: appContextMenu
NavigableMenuItem { NavigableMenuItem {
parentMenu: appContextMenu
text: model.running ? "Resume Game" : "Launch Game" text: model.running ? "Resume Game" : "Launch Game"
onTriggered: { onTriggered: {
appContextMenu.close() appContextMenu.close()
@@ -149,12 +203,9 @@ GridView {
} }
} }
NavigableMenuItem { NavigableMenuItem {
parentMenu: appContextMenu
text: "Quit Game" text: "Quit Game"
onTriggered: { onTriggered: doQuitGame()
quitAppDialog.appName = appModel.getRunningAppName()
quitAppDialog.segueToStream = false
quitAppDialog.open()
}
visible: model.running visible: model.running
} }
} }
+3 -3
View File
@@ -1,4 +1,4 @@
import QtQuick 2.11 import QtQuick 2.9
import QtQuick.Controls 2.2 import QtQuick.Controls 2.2
// https://stackoverflow.com/questions/45029968/how-do-i-set-the-combobox-width-to-fit-the-largest-item // https://stackoverflow.com/questions/45029968/how-do-i-set-the-combobox-width-to-fit-the-largest-item
@@ -9,17 +9,17 @@ ComboBox {
TextMetrics { TextMetrics {
id: popupMetrics id: popupMetrics
font: popup.font
} }
TextMetrics { TextMetrics {
id: textMetrics id: textMetrics
font: parent.font
} }
// We call this every time the options change (and init) // We call this every time the options change (and init)
// so we can adjust the combo box width here too // so we can adjust the combo box width here too
onActivated: { onActivated: {
textMetrics.font = font
popupMetrics.font = popup.font
textWidth = 0 textWidth = 0
for (var i = 0; i < count; i++){ for (var i = 0; i < count; i++){
textMetrics.text = textAt(i) textMetrics.text = textAt(i)
+70
View File
@@ -0,0 +1,70 @@
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..."
}
function onQuittingApp() {
stageLabel.text = "Quitting app..."
}
function onFailure(message) {
errorDialog.text = message
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()) {
toolBar.visible = false
launcher.searchingComputer.connect(onSearchingComputer)
launcher.quittingApp.connect(onQuittingApp)
launcher.failed.connect(onFailure)
launcher.execute(ComputerManager)
}
}
Row {
anchors.centerIn: parent
spacing: 5
BusyIndicator {
id: stageSpinner
}
Label {
id: stageLabel
height: stageSpinner.height
text: stageText
font.pointSize: 20
verticalAlignment: Text.AlignVCenter
wrapMode: Text.Wrap
}
}
MessageDialog {
id: errorDialog
modality:Qt.WindowModal
icon: StandardIcon.Critical
standardButtons: StandardButton.Ok
onVisibleChanged: {
if (!visible) {
Qt.quit()
}
}
}
}
+48 -2
View File
@@ -1,3 +1,4 @@
import QtQml 2.2
import QtQuick 2.0 import QtQuick 2.0
import QtQuick.Controls 2.2 import QtQuick.Controls 2.2
import QtQuick.Dialogs 1.2 import QtQuick.Dialogs 1.2
@@ -30,13 +31,19 @@ Item {
errorDialog.open() errorDialog.open()
} }
function onAppQuitRequired(appName) {
quitAppDialog.appName = appName
quitAppDialog.open()
}
onVisibleChanged: { onVisibleChanged: {
if (visible) { if (visible && !launcher.isExecuted()) {
toolBar.visible = false toolBar.visible = false
launcher.searchingComputer.connect(onSearchingComputer) launcher.searchingComputer.connect(onSearchingComputer)
launcher.searchingApp.connect(onSearchingApp) launcher.searchingApp.connect(onSearchingApp)
launcher.sessionCreated.connect(onSessionCreated) launcher.sessionCreated.connect(onSessionCreated)
launcher.failed.connect(onLaunchFailed) launcher.failed.connect(onLaunchFailed)
launcher.appQuitRequired.connect(onAppQuitRequired)
launcher.execute(ComputerManager) launcher.execute(ComputerManager)
} }
} }
@@ -56,7 +63,6 @@ Item {
verticalAlignment: Text.AlignVCenter verticalAlignment: Text.AlignVCenter
wrapMode: Text.Wrap wrapMode: Text.Wrap
color: "white"
} }
} }
@@ -75,4 +81,44 @@ Item {
} }
} }
MessageDialog {
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
property string appName : ""
function quitApp() {
var component = Qt.createComponent("QuitSegue.qml")
var params = {"appName": appName}
stackView.push(component.createObject(stackView, params))
// Trigger the quit after pushing the quit segue on screen
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()
}
} }
+8
View File
@@ -0,0 +1,8 @@
import QtQuick 2.0
import QtQuick.Controls 2.2
Menu {
onOpened: {
itemAt(0).forceActiveFocus(Qt.TabFocusReason)
}
}
+9
View File
@@ -2,11 +2,20 @@ import QtQuick 2.0
import QtQuick.Controls 2.2 import QtQuick.Controls 2.2
MenuItem { MenuItem {
// Qt 5.10 has a menu property, but we need to support 5.9
// so we must make our own.
property Menu parentMenu
// Ensure focus can't be given to an invisible item // Ensure focus can't be given to an invisible item
enabled: visible enabled: visible
height: visible ? implicitHeight : 0 height: visible ? implicitHeight : 0
focusPolicy: visible ? Qt.TabFocus : Qt.NoFocus
Keys.onReturnPressed: { Keys.onReturnPressed: {
triggered() triggered()
} }
Keys.onEscapePressed: {
parentMenu.close()
}
} }
+23 -12
View File
@@ -124,10 +124,9 @@ GridView {
visible: !model.addPc && model.statusUnknown visible: !model.addPc && model.statusUnknown
} }
Text { Label {
id: pcNameText id: pcNameText
text: model.name text: model.name
color: "white"
width: parent.width width: parent.width
anchors.top: pcIcon.bottom anchors.top: pcIcon.bottom
@@ -136,14 +135,10 @@ GridView {
wrapMode: Text.Wrap wrapMode: Text.Wrap
} }
Menu { NavigableMenu {
id: pcContextMenu id: pcContextMenu
NavigableMenuItem { NavigableMenuItem {
text: "Wake PC" parentMenu: pcContextMenu
onTriggered: computerModel.wakeComputer(index)
visible: !model.addPc && !model.online && model.wakeable
}
NavigableMenuItem {
text: "Delete PC" text: "Delete PC"
onTriggered: { onTriggered: {
deletePcDialog.pcIndex = index deletePcDialog.pcIndex = index
@@ -151,6 +146,12 @@ GridView {
deletePcDialog.open() deletePcDialog.open()
} }
} }
NavigableMenuItem {
parentMenu: pcContextMenu
text: "Wake PC"
onTriggered: computerModel.wakeComputer(index)
visible: !model.addPc && !model.online && model.wakeable
}
} }
onClicked: { onClicked: {
@@ -187,14 +188,24 @@ 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()
}
}
}
MouseArea { MouseArea {
anchors.fill: parent anchors.fill: parent
acceptedButtons: Qt.RightButton; acceptedButtons: Qt.RightButton;
onClicked: { onClicked: {
if (!model.addPc) { parent.onPressAndHold()
// popup() ensures the menu appears under the mouse cursor
pcContextMenu.popup()
}
} }
} }
+11 -5
View File
@@ -27,19 +27,26 @@ Item {
errorDialog.open() errorDialog.open()
} }
// Exit this view
stackView.pop()
// If we're supposed to launch another game after this, do so now // If we're supposed to launch another game after this, do so now
if (error === undefined && nextSession !== null) { if (error === undefined && nextSession !== null) {
var component = Qt.createComponent("StreamSegue.qml") var component = Qt.createComponent("StreamSegue.qml")
var segue = component.createObject(stackView, {"appName": nextAppName, "session": nextSession}) var segue = component.createObject(stackView, {"appName": nextAppName, "session": nextSession})
stackView.push(segue) stackView.replace(segue)
}
else {
// Show the toolbar again
toolBar.visible = true
// Exit this view
stackView.pop()
} }
} }
onVisibleChanged: { onVisibleChanged: {
if (visible) { if (visible) {
// Hide the toolbar before we start loading
toolBar.visible = false
// Connect the quit completion signal // Connect the quit completion signal
ComputerManager.quitAppCompleted.connect(quitAppCompleted) ComputerManager.quitAppCompleted.connect(quitAppCompleted)
} }
@@ -65,7 +72,6 @@ Item {
verticalAlignment: Text.AlignVCenter verticalAlignment: Text.AlignVCenter
wrapMode: Text.Wrap wrapMode: Text.Wrap
color: "white"
} }
} }
+116 -30
View File
@@ -5,9 +5,24 @@ import StreamingPreferences 1.0
import ComputerManager 1.0 import ComputerManager 1.0
import SdlGamepadKeyNavigation 1.0 import SdlGamepadKeyNavigation 1.0
ScrollView { Flickable {
id: settingsPage id: settingsPage
objectName: "Settings" objectName: "Settings"
anchors.fill: parent
contentWidth: settingsColumn1.width > settingsColumn2.width ? settingsColumn1.width : settingsColumn2.width
contentHeight: settingsColumn1.height > settingsColumn2.height ? settingsColumn1.height : settingsColumn2.height
ScrollBar.vertical: ScrollBar {
parent: settingsPage.parent
anchors {
top: settingsPage.top
left: settingsPage.right
bottom: settingsPage.bottom
leftMargin: -10
}
}
StreamingPreferences { StreamingPreferences {
id: prefs id: prefs
@@ -58,16 +73,14 @@ ScrollView {
text: qsTr("Resolution and FPS") text: qsTr("Resolution and FPS")
font.pointSize: 12 font.pointSize: 12
wrapMode: Text.Wrap wrapMode: Text.Wrap
color: "white"
} }
Label { Label {
width: parent.width width: parent.width
id: resFPSdesc id: resFPSdesc
text: qsTr("Setting values too high for your PC may cause lag, stuttering, or errors.") text: qsTr("Setting values too high for your PC or network connection may cause lag, stuttering, or errors.")
font.pointSize: 9 font.pointSize: 9
wrapMode: Text.Wrap wrapMode: Text.Wrap
color: "white"
} }
Row { Row {
@@ -211,7 +224,16 @@ ScrollView {
// Use 64 as the cutoff for adding a separate option to // Use 64 as the cutoff for adding a separate option to
// handle wonky displays that report just over 60 Hz. // handle wonky displays that report just over 60 Hz.
if (max_fps > 64) { if (max_fps > 64) {
fpsListModel.append({"text": max_fps+" FPS", "video_fps": ""+max_fps}) // Mark any FPS value greater than 120 as unsupported
if (prefs.unsupportedFps && max_fps > 120) {
fpsListModel.append({"text": max_fps+" FPS (Unsupported)", "video_fps": ""+max_fps})
}
else if (max_fps > 120) {
fpsListModel.append({"text": "120 FPS", "video_fps": "120"})
}
else {
fpsListModel.append({"text": max_fps+" FPS", "video_fps": ""+max_fps})
}
} }
// Add unsupported FPS values that come after the display max FPS // Add unsupported FPS values that come after the display max FPS
@@ -273,21 +295,18 @@ ScrollView {
text: qsTr("Video bitrate: ") text: qsTr("Video bitrate: ")
font.pointSize: 12 font.pointSize: 12
wrapMode: Text.Wrap wrapMode: Text.Wrap
color: "white"
} }
Label { Label {
width: parent.width width: parent.width
id: bitrateDesc id: bitrateDesc
text: qsTr("Lower bitrate to reduce lag and stuttering. Raise bitrate to increase image quality.") text: qsTr("Lower the bitrate on slower connections. Raise the bitrate to increase image quality.")
font.pointSize: 9 font.pointSize: 9
wrapMode: Text.Wrap wrapMode: Text.Wrap
color: "white"
} }
Slider { Slider {
id: slider id: slider
wheelEnabled: true
value: prefs.bitrateKbps value: prefs.bitrateKbps
@@ -310,7 +329,6 @@ ScrollView {
text: qsTr("Display mode") text: qsTr("Display mode")
font.pointSize: 12 font.pointSize: 12
wrapMode: Text.Wrap wrapMode: Text.Wrap
color: "white"
} }
AutoResizingComboBox { AutoResizingComboBox {
@@ -329,6 +347,7 @@ ScrollView {
} }
id: windowModeComboBox id: windowModeComboBox
hoverEnabled: true
textRole: "text" textRole: "text"
model: ListModel { model: ListModel {
id: windowModeListModel id: windowModeListModel
@@ -348,16 +367,43 @@ ScrollView {
onActivated: { onActivated: {
prefs.windowMode = windowModeListModel.get(currentIndex).val prefs.windowMode = windowModeListModel.get(currentIndex).val
} }
ToolTip.delay: 1000
ToolTip.timeout: 5000
ToolTip.visible: hovered
ToolTip.text: "Full-screen generally provides the best performance, but borderless windowed may work better with features like macOS Spaces, Alt+Tab, screenshot tools, on-screen overlays, etc."
} }
CheckBox { CheckBox {
id: vsyncCheck id: vsyncCheck
text: "<font color=\"white\">Enable V-Sync</font>" hoverEnabled: true
text: "V-Sync"
font.pointSize: 12 font.pointSize: 12
checked: prefs.enableVsync checked: prefs.enableVsync
onCheckedChanged: { onCheckedChanged: {
prefs.enableVsync = checked prefs.enableVsync = checked
} }
ToolTip.delay: 1000
ToolTip.timeout: 5000
ToolTip.visible: hovered
ToolTip.text: "Disabling V-Sync allows sub-frame rendering latency, but it can display visible tearing"
}
CheckBox {
id: framePacingCheck
hoverEnabled: true
text: "Frame pacing"
font.pointSize: 12
enabled: prefs.enableVsync
checked: prefs.enableVsync && prefs.framePacing
onCheckedChanged: {
prefs.framePacing = checked
}
ToolTip.delay: 1000
ToolTip.timeout: 5000
ToolTip.visible: hovered
ToolTip.text: "Frame pacing reduces micro-stutter by delaying frames that come in too early"
} }
} }
} }
@@ -380,7 +426,6 @@ ScrollView {
text: qsTr("Audio configuration") text: qsTr("Audio configuration")
font.pointSize: 12 font.pointSize: 12
wrapMode: Text.Wrap wrapMode: Text.Wrap
color: "white"
} }
AutoResizingComboBox { AutoResizingComboBox {
@@ -419,6 +464,29 @@ ScrollView {
} }
} }
GroupBox {
id: uiSettingsGroupBox
width: (parent.width - 2 * parent.padding)
padding: 12
title: "<font color=\"skyblue\">UI Settings</font>"
font.pointSize: 12
Column {
anchors.fill: parent
spacing: 5
CheckBox {
id: startMaximizedCheck
text: "Maximize Moonlight window on startup"
font.pointSize: 12
checked: !prefs.startWindowed
onCheckedChanged: {
prefs.startWindowed = !checked
}
}
}
}
} }
Column { Column {
@@ -434,28 +502,40 @@ ScrollView {
title: "<font color=\"skyblue\">Input Settings</font>" title: "<font color=\"skyblue\">Input Settings</font>"
font.pointSize: 12 font.pointSize: 12
Row { Column {
anchors.fill: parent anchors.fill: parent
spacing: 5 spacing: 5
CheckBox { CheckBox {
id: multiControllerCheck id: singleControllerCheck
text: "<font color=\"white\">Multiple controller support</font>" text: "Force gamepad #1 always present"
font.pointSize: 12 font.pointSize: 12
checked: prefs.multiController checked: !prefs.multiController
onCheckedChanged: { onCheckedChanged: {
prefs.multiController = checked prefs.multiController = !checked
} }
ToolTip.delay: 1000
ToolTip.timeout: 5000
ToolTip.visible: hovered
ToolTip.text: "Forces a single gamepad to always stay connected to the host, even if no gamepads are actually connected to this PC.\n" +
"Only enable this option when streaming a game that doesn't support gamepads being connected after startup."
} }
CheckBox { CheckBox {
id: mouseAccelerationCheck id: rawInputCheck
text: "<font color=\"white\">Enable mouse acceleration</font>" hoverEnabled: true
text: "Raw mouse input"
font.pointSize: 12 font.pointSize: 12
checked: prefs.mouseAcceleration checked: !prefs.mouseAcceleration
onCheckedChanged: { onCheckedChanged: {
prefs.mouseAcceleration = checked prefs.mouseAcceleration = !checked
} }
ToolTip.delay: 1000
ToolTip.timeout: 3000
ToolTip.visible: hovered
ToolTip.text: "When checked, mouse input is not accelerated or scaled by the OS before passing to Moonlight"
} }
} }
} }
@@ -467,15 +547,13 @@ ScrollView {
title: "<font color=\"skyblue\">Host Settings</font>" title: "<font color=\"skyblue\">Host Settings</font>"
font.pointSize: 12 font.pointSize: 12
Row { Column {
anchors.fill: parent anchors.fill: parent
spacing: 5 spacing: 5
CheckBox { CheckBox {
id: optimizeGameSettingsCheck id: optimizeGameSettingsCheck
text: "<font color=\"white\">Optimize game settings</font>" text: "Optimize game settings"
// HACK: Match width of the other checkbox to make the UI not look bad
width: multiControllerCheck.width
font.pointSize: 12 font.pointSize: 12
checked: prefs.gameOptimizations checked: prefs.gameOptimizations
onCheckedChanged: { onCheckedChanged: {
@@ -485,7 +563,7 @@ ScrollView {
CheckBox { CheckBox {
id: audioPcCheck id: audioPcCheck
text: "<font color=\"white\">Play audio on host PC</font>" text: "Play audio on host PC"
font.pointSize: 12 font.pointSize: 12
checked: prefs.playAudioOnHost checked: prefs.playAudioOnHost
onCheckedChanged: { onCheckedChanged: {
@@ -512,7 +590,6 @@ ScrollView {
text: qsTr("Video decoder") text: qsTr("Video decoder")
font.pointSize: 12 font.pointSize: 12
wrapMode: Text.Wrap wrapMode: Text.Wrap
color: "white"
} }
AutoResizingComboBox { AutoResizingComboBox {
@@ -559,7 +636,6 @@ ScrollView {
text: qsTr("Video codec") text: qsTr("Video codec")
font.pointSize: 12 font.pointSize: 12
wrapMode: Text.Wrap wrapMode: Text.Wrap
color: "white"
} }
AutoResizingComboBox { AutoResizingComboBox {
@@ -609,7 +685,7 @@ ScrollView {
CheckBox { CheckBox {
id: unlockUnsupportedFps id: unlockUnsupportedFps
text: "<font color=\"white\">Unlock unsupported FPS options</font>" text: "Unlock unsupported FPS options"
font.pointSize: 12 font.pointSize: 12
checked: prefs.unsupportedFps checked: prefs.unsupportedFps
onCheckedChanged: { onCheckedChanged: {
@@ -623,7 +699,7 @@ ScrollView {
CheckBox { CheckBox {
id: enableMdns id: enableMdns
text: "<font color=\"white\">Automatically find PCs on the local network (Recommended)</font>" text: "Automatically find PCs on the local network (Recommended)"
font.pointSize: 12 font.pointSize: 12
checked: prefs.enableMdns checked: prefs.enableMdns
onCheckedChanged: { onCheckedChanged: {
@@ -640,6 +716,16 @@ ScrollView {
} }
} }
} }
CheckBox {
id: quitAppAfter
text: "Quit app after quitting session"
font.pointSize: 12
checked: prefs.quitAppAfter
onCheckedChanged: {
prefs.quitAppAfter = checked
}
}
} }
} }
} }
+70 -22
View File
@@ -3,6 +3,8 @@ import QtQuick.Controls 2.2
import QtQuick.Dialogs 1.2 import QtQuick.Dialogs 1.2
import QtQuick.Window 2.2 import QtQuick.Window 2.2
import ComputerManager 1.0
import SdlGamepadKeyNavigation 1.0
import Session 1.0 import Session 1.0
Item { Item {
@@ -58,40 +60,77 @@ Item {
toast.visible = true toast.visible = true
} }
function quitStarting()
{
// Avoid the push transition animation
var component = Qt.createComponent("QuitSegue.qml")
stackView.replace(stackView.currentItem, component.createObject(stackView, {"appName": appName}), StackView.Immediate)
// Show the Qt window again to show quit segue
window.visible = true
}
function sessionFinished()
{
if (quitAfter) {
if (errorDialog.text) {
// Quit when the error dialog is acknowledged
errorDialog.open()
}
else {
// Quit immediately
Qt.quit()
}
} else {
// Exit this view
stackView.pop()
// Show the Qt window again after streaming
window.visible = true
// Display any launch errors. We do this after
// the Qt UI is visible again to prevent losing
// focus on the dialog which would impact gamepad
// users.
if (errorDialog.text) {
errorDialog.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
}
onVisibleChanged: { onVisibleChanged: {
if (visible) { if (visible) {
// Hide the toolbar before we start loading // Hide the toolbar before we start loading
toolBar.visible = false toolBar.visible = false
// 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 ?
"Tip: Press Start+Select+L1+R1 to disconnect your session" :
"Tip: Press Ctrl+Alt+Shift+Q to disconnect your session"
// Hook up our signals // Hook up our signals
session.stageStarting.connect(stageStarting) session.stageStarting.connect(stageStarting)
session.stageFailed.connect(stageFailed) session.stageFailed.connect(stageFailed)
session.connectionStarted.connect(connectionStarted) session.connectionStarted.connect(connectionStarted)
session.displayLaunchError.connect(displayLaunchError) session.displayLaunchError.connect(displayLaunchError)
session.displayLaunchWarning.connect(displayLaunchWarning) session.displayLaunchWarning.connect(displayLaunchWarning)
session.quitStarting.connect(quitStarting)
session.sessionFinished.connect(sessionFinished)
// Run the streaming session to completion // Run the streaming session to completion
session.exec(Screen.virtualX, Screen.virtualY) session.exec(Screen.virtualX, Screen.virtualY)
if (quitAfter) {
Qt.quit()
} else {
// Show the Qt window again after streaming
window.visible = true
// Exit this view
stackView.pop()
// Display any launch errors. We do this after
// the Qt UI is visible again to prevent losing
// focus on the dialog which would impact gamepad
// users.
if (errorDialog.text) {
errorDialog.open()
}
}
} }
else { else if (!quitAfter) {
// Show the toolbar again when we become hidden // Show the toolbar again when we become hidden
toolBar.visible = true toolBar.visible = true
} }
@@ -113,12 +152,11 @@ Item {
verticalAlignment: Text.AlignVCenter verticalAlignment: Text.AlignVCenter
wrapMode: Text.Wrap wrapMode: Text.Wrap
color: "white"
} }
} }
Label { Label {
text: "Tip: Press Ctrl+Alt+Shift+Q to disconnect your session" id: hintText
anchors.bottom: parent.bottom anchors.bottom: parent.bottom
anchors.bottomMargin: 50 anchors.bottomMargin: 50
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
@@ -126,7 +164,6 @@ Item {
verticalAlignment: Text.AlignVCenter verticalAlignment: Text.AlignVCenter
wrapMode: Text.Wrap wrapMode: Text.Wrap
color: "white"
} }
MessageDialog { MessageDialog {
@@ -134,8 +171,19 @@ Item {
modality:Qt.WindowModal modality:Qt.WindowModal
icon: StandardIcon.Critical icon: StandardIcon.Critical
standardButtons: StandardButton.Ok | StandardButton.Help standardButtons: StandardButton.Ok | StandardButton.Help
onAccepted: {
if (quitAfter) {
Qt.quit()
}
}
onHelp: { onHelp: {
Qt.openUrlExternally("https://github.com/moonlight-stream/moonlight-docs/wiki/Troubleshooting"); Qt.openUrlExternally("https://github.com/moonlight-stream/moonlight-docs/wiki/Troubleshooting");
if (quitAfter) {
Qt.quit()
}
} }
} }
} }
+80 -28
View File
@@ -4,8 +4,6 @@ import QtQuick.Dialogs 1.2
import QtQuick.Layouts 1.3 import QtQuick.Layouts 1.3
import QtQuick.Window 2.2 import QtQuick.Window 2.2
import QtQuick.Controls.Material 2.1
import ComputerManager 1.0 import ComputerManager 1.0
import AutoUpdateChecker 1.0 import AutoUpdateChecker 1.0
import StreamingPreferences 1.0 import StreamingPreferences 1.0
@@ -18,14 +16,7 @@ ApplicationWindow {
width: 1280 width: 1280
height: 600 height: 600
// Maximize the window by default when the stream is configured visibility: prefs.startWindowed ? "Windowed" : "Maximized"
// for full-screen or borderless windowed. This is ideal for TV
// setups where the user doesn't want a tiny window in the middle
// of their screen when starting Moonlight.
visibility: prefs.windowMode != StreamingPreferences.WM_WINDOWED ? "Maximized" : "Windowed"
Material.theme: Material.Dark
Material.accent: Material.Purple
StreamingPreferences { StreamingPreferences {
id: prefs id: prefs
@@ -48,12 +39,18 @@ ApplicationWindow {
if (depth > 1) { if (depth > 1) {
stackView.pop() stackView.pop()
} }
else {
quitConfirmationDialog.open()
}
} }
Keys.onBackPressed: { Keys.onBackPressed: {
if (depth > 1) { if (depth > 1) {
stackView.pop() stackView.pop()
} }
else {
quitConfirmationDialog.open()
}
} }
Keys.onMenuPressed: { Keys.onMenuPressed: {
@@ -68,21 +65,59 @@ ApplicationWindow {
} }
} }
onVisibilityChanged: { // This timer keeps us polling for 5 minutes of inactivity
// We don't want to just use 'active' here because that will stop polling if // to allow the user to work with Moonlight on a second display
// we lose focus, which might be overzealous for users with multiple screens // while dealing with configuration issues. This will ensure
// where we may be clearly visible on the other display. Ideally we'll poll // machines come online even if the input focus isn't on Moonlight.
// only if the window is visible to the user (not if obscured by other windows), Timer {
// but it seems difficult to do this portably. id: inactivityTimer
var shouldPoll = visibility !== Window.Minimized && visibility !== Window.Hidden interval: 5 * 60000
onTriggered: {
if (shouldPoll && !pollingActive) { if (!active && pollingActive) {
ComputerManager.startPolling() ComputerManager.stopPollingAsync()
pollingActive = true pollingActive = false
}
} }
else if (!shouldPoll && pollingActive) { }
ComputerManager.stopPollingAsync()
pollingActive = false onVisibleChanged: {
// When we become invisible while streaming is going on,
// stop polling immediately.
if (!visible) {
inactivityTimer.stop()
if (pollingActive) {
ComputerManager.stopPollingAsync()
pollingActive = false
}
}
else if (active) {
// When we become visible and active again, start polling
inactivityTimer.stop()
// Restart polling if it was stopped
if (!pollingActive) {
ComputerManager.startPolling()
pollingActive = true
}
}
}
onActiveChanged: {
if (active) {
// Stop the inactivity timer
inactivityTimer.stop()
// Restart polling if it was stopped
if (!pollingActive) {
ComputerManager.startPolling()
pollingActive = true
}
}
else {
// Start the inactivity timer to stop polling
// if focus does not return within a few minutes.
inactivityTimer.restart()
} }
} }
@@ -97,6 +132,11 @@ ApplicationWindow {
// Doing it earlier can lead to the dialog appearing behind // Doing it earlier can lead to the dialog appearing behind
// the window or otherwise without input focus. // the window or otherwise without input focus.
if (!initialized) { if (!initialized) {
// Set initialized before calling anything else, because
// pumping the event loop can cause us to get another
// onAfterRendering call and potentially reenter this code.
initialized = true;
if (prefs.isRunningWayland()) { if (prefs.isRunningWayland()) {
waylandDialog.open() waylandDialog.open()
} }
@@ -112,8 +152,6 @@ ApplicationWindow {
unmappedGamepadDialog.unmappedGamepads = unmappedGamepads unmappedGamepadDialog.unmappedGamepads = unmappedGamepads
unmappedGamepadDialog.open() unmappedGamepadDialog.open()
} }
initialized = true;
} }
} }
@@ -192,7 +230,6 @@ ApplicationWindow {
ToolTip.delay: 1000 ToolTip.delay: 1000
ToolTip.timeout: 3000 ToolTip.timeout: 3000
ToolTip.visible: hovered ToolTip.visible: hovered
ToolTip.text: "Update available for Moonlight"
// Invisible until we get a callback notifying us that // Invisible until we get a callback notifying us that
// an update is available // an update is available
@@ -200,8 +237,9 @@ ApplicationWindow {
onClicked: Qt.openUrlExternally(browserUrl); onClicked: Qt.openUrlExternally(browserUrl);
function updateAvailable(url) function updateAvailable(version, url)
{ {
ToolTip.text = "Update available for Moonlight: Version " + version
updateButton.browserUrl = url updateButton.browserUrl = url
updateButton.visible = true updateButton.visible = true
} }
@@ -353,4 +391,18 @@ ApplicationWindow {
Qt.openUrlExternally("https://github.com/moonlight-stream/moonlight-docs/wiki/Gamepad-Mapping"); Qt.openUrlExternally("https://github.com/moonlight-stream/moonlight-docs/wiki/Gamepad-Mapping");
} }
} }
// This dialog appears when quitting via keyboard or gamepad button
MessageDialog {
id: quitConfirmationDialog
modality:Qt.WindowModal
icon: StandardIcon.Warning
standardButtons: StandardButton.Yes | StandardButton.No
text: "Are you sure you want to quit?"
onYes: Qt.quit()
// For keyboard/gamepad navigation
onAccepted: Qt.quit()
}
} }
+25
View File
@@ -244,3 +244,28 @@ void SdlGamepadKeyNavigation::setSettingsMode(bool settingsMode)
{ {
m_SettingsMode = settingsMode; m_SettingsMode = settingsMode;
} }
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();
int count = 0;
for (int i = 0; i < SDL_NumJoysticks(); i++) {
if (SDL_IsGameController(i)) {
count++;
}
}
SDL_QuitSubSystem(SDL_INIT_GAMECONTROLLER);
return count;
}
+2
View File
@@ -20,6 +20,8 @@ public:
Q_INVOKABLE void setSettingsMode(bool settingsMode); Q_INVOKABLE void setSettingsMode(bool settingsMode);
Q_INVOKABLE int getConnectedGamepads();
private: private:
void sendKey(QEvent::Type type, Qt::Key key, Qt::KeyboardModifiers modifiers = Qt::NoModifier); void sendKey(QEvent::Type type, Qt::Key key, Qt::KeyboardModifiers modifiers = Qt::NoModifier);
+68 -3
View File
@@ -5,6 +5,7 @@
#include <QQuickStyle> #include <QQuickStyle>
#include <QMutex> #include <QMutex>
#include <QtDebug> #include <QtDebug>
#include <QNetworkProxyFactory>
// Don't let SDL hook our main function, since Qt is already // Don't let SDL hook our main function, since Qt is already
// doing the same thing. This needs to be before any headers // doing the same thing. This needs to be before any headers
@@ -16,6 +17,11 @@
#include "streaming/video/ffmpeg.h" #include "streaming/video/ffmpeg.h"
#endif #endif
#ifdef Q_OS_WIN32
#include "antihookingprotection.h"
#endif
#include "cli/quitstream.h"
#include "cli/startstream.h" #include "cli/startstream.h"
#include "cli/commandlineparser.h" #include "cli/commandlineparser.h"
#include "path.h" #include "path.h"
@@ -130,6 +136,14 @@ void qtLogToDiskHandler(QtMsgType type, const QMessageLogContext&, const QString
break; break;
} }
// HACK: Avoid printing this internal Qt warning which seems to print every time we
// start our event loop for the QNetworkReply. I have found this warning in many
// unrelated bug reports, but none actually tackle this warning itself. It seems to be
// new in Qt 5.12.
if (msg.startsWith("QNetworkReplyHttpImplPrivate::_q_startOperation was called more than once")) {
return;
}
QTime logTime = QTime::fromMSecsSinceStartOfDay(s_LoggerTime.elapsed()); QTime logTime = QTime::fromMSecsSinceStartOfDay(s_LoggerTime.elapsed());
QString txt = QString("%1 - Qt %2: %3\n").arg(logTime.toString()).arg(typeTxt).arg(msg); QString txt = QString("%1 - Qt %2: %3\n").arg(logTime.toString()).arg(typeTxt).arg(msg);
@@ -217,6 +231,9 @@ LONG WINAPI UnhandledExceptionHandler(struct _EXCEPTION_POINTERS *ExceptionInfo)
int main(int argc, char *argv[]) int main(int argc, char *argv[])
{ {
// Set the app version for the QCommandLineParser's showVersion() command
QCoreApplication::setApplicationVersion(VERSION_STR);
// Set these here to allow us to use the default QSettings constructor. // Set these here to allow us to use the default QSettings constructor.
// These also ensure that our cache directory is named correctly. As such, // These also ensure that our cache directory is named correctly. As such,
// it is critical that these be called before Path::initialize(). // it is critical that these be called before Path::initialize().
@@ -263,11 +280,17 @@ int main(int argc, char *argv[])
SetUnhandledExceptionFilter(UnhandledExceptionHandler); SetUnhandledExceptionFilter(UnhandledExceptionHandler);
#endif #endif
#ifdef Q_OS_WIN32
// Force AntiHooking.dll to be statically imported and loaded
// by ntdll by calling a dummy function.
AntiHookingDummyImport();
#endif
QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling); QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
// This avoids using the default keychain for SSL, which may cause // This avoids using the default keychain for SSL, which may cause
// password prompts on macOS. // password prompts on macOS.
qputenv("QT_SSL_USE_TEMPORARY_KEYCHAIN", QByteArray("1")); qputenv("QT_SSL_USE_TEMPORARY_KEYCHAIN", "1");
#ifdef Q_OS_WIN32 #ifdef Q_OS_WIN32
if (!qEnvironmentVariableIsSet("QT_OPENGL")) { if (!qEnvironmentVariableIsSet("QT_OPENGL")) {
@@ -278,9 +301,20 @@ int main(int argc, char *argv[])
} }
#endif #endif
// We don't want system proxies to apply to us
QNetworkProxyFactory::setUseSystemConfiguration(false);
// Clear any default application proxy
QNetworkProxy noProxy(QNetworkProxy::NoProxy);
QNetworkProxy::setApplicationProxy(noProxy);
// Register custom metatypes for use in signals // Register custom metatypes for use in signals
qRegisterMetaType<NvApp>("NvApp"); qRegisterMetaType<NvApp>("NvApp");
// Disable desktop settings awareness due to issues with QGnomePlatform
// breaking our dialog rendering on Linux.
QGuiApplication::setDesktopSettingsAware(false);
QGuiApplication app(argc, argv); QGuiApplication app(argc, argv);
app.setWindowIcon(QIcon(":/res/moonlight.svg")); app.setWindowIcon(QIcon(":/res/moonlight.svg"));
@@ -302,7 +336,25 @@ int main(int argc, char *argv[])
return new AutoUpdateChecker(); return new AutoUpdateChecker();
}); });
QQuickStyle::setStyle("Material"); // Use the dense material dark theme by default
if (!qEnvironmentVariableIsSet("QT_QUICK_CONTROLS_STYLE")) {
qputenv("QT_QUICK_CONTROLS_STYLE", "Material");
}
if (!qEnvironmentVariableIsSet("QT_QUICK_CONTROLS_MATERIAL_THEME")) {
qputenv("QT_QUICK_CONTROLS_MATERIAL_THEME", "Dark");
}
if (!qEnvironmentVariableIsSet("QT_QUICK_CONTROLS_MATERIAL_ACCENT")) {
qputenv("QT_QUICK_CONTROLS_MATERIAL_ACCENT", "Purple");
}
if (!qEnvironmentVariableIsSet("QT_QUICK_CONTROLS_MATERIAL_VARIANT")) {
qputenv("QT_QUICK_CONTROLS_MATERIAL_VARIANT", "Dense");
}
if (!qEnvironmentVariableIsSet("QT_QUICK_CONTROLS_UNIVERSAL_THEME")) {
qputenv("QT_QUICK_CONTROLS_UNIVERSAL_THEME", "Dark");
}
if (!qEnvironmentVariableIsSet("QT_QUICK_CONTROLS_1_STYLE")) {
qputenv("QT_QUICK_CONTROLS_1_STYLE", "Base");
}
QQmlApplicationEngine engine; QQmlApplicationEngine engine;
QString initialView; QString initialView;
@@ -324,6 +376,15 @@ int main(int argc, char *argv[])
engine.rootContext()->setContextProperty("launcher", launcher); engine.rootContext()->setContextProperty("launcher", launcher);
break; break;
} }
case GlobalCommandLineParser::QuitRequested:
{
initialView = "CliQuitStreamSegue.qml";
QuitCommandLineParser quitParser;
quitParser.parse(app.arguments());
auto launcher = new CliQuitStream::Launcher(quitParser.getHost(), &app);
engine.rootContext()->setContextProperty("launcher", launcher);
break;
}
} }
engine.rootContext()->setContextProperty("initialView", initialView); engine.rootContext()->setContextProperty("initialView", initialView);
@@ -334,7 +395,7 @@ int main(int argc, char *argv[])
return -1; return -1;
SDL_SetMainReady(); SDL_SetMainReady();
if (SDL_Init(0) != 0) { if (SDL_Init(SDL_INIT_TIMER) != 0) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"SDL_Init() failed: %s", "SDL_Init() failed: %s",
SDL_GetError()); SDL_GetError());
@@ -351,5 +412,9 @@ int main(int argc, char *argv[])
int err = app.exec(); int err = app.exec();
// Give worker tasks time to properly exit. Fixes PendingQuitTask
// sometimes freezing and blocking process exit.
QThreadPool::globalInstance()->waitForDone(30000);
return err; return err;
} }
Binary file not shown.
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 345 KiB

After

Width:  |  Height:  |  Size: 328 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.4 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

+6 -7
View File
@@ -8,7 +8,6 @@
QString Path::s_LogDir; QString Path::s_LogDir;
QString Path::s_BoxArtCacheDir; QString Path::s_BoxArtCacheDir;
const QString Path::k_GameControllerMappingFile("gamecontrollerdb.txt");
QString Path::getLogDir() QString Path::getLogDir()
{ {
@@ -22,24 +21,24 @@ QString Path::getBoxArtCacheDir()
return s_BoxArtCacheDir; return s_BoxArtCacheDir;
} }
QString Path::getGamepadMappingFile() QString Path::getDataFilePath(QString fileName)
{ {
QString candidatePath; QString candidatePath;
// Check the current directory first // Check the current directory first
candidatePath = QDir(QDir::currentPath()).absoluteFilePath(k_GameControllerMappingFile); candidatePath = QDir(QDir::currentPath()).absoluteFilePath(fileName);
if (QFile::exists(candidatePath)) { if (QFile::exists(candidatePath)) {
return candidatePath; return candidatePath;
} }
// Now check the data directories (for Linux, in particular) // Now check the data directories (for Linux, in particular)
candidatePath = QStandardPaths::locate(QStandardPaths::DataLocation, k_GameControllerMappingFile); candidatePath = QStandardPaths::locate(QStandardPaths::DataLocation, fileName);
if (!candidatePath.isEmpty() && QFile::exists(candidatePath)) { if (!candidatePath.isEmpty() && QFile::exists(candidatePath)) {
return candidatePath; return candidatePath;
} }
// Now try the directory of our app installation (for Windows, if current dir doesn't find it) // Now try the directory of our app installation (for Windows, if current dir doesn't find it)
candidatePath = QDir(QCoreApplication::applicationDirPath()).absoluteFilePath(k_GameControllerMappingFile); candidatePath = QDir(QCoreApplication::applicationDirPath()).absoluteFilePath(fileName);
if (QFile::exists(candidatePath)) { if (QFile::exists(candidatePath)) {
return candidatePath; return candidatePath;
} }
@@ -48,8 +47,8 @@ QString Path::getGamepadMappingFile()
QDir dir = QDir(QCoreApplication::applicationDirPath()); QDir dir = QDir(QCoreApplication::applicationDirPath());
dir.cdUp(); dir.cdUp();
dir.cd("Resources"); dir.cd("Resources");
dir.filePath(k_GameControllerMappingFile); dir.filePath(fileName);
candidatePath = dir.absoluteFilePath(k_GameControllerMappingFile); candidatePath = dir.absoluteFilePath(fileName);
if (QFile::exists(candidatePath)) { if (QFile::exists(candidatePath)) {
return candidatePath; return candidatePath;
} }
+1 -2
View File
@@ -9,12 +9,11 @@ public:
static QString getBoxArtCacheDir(); static QString getBoxArtCacheDir();
static QString getGamepadMappingFile(); static QString getDataFilePath(QString fileName);
static void initialize(bool portable); static void initialize(bool portable);
private: private:
static QString s_LogDir; static QString s_LogDir;
static QString s_BoxArtCacheDir; static QString s_BoxArtCacheDir;
static const QString k_GameControllerMappingFile;
}; };
+2
View File
@@ -10,7 +10,9 @@
<file>gui/NavigableToolButton.qml</file> <file>gui/NavigableToolButton.qml</file>
<file>gui/NavigableItemDelegate.qml</file> <file>gui/NavigableItemDelegate.qml</file>
<file>gui/NavigableMenuItem.qml</file> <file>gui/NavigableMenuItem.qml</file>
<file>gui/CliQuitStreamSegue.qml</file>
<file>gui/CliStartStreamSegue.qml</file> <file>gui/CliStartStreamSegue.qml</file>
<file>gui/AutoResizingComboBox.qml</file> <file>gui/AutoResizingComboBox.qml</file>
<file>gui/NavigableMenu.qml</file>
</qresource> </qresource>
</RCC> </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="#fff"><path d="M12 2C6.47 2 2 6.47 2 12s4.47 10 10 10 10-4.47 10-10S17.53 2 12 2zm5 13.59L15.59 17 12 13.41 8.41 17 7 15.59 10.59 12 7 8.41 8.41 7 12 10.59 15.59 7 17 8.41 13.41 12 17 15.59z"/><path d="M0 0h24v24H0z" fill="none"/></svg>

After

Width:  |  Height:  |  Size: 325 B

+1 -69
View File
@@ -1,69 +1 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?> <?xml version="1.0" standalone="no"?><!-- Generator: Gravit.io --><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" style="isolation:isolate" viewBox="0 0 256 256" width="256" height="256"><g><circle vector-effect="non-scaling-stroke" cx="128" cy="128" r="128" fill="rgb(86,92,100)"/><circle vector-effect="non-scaling-stroke" cx="128.00000000000003" cy="128.00000000000003" r="96" fill="rgb(255,255,255)"/><path d=" M 137.66 132 L 224 132 L 224 124 L 137.66 124 L 198.71 62.95 L 193.05 57.29 L 132 118.34 L 132 32 L 124 32 L 124 118.34 L 62.95 57.29 L 57.29 62.95 L 118.34 124 L 32 124 L 32 132 L 118.34 132 L 57.29 193.05 L 62.95 198.71 L 124 137.66 L 124 224 L 132 224 L 132 137.66 L 193.05 198.71 L 198.71 193.05 L 137.66 132 Z " fill-rule="evenodd" fill="rgb(86,92,100)"/></g></svg>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
id="svg3761"
viewBox="0 0 680 680"
height="680"
width="680"
version="1.0">
<metadata
id="metadata3767">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<defs
id="defs3765" />
<g
id="g3759"
transform="translate(-135,491) scale(0.100000,-0.100000)">
<circle
id="circle3741"
fill="#565C64"
r="3400"
cy="1505"
cx="4755" />
<path
id="path3743"
style="fill:#ffffff"
d="M4550 4230 c-582 -48 -1123 -271 -1568 -646 l-84 -70 869 -869 c477 -477 871 -868 876 -868 4 0 6 552 5 1228 l-3 1227 -25 1 c-14 0 -45 -1 -70 -3z" />
<path
id="path3745"
style="fill:#ffffff"
d="M4867 4230 c-10 -9 -9 -2453 1 -2453 4 0 398 391 875 868 l869 869 -84 70 c-344 290 -749 492 -1178 587 -166 36 -469 74 -483 59z" />
<path
id="path3747"
style="fill:#ffffff"
d="M2673 3275 c-241 -285 -412 -595 -526 -953 -63 -196 -111 -458 -119 -640 l-3 -70 1228 -3 c675 -1 1227 1 1227 5 0 5 -391 399 -868 876 l-869 869 -70 -84z" />
<path
id="path3749"
style="fill:#ffffff"
d="M5898 2490 c-478 -477 -868 -871 -868 -876 0 -4 552 -6 1228 -5 l1227 3 -3 70 c-14 326 -124 729 -287 1055 -91 182 -259 430 -387 573 l-43 48 -867 -868z" />
<path
id="path3751"
style="fill:#ffffff"
d="M2026 1388 c-7 -11 10 -198 30 -321 76 -486 291 -952 617 -1338 l70 -84 869 869 c477 477 868 871 868 875 0 11 -2448 10 -2454 -1z" />
<path
id="path3753"
style="fill:#ffffff"
d="M5030 1390 c0 -5 391 -399 868 -876 l869 -869 70 84 c241 285 412 595 526 953 63 196 111 458 119 640 l3 70 -1227 3 c-676 1 -1228 -1 -1228 -5z" />
<path
id="path3755"
style="fill:#ffffff"
d="M3767 359 l-869 -869 84 -70 c285 -241 595 -412 953 -526 196 -63 458 -111 640 -119 l70 -3 3 1228 c1 675 -1 1227 -5 1227 -5 0 -399 -391 -876 -868z" />
<path
id="path3757"
style="fill:#ffffff"
d="M4862 0 l3 -1228 70 3 c182 8 444 56 640 119 358 114 668 285 953 526 l84 70 -869 869 c-477 477 -871 868 -876 868 -4 0 -6 -552 -5 -1227z" />
</g>
</svg>

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 820 B

+1 -1
View File
@@ -1,3 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="24" height="24" viewBox="0 0 24 24" fill="#0BF200"> <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="24" height="24" viewBox="0 0 24 24" fill="#FF0000">
<path d="M21,10.12H14.22L16.96,7.3C14.23,4.6 9.81,4.5 7.08,7.2C4.35,9.91 4.35,14.28 7.08,17C9.81,19.7 14.23,19.7 16.96,17C18.32,15.65 19,14.08 19,12.1H21C21,14.08 20.12,16.65 18.36,18.39C14.85,21.87 9.15,21.87 5.64,18.39C2.14,14.92 2.11,9.28 5.62,5.81C9.13,2.34 14.76,2.34 18.27,5.81L21,3V10.12M12.5,8V12.25L16,14.33L15.28,15.54L11,13V8H12.5Z" /> <path d="M21,10.12H14.22L16.96,7.3C14.23,4.6 9.81,4.5 7.08,7.2C4.35,9.91 4.35,14.28 7.08,17C9.81,19.7 14.23,19.7 16.96,17C18.32,15.65 19,14.08 19,12.1H21C21,14.08 20.12,16.65 18.36,18.39C14.85,21.87 9.15,21.87 5.64,18.39C2.14,14.92 2.11,9.28 5.62,5.81C9.13,2.34 14.76,2.34 18.27,5.81L21,3V10.12M12.5,8V12.25L16,14.33L15.28,15.54L11,13V8H12.5Z" />
</svg> </svg>

Before

Width:  |  Height:  |  Size: 499 B

After

Width:  |  Height:  |  Size: 499 B

+1
View File
@@ -8,6 +8,7 @@
<file>res/baseline-lock-24px.svg</file> <file>res/baseline-lock-24px.svg</file>
<file>res/baseline-play_circle_filled_white-48px.svg</file> <file>res/baseline-play_circle_filled_white-48px.svg</file>
<file>res/baseline-warning-24px.svg</file> <file>res/baseline-warning-24px.svg</file>
<file>res/baseline-cancel-24px.svg</file>
<file>res/no_app_image.png</file> <file>res/no_app_image.png</file>
<file>res/settings.svg</file> <file>res/settings.svg</file>
<file>res/arrow_left.svg</file> <file>res/arrow_left.svg</file>
+1 -1
View File
@@ -54,7 +54,7 @@ void MappingManager::save()
void MappingManager::applyMappings() void MappingManager::applyMappings()
{ {
QString mappingFile = Path::getGamepadMappingFile(); QString mappingFile = Path::getDataFilePath("gamecontrollerdb.txt");
if (!mappingFile.isEmpty()) { if (!mappingFile.isEmpty()) {
std::string mappingFileNative = QDir::toNativeSeparators(mappingFile).toStdString(); std::string mappingFileNative = QDir::toNativeSeparators(mappingFile).toStdString();
+10 -3
View File
@@ -20,7 +20,10 @@
#define SER_WINDOWMODE "windowmode" #define SER_WINDOWMODE "windowmode"
#define SER_UNSUPPORTEDFPS "unsupportedfps" #define SER_UNSUPPORTEDFPS "unsupportedfps"
#define SER_MDNS "mdns" #define SER_MDNS "mdns"
#define SER_QUITAPPAFTER "quitAppAfter"
#define SER_MOUSEACCELERATION "mouseacceleration" #define SER_MOUSEACCELERATION "mouseacceleration"
#define SER_STARTWINDOWED "startwindowed"
#define SER_FRAMEPACING "framepacing"
StreamingPreferences::StreamingPreferences(QObject *parent) StreamingPreferences::StreamingPreferences(QObject *parent)
: QObject(parent) : QObject(parent)
@@ -42,7 +45,10 @@ void StreamingPreferences::reload()
multiController = settings.value(SER_MULTICONT, true).toBool(); multiController = settings.value(SER_MULTICONT, true).toBool();
unsupportedFps = settings.value(SER_UNSUPPORTEDFPS, false).toBool(); unsupportedFps = settings.value(SER_UNSUPPORTEDFPS, false).toBool();
enableMdns = settings.value(SER_MDNS, true).toBool(); enableMdns = settings.value(SER_MDNS, true).toBool();
quitAppAfter = settings.value(SER_QUITAPPAFTER, false).toBool();
mouseAcceleration = settings.value(SER_MOUSEACCELERATION, false).toBool(); mouseAcceleration = settings.value(SER_MOUSEACCELERATION, false).toBool();
startWindowed = settings.value(SER_STARTWINDOWED, false).toBool();
framePacing = settings.value(SER_FRAMEPACING, false).toBool();
audioConfig = static_cast<AudioConfig>(settings.value(SER_AUDIOCFG, audioConfig = static_cast<AudioConfig>(settings.value(SER_AUDIOCFG,
static_cast<int>(AudioConfig::AC_STEREO)).toInt()); static_cast<int>(AudioConfig::AC_STEREO)).toInt());
videoCodecConfig = static_cast<VideoCodecConfig>(settings.value(SER_VIDEOCFG, videoCodecConfig = static_cast<VideoCodecConfig>(settings.value(SER_VIDEOCFG,
@@ -69,7 +75,10 @@ void StreamingPreferences::save()
settings.setValue(SER_MULTICONT, multiController); settings.setValue(SER_MULTICONT, multiController);
settings.setValue(SER_UNSUPPORTEDFPS, unsupportedFps); settings.setValue(SER_UNSUPPORTEDFPS, unsupportedFps);
settings.setValue(SER_MDNS, enableMdns); settings.setValue(SER_MDNS, enableMdns);
settings.setValue(SER_QUITAPPAFTER, quitAppAfter);
settings.setValue(SER_MOUSEACCELERATION, mouseAcceleration); settings.setValue(SER_MOUSEACCELERATION, mouseAcceleration);
settings.setValue(SER_STARTWINDOWED, startWindowed);
settings.setValue(SER_FRAMEPACING, framePacing);
settings.setValue(SER_AUDIOCFG, static_cast<int>(audioConfig)); settings.setValue(SER_AUDIOCFG, static_cast<int>(audioConfig));
settings.setValue(SER_VIDEOCFG, static_cast<int>(videoCodecConfig)); settings.setValue(SER_VIDEOCFG, static_cast<int>(videoCodecConfig));
settings.setValue(SER_VIDEODEC, static_cast<int>(videoDecoderSelection)); settings.setValue(SER_VIDEODEC, static_cast<int>(videoDecoderSelection));
@@ -133,9 +142,7 @@ int StreamingPreferences::getMaximumStreamingFrameRate()
} }
} }
// Cap the frame rate at 120 FPS. Past this, the encoders start maxFrameRate = qMax(maxFrameRate, bestMode.refresh_rate);
// to max out and drop frames.
maxFrameRate = qMax(maxFrameRate, qMin(120, bestMode.refresh_rate));
} }
} }
+9
View File
@@ -73,7 +73,10 @@ public:
Q_PROPERTY(bool multiController MEMBER multiController NOTIFY multiControllerChanged) Q_PROPERTY(bool multiController MEMBER multiController NOTIFY multiControllerChanged)
Q_PROPERTY(bool unsupportedFps MEMBER unsupportedFps NOTIFY unsupportedFpsChanged) Q_PROPERTY(bool unsupportedFps MEMBER unsupportedFps NOTIFY unsupportedFpsChanged)
Q_PROPERTY(bool enableMdns MEMBER enableMdns NOTIFY enableMdnsChanged) Q_PROPERTY(bool enableMdns MEMBER enableMdns NOTIFY enableMdnsChanged)
Q_PROPERTY(bool quitAppAfter MEMBER quitAppAfter NOTIFY quitAppAfterChanged)
Q_PROPERTY(bool mouseAcceleration MEMBER mouseAcceleration NOTIFY mouseAccelerationChanged) Q_PROPERTY(bool mouseAcceleration MEMBER mouseAcceleration NOTIFY mouseAccelerationChanged)
Q_PROPERTY(bool startWindowed MEMBER startWindowed NOTIFY startWindowedChanged)
Q_PROPERTY(bool framePacing MEMBER framePacing NOTIFY framePacingChanged)
Q_PROPERTY(AudioConfig audioConfig MEMBER audioConfig NOTIFY audioConfigChanged) Q_PROPERTY(AudioConfig audioConfig MEMBER audioConfig NOTIFY audioConfigChanged)
Q_PROPERTY(VideoCodecConfig videoCodecConfig MEMBER videoCodecConfig NOTIFY videoCodecConfigChanged) Q_PROPERTY(VideoCodecConfig videoCodecConfig MEMBER videoCodecConfig NOTIFY videoCodecConfigChanged)
Q_PROPERTY(VideoDecoderSelection videoDecoderSelection MEMBER videoDecoderSelection NOTIFY videoDecoderSelectionChanged) Q_PROPERTY(VideoDecoderSelection videoDecoderSelection MEMBER videoDecoderSelection NOTIFY videoDecoderSelectionChanged)
@@ -90,7 +93,10 @@ public:
bool multiController; bool multiController;
bool unsupportedFps; bool unsupportedFps;
bool enableMdns; bool enableMdns;
bool quitAppAfter;
bool mouseAcceleration; bool mouseAcceleration;
bool startWindowed;
bool framePacing;
AudioConfig audioConfig; AudioConfig audioConfig;
VideoCodecConfig videoCodecConfig; VideoCodecConfig videoCodecConfig;
VideoDecoderSelection videoDecoderSelection; VideoDecoderSelection videoDecoderSelection;
@@ -105,10 +111,13 @@ signals:
void multiControllerChanged(); void multiControllerChanged();
void unsupportedFpsChanged(); void unsupportedFpsChanged();
void enableMdnsChanged(); void enableMdnsChanged();
void quitAppAfterChanged();
void mouseAccelerationChanged(); void mouseAccelerationChanged();
void audioConfigChanged(); void audioConfigChanged();
void videoCodecConfigChanged(); void videoCodecConfigChanged();
void videoDecoderSelectionChanged(); void videoDecoderSelectionChanged();
void windowModeChanged(); void windowModeChanged();
void startWindowedChanged();
void framePacingChanged();
}; };
+33 -2
View File
@@ -3,15 +3,19 @@
#ifdef HAVE_SOUNDIO #ifdef HAVE_SOUNDIO
#include "renderers/soundioaudiorenderer.h" #include "renderers/soundioaudiorenderer.h"
#else
#include "renderers/sdl.h"
#endif #endif
#include "renderers/sdl.h"
#include <Limelight.h> #include <Limelight.h>
IAudioRenderer* Session::createAudioRenderer() IAudioRenderer* Session::createAudioRenderer()
{ {
#ifdef HAVE_SOUNDIO #ifdef HAVE_SOUNDIO
if (qgetenv("ML_AUDIO") == "SDL") {
return new SdlAudioRenderer();
}
return new SoundIoAudioRenderer(); return new SoundIoAudioRenderer();
#else #else
return new SdlAudioRenderer(); return new SdlAudioRenderer();
@@ -111,6 +115,21 @@ void Session::arDecodeAndPlaySample(char* sampleData, int sampleLength)
} }
} }
// See if we need to drop this sample
if (s_ActiveSession->m_DropAudioEndTime != 0) {
if (SDL_TICKS_PASSED(SDL_GetTicks(), s_ActiveSession->m_DropAudioEndTime)) {
// Avoid calling SDL_GetTicks() now
s_ActiveSession->m_DropAudioEndTime = 0;
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Audio drop window has ended");
}
else {
// We're still in the drop window
return;
}
}
s_ActiveSession->m_AudioSampleCount++; s_ActiveSession->m_AudioSampleCount++;
if (s_ActiveSession->m_AudioRenderer != nullptr) { if (s_ActiveSession->m_AudioRenderer != nullptr) {
@@ -141,10 +160,22 @@ void Session::arDecodeAndPlaySample(char* sampleData, int sampleLength)
// safe to reinitialize here because we can't be torn down while // safe to reinitialize here because we can't be torn down while
// the audio decoder/playback thread is still alive. // the audio decoder/playback thread is still alive.
if (s_ActiveSession->m_AudioRenderer == nullptr && (s_ActiveSession->m_AudioSampleCount % 200) == 0) { if (s_ActiveSession->m_AudioRenderer == nullptr && (s_ActiveSession->m_AudioSampleCount % 200) == 0) {
// Since we're doing this inline and audio initialization takes time, we need
// to drop samples to account for the time we've spent blocking audio rendering
// so we return to real-time playback and don't accumulate latency.
Uint32 audioReinitStartTime = SDL_GetTicks();
s_ActiveSession->m_AudioRenderer = s_ActiveSession->createAudioRenderer(); s_ActiveSession->m_AudioRenderer = s_ActiveSession->createAudioRenderer();
if (!s_ActiveSession->m_AudioRenderer->prepareForPlayback(&s_ActiveSession->m_AudioConfig)) { if (!s_ActiveSession->m_AudioRenderer->prepareForPlayback(&s_ActiveSession->m_AudioConfig)) {
delete s_ActiveSession->m_AudioRenderer; delete s_ActiveSession->m_AudioRenderer;
s_ActiveSession->m_AudioRenderer = nullptr; s_ActiveSession->m_AudioRenderer = nullptr;
} }
Uint32 audioReinitStopTime = SDL_GetTicks();
s_ActiveSession->m_DropAudioEndTime = audioReinitStopTime + (audioReinitStopTime - audioReinitStartTime);
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Audio reinitialization took %d ms - starting drop window",
audioReinitStopTime - audioReinitStartTime);
} }
} }
-5
View File
@@ -16,9 +16,4 @@ public:
private: private:
SDL_AudioDeviceID m_AudioDevice; SDL_AudioDeviceID m_AudioDevice;
Uint32 m_ChannelCount;
Uint32 m_PendingDrops;
Uint32 m_PendingHardDrops;
Uint32 m_SampleIndex;
Uint32 m_BaselinePendingData;
}; };
+1 -79
View File
@@ -5,18 +5,8 @@
#include <QtGlobal> #include <QtGlobal>
#define MIN_QUEUED_FRAMES 2
#define MAX_QUEUED_FRAMES 4
#define STOP_THE_WORLD_LIMIT 20
#define DROP_RATIO_DENOM 32
SdlAudioRenderer::SdlAudioRenderer() SdlAudioRenderer::SdlAudioRenderer()
: m_AudioDevice(0), : m_AudioDevice(0)
m_ChannelCount(0),
m_PendingDrops(0),
m_PendingHardDrops(0),
m_SampleIndex(0),
m_BaselinePendingData(0)
{ {
SDL_assert(!SDL_WasInit(SDL_INIT_AUDIO)); SDL_assert(!SDL_WasInit(SDL_INIT_AUDIO));
if (SDL_InitSubSystem(SDL_INIT_AUDIO) != 0) { if (SDL_InitSubSystem(SDL_INIT_AUDIO) != 0) {
@@ -61,27 +51,6 @@ bool SdlAudioRenderer::prepareForPlayback(const OPUS_MULTISTREAM_CONFIGURATION*
have.samples, have.samples,
have.size); have.size);
// SDL counts pending samples in the queued
// audio size using the WASAPI backend. This
// includes silence, which can throw off our
// pending data count. Get a baseline so we
// can exclude that data.
m_BaselinePendingData = 0;
#ifdef Q_OS_WIN32
for (int i = 0; i < 100; i++) {
m_BaselinePendingData = qMax(m_BaselinePendingData, SDL_GetQueuedAudioSize(m_AudioDevice));
SDL_Delay(10);
}
#endif
m_BaselinePendingData *= 2;
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Baseline pending audio data: %d bytes",
m_BaselinePendingData);
m_ChannelCount = opusConfig->channelCount;
m_SampleIndex = 0;
m_PendingDrops = m_PendingHardDrops = 0;
// Start playback // Start playback
SDL_PauseAudioDevice(m_AudioDevice, 0); SDL_PauseAudioDevice(m_AudioDevice, 0);
@@ -102,53 +71,6 @@ SdlAudioRenderer::~SdlAudioRenderer()
bool SdlAudioRenderer::submitAudio(short* audioBuffer, int audioSize) bool SdlAudioRenderer::submitAudio(short* audioBuffer, int audioSize)
{ {
m_SampleIndex++;
Uint32 queuedAudio = SDL_GetQueuedAudioSize(m_AudioDevice);
if (queuedAudio > m_BaselinePendingData) {
queuedAudio -= m_BaselinePendingData;
}
else {
queuedAudio = 0;
}
Uint32 framesQueued = queuedAudio / (SAMPLES_PER_FRAME * m_ChannelCount * sizeof(short));
// We must check this prior to the below checks to ensure we don't
// underflow if framesQueued - m_PendingHardDrops < 0.
if (framesQueued <= MIN_QUEUED_FRAMES) {
m_PendingDrops = m_PendingHardDrops = 0;
}
// Pend enough drops to get us back to MIN_QUEUED_FRAMES, checking first
// to ensure we don't underflow.
else if (framesQueued > m_PendingHardDrops &&
framesQueued - m_PendingHardDrops > STOP_THE_WORLD_LIMIT) {
m_PendingHardDrops = framesQueued - MIN_QUEUED_FRAMES;
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Pending hard drop of %u audio frames",
m_PendingHardDrops);
}
// If we're under the stop the world limit, we can drop samples
// gracefully over the next little while.
else if (framesQueued > m_PendingHardDrops + m_PendingDrops &&
framesQueued - m_PendingHardDrops - m_PendingDrops > MAX_QUEUED_FRAMES) {
m_PendingDrops = framesQueued - MIN_QUEUED_FRAMES;
}
// Determine if this frame should be dropped
if (m_PendingHardDrops != 0) {
// Hard drops happen all at once to forcefully
// resync with the source.
m_PendingHardDrops--;
return true;
}
else if (m_PendingDrops != 0 && m_SampleIndex % DROP_RATIO_DENOM == 0) {
// Normal drops are interspersed with the audio data
// to hide the glitches.
m_PendingDrops--;
return true;
}
if (SDL_QueueAudio(m_AudioDevice, audioBuffer, audioSize) < 0) { if (SDL_QueueAudio(m_AudioDevice, audioBuffer, audioSize) < 0) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Failed to queue audio sample: %s", "Failed to queue audio sample: %s",
@@ -4,12 +4,30 @@
#include <QtGlobal> #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() SoundIoAudioRenderer::SoundIoAudioRenderer()
: m_OpusChannelCount(0), : m_OpusChannelCount(0),
m_SoundIo(nullptr), m_SoundIo(nullptr),
m_Device(nullptr), m_Device(nullptr),
m_OutputStream(nullptr), m_OutputStream(nullptr),
m_RingBuffer(nullptr), m_RingBuffer(nullptr),
m_Latency(0),
m_Errored(false) m_Errored(false)
{ {
@@ -17,6 +35,10 @@ SoundIoAudioRenderer::SoundIoAudioRenderer()
SoundIoAudioRenderer::~SoundIoAudioRenderer() SoundIoAudioRenderer::~SoundIoAudioRenderer()
{ {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Audio latency: %f",
m_Latency);
if (m_OutputStream != nullptr) { if (m_OutputStream != nullptr) {
soundio_outstream_destroy(m_OutputStream); soundio_outstream_destroy(m_OutputStream);
} }
@@ -152,17 +174,12 @@ bool SoundIoAudioRenderer::prepareForPlayback(const OPUS_MULTISTREAM_CONFIGURATI
m_OutputStream->format = SoundIoFormatS16NE; m_OutputStream->format = SoundIoFormatS16NE;
m_OutputStream->sample_rate = opusConfig->sampleRate; m_OutputStream->sample_rate = opusConfig->sampleRate;
m_OutputStream->software_latency = k_MinSampleLengthSec;
m_OutputStream->name = "Moonlight"; m_OutputStream->name = "Moonlight";
m_OutputStream->userdata = this; m_OutputStream->userdata = this;
m_OutputStream->error_callback = sioErrorCallback; m_OutputStream->error_callback = sioErrorCallback;
m_OutputStream->write_callback = sioWriteCallback; m_OutputStream->write_callback = sioWriteCallback;
// This determines the size of the buffers we'll
// get from CoreAudio. Since GFE sends us packets
// in 5 ms chunks, we'll give them to the OS in
// buffers of the same size.
m_OutputStream->software_latency = 0.005;
SoundIoChannelLayout bestLayout = m_Device->current_layout; SoundIoChannelLayout bestLayout = m_Device->current_layout;
for (int i = 0; i < m_Device->layout_count; i++) { for (int i = 0; i < m_Device->layout_count; i++) {
if (scoreChannelLayout(&bestLayout, opusConfig) < if (scoreChannelLayout(&bestLayout, opusConfig) <
@@ -218,12 +235,33 @@ bool SoundIoAudioRenderer::prepareForPlayback(const OPUS_MULTISTREAM_CONFIGURATI
} }
} }
// Buffer up to 6 packets of audio (30 ms) to smooth int packetsToBuffer;
// out network packet delivery jitter
#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;
}
else {
// 30 ms buffer on CoreAudio to avoid glitching on macOS
packetsToBuffer = 6;
}
#endif
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Audio buffer size: %d packets",
packetsToBuffer);
m_RingBuffer = soundio_ring_buffer_create(m_SoundIo, m_RingBuffer = soundio_ring_buffer_create(m_SoundIo,
m_OutputStream->bytes_per_sample * m_OutputStream->bytes_per_sample *
m_OpusChannelCount * m_OpusChannelCount *
SAMPLES_PER_FRAME * 6); SAMPLES_PER_FRAME *
packetsToBuffer);
if (m_RingBuffer == nullptr) { if (m_RingBuffer == nullptr) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"soundio_ring_buffer_create() failed"); "soundio_ring_buffer_create() failed");
@@ -238,6 +276,11 @@ bool SoundIoAudioRenderer::prepareForPlayback(const OPUS_MULTISTREAM_CONFIGURATI
return false; return false;
} }
// HACK: For some reason, a constant latency hangs around in the audio pipeline
// unless we wait for the audio stream to drain before actually submitting any samples.
// This is a gross hack, but it works remarkably well.
SDL_Delay(500);
return true; return true;
} }
@@ -331,31 +374,15 @@ void SoundIoAudioRenderer::sioWriteCallback(SoundIoOutStream* stream, int frameC
// Clamp framesLeft to frameCountMax // Clamp framesLeft to frameCountMax
framesLeft = qMin(framesLeft, frameCountMax); framesLeft = qMin(framesLeft, frameCountMax);
// Place an upper-bound on audio stream latency to // Ensure we always write at least a buffer, even if it's silence, to avoid
// avoid accumulating packets in queue-based backends // busy looping when no audio data is available while libsoundio tries to keep
// like WASAPI. This bound was set by testing on several // us from starving the output device.
// Windows machines. The highest latency was found on frameCountMin = qMax(frameCountMin, (int)(stream->sample_rate * k_MinSampleLengthSec));
// a XPS 9343 running Windows 7 in Steam Big Picture frameCountMin = qMin(frameCountMin, frameCountMax);
// and the 5.1 audio test clip from Fraunhofer.
if (me->m_SoundIo->current_backend == SoundIoBackendWasapi) {
double latency;
if (soundio_outstream_get_latency(stream, &latency) == SoundIoErrorNone) {
if (latency > 0.050) {
// If our latency is higher than desired, drop these samples to gracefully lower
// the latency without glitching too much. Dropping the whole buffer causes
// a much more noticeable glitch. This approach also ensures that we don't
// accidentally underflow if the driver/kernel side is delayed and isn't
// consuming data fast enough. Dropping a frame at a time and re-evaluating
// each time ensures that we'll stop dropping if latency returns to normal.
readPtr += framesLeft * stream->bytes_per_sample * me->m_OpusChannelCount;
bytesRead += framesLeft * stream->bytes_per_sample * me->m_OpusChannelCount;
framesLeft = 0;
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, // Track latency on queueing-based backends
"Latency exceeded drop cap: %f", if (me->m_SoundIo->current_backend != SoundIoBackendCoreAudio && me->m_SoundIo->current_backend != SoundIoBackendJack) {
latency); soundio_outstream_get_latency(stream, &me->m_Latency);
}
}
} }
for (;;) { for (;;) {
@@ -32,5 +32,9 @@ private:
struct SoundIoOutStream* m_OutputStream; struct SoundIoOutStream* m_OutputStream;
struct SoundIoRingBuffer* m_RingBuffer; struct SoundIoRingBuffer* m_RingBuffer;
struct SoundIoChannelLayout m_EffectiveLayout; struct SoundIoChannelLayout m_EffectiveLayout;
double m_Latency;
bool m_Errored; bool m_Errored;
static const double k_RawSampleLengthSec;
static const double k_MinSampleLengthSec;
}; };
+224 -77
View File
@@ -17,6 +17,8 @@
#define VK_NUMPAD0 0x60 #define VK_NUMPAD0 0x60
#endif #endif
#define MOUSE_POLLING_INTERVAL 5
// How long the mouse button will be pressed for a tap to click gesture // How long the mouse button will be pressed for a tap to click gesture
#define TAP_BUTTON_RELEASE_DELAY 100 #define TAP_BUTTON_RELEASE_DELAY 100
@@ -34,10 +36,9 @@ const int SdlInputHandler::k_ButtonMap[] = {
UP_FLAG, DOWN_FLAG, LEFT_FLAG, RIGHT_FLAG UP_FLAG, DOWN_FLAG, LEFT_FLAG, RIGHT_FLAG
}; };
SdlInputHandler::SdlInputHandler(StreamingPreferences& prefs, NvComputer* computer, int streamWidth, int streamHeight) SdlInputHandler::SdlInputHandler(StreamingPreferences& prefs, NvComputer*, int streamWidth, int streamHeight)
: m_LastMouseMotionTime(0), : m_MultiController(prefs.multiController),
m_MultiController(prefs.multiController), m_MouseMoveTimer(0),
m_NeedsInputDelay(false),
m_LeftButtonReleaseTimer(0), m_LeftButtonReleaseTimer(0),
m_RightButtonReleaseTimer(0), m_RightButtonReleaseTimer(0),
m_DragTimer(0), m_DragTimer(0),
@@ -56,6 +57,21 @@ SdlInputHandler::SdlInputHandler(StreamingPreferences& prefs, NvComputer* comput
SDL_SetHint(SDL_HINT_MOUSE_RELATIVE_MODE_WARP, SDL_SetHint(SDL_HINT_MOUSE_RELATIVE_MODE_WARP,
prefs.mouseAcceleration ? "1" : "0"); prefs.mouseAcceleration ? "1" : "0");
// We must initialize joystick explicitly before gamecontroller in order
// to ensure we receive gamecontroller attach events for gamepads where
// SDL doesn't have a built-in mapping. By starting joystick first, we
// can allow mapping manager to update the mappings before GC attach
// events are generated.
SDL_assert(!SDL_WasInit(SDL_INIT_JOYSTICK));
if (SDL_InitSubSystem(SDL_INIT_JOYSTICK) != 0) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"SDL_InitSubSystem(SDL_INIT_JOYSTICK) failed: %s",
SDL_GetError());
}
MappingManager mappingManager;
mappingManager.applyMappings();
// We need to reinit this each time, since you only get // We need to reinit this each time, since you only get
// an initial set of gamepad arrival events once per init. // an initial set of gamepad arrival events once per init.
SDL_assert(!SDL_WasInit(SDL_INIT_GAMECONTROLLER)); SDL_assert(!SDL_WasInit(SDL_INIT_GAMECONTROLLER));
@@ -65,50 +81,52 @@ SdlInputHandler::SdlInputHandler(StreamingPreferences& prefs, NvComputer* comput
SDL_GetError()); SDL_GetError());
} }
MappingManager mappingManager; SDL_assert(!SDL_WasInit(SDL_INIT_HAPTIC));
mappingManager.applyMappings(); if (SDL_InitSubSystem(SDL_INIT_HAPTIC) != 0) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
if (!m_MultiController) { "SDL_InitSubSystem(SDL_INIT_HAPTIC) failed: %s",
// Player 1 is always present in non-MC mode SDL_GetError());
m_GamepadMask = 0x1;
}
else {
// Otherwise, detect gamepads on the fly
m_GamepadMask = 0;
} }
// Prior to GFE 3.14.1, sending too many mouse motion events can cause // Initialize the gamepad mask with currently attached gamepads to avoid
// GFE to choke and input latency to increase significantly. We will // causing gamepads to unexpectedly disappear and reappear on the host
// artificially throttle them to avoid this situation. // during stream startup as we detect currently attached gamepads one at a time.
QVector<int> gfeVersion = NvHTTP::parseQuad(computer->gfeVersion); m_GamepadMask = getAttachedGamepadMask();
if (gfeVersion.isEmpty() || // Very old versions don't have GfeVersion at all
gfeVersion[0] < 3 ||
(gfeVersion[0] == 3 && gfeVersion[1] < 14) ||
(gfeVersion[0] == 3 && gfeVersion[1] == 14 && gfeVersion[2] < 1)) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"This older version of GFE requires input delay hack");
m_NeedsInputDelay = true;
}
SDL_zero(m_GamepadState); SDL_zero(m_GamepadState);
SDL_zero(m_TouchDownEvent); SDL_zero(m_TouchDownEvent);
SDL_zero(m_CumulativeDelta); SDL_zero(m_CumulativeDelta);
SDL_AtomicSet(&m_MouseDeltaX, 0);
SDL_AtomicSet(&m_MouseDeltaY, 0);
m_MouseMoveTimer = SDL_AddTimer(MOUSE_POLLING_INTERVAL, SdlInputHandler::mouseMoveTimerCallback, this);
} }
SdlInputHandler::~SdlInputHandler() SdlInputHandler::~SdlInputHandler()
{ {
for (int i = 0; i < MAX_GAMEPADS; i++) { for (int i = 0; i < MAX_GAMEPADS; i++) {
if (m_GamepadState[i].haptic != nullptr) {
SDL_HapticClose(m_GamepadState[i].haptic);
}
if (m_GamepadState[i].controller != nullptr) { if (m_GamepadState[i].controller != nullptr) {
SDL_GameControllerClose(m_GamepadState[i].controller); SDL_GameControllerClose(m_GamepadState[i].controller);
} }
} }
SDL_RemoveTimer(m_MouseMoveTimer);
SDL_RemoveTimer(m_LeftButtonReleaseTimer); SDL_RemoveTimer(m_LeftButtonReleaseTimer);
SDL_RemoveTimer(m_RightButtonReleaseTimer); SDL_RemoveTimer(m_RightButtonReleaseTimer);
SDL_RemoveTimer(m_DragTimer); SDL_RemoveTimer(m_DragTimer);
SDL_QuitSubSystem(SDL_INIT_HAPTIC);
SDL_assert(!SDL_WasInit(SDL_INIT_HAPTIC));
SDL_QuitSubSystem(SDL_INIT_GAMECONTROLLER); SDL_QuitSubSystem(SDL_INIT_GAMECONTROLLER);
SDL_assert(!SDL_WasInit(SDL_INIT_GAMECONTROLLER)); SDL_assert(!SDL_WasInit(SDL_INIT_GAMECONTROLLER));
SDL_QuitSubSystem(SDL_INIT_JOYSTICK);
SDL_assert(!SDL_WasInit(SDL_INIT_JOYSTICK));
} }
void SdlInputHandler::handleKeyEvent(SDL_KeyboardEvent* event) void SdlInputHandler::handleKeyEvent(SDL_KeyboardEvent* event)
@@ -121,10 +139,21 @@ void SdlInputHandler::handleKeyEvent(SDL_KeyboardEvent* event)
(event->keysym.mod & KMOD_CTRL) && (event->keysym.mod & KMOD_CTRL) &&
(event->keysym.mod & KMOD_ALT) && (event->keysym.mod & KMOD_ALT) &&
(event->keysym.mod & KMOD_SHIFT)) { (event->keysym.mod & KMOD_SHIFT)) {
// First we test the SDLK combos for matches,
// that way we ensure that latin keyboard users
// can match to the key they see on their keyboards.
// If nothing matches that, we'll then go on to
// checking scancodes so non-latin keyboard users
// can have working hotkeys (though possibly in
// odd positions). We must do all SDLK tests before
// any scancode tests to avoid issues in cases
// where the SDLK for one shortcut collides with
// the scancode of another.
// Check for quit combo (Ctrl+Alt+Shift+Q) // Check for quit combo (Ctrl+Alt+Shift+Q)
if (event->keysym.sym == SDLK_q) { if (event->keysym.sym == SDLK_q) {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Detected quit key combo"); "Detected quit key combo (SDLK)");
// Push a quit event to the main loop // Push a quit event to the main loop
SDL_Event event; SDL_Event event;
@@ -136,7 +165,55 @@ void SdlInputHandler::handleKeyEvent(SDL_KeyboardEvent* event)
// Check for the unbind combo (Ctrl+Alt+Shift+Z) // Check for the unbind combo (Ctrl+Alt+Shift+Z)
else if (event->keysym.sym == SDLK_z) { else if (event->keysym.sym == SDLK_z) {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Detected mouse capture toggle combo"); "Detected mouse capture toggle combo (SDLK)");
// Stop handling future input
SDL_SetRelativeMouseMode((SDL_bool)!SDL_GetRelativeMouseMode());
// Force raise all keys to ensure they aren't stuck,
// since we won't get their key up events.
raiseAllKeys();
return;
}
else if (event->keysym.sym == SDLK_x) {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Detected full-screen toggle combo (SDLK)");
Session::s_ActiveSession->toggleFullscreen();
// Force raise all keys just be safe across this full-screen/windowed
// transition just in case key events get lost.
raiseAllKeys();
return;
}
else if (event->keysym.sym == SDLK_s) {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Detected stats toggle combo (SDLK)");
// Toggle the stats overlay
Session::get()->getOverlayManager().setOverlayState(Overlay::OverlayDebug,
!Session::get()->getOverlayManager().isOverlayEnabled(Overlay::OverlayDebug));
// Force raise all keys just be safe across this full-screen/windowed
// transition just in case key events get lost.
raiseAllKeys();
return;
}
// Check for quit combo (Ctrl+Alt+Shift+Q)
else if (event->keysym.scancode == SDL_SCANCODE_Q) {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Detected quit key combo (scancode)");
// Push a quit event to the main loop
SDL_Event event;
event.type = SDL_QUIT;
event.quit.timestamp = SDL_GetTicks();
SDL_PushEvent(&event);
return;
}
// Check for the unbind combo (Ctrl+Alt+Shift+Z)
else if (event->keysym.scancode == SDL_SCANCODE_Z) {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Detected mouse capture toggle combo (scancode)");
// Stop handling future input // Stop handling future input
SDL_SetRelativeMouseMode((SDL_bool)!SDL_GetRelativeMouseMode()); SDL_SetRelativeMouseMode((SDL_bool)!SDL_GetRelativeMouseMode());
@@ -147,11 +224,24 @@ void SdlInputHandler::handleKeyEvent(SDL_KeyboardEvent* event)
return; return;
} }
// Check for the full-screen combo (Ctrl+Alt+Shift+X) // Check for the full-screen combo (Ctrl+Alt+Shift+X)
else if (event->keysym.sym == SDLK_x) { else if (event->keysym.scancode == SDL_SCANCODE_X) {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Detected full-screen toggle combo"); "Detected full-screen toggle combo (scancode)");
Session::s_ActiveSession->toggleFullscreen(); Session::s_ActiveSession->toggleFullscreen();
// Force raise all keys just be safe across this full-screen/windowed
// transition just in case key events get lost.
raiseAllKeys();
return;
}
else if (event->keysym.scancode == SDL_SCANCODE_S) {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Detected stats toggle combo (scancode)");
// Toggle the stats overlay
Session::get()->getOverlayManager().setOverlayState(Overlay::OverlayDebug,
!Session::get()->getOverlayManager().isOverlayEnabled(Overlay::OverlayDebug));
// Force raise all keys just be safe across this full-screen/windowed // Force raise all keys just be safe across this full-screen/windowed
// transition just in case key events get lost. // transition just in case key events get lost.
raiseAllKeys(); raiseAllKeys();
@@ -459,40 +549,10 @@ void SdlInputHandler::handleMouseMotionEvent(SDL_MouseMotionEvent* event)
return; return;
} }
short xdelta = (short)event->xrel; // Batch until the next mouse polling window or we'll get awful
short ydelta = (short)event->yrel; // input lag everything except GFE 3.14 and 3.15.
SDL_AtomicAdd(&m_MouseDeltaX, event->xrel);
// If we're sending more than one motion event per millisecond, SDL_AtomicAdd(&m_MouseDeltaY, event->yrel);
// delay for 1 ms to allow batching of mouse move events. On older
// versions of GFE, we will unconditionally wait this 1 ms to
// work around an input processing issue that causes massive mouse latency.
Uint32 currentTime = SDL_GetTicks();
if (m_NeedsInputDelay || !SDL_TICKS_PASSED(currentTime, m_LastMouseMotionTime + 1)) {
SDL_Delay(1);
currentTime = SDL_GetTicks();
}
m_LastMouseMotionTime = currentTime;
// Pump even if we didn't delay since we might get some extra events
SDL_PumpEvents();
// Batch all of the pending mouse motion events
SDL_Event nextEvent;
while (SDL_PeepEvents(&nextEvent,
1,
SDL_GETEVENT,
SDL_MOUSEMOTION,
SDL_MOUSEMOTION) == 1) {
// In theory, these can overflow but in practice
// it should be highly unlikely since it would require
// moving 64K pixels in 1 ms.
xdelta += nextEvent.motion.xrel;
ydelta += nextEvent.motion.yrel;
}
if (xdelta != 0 || ydelta != 0) {
LiSendMouseMoveEvent(xdelta, ydelta);
}
} }
void SdlInputHandler::handleMouseWheelEvent(SDL_MouseWheelEvent* event) void SdlInputHandler::handleMouseWheelEvent(SDL_MouseWheelEvent* event)
@@ -572,6 +632,20 @@ Uint32 SdlInputHandler::dragTimerCallback(Uint32, void *param)
return 0; return 0;
} }
Uint32 SdlInputHandler::mouseMoveTimerCallback(Uint32 interval, void *param)
{
auto me = reinterpret_cast<SdlInputHandler*>(param);
short deltaX = (short)SDL_AtomicSet(&me->m_MouseDeltaX, 0);
short deltaY = (short)SDL_AtomicSet(&me->m_MouseDeltaY, 0);
if (deltaX != 0 || deltaY != 0) {
LiSendMouseMoveEvent(deltaX, deltaY);
}
return interval;
}
void SdlInputHandler::handleControllerAxisEvent(SDL_ControllerAxisEvent* event) void SdlInputHandler::handleControllerAxisEvent(SDL_ControllerAxisEvent* event)
{ {
GamepadState* state = findStateForGamepad(event->which); GamepadState* state = findStateForGamepad(event->which);
@@ -655,8 +729,26 @@ void SdlInputHandler::handleControllerDeviceEvent(SDL_ControllerDeviceEvent* eve
if (event->type == SDL_CONTROLLERDEVICEADDED) { if (event->type == SDL_CONTROLLERDEVICEADDED) {
int i; int i;
const char* name; const char* name;
SDL_GameController* controller;
const char* mapping;
char guidStr[33];
for (i = 0; i < MAX_GAMEPADS; i++) { controller = SDL_GameControllerOpen(event->which);
if (controller == NULL) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Failed to open gamepad: %s",
SDL_GetError());
return;
}
// We used to use SDL_GameControllerGetPlayerIndex() here but that
// can lead to strange issues due to bugs in Windows where an Xbox
// controller will join as player 2, even though no player 1 controller
// is connected at all. This pretty much screws any attempt to use
// the gamepad in single player games, so just assign them in order from 0.
i = 0;
for (; i < MAX_GAMEPADS; i++) {
if (m_GamepadState[i].controller == NULL) { if (m_GamepadState[i].controller == NULL) {
// Found an empty slot // Found an empty slot
break; break;
@@ -666,6 +758,7 @@ void SdlInputHandler::handleControllerDeviceEvent(SDL_ControllerDeviceEvent* eve
if (i == MAX_GAMEPADS) { if (i == MAX_GAMEPADS) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"No open gamepad slots found!"); "No open gamepad slots found!");
SDL_GameControllerClose(controller);
return; return;
} }
@@ -677,26 +770,36 @@ void SdlInputHandler::handleControllerDeviceEvent(SDL_ControllerDeviceEvent* eve
// Always player 1 in single controller mode // Always player 1 in single controller mode
state->index = 0; state->index = 0;
} }
state->controller = SDL_GameControllerOpen(event->which);
if (state->controller == NULL) { state->controller = controller;
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, state->jsId = SDL_JoystickInstanceID(SDL_GameControllerGetJoystick(state->controller));
"Failed to open gamepad: %s", state->haptic = SDL_HapticOpenFromJoystick(SDL_GameControllerGetJoystick(state->controller));
SDL_GetError()); state->hapticEffectId = -1;
return; if (state->haptic != nullptr && (SDL_HapticQuery(state->haptic) & SDL_HAPTIC_LEFTRIGHT) == 0) {
SDL_HapticClose(state->haptic);
state->haptic = nullptr;
} }
state->jsId = SDL_JoystickInstanceID(SDL_GameControllerGetJoystick(state->controller)); SDL_JoystickGetGUIDString(SDL_JoystickGetGUID(SDL_GameControllerGetJoystick(state->controller)),
guidStr, sizeof(guidStr));
mapping = SDL_GameControllerMapping(state->controller);
name = SDL_GameControllerName(state->controller); name = SDL_GameControllerName(state->controller);
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Gamepad %d (player %d) is: %s", "Gamepad %d (player %d) is: %s (%s -> %s)",
i, i,
state->index, state->index,
name != NULL ? name : "<null>"); name != nullptr ? name : "<null>",
guidStr,
mapping != nullptr ? mapping : "<null>");
if (mapping != nullptr) {
SDL_free((void*)mapping);
}
// Add this gamepad to the gamepad mask // Add this gamepad to the gamepad mask
if (m_MultiController) { if (m_MultiController) {
SDL_assert(!(m_GamepadMask & (1 << state->index))); // NB: Don't assert that it's unset here because we will already
// have the mask set for initially attached gamepads to avoid confusing
// apps running on the host.
m_GamepadMask |= (1 << state->index); m_GamepadMask |= (1 << state->index);
} }
else { else {
@@ -710,6 +813,9 @@ void SdlInputHandler::handleControllerDeviceEvent(SDL_ControllerDeviceEvent* eve
state = findStateForGamepad(event->which); state = findStateForGamepad(event->which);
if (state != NULL) { if (state != NULL) {
SDL_GameControllerClose(state->controller); SDL_GameControllerClose(state->controller);
if (state->haptic != nullptr) {
SDL_HapticClose(state->haptic);
}
// Remove this from the gamepad mask in MC-mode // Remove this from the gamepad mask in MC-mode
if (m_MultiController) { if (m_MultiController) {
@@ -763,6 +869,47 @@ void SdlInputHandler::handleJoystickArrivalEvent(SDL_JoyDeviceEvent* event)
} }
} }
void SdlInputHandler::rumble(unsigned short controllerNumber, unsigned short lowFreqMotor, unsigned short highFreqMotor)
{
// Make sure the controller number is within our supported count
if (controllerNumber >= MAX_GAMEPADS) {
return;
}
// Check if the controller supports haptics (and if the controller exists at all)
SDL_Haptic* haptic = m_GamepadState[controllerNumber].haptic;
if (haptic == nullptr) {
return;
}
// Stop the last effect we played
if (m_GamepadState[controllerNumber].hapticEffectId >= 0) {
SDL_HapticDestroyEffect(haptic, m_GamepadState[controllerNumber].hapticEffectId);
}
// If this callback is telling us to stop both motors, don't bother queuing a new effect
if (lowFreqMotor == 0 && highFreqMotor == 0) {
return;
}
SDL_HapticEffect effect;
SDL_memset(&effect, 0, sizeof(effect));
effect.type = SDL_HAPTIC_LEFTRIGHT;
// The effect should last until we are instructed to stop or change it
effect.leftright.length = SDL_HAPTIC_INFINITY;
// SDL haptics range from 0-32767 but XInput uses 0-65535, so divide by 2 to correct for SDL's scaling
effect.leftright.large_magnitude = lowFreqMotor / 2;
effect.leftright.small_magnitude = highFreqMotor / 2;
// Play the new effect
m_GamepadState[controllerNumber].hapticEffectId = SDL_HapticNewEffect(haptic, &effect);
if (m_GamepadState[controllerNumber].hapticEffectId >= 0) {
SDL_HapticRunEffect(haptic, m_GamepadState[controllerNumber].hapticEffectId, 1);
}
}
void SdlInputHandler::handleTouchFingerEvent(SDL_TouchFingerEvent* event) void SdlInputHandler::handleTouchFingerEvent(SDL_TouchFingerEvent* event)
{ {
int fingerIndex = -1; int fingerIndex = -1;
@@ -968,7 +1115,7 @@ QString SdlInputHandler::getUnmappedGamepads()
"Number of axes: %d | Number of buttons: %d | Number of hats: %d", "Number of axes: %d | Number of buttons: %d | Number of hats: %d",
numAxes, numButtons, numHats); numAxes, numButtons, numHats);
if (numAxes >= 4 && numButtons >= 8 && numHats <= 1) { if ((numAxes >= 4 && numAxes <= 8) && numButtons >= 8 && numHats <= 1) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"Joystick likely to be an unmapped game controller"); "Joystick likely to be an unmapped game controller");
if (!ret.isEmpty()) { if (!ret.isEmpty()) {
+10 -2
View File
@@ -8,6 +8,8 @@
struct GamepadState { struct GamepadState {
SDL_GameController* controller; SDL_GameController* controller;
SDL_JoystickID jsId; SDL_JoystickID jsId;
SDL_Haptic* haptic;
int hapticEffectId;
short index; short index;
short buttons; short buttons;
@@ -43,6 +45,8 @@ public:
void handleJoystickArrivalEvent(SDL_JoyDeviceEvent* event); void handleJoystickArrivalEvent(SDL_JoyDeviceEvent* event);
void rumble(unsigned short controllerNumber, unsigned short lowFreqMotor, unsigned short highFreqMotor);
void handleTouchFingerEvent(SDL_TouchFingerEvent* event); void handleTouchFingerEvent(SDL_TouchFingerEvent* event);
int getAttachedGamepadMask(); int getAttachedGamepadMask();
@@ -67,9 +71,13 @@ private:
static static
Uint32 dragTimerCallback(Uint32 interval, void* param); Uint32 dragTimerCallback(Uint32 interval, void* param);
Uint32 m_LastMouseMotionTime; static
Uint32 mouseMoveTimerCallback(Uint32 interval, void* param);
bool m_MultiController; bool m_MultiController;
bool m_NeedsInputDelay; SDL_TimerID m_MouseMoveTimer;
SDL_atomic_t m_MouseDeltaX;
SDL_atomic_t m_MouseDeltaY;
int m_GamepadMask; int m_GamepadMask;
GamepadState m_GamepadState[MAX_GAMEPADS]; GamepadState m_GamepadState[MAX_GAMEPADS];
QSet<short> m_KeysDown; QSet<short> m_KeysDown;
+205 -103
View File
@@ -6,12 +6,6 @@
#include <SDL.h> #include <SDL.h>
#include "utils.h" #include "utils.h"
// HACK: Need to call SetThreadExecutionState() because SDL doesn't
#ifdef Q_OS_WIN32
#define WIN32_LEAN_AND_MEAN
#include <Windows.h>
#endif
#ifdef HAVE_FFMPEG #ifdef HAVE_FFMPEG
#include "video/ffmpeg.h" #include "video/ffmpeg.h"
#endif #endif
@@ -44,7 +38,8 @@ CONNECTION_LISTENER_CALLBACKS Session::k_ConnCallbacks = {
Session::clConnectionTerminated, Session::clConnectionTerminated,
nullptr, nullptr,
nullptr, nullptr,
Session::clLogMessage Session::clLogMessage,
Session::clRumble
}; };
AUDIO_RENDERER_CALLBACKS Session::k_AudioCallbacks = { AUDIO_RENDERER_CALLBACKS Session::k_AudioCallbacks = {
@@ -79,7 +74,11 @@ void Session::clStageFailed(int stage, long errorCode)
void Session::clConnectionTerminated(long errorCode) void Session::clConnectionTerminated(long errorCode)
{ {
emit s_ActiveSession->displayLaunchError("Connection terminated"); // Display the termination dialog if this was not intended
if (errorCode != 0) {
s_ActiveSession->m_UnexpectedTermination = true;
emit s_ActiveSession->displayLaunchError("Connection terminated");
}
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Connection terminated: %ld", "Connection terminated: %ld",
@@ -104,14 +103,29 @@ void Session::clLogMessage(const char* format, ...)
va_end(ap); va_end(ap);
} }
#define CALL_INITIALIZE(dec) (dec)->initialize(vds, window, videoFormat, width, height, frameRate, enableVsync) void Session::clRumble(unsigned short controllerNumber, unsigned short lowFreqMotor, unsigned short highFreqMotor)
{
// The input handler can be closed during the stream if LiStopConnection() hasn't completed yet
// but the stream has been stopped by the user. In this case, just discard the rumble.
SDL_AtomicLock(&s_ActiveSession->m_InputHandlerLock);
if (s_ActiveSession->m_InputHandler != nullptr) {
s_ActiveSession->m_InputHandler->rumble(controllerNumber, lowFreqMotor, highFreqMotor);
}
SDL_AtomicUnlock(&s_ActiveSession->m_InputHandlerLock);
}
#define CALL_INITIALIZE(dec) (dec)->initialize(vds, window, videoFormat, width, height, frameRate, enableVsync, enableFramePacing)
bool Session::chooseDecoder(StreamingPreferences::VideoDecoderSelection vds, bool Session::chooseDecoder(StreamingPreferences::VideoDecoderSelection vds,
SDL_Window* window, int videoFormat, int width, int height, SDL_Window* window, int videoFormat, int width, int height,
int frameRate, bool enableVsync, IVideoDecoder*& chosenDecoder) int frameRate, bool enableVsync, bool enableFramePacing, bool testOnly, IVideoDecoder*& chosenDecoder)
{ {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"V-sync %s",
enableVsync ? "enabled" : "disabled");
#ifdef HAVE_SLVIDEO #ifdef HAVE_SLVIDEO
chosenDecoder = new SLVideoDecoder(); chosenDecoder = new SLVideoDecoder(testOnly);
if (CALL_INITIALIZE(chosenDecoder)) { if (CALL_INITIALIZE(chosenDecoder)) {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"SLVideo video decoder chosen"); "SLVideo video decoder chosen");
@@ -126,7 +140,7 @@ bool Session::chooseDecoder(StreamingPreferences::VideoDecoderSelection vds,
#endif #endif
#ifdef HAVE_FFMPEG #ifdef HAVE_FFMPEG
chosenDecoder = new FFmpegVideoDecoder(); chosenDecoder = new FFmpegVideoDecoder(testOnly);
if (CALL_INITIALIZE(chosenDecoder)) { if (CALL_INITIALIZE(chosenDecoder)) {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"FFmpeg-based video decoder chosen"); "FFmpeg-based video decoder chosen");
@@ -222,17 +236,20 @@ bool Session::isHardwareDecodeAvailable(StreamingPreferences::VideoDecoderSelect
return false; return false;
} }
if (!chooseDecoder(vds, window, videoFormat, width, height, frameRate, true, decoder)) { if (!chooseDecoder(vds, window, videoFormat, width, height, frameRate, true, false, true, decoder)) {
SDL_DestroyWindow(window); SDL_DestroyWindow(window);
SDL_QuitSubSystem(SDL_INIT_VIDEO); SDL_QuitSubSystem(SDL_INIT_VIDEO);
return false; return false;
} }
SDL_DestroyWindow(window);
bool ret = decoder->isHardwareAccelerated(); bool ret = decoder->isHardwareAccelerated();
delete decoder; delete decoder;
// This must be called after the decoder is deleted, because
// the renderer may want to interact with the window
SDL_DestroyWindow(window);
SDL_QuitSubSystem(SDL_INIT_VIDEO); SDL_QuitSubSystem(SDL_INIT_VIDEO);
return ret; return ret;
@@ -259,17 +276,20 @@ int Session::getDecoderCapabilities(StreamingPreferences::VideoDecoderSelection
return false; return false;
} }
if (!chooseDecoder(vds, window, videoFormat, width, height, frameRate, true, decoder)) { if (!chooseDecoder(vds, window, videoFormat, width, height, frameRate, true, false, true, decoder)) {
SDL_DestroyWindow(window); SDL_DestroyWindow(window);
SDL_QuitSubSystem(SDL_INIT_VIDEO); SDL_QuitSubSystem(SDL_INIT_VIDEO);
return false; return false;
} }
SDL_DestroyWindow(window);
int caps = decoder->getDecoderCapabilities(); int caps = decoder->getDecoderCapabilities();
delete decoder; delete decoder;
// This must be called after the decoder is deleted, because
// the renderer may want to interact with the window
SDL_DestroyWindow(window);
SDL_QuitSubSystem(SDL_INIT_VIDEO); SDL_QuitSubSystem(SDL_INIT_VIDEO);
return caps; return caps;
@@ -287,12 +307,26 @@ Session::Session(NvComputer* computer, NvApp& app, StreamingPreferences *prefere
m_DisplayOriginX(0), m_DisplayOriginX(0),
m_DisplayOriginY(0), m_DisplayOriginY(0),
m_PendingWindowedTransition(false), m_PendingWindowedTransition(false),
m_UnexpectedTermination(true), // Failure prior to streaming is unexpected
m_InputHandler(nullptr),
m_InputHandlerLock(0),
m_OpusDecoder(nullptr), m_OpusDecoder(nullptr),
m_AudioRenderer(nullptr), m_AudioRenderer(nullptr),
m_AudioSampleCount(0) m_AudioSampleCount(0),
m_DropAudioEndTime(0)
{ {
} }
// NB: This may not get destroyed for a long time! Don't put any vital cleanup here.
// Use Session::exec() or DeferredSessionCleanupTask instead.
Session::~Session()
{
// Acquire session semaphore to ensure all cleanup is done before the destructor returns
// and the object is deallocated.
s_ActiveSessionSemaphore.acquire();
s_ActiveSessionSemaphore.release();
}
void Session::initialize() void Session::initialize()
{ {
qInfo() << "Server GPU:" << m_Computer->gpuModel; qInfo() << "Server GPU:" << m_Computer->gpuModel;
@@ -315,6 +349,8 @@ void Session::initialize()
m_StreamConfig.fps = m_Preferences->fps; m_StreamConfig.fps = m_Preferences->fps;
m_StreamConfig.bitrate = m_Preferences->bitrateKbps; m_StreamConfig.bitrate = m_Preferences->bitrateKbps;
m_StreamConfig.hevcBitratePercentageMultiplier = 75; m_StreamConfig.hevcBitratePercentageMultiplier = 75;
m_StreamConfig.streamingRemotely = STREAM_CFG_AUTO;
m_StreamConfig.packetSize = 1392;
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Video bitrate: %d kbps", "Video bitrate: %d kbps",
@@ -350,6 +386,22 @@ void Session::initialize()
m_StreamConfig.width, m_StreamConfig.width,
m_StreamConfig.height, m_StreamConfig.height,
m_StreamConfig.fps); m_StreamConfig.fps);
#ifdef Q_OS_DARWIN
{
// Prior to GFE 3.11, GFE did not allow us to constrain
// the number of reference frames, so we have to fixup the SPS
// to allow decoding via VideoToolbox on macOS. Since we don't
// have fixup code for HEVC, just avoid it if GFE is too old.
QVector<int> gfeVersion = NvHTTP::parseQuad(m_Computer->gfeVersion);
if (gfeVersion.isEmpty() || // Very old versions don't have GfeVersion at all
gfeVersion[0] < 3 ||
(gfeVersion[0] == 3 && gfeVersion[1] < 11)) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"Disabling HEVC on macOS due to old GFE version");
m_StreamConfig.supportsHevc = false;
}
}
#endif
m_StreamConfig.enableHdr = false; m_StreamConfig.enableHdr = false;
break; break;
case StreamingPreferences::VCC_FORCE_H264: case StreamingPreferences::VCC_FORCE_H264:
@@ -366,20 +418,6 @@ void Session::initialize()
break; break;
} }
if (m_Computer->activeAddress == m_Computer->remoteAddress) {
m_StreamConfig.streamingRemotely = 1;
}
else {
m_StreamConfig.streamingRemotely = 0;
}
if (m_Computer->activeAddress == m_Computer->localAddress) {
m_StreamConfig.packetSize = 1392;
}
else {
m_StreamConfig.packetSize = 1024;
}
switch (m_Preferences->windowMode) switch (m_Preferences->windowMode)
{ {
case StreamingPreferences::WM_FULLSCREEN_DESKTOP: case StreamingPreferences::WM_FULLSCREEN_DESKTOP:
@@ -489,15 +527,6 @@ bool Session::validateLaunch()
if (m_Computer->gfeVersion.isEmpty() || m_Computer->gfeVersion.startsWith("2.")) { if (m_Computer->gfeVersion.isEmpty() || m_Computer->gfeVersion.startsWith("2.")) {
emitLaunchWarning("GeForce Experience 3.0 or higher is required for 4K streaming."); emitLaunchWarning("GeForce Experience 3.0 or higher is required for 4K streaming.");
m_StreamConfig.width = 1920;
m_StreamConfig.height = 1080;
}
// This list is sorted from least to greatest
else if (m_Computer->displayModes.last().width < 3840 ||
(m_Computer->displayModes.last().refreshRate < 60 && m_StreamConfig.fps >= 60)) {
emitLaunchWarning("Your host PC GPU doesn't support 4K streaming. "
"A GeForce GTX 900-series (Maxwell) or later GPU is required for 4K streaming.");
m_StreamConfig.width = 1920; m_StreamConfig.width = 1920;
m_StreamConfig.height = 1080; m_StreamConfig.height = 1080;
} }
@@ -556,25 +585,65 @@ bool Session::validateLaunch()
class DeferredSessionCleanupTask : public QRunnable class DeferredSessionCleanupTask : public QRunnable
{ {
void run() override public:
{ DeferredSessionCleanupTask(Session* session) :
// Finish cleanup of the connection state m_Session(session) {}
LiStopConnection();
private:
virtual ~DeferredSessionCleanupTask() override
{
// Allow another session to start now that we're cleaned up // Allow another session to start now that we're cleaned up
Session::s_ActiveSession = nullptr; Session::s_ActiveSession = nullptr;
Session::s_ActiveSessionSemaphore.release(); Session::s_ActiveSessionSemaphore.release();
} }
void run() override
{
// Only quit the running app if our session terminated gracefully
bool shouldQuit =
!m_Session->m_UnexpectedTermination &&
m_Session->m_Preferences->quitAppAfter;
// Notify the UI
if (shouldQuit) {
emit m_Session->quitStarting();
}
else {
emit m_Session->sessionFinished();
}
// Finish cleanup of the connection state
LiStopConnection();
// Perform a best-effort app quit
if (shouldQuit) {
NvHTTP http(m_Session->m_Computer->activeAddress, m_Session->m_Computer->serverCert);
// Logging is already done inside NvHTTP
try {
http.quitApp();
} catch (const GfeHttpResponseException&) {
} catch (const QtNetworkReplyException&) {
}
// Session is finished now
emit m_Session->sessionFinished();
}
}
Session* m_Session;
}; };
void Session::getWindowDimensions(int& x, int& y, void Session::getWindowDimensions(int& x, int& y,
int& width, int& height) int& width, int& height)
{ {
int displayIndex = 0; int displayIndex = 0;
bool fullScreen;
if (m_Window != nullptr) { if (m_Window != nullptr) {
displayIndex = SDL_GetWindowDisplayIndex(m_Window); displayIndex = SDL_GetWindowDisplayIndex(m_Window);
SDL_assert(displayIndex >= 0); SDL_assert(displayIndex >= 0);
fullScreen = (SDL_GetWindowFlags(m_Window) & SDL_WINDOW_FULLSCREEN);
} }
// Create our window on the same display that Qt's UI // Create our window on the same display that Qt's UI
// was being displayed on. // was being displayed on.
@@ -601,10 +670,16 @@ void Session::getWindowDimensions(int& x, int& y,
i, SDL_GetError()); i, SDL_GetError());
} }
} }
fullScreen = (m_Preferences->windowMode != StreamingPreferences::WM_WINDOWED);
} }
SDL_Rect usableBounds; SDL_Rect usableBounds;
if (SDL_GetDisplayUsableBounds(displayIndex, &usableBounds) == 0) { if (fullScreen && SDL_GetDisplayBounds(displayIndex, &usableBounds) == 0) {
width = usableBounds.w;
height = usableBounds.h;
}
else if (SDL_GetDisplayUsableBounds(displayIndex, &usableBounds) == 0) {
width = usableBounds.w; width = usableBounds.w;
height = usableBounds.h; height = usableBounds.h;
@@ -689,9 +764,13 @@ void Session::updateOptimalWindowDisplayMode()
bestMode = desktopMode; bestMode = desktopMode;
} }
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, if ((SDL_GetWindowFlags(m_Window) & SDL_WINDOW_FULLSCREEN_DESKTOP) == SDL_WINDOW_FULLSCREEN) {
"Chosen best display mode: %dx%dx%d", // Only print when the window is actually in full-screen exclusive mode,
bestMode.w, bestMode.h, bestMode.refresh_rate); // otherwise we're not actually using the mode we've set here
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Chosen best display mode: %dx%dx%d",
bestMode.w, bestMode.h, bestMode.refresh_rate);
}
SDL_SetWindowDisplayMode(m_Window, &bestMode); SDL_SetWindowDisplayMode(m_Window, &bestMode);
} }
@@ -726,6 +805,7 @@ void Session::exec(int displayOriginX, int displayOriginY)
// Check for validation errors/warnings and emit // Check for validation errors/warnings and emit
// signals for them, if appropriate // signals for them, if appropriate
if (!validateLaunch()) { if (!validateLaunch()) {
emit sessionFinished();
return; return;
} }
@@ -746,15 +826,18 @@ void Session::exec(int displayOriginX, int displayOriginY)
// Initialize the gamepad code with our preferences // Initialize the gamepad code with our preferences
StreamingPreferences prefs; StreamingPreferences prefs;
SdlInputHandler inputHandler(prefs, m_Computer, m_InputHandler = new SdlInputHandler(prefs, m_Computer,
m_StreamConfig.width, m_StreamConfig.width,
m_StreamConfig.height); m_StreamConfig.height);
// The UI should have ensured the old game was already quit // The UI should have ensured the old game was already quit
// if we decide to stream a different game. // if we decide to stream a different game.
Q_ASSERT(m_Computer->currentGameId == 0 || Q_ASSERT(m_Computer->currentGameId == 0 ||
m_Computer->currentGameId == m_App.id); m_Computer->currentGameId == m_App.id);
// SOPS will set all settings to 720p60 if it doesn't recognize
// the chosen resolution. Avoid that by disabling SOPS when it
// is not streaming a supported resolution.
bool enableGameOptimizations = false; bool enableGameOptimizations = false;
for (const NvDisplayMode &mode : m_Computer->displayModes) { for (const NvDisplayMode &mode : m_Computer->displayModes) {
if (mode.width == m_StreamConfig.width && if (mode.width == m_StreamConfig.width &&
@@ -768,7 +851,7 @@ void Session::exec(int displayOriginX, int displayOriginY)
} }
try { try {
NvHTTP http(m_Computer->activeAddress); NvHTTP http(m_Computer->activeAddress, m_Computer->serverCert);
if (m_Computer->currentGameId != 0) { if (m_Computer->currentGameId != 0) {
http.resumeApp(&m_StreamConfig); http.resumeApp(&m_StreamConfig);
} }
@@ -776,15 +859,19 @@ void Session::exec(int displayOriginX, int displayOriginY)
http.launchApp(m_App.id, &m_StreamConfig, http.launchApp(m_App.id, &m_StreamConfig,
enableGameOptimizations, enableGameOptimizations,
prefs.playAudioOnHost, prefs.playAudioOnHost,
inputHandler.getAttachedGamepadMask()); m_InputHandler->getAttachedGamepadMask());
} }
} catch (const GfeHttpResponseException& e) { } catch (const GfeHttpResponseException& e) {
delete m_InputHandler;
m_InputHandler = nullptr;
emit displayLaunchError(e.toQString()); emit displayLaunchError(e.toQString());
s_ActiveSessionSemaphore.release(); QThreadPool::globalInstance()->start(new DeferredSessionCleanupTask(this));
return; return;
} catch (const QtNetworkReplyException& e) { } catch (const QtNetworkReplyException& e) {
delete m_InputHandler;
m_InputHandler = nullptr;
emit displayLaunchError(e.toQString()); emit displayLaunchError(e.toQString());
s_ActiveSessionSemaphore.release(); QThreadPool::globalInstance()->start(new DeferredSessionCleanupTask(this));
return; return;
} }
@@ -793,8 +880,10 @@ void Session::exec(int displayOriginX, int displayOriginY)
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"SDL_InitSubSystem(SDL_INIT_VIDEO) failed: %s", "SDL_InitSubSystem(SDL_INIT_VIDEO) failed: %s",
SDL_GetError()); SDL_GetError());
delete m_InputHandler;
m_InputHandler = nullptr;
emit displayLaunchError(QString::fromLocal8Bit(SDL_GetError())); emit displayLaunchError(QString::fromLocal8Bit(SDL_GetError()));
s_ActiveSessionSemaphore.release(); QThreadPool::globalInstance()->start(new DeferredSessionCleanupTask(this));
return; return;
} }
@@ -821,8 +910,10 @@ void Session::exec(int displayOriginX, int displayOriginY)
if (err != 0) { if (err != 0) {
// We already displayed an error dialog in the stage failure // We already displayed an error dialog in the stage failure
// listener. // listener.
delete m_InputHandler;
m_InputHandler = nullptr;
SDL_QuitSubSystem(SDL_INIT_VIDEO); SDL_QuitSubSystem(SDL_INIT_VIDEO);
s_ActiveSessionSemaphore.release(); QThreadPool::globalInstance()->start(new DeferredSessionCleanupTask(this));
return; return;
} }
@@ -843,11 +934,34 @@ void Session::exec(int displayOriginX, int displayOriginY)
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"SDL_CreateWindow() failed: %s", "SDL_CreateWindow() failed: %s",
SDL_GetError()); SDL_GetError());
delete m_InputHandler;
m_InputHandler = nullptr;
SDL_QuitSubSystem(SDL_INIT_VIDEO); SDL_QuitSubSystem(SDL_INIT_VIDEO);
s_ActiveSessionSemaphore.release(); QThreadPool::globalInstance()->start(new DeferredSessionCleanupTask(this));
return; return;
} }
QSvgRenderer svgIconRenderer(QString(":/res/moonlight.svg"));
QImage svgImage(ICON_SIZE, ICON_SIZE, QImage::Format_RGBA8888);
svgImage.fill(0);
QPainter svgPainter(&svgImage);
svgIconRenderer.render(&svgPainter);
SDL_Surface* iconSurface = SDL_CreateRGBSurfaceWithFormatFrom((void*)svgImage.constBits(),
svgImage.width(),
svgImage.height(),
32,
4 * svgImage.width(),
SDL_PIXELFORMAT_RGBA32);
#ifndef Q_OS_DARWIN
// Other platforms seem to preserve our Qt icon when creating a new window.
if (iconSurface != nullptr) {
// This must be called before entering full-screen mode on Windows
// or our icon will not persist when toggling to windowed mode
SDL_SetWindowIcon(m_Window, iconSurface);
}
#endif
// For non-full screen windows, call getWindowDimensions() // For non-full screen windows, call getWindowDimensions()
// again after creating a window to allow it to account // again after creating a window to allow it to account
// for window chrome size. // for window chrome size.
@@ -871,25 +985,6 @@ void Session::exec(int displayOriginX, int displayOriginY)
SDL_SetWindowFullscreen(m_Window, m_FullScreenFlag); SDL_SetWindowFullscreen(m_Window, m_FullScreenFlag);
} }
QSvgRenderer svgIconRenderer(QString(":/res/moonlight.svg"));
QImage svgImage(ICON_SIZE, ICON_SIZE, QImage::Format_RGBA8888);
svgImage.fill(0);
QPainter svgPainter(&svgImage);
svgIconRenderer.render(&svgPainter);
SDL_Surface* iconSurface = SDL_CreateRGBSurfaceWithFormatFrom((void*)svgImage.constBits(),
svgImage.width(),
svgImage.height(),
32,
4 * svgImage.width(),
SDL_PIXELFORMAT_RGBA32);
#ifndef Q_OS_DARWIN
// Other platforms seem to preserve our Qt icon when creating a new window
if (iconSurface != nullptr) {
SDL_SetWindowIcon(m_Window, iconSurface);
}
#endif
#ifndef QT_DEBUG #ifndef QT_DEBUG
// Capture the mouse by default on release builds only. // Capture the mouse by default on release builds only.
// This prevents the mouse from becoming trapped inside // This prevents the mouse from becoming trapped inside
@@ -908,13 +1003,6 @@ void Session::exec(int displayOriginX, int displayOriginY)
// Disable the screen saver // Disable the screen saver
SDL_DisableScreenSaver(); SDL_DisableScreenSaver();
#ifdef Q_OS_WIN32
// HACK: SDL doesn't call this, so we must do so to disable the
// screensaver when we're in windowed mode. DirectX will disable
// it for us when we're in full-screen exclusive mode.
SetThreadExecutionState(ES_CONTINUOUS | ES_DISPLAY_REQUIRED);
#endif
// Set timer resolution to 1 ms on Windows for greater // Set timer resolution to 1 ms on Windows for greater
// sleep precision and more accurate callback timing. // sleep precision and more accurate callback timing.
SDL_SetHint(SDL_HINT_TIMER_RESOLUTION, "1"); SDL_SetHint(SDL_HINT_TIMER_RESOLUTION, "1");
@@ -929,6 +1017,11 @@ void Session::exec(int displayOriginX, int displayOriginY)
int currentDisplayIndex = SDL_GetWindowDisplayIndex(m_Window); int currentDisplayIndex = SDL_GetWindowDisplayIndex(m_Window);
// Now that we're about to stream, any SDL_QUIT event is expected
// unless it comes from the connection termination callback where
// (m_UnexpectedTermination is set back to true).
m_UnexpectedTermination = false;
// Hijack this thread to be the SDL main thread. We have to do this // Hijack this thread to be the SDL main thread. We have to do this
// because we want to suspend all Qt processing until the stream is over. // because we want to suspend all Qt processing until the stream is over.
SDL_Event event; SDL_Event event;
@@ -1004,7 +1097,7 @@ void Session::exec(int displayOriginX, int displayOriginY)
// Raise all keys that are currently pressed. If we don't do this, certain keys // Raise all keys that are currently pressed. If we don't do this, certain keys
// used in shortcuts that cause focus loss (such as Alt+Tab) may get stuck down. // used in shortcuts that cause focus loss (such as Alt+Tab) may get stuck down.
inputHandler.raiseAllKeys(); m_InputHandler->raiseAllKeys();
} }
// We want to recreate the decoder for resizes (full-screen toggles) and the initial shown event. // We want to recreate the decoder for resizes (full-screen toggles) and the initial shown event.
@@ -1076,6 +1169,8 @@ void Session::exec(int displayOriginX, int displayOriginY)
m_Window, m_ActiveVideoFormat, m_ActiveVideoWidth, m_Window, m_ActiveVideoFormat, m_ActiveVideoWidth,
m_ActiveVideoHeight, m_ActiveVideoFrameRate, m_ActiveVideoHeight, m_ActiveVideoFrameRate,
enableVsync, enableVsync,
enableVsync && m_Preferences->framePacing,
false,
s_ActiveSession->m_VideoDecoder)) { s_ActiveSession->m_VideoDecoder)) {
SDL_AtomicUnlock(&m_DecoderLock); SDL_AtomicUnlock(&m_DecoderLock);
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
@@ -1093,31 +1188,31 @@ void Session::exec(int displayOriginX, int displayOriginY)
case SDL_KEYUP: case SDL_KEYUP:
case SDL_KEYDOWN: case SDL_KEYDOWN:
inputHandler.handleKeyEvent(&event.key); m_InputHandler->handleKeyEvent(&event.key);
break; break;
case SDL_MOUSEBUTTONDOWN: case SDL_MOUSEBUTTONDOWN:
case SDL_MOUSEBUTTONUP: case SDL_MOUSEBUTTONUP:
inputHandler.handleMouseButtonEvent(&event.button); m_InputHandler->handleMouseButtonEvent(&event.button);
break; break;
case SDL_MOUSEMOTION: case SDL_MOUSEMOTION:
inputHandler.handleMouseMotionEvent(&event.motion); m_InputHandler->handleMouseMotionEvent(&event.motion);
break; break;
case SDL_MOUSEWHEEL: case SDL_MOUSEWHEEL:
inputHandler.handleMouseWheelEvent(&event.wheel); m_InputHandler->handleMouseWheelEvent(&event.wheel);
break; break;
case SDL_CONTROLLERAXISMOTION: case SDL_CONTROLLERAXISMOTION:
inputHandler.handleControllerAxisEvent(&event.caxis); m_InputHandler->handleControllerAxisEvent(&event.caxis);
break; break;
case SDL_CONTROLLERBUTTONDOWN: case SDL_CONTROLLERBUTTONDOWN:
case SDL_CONTROLLERBUTTONUP: case SDL_CONTROLLERBUTTONUP:
inputHandler.handleControllerButtonEvent(&event.cbutton); m_InputHandler->handleControllerButtonEvent(&event.cbutton);
break; break;
case SDL_CONTROLLERDEVICEADDED: case SDL_CONTROLLERDEVICEADDED:
case SDL_CONTROLLERDEVICEREMOVED: case SDL_CONTROLLERDEVICEREMOVED:
inputHandler.handleControllerDeviceEvent(&event.cdevice); m_InputHandler->handleControllerDeviceEvent(&event.cdevice);
break; break;
case SDL_JOYDEVICEADDED: case SDL_JOYDEVICEADDED:
inputHandler.handleJoystickArrivalEvent(&event.jdevice); m_InputHandler->handleJoystickArrivalEvent(&event.jdevice);
break; break;
// SDL2 sends touch events from trackpads by default on // SDL2 sends touch events from trackpads by default on
@@ -1127,18 +1222,13 @@ void Session::exec(int displayOriginX, int displayOriginY)
case SDL_FINGERDOWN: case SDL_FINGERDOWN:
case SDL_FINGERMOTION: case SDL_FINGERMOTION:
case SDL_FINGERUP: case SDL_FINGERUP:
inputHandler.handleTouchFingerEvent(&event.tfinger); m_InputHandler->handleTouchFingerEvent(&event.tfinger);
break; break;
#endif #endif
} }
} }
DispatchDeferredCleanup: DispatchDeferredCleanup:
#ifdef Q_OS_WIN32
// HACK: See comment above
SetThreadExecutionState(ES_CONTINUOUS);
#endif
// Uncapture the mouse and hide the window immediately, // Uncapture the mouse and hide the window immediately,
// so we can return to the Qt GUI ASAP. // so we can return to the Qt GUI ASAP.
SDL_SetRelativeMouseMode(SDL_FALSE); SDL_SetRelativeMouseMode(SDL_FALSE);
@@ -1146,7 +1236,16 @@ DispatchDeferredCleanup:
SDL_SetHint(SDL_HINT_TIMER_RESOLUTION, "0"); SDL_SetHint(SDL_HINT_TIMER_RESOLUTION, "0");
// Raise any keys that are still down // Raise any keys that are still down
inputHandler.raiseAllKeys(); m_InputHandler->raiseAllKeys();
// Destroy the input handler now. Any rumble callbacks that
// occur after this point will be discarded. This must be destroyed
// before allow the UI to continue execution or it could interfere
// with SDLGamepadKeyNavigation.
SDL_AtomicLock(&m_InputHandlerLock);
delete m_InputHandler;
m_InputHandler = nullptr;
SDL_AtomicUnlock(&m_InputHandlerLock);
// Destroy the decoder, since this must be done on the main thread // Destroy the decoder, since this must be done on the main thread
SDL_AtomicLock(&m_DecoderLock); SDL_AtomicLock(&m_DecoderLock);
@@ -1154,7 +1253,10 @@ DispatchDeferredCleanup:
m_VideoDecoder = nullptr; m_VideoDecoder = nullptr;
SDL_AtomicUnlock(&m_DecoderLock); SDL_AtomicUnlock(&m_DecoderLock);
// This must be called after the decoder is deleted, because
// the renderer may want to interact with the window
SDL_DestroyWindow(m_Window); SDL_DestroyWindow(m_Window);
if (iconSurface != nullptr) { if (iconSurface != nullptr) {
SDL_FreeSurface(iconSurface); SDL_FreeSurface(iconSurface);
} }
@@ -1164,6 +1266,6 @@ DispatchDeferredCleanup:
// Cleanup can take a while, so dispatch it to a worker thread. // Cleanup can take a while, so dispatch it to a worker thread.
// When it is complete, it will release our s_ActiveSessionSemaphore // When it is complete, it will release our s_ActiveSessionSemaphore
// reference. // reference.
QThreadPool::globalInstance()->start(new DeferredSessionCleanupTask()); QThreadPool::globalInstance()->start(new DeferredSessionCleanupTask(this));
} }
+29 -2
View File
@@ -4,11 +4,11 @@
#include <Limelight.h> #include <Limelight.h>
#include <opus_multistream.h> #include <opus_multistream.h>
#include "backend/computermanager.h"
#include "settings/streamingpreferences.h" #include "settings/streamingpreferences.h"
#include "input.h" #include "input.h"
#include "video/decoder.h" #include "video/decoder.h"
#include "audio/renderers/renderer.h" #include "audio/renderers/renderer.h"
#include "video/overlaymanager.h"
class Session : public QObject class Session : public QObject
{ {
@@ -20,6 +20,8 @@ class Session : public QObject
public: public:
explicit Session(NvComputer* computer, NvApp& app, StreamingPreferences *preferences = nullptr); explicit Session(NvComputer* computer, NvApp& app, StreamingPreferences *preferences = nullptr);
virtual ~Session();
Q_INVOKABLE void exec(int displayOriginX, int displayOriginY); Q_INVOKABLE void exec(int displayOriginX, int displayOriginY);
static static
@@ -30,6 +32,16 @@ public:
int getDecoderCapabilities(StreamingPreferences::VideoDecoderSelection vds, int getDecoderCapabilities(StreamingPreferences::VideoDecoderSelection vds,
int videoFormat, int width, int height, int frameRate); int videoFormat, int width, int height, int frameRate);
static Session* get()
{
return s_ActiveSession;
}
Overlay::OverlayManager& getOverlayManager()
{
return m_OverlayManager;
}
signals: signals:
void stageStarting(QString stage); void stageStarting(QString stage);
@@ -41,6 +53,10 @@ signals:
void displayLaunchWarning(QString text); void displayLaunchWarning(QString text);
void quitStarting();
void sessionFinished();
private: private:
void initialize(); void initialize();
@@ -66,7 +82,9 @@ private:
static static
bool chooseDecoder(StreamingPreferences::VideoDecoderSelection vds, bool chooseDecoder(StreamingPreferences::VideoDecoderSelection vds,
SDL_Window* window, int videoFormat, int width, int height, SDL_Window* window, int videoFormat, int width, int height,
int frameRate, bool enableVsync, IVideoDecoder*& chosenDecoder); int frameRate, bool enableVsync, bool enableFramePacing,
bool testOnly,
IVideoDecoder*& chosenDecoder);
static static
void clStageStarting(int stage); void clStageStarting(int stage);
@@ -80,6 +98,9 @@ private:
static static
void clLogMessage(const char* format, ...); void clLogMessage(const char* format, ...);
static
void clRumble(unsigned short controllerNumber, unsigned short lowFreqMotor, unsigned short highFreqMotor);
static static
int arInit(int audioConfiguration, int arInit(int audioConfiguration,
const POPUS_MULTISTREAM_CONFIGURATION opusConfig, const POPUS_MULTISTREAM_CONFIGURATION opusConfig,
@@ -114,6 +135,9 @@ private:
int m_DisplayOriginX; int m_DisplayOriginX;
int m_DisplayOriginY; int m_DisplayOriginY;
bool m_PendingWindowedTransition; bool m_PendingWindowedTransition;
bool m_UnexpectedTermination;
SdlInputHandler* m_InputHandler;
SDL_SpinLock m_InputHandlerLock;
int m_ActiveVideoFormat; int m_ActiveVideoFormat;
int m_ActiveVideoWidth; int m_ActiveVideoWidth;
@@ -125,6 +149,9 @@ private:
IAudioRenderer* m_AudioRenderer; IAudioRenderer* m_AudioRenderer;
OPUS_MULTISTREAM_CONFIGURATION m_AudioConfig; OPUS_MULTISTREAM_CONFIGURATION m_AudioConfig;
int m_AudioSampleCount; int m_AudioSampleCount;
Uint32 m_DropAudioEndTime;
Overlay::OverlayManager m_OverlayManager;
static AUDIO_RENDERER_CALLBACKS k_AudioCallbacks; static AUDIO_RENDERER_CALLBACKS k_AudioCallbacks;
static CONNECTION_LISTENER_CALLBACKS k_ConnCallbacks; static CONNECTION_LISTENER_CALLBACKS k_ConnCallbacks;
+5 -1
View File
@@ -12,11 +12,14 @@ typedef struct _VIDEO_STATS {
uint32_t receivedFrames; uint32_t receivedFrames;
uint32_t decodedFrames; uint32_t decodedFrames;
uint32_t renderedFrames; uint32_t renderedFrames;
uint32_t totalFrames;
uint32_t networkDroppedFrames; uint32_t networkDroppedFrames;
uint32_t pacerDroppedFrames; uint32_t pacerDroppedFrames;
uint32_t totalReassemblyTime; uint32_t totalReassemblyTime;
uint32_t totalDecodeTime; uint32_t totalDecodeTime;
uint32_t totalPacerTime;
uint32_t totalRenderTime; uint32_t totalRenderTime;
float totalFps;
float receivedFps; float receivedFps;
float decodedFps; float decodedFps;
float renderedFps; float renderedFps;
@@ -32,7 +35,8 @@ public:
int width, int width,
int height, int height,
int frameRate, int frameRate,
bool enableVsync) = 0; bool enableVsync,
bool enableFramePacing) = 0;
virtual bool isHardwareAccelerated() = 0; virtual bool isHardwareAccelerated() = 0;
virtual int getDecoderCapabilities() = 0; virtual int getDecoderCapabilities() = 0;
virtual int submitDecodeUnit(PDECODE_UNIT du) = 0; virtual int submitDecodeUnit(PDECODE_UNIT du) = 0;
+211 -62
View File
@@ -2,6 +2,7 @@
#include "dxva2.h" #include "dxva2.h"
#include "../ffmpeg.h" #include "../ffmpeg.h"
#include <streaming/streamutils.h> #include <streaming/streamutils.h>
#include <streaming/session.h>
#include <SDL_syswm.h> #include <SDL_syswm.h>
@@ -25,7 +26,9 @@ DXVA2Renderer::DXVA2Renderer() :
m_RenderTarget(nullptr), m_RenderTarget(nullptr),
m_ProcService(nullptr), m_ProcService(nullptr),
m_Processor(nullptr), m_Processor(nullptr),
m_FrameIndex(0) m_FrameIndex(0),
m_DebugOverlayFont(nullptr),
m_BlockingPresent(false)
{ {
RtlZeroMemory(m_DecSurfaces, sizeof(m_DecSurfaces)); RtlZeroMemory(m_DecSurfaces, sizeof(m_DecSurfaces));
RtlZeroMemory(&m_DXVAContext, sizeof(m_DXVAContext)); RtlZeroMemory(&m_DXVAContext, sizeof(m_DXVAContext));
@@ -44,6 +47,7 @@ DXVA2Renderer::~DXVA2Renderer()
SAFE_COM_RELEASE(m_RenderTarget); SAFE_COM_RELEASE(m_RenderTarget);
SAFE_COM_RELEASE(m_ProcService); SAFE_COM_RELEASE(m_ProcService);
SAFE_COM_RELEASE(m_Processor); SAFE_COM_RELEASE(m_Processor);
SAFE_COM_RELEASE(m_DebugOverlayFont);
for (int i = 0; i < ARRAYSIZE(m_DecSurfaces); i++) { for (int i = 0; i < ARRAYSIZE(m_DecSurfaces); i++) {
SAFE_COM_RELEASE(m_DecSurfaces[i]); SAFE_COM_RELEASE(m_DecSurfaces[i]);
@@ -256,68 +260,114 @@ bool DXVA2Renderer::initializeRenderer()
m_DisplayWidth = renderTargetDesc.Width; m_DisplayWidth = renderTargetDesc.Width;
m_DisplayHeight = renderTargetDesc.Height; m_DisplayHeight = renderTargetDesc.Height;
hr = DXVA2CreateVideoService(m_Device, IID_IDirectXVideoProcessorService, if (!isDXVideoProcessorAPIBlacklisted()) {
reinterpret_cast<void**>(&m_ProcService)); hr = DXVA2CreateVideoService(m_Device, IID_IDirectXVideoProcessorService,
reinterpret_cast<void**>(&m_ProcService));
if (FAILED(hr)) { if (FAILED(hr)) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"DXVA2CreateVideoService(IID_IDirectXVideoProcessorService) failed: %x", "DXVA2CreateVideoService(IID_IDirectXVideoProcessorService) failed: %x",
hr); hr);
return false; return false;
} }
DXVA2_VideoProcessorCaps caps; DXVA2_VideoProcessorCaps caps;
hr = m_ProcService->GetVideoProcessorCaps(DXVA2_VideoProcProgressiveDevice, &m_Desc, renderTargetDesc.Format, &caps); hr = m_ProcService->GetVideoProcessorCaps(DXVA2_VideoProcProgressiveDevice, &m_Desc, renderTargetDesc.Format, &caps);
if (FAILED(hr)) { if (FAILED(hr)) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"GetVideoProcessorCaps() failed for DXVA2_VideoProcProgressiveDevice: %x", "GetVideoProcessorCaps() failed for DXVA2_VideoProcProgressiveDevice: %x",
hr); hr);
return false; return false;
} }
if (!(caps.DeviceCaps & DXVA2_VPDev_HardwareDevice)) { if (!(caps.DeviceCaps & DXVA2_VPDev_HardwareDevice)) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"DXVA2_VideoProcProgressiveDevice is not hardware: %x", "DXVA2_VideoProcProgressiveDevice is not hardware: %x",
caps.DeviceCaps); caps.DeviceCaps);
return false; return false;
} }
else if (!(caps.VideoProcessorOperations & DXVA2_VideoProcess_YUV2RGB) && else if (!(caps.VideoProcessorOperations & DXVA2_VideoProcess_YUV2RGB) &&
!(caps.VideoProcessorOperations & DXVA2_VideoProcess_YUV2RGBExtended)) { !(caps.VideoProcessorOperations & DXVA2_VideoProcess_YUV2RGBExtended)) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"DXVA2_VideoProcProgressiveDevice can't convert YUV2RGB: %x", "DXVA2_VideoProcProgressiveDevice can't convert YUV2RGB: %x",
caps.VideoProcessorOperations); caps.VideoProcessorOperations);
return false; return false;
} }
else if (!(caps.VideoProcessorOperations & DXVA2_VideoProcess_StretchX) || else if (!(caps.VideoProcessorOperations & DXVA2_VideoProcess_StretchX) ||
!(caps.VideoProcessorOperations & DXVA2_VideoProcess_StretchY)) { !(caps.VideoProcessorOperations & DXVA2_VideoProcess_StretchY)) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"DXVA2_VideoProcProgressiveDevice can't stretch video: %x", "DXVA2_VideoProcProgressiveDevice can't stretch video: %x",
caps.VideoProcessorOperations); caps.VideoProcessorOperations);
return false; return false;
} }
if (caps.DeviceCaps & DXVA2_VPDev_EmulatedDXVA1) { if (caps.DeviceCaps & DXVA2_VPDev_EmulatedDXVA1) {
// DXVA2 over DXVA1 may have bad performance // DXVA2 over DXVA1 may have bad performance
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"DXVA2_VideoProcProgressiveDevice is DXVA1"); "DXVA2_VideoProcProgressiveDevice is DXVA1");
} }
m_ProcService->GetProcAmpRange(DXVA2_VideoProcProgressiveDevice, &m_Desc, renderTargetDesc.Format, DXVA2_ProcAmp_Brightness, &m_BrightnessRange); m_ProcService->GetProcAmpRange(DXVA2_VideoProcProgressiveDevice, &m_Desc, renderTargetDesc.Format, DXVA2_ProcAmp_Brightness, &m_BrightnessRange);
m_ProcService->GetProcAmpRange(DXVA2_VideoProcProgressiveDevice, &m_Desc, renderTargetDesc.Format, DXVA2_ProcAmp_Contrast, &m_ContrastRange); m_ProcService->GetProcAmpRange(DXVA2_VideoProcProgressiveDevice, &m_Desc, renderTargetDesc.Format, DXVA2_ProcAmp_Contrast, &m_ContrastRange);
m_ProcService->GetProcAmpRange(DXVA2_VideoProcProgressiveDevice, &m_Desc, renderTargetDesc.Format, DXVA2_ProcAmp_Hue, &m_HueRange); m_ProcService->GetProcAmpRange(DXVA2_VideoProcProgressiveDevice, &m_Desc, renderTargetDesc.Format, DXVA2_ProcAmp_Hue, &m_HueRange);
m_ProcService->GetProcAmpRange(DXVA2_VideoProcProgressiveDevice, &m_Desc, renderTargetDesc.Format, DXVA2_ProcAmp_Saturation, &m_SaturationRange); m_ProcService->GetProcAmpRange(DXVA2_VideoProcProgressiveDevice, &m_Desc, renderTargetDesc.Format, DXVA2_ProcAmp_Saturation, &m_SaturationRange);
hr = m_ProcService->CreateVideoProcessor(DXVA2_VideoProcProgressiveDevice, &m_Desc, renderTargetDesc.Format, 0, &m_Processor); hr = m_ProcService->CreateVideoProcessor(DXVA2_VideoProcProgressiveDevice, &m_Desc, renderTargetDesc.Format, 0, &m_Processor);
if (FAILED(hr)) { if (FAILED(hr)) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"CreateVideoProcessor() failed for DXVA2_VideoProcProgressiveDevice: %x", "CreateVideoProcessor() failed for DXVA2_VideoProcProgressiveDevice: %x",
hr); hr);
return false; return false;
}
} }
return true; return true;
} }
bool DXVA2Renderer::isDXVideoProcessorAPIBlacklisted()
{
IDirect3D9* d3d9;
HRESULT hr;
bool result = false;
hr = m_Device->GetDirect3D(&d3d9);
if (SUCCEEDED(hr)) {
D3DCAPS9 caps;
hr = m_Device->GetDeviceCaps(&caps);
if (SUCCEEDED(hr)) {
D3DADAPTER_IDENTIFIER9 id;
hr = d3d9->GetAdapterIdentifier(caps.AdapterOrdinal, 0, &id);
if (SUCCEEDED(hr) && id.VendorId == 0x8086) {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Avoiding IDirectXVideoProcessor API on Intel GPU");
// On Intel GPUs, we can get unwanted video "enhancements" due to post-processing
// effects that the GPU driver forces on us. In many cases, this makes the video
// actually look worse. We can avoid these by using StretchRect() instead on these
// platforms.
result = true;
}
else {
result = false;
}
}
else {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"GetDeviceCaps() failed: %x", hr);
}
d3d9->Release();
}
else {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"GetDirect3D() failed: %x", hr);
}
return result;
}
bool DXVA2Renderer::isDecoderBlacklisted() bool DXVA2Renderer::isDecoderBlacklisted()
{ {
IDirect3D9* d3d9; IDirect3D9* d3d9;
@@ -512,6 +562,8 @@ bool DXVA2Renderer::initializeDevice(SDL_Window* window, bool enableVsync)
d3dpp.BackBufferCount = 1; d3dpp.BackBufferCount = 1;
} }
m_BlockingPresent = false;
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Windowed mode with DWM running"); "Windowed mode with DWM running");
} }
@@ -521,6 +573,7 @@ bool DXVA2Renderer::initializeDevice(SDL_Window* window, bool enableVsync)
d3dpp.PresentationInterval = D3DPRESENT_INTERVAL_ONE; d3dpp.PresentationInterval = D3DPRESENT_INTERVAL_ONE;
d3dpp.SwapEffect = D3DSWAPEFFECT_DISCARD; d3dpp.SwapEffect = D3DSWAPEFFECT_DISCARD;
d3dpp.BackBufferCount = 1; d3dpp.BackBufferCount = 1;
m_BlockingPresent = true;
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"V-Sync enabled"); "V-Sync enabled");
@@ -531,6 +584,7 @@ bool DXVA2Renderer::initializeDevice(SDL_Window* window, bool enableVsync)
d3dpp.PresentationInterval = D3DPRESENT_INTERVAL_IMMEDIATE; d3dpp.PresentationInterval = D3DPRESENT_INTERVAL_IMMEDIATE;
d3dpp.SwapEffect = D3DSWAPEFFECT_DISCARD; d3dpp.SwapEffect = D3DSWAPEFFECT_DISCARD;
d3dpp.BackBufferCount = 1; d3dpp.BackBufferCount = 1;
m_BlockingPresent = false;
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"V-Sync disabled in tearing mode"); "V-Sync disabled in tearing mode");
@@ -591,9 +645,13 @@ bool DXVA2Renderer::initialize(SDL_Window* window, int videoFormat, int width, i
int alignment; int alignment;
// HEVC using DXVA requires 128B alignment // HEVC using DXVA requires 128 pixel alignment, however Intel GPUs decoding HEVC
// using StretchRect() to render draw a translucent green line at the top of
// the screen in full-screen mode at 720p/1080p unless we use 32 pixel alignment.
// This appears to work without issues on AMD and Nvidia GPUs too, so we will
// do it unconditionally for now.
if (m_VideoFormat & VIDEO_FORMAT_MASK_H265) { if (m_VideoFormat & VIDEO_FORMAT_MASK_H265) {
alignment = 128; alignment = 32;
} }
else { else {
alignment = 16; alignment = 16;
@@ -644,12 +702,39 @@ int DXVA2Renderer::getDecoderCapabilities()
return 0; return 0;
} }
IFFmpegRenderer::VSyncConstraint DXVA2Renderer::getVsyncConstraint() IFFmpegRenderer::FramePacingConstraint DXVA2Renderer::getFramePacingConstraint()
{ {
return VSYNC_ANY; return PACING_ANY;
} }
void DXVA2Renderer::renderFrameAtVsync(AVFrame *frame) void DXVA2Renderer::notifyOverlayUpdated(Overlay::OverlayType)
{
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;
}
}
}
void DXVA2Renderer::renderFrame(AVFrame *frame)
{ {
IDirect3DSurface9* surface = reinterpret_cast<IDirect3DSurface9*>(frame->data[3]); IDirect3DSurface9* surface = reinterpret_cast<IDirect3DSurface9*>(frame->data[3]);
HRESULT hr; HRESULT hr;
@@ -785,12 +870,10 @@ void DXVA2Renderer::renderFrameAtVsync(AVFrame *frame)
bltParams.Alpha = DXVA2_Fixed32OpaqueAlpha(); bltParams.Alpha = DXVA2_Fixed32OpaqueAlpha();
m_Device->Clear(0, nullptr, D3DCLEAR_TARGET, D3DCOLOR_ARGB(255, 0, 0, 0), 0.0f, 0); hr = m_Device->Clear(0, nullptr, D3DCLEAR_TARGET, D3DCOLOR_ARGB(255, 0, 0, 0), 0.0f, 0);
hr = m_Processor->VideoProcessBlt(m_RenderTarget, &bltParams, &sample, 1, nullptr);
if (FAILED(hr)) { if (FAILED(hr)) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"VideoProcessBlt() failed: %x", "Clear() failed: %x",
hr); hr);
SDL_Event event; SDL_Event event;
event.type = SDL_RENDER_TARGETS_RESET; event.type = SDL_RENDER_TARGETS_RESET;
@@ -798,7 +881,73 @@ void DXVA2Renderer::renderFrameAtVsync(AVFrame *frame)
return; return;
} }
hr = m_Device->PresentEx(nullptr, nullptr, nullptr, nullptr, 0); hr = m_Device->BeginScene();
if (FAILED(hr)) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"BeginScene() failed: %x",
hr);
SDL_Event event;
event.type = SDL_RENDER_TARGETS_RESET;
SDL_PushEvent(&event);
return;
}
if (m_Processor) {
hr = m_Processor->VideoProcessBlt(m_RenderTarget, &bltParams, &sample, 1, nullptr);
if (FAILED(hr)) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"VideoProcessBlt() failed, falling back to StretchRect(): %x",
hr);
m_Processor->Release();
m_Processor = nullptr;
}
}
if (!m_Processor) {
// This function doesn't trigger any of Intel's garbage video "enhancements"
hr = m_Device->StretchRect(surface, &sample.SrcRect, m_RenderTarget, &sample.DstRect, D3DTEXF_NONE);
if (FAILED(hr)) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"StretchRect() failed: %x",
hr);
SDL_Event event;
event.type = SDL_RENDER_TARGETS_RESET;
SDL_PushEvent(&event);
return;
}
}
if (m_DebugOverlayFont != nullptr) {
if (Session::get()->getOverlayManager().isOverlayEnabled(Overlay::OverlayDebug)) {
SDL_Color color = Session::get()->getOverlayManager().getOverlayColor(Overlay::OverlayDebug);
m_DebugOverlayFont->DrawTextA(nullptr,
Session::get()->getOverlayManager().getOverlayText(Overlay::OverlayDebug),
-1,
&sample.DstRect,
DT_LEFT | DT_NOCLIP,
D3DCOLOR_ARGB(color.a, color.r, color.g, color.b));
}
}
hr = m_Device->EndScene();
if (FAILED(hr)) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"EndScene() failed: %x",
hr);
SDL_Event event;
event.type = SDL_RENDER_TARGETS_RESET;
SDL_PushEvent(&event);
return;
}
do {
// Use D3DPRESENT_DONOTWAIT if present may block in order to avoid holding the giant
// lock around this D3D device for excessive lengths of time (blocking concurrent decoding tasks).
hr = m_Device->PresentEx(nullptr, nullptr, nullptr, nullptr, m_BlockingPresent ? D3DPRESENT_DONOTWAIT : 0);
if (hr == D3DERR_WASSTILLDRAWING) {
SDL_Delay(1);
}
} while (hr == D3DERR_WASSTILLDRAWING);
if (FAILED(hr)) { if (FAILED(hr)) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"PresentEx() failed: %x", "PresentEx() failed: %x",
+11 -6
View File
@@ -4,6 +4,7 @@
#include "pacer/pacer.h" #include "pacer/pacer.h"
#include <d3d9.h> #include <d3d9.h>
#include <d3dx9.h>
#include <dxva2api.h> #include <dxva2api.h>
extern "C" { extern "C" {
@@ -20,18 +21,20 @@ public:
int width, int width,
int height, int height,
int maxFps, int maxFps,
bool enableVsync); bool enableVsync) override;
virtual bool prepareDecoderContext(AVCodecContext* context); virtual bool prepareDecoderContext(AVCodecContext* context) override;
virtual void renderFrameAtVsync(AVFrame* frame); virtual void renderFrame(AVFrame* frame) override;
virtual bool needsTestFrame(); virtual bool needsTestFrame() override;
virtual int getDecoderCapabilities(); virtual int getDecoderCapabilities() override;
virtual VSyncConstraint getVsyncConstraint(); virtual FramePacingConstraint getFramePacingConstraint() override;
virtual void notifyOverlayUpdated(Overlay::OverlayType) override;
private: private:
bool initializeDecoder(); bool initializeDecoder();
bool initializeRenderer(); bool initializeRenderer();
bool initializeDevice(SDL_Window* window, bool enableVsync); bool initializeDevice(SDL_Window* window, bool enableVsync);
bool isDecoderBlacklisted(); bool isDecoderBlacklisted();
bool isDXVideoProcessorAPIBlacklisted();
static static
AVBufferRef* ffPoolAlloc(void* opaque, int size); AVBufferRef* ffPoolAlloc(void* opaque, int size);
@@ -67,4 +70,6 @@ private:
DXVA2_ValueRange m_SaturationRange; DXVA2_ValueRange m_SaturationRange;
DXVA2_VideoDesc m_Desc; DXVA2_VideoDesc m_Desc;
REFERENCE_TIME m_FrameIndex; REFERENCE_TIME m_FrameIndex;
LPD3DXFONT m_DebugOverlayFont;
bool m_BlockingPresent;
}; };
@@ -8,11 +8,12 @@
#endif #endif
#ifdef Q_OS_WIN32 #ifdef Q_OS_WIN32
#define WIN32_LEAN_AND_MEAN
#include <Windows.h>
#include <VersionHelpers.h>
#include "dxvsyncsource.h" #include "dxvsyncsource.h"
#endif #endif
#define FRAME_HISTORY_ENTRIES 8
// We may be woken up slightly late so don't go all the way // We may be woken up slightly late so don't go all the way
// up to the next V-sync since we may accidentally step into // up to the next V-sync since we may accidentally step into
// the next V-sync period. It also takes some amount of time // the next V-sync period. It also takes some amount of time
@@ -21,7 +22,8 @@
#define TIMER_SLACK_MS 3 #define TIMER_SLACK_MS 3
Pacer::Pacer(IFFmpegRenderer* renderer, PVIDEO_STATS videoStats) : Pacer::Pacer(IFFmpegRenderer* renderer, PVIDEO_STATS videoStats) :
m_FrameQueueLock(0), m_RenderThread(nullptr),
m_Stopping(false),
m_VsyncSource(nullptr), m_VsyncSource(nullptr),
m_VsyncRenderer(renderer), m_VsyncRenderer(renderer),
m_MaxVideoFps(0), m_MaxVideoFps(0),
@@ -37,10 +39,70 @@ Pacer::~Pacer()
delete m_VsyncSource; delete m_VsyncSource;
m_VsyncSource = nullptr; m_VsyncSource = nullptr;
while (!m_FrameQueue.isEmpty()) { // Stop the render thread
AVFrame* frame = m_FrameQueue.dequeue(); m_Stopping = true;
if (m_RenderThread != nullptr) {
m_RenderQueueNotEmpty.wakeAll();
SDL_WaitThread(m_RenderThread, nullptr);
}
// Delete any remaining unconsumed frames
while (!m_RenderQueue.isEmpty()) {
AVFrame* frame = m_RenderQueue.dequeue();
av_frame_free(&frame); av_frame_free(&frame);
} }
while (!m_PacingQueue.isEmpty()) {
AVFrame* frame = m_PacingQueue.dequeue();
av_frame_free(&frame);
}
}
int Pacer::renderThread(void* context)
{
Pacer* me = reinterpret_cast<Pacer*>(context);
while (!me->m_Stopping) {
// Acquire the frame queue lock to protect the queue and
// the not empty condition
me->m_FrameQueueLock.lock();
// Wait for a frame to be ready to render
while (!me->m_Stopping && me->m_RenderQueue.isEmpty()) {
me->m_RenderQueueNotEmpty.wait(&me->m_FrameQueueLock);
}
if (me->m_Stopping) {
// Exit this thread
me->m_FrameQueueLock.unlock();
break;
}
// Dequeue the most recent frame for rendering and free the others.
// NB: m_FrameQueueLock still held here!
AVFrame* lastFrame = nullptr;
while (!me->m_RenderQueue.isEmpty()) {
if (lastFrame != nullptr) {
// Don't hold the frame queue lock across av_frame_free(),
// since it could need to talk to the GPU driver. This is safe
// because we're guaranteed that the queue will not shrink during
// this time (and so dequeue() below will always get something).
me->m_FrameQueueLock.unlock();
av_frame_free(&lastFrame);
me->m_VideoStats->pacerDroppedFrames++;
me->m_FrameQueueLock.lock();
}
lastFrame = me->m_RenderQueue.dequeue();
}
// Release the frame queue lock before rendering
me->m_FrameQueueLock.unlock();
// Render and free the mot current frame
me->renderFrame(lastFrame);
}
return 0;
} }
// Called in an arbitrary thread by the IVsyncSource on V-sync // Called in an arbitrary thread by the IVsyncSource on V-sync
@@ -52,9 +114,7 @@ void Pacer::vsyncCallback(int timeUntilNextVsyncMillis)
SDL_assert(timeUntilNextVsyncMillis >= TIMER_SLACK_MS); SDL_assert(timeUntilNextVsyncMillis >= TIMER_SLACK_MS);
Uint32 vsyncCallbackStartTime = SDL_GetTicks(); m_FrameQueueLock.lock();
SDL_AtomicLock(&m_FrameQueueLock);
// If the queue length history entries are large, be strict // If the queue length history entries are large, be strict
// about dropping excess frames. // about dropping excess frames.
@@ -64,8 +124,8 @@ void Pacer::vsyncCallback(int timeUntilNextVsyncMillis)
// frame history to drop frames only if consistently above the // frame history to drop frames only if consistently above the
// one queued frame mark. // one queued frame mark.
if (m_MaxVideoFps >= m_DisplayFps) { if (m_MaxVideoFps >= m_DisplayFps) {
for (int i = 0; i < m_FrameQueueHistory.count(); i++) { for (int queueHistoryEntry : m_PacingQueueHistory) {
if (m_FrameQueueHistory[i] <= 1) { if (queueHistoryEntry <= 1) {
// Be lenient as long as the queue length // Be lenient as long as the queue length
// resolves before the end of frame history // resolves before the end of frame history
frameDropTarget = 3; frameDropTarget = 3;
@@ -73,70 +133,58 @@ void Pacer::vsyncCallback(int timeUntilNextVsyncMillis)
} }
} }
if (m_FrameQueueHistory.count() == FRAME_HISTORY_ENTRIES) { // Keep a rolling 500 ms window of pacing queue history
m_FrameQueueHistory.dequeue(); if (m_PacingQueueHistory.count() == m_DisplayFps / 2) {
m_PacingQueueHistory.dequeue();
} }
m_FrameQueueHistory.enqueue(m_FrameQueue.count()); m_PacingQueueHistory.enqueue(m_PacingQueue.count());
} }
// Catch up if we're several frames ahead // Catch up if we're several frames ahead
while (m_FrameQueue.count() > frameDropTarget) { while (m_PacingQueue.count() > frameDropTarget) {
AVFrame* frame = m_FrameQueue.dequeue(); AVFrame* frame = m_PacingQueue.dequeue();
// Drop the lock while we call av_frame_free()
m_FrameQueueLock.unlock();
m_VideoStats->pacerDroppedFrames++; m_VideoStats->pacerDroppedFrames++;
av_frame_free(&frame); av_frame_free(&frame);
m_FrameQueueLock.lock();
} }
if (m_PacingQueue.isEmpty()) {
if (m_FrameQueue.isEmpty()) { // Wait for a frame to arrive or our V-sync timeout to expire
SDL_AtomicUnlock(&m_FrameQueueLock); if (!m_PacingQueueNotEmpty.wait(&m_FrameQueueLock, timeUntilNextVsyncMillis - TIMER_SLACK_MS)) {
// Wait timed out - unlock and bail
while (!SDL_TICKS_PASSED(SDL_GetTicks(), m_FrameQueueLock.unlock();
vsyncCallbackStartTime + timeUntilNextVsyncMillis - TIMER_SLACK_MS)) { return;
SDL_Delay(1);
SDL_AtomicLock(&m_FrameQueueLock);
if (!m_FrameQueue.isEmpty()) {
// Don't release the lock
goto RenderNextFrame;
}
SDL_AtomicUnlock(&m_FrameQueueLock);
} }
// Nothing to render at this time
return;
} }
RenderNextFrame: // Place the first frame on the render queue
// Grab the first frame m_RenderQueue.enqueue(m_PacingQueue.dequeue());
AVFrame* frame = m_FrameQueue.dequeue(); m_FrameQueueLock.unlock();
SDL_AtomicUnlock(&m_FrameQueueLock); m_RenderQueueNotEmpty.wakeOne();
// Render it
Uint32 beforeRender = SDL_GetTicks();
m_VsyncRenderer->renderFrameAtVsync(frame);
m_VideoStats->totalRenderTime += SDL_GetTicks() - beforeRender;
m_VideoStats->renderedFrames++;
// Free the frame
av_frame_free(&frame);
} }
bool Pacer::initialize(SDL_Window* window, int maxVideoFps, bool enableVsync) bool Pacer::initialize(SDL_Window* window, int maxVideoFps, bool enablePacing)
{ {
m_MaxVideoFps = maxVideoFps; m_MaxVideoFps = maxVideoFps;
m_EnableVsync = enableVsync;
m_DisplayFps = StreamUtils::getDisplayRefreshRate(window); m_DisplayFps = StreamUtils::getDisplayRefreshRate(window);
if (m_EnableVsync) { if (enablePacing) {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Frame pacing in tear-free mode: target %d Hz with %d FPS stream", "Frame pacing active: target %d Hz with %d FPS stream",
m_DisplayFps, m_MaxVideoFps); m_DisplayFps, m_MaxVideoFps);
#if defined(Q_OS_DARWIN) #if defined(Q_OS_DARWIN)
m_VsyncSource = DisplayLinkVsyncSourceFactory::createVsyncSource(this); m_VsyncSource = DisplayLinkVsyncSourceFactory::createVsyncSource(this);
#elif defined(Q_OS_WIN32) #elif defined(Q_OS_WIN32)
m_VsyncSource = new DxVsyncSource(this); // Don't use D3DKMTWaitForVerticalBlankEvent() on Windows 7, because
// it blocks during other concurrent DX operations (like actually rendering).
if (IsWindows8OrGreater()) {
m_VsyncSource = new DxVsyncSource(this);
}
#else #else
// Platforms without a VsyncSource will just render frames // Platforms without a VsyncSource will just render frames
// immediately like they used to. // immediately like they used to.
@@ -148,36 +196,81 @@ bool Pacer::initialize(SDL_Window* window, int maxVideoFps, bool enableVsync)
} }
else { else {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Minimal latency tearing mode: target %d Hz with %d FPS stream", "Frame pacing disabled: target %d Hz with %d FPS stream",
m_DisplayFps, m_MaxVideoFps); m_DisplayFps, m_MaxVideoFps);
} }
m_RenderThread = SDL_CreateThread(Pacer::renderThread, "Pacer Render Thread", this);
return true; return true;
} }
void Pacer::renderFrame(AVFrame* frame)
{
// Count time spent in Pacer's queues
Uint32 beforeRender = SDL_GetTicks();
m_VideoStats->totalPacerTime += beforeRender - frame->pts;
// Render it
m_VsyncRenderer->renderFrame(frame);
Uint32 afterRender = SDL_GetTicks();
m_VideoStats->totalRenderTime += afterRender - beforeRender;
m_VideoStats->renderedFrames++;
av_frame_free(&frame);
// Drop frames if we have too many queued up for a while
m_FrameQueueLock.lock();
int frameDropTarget = 0;
for (int queueHistoryEntry : m_RenderQueueHistory) {
if (queueHistoryEntry == 0) {
// Be lenient as long as the queue length
// resolves before the end of frame history
frameDropTarget = 2;
break;
}
}
// Keep a rolling 500 ms window of render queue history
if (m_RenderQueueHistory.count() == m_MaxVideoFps / 2) {
m_RenderQueueHistory.dequeue();
}
m_RenderQueueHistory.enqueue(m_RenderQueue.count());
// Catch up if we're several frames ahead
while (m_RenderQueue.count() > frameDropTarget) {
AVFrame* frame = m_RenderQueue.dequeue();
// Drop the lock while we call av_frame_free()
m_FrameQueueLock.unlock();
m_VideoStats->pacerDroppedFrames++;
av_frame_free(&frame);
m_FrameQueueLock.lock();
}
m_FrameQueueLock.unlock();
}
void Pacer::submitFrame(AVFrame* frame) void Pacer::submitFrame(AVFrame* frame)
{ {
// Make sure initialize() has been called // Make sure initialize() has been called
SDL_assert(m_MaxVideoFps != 0); SDL_assert(m_MaxVideoFps != 0);
// Queue the frame until the V-sync callback if // Queue the frame and possibly wake up the render thread
// we have a V-sync source, otherwise deliver it m_FrameQueueLock.lock();
// immediately and hope for the best. if (m_VsyncSource != nullptr) {
if (isUsingFrameQueue()) { m_PacingQueue.enqueue(frame);
SDL_AtomicLock(&m_FrameQueueLock);
m_FrameQueue.enqueue(frame);
SDL_AtomicUnlock(&m_FrameQueueLock);
} }
else { else {
Uint32 beforeRender = SDL_GetTicks(); m_RenderQueue.enqueue(frame);
m_VsyncRenderer->renderFrameAtVsync(frame); }
m_VideoStats->totalRenderTime += SDL_GetTicks() - beforeRender; m_FrameQueueLock.unlock();
m_VideoStats->renderedFrames++; if (m_VsyncSource != nullptr) {
av_frame_free(&frame); m_PacingQueueNotEmpty.wakeOne();
}
else {
m_RenderQueueNotEmpty.wakeOne();
} }
} }
bool Pacer::isUsingFrameQueue()
{
return m_VsyncSource != nullptr;
}
@@ -4,6 +4,8 @@
#include "../renderer.h" #include "../renderer.h"
#include <QQueue> #include <QQueue>
#include <QMutex>
#include <QWaitCondition>
class IVsyncSource { class IVsyncSource {
public: public:
@@ -20,21 +22,28 @@ public:
void submitFrame(AVFrame* frame); void submitFrame(AVFrame* frame);
bool initialize(SDL_Window* window, int maxVideoFps, bool enableVsync); bool initialize(SDL_Window* window, int maxVideoFps, bool enablePacing);
void vsyncCallback(int timeUntilNextVsyncMillis); void vsyncCallback(int timeUntilNextVsyncMillis);
bool isUsingFrameQueue();
private: private:
QQueue<AVFrame*> m_FrameQueue; static int renderThread(void* context);
QQueue<int> m_FrameQueueHistory;
SDL_SpinLock m_FrameQueueLock; void renderFrame(AVFrame* frame);
QQueue<AVFrame*> m_RenderQueue;
QQueue<AVFrame*> m_PacingQueue;
QQueue<int> m_PacingQueueHistory;
QQueue<int> m_RenderQueueHistory;
QMutex m_FrameQueueLock;
QWaitCondition m_RenderQueueNotEmpty;
QWaitCondition m_PacingQueueNotEmpty;
SDL_Thread* m_RenderThread;
bool m_Stopping;
IVsyncSource* m_VsyncSource; IVsyncSource* m_VsyncSource;
IFFmpegRenderer* m_VsyncRenderer; IFFmpegRenderer* m_VsyncRenderer;
int m_MaxVideoFps; int m_MaxVideoFps;
int m_DisplayFps; int m_DisplayFps;
bool m_EnableVsync;
PVIDEO_STATS m_VideoStats; PVIDEO_STATS m_VideoStats;
}; };
+13 -28
View File
@@ -2,19 +2,20 @@
#include <SDL.h> #include <SDL.h>
#include "streaming/video/overlaymanager.h"
extern "C" { extern "C" {
#include <libavcodec/avcodec.h> #include <libavcodec/avcodec.h>
} }
class IFFmpegRenderer { class IFFmpegRenderer : public Overlay::IOverlayRenderer {
public: public:
enum VSyncConstraint { enum FramePacingConstraint {
VSYNC_FORCE_OFF, PACING_FORCE_OFF,
VSYNC_FORCE_ON, PACING_FORCE_ON,
VSYNC_ANY PACING_ANY
}; };
virtual ~IFFmpegRenderer() {}
virtual bool initialize(SDL_Window* window, virtual bool initialize(SDL_Window* window,
int videoFormat, int videoFormat,
int width, int width,
@@ -22,29 +23,13 @@ public:
int maxFps, int maxFps,
bool enableVsync) = 0; bool enableVsync) = 0;
virtual bool prepareDecoderContext(AVCodecContext* context) = 0; virtual bool prepareDecoderContext(AVCodecContext* context) = 0;
virtual void renderFrameAtVsync(AVFrame* frame) = 0; virtual void renderFrame(AVFrame* frame) = 0;
virtual bool needsTestFrame() = 0; virtual bool needsTestFrame() = 0;
virtual int getDecoderCapabilities() = 0; virtual int getDecoderCapabilities() = 0;
virtual VSyncConstraint getVsyncConstraint() = 0; virtual FramePacingConstraint getFramePacingConstraint() = 0;
};
class SdlRenderer : public IFFmpegRenderer { // IOverlayRenderer
public: virtual void notifyOverlayUpdated(Overlay::OverlayType) override {
SdlRenderer(); // Nothing
virtual ~SdlRenderer(); }
virtual bool initialize(SDL_Window* window,
int videoFormat,
int width,
int height,
int maxFps,
bool enableVsync);
virtual bool prepareDecoderContext(AVCodecContext* context);
virtual void renderFrameAtVsync(AVFrame* frame);
virtual bool needsTestFrame();
virtual int getDecoderCapabilities();
virtual VSyncConstraint getVsyncConstraint();
private:
SDL_Renderer* m_Renderer;
SDL_Texture* m_Texture;
}; };
+108 -6
View File
@@ -1,16 +1,45 @@
#include "renderer.h" #include "sdlvid.h"
#include "streaming/session.h"
#include "path.h"
#include <QDir>
#include <Limelight.h> #include <Limelight.h>
SdlRenderer::SdlRenderer() SdlRenderer::SdlRenderer()
: m_Renderer(nullptr), : m_Renderer(nullptr),
m_Texture(nullptr) m_Texture(nullptr),
m_DebugOverlayFont(nullptr),
m_DebugOverlaySurface(nullptr),
m_DebugOverlayTexture(nullptr)
{ {
SDL_assert(TTF_WasInit() == 0);
if (TTF_Init() != 0) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"TTF_Init() failed: %s",
TTF_GetError());
return;
}
} }
SdlRenderer::~SdlRenderer() SdlRenderer::~SdlRenderer()
{ {
if (m_DebugOverlayFont != nullptr) {
TTF_CloseFont(m_DebugOverlayFont);
}
TTF_Quit();
SDL_assert(TTF_WasInit() == 0);
if (m_DebugOverlayTexture != nullptr) {
SDL_DestroyTexture(m_DebugOverlayTexture);
}
if (m_DebugOverlaySurface != nullptr) {
SDL_FreeSurface(m_DebugOverlaySurface);
}
if (m_Texture != nullptr) { if (m_Texture != nullptr) {
SDL_DestroyTexture(m_Texture); SDL_DestroyTexture(m_Texture);
} }
@@ -43,9 +72,53 @@ int SdlRenderer::getDecoderCapabilities()
return CAPABILITY_REFERENCE_FRAME_INVALIDATION_AVC; return CAPABILITY_REFERENCE_FRAME_INVALIDATION_AVC;
} }
IFFmpegRenderer::VSyncConstraint SdlRenderer::getVsyncConstraint() IFFmpegRenderer::FramePacingConstraint SdlRenderer::getFramePacingConstraint()
{ {
return VSYNC_ANY; 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, bool SdlRenderer::initialize(SDL_Window* window,
@@ -98,7 +171,7 @@ bool SdlRenderer::initialize(SDL_Window* window,
return true; return true;
} }
void SdlRenderer::renderFrameAtVsync(AVFrame* frame) void SdlRenderer::renderFrame(AVFrame* frame)
{ {
SDL_UpdateYUVTexture(m_Texture, nullptr, SDL_UpdateYUVTexture(m_Texture, nullptr,
frame->data[0], frame->data[0],
@@ -109,6 +182,35 @@ void SdlRenderer::renderFrameAtVsync(AVFrame* frame)
frame->linesize[2]); frame->linesize[2]);
SDL_RenderClear(m_Renderer); SDL_RenderClear(m_Renderer);
// Draw the video content itself
SDL_RenderCopy(m_Renderer, m_Texture, nullptr, nullptr); SDL_RenderCopy(m_Renderer, m_Texture, nullptr, nullptr);
// Draw the overlays
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);
}
}
SDL_RenderPresent(m_Renderer); SDL_RenderPresent(m_Renderer);
} }
@@ -0,0 +1,32 @@
#pragma once
#include "renderer.h"
#include <SDL_ttf.h>
class SdlRenderer : public IFFmpegRenderer {
public:
SdlRenderer();
virtual ~SdlRenderer() override;
virtual bool initialize(SDL_Window* window,
int videoFormat,
int width,
int height,
int maxFps,
bool enableVsync) override;
virtual bool prepareDecoderContext(AVCodecContext* context) override;
virtual void renderFrame(AVFrame* frame) override;
virtual bool needsTestFrame() override;
virtual int getDecoderCapabilities() override;
virtual FramePacingConstraint getFramePacingConstraint() override;
virtual void notifyOverlayUpdated(Overlay::OverlayType) override;
private:
SDL_Renderer* m_Renderer;
SDL_Texture* m_Texture;
TTF_Font* m_DebugOverlayFont;
SDL_Surface* m_DebugOverlaySurface;
SDL_Texture* m_DebugOverlayTexture;
SDL_Rect m_DebugOverlayRect;
};
+20 -3
View File
@@ -1,3 +1,5 @@
#include <QString>
#include "vaapi.h" #include "vaapi.h"
#include <streaming/streamutils.h> #include <streaming/streamutils.h>
@@ -114,6 +116,21 @@ VAAPIRenderer::initialize(SDL_Window* window, int, int width, int height, int, b
"Driver: %s", "Driver: %s",
vendorString ? vendorString : "<unknown>"); 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") {
QString vendorStr(vendorString);
if (vendorStr.contains("AMD", Qt::CaseInsensitive) ||
vendorStr.contains("Radeon", Qt::CaseInsensitive)) {
// Fail and let VDPAU pick this up
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"Avoiding VAAPI on AMD driver");
return false;
}
}
// This will populate the driver_quirks // This will populate the driver_quirks
err = av_hwdevice_ctx_init(m_HwContext); err = av_hwdevice_ctx_init(m_HwContext);
if (err < 0) { if (err < 0) {
@@ -160,13 +177,13 @@ VAAPIRenderer::getDecoderCapabilities()
return 0; return 0;
} }
IFFmpegRenderer::VSyncConstraint VAAPIRenderer::getVsyncConstraint() IFFmpegRenderer::FramePacingConstraint VAAPIRenderer::getFramePacingConstraint()
{ {
return VSYNC_ANY; return PACING_ANY;
} }
void void
VAAPIRenderer::renderFrameAtVsync(AVFrame* frame) VAAPIRenderer::renderFrame(AVFrame* frame)
{ {
VASurfaceID surface = (VASurfaceID)(uintptr_t)frame->data[3]; VASurfaceID surface = (VASurfaceID)(uintptr_t)frame->data[3];
AVHWDeviceContext* deviceContext = (AVHWDeviceContext*)m_HwContext->data; AVHWDeviceContext* deviceContext = (AVHWDeviceContext*)m_HwContext->data;
+2 -2
View File
@@ -37,10 +37,10 @@ public:
int maxFps, int maxFps,
bool enableVsync); bool enableVsync);
virtual bool prepareDecoderContext(AVCodecContext* context); virtual bool prepareDecoderContext(AVCodecContext* context);
virtual void renderFrameAtVsync(AVFrame* frame); virtual void renderFrame(AVFrame* frame);
virtual bool needsTestFrame(); virtual bool needsTestFrame();
virtual int getDecoderCapabilities(); virtual int getDecoderCapabilities();
virtual VSyncConstraint getVsyncConstraint(); virtual FramePacingConstraint getFramePacingConstraint();
private: private:
int m_WindowSystem; int m_WindowSystem;
@@ -246,12 +246,12 @@ int VDPAURenderer::getDecoderCapabilities()
return 0; return 0;
} }
IFFmpegRenderer::VSyncConstraint VDPAURenderer::getVsyncConstraint() IFFmpegRenderer::FramePacingConstraint VDPAURenderer::getFramePacingConstraint()
{ {
return VSYNC_ANY; return PACING_ANY;
} }
void VDPAURenderer::renderFrameAtVsync(AVFrame* frame) void VDPAURenderer::renderFrame(AVFrame* frame)
{ {
VdpStatus status; VdpStatus status;
VdpVideoSurface videoSurface = (VdpVideoSurface)(uintptr_t)frame->data[3]; VdpVideoSurface videoSurface = (VdpVideoSurface)(uintptr_t)frame->data[3];
@@ -335,8 +335,8 @@ void VDPAURenderer::renderFrameAtVsync(AVFrame* frame)
0, nullptr, 0, nullptr,
&sourceRect, &sourceRect,
chosenSurface, chosenSurface,
&outputRect,
nullptr, nullptr,
&outputRect,
0, 0,
nullptr); nullptr);
if (status != VDP_STATUS_OK) { if (status != VDP_STATUS_OK) {
+2 -2
View File
@@ -20,10 +20,10 @@ public:
int maxFps, int maxFps,
bool enableVsync); bool enableVsync);
virtual bool prepareDecoderContext(AVCodecContext* context); virtual bool prepareDecoderContext(AVCodecContext* context);
virtual void renderFrameAtVsync(AVFrame* frame); virtual void renderFrame(AVFrame* frame);
virtual bool needsTestFrame(); virtual bool needsTestFrame();
virtual int getDecoderCapabilities(); virtual int getDecoderCapabilities();
virtual VSyncConstraint getVsyncConstraint(); virtual FramePacingConstraint getFramePacingConstraint();
private: private:
uint32_t m_VideoWidth, m_VideoHeight; uint32_t m_VideoWidth, m_VideoHeight;
+61 -15
View File
@@ -7,11 +7,13 @@
#include <SDL_syswm.h> #include <SDL_syswm.h>
#include <Limelight.h> #include <Limelight.h>
#include <streaming/session.h>
#include <mach/mach_time.h> #include <mach/mach_time.h>
#import <Cocoa/Cocoa.h> #import <Cocoa/Cocoa.h>
#import <VideoToolbox/VideoToolbox.h> #import <VideoToolbox/VideoToolbox.h>
#import <AVFoundation/AVFoundation.h> #import <AVFoundation/AVFoundation.h>
#import <dispatch/dispatch.h>
class VTRenderer : public IFFmpegRenderer class VTRenderer : public IFFmpegRenderer
{ {
@@ -20,7 +22,8 @@ public:
: m_HwContext(nullptr), : m_HwContext(nullptr),
m_DisplayLayer(nullptr), m_DisplayLayer(nullptr),
m_FormatDesc(nullptr), m_FormatDesc(nullptr),
m_View(nullptr) m_StreamView(nullptr),
m_DebugOverlayTextField(nullptr)
{ {
} }
@@ -34,13 +37,17 @@ public:
CFRelease(m_FormatDesc); CFRelease(m_FormatDesc);
} }
if (m_View != nullptr) { if (m_DebugOverlayTextField != nullptr) {
[m_View removeFromSuperview]; [m_DebugOverlayTextField removeFromSuperview];
}
if (m_StreamView != nullptr) {
[m_StreamView removeFromSuperview];
} }
} }
// Caller frees frame after we return // Caller frees frame after we return
virtual void renderFrameAtVsync(AVFrame* frame) override virtual void renderFrame(AVFrame* frame) override
{ {
OSStatus status; OSStatus status;
CVPixelBufferRef pixBuf = reinterpret_cast<CVPixelBufferRef>(frame->data[3]); CVPixelBufferRef pixBuf = reinterpret_cast<CVPixelBufferRef>(frame->data[3]);
@@ -159,21 +166,21 @@ public:
// SDL adds its own content view to listen for events. // SDL adds its own content view to listen for events.
// We need to add a subview for our display layer. // We need to add a subview for our display layer.
NSView* contentView = info.info.cocoa.window.contentView; NSView* contentView = info.info.cocoa.window.contentView;
m_View = [[NSView alloc] initWithFrame:contentView.bounds]; m_StreamView = [[NSView alloc] initWithFrame:contentView.bounds];
m_DisplayLayer = [[AVSampleBufferDisplayLayer alloc] init]; m_DisplayLayer = [[AVSampleBufferDisplayLayer alloc] init];
m_DisplayLayer.bounds = m_View.bounds; m_DisplayLayer.bounds = m_StreamView.bounds;
m_DisplayLayer.position = CGPointMake(CGRectGetMidX(m_View.bounds), CGRectGetMidY(m_View.bounds)); m_DisplayLayer.position = CGPointMake(CGRectGetMidX(m_StreamView.bounds), CGRectGetMidY(m_StreamView.bounds));
m_DisplayLayer.videoGravity = AVLayerVideoGravityResizeAspect; m_DisplayLayer.videoGravity = AVLayerVideoGravityResizeAspect;
// Create a layer-hosted view by setting the layer before wantsLayer // Create a layer-hosted view by setting the layer before wantsLayer
// This avoids us having to add our AVSampleBufferDisplayLayer as a // This avoids us having to add our AVSampleBufferDisplayLayer as a
// sublayer of a layer-backed view which leaves a useless layer in // sublayer of a layer-backed view which leaves a useless layer in
// the middle. // the middle.
m_View.layer = m_DisplayLayer; m_StreamView.layer = m_DisplayLayer;
m_View.wantsLayer = YES; m_StreamView.wantsLayer = YES;
[contentView addSubview: m_View]; [contentView addSubview: m_StreamView];
err = av_hwdevice_ctx_create(&m_HwContext, err = av_hwdevice_ctx_create(&m_HwContext,
AV_HWDEVICE_TYPE_VIDEOTOOLBOX, AV_HWDEVICE_TYPE_VIDEOTOOLBOX,
@@ -190,6 +197,41 @@ public:
return true; return true;
} }
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];
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)]];
[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)];
}
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) {
dispatch_async_f(dispatch_get_main_queue(), this, updateDebugOverlayOnMainThread);
}
}
virtual bool prepareDecoderContext(AVCodecContext* context) override virtual bool prepareDecoderContext(AVCodecContext* context) override
{ {
context->hw_device_ctx = av_buffer_ref(m_HwContext); context->hw_device_ctx = av_buffer_ref(m_HwContext);
@@ -202,8 +244,11 @@ public:
virtual bool needsTestFrame() override virtual bool needsTestFrame() override
{ {
// We query VT to determine whether the codec is supported // We used to trust VT to tell us whether decode will work, but
return false; // there are cases where it can lie because the hardware technically
// can decode the format but VT is unserviceable for some other reason.
// Decoding the test frame will tell us for sure whether it will work.
return true;
} }
virtual int getDecoderCapabilities() override virtual int getDecoderCapabilities() override
@@ -211,19 +256,20 @@ public:
return 0; return 0;
} }
virtual IFFmpegRenderer::VSyncConstraint getVsyncConstraint() override virtual IFFmpegRenderer::FramePacingConstraint getFramePacingConstraint() override
{ {
// This renderer is inherently tied to V-sync due how we're // This renderer is inherently tied to V-sync due how we're
// rendering with AVSampleBufferDisplay layer. Running without // rendering with AVSampleBufferDisplay layer. Running without
// the V-Sync source leads to massive stuttering. // the V-Sync source leads to massive stuttering.
return VSYNC_FORCE_ON; return PACING_FORCE_ON;
} }
private: private:
AVBufferRef* m_HwContext; AVBufferRef* m_HwContext;
AVSampleBufferDisplayLayer* m_DisplayLayer; AVSampleBufferDisplayLayer* m_DisplayLayer;
CMVideoFormatDescriptionRef m_FormatDesc; CMVideoFormatDescriptionRef m_FormatDesc;
NSView* m_View; NSView* m_StreamView;
NSTextField* m_DebugOverlayTextField;
}; };
IFFmpegRenderer* VTRendererFactory::createRenderer() { IFFmpegRenderer* VTRendererFactory::createRenderer() {
+204 -140
View File
@@ -1,6 +1,11 @@
#include <Limelight.h> #include <Limelight.h>
#include "ffmpeg.h" #include "ffmpeg.h"
#include "streaming/streamutils.h" #include "streaming/streamutils.h"
#include "streaming/session.h"
#include <h264_stream.h>
#include "ffmpeg-renderers/sdlvid.h"
#ifdef Q_OS_WIN32 #ifdef Q_OS_WIN32
#include "ffmpeg-renderers/dxva2.h" #include "ffmpeg-renderers/dxva2.h"
@@ -21,6 +26,8 @@
// This is gross but it allows us to use sizeof() // This is gross but it allows us to use sizeof()
#include "ffmpeg_videosamples.cpp" #include "ffmpeg_videosamples.cpp"
#define MAX_SPS_EXTRA_SIZE 16
#define FAILED_DECODES_RESET_THRESHOLD 20 #define FAILED_DECODES_RESET_THRESHOLD 20
bool FFmpegVideoDecoder::isHardwareAccelerated() bool FFmpegVideoDecoder::isHardwareAccelerated()
@@ -30,7 +37,7 @@ bool FFmpegVideoDecoder::isHardwareAccelerated()
int FFmpegVideoDecoder::getDecoderCapabilities() int FFmpegVideoDecoder::getDecoderCapabilities()
{ {
return m_Renderer->getDecoderCapabilities(); return CAPABILITY_DIRECT_SUBMIT | m_Renderer->getDecoderCapabilities();
} }
enum AVPixelFormat FFmpegVideoDecoder::ffGetFormat(AVCodecContext* context, enum AVPixelFormat FFmpegVideoDecoder::ffGetFormat(AVCodecContext* context,
@@ -54,7 +61,7 @@ enum AVPixelFormat FFmpegVideoDecoder::ffGetFormat(AVCodecContext* context,
return AV_PIX_FMT_NONE; return AV_PIX_FMT_NONE;
} }
FFmpegVideoDecoder::FFmpegVideoDecoder() FFmpegVideoDecoder::FFmpegVideoDecoder(bool testOnly)
: m_VideoDecoderCtx(nullptr), : m_VideoDecoderCtx(nullptr),
m_DecodeBuffer(1024 * 1024, 0), m_DecodeBuffer(1024 * 1024, 0),
m_HwDecodeCfg(nullptr), m_HwDecodeCfg(nullptr),
@@ -62,10 +69,11 @@ FFmpegVideoDecoder::FFmpegVideoDecoder()
m_ConsecutiveFailedDecodes(0), m_ConsecutiveFailedDecodes(0),
m_Pacer(nullptr), m_Pacer(nullptr),
m_LastFrameNumber(0), m_LastFrameNumber(0),
m_StreamFps(0) m_StreamFps(0),
m_NeedsSpsFixup(false),
m_TestOnly(testOnly)
{ {
av_init_packet(&m_Pkt); av_init_packet(&m_Pkt);
SDL_AtomicSet(&m_QueuedFrames, 0);
SDL_zero(m_ActiveWndVideoStats); SDL_zero(m_ActiveWndVideoStats);
SDL_zero(m_LastWndVideoStats); SDL_zero(m_LastWndVideoStats);
@@ -78,6 +86,12 @@ FFmpegVideoDecoder::FFmpegVideoDecoder()
FFmpegVideoDecoder::~FFmpegVideoDecoder() FFmpegVideoDecoder::~FFmpegVideoDecoder()
{ {
reset(); reset();
// Set log level back to default.
// NB: We don't do this in reset() because we want
// to preserve the log level across reset() during
// test initialization.
av_log_set_level(AV_LOG_INFO);
} }
IFFmpegRenderer* FFmpegVideoDecoder::getRenderer() IFFmpegRenderer* FFmpegVideoDecoder::getRenderer()
@@ -87,28 +101,6 @@ IFFmpegRenderer* FFmpegVideoDecoder::getRenderer()
void FFmpegVideoDecoder::reset() void FFmpegVideoDecoder::reset()
{ {
// Drop any frames still queued to ensure
// they are properly freed.
SDL_Event event;
while (SDL_AtomicGet(&m_QueuedFrames) > 0) {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Waiting for %d frames to return",
SDL_AtomicGet(&m_QueuedFrames));
if (SDL_PeepEvents(&event,
1,
SDL_GETEVENT,
SDL_USEREVENT,
SDL_USEREVENT) == 1) {
dropFrame(&event.user);
}
else {
SDL_Delay(10);
SDL_PumpEvents();
}
}
delete m_Pacer; delete m_Pacer;
m_Pacer = nullptr; m_Pacer = nullptr;
@@ -119,36 +111,53 @@ void FFmpegVideoDecoder::reset()
// need to delete in the renderer destructor. // need to delete in the renderer destructor.
avcodec_free_context(&m_VideoDecoderCtx); avcodec_free_context(&m_VideoDecoderCtx);
if (!m_TestOnly) {
Session::get()->getOverlayManager().setOverlayRenderer(nullptr);
}
delete m_Renderer; delete m_Renderer;
m_Renderer = nullptr; m_Renderer = nullptr;
logVideoStats(m_GlobalVideoStats, "Global video stats"); if (!m_TestOnly) {
logVideoStats(m_GlobalVideoStats, "Global video stats");
}
else {
// Test-only decoders can't have any frames submitted
SDL_assert(m_GlobalVideoStats.totalFrames == 0);
}
} }
bool FFmpegVideoDecoder::completeInitialization(AVCodec* decoder, SDL_Window* window, bool FFmpegVideoDecoder::completeInitialization(AVCodec* decoder, SDL_Window* window,
int videoFormat, int width, int height, int videoFormat, int width, int height,
int maxFps, bool enableVsync, bool testOnly) int maxFps, bool enableFramePacing, bool testFrame)
{ {
auto vsyncConstraint = m_Renderer->getVsyncConstraint(); // In test-only mode, we should only see test frames
if (vsyncConstraint == IFFmpegRenderer::VSYNC_FORCE_OFF && enableVsync) { SDL_assert(!m_TestOnly || testFrame);
auto vsyncConstraint = m_Renderer->getFramePacingConstraint();
if (vsyncConstraint == IFFmpegRenderer::PACING_FORCE_OFF && enableFramePacing) {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"V-sync is forcefully disabled by the active renderer"); "Frame pacing is forcefully disabled by the active renderer");
enableVsync = false; enableFramePacing = false;
} }
else if (vsyncConstraint == IFFmpegRenderer::VSYNC_FORCE_ON && !enableVsync) { else if (vsyncConstraint == IFFmpegRenderer::PACING_FORCE_ON && !enableFramePacing) {
// FIXME: This duplicates logic in Session.cpp // FIXME: This duplicates logic in Session.cpp
int displayHz = StreamUtils::getDisplayRefreshRate(window); int displayHz = StreamUtils::getDisplayRefreshRate(window);
if (displayHz + 5 >= maxFps) { if (displayHz + 5 >= maxFps) {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"V-sync is forcefully enabled by the active renderer"); "Frame pacing is forcefully enabled by the active renderer");
enableVsync = true; enableFramePacing = true;
} }
} }
m_StreamFps = maxFps; m_StreamFps = maxFps;
m_Pacer = new Pacer(m_Renderer, &m_ActiveWndVideoStats);
if (!m_Pacer->initialize(window, maxFps, enableVsync)) { // Don't bother initializing Pacer if we're not actually going to render
return false; if (!testFrame) {
m_Pacer = new Pacer(m_Renderer, &m_ActiveWndVideoStats);
if (!m_Pacer->initialize(window, maxFps, enableFramePacing)) {
return false;
}
} }
m_VideoDecoderCtx = avcodec_alloc_context3(decoder); m_VideoDecoderCtx = avcodec_alloc_context3(decoder);
@@ -206,7 +215,7 @@ bool FFmpegVideoDecoder::completeInitialization(AVCodec* decoder, SDL_Window* wi
// our minds on the selected video codec, so we'll do a trial run // our minds on the selected video codec, so we'll do a trial run
// now to see if things will actually work when the video stream // now to see if things will actually work when the video stream
// comes in. // comes in.
if (testOnly) { if (testFrame) {
if (videoFormat & VIDEO_FORMAT_MASK_H264) { if (videoFormat & VIDEO_FORMAT_MASK_H264) {
m_Pkt.data = (uint8_t*)k_H264TestFrame; m_Pkt.data = (uint8_t*)k_H264TestFrame;
m_Pkt.size = sizeof(k_H264TestFrame); m_Pkt.size = sizeof(k_H264TestFrame);
@@ -225,11 +234,20 @@ bool FFmpegVideoDecoder::completeInitialization(AVCodec* decoder, SDL_Window* wi
return false; return false;
} }
} }
else {
if ((videoFormat & VIDEO_FORMAT_MASK_H264) &&
!(m_Renderer->getDecoderCapabilities() & CAPABILITY_REFERENCE_FRAME_INVALIDATION_AVC)) {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Using H.264 SPS fixup");
m_NeedsSpsFixup = true;
}
else {
m_NeedsSpsFixup = false;
}
#ifdef QT_DEBUG // Tell overlay manager to use this renderer
// Restore default log level before streaming Session::get()->getOverlayManager().setOverlayRenderer(m_Renderer);
av_log_set_level(AV_LOG_INFO); }
#endif
return true; return true;
} }
@@ -239,10 +257,12 @@ void FFmpegVideoDecoder::addVideoStats(VIDEO_STATS& src, VIDEO_STATS& dst)
dst.receivedFrames += src.receivedFrames; dst.receivedFrames += src.receivedFrames;
dst.decodedFrames += src.decodedFrames; dst.decodedFrames += src.decodedFrames;
dst.renderedFrames += src.renderedFrames; dst.renderedFrames += src.renderedFrames;
dst.totalFrames += src.totalFrames;
dst.networkDroppedFrames += src.networkDroppedFrames; dst.networkDroppedFrames += src.networkDroppedFrames;
dst.pacerDroppedFrames += src.pacerDroppedFrames; dst.pacerDroppedFrames += src.pacerDroppedFrames;
dst.totalReassemblyTime += src.totalReassemblyTime; dst.totalReassemblyTime += src.totalReassemblyTime;
dst.totalDecodeTime += src.totalDecodeTime; dst.totalDecodeTime += src.totalDecodeTime;
dst.totalPacerTime += src.totalPacerTime;
dst.totalRenderTime += src.totalRenderTime; dst.totalRenderTime += src.totalRenderTime;
Uint32 now = SDL_GetTicks(); Uint32 now = SDL_GetTicks();
@@ -255,56 +275,59 @@ void FFmpegVideoDecoder::addVideoStats(VIDEO_STATS& src, VIDEO_STATS& dst)
// The following code assumes the global measure was already started first // The following code assumes the global measure was already started first
SDL_assert(dst.measurementStartTimestamp <= src.measurementStartTimestamp); SDL_assert(dst.measurementStartTimestamp <= src.measurementStartTimestamp);
dst.totalFps = (float)dst.totalFrames / ((float)(now - dst.measurementStartTimestamp) / 1000);
dst.receivedFps = (float)dst.receivedFrames / ((float)(now - dst.measurementStartTimestamp) / 1000); dst.receivedFps = (float)dst.receivedFrames / ((float)(now - dst.measurementStartTimestamp) / 1000);
dst.decodedFps = (float)dst.decodedFrames / ((float)(now - dst.measurementStartTimestamp) / 1000); dst.decodedFps = (float)dst.decodedFrames / ((float)(now - dst.measurementStartTimestamp) / 1000);
dst.renderedFps = (float)dst.renderedFrames / ((float)(now - dst.measurementStartTimestamp) / 1000); dst.renderedFps = (float)dst.renderedFrames / ((float)(now - dst.measurementStartTimestamp) / 1000);
} }
void FFmpegVideoDecoder::stringifyVideoStats(VIDEO_STATS& stats, char* output)
{
int offset = 0;
// Start with an empty string
output[offset] = 0;
if (stats.receivedFps > 0) {
offset += sprintf(&output[offset],
"Estimated host PC frame rate: %.2f FPS\n"
"Incoming frame rate from network: %.2f FPS\n"
"Decoding frame rate: %.2f FPS\n"
"Rendering frame rate: %.2f FPS\n",
stats.totalFps,
stats.receivedFps,
stats.decodedFps,
stats.renderedFps);
}
if (stats.renderedFrames != 0) {
offset += sprintf(&output[offset],
"Frames dropped by your network connection: %.2f%%\n"
"Frames dropped due to network jitter: %.2f%%\n"
"Average receive time: %.2f ms\n"
"Average decoding time: %.2f ms\n"
"Average frame queue delay: %.2f ms\n"
"Average rendering time (including monitor V-sync latency): %.2f ms\n",
(float)stats.networkDroppedFrames / stats.totalFrames * 100,
(float)stats.pacerDroppedFrames / stats.decodedFrames * 100,
(float)stats.totalReassemblyTime / stats.receivedFrames,
(float)stats.totalDecodeTime / stats.decodedFrames,
(float)stats.totalPacerTime / stats.renderedFrames,
(float)stats.totalRenderTime / stats.renderedFrames);
}
}
void FFmpegVideoDecoder::logVideoStats(VIDEO_STATS& stats, const char* title) void FFmpegVideoDecoder::logVideoStats(VIDEO_STATS& stats, const char* title)
{ {
if (stats.renderedFps > 0 || stats.renderedFrames != 0) { if (stats.renderedFps > 0 || stats.renderedFrames != 0) {
char videoStatsStr[512];
stringifyVideoStats(stats, videoStatsStr);
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"%s", title); "%s", title);
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"----------------------------------------------------------"); "----------------------------------------------------------\n%s",
} videoStatsStr);
if (stats.renderedFps > 0) {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Incoming frame rate from network: %.2f FPS",
stats.receivedFps);
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Decoding frame rate: %.2f FPS",
stats.decodedFps);
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Rendering frame rate: %.2f FPS",
stats.renderedFps);
}
if (stats.renderedFrames != 0) {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Total frames received: %u",
stats.receivedFrames);
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Total frames decoded: %u",
stats.decodedFrames);
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Total frames rendered: %u",
stats.renderedFrames);
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Average reassembly time: %.2f ms",
(float)stats.totalReassemblyTime / stats.decodedFrames);
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Average decode time: %.2f ms",
(float)stats.totalDecodeTime / stats.decodedFrames);
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Average render time: %.2f ms",
(float)stats.totalRenderTime / stats.renderedFrames);
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Frames lost during network transmission: %.2f%%",
(float)stats.networkDroppedFrames / stats.decodedFrames);
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Frames dropped by frame pacing: %.2f%%",
(float)stats.pacerDroppedFrames / stats.decodedFrames);
} }
} }
@@ -344,14 +367,13 @@ bool FFmpegVideoDecoder::initialize(
int width, int width,
int height, int height,
int maxFps, int maxFps,
bool enableVsync) bool enableVsync,
bool enableFramePacing)
{ {
AVCodec* decoder; AVCodec* decoder;
#ifdef QT_DEBUG // Increase log level until the first frame is decoded
// Increase log level during initialization
av_log_set_level(AV_LOG_DEBUG); av_log_set_level(AV_LOG_DEBUG);
#endif
if (videoFormat & VIDEO_FORMAT_MASK_H264) { if (videoFormat & VIDEO_FORMAT_MASK_H264) {
decoder = avcodec_find_decoder(AV_CODEC_ID_H264); decoder = avcodec_find_decoder(AV_CODEC_ID_H264);
@@ -380,7 +402,7 @@ bool FFmpegVideoDecoder::initialize(
m_Renderer = new SdlRenderer(); m_Renderer = new SdlRenderer();
if (vds != StreamingPreferences::VDS_FORCE_HARDWARE && if (vds != StreamingPreferences::VDS_FORCE_HARDWARE &&
m_Renderer->initialize(window, videoFormat, width, height, maxFps, enableVsync) && m_Renderer->initialize(window, videoFormat, width, height, maxFps, enableVsync) &&
completeInitialization(decoder, window, videoFormat, width, height, maxFps, enableVsync, false)) { completeInitialization(decoder, window, videoFormat, width, height, maxFps, enableFramePacing, m_TestOnly)) {
return true; return true;
} }
else { else {
@@ -397,13 +419,19 @@ bool FFmpegVideoDecoder::initialize(
m_HwDecodeCfg = config; m_HwDecodeCfg = config;
// Initialize the hardware codec and submit a test frame if the renderer needs it // Initialize the hardware codec and submit a test frame if the renderer needs it
if (m_Renderer->initialize(window, videoFormat, width, height, maxFps, enableVsync) && if (m_Renderer->initialize(window, videoFormat, width, height, maxFps, enableVsync) &&
completeInitialization(decoder, window, videoFormat, width, height, maxFps, enableVsync, m_Renderer->needsTestFrame())) { completeInitialization(decoder, window, videoFormat, width, height, maxFps, enableFramePacing, m_TestOnly || m_Renderer->needsTestFrame())) {
if (m_TestOnly) {
// This decoder is only for testing capabilities, so don't bother
// creating a usable renderer
return true;
}
if (m_Renderer->needsTestFrame()) { if (m_Renderer->needsTestFrame()) {
// The test worked, so now let's initialize it for real // The test worked, so now let's initialize it for real
reset(); reset();
if ((m_Renderer = createAcceleratedRenderer(config)) != nullptr && if ((m_Renderer = createAcceleratedRenderer(config)) != nullptr &&
m_Renderer->initialize(window, videoFormat, width, height, maxFps, enableVsync) && m_Renderer->initialize(window, videoFormat, width, height, maxFps, enableVsync) &&
completeInitialization(decoder, window, videoFormat, width, height, maxFps, enableVsync, false)) { completeInitialization(decoder, window, videoFormat, width, height, maxFps, enableFramePacing, false)) {
return true; return true;
} }
else { else {
@@ -424,21 +452,78 @@ bool FFmpegVideoDecoder::initialize(
} }
} }
void FFmpegVideoDecoder::writeBuffer(PLENTRY entry, int& offset)
{
if (m_NeedsSpsFixup && entry->bufferType == BUFFER_TYPE_SPS) {
const char naluHeader[] = {0x00, 0x00, 0x00, 0x01};
h264_stream_t* stream = h264_new();
int nalStart, nalEnd;
// Read the old NALU
find_nal_unit((uint8_t*)entry->data, entry->length, &nalStart, &nalEnd);
read_nal_unit(stream,
(unsigned char *)&entry->data[nalStart],
nalEnd - nalStart);
SDL_assert(nalStart == sizeof(naluHeader));
SDL_assert(nalEnd == entry->length);
// Fixup the SPS to what OS X needs to use hardware acceleration
stream->sps->num_ref_frames = 1;
stream->sps->vui.max_dec_frame_buffering = 1;
int initialOffset = offset;
// Copy the modified NALU data. This assumes a 3 byte prefix and
// begins writing from the 2nd byte, so we must write the data
// first, then go back and write the Annex B prefix.
offset += write_nal_unit(stream, (uint8_t*)&m_DecodeBuffer.data()[initialOffset + 3],
MAX_SPS_EXTRA_SIZE + entry->length - sizeof(naluHeader));
// Copy the NALU prefix over from the original SPS
memcpy(&m_DecodeBuffer.data()[initialOffset], naluHeader, sizeof(naluHeader));
offset += sizeof(naluHeader);
h264_free(stream);
}
else {
// Write the buffer as-is
memcpy(&m_DecodeBuffer.data()[offset],
entry->data,
entry->length);
offset += entry->length;
}
}
int FFmpegVideoDecoder::submitDecodeUnit(PDECODE_UNIT du) int FFmpegVideoDecoder::submitDecodeUnit(PDECODE_UNIT du)
{ {
PLENTRY entry = du->bufferList; PLENTRY entry = du->bufferList;
int err; int err;
// Flip stats windows roughly every second SDL_assert(!m_TestOnly);
if (m_ActiveWndVideoStats.receivedFrames == m_StreamFps) {
#if 0
VIDEO_STATS lastTwoWndStats = {};
addVideoStats(m_LastWndVideoStats, lastTwoWndStats);
addVideoStats(m_ActiveWndVideoStats, lastTwoWndStats);
// Print stats from the last 2 windows if (!m_LastFrameNumber) {
logVideoStats(lastTwoWndStats, "Periodic video stats"); m_ActiveWndVideoStats.measurementStartTimestamp = SDL_GetTicks();
#endif m_LastFrameNumber = du->frameNumber;
}
else {
// Any frame number greater than m_LastFrameNumber + 1 represents a dropped frame
m_ActiveWndVideoStats.networkDroppedFrames += du->frameNumber - (m_LastFrameNumber + 1);
m_ActiveWndVideoStats.totalFrames += du->frameNumber - (m_LastFrameNumber + 1);
m_LastFrameNumber = du->frameNumber;
}
// Flip stats windows roughly every second
if (SDL_TICKS_PASSED(SDL_GetTicks(), m_ActiveWndVideoStats.measurementStartTimestamp + 1000)) {
// Update overlay stats if it's enabled
if (Session::get()->getOverlayManager().isOverlayEnabled(Overlay::OverlayDebug)) {
VIDEO_STATS lastTwoWndStats = {};
addVideoStats(m_LastWndVideoStats, lastTwoWndStats);
addVideoStats(m_ActiveWndVideoStats, lastTwoWndStats);
stringifyVideoStats(lastTwoWndStats, Session::get()->getOverlayManager().getOverlayText(Overlay::OverlayDebug));
Session::get()->getOverlayManager().setOverlayTextUpdated(Overlay::OverlayDebug);
}
// Accumulate these values into the global stats // Accumulate these values into the global stats
addVideoStats(m_ActiveWndVideoStats, m_GlobalVideoStats); addVideoStats(m_ActiveWndVideoStats, m_GlobalVideoStats);
@@ -450,34 +535,25 @@ int FFmpegVideoDecoder::submitDecodeUnit(PDECODE_UNIT du)
} }
m_ActiveWndVideoStats.receivedFrames++; m_ActiveWndVideoStats.receivedFrames++;
m_ActiveWndVideoStats.totalFrames++;
if (!m_LastFrameNumber) { int requiredBufferSize = du->fullLength;
m_ActiveWndVideoStats.measurementStartTimestamp = SDL_GetTicks(); if (du->frameType == FRAME_TYPE_IDR) {
m_LastFrameNumber = du->frameNumber; // Add some extra space in case we need to do an SPS fixup
requiredBufferSize += MAX_SPS_EXTRA_SIZE;
} }
else { if (requiredBufferSize + AV_INPUT_BUFFER_PADDING_SIZE > m_DecodeBuffer.length()) {
// Any frame number greater than m_LastFrameNumber + 1 represents a dropped frame m_DecodeBuffer = QByteArray(requiredBufferSize + AV_INPUT_BUFFER_PADDING_SIZE, 0);
m_ActiveWndVideoStats.networkDroppedFrames += du->frameNumber - (m_LastFrameNumber + 1);
m_LastFrameNumber = du->frameNumber;
}
if (du->fullLength + AV_INPUT_BUFFER_PADDING_SIZE > m_DecodeBuffer.length()) {
m_DecodeBuffer = QByteArray(du->fullLength + AV_INPUT_BUFFER_PADDING_SIZE, 0);
} }
int offset = 0; int offset = 0;
while (entry != nullptr) { while (entry != nullptr) {
memcpy(&m_DecodeBuffer.data()[offset], writeBuffer(entry, offset);
entry->data,
entry->length);
offset += entry->length;
entry = entry->next; entry = entry->next;
} }
SDL_assert(offset == du->fullLength);
m_Pkt.data = reinterpret_cast<uint8_t*>(m_DecodeBuffer.data()); m_Pkt.data = reinterpret_cast<uint8_t*>(m_DecodeBuffer.data());
m_Pkt.size = du->fullLength; m_Pkt.size = offset;
m_ActiveWndVideoStats.totalReassemblyTime += LiGetMillis() - du->receiveTimeMs; m_ActiveWndVideoStats.totalReassemblyTime += LiGetMillis() - du->receiveTimeMs;
@@ -519,14 +595,19 @@ int FFmpegVideoDecoder::submitDecodeUnit(PDECODE_UNIT du)
// Reset failed decodes count if we reached this far // Reset failed decodes count if we reached this far
m_ConsecutiveFailedDecodes = 0; m_ConsecutiveFailedDecodes = 0;
// Queue the frame for rendering from the main thread // Restore default log level after a successful decode
SDL_AtomicIncRef(&m_QueuedFrames); av_log_set_level(AV_LOG_INFO);
queueFrame(frame);
// Capture a frame timestamp to measuring pacing delay
frame->pts = SDL_GetTicks();
// Count time in avcodec_send_packet() and avcodec_receive_frame() // Count time in avcodec_send_packet() and avcodec_receive_frame()
// as time spent decoding // as time spent decoding
m_ActiveWndVideoStats.totalDecodeTime += SDL_GetTicks() - beforeDecode; m_ActiveWndVideoStats.totalDecodeTime += SDL_GetTicks() - beforeDecode;
m_ActiveWndVideoStats.decodedFrames++; m_ActiveWndVideoStats.decodedFrames++;
// Queue the frame for rendering (or render now if pacer is disabled)
m_Pacer->submitFrame(frame);
} }
else { else {
av_frame_free(&frame); av_frame_free(&frame);
@@ -550,30 +631,13 @@ int FFmpegVideoDecoder::submitDecodeUnit(PDECODE_UNIT du)
} }
// Called on main thread // Called on main thread
void FFmpegVideoDecoder::renderFrame(SDL_UserEvent* event) void FFmpegVideoDecoder::renderFrame(SDL_UserEvent*)
{ {
AVFrame* frame = reinterpret_cast<AVFrame*>(event->data1); SDL_assert(false);
m_Pacer->submitFrame(frame);
SDL_AtomicDecRef(&m_QueuedFrames);
} }
// Called on main thread // Called on main thread
void FFmpegVideoDecoder::dropFrame(SDL_UserEvent* event) void FFmpegVideoDecoder::dropFrame(SDL_UserEvent*)
{ {
AVFrame* frame = reinterpret_cast<AVFrame*>(event->data1); SDL_assert(false);
// If Pacer is using a frame queue, we can just queue the
// frame and let it decide whether to drop or not. If Pacer
// is submitting directly for rendering, we drop the frame
// ourselves.
if (m_Pacer->isUsingFrameQueue()) {
m_Pacer->submitFrame(frame);
}
else {
av_frame_free(&frame);
// Since Pacer won't see this frame, we'll mark it as a drop
// on its behalf
m_ActiveWndVideoStats.pacerDroppedFrames++;
}
SDL_AtomicDecRef(&m_QueuedFrames);
} }
+12 -5
View File
@@ -10,15 +10,16 @@ extern "C" {
class FFmpegVideoDecoder : public IVideoDecoder { class FFmpegVideoDecoder : public IVideoDecoder {
public: public:
FFmpegVideoDecoder(); FFmpegVideoDecoder(bool testOnly);
virtual ~FFmpegVideoDecoder(); virtual ~FFmpegVideoDecoder() override;
virtual bool initialize(StreamingPreferences::VideoDecoderSelection vds, virtual bool initialize(StreamingPreferences::VideoDecoderSelection vds,
SDL_Window* window, SDL_Window* window,
int videoFormat, int videoFormat,
int width, int width,
int height, int height,
int maxFps, int maxFps,
bool enableVsync) override; bool enableVsync,
bool enableFramePacing) override;
virtual bool isHardwareAccelerated() override; virtual bool isHardwareAccelerated() override;
virtual int getDecoderCapabilities() override; virtual int getDecoderCapabilities() override;
virtual int submitDecodeUnit(PDECODE_UNIT du) override; virtual int submitDecodeUnit(PDECODE_UNIT du) override;
@@ -30,7 +31,9 @@ public:
private: private:
bool completeInitialization(AVCodec* decoder, SDL_Window* window, bool completeInitialization(AVCodec* decoder, SDL_Window* window,
int videoFormat, int width, int height, int videoFormat, int width, int height,
int maxFps, bool enableVsync, bool testOnly); int maxFps, bool enableFramePacing, bool testFrame);
void stringifyVideoStats(VIDEO_STATS& stats, char* output);
void logVideoStats(VIDEO_STATS& stats, const char* title); void logVideoStats(VIDEO_STATS& stats, const char* title);
@@ -40,6 +43,8 @@ private:
void reset(); void reset();
void writeBuffer(PLENTRY entry, int& offset);
static static
enum AVPixelFormat ffGetFormat(AVCodecContext* context, enum AVPixelFormat ffGetFormat(AVCodecContext* context,
const enum AVPixelFormat* pixFmts); const enum AVPixelFormat* pixFmts);
@@ -49,14 +54,16 @@ private:
QByteArray m_DecodeBuffer; QByteArray m_DecodeBuffer;
const AVCodecHWConfig* m_HwDecodeCfg; const AVCodecHWConfig* m_HwDecodeCfg;
IFFmpegRenderer* m_Renderer; IFFmpegRenderer* m_Renderer;
SDL_atomic_t m_QueuedFrames;
int m_ConsecutiveFailedDecodes; int m_ConsecutiveFailedDecodes;
Pacer* m_Pacer; Pacer* m_Pacer;
VIDEO_STATS m_ActiveWndVideoStats; VIDEO_STATS m_ActiveWndVideoStats;
VIDEO_STATS m_LastWndVideoStats; VIDEO_STATS m_LastWndVideoStats;
VIDEO_STATS m_GlobalVideoStats; VIDEO_STATS m_GlobalVideoStats;
int m_LastFrameNumber; int m_LastFrameNumber;
int m_StreamFps; int m_StreamFps;
bool m_NeedsSpsFixup;
bool m_TestOnly;
static const uint8_t k_H264TestFrame[]; static const uint8_t k_H264TestFrame[];
static const uint8_t k_HEVCTestFrame[]; static const uint8_t k_HEVCTestFrame[];
+64
View File
@@ -0,0 +1,64 @@
#include "overlaymanager.h"
using namespace Overlay;
OverlayManager::OverlayManager() :
m_Renderer(nullptr)
{
memset(m_Overlays, 0, sizeof(m_Overlays));
m_Overlays[OverlayType::OverlayDebug].color = {0xD0, 0xD0, 0x00, 0xFF};
m_Overlays[OverlayType::OverlayDebug].fontSize = 20;
}
bool OverlayManager::isOverlayEnabled(OverlayType type)
{
return m_Overlays[type].enabled;
}
char* OverlayManager::getOverlayText(OverlayType type)
{
return m_Overlays[type].text;
}
int OverlayManager::getOverlayFontSize(OverlayType type)
{
return m_Overlays[type].fontSize;
}
void OverlayManager::setOverlayTextUpdated(OverlayType type)
{
// Only update the overlay state if it's enabled. If it's not enabled,
// the renderer has already been notified by setOverlayState().
if (m_Overlays[type].enabled && m_Renderer != nullptr) {
m_Renderer->notifyOverlayUpdated(type);
}
}
void OverlayManager::setOverlayState(OverlayType type, bool enabled)
{
bool stateChanged = m_Overlays[type].enabled != enabled;
m_Overlays[type].enabled = enabled;
if (stateChanged) {
if (!enabled) {
// Set the text to empty string on disable
m_Overlays[type].text[0] = 0;
}
if (m_Renderer != nullptr) {
m_Renderer->notifyOverlayUpdated(type);
}
}
}
SDL_Color OverlayManager::getOverlayColor(OverlayType type)
{
return m_Overlays[type].color;
}
void OverlayManager::setOverlayRenderer(IOverlayRenderer* renderer)
{
m_Renderer = renderer;
}
+47
View File
@@ -0,0 +1,47 @@
#pragma once
#include <QString>
#include <SDL.h>
namespace Overlay {
enum OverlayType {
OverlayDebug,
OverlayMinorNotification,
OverlayMajorNotification,
OverlayMax
};
class IOverlayRenderer
{
public:
virtual ~IOverlayRenderer() = default;
virtual void notifyOverlayUpdated(OverlayType type) = 0;
};
class OverlayManager
{
public:
OverlayManager();
bool isOverlayEnabled(OverlayType type);
char* getOverlayText(OverlayType type);
void setOverlayTextUpdated(OverlayType type);
void setOverlayState(OverlayType type, bool enabled);
SDL_Color getOverlayColor(OverlayType type);
int getOverlayFontSize(OverlayType type);
void setOverlayRenderer(IOverlayRenderer* renderer);
struct {
bool enabled;
int fontSize;
SDL_Color color;
char text[512];
} m_Overlays[OverlayMax];
IOverlayRenderer* m_Renderer;
};
}
+2 -2
View File
@@ -1,6 +1,6 @@
#include "sl.h" #include "sl.h"
SLVideoDecoder::SLVideoDecoder() SLVideoDecoder::SLVideoDecoder(bool)
: m_VideoContext(nullptr), : m_VideoContext(nullptr),
m_VideoStream(nullptr) m_VideoStream(nullptr)
{ {
@@ -34,7 +34,7 @@ SLVideoDecoder::getDecoderCapabilities()
bool bool
SLVideoDecoder::initialize(StreamingPreferences::VideoDecoderSelection vds, SLVideoDecoder::initialize(StreamingPreferences::VideoDecoderSelection vds,
SDL_Window*, SDL_Window*,
int videoFormat, int, int, int frameRate, bool) int videoFormat, int, int, int frameRate, bool, bool)
{ {
// SLVideo only supports hardware decoding // SLVideo only supports hardware decoding
if (vds == StreamingPreferences::VDS_FORCE_SOFTWARE) { if (vds == StreamingPreferences::VDS_FORCE_SOFTWARE) {
+3 -2
View File
@@ -7,7 +7,7 @@
class SLVideoDecoder : public IVideoDecoder class SLVideoDecoder : public IVideoDecoder
{ {
public: public:
SLVideoDecoder(); SLVideoDecoder(bool testOnly);
virtual ~SLVideoDecoder(); virtual ~SLVideoDecoder();
virtual bool initialize(StreamingPreferences::VideoDecoderSelection vds, virtual bool initialize(StreamingPreferences::VideoDecoderSelection vds,
SDL_Window* window, SDL_Window* window,
@@ -15,7 +15,8 @@ public:
int width, int width,
int height, int height,
int frameRate, int frameRate,
bool enableVsync); bool enableVsync,
bool enableFramePacing);
virtual bool isHardwareAccelerated(); virtual bool isHardwareAccelerated();
virtual int getDecoderCapabilities(); virtual int getDecoderCapabilities();
virtual int submitDecodeUnit(PDECODE_UNIT du); virtual int submitDecodeUnit(PDECODE_UNIT du);
+1
View File
@@ -0,0 +1 @@
0.9.1
+46
View File
@@ -0,0 +1,46 @@
#-------------------------------------------------
#
# Project created by QtCreator 2018-10-12T15:50:59
#
#-------------------------------------------------
QT -= core gui
TARGET = h264bitstream
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
# Build a static library
CONFIG += staticlib
# Disable warnings
CONFIG += warn_off
# Older GCC versions defaulted to GNU89
*-g++ {
QMAKE_CFLAGS += -std=gnu99
}
SRC_DIR = $$PWD/h264bitstream
SOURCES += \
$$SRC_DIR/h264_avcc.c \
$$SRC_DIR/h264_nal.c \
$$SRC_DIR/h264_sei.c \
$$SRC_DIR/h264_slice_data.c \
$$SRC_DIR/h264_stream.c
HEADERS += \
$$SRC_DIR/bs.h \
$$SRC_DIR/h264_avcc.h \
$$SRC_DIR/h264_sei.h \
$$SRC_DIR/h264_slice_data.h \
$$SRC_DIR/h264_stream.h
INCLUDEPATH += $$INC_DIR
+1
View File
@@ -0,0 +1 @@
Versions/Current/Frameworks
+1
View File
@@ -0,0 +1 @@
Versions/Current/Headers
+1
View File
@@ -0,0 +1 @@
Versions/Current/Resources
+1
View File
@@ -0,0 +1 @@
Versions/Current/SDL2_ttf
@@ -0,0 +1 @@
Versions/Current/FreeType
@@ -0,0 +1 @@
Versions/Current/Headers
@@ -0,0 +1 @@
Versions/Current/Resources
@@ -0,0 +1,603 @@
/* ftconfig.h. Generated from ftconfig.in by configure. */
/***************************************************************************/
/* */
/* ftconfig.in */
/* */
/* UNIX-specific configuration file (specification only). */
/* */
/* Copyright 1996-2018 by */
/* David Turner, Robert Wilhelm, and Werner Lemberg. */
/* */
/* This file is part of the FreeType project, and may only be used, */
/* modified, and distributed under the terms of the FreeType project */
/* license, LICENSE.TXT. By continuing to use, modify, or distribute */
/* this file you indicate that you have read the license and */
/* understand and accept it fully. */
/* */
/***************************************************************************/
/*************************************************************************/
/* */
/* This header file contains a number of macro definitions that are used */
/* by the rest of the engine. Most of the macros here are automatically */
/* determined at compile time, and you should not need to change it to */
/* port FreeType, except to compile the library with a non-ANSI */
/* compiler. */
/* */
/* Note however that if some specific modifications are needed, we */
/* advise you to place a modified copy in your build directory. */
/* */
/* The build directory is usually `builds/<system>', and contains */
/* system-specific files that are always included first when building */
/* the library. */
/* */
/*************************************************************************/
#ifndef FTCONFIG_H_
#define FTCONFIG_H_
#include <ft2build.h>
#include FT_CONFIG_OPTIONS_H
#include FT_CONFIG_STANDARD_LIBRARY_H
FT_BEGIN_HEADER
/*************************************************************************/
/* */
/* PLATFORM-SPECIFIC CONFIGURATION MACROS */
/* */
/* These macros can be toggled to suit a specific system. The current */
/* ones are defaults used to compile FreeType in an ANSI C environment */
/* (16bit compilers are also supported). Copy this file to your own */
/* `builds/<system>' directory, and edit it to port the engine. */
/* */
/*************************************************************************/
#define HAVE_UNISTD_H 1
#define HAVE_FCNTL_H 1
#define HAVE_STDINT_H 1
/* There are systems (like the Texas Instruments 'C54x) where a `char' */
/* has 16 bits. ANSI C says that sizeof(char) is always 1. Since an */
/* `int' has 16 bits also for this system, sizeof(int) gives 1 which */
/* is probably unexpected. */
/* */
/* `CHAR_BIT' (defined in limits.h) gives the number of bits in a */
/* `char' type. */
#ifndef FT_CHAR_BIT
#define FT_CHAR_BIT CHAR_BIT
#endif
/* #undef FT_USE_AUTOCONF_SIZEOF_TYPES */
#ifdef FT_USE_AUTOCONF_SIZEOF_TYPES
#define SIZEOF_INT 4
#define SIZEOF_LONG 8
#define FT_SIZEOF_INT SIZEOF_INT
#define FT_SIZEOF_LONG SIZEOF_LONG
#else /* !FT_USE_AUTOCONF_SIZEOF_TYPES */
/* Following cpp computation of the bit length of int and long */
/* is copied from default include/freetype/config/ftconfig.h. */
/* If any improvement is required for this file, it should be */
/* applied to the original header file for the builders that */
/* do not use configure script. */
/* The size of an `int' type. */
#if FT_UINT_MAX == 0xFFFFUL
#define FT_SIZEOF_INT (16 / FT_CHAR_BIT)
#elif FT_UINT_MAX == 0xFFFFFFFFUL
#define FT_SIZEOF_INT (32 / FT_CHAR_BIT)
#elif FT_UINT_MAX > 0xFFFFFFFFUL && FT_UINT_MAX == 0xFFFFFFFFFFFFFFFFUL
#define FT_SIZEOF_INT (64 / FT_CHAR_BIT)
#else
#error "Unsupported size of `int' type!"
#endif
/* The size of a `long' type. A five-byte `long' (as used e.g. on the */
/* DM642) is recognized but avoided. */
#if FT_ULONG_MAX == 0xFFFFFFFFUL
#define FT_SIZEOF_LONG (32 / FT_CHAR_BIT)
#elif FT_ULONG_MAX > 0xFFFFFFFFUL && FT_ULONG_MAX == 0xFFFFFFFFFFUL
#define FT_SIZEOF_LONG (32 / FT_CHAR_BIT)
#elif FT_ULONG_MAX > 0xFFFFFFFFUL && FT_ULONG_MAX == 0xFFFFFFFFFFFFFFFFUL
#define FT_SIZEOF_LONG (64 / FT_CHAR_BIT)
#else
#error "Unsupported size of `long' type!"
#endif
#endif /* !FT_USE_AUTOCONF_SIZEOF_TYPES */
/* FT_UNUSED is a macro used to indicate that a given parameter is not */
/* used -- this is only used to get rid of unpleasant compiler warnings */
#ifndef FT_UNUSED
#define FT_UNUSED( arg ) ( (arg) = (arg) )
#endif
/*************************************************************************/
/* */
/* AUTOMATIC CONFIGURATION MACROS */
/* */
/* These macros are computed from the ones defined above. Don't touch */
/* their definition, unless you know precisely what you are doing. No */
/* porter should need to mess with them. */
/* */
/*************************************************************************/
/*************************************************************************/
/* */
/* Mac support */
/* */
/* This is the only necessary change, so it is defined here instead */
/* providing a new configuration file. */
/* */
#if defined( __APPLE__ ) || ( defined( __MWERKS__ ) && defined( macintosh ) )
/* no Carbon frameworks for 64bit 10.4.x */
/* AvailabilityMacros.h is available since Mac OS X 10.2, */
/* so guess the system version by maximum errno before inclusion */
#include <errno.h>
#ifdef ECANCELED /* defined since 10.2 */
#include "AvailabilityMacros.h"
#endif
#if defined( __LP64__ ) && \
( MAC_OS_X_VERSION_MIN_REQUIRED <= MAC_OS_X_VERSION_10_4 )
#undef FT_MACINTOSH
#endif
#elif defined( __SC__ ) || defined( __MRC__ )
/* Classic MacOS compilers */
#include "ConditionalMacros.h"
#if TARGET_OS_MAC
#define FT_MACINTOSH 1
#endif
#endif
/* Fix compiler warning with sgi compiler */
#if defined( __sgi ) && !defined( __GNUC__ )
#if defined( _COMPILER_VERSION ) && ( _COMPILER_VERSION >= 730 )
#pragma set woff 3505
#endif
#endif
/*************************************************************************/
/* */
/* <Section> */
/* basic_types */
/* */
/*************************************************************************/
/*************************************************************************/
/* */
/* <Type> */
/* FT_Int16 */
/* */
/* <Description> */
/* A typedef for a 16bit signed integer type. */
/* */
typedef signed short FT_Int16;
/*************************************************************************/
/* */
/* <Type> */
/* FT_UInt16 */
/* */
/* <Description> */
/* A typedef for a 16bit unsigned integer type. */
/* */
typedef unsigned short FT_UInt16;
/* */
/* this #if 0 ... #endif clause is for documentation purposes */
#if 0
/*************************************************************************/
/* */
/* <Type> */
/* FT_Int32 */
/* */
/* <Description> */
/* A typedef for a 32bit signed integer type. The size depends on */
/* the configuration. */
/* */
typedef signed XXX FT_Int32;
/*************************************************************************/
/* */
/* <Type> */
/* FT_UInt32 */
/* */
/* A typedef for a 32bit unsigned integer type. The size depends on */
/* the configuration. */
/* */
typedef unsigned XXX FT_UInt32;
/*************************************************************************/
/* */
/* <Type> */
/* FT_Int64 */
/* */
/* A typedef for a 64bit signed integer type. The size depends on */
/* the configuration. Only defined if there is real 64bit support; */
/* otherwise, it gets emulated with a structure (if necessary). */
/* */
typedef signed XXX FT_Int64;
/*************************************************************************/
/* */
/* <Type> */
/* FT_UInt64 */
/* */
/* A typedef for a 64bit unsigned integer type. The size depends on */
/* the configuration. Only defined if there is real 64bit support; */
/* otherwise, it gets emulated with a structure (if necessary). */
/* */
typedef unsigned XXX FT_UInt64;
/* */
#endif
#if FT_SIZEOF_INT == 4
typedef signed int FT_Int32;
typedef unsigned int FT_UInt32;
#elif FT_SIZEOF_LONG == 4
typedef signed long FT_Int32;
typedef unsigned long FT_UInt32;
#else
#error "no 32bit type found -- please check your configuration files"
#endif
/* look up an integer type that is at least 32 bits */
#if FT_SIZEOF_INT >= 4
typedef int FT_Fast;
typedef unsigned int FT_UFast;
#elif FT_SIZEOF_LONG >= 4
typedef long FT_Fast;
typedef unsigned long FT_UFast;
#endif
/* determine whether we have a 64-bit int type */
/* (mostly for environments without `autoconf') */
#if FT_SIZEOF_LONG == 8
/* FT_LONG64 must be defined if a 64-bit type is available */
#define FT_LONG64
#define FT_INT64 long
#define FT_UINT64 unsigned long
/* we handle the LLP64 scheme separately for GCC and clang, */
/* suppressing the `long long' warning */
#elif ( FT_SIZEOF_LONG == 4 ) && \
defined( HAVE_LONG_LONG_INT ) && \
defined( __GNUC__ )
#pragma GCC diagnostic ignored "-Wlong-long"
#define FT_LONG64
#define FT_INT64 long long int
#define FT_UINT64 unsigned long long int
/*************************************************************************/
/* */
/* A 64-bit data type may create compilation problems if you compile */
/* in strict ANSI mode. To avoid them, we disable other 64-bit data */
/* types if __STDC__ is defined. You can however ignore this rule */
/* by defining the FT_CONFIG_OPTION_FORCE_INT64 configuration macro. */
/* */
#elif !defined( __STDC__ ) || defined( FT_CONFIG_OPTION_FORCE_INT64 )
#if defined( __STDC_VERSION__ ) && __STDC_VERSION__ >= 199901L
#define FT_LONG64
#define FT_INT64 long long int
#define FT_UINT64 unsigned long long int
#elif defined( _MSC_VER ) && _MSC_VER >= 900 /* Visual C++ (and Intel C++) */
/* this compiler provides the __int64 type */
#define FT_LONG64
#define FT_INT64 __int64
#define FT_UINT64 unsigned __int64
#elif defined( __BORLANDC__ ) /* Borland C++ */
/* XXXX: We should probably check the value of __BORLANDC__ in order */
/* to test the compiler version. */
/* this compiler provides the __int64 type */
#define FT_LONG64
#define FT_INT64 __int64
#define FT_UINT64 unsigned __int64
#elif defined( __WATCOMC__ ) /* Watcom C++ */
/* Watcom doesn't provide 64-bit data types */
#elif defined( __MWERKS__ ) /* Metrowerks CodeWarrior */
#define FT_LONG64
#define FT_INT64 long long int
#define FT_UINT64 unsigned long long int
#elif defined( __GNUC__ )
/* GCC provides the `long long' type */
#define FT_LONG64
#define FT_INT64 long long int
#define FT_UINT64 unsigned long long int
#endif /* __STDC_VERSION__ >= 199901L */
#endif /* FT_SIZEOF_LONG == 8 */
#ifdef FT_LONG64
typedef FT_INT64 FT_Int64;
typedef FT_UINT64 FT_UInt64;
#endif
#ifdef _WIN64
/* only 64bit Windows uses the LLP64 data model, i.e., */
/* 32bit integers, 64bit pointers */
#define FT_UINT_TO_POINTER( x ) (void*)(unsigned __int64)(x)
#else
#define FT_UINT_TO_POINTER( x ) (void*)(unsigned long)(x)
#endif
/*************************************************************************/
/* */
/* miscellaneous */
/* */
/*************************************************************************/
#define FT_BEGIN_STMNT do {
#define FT_END_STMNT } while ( 0 )
#define FT_DUMMY_STMNT FT_BEGIN_STMNT FT_END_STMNT
/* typeof condition taken from gnulib's `intprops.h' header file */
#if ( ( defined( __GNUC__ ) && __GNUC__ >= 2 ) || \
( defined( __IBMC__ ) && __IBMC__ >= 1210 && \
defined( __IBM__TYPEOF__ ) ) || \
( defined( __SUNPRO_C ) && __SUNPRO_C >= 0x5110 && !__STDC__ ) )
#define FT_TYPEOF( type ) ( __typeof__ ( type ) )
#else
#define FT_TYPEOF( type ) /* empty */
#endif
/* Use FT_LOCAL and FT_LOCAL_DEF to declare and define, respectively, */
/* a function that gets used only within the scope of a module. */
/* Normally, both the header and source code files for such a */
/* function are within a single module directory. */
/* */
/* Intra-module arrays should be tagged with FT_LOCAL_ARRAY and */
/* FT_LOCAL_ARRAY_DEF. */
/* */
#ifdef FT_MAKE_OPTION_SINGLE_OBJECT
#define FT_LOCAL( x ) static x
#define FT_LOCAL_DEF( x ) static x
#else
#ifdef __cplusplus
#define FT_LOCAL( x ) extern "C" x
#define FT_LOCAL_DEF( x ) extern "C" x
#else
#define FT_LOCAL( x ) extern x
#define FT_LOCAL_DEF( x ) x
#endif
#endif /* FT_MAKE_OPTION_SINGLE_OBJECT */
#define FT_LOCAL_ARRAY( x ) extern const x
#define FT_LOCAL_ARRAY_DEF( x ) const x
/* Use FT_BASE and FT_BASE_DEF to declare and define, respectively, */
/* functions that are used in more than a single module. In the */
/* current setup this implies that the declaration is in a header */
/* file in the `include/freetype/internal' directory, and the */
/* function body is in a file in `src/base'. */
/* */
#ifndef FT_BASE
#ifdef __cplusplus
#define FT_BASE( x ) extern "C" x
#else
#define FT_BASE( x ) extern x
#endif
#endif /* !FT_BASE */
#ifndef FT_BASE_DEF
#ifdef __cplusplus
#define FT_BASE_DEF( x ) x
#else
#define FT_BASE_DEF( x ) x
#endif
#endif /* !FT_BASE_DEF */
/* When compiling FreeType as a DLL or DSO with hidden visibility */
/* some systems/compilers need a special attribute in front OR after */
/* the return type of function declarations. */
/* */
/* Two macros are used within the FreeType source code to define */
/* exported library functions: FT_EXPORT and FT_EXPORT_DEF. */
/* */
/* FT_EXPORT( return_type ) */
/* */
/* is used in a function declaration, as in */
/* */
/* FT_EXPORT( FT_Error ) */
/* FT_Init_FreeType( FT_Library* alibrary ); */
/* */
/* */
/* FT_EXPORT_DEF( return_type ) */
/* */
/* is used in a function definition, as in */
/* */
/* FT_EXPORT_DEF( FT_Error ) */
/* FT_Init_FreeType( FT_Library* alibrary ) */
/* { */
/* ... some code ... */
/* return FT_Err_Ok; */
/* } */
/* */
/* You can provide your own implementation of FT_EXPORT and */
/* FT_EXPORT_DEF here if you want. */
/* */
/* To export a variable, use FT_EXPORT_VAR. */
/* */
#ifndef FT_EXPORT
#ifdef FT2_BUILD_LIBRARY
#if defined( _WIN32 ) && ( defined( _DLL ) || defined( DLL_EXPORT ) )
#define FT_EXPORT( x ) __declspec( dllexport ) x
#elif defined( __GNUC__ ) && __GNUC__ >= 4
#define FT_EXPORT( x ) __attribute__(( visibility( "default" ) )) x
#elif defined( __cplusplus )
#define FT_EXPORT( x ) extern "C" x
#else
#define FT_EXPORT( x ) extern x
#endif
#else
#if defined( FT2_DLLIMPORT )
#define FT_EXPORT( x ) __declspec( dllimport ) x
#elif defined( __cplusplus )
#define FT_EXPORT( x ) extern "C" x
#else
#define FT_EXPORT( x ) extern x
#endif
#endif
#endif /* !FT_EXPORT */
#ifndef FT_EXPORT_DEF
#ifdef __cplusplus
#define FT_EXPORT_DEF( x ) extern "C" x
#else
#define FT_EXPORT_DEF( x ) extern x
#endif
#endif /* !FT_EXPORT_DEF */
#ifndef FT_EXPORT_VAR
#ifdef __cplusplus
#define FT_EXPORT_VAR( x ) extern "C" x
#else
#define FT_EXPORT_VAR( x ) extern x
#endif
#endif /* !FT_EXPORT_VAR */
/* The following macros are needed to compile the library with a */
/* C++ compiler and with 16bit compilers. */
/* */
/* This is special. Within C++, you must specify `extern "C"' for */
/* functions which are used via function pointers, and you also */
/* must do that for structures which contain function pointers to */
/* assure C linkage -- it's not possible to have (local) anonymous */
/* functions which are accessed by (global) function pointers. */
/* */
/* */
/* FT_CALLBACK_DEF is used to _define_ a callback function, */
/* located in the same source code file as the structure that uses */
/* it. */
/* */
/* FT_BASE_CALLBACK and FT_BASE_CALLBACK_DEF are used to declare */
/* and define a callback function, respectively, in a similar way */
/* as FT_BASE and FT_BASE_DEF work. */
/* */
/* FT_CALLBACK_TABLE is used to _declare_ a constant variable that */
/* contains pointers to callback functions. */
/* */
/* FT_CALLBACK_TABLE_DEF is used to _define_ a constant variable */
/* that contains pointers to callback functions. */
/* */
/* */
/* Some 16bit compilers have to redefine these macros to insert */
/* the infamous `_cdecl' or `__fastcall' declarations. */
/* */
#ifndef FT_CALLBACK_DEF
#ifdef __cplusplus
#define FT_CALLBACK_DEF( x ) extern "C" x
#else
#define FT_CALLBACK_DEF( x ) static x
#endif
#endif /* FT_CALLBACK_DEF */
#ifndef FT_BASE_CALLBACK
#ifdef __cplusplus
#define FT_BASE_CALLBACK( x ) extern "C" x
#define FT_BASE_CALLBACK_DEF( x ) extern "C" x
#else
#define FT_BASE_CALLBACK( x ) extern x
#define FT_BASE_CALLBACK_DEF( x ) x
#endif
#endif /* FT_BASE_CALLBACK */
#ifndef FT_CALLBACK_TABLE
#ifdef __cplusplus
#define FT_CALLBACK_TABLE extern "C"
#define FT_CALLBACK_TABLE_DEF extern "C"
#else
#define FT_CALLBACK_TABLE extern
#define FT_CALLBACK_TABLE_DEF /* nothing */
#endif
#endif /* FT_CALLBACK_TABLE */
FT_END_HEADER
#endif /* FTCONFIG_H_ */
/* END */
@@ -0,0 +1,804 @@
/***************************************************************************/
/* */
/* ftheader.h */
/* */
/* Build macros of the FreeType 2 library. */
/* */
/* Copyright 1996-2018 by */
/* David Turner, Robert Wilhelm, and Werner Lemberg. */
/* */
/* This file is part of the FreeType project, and may only be used, */
/* modified, and distributed under the terms of the FreeType project */
/* license, LICENSE.TXT. By continuing to use, modify, or distribute */
/* this file you indicate that you have read the license and */
/* understand and accept it fully. */
/* */
/***************************************************************************/
#ifndef FTHEADER_H_
#define FTHEADER_H_
/*@***********************************************************************/
/* */
/* <Macro> */
/* FT_BEGIN_HEADER */
/* */
/* <Description> */
/* This macro is used in association with @FT_END_HEADER in header */
/* files to ensure that the declarations within are properly */
/* encapsulated in an `extern "C" { .. }' block when included from a */
/* C++ compiler. */
/* */
#ifdef __cplusplus
#define FT_BEGIN_HEADER extern "C" {
#else
#define FT_BEGIN_HEADER /* nothing */
#endif
/*@***********************************************************************/
/* */
/* <Macro> */
/* FT_END_HEADER */
/* */
/* <Description> */
/* This macro is used in association with @FT_BEGIN_HEADER in header */
/* files to ensure that the declarations within are properly */
/* encapsulated in an `extern "C" { .. }' block when included from a */
/* C++ compiler. */
/* */
#ifdef __cplusplus
#define FT_END_HEADER }
#else
#define FT_END_HEADER /* nothing */
#endif
/*************************************************************************/
/* */
/* Aliases for the FreeType 2 public and configuration files. */
/* */
/*************************************************************************/
/*************************************************************************/
/* */
/* <Section> */
/* header_file_macros */
/* */
/* <Title> */
/* Header File Macros */
/* */
/* <Abstract> */
/* Macro definitions used to #include specific header files. */
/* */
/* <Description> */
/* The following macros are defined to the name of specific */
/* FreeType~2 header files. They can be used directly in #include */
/* statements as in: */
/* */
/* { */
/* #include FT_FREETYPE_H */
/* #include FT_MULTIPLE_MASTERS_H */
/* #include FT_GLYPH_H */
/* } */
/* */
/* There are several reasons why we are now using macros to name */
/* public header files. The first one is that such macros are not */
/* limited to the infamous 8.3~naming rule required by DOS (and */
/* `FT_MULTIPLE_MASTERS_H' is a lot more meaningful than `ftmm.h'). */
/* */
/* The second reason is that it allows for more flexibility in the */
/* way FreeType~2 is installed on a given system. */
/* */
/*************************************************************************/
/* configuration files */
/*************************************************************************
*
* @macro:
* FT_CONFIG_CONFIG_H
*
* @description:
* A macro used in #include statements to name the file containing
* FreeType~2 configuration data.
*
*/
#ifndef FT_CONFIG_CONFIG_H
#define FT_CONFIG_CONFIG_H <freetype/config/ftconfig.h>
#endif
/*************************************************************************
*
* @macro:
* FT_CONFIG_STANDARD_LIBRARY_H
*
* @description:
* A macro used in #include statements to name the file containing
* FreeType~2 interface to the standard C library functions.
*
*/
#ifndef FT_CONFIG_STANDARD_LIBRARY_H
#define FT_CONFIG_STANDARD_LIBRARY_H <freetype/config/ftstdlib.h>
#endif
/*************************************************************************
*
* @macro:
* FT_CONFIG_OPTIONS_H
*
* @description:
* A macro used in #include statements to name the file containing
* FreeType~2 project-specific configuration options.
*
*/
#ifndef FT_CONFIG_OPTIONS_H
#define FT_CONFIG_OPTIONS_H <freetype/config/ftoption.h>
#endif
/*************************************************************************
*
* @macro:
* FT_CONFIG_MODULES_H
*
* @description:
* A macro used in #include statements to name the file containing the
* list of FreeType~2 modules that are statically linked to new library
* instances in @FT_Init_FreeType.
*
*/
#ifndef FT_CONFIG_MODULES_H
#define FT_CONFIG_MODULES_H <freetype/config/ftmodule.h>
#endif
/* */
/* public headers */
/*************************************************************************
*
* @macro:
* FT_FREETYPE_H
*
* @description:
* A macro used in #include statements to name the file containing the
* base FreeType~2 API.
*
*/
#define FT_FREETYPE_H <freetype/freetype.h>
/*************************************************************************
*
* @macro:
* FT_ERRORS_H
*
* @description:
* A macro used in #include statements to name the file containing the
* list of FreeType~2 error codes (and messages).
*
* It is included by @FT_FREETYPE_H.
*
*/
#define FT_ERRORS_H <freetype/fterrors.h>
/*************************************************************************
*
* @macro:
* FT_MODULE_ERRORS_H
*
* @description:
* A macro used in #include statements to name the file containing the
* list of FreeType~2 module error offsets (and messages).
*
*/
#define FT_MODULE_ERRORS_H <freetype/ftmoderr.h>
/*************************************************************************
*
* @macro:
* FT_SYSTEM_H
*
* @description:
* A macro used in #include statements to name the file containing the
* FreeType~2 interface to low-level operations (i.e., memory management
* and stream i/o).
*
* It is included by @FT_FREETYPE_H.
*
*/
#define FT_SYSTEM_H <freetype/ftsystem.h>
/*************************************************************************
*
* @macro:
* FT_IMAGE_H
*
* @description:
* A macro used in #include statements to name the file containing type
* definitions related to glyph images (i.e., bitmaps, outlines,
* scan-converter parameters).
*
* It is included by @FT_FREETYPE_H.
*
*/
#define FT_IMAGE_H <freetype/ftimage.h>
/*************************************************************************
*
* @macro:
* FT_TYPES_H
*
* @description:
* A macro used in #include statements to name the file containing the
* basic data types defined by FreeType~2.
*
* It is included by @FT_FREETYPE_H.
*
*/
#define FT_TYPES_H <freetype/fttypes.h>
/*************************************************************************
*
* @macro:
* FT_LIST_H
*
* @description:
* A macro used in #include statements to name the file containing the
* list management API of FreeType~2.
*
* (Most applications will never need to include this file.)
*
*/
#define FT_LIST_H <freetype/ftlist.h>
/*************************************************************************
*
* @macro:
* FT_OUTLINE_H
*
* @description:
* A macro used in #include statements to name the file containing the
* scalable outline management API of FreeType~2.
*
*/
#define FT_OUTLINE_H <freetype/ftoutln.h>
/*************************************************************************
*
* @macro:
* FT_SIZES_H
*
* @description:
* A macro used in #include statements to name the file containing the
* API which manages multiple @FT_Size objects per face.
*
*/
#define FT_SIZES_H <freetype/ftsizes.h>
/*************************************************************************
*
* @macro:
* FT_MODULE_H
*
* @description:
* A macro used in #include statements to name the file containing the
* module management API of FreeType~2.
*
*/
#define FT_MODULE_H <freetype/ftmodapi.h>
/*************************************************************************
*
* @macro:
* FT_RENDER_H
*
* @description:
* A macro used in #include statements to name the file containing the
* renderer module management API of FreeType~2.
*
*/
#define FT_RENDER_H <freetype/ftrender.h>
/*************************************************************************
*
* @macro:
* FT_DRIVER_H
*
* @description:
* A macro used in #include statements to name the file containing
* structures and macros related to the driver modules.
*
*/
#define FT_DRIVER_H <freetype/ftdriver.h>
/*************************************************************************
*
* @macro:
* FT_AUTOHINTER_H
*
* @description:
* A macro used in #include statements to name the file containing
* structures and macros related to the auto-hinting module.
*
* Deprecated since version 2.9; use @FT_DRIVER_H instead.
*
*/
#define FT_AUTOHINTER_H FT_DRIVER_H
/*************************************************************************
*
* @macro:
* FT_CFF_DRIVER_H
*
* @description:
* A macro used in #include statements to name the file containing
* structures and macros related to the CFF driver module.
*
* Deprecated since version 2.9; use @FT_DRIVER_H instead.
*
*/
#define FT_CFF_DRIVER_H FT_DRIVER_H
/*************************************************************************
*
* @macro:
* FT_TRUETYPE_DRIVER_H
*
* @description:
* A macro used in #include statements to name the file containing
* structures and macros related to the TrueType driver module.
*
* Deprecated since version 2.9; use @FT_DRIVER_H instead.
*
*/
#define FT_TRUETYPE_DRIVER_H FT_DRIVER_H
/*************************************************************************
*
* @macro:
* FT_PCF_DRIVER_H
*
* @description:
* A macro used in #include statements to name the file containing
* structures and macros related to the PCF driver module.
*
* Deprecated since version 2.9; use @FT_DRIVER_H instead.
*
*/
#define FT_PCF_DRIVER_H FT_DRIVER_H
/*************************************************************************
*
* @macro:
* FT_TYPE1_TABLES_H
*
* @description:
* A macro used in #include statements to name the file containing the
* types and API specific to the Type~1 format.
*
*/
#define FT_TYPE1_TABLES_H <freetype/t1tables.h>
/*************************************************************************
*
* @macro:
* FT_TRUETYPE_IDS_H
*
* @description:
* A macro used in #include statements to name the file containing the
* enumeration values which identify name strings, languages, encodings,
* etc. This file really contains a _large_ set of constant macro
* definitions, taken from the TrueType and OpenType specifications.
*
*/
#define FT_TRUETYPE_IDS_H <freetype/ttnameid.h>
/*************************************************************************
*
* @macro:
* FT_TRUETYPE_TABLES_H
*
* @description:
* A macro used in #include statements to name the file containing the
* types and API specific to the TrueType (as well as OpenType) format.
*
*/
#define FT_TRUETYPE_TABLES_H <freetype/tttables.h>
/*************************************************************************
*
* @macro:
* FT_TRUETYPE_TAGS_H
*
* @description:
* A macro used in #include statements to name the file containing the
* definitions of TrueType four-byte `tags' which identify blocks in
* SFNT-based font formats (i.e., TrueType and OpenType).
*
*/
#define FT_TRUETYPE_TAGS_H <freetype/tttags.h>
/*************************************************************************
*
* @macro:
* FT_BDF_H
*
* @description:
* A macro used in #include statements to name the file containing the
* definitions of an API which accesses BDF-specific strings from a
* face.
*
*/
#define FT_BDF_H <freetype/ftbdf.h>
/*************************************************************************
*
* @macro:
* FT_CID_H
*
* @description:
* A macro used in #include statements to name the file containing the
* definitions of an API which access CID font information from a
* face.
*
*/
#define FT_CID_H <freetype/ftcid.h>
/*************************************************************************
*
* @macro:
* FT_GZIP_H
*
* @description:
* A macro used in #include statements to name the file containing the
* definitions of an API which supports gzip-compressed files.
*
*/
#define FT_GZIP_H <freetype/ftgzip.h>
/*************************************************************************
*
* @macro:
* FT_LZW_H
*
* @description:
* A macro used in #include statements to name the file containing the
* definitions of an API which supports LZW-compressed files.
*
*/
#define FT_LZW_H <freetype/ftlzw.h>
/*************************************************************************
*
* @macro:
* FT_BZIP2_H
*
* @description:
* A macro used in #include statements to name the file containing the
* definitions of an API which supports bzip2-compressed files.
*
*/
#define FT_BZIP2_H <freetype/ftbzip2.h>
/*************************************************************************
*
* @macro:
* FT_WINFONTS_H
*
* @description:
* A macro used in #include statements to name the file containing the
* definitions of an API which supports Windows FNT files.
*
*/
#define FT_WINFONTS_H <freetype/ftwinfnt.h>
/*************************************************************************
*
* @macro:
* FT_GLYPH_H
*
* @description:
* A macro used in #include statements to name the file containing the
* API of the optional glyph management component.
*
*/
#define FT_GLYPH_H <freetype/ftglyph.h>
/*************************************************************************
*
* @macro:
* FT_BITMAP_H
*
* @description:
* A macro used in #include statements to name the file containing the
* API of the optional bitmap conversion component.
*
*/
#define FT_BITMAP_H <freetype/ftbitmap.h>
/*************************************************************************
*
* @macro:
* FT_BBOX_H
*
* @description:
* A macro used in #include statements to name the file containing the
* API of the optional exact bounding box computation routines.
*
*/
#define FT_BBOX_H <freetype/ftbbox.h>
/*************************************************************************
*
* @macro:
* FT_CACHE_H
*
* @description:
* A macro used in #include statements to name the file containing the
* API of the optional FreeType~2 cache sub-system.
*
*/
#define FT_CACHE_H <freetype/ftcache.h>
/*************************************************************************
*
* @macro:
* FT_MAC_H
*
* @description:
* A macro used in #include statements to name the file containing the
* Macintosh-specific FreeType~2 API. The latter is used to access
* fonts embedded in resource forks.
*
* This header file must be explicitly included by client applications
* compiled on the Mac (note that the base API still works though).
*
*/
#define FT_MAC_H <freetype/ftmac.h>
/*************************************************************************
*
* @macro:
* FT_MULTIPLE_MASTERS_H
*
* @description:
* A macro used in #include statements to name the file containing the
* optional multiple-masters management API of FreeType~2.
*
*/
#define FT_MULTIPLE_MASTERS_H <freetype/ftmm.h>
/*************************************************************************
*
* @macro:
* FT_SFNT_NAMES_H
*
* @description:
* A macro used in #include statements to name the file containing the
* optional FreeType~2 API which accesses embedded `name' strings in
* SFNT-based font formats (i.e., TrueType and OpenType).
*
*/
#define FT_SFNT_NAMES_H <freetype/ftsnames.h>
/*************************************************************************
*
* @macro:
* FT_OPENTYPE_VALIDATE_H
*
* @description:
* A macro used in #include statements to name the file containing the
* optional FreeType~2 API which validates OpenType tables (BASE, GDEF,
* GPOS, GSUB, JSTF).
*
*/
#define FT_OPENTYPE_VALIDATE_H <freetype/ftotval.h>
/*************************************************************************
*
* @macro:
* FT_GX_VALIDATE_H
*
* @description:
* A macro used in #include statements to name the file containing the
* optional FreeType~2 API which validates TrueTypeGX/AAT tables (feat,
* mort, morx, bsln, just, kern, opbd, trak, prop).
*
*/
#define FT_GX_VALIDATE_H <freetype/ftgxval.h>
/*************************************************************************
*
* @macro:
* FT_PFR_H
*
* @description:
* A macro used in #include statements to name the file containing the
* FreeType~2 API which accesses PFR-specific data.
*
*/
#define FT_PFR_H <freetype/ftpfr.h>
/*************************************************************************
*
* @macro:
* FT_STROKER_H
*
* @description:
* A macro used in #include statements to name the file containing the
* FreeType~2 API which provides functions to stroke outline paths.
*/
#define FT_STROKER_H <freetype/ftstroke.h>
/*************************************************************************
*
* @macro:
* FT_SYNTHESIS_H
*
* @description:
* A macro used in #include statements to name the file containing the
* FreeType~2 API which performs artificial obliquing and emboldening.
*/
#define FT_SYNTHESIS_H <freetype/ftsynth.h>
/*************************************************************************
*
* @macro:
* FT_FONT_FORMATS_H
*
* @description:
* A macro used in #include statements to name the file containing the
* FreeType~2 API which provides functions specific to font formats.
*/
#define FT_FONT_FORMATS_H <freetype/ftfntfmt.h>
/* deprecated */
#define FT_XFREE86_H FT_FONT_FORMATS_H
/*************************************************************************
*
* @macro:
* FT_TRIGONOMETRY_H
*
* @description:
* A macro used in #include statements to name the file containing the
* FreeType~2 API which performs trigonometric computations (e.g.,
* cosines and arc tangents).
*/
#define FT_TRIGONOMETRY_H <freetype/fttrigon.h>
/*************************************************************************
*
* @macro:
* FT_LCD_FILTER_H
*
* @description:
* A macro used in #include statements to name the file containing the
* FreeType~2 API which performs color filtering for subpixel rendering.
*/
#define FT_LCD_FILTER_H <freetype/ftlcdfil.h>
/*************************************************************************
*
* @macro:
* FT_INCREMENTAL_H
*
* @description:
* A macro used in #include statements to name the file containing the
* FreeType~2 API which performs incremental glyph loading.
*/
#define FT_INCREMENTAL_H <freetype/ftincrem.h>
/*************************************************************************
*
* @macro:
* FT_GASP_H
*
* @description:
* A macro used in #include statements to name the file containing the
* FreeType~2 API which returns entries from the TrueType GASP table.
*/
#define FT_GASP_H <freetype/ftgasp.h>
/*************************************************************************
*
* @macro:
* FT_ADVANCES_H
*
* @description:
* A macro used in #include statements to name the file containing the
* FreeType~2 API which returns individual and ranged glyph advances.
*/
#define FT_ADVANCES_H <freetype/ftadvanc.h>
/* */
/* These header files don't need to be included by the user. */
#define FT_ERROR_DEFINITIONS_H <freetype/fterrdef.h>
#define FT_PARAMETER_TAGS_H <freetype/ftparams.h>
/* Deprecated macros. */
#define FT_UNPATENTED_HINTING_H <freetype/ftparams.h>
#define FT_TRUETYPE_UNPATENTED_H <freetype/ftparams.h>
/* FT_CACHE_H is the only header file needed for the cache subsystem. */
#define FT_CACHE_IMAGE_H FT_CACHE_H
#define FT_CACHE_SMALL_BITMAPS_H FT_CACHE_H
#define FT_CACHE_CHARMAP_H FT_CACHE_H
/* The internals of the cache sub-system are no longer exposed. We */
/* default to FT_CACHE_H at the moment just in case, but we know of */
/* no rogue client that uses them. */
/* */
#define FT_CACHE_MANAGER_H FT_CACHE_H
#define FT_CACHE_INTERNAL_MRU_H FT_CACHE_H
#define FT_CACHE_INTERNAL_MANAGER_H FT_CACHE_H
#define FT_CACHE_INTERNAL_CACHE_H FT_CACHE_H
#define FT_CACHE_INTERNAL_GLYPH_H FT_CACHE_H
#define FT_CACHE_INTERNAL_IMAGE_H FT_CACHE_H
#define FT_CACHE_INTERNAL_SBITS_H FT_CACHE_H
/*
* Include internal headers definitions from <internal/...>
* only when building the library.
*/
#ifdef FT2_BUILD_LIBRARY
#define FT_INTERNAL_INTERNAL_H <freetype/internal/internal.h>
#include FT_INTERNAL_INTERNAL_H
#endif /* FT2_BUILD_LIBRARY */
#endif /* FTHEADER_H_ */
/* END */
Loaded 100 of 244 files, more files were not shown because too many files have changed in this diff. Show more