Compare commits

...
206 Commits
Author SHA1 Message Date
Cameron Gutman 12b7f6b7a3 Fix appstream metadata release date for v0.8.1 2019-01-29 18:52:01 -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
Cameron Gutman 6e4dd161ac Version 0.6.0 2018-10-06 20:46:01 -07:00
Cameron Gutman c724f01c98 Remove PortAudio from required libraries 2018-10-06 20:12:49 -07:00
Cameron Gutman 1fc63a6529 Exclude libsoundio from Linux build 2018-10-06 18:24:34 -07:00
Cameron Gutman 5d84ab4a47 Update SDL_GameControllerDB to 19944691bf18b97c0df2254ce52772b549d4d2bf 2018-10-06 14:07:22 -07:00
Cameron Gutman fc315feac4 Maximize the Qt UI window if configured for a full-screen display mode 2018-10-06 13:14:32 -07:00
Cameron Gutman 82a87e96b3 Include the indicator in combo box width 2018-10-06 12:59:59 -07:00
Cameron Gutman 7d1ecc8d82 Fix popup font resizing on macOS 2018-10-06 12:41:55 -07:00
Cameron Gutman f9daf2a45c Resize settings combo boxes to match their contents 2018-10-06 12:12:05 -07:00
Cameron Gutman 113751a4bc Avoid Qt bug by using onAfterRendering instead of onAfterSynchronizing to display dialogs 2018-10-05 19:54:13 -07:00
Cameron Gutman e182445593 libsoundio audio renderer for Windows and Mac (#97) 2018-10-05 19:22:57 -07:00
Cameron Gutman 6661ca17c2 Fix keys being stuck after Moonlight loses focus or is quit via OS shortcut (Alt+Tab/Alt+F4) 2018-10-03 18:27:12 -07:00
Cameron Gutman 1b4e75f49e Add support for sending X1 and X2 mouse buttons. Fixes #91 2018-10-03 17:56:42 -07:00
Cameron Gutman 22ed7f4e49 Merge pull request #94 from jhakonen/review/fix-pc-view-alignment
PC view: position app's icon and text on same level with other apps
2018-10-03 17:40:49 -07:00
Janne Hakonen 1d691cf763 Reserve a static size for app icon in PC view to position the icon and text on same level with other apps 2018-10-03 19:31:37 +03:00
Cameron Gutman 9075f309ad Disable annoying stream stats prints 2018-10-02 20:12:53 -07:00
Cameron Gutman 18d97e9f73 Fix errant touch events on macOS 2018-10-02 20:11:13 -07:00
Cameron Gutman bccd118abb Finish removal of auto audio option and rename the remaining options 2018-10-02 15:30:22 -07:00
Cameron Gutman 3dc049ee5f Remove automatic audio configuration permanently due to brokenness and allow renderers to request reinitialization on demand 2018-10-02 01:21:42 -07:00
Cameron Gutman a614a693e5 Revert "Fix surround sound channel mapping on ALSA" since it can be handled inside PortAudio 2018-10-02 01:09:13 -07:00
Cameron Gutman 8dddcd04d5 Remove IAudioRenderer::testAudio() and just use IAudioRenderer::prepareForPlayback() instead 2018-10-01 19:49:08 -07:00
Cameron Gutman 209b4a1b02 Fix surround sound channel mapping on ALSA 2018-10-01 18:46:16 -07:00
Cameron Gutman b14a0fa2ce Ensure initial warning dialogs always appear on top of the Moonlight window 2018-09-30 22:39:51 -07:00
Cameron Gutman 5f05363996 Fix keyboard navigation of quit app and delete PC dialogs 2018-09-30 22:20:19 -07:00
Cameron Gutman d5770ad8cc Fix "Connection terminated" dialog being stuck out of input focus on gamepad 2018-09-30 18:38:54 -07:00
Cameron Gutman c239d0814b Move audio renderer destruction back off the main thread now that we're using atexit(SDL_Quit) 2018-09-30 18:09:12 -07:00
Cameron Gutman f7789a1e4a Add Select+Start+L1+R1 quit combo 2018-09-30 14:38:54 -07:00
Cameron Gutman 6d382078d3 Allow gamepad navigation with the left analog stick 2018-09-30 14:30:12 -07:00
Cameron Gutman 162a16ed3a Allow settings to be activated when an app or PC is in focus 2018-09-30 13:52:38 -07:00
Cameron Gutman d706e81cd4 Add gamepad navigation support for everything except context menus and dialogs 2018-09-30 13:41:32 -07:00
Cameron Gutman 9fb0bffd61 Finish touch input support 2018-09-30 01:03:26 -07:00
Cameron Gutman 4a452a2467 Touch input WIP 2018-09-29 22:43:28 -07:00
Cameron Gutman 396d65b003 Fix typo in previous commit 2018-09-29 22:24:26 -07:00
Cameron Gutman fa52e7c1b7 Fix audio device error with PA renderer (and constify some methods) 2018-09-29 22:19:41 -07:00
Cameron Gutman 6758d6c43e Create MappingManager class to handle gamepad mappings (currently only saving custom mappings) 2018-09-29 20:33:45 -07:00
Cameron Gutman 4ad27670ec Provide detection and help for unmapped gamepads. Fixes #70 2018-09-29 19:14:52 -07:00
Cameron Gutman 1ad072236d Avoid calling Pa_IsStreamStopped() on each audio frame 2018-09-29 16:52:40 -07:00
Cameron Gutman 76387aacad Switch SDL_GameControllerDB to submodule 2018-09-29 16:43:14 -07:00
Cameron Gutman 200b1c5095 Rename hpp -> h 2018-09-29 16:18:46 -07:00
Janne Hakonen d14cfb577b Add command line parameters. Fixes #30 (#89)
* Add command line parameters. Fixes #30

* Fixed compile errors

* Fixed code review findings

* Fixed code review findings, take 2
2018-09-29 14:06:55 -07:00
Cameron Gutman c99b4f1559 Add additional requirements for generate-installers.bat 2018-09-29 03:30:50 -07:00
Cameron Gutman 45fdbd16b4 Update readme with better build instructions 2018-09-29 03:24:26 -07:00
Cameron Gutman c04113c9fa Fix GCC and Clang build 2018-09-29 03:11:17 -07:00
Cameron Gutman 4620fa5001 Retry serverinfo query for adding a PC if the request fails with ServiceUnavailableError. Fixes #88 2018-09-29 03:01:49 -07:00
Cameron Gutman ba7dd31035 Refactor ComputerManager and BoxArtManager to get code out of headers and split NvComputer into its own file 2018-09-29 02:19:44 -07:00
Cameron Gutman 3928155b9c Fix Windows build 2018-09-25 00:59:40 -07:00
Cameron Gutman a0249b4337 Add video stats for performance overlay 2018-09-25 00:47:59 -07:00
Cameron Gutman d89982237d Use clock_gettime() on Linux 2018-09-24 20:40:49 -07:00
Cameron Gutman 7e414f0af3 Add keyboard shortcuts for Help and Settings 2018-09-23 19:24:47 -07:00
Cameron Gutman 8fb37ae229 Finish keyboard navigation for GridViews and MenuItems 2018-09-23 19:06:26 -07:00
Cameron Gutman 2e3de32810 Partially working keyboard navigation support 2018-09-23 15:16:27 -07:00
Cameron Gutman f2e40889b2 Performance tweaks to PortAudio renderer 2018-09-22 21:00:44 -07:00
Cameron Gutman 896b247bc0 Remove H.264 high performance preference and enable HEVC split encode 2018-09-22 20:27:02 -07:00
Cameron Gutman 605d52f160 Don't block in audio/video renderer callbacks while the renderer is being destroyed 2018-09-22 20:03:41 -07:00
Cameron Gutman afb2b465a8 Avoid using direct submit for video decoding because FFmpeg can delay for a fairly significant period of time within avcodec_send_packet() and avcodec_receive_frame(), even for software decoding. This is particularly apparent when streaming well above hardware capabilities and leads to packet loss. 2018-09-22 19:30:13 -07:00
Cameron Gutman b1de66d878 Directly use the AVBufferSampleDisplayLayer as the rendering view's layer to increase efficiency 2018-09-22 18:34:15 -07:00
Cameron Gutman 71b625081a Switch to PortAudio for audio playback 2018-09-22 17:39:12 -07:00
Cameron Gutman 2cd4851601 Improve PortAudio channel autodetection algorithm to workaround PulseAudio issues 2018-09-22 16:51:45 -07:00
Cameron Gutman 987d104a6c Fix library load name 2018-09-22 16:51:04 -07:00
Cameron Gutman 60e6398dfa Sign libportaudio.dylib 2018-09-22 16:25:10 -07:00
Cameron Gutman 14ec0259df Switch back from Qt Multimedia to SDL for audio configuration detection. Qt is also broken, but just broken in a way that always reports stereo 2018-09-22 16:20:00 -07:00
Cameron Gutman 9f0617f6ee Gracefully fall back to stereo audio if 5.1 doesn't work 2018-09-22 16:04:36 -07:00
Cameron Gutman 98297e0aed Add PortAudio libraries and headers 2018-09-22 15:56:10 -07:00
Cameron Gutman 926b192722 Warn portable users if they download the wrong version 2018-09-22 14:16:48 -07:00
Cameron Gutman 97e5acecd7 Precompile QML files to avoid writing qmlcache. Fixes #86 2018-09-22 13:44:58 -07:00
Cameron Gutman 2c960059ee Add PoC PortAudio audio renderer 2018-09-21 23:30:47 -07:00
Cameron Gutman e20bf3fbd9 Fix persistence of deleted PCs. Fixes #82 2018-09-21 20:21:49 -07:00
Cameron Gutman af9fb0e091 Fix GetVideoProcessorCaps() failure handling 2018-09-21 17:29:53 -07:00
Cameron Gutman f96911e1ba Use DXVA2_VideoProcProgressiveDevice since we don't need any fancy image processing or deinterlacing 2018-09-21 17:07:28 -07:00
Cameron Gutman fdbb1c8d72 Only set QT_OPENGL if it's not already set 2018-09-20 21:27:19 -07:00
Cameron Gutman 8a0089eb81 Print driver information in VDPAU and VAAPI renderers 2018-09-20 21:23:16 -07:00
Cameron Gutman 821d4ac9dd Version 0.5.1 (hotfix for Windows and Mac only) 2018-09-16 12:18:58 -07:00
Cameron Gutman f4408473e6 Fix broken Alt+Tab behavior. Fixes #72 2018-09-16 11:21:15 -07:00
167 changed files with 6999 additions and 2205 deletions
+10 -2
View File
@@ -13,6 +13,9 @@ Any special steps that are required for the bug to appear.
**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)**
- Have any settings been adjusted from defaults?
- 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]
- 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 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`
**Additional context**
Add any other context about the problem here.
Anything else you think may be relevant to the issue
+3 -1
View File
@@ -2,4 +2,6 @@
**/.vs/
build/
build/
config.tests/*/.qmake.stash
config.tests/*/Makefile
+9
View File
@@ -4,3 +4,12 @@
[submodule "qmdnsengine/qmdnsengine"]
path = qmdnsengine/qmdnsengine
url = https://github.com/cgutman/qmdnsengine.git
[submodule "app/SDL_GameControllerDB"]
path = app/SDL_GameControllerDB
url = https://github.com/gabomdq/SDL_GameControllerDB.git
[submodule "soundio/libsoundio"]
path = soundio/libsoundio
url = https://github.com/andrewrk/libsoundio.git
[submodule "h264bitstream/h264bitstream"]
path = h264bitstream/h264bitstream
url = https://github.com/aizvorski/h264bitstream.git
+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();
+34 -8
View File
@@ -1,11 +1,9 @@
# 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).
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).
[![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)
@@ -19,18 +17,46 @@ You can follow development on our [Discord server](https://discord.gg/6ERtzFY).
- HEVC support for better image quality at reduced bandwidth
- Keyboard and mouse support
- Gamepad support with SDL gamepad mappings
## Downloads
- [Windows and macOS](https://github.com/moonlight-stream/moonlight-qt/releases)
- [Linux](https://flathub.org/apps/details/com.moonlight_stream.Moonlight)
## Building
### General Build Requirements
* Qt 5.9 SDK or later
### Windows-specific Requirements
* Windows 7 or later
* Visual Studio 2015 or later
* Select MSVC Desktop toolchain during Qt installation
* 7-Zip (only if building installers for non-development PCs)
* WiX Toolset v3.11 or later (only if building installers for non-development PCs)
### Mac-specific Requirements
* macOS Sierra (10.12) or later
* Xcode 10
* [create-dmg](https://github.com/sindresorhus/create-dmg) (only if building DMGs for use on non-development Macs)
### Linux-specific Requirements
* GCC or Clang
* Install your distro equivalents of: `openssl-devel qt5-devel SDL2-devel ffmpeg-devel qt5-qtquickcontrols2-devel libva-devel libvdpau-devel opus-devel pulseaudio-libs-devel alsa-lib-devel`
* 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
1. Install the latest Qt SDK (and optionally, the Qt Creator IDE) from https://www.qt.io/download
- On Windows, install Visual Studio 2017 and select MSVC 2017 toolchain during Qt installation
- On Mac, install the latest version of XCode
- On Linux, install your distro equivalents of: `openssl-devel qt5-devel SDL2-devel ffmpeg-devel qt5-qtquickcontrols2-devel libva-devel libvdpau-devel`
* 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/`
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.
* 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`.
* To build from the command line for development use, run `qmake moonlight-qt.pro` then `make debug` or `make release`
## Contribute
1. Fork us
2. Write code
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>
<true/>
<key>CFBundleVersion</key>
<string>0.5.0</string>
<string>VERSION</string>
<key>CFBundleShortVersionString</key>
<string>0.5.0</string>
<string>VERSION</string>
<key>CFBundleDisplayName</key>
<string>Moonlight</string>
</dict>
+87 -16
View File
@@ -1,4 +1,4 @@
QT += core quick network quickcontrols2 svg multimedia
QT += core quick network quickcontrols2 svg
CONFIG += c++11
unix:!macx {
@@ -14,6 +14,15 @@ CONFIG += debug_and_release
# Ensure symbols are always generated
CONFIG += force_debug_info
# Precompile QML files to avoid writing qmlcache on portable versions.
# 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.
win32|macx {
CONFIG(release, debug|release) {
CONFIG += qtquickcompiler
}
}
TEMPLATE = app
# The following define makes your compiler emit warnings if you use
@@ -45,9 +54,17 @@ macx {
}
unix:!macx {
CONFIG += link_pkgconfig
CONFIG += link_pkgconfig soundio
PKGCONFIG += openssl sdl2 opus
# For libsoundio
packagesExist(libpulse) {
PKGCONFIG += libpulse
}
packagesExist(alsa) {
PKGCONFIG += alsa
}
packagesExist(libavcodec) {
PKGCONFIG += libavcodec libavutil
CONFIG += ffmpeg
@@ -69,52 +86,68 @@ unix:!macx {
}
win32 {
LIBS += -llibssl -llibcrypto -lSDL2 -lavcodec -lavutil -lopus
CONFIG += ffmpeg
CONFIG += ffmpeg soundio
}
macx {
LIBS += -lssl -lcrypto -lavcodec.58 -lavutil.56 -lopus -framework SDL2
LIBS += -lobjc -framework VideoToolbox -framework AVFoundation -framework CoreVideo -framework CoreGraphics -framework CoreMedia -framework AppKit
CONFIG += ffmpeg
# For libsoundio
LIBS += -framework CoreAudio -framework AudioUnit
CONFIG += ffmpeg soundio
}
SOURCES += \
main.cpp \
backend/computerseeker.cpp \
backend/identitymanager.cpp \
backend/nvcomputer.cpp \
backend/nvhttp.cpp \
backend/nvpairingmanager.cpp \
backend/computermanager.cpp \
backend/boxartmanager.cpp \
cli/commandlineparser.cpp \
cli/quitstream.cpp \
cli/startstream.cpp \
settings/streamingpreferences.cpp \
streaming/input.cpp \
streaming/session.cpp \
streaming/audio/audio.cpp \
streaming/audio/renderers/sdlaud.cpp \
streaming/audio/renderers/qtaud.cpp \
gui/computermodel.cpp \
gui/appmodel.cpp \
streaming/streamutils.cpp \
backend/autoupdatechecker.cpp \
path.cpp
path.cpp \
settings/mappingmanager.cpp \
gui/sdlgamepadkeynavigation.cpp
HEADERS += \
utils.h \
backend/computerseeker.h \
backend/identitymanager.h \
backend/nvcomputer.h \
backend/nvhttp.h \
backend/nvpairingmanager.h \
backend/computermanager.h \
backend/boxartmanager.h \
cli/commandlineparser.h \
cli/quitstream.h \
cli/startstream.h \
settings/streamingpreferences.h \
streaming/input.hpp \
streaming/session.hpp \
streaming/input.h \
streaming/session.h \
streaming/audio/renderers/renderer.h \
streaming/audio/renderers/sdl.h \
streaming/audio/renderers/qtaud.h \
gui/computermodel.h \
gui/appmodel.h \
streaming/video/decoder.h \
streaming/streamutils.h \
backend/autoupdatechecker.h \
path.h
path.h \
settings/mappingmanager.h \
gui/sdlgamepadkeynavigation.h
# Platform-specific renderers and decoders
ffmpeg {
@@ -166,6 +199,7 @@ config_SLVideo {
DEFINES += HAVE_SLVIDEO
LIBS += -lSLVideo
SOURCES += streaming/video/sl.cpp
HEADERS += streaming/video/sl.h
}
@@ -191,6 +225,13 @@ macx {
streaming/video/ffmpeg-renderers/vt.h \
streaming/video/ffmpeg-renderers/pacer/displaylinkvsyncsource.h
}
soundio {
message(libsoundio audio renderer selected)
DEFINES += HAVE_SOUNDIO SOUNDIO_STATIC_LIBRARY
SOURCES += streaming/audio/renderers/soundioaudiorenderer.cpp
HEADERS += streaming/audio/renderers/soundioaudiorenderer.h
}
RESOURCES += \
resources.qrc \
@@ -216,6 +257,26 @@ else:unix: LIBS += -L$$OUT_PWD/../qmdnsengine/ -lqmdnsengine
INCLUDEPATH += $$PWD/../qmdnsengine/qmdnsengine/src/include $$PWD/../qmdnsengine
DEPENDPATH += $$PWD/../qmdnsengine/qmdnsengine/src/include $$PWD/../qmdnsengine
win32:CONFIG(release, debug|release): LIBS += -L$$OUT_PWD/../soundio/release/ -lsoundio
else:win32:CONFIG(debug, debug|release): LIBS += -L$$OUT_PWD/../soundio/debug/ -lsoundio
else:unix: LIBS += -L$$OUT_PWD/../soundio/ -lsoundio
INCLUDEPATH += $$PWD/../soundio/libsoundio
DEPENDPATH += $$PWD/../soundio/libsoundio
win32:CONFIG(release, debug|release): LIBS += -L$$OUT_PWD/../h264bitstream/release/ -lh264bitstream
else:win32:CONFIG(debug, debug|release): LIBS += -L$$OUT_PWD/../h264bitstream/debug/ -lh264bitstream
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
unix:!macx: {
isEmpty(PREFIX) {
PREFIX = /usr/local
@@ -238,7 +299,7 @@ unix:!macx: {
appstream.files = deploy/linux/com.moonlight_stream.Moonlight.appdata.xml
appstream.path = $$PREFIX/$$DATADIR/metainfo/
appdata.files = gamecontrollerdb.txt
appdata.files = SDL_GameControllerDB/gamecontrollerdb.txt
appdata.path = "$$PREFIX/$$DATADIR/Moonlight Game Streaming Project/Moonlight/"
INSTALLS += target appdata desktop icons appstream
@@ -253,12 +314,22 @@ win32 {
QMAKE_LFLAGS += /MANIFEST:embed /MANIFESTINPUT:$${PWD}/Moonlight.exe.manifest
}
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 gamecontrollerdb.txt
QMAKE_INFO_PLIST = $$OUT_PWD/Info.plist
APP_BUNDLE_RESOURCES.files = moonlight.icns SDL_GameControllerDB/gamecontrollerdb.txt
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.5.0
DEFINES += VERSION_STR=\\\"0.5.0\\\"
VERSION = "$$cat(version.txt)"
DEFINES += VERSION_STR=\\\"$$cat(version.txt)\\\"
+6 -2
View File
@@ -12,6 +12,9 @@ AutoUpdateChecker::AutoUpdateChecker(QObject *parent) :
// Never communicate over HTTP
m_Nam.setStrictTransportSecurityEnabled(true);
// Allow HTTP redirects
m_Nam.setRedirectPolicy(QNetworkRequest::NoLessSafeRedirectPolicy);
connect(&m_Nam, SIGNAL(finished(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
// 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));
#endif
}
@@ -113,7 +116,8 @@ void AutoUpdateChecker::handleUpdateCheckRequestFinished(QNetworkReply* reply)
if (currentVer < latestVer) {
qDebug() << "Update available";
emit onUpdateAvailable(updateObj["browser_url"].toString());
emit onUpdateAvailable(updateObj["version"].toString(),
updateObj["browser_url"].toString());
return;
}
else if (currentVer > latestVer) {
+1 -1
View File
@@ -12,7 +12,7 @@ public:
Q_INVOKABLE void start();
signals:
void onUpdateAvailable(QString url);
void onUpdateAvailable(QString newVersion, QString url);
private slots:
void handleUpdateCheckRequestFinished(QNetworkReply* reply);
+36 -1
View File
@@ -36,6 +36,39 @@ BoxArtManager::getFilePathForBoxArt(NvComputer* computer, int appId)
return dir.filePath(QString::number(appId) + ".png");
}
class NetworkBoxArtLoadTask : public QObject, public QRunnable
{
Q_OBJECT
public:
NetworkBoxArtLoadTask(BoxArtManager* boxArtManager, NvComputer* computer, NvApp& app)
: m_Bam(boxArtManager),
m_Computer(computer),
m_App(app)
{
connect(this, SIGNAL(boxArtFetchCompleted(NvComputer*,NvApp,QUrl)),
boxArtManager, SLOT(handleBoxArtLoadComplete(NvComputer*,NvApp,QUrl)));
}
signals:
void boxArtFetchCompleted(NvComputer* computer, NvApp app, QUrl image);
private:
void run()
{
QUrl image = m_Bam->loadBoxArtFromNetwork(m_Computer, m_App.id);
if (image.isEmpty()) {
// Give it another shot if it fails once
image = m_Bam->loadBoxArtFromNetwork(m_Computer, m_App.id);
}
emit boxArtFetchCompleted(m_Computer, m_App, image);
}
BoxArtManager* m_Bam;
NvComputer* m_Computer;
NvApp m_App;
};
QUrl BoxArtManager::loadBoxArt(NvComputer* computer, NvApp& app)
{
// Try to open the cached file
@@ -64,7 +97,7 @@ void BoxArtManager::handleBoxArtLoadComplete(NvComputer* computer, NvApp app, QU
QUrl BoxArtManager::loadBoxArtFromNetwork(NvComputer* computer, int appId)
{
NvHTTP http(computer->activeAddress);
NvHTTP http(computer->activeAddress, computer->serverCert);
QString cachePath = getFilePathForBoxArt(computer, appId);
QImage image;
@@ -81,3 +114,5 @@ QUrl BoxArtManager::loadBoxArtFromNetwork(NvComputer* computer, int appId)
return QUrl();
}
#include "boxartmanager.moc"
-33
View File
@@ -38,36 +38,3 @@ private:
QDir m_BoxArtDir;
QThreadPool m_ThreadPool;
};
class NetworkBoxArtLoadTask : public QObject, public QRunnable
{
Q_OBJECT
public:
NetworkBoxArtLoadTask(BoxArtManager* boxArtManager, NvComputer* computer, NvApp& app)
: m_Bam(boxArtManager),
m_Computer(computer),
m_App(app)
{
connect(this, SIGNAL(boxArtFetchCompleted(NvComputer*,NvApp,QUrl)),
boxArtManager, SLOT(handleBoxArtLoadComplete(NvComputer*,NvApp,QUrl)));
}
signals:
void boxArtFetchCompleted(NvComputer* computer, NvApp app, QUrl image);
private:
void run()
{
QUrl image = m_Bam->loadBoxArtFromNetwork(m_Computer, m_App.id);
if (image.isEmpty()) {
// Give it another shot if it fails once
image = m_Bam->loadBoxArtFromNetwork(m_Computer, m_App.id);
}
emit boxArtFetchCompleted(m_Computer, m_App, image);
}
BoxArtManager* m_Bam;
NvComputer* m_Computer;
NvApp m_App;
};
+410 -282
View File
@@ -2,283 +2,145 @@
#include "nvhttp.h"
#include "settings/streamingpreferences.h"
#include <Limelight.h>
#include <QtEndian>
#include <QThread>
#include <QUdpSocket>
#include <QHostInfo>
#include <QThreadPool>
#define SER_HOSTS "hosts"
#define SER_NAME "hostname"
#define SER_UUID "uuid"
#define SER_MAC "mac"
#define SER_LOCALADDR "localaddress"
#define SER_REMOTEADDR "remoteaddress"
#define SER_MANUALADDR "manualaddress"
#define SER_APPLIST "apps"
#define SER_APPNAME "name"
#define SER_APPID "id"
#define SER_APPHDR "hdr"
NvComputer::NvComputer(QSettings& settings)
class PcMonitorThread : public QThread
{
this->name = settings.value(SER_NAME).toString();
this->uuid = settings.value(SER_UUID).toString();
this->macAddress = settings.value(SER_MAC).toByteArray();
this->localAddress = settings.value(SER_LOCALADDR).toString();
this->remoteAddress = settings.value(SER_REMOTEADDR).toString();
this->manualAddress = settings.value(SER_MANUALADDR).toString();
Q_OBJECT
int appCount = settings.beginReadArray(SER_APPLIST);
for (int i = 0; i < appCount; i++) {
NvApp app;
#define TRIES_BEFORE_OFFLINING 2
#define POLLS_PER_APPLIST_FETCH 10
settings.setArrayIndex(i);
app.name = settings.value(SER_APPNAME).toString();
app.id = settings.value(SER_APPID).toInt();
app.hdrSupported = settings.value(SER_APPHDR).toBool();
this->appList.append(app);
public:
PcMonitorThread(NvComputer* computer)
: m_Computer(computer)
{
setObjectName("Polling thread for " + computer->name);
}
settings.endArray();
sortAppList();
this->activeAddress = nullptr;
this->currentGameId = 0;
this->pairState = PS_UNKNOWN;
this->state = CS_UNKNOWN;
this->gfeVersion = nullptr;
this->appVersion = nullptr;
this->maxLumaPixelsHEVC = 0;
this->serverCodecModeSupport = 0;
this->pendingQuit = false;
this->gpuModel = nullptr;
}
private:
bool tryPollComputer(QString address, bool& changed)
{
NvHTTP http(address, m_Computer->serverCert);
void
NvComputer::serialize(QSettings& settings)
{
QReadLocker lock(&this->lock);
settings.setValue(SER_NAME, name);
settings.setValue(SER_UUID, uuid);
settings.setValue(SER_MAC, macAddress);
settings.setValue(SER_LOCALADDR, localAddress);
settings.setValue(SER_REMOTEADDR, remoteAddress);
settings.setValue(SER_MANUALADDR, manualAddress);
// Avoid deleting an existing applist if we couldn't get one
if (!appList.isEmpty()) {
settings.remove(SER_APPLIST);
settings.beginWriteArray(SER_APPLIST);
for (int i = 0; i < appList.count(); i++) {
settings.setArrayIndex(i);
settings.setValue(SER_APPNAME, appList[i].name);
settings.setValue(SER_APPID, appList[i].id);
settings.setValue(SER_APPHDR, appList[i].hdrSupported);
QString serverInfo;
try {
serverInfo = http.getServerInfo(NvHTTP::NvLogLevel::NONE);
} catch (...) {
return false;
}
settings.endArray();
}
}
void NvComputer::sortAppList()
{
std::stable_sort(appList.begin(), appList.end(), [](const NvApp& app1, const NvApp& app2) {
return app1.name.toLower() < app2.name.toLower();
});
}
NvComputer newState(address, serverInfo, QSslCertificate());
NvComputer::NvComputer(QString address, QString serverInfo)
{
this->name = NvHTTP::getXmlString(serverInfo, "hostname");
if (this->name.isEmpty()) {
this->name = "UNKNOWN";
}
this->uuid = NvHTTP::getXmlString(serverInfo, "uniqueid");
QString newMacString = NvHTTP::getXmlString(serverInfo, "mac");
if (newMacString != "00:00:00:00:00:00") {
QStringList macOctets = newMacString.split(':');
for (QString macOctet : macOctets) {
this->macAddress.append((char) macOctet.toInt(nullptr, 16));
// Ensure the machine that responded is the one we intended to contact
if (m_Computer->uuid != newState.uuid) {
qInfo() << "Found unexpected PC " << newState.name << " looking for " << m_Computer->name;
return false;
}
}
QString codecSupport = NvHTTP::getXmlString(serverInfo, "ServerCodecModeSupport");
if (!codecSupport.isEmpty()) {
this->serverCodecModeSupport = codecSupport.toInt();
}
else {
this->serverCodecModeSupport = 0;
}
QString maxLumaPixelsHEVC = NvHTTP::getXmlString(serverInfo, "MaxLumaPixelsHEVC");
if (!maxLumaPixelsHEVC.isEmpty()) {
this->maxLumaPixelsHEVC = maxLumaPixelsHEVC.toInt();
}
else {
this->maxLumaPixelsHEVC = 0;
}
this->displayModes = NvHTTP::getDisplayModeList(serverInfo);
std::stable_sort(this->displayModes.begin(), this->displayModes.end(),
[](const NvDisplayMode& mode1, const NvDisplayMode& mode2) {
return mode1.width * mode1.height * mode1.refreshRate <
mode2.width * mode2.height * mode2.refreshRate;
});
this->localAddress = NvHTTP::getXmlString(serverInfo, "LocalIP");
this->remoteAddress = NvHTTP::getXmlString(serverInfo, "ExternalIP");
this->pairState = NvHTTP::getXmlString(serverInfo, "PairStatus") == "1" ?
PS_PAIRED : PS_NOT_PAIRED;
this->currentGameId = NvHTTP::getCurrentGame(serverInfo);
this->appVersion = NvHTTP::getXmlString(serverInfo, "appversion");
this->gfeVersion = NvHTTP::getXmlString(serverInfo, "GfeVersion");
this->gpuModel = NvHTTP::getXmlString(serverInfo, "gputype");
this->activeAddress = address;
this->state = NvComputer::CS_ONLINE;
this->pendingQuit = false;
}
bool NvComputer::wake()
{
if (state == NvComputer::CS_ONLINE) {
qWarning() << name << "is already online";
changed = m_Computer->update(newState);
return true;
}
if (macAddress.isEmpty()) {
qWarning() << name << "has no MAC address stored";
return false;
}
bool updateAppList(bool& changed)
{
Q_ASSERT(m_Computer->activeAddress != nullptr);
const quint16 WOL_PORTS[] = {
7, 9, // Standard WOL ports
47998, 47999, 48000, // Ports opened by GFE
};
NvHTTP http(m_Computer->activeAddress, m_Computer->serverCert);
// Create the WoL payload
QByteArray wolPayload;
wolPayload.append(QByteArray::fromHex("FFFFFFFFFFFF"));
for (int i = 0; i < 16; i++) {
wolPayload.append(macAddress);
}
Q_ASSERT(wolPayload.count() == 102);
QVector<NvApp> appList;
// Add the addresses that we know this host to be
// and broadcast addresses for this link just in
// case the host has timed out in ARP entries.
QVector<QString> addressList = uniqueAddresses();
addressList.append("255.255.255.255");
// Try all unique address strings or host names
bool success = false;
for (QString& addressString : addressList) {
QHostInfo hostInfo = QHostInfo::fromName(addressString);
if (hostInfo.error() != QHostInfo::NoError) {
qWarning() << "Error resolving" << addressString << ":" << hostInfo.errorString();
continue;
try {
appList = http.getAppList();
if (appList.isEmpty()) {
return false;
}
} catch (...) {
return false;
}
// Try all IP addresses that this string resolves to
for (QHostAddress& address : hostInfo.addresses()) {
QUdpSocket sock;
QWriteLocker lock(&m_Computer->lock);
if (m_Computer->appList != appList) {
m_Computer->appList = appList;
m_Computer->sortAppList();
changed = true;
}
// Bind to any address on the correct protocol
if (sock.bind(address.protocol() == QUdpSocket::IPv4Protocol ?
QHostAddress::AnyIPv4 : QHostAddress::AnyIPv6)) {
return true;
}
// Send to all ports
for (quint16 port : WOL_PORTS) {
if (sock.writeDatagram(wolPayload, address, port)) {
qInfo().nospace().noquote() << "Send WoL packet to " << name << " via " << address.toString() << ":" << port;
success = true;
void run() override
{
// Always fetch the applist the first time
int pollsSinceLastAppListFetch = POLLS_PER_APPLIST_FETCH;
while (!isInterruptionRequested()) {
bool stateChanged = false;
bool online = false;
bool wasOnline = m_Computer->state == NvComputer::CS_ONLINE;
for (int i = 0; i < TRIES_BEFORE_OFFLINING && !online; i++) {
for (auto& address : m_Computer->uniqueAddresses()) {
if (isInterruptionRequested()) {
return;
}
if (tryPollComputer(address, stateChanged)) {
if (!wasOnline) {
qInfo() << m_Computer->name << "is now online at" << m_Computer->activeAddress;
}
online = true;
break;
}
}
}
}
}
return success;
}
QVector<QString> NvComputer::uniqueAddresses()
{
QVector<QString> uniqueAddressList;
// Start with addresses correctly ordered
uniqueAddressList.append(activeAddress);
uniqueAddressList.append(localAddress);
uniqueAddressList.append(remoteAddress);
uniqueAddressList.append(manualAddress);
// Prune duplicates (always giving precedence to the first)
for (int i = 0; i < uniqueAddressList.count(); i++) {
if (uniqueAddressList[i].isEmpty()) {
uniqueAddressList.remove(i);
i--;
continue;
}
for (int j = i + 1; j < uniqueAddressList.count(); j++) {
if (uniqueAddressList[i] == uniqueAddressList[j]) {
// Always remove the later occurrence
uniqueAddressList.remove(j);
j--;
// Check if we failed after all retry attempts
// Note: we don't need to acquire the read lock here,
// because we're on the writing thread.
if (!online && m_Computer->state != NvComputer::CS_OFFLINE) {
qInfo() << m_Computer->name << "is now offline";
m_Computer->state = NvComputer::CS_OFFLINE;
stateChanged = true;
}
// Grab the applist if it's empty or it's been long enough that we need to refresh
pollsSinceLastAppListFetch++;
if (m_Computer->state == NvComputer::CS_ONLINE &&
m_Computer->pairState == NvComputer::PS_PAIRED &&
(m_Computer->appList.isEmpty() || pollsSinceLastAppListFetch >= POLLS_PER_APPLIST_FETCH)) {
// Notify prior to the app list poll since it may take a while, and we don't
// want to delay onlining of a machine, especially if we already have a cached list.
if (stateChanged) {
emit computerStateChanged(m_Computer);
stateChanged = false;
}
if (updateAppList(stateChanged)) {
pollsSinceLastAppListFetch = 0;
}
}
if (stateChanged) {
// Tell anyone listening that we've changed state
emit computerStateChanged(m_Computer);
}
// Wait a bit to poll again
QThread::sleep(3);
}
}
// We must have at least 1 address
Q_ASSERT(!uniqueAddressList.isEmpty());
signals:
void computerStateChanged(NvComputer* computer);
return uniqueAddressList;
}
bool NvComputer::update(NvComputer& that)
{
bool changed = false;
// Lock us for write and them for read
QWriteLocker thisLock(&this->lock);
QReadLocker thatLock(&that.lock);
// UUID may not change or we're talking to a new PC
Q_ASSERT(this->uuid == that.uuid);
#define ASSIGN_IF_CHANGED(field) \
if (this->field != that.field) { \
this->field = that.field; \
changed = true; \
}
#define ASSIGN_IF_CHANGED_AND_NONEMPTY(field) \
if (!that.field.isEmpty() && \
this->field != that.field) { \
this->field = that.field; \
changed = true; \
}
ASSIGN_IF_CHANGED(name);
ASSIGN_IF_CHANGED_AND_NONEMPTY(macAddress);
ASSIGN_IF_CHANGED_AND_NONEMPTY(localAddress);
ASSIGN_IF_CHANGED_AND_NONEMPTY(remoteAddress);
ASSIGN_IF_CHANGED_AND_NONEMPTY(manualAddress);
ASSIGN_IF_CHANGED(pairState);
ASSIGN_IF_CHANGED(serverCodecModeSupport);
ASSIGN_IF_CHANGED(currentGameId);
ASSIGN_IF_CHANGED(activeAddress);
ASSIGN_IF_CHANGED(state);
ASSIGN_IF_CHANGED(gfeVersion);
ASSIGN_IF_CHANGED(appVersion);
ASSIGN_IF_CHANGED(maxLumaPixelsHEVC);
ASSIGN_IF_CHANGED(gpuModel);
ASSIGN_IF_CHANGED_AND_NONEMPTY(appList);
ASSIGN_IF_CHANGED_AND_NONEMPTY(displayModes);
return changed;
}
private:
NvComputer* m_Computer;
};
ComputerManager::ComputerManager(QObject *parent)
: QObject(parent),
@@ -356,7 +218,7 @@ void ComputerManager::saveHosts()
settings.beginWriteArray(SER_HOSTS);
for (int i = 0; i < m_KnownHosts.count(); i++) {
settings.setArrayIndex(i);
m_KnownHosts[m_KnownHosts.keys()[i]]->serialize(settings);
m_KnownHosts.value(m_KnownHosts.keys()[i])->serialize(settings);
}
settings.endArray();
}
@@ -396,6 +258,25 @@ void ComputerManager::startPolling()
}
}
// Must hold m_Lock for write
void ComputerManager::startPollingComputer(NvComputer* computer)
{
if (m_PollingRef == 0) {
return;
}
if (m_PollThreads.contains(computer->uuid)) {
Q_ASSERT(m_PollThreads.value(computer->uuid)->isRunning());
return;
}
PcMonitorThread* thread = new PcMonitorThread(computer);
connect(thread, SIGNAL(computerStateChanged(NvComputer*)),
this, SLOT(handleComputerStateChanged(NvComputer*)));
m_PollThreads[computer->uuid] = thread;
thread->start();
}
void ComputerManager::handleMdnsServiceResolved(MdnsPendingComputer* computer,
const QHostAddress& address)
{
@@ -407,6 +288,19 @@ void ComputerManager::handleMdnsServiceResolved(MdnsPendingComputer* computer,
computer->deleteLater();
}
void ComputerManager::handleComputerStateChanged(NvComputer* computer)
{
emit computerStateChanged(computer);
if (computer->pendingQuit && computer->currentGameId == 0) {
computer->pendingQuit = false;
emit quitAppCompleted(QVariant());
}
// Save updated hosts to QSettings
saveHosts();
}
QVector<NvComputer*> ComputerManager::getComputers()
{
QReadLocker lock(&m_Lock);
@@ -423,9 +317,22 @@ public:
void run()
{
QWriteLocker lock(&m_ComputerManager->m_Lock);
QThread* pollingThread;
QThread* pollingThread = m_ComputerManager->m_PollThreads[m_Computer->uuid];
// Only do the minimum amount of work while holding the writer lock.
// We must release it before calling saveHosts().
{
QWriteLocker lock(&m_ComputerManager->m_Lock);
pollingThread = m_ComputerManager->m_PollThreads.take(m_Computer->uuid);
m_ComputerManager->m_KnownHosts.remove(m_Computer->uuid);
}
// Persist the new host list
m_ComputerManager->saveHosts();
// Delete the polling thread
if (pollingThread != nullptr) {
pollingThread->requestInterruption();
@@ -438,9 +345,8 @@ public:
delete pollingThread;
}
m_ComputerManager->m_PollThreads.remove(m_Computer->uuid);
m_ComputerManager->m_KnownHosts.remove(m_Computer->uuid);
// Finally, delete the computer itself. This must be done
// last because the polling thread might be using it.
delete m_Computer;
}
@@ -456,6 +362,55 @@ void ComputerManager::deleteHost(NvComputer* computer)
QThreadPool::globalInstance()->start(new DeferredHostDeletionTask(this, computer));
}
class PendingPairingTask : public QObject, public QRunnable
{
Q_OBJECT
public:
PendingPairingTask(ComputerManager* computerManager, NvComputer* computer, QString pin)
: m_Computer(computer),
m_Pin(pin)
{
connect(this, &PendingPairingTask::pairingCompleted,
computerManager, &ComputerManager::pairingCompleted);
}
signals:
void pairingCompleted(NvComputer* computer, QString error);
private:
void run()
{
NvPairingManager pairingManager(m_Computer->activeAddress);
try {
NvPairingManager::PairState result = pairingManager.pair(m_Computer->appVersion, m_Pin, m_Computer->serverCert);
switch (result)
{
case NvPairingManager::PairState::PIN_WRONG:
emit pairingCompleted(m_Computer, "The PIN from the PC didn't match. Please try again.");
break;
case NvPairingManager::PairState::FAILED:
emit pairingCompleted(m_Computer, "Pairing failed. Please try again.");
break;
case NvPairingManager::PairState::ALREADY_IN_PROGRESS:
emit pairingCompleted(m_Computer, "Another pairing attempt is already in progress.");
break;
case NvPairingManager::PairState::PAIRED:
emit pairingCompleted(m_Computer, nullptr);
break;
}
} catch (const GfeHttpResponseException& e) {
emit pairingCompleted(m_Computer, e.toQString());
} catch (const QtNetworkReplyException& e) {
emit pairingCompleted(m_Computer, e.toQString());
}
}
NvComputer* m_Computer;
QString m_Pin;
};
void ComputerManager::pairHost(NvComputer* computer, QString pin)
{
// Punt to a worker thread to avoid stalling the
@@ -464,6 +419,55 @@ void ComputerManager::pairHost(NvComputer* computer, QString pin)
QThreadPool::globalInstance()->start(pairing);
}
class PendingQuitTask : public QObject, public QRunnable
{
Q_OBJECT
public:
PendingQuitTask(ComputerManager* computerManager, NvComputer* computer)
: m_Computer(computer)
{
connect(this, &PendingQuitTask::quitAppFailed,
computerManager, &ComputerManager::quitAppCompleted);
}
signals:
void quitAppFailed(QString error);
private:
void run()
{
NvHTTP http(m_Computer->activeAddress, m_Computer->serverCert);
try {
if (m_Computer->currentGameId != 0) {
http.quitApp();
}
} catch (const GfeHttpResponseException& e) {
{
QWriteLocker lock(&m_Computer->lock);
m_Computer->pendingQuit = false;
}
if (e.getStatusCode() == 599) {
// 599 is a special code we make a custom message for
emit quitAppFailed("The running game wasn't started by this PC. "
"You must quit the game on the host PC manually or use the device that originally started the game.");
}
else {
emit quitAppFailed(e.toQString());
}
} catch (const QtNetworkReplyException& e) {
{
QWriteLocker lock(&m_Computer->lock);
m_Computer->pendingQuit = false;
}
emit quitAppFailed(e.toQString());
}
}
NvComputer* m_Computer;
};
void ComputerManager::quitRunningApp(NvComputer* computer)
{
QWriteLocker lock(&computer->lock);
@@ -511,6 +515,163 @@ void ComputerManager::stopPollingAsync()
}
}
class PendingAddTask : public QObject, public QRunnable
{
Q_OBJECT
public:
PendingAddTask(ComputerManager* computerManager, QString address, bool mdns)
: m_ComputerManager(computerManager),
m_Address(address),
m_Mdns(mdns)
{
connect(this, &PendingAddTask::computerAddCompleted,
computerManager, &ComputerManager::computerAddCompleted);
connect(this, &PendingAddTask::computerStateChanged,
computerManager, &ComputerManager::handleComputerStateChanged);
}
signals:
void computerAddCompleted(QVariant success);
void computerStateChanged(NvComputer* computer);
private:
QString fetchServerInfo(NvHTTP& http)
{
QString serverInfo;
try {
// There's a race condition between GameStream servers reporting presence over
// mDNS and the HTTPS server being ready to respond to our queries. To work
// around this issue, we will issue the request again after a few seconds if
// we see a ServiceUnavailableError error.
try {
serverInfo = http.getServerInfo(NvHTTP::NvLogLevel::VERBOSE);
} catch (const QtNetworkReplyException& e) {
if (e.getError() == QNetworkReply::ServiceUnavailableError) {
qWarning() << "Retrying request in 5 seconds after ServiceUnavailableError";
QThread::sleep(5);
serverInfo = http.getServerInfo(NvHTTP::NvLogLevel::VERBOSE);
qInfo() << "Retry successful";
}
else {
// Rethrow other errors
throw e;
}
}
return serverInfo;
} catch (...) {
if (!m_Mdns) {
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;
}
// 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
if (m_Mdns) {
newComputer->localAddress = m_Address;
// Get the WAN IP address using STUN if we're on mDNS
quint32 addr;
int err = LiFindExternalAddressIP4("stun.moonlight-stream.org", 3478, &addr);
if (err == 0) {
newComputer->remoteAddress = QHostAddress(qFromBigEndian(addr)).toString();
}
else {
qWarning() << "STUN failed to get WAN address:" << err;
}
}
else {
newComputer->manualAddress = m_Address;
}
{
// Check if this PC already exists
QWriteLocker lock(&m_ComputerManager->m_Lock);
NvComputer* existingComputer = m_ComputerManager->m_KnownHosts.value(newComputer->uuid);
if (existingComputer != nullptr) {
// Fold it into the existing PC
bool changed = existingComputer->update(*newComputer);
delete 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 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;
// 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;
QString m_Address;
bool m_Mdns;
};
void ComputerManager::addNewHost(QString address, bool mdns)
{
// Punt to a worker thread to avoid stalling the
@@ -519,37 +680,4 @@ void ComputerManager::addNewHost(QString address, bool mdns)
QThreadPool::globalInstance()->start(addTask);
}
void
ComputerManager::handleComputerStateChanged(NvComputer* computer)
{
emit computerStateChanged(computer);
if (computer->pendingQuit && computer->currentGameId == 0) {
computer->pendingQuit = false;
emit quitAppCompleted(QVariant());
}
// Save updated hosts to QSettings
saveHosts();
}
// Must hold m_Lock for write
void
ComputerManager::startPollingComputer(NvComputer* computer)
{
if (m_PollingRef == 0) {
return;
}
if (m_PollThreads.contains(computer->uuid)) {
Q_ASSERT(m_PollThreads[computer->uuid]->isRunning());
return;
}
PcMonitorThread* thread = new PcMonitorThread(computer);
connect(thread, SIGNAL(computerStateChanged(NvComputer*)),
this, SLOT(handleComputerStateChanged(NvComputer*)));
m_PollThreads[computer->uuid] = thread;
thread->start();
}
#include "computermanager.moc"
+2 -387
View File
@@ -1,5 +1,6 @@
#pragma once
#include "nvhttp.h"
#include "nvcomputer.h"
#include "nvpairingmanager.h"
#include <qmdnsengine/server.h>
@@ -13,207 +14,6 @@
#include <QSettings>
#include <QRunnable>
class NvComputer
{
friend class PcMonitorThread;
friend class ComputerManager;
friend class PendingQuitTask;
private:
void sortAppList();
bool pendingQuit;
public:
explicit NvComputer(QString address, QString serverInfo);
explicit NvComputer(QSettings& settings);
bool
update(NvComputer& that);
bool
wake();
QVector<QString>
uniqueAddresses();
void
serialize(QSettings& settings);
enum PairState
{
PS_UNKNOWN,
PS_PAIRED,
PS_NOT_PAIRED
};
enum ComputerState
{
CS_UNKNOWN,
CS_ONLINE,
CS_OFFLINE
};
// Ephemeral traits
ComputerState state;
PairState pairState;
QString activeAddress;
int currentGameId;
QString gfeVersion;
QString appVersion;
QVector<NvDisplayMode> displayModes;
int maxLumaPixelsHEVC;
int serverCodecModeSupport;
QString gpuModel;
// Persisted traits
QString localAddress;
QString remoteAddress;
QString manualAddress;
QByteArray macAddress;
QString name;
QString uuid;
QVector<NvApp> appList;
// Synchronization
QReadWriteLock lock;
};
// FIXME: MOC isn't finding Q_OBJECT properly when this is confined
// to computermanager.cpp as it should be.
class PcMonitorThread : public QThread
{
Q_OBJECT
#define TRIES_BEFORE_OFFLINING 2
#define POLLS_PER_APPLIST_FETCH 10
public:
PcMonitorThread(NvComputer* computer)
: m_Computer(computer)
{
setObjectName("Polling thread for " + computer->name);
}
private:
bool tryPollComputer(QString address, bool& changed)
{
NvHTTP http(address);
QString serverInfo;
try {
serverInfo = http.getServerInfo(NvHTTP::NvLogLevel::NONE);
} catch (...) {
return false;
}
NvComputer newState(address, serverInfo);
// Ensure the machine that responded is the one we intended to contact
if (m_Computer->uuid != newState.uuid) {
qInfo() << "Found unexpected PC " << newState.name << " looking for " << m_Computer->name;
return false;
}
changed = m_Computer->update(newState);
return true;
}
bool updateAppList(bool& changed)
{
Q_ASSERT(m_Computer->activeAddress != nullptr);
NvHTTP http(m_Computer->activeAddress);
QVector<NvApp> appList;
try {
appList = http.getAppList();
if (appList.isEmpty()) {
return false;
}
} catch (...) {
return false;
}
QWriteLocker lock(&m_Computer->lock);
if (m_Computer->appList != appList) {
m_Computer->appList = appList;
m_Computer->sortAppList();
changed = true;
}
return true;
}
void run() override
{
// Always fetch the applist the first time
int pollsSinceLastAppListFetch = POLLS_PER_APPLIST_FETCH;
while (!isInterruptionRequested()) {
bool stateChanged = false;
bool online = false;
bool wasOnline = m_Computer->state == NvComputer::CS_ONLINE;
for (int i = 0; i < TRIES_BEFORE_OFFLINING && !online; i++) {
for (auto& address : m_Computer->uniqueAddresses()) {
if (isInterruptionRequested()) {
return;
}
if (tryPollComputer(address, stateChanged)) {
if (!wasOnline) {
qInfo() << m_Computer->name << "is now online at" << m_Computer->activeAddress;
}
online = true;
break;
}
}
}
// Check if we failed after all retry attempts
// Note: we don't need to acquire the read lock here,
// because we're on the writing thread.
if (!online && m_Computer->state != NvComputer::CS_OFFLINE) {
qInfo() << m_Computer->name << "is now offline";
m_Computer->state = NvComputer::CS_OFFLINE;
stateChanged = true;
}
// Grab the applist if it's empty or it's been long enough that we need to refresh
pollsSinceLastAppListFetch++;
if (m_Computer->state == NvComputer::CS_ONLINE &&
m_Computer->pairState == NvComputer::PS_PAIRED &&
(m_Computer->appList.isEmpty() || pollsSinceLastAppListFetch >= POLLS_PER_APPLIST_FETCH)) {
// Notify prior to the app list poll since it may take a while, and we don't
// want to delay onlining of a machine, especially if we already have a cached list.
if (stateChanged) {
emit computerStateChanged(m_Computer);
stateChanged = false;
}
if (updateAppList(stateChanged)) {
pollsSinceLastAppListFetch = 0;
}
}
if (stateChanged) {
// Tell anyone listening that we've changed state
emit computerStateChanged(m_Computer);
}
// Wait a bit to poll again
QThread::sleep(3);
}
}
signals:
void computerStateChanged(NvComputer* computer);
private:
NvComputer* m_Computer;
};
class MdnsPendingComputer : public QObject
{
Q_OBJECT
@@ -306,188 +106,3 @@ private:
QMdnsEngine::Cache m_MdnsCache;
QVector<MdnsPendingComputer*> m_PendingResolution;
};
class PendingPairingTask : public QObject, public QRunnable
{
Q_OBJECT
public:
PendingPairingTask(ComputerManager* computerManager, NvComputer* computer, QString pin)
: m_Computer(computer),
m_Pin(pin)
{
connect(this, &PendingPairingTask::pairingCompleted,
computerManager, &ComputerManager::pairingCompleted);
}
signals:
void pairingCompleted(NvComputer* computer, QString error);
private:
void run()
{
NvPairingManager pairingManager(m_Computer->activeAddress);
try {
NvPairingManager::PairState result = pairingManager.pair(m_Computer->appVersion, m_Pin);
switch (result)
{
case NvPairingManager::PairState::PIN_WRONG:
emit pairingCompleted(m_Computer, "The PIN from the PC didn't match. Please try again.");
break;
case NvPairingManager::PairState::FAILED:
emit pairingCompleted(m_Computer, "Pairing failed. Please try again.");
break;
case NvPairingManager::PairState::ALREADY_IN_PROGRESS:
emit pairingCompleted(m_Computer, "Another pairing attempt is already in progress.");
break;
case NvPairingManager::PairState::PAIRED:
emit pairingCompleted(m_Computer, nullptr);
break;
}
} catch (const GfeHttpResponseException& e) {
emit pairingCompleted(m_Computer, e.toQString());
}
}
NvComputer* m_Computer;
QString m_Pin;
};
class PendingQuitTask : public QObject, public QRunnable
{
Q_OBJECT
public:
PendingQuitTask(ComputerManager* computerManager, NvComputer* computer)
: m_Computer(computer)
{
connect(this, &PendingQuitTask::quitAppFailed,
computerManager, &ComputerManager::quitAppCompleted);
}
signals:
void quitAppFailed(QString error);
private:
void run()
{
NvHTTP http(m_Computer->activeAddress);
try {
if (m_Computer->currentGameId != 0) {
http.quitApp();
}
} catch (const GfeHttpResponseException& e) {
{
QWriteLocker lock(&m_Computer->lock);
m_Computer->pendingQuit = false;
}
if (e.getStatusCode() == 599) {
// 599 is a special code we make a custom message for
emit quitAppFailed("The running game wasn't started by this PC. "
"You must quit the game on the host PC manually or use the device that originally started the game.");
}
else {
emit quitAppFailed(e.toQString());
}
}
}
NvComputer* m_Computer;
};
class PendingAddTask : public QObject, public QRunnable
{
Q_OBJECT
public:
PendingAddTask(ComputerManager* computerManager, QString address, bool mdns)
: m_ComputerManager(computerManager),
m_Address(address),
m_Mdns(mdns)
{
connect(this, &PendingAddTask::computerAddCompleted,
computerManager, &ComputerManager::computerAddCompleted);
connect(this, &PendingAddTask::computerStateChanged,
computerManager, &ComputerManager::handleComputerStateChanged);
}
signals:
void computerAddCompleted(QVariant success);
void computerStateChanged(NvComputer* computer);
private:
void run()
{
NvHTTP http(m_Address);
qInfo() << "Processing new PC at" << m_Address << "from" << (m_Mdns ? "mDNS" : "user");
QString serverInfo;
try {
serverInfo = http.getServerInfo(NvHTTP::NvLogLevel::VERBOSE);
} catch (...) {
if (!m_Mdns) {
emit computerAddCompleted(false);
}
return;
}
NvComputer* newComputer = new NvComputer(m_Address, serverInfo);
// Update addresses depending on the context
if (m_Mdns) {
newComputer->localAddress = m_Address;
}
else {
newComputer->manualAddress = m_Address;
}
// Check if this PC already exists
QWriteLocker lock(&m_ComputerManager->m_Lock);
NvComputer* existingComputer = m_ComputerManager->m_KnownHosts[newComputer->uuid];
if (existingComputer != nullptr) {
// Fold it into the existing PC
bool changed = existingComputer->update(*newComputer);
delete 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 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;
// 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;
QString m_Address;
bool m_Mdns;
};
+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;
};
+313
View File
@@ -0,0 +1,313 @@
#include "nvcomputer.h"
#include <QUdpSocket>
#include <QHostInfo>
#include <QNetworkInterface>
#define SER_NAME "hostname"
#define SER_UUID "uuid"
#define SER_MAC "mac"
#define SER_LOCALADDR "localaddress"
#define SER_REMOTEADDR "remoteaddress"
#define SER_MANUALADDR "manualaddress"
#define SER_APPLIST "apps"
#define SER_SRVCERT "srvcert"
#define SER_APPNAME "name"
#define SER_APPID "id"
#define SER_APPHDR "hdr"
NvComputer::NvComputer(QSettings& settings)
{
this->name = settings.value(SER_NAME).toString();
this->uuid = settings.value(SER_UUID).toString();
this->macAddress = settings.value(SER_MAC).toByteArray();
this->localAddress = settings.value(SER_LOCALADDR).toString();
this->remoteAddress = settings.value(SER_REMOTEADDR).toString();
this->manualAddress = settings.value(SER_MANUALADDR).toString();
this->serverCert = QSslCertificate(settings.value(SER_SRVCERT).toByteArray());
int appCount = settings.beginReadArray(SER_APPLIST);
for (int i = 0; i < appCount; i++) {
NvApp app;
settings.setArrayIndex(i);
app.name = settings.value(SER_APPNAME).toString();
app.id = settings.value(SER_APPID).toInt();
app.hdrSupported = settings.value(SER_APPHDR).toBool();
this->appList.append(app);
}
settings.endArray();
sortAppList();
this->activeAddress = nullptr;
this->currentGameId = 0;
this->pairState = PS_UNKNOWN;
this->state = CS_UNKNOWN;
this->gfeVersion = nullptr;
this->appVersion = nullptr;
this->maxLumaPixelsHEVC = 0;
this->serverCodecModeSupport = 0;
this->pendingQuit = false;
this->gpuModel = nullptr;
}
void NvComputer::serialize(QSettings& settings)
{
QReadLocker lock(&this->lock);
settings.setValue(SER_NAME, name);
settings.setValue(SER_UUID, uuid);
settings.setValue(SER_MAC, macAddress);
settings.setValue(SER_LOCALADDR, localAddress);
settings.setValue(SER_REMOTEADDR, remoteAddress);
settings.setValue(SER_MANUALADDR, manualAddress);
settings.setValue(SER_SRVCERT, serverCert.toPem());
// Avoid deleting an existing applist if we couldn't get one
if (!appList.isEmpty()) {
settings.remove(SER_APPLIST);
settings.beginWriteArray(SER_APPLIST);
for (int i = 0; i < appList.count(); i++) {
settings.setArrayIndex(i);
settings.setValue(SER_APPNAME, appList[i].name);
settings.setValue(SER_APPID, appList[i].id);
settings.setValue(SER_APPHDR, appList[i].hdrSupported);
}
settings.endArray();
}
}
void NvComputer::sortAppList()
{
std::stable_sort(appList.begin(), appList.end(), [](const NvApp& app1, const NvApp& app2) {
return app1.name.toLower() < app2.name.toLower();
});
}
NvComputer::NvComputer(QString address, QString serverInfo, QSslCertificate serverCert)
{
this->serverCert = serverCert;
this->name = NvHTTP::getXmlString(serverInfo, "hostname");
if (this->name.isEmpty()) {
this->name = "UNKNOWN";
}
this->uuid = NvHTTP::getXmlString(serverInfo, "uniqueid");
QString newMacString = NvHTTP::getXmlString(serverInfo, "mac");
if (newMacString != "00:00:00:00:00:00") {
QStringList macOctets = newMacString.split(':');
for (QString macOctet : macOctets) {
this->macAddress.append((char) macOctet.toInt(nullptr, 16));
}
}
QString codecSupport = NvHTTP::getXmlString(serverInfo, "ServerCodecModeSupport");
if (!codecSupport.isEmpty()) {
this->serverCodecModeSupport = codecSupport.toInt();
}
else {
this->serverCodecModeSupport = 0;
}
QString maxLumaPixelsHEVC = NvHTTP::getXmlString(serverInfo, "MaxLumaPixelsHEVC");
if (!maxLumaPixelsHEVC.isEmpty()) {
this->maxLumaPixelsHEVC = maxLumaPixelsHEVC.toInt();
}
else {
this->maxLumaPixelsHEVC = 0;
}
this->displayModes = NvHTTP::getDisplayModeList(serverInfo);
std::stable_sort(this->displayModes.begin(), this->displayModes.end(),
[](const NvDisplayMode& mode1, const NvDisplayMode& mode2) {
return mode1.width * mode1.height * mode1.refreshRate <
mode2.width * mode2.height * mode2.refreshRate;
});
this->localAddress = NvHTTP::getXmlString(serverInfo, "LocalIP");
this->remoteAddress = NvHTTP::getXmlString(serverInfo, "ExternalIP");
this->pairState = NvHTTP::getXmlString(serverInfo, "PairStatus") == "1" ?
PS_PAIRED : PS_NOT_PAIRED;
this->currentGameId = NvHTTP::getCurrentGame(serverInfo);
this->appVersion = NvHTTP::getXmlString(serverInfo, "appversion");
this->gfeVersion = NvHTTP::getXmlString(serverInfo, "GfeVersion");
this->gpuModel = NvHTTP::getXmlString(serverInfo, "gputype");
this->activeAddress = address;
this->state = NvComputer::CS_ONLINE;
this->pendingQuit = false;
}
bool NvComputer::wake()
{
if (state == NvComputer::CS_ONLINE) {
qWarning() << name << "is already online";
return true;
}
if (macAddress.isEmpty()) {
qWarning() << name << "has no MAC address stored";
return false;
}
const quint16 WOL_PORTS[] = {
7, 9, // Standard WOL ports
47998, 47999, 48000, // Ports opened by GFE
};
// Create the WoL payload
QByteArray wolPayload;
wolPayload.append(QByteArray::fromHex("FFFFFFFFFFFF"));
for (int i = 0; i < 16; i++) {
wolPayload.append(macAddress);
}
Q_ASSERT(wolPayload.count() == 102);
// Add the addresses that we know this host to be
// and broadcast addresses for this link just in
// case the host has timed out in ARP entries.
QVector<QString> addressList = uniqueAddresses();
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
bool success = false;
for (QString& addressString : addressList) {
QHostInfo hostInfo = QHostInfo::fromName(addressString);
if (hostInfo.error() != QHostInfo::NoError) {
qWarning() << "Error resolving" << addressString << ":" << hostInfo.errorString();
continue;
}
// Try all IP addresses that this string resolves to
for (QHostAddress& address : hostInfo.addresses()) {
QUdpSocket sock;
// Send to all ports
for (quint16 port : WOL_PORTS) {
if (sock.writeDatagram(wolPayload, address, port)) {
qInfo().nospace().noquote() << "Sent WoL packet to " << name << " via " << address.toString() << ":" << port;
success = true;
}
else {
qWarning() << "Send failed:" << sock.error();
}
}
}
}
return success;
}
QVector<QString> NvComputer::uniqueAddresses()
{
QVector<QString> uniqueAddressList;
// Start with addresses correctly ordered
uniqueAddressList.append(activeAddress);
uniqueAddressList.append(localAddress);
uniqueAddressList.append(remoteAddress);
uniqueAddressList.append(manualAddress);
// Prune duplicates (always giving precedence to the first)
for (int i = 0; i < uniqueAddressList.count(); i++) {
if (uniqueAddressList[i].isEmpty()) {
uniqueAddressList.remove(i);
i--;
continue;
}
for (int j = i + 1; j < uniqueAddressList.count(); j++) {
if (uniqueAddressList[i] == uniqueAddressList[j]) {
// Always remove the later occurrence
uniqueAddressList.remove(j);
j--;
}
}
}
// We must have at least 1 address
Q_ASSERT(!uniqueAddressList.isEmpty());
return uniqueAddressList;
}
bool NvComputer::update(NvComputer& that)
{
bool changed = false;
// Lock us for write and them for read
QWriteLocker thisLock(&this->lock);
QReadLocker thatLock(&that.lock);
// UUID may not change or we're talking to a new PC
Q_ASSERT(this->uuid == that.uuid);
#define ASSIGN_IF_CHANGED(field) \
if (this->field != that.field) { \
this->field = that.field; \
changed = true; \
}
#define ASSIGN_IF_CHANGED_AND_NONEMPTY(field) \
if (!that.field.isEmpty() && \
this->field != that.field) { \
this->field = that.field; \
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_AND_NONEMPTY(macAddress);
ASSIGN_IF_CHANGED_AND_NONEMPTY(localAddress);
ASSIGN_IF_CHANGED_AND_NONEMPTY(remoteAddress);
ASSIGN_IF_CHANGED_AND_NONEMPTY(manualAddress);
ASSIGN_IF_CHANGED(pairState);
ASSIGN_IF_CHANGED(serverCodecModeSupport);
ASSIGN_IF_CHANGED(currentGameId);
ASSIGN_IF_CHANGED(activeAddress);
ASSIGN_IF_CHANGED(state);
ASSIGN_IF_CHANGED(gfeVersion);
ASSIGN_IF_CHANGED(appVersion);
ASSIGN_IF_CHANGED(maxLumaPixelsHEVC);
ASSIGN_IF_CHANGED(gpuModel);
ASSIGN_IF_CHANGED_AND_NONNULL(serverCert);
ASSIGN_IF_CHANGED_AND_NONEMPTY(appList);
ASSIGN_IF_CHANGED_AND_NONEMPTY(displayModes);
return changed;
}
+76
View File
@@ -0,0 +1,76 @@
#pragma once
#include "nvhttp.h"
#include <QThread>
#include <QReadWriteLock>
#include <QSettings>
#include <QRunnable>
class NvComputer
{
friend class PcMonitorThread;
friend class ComputerManager;
friend class PendingQuitTask;
private:
void sortAppList();
bool pendingQuit;
public:
explicit NvComputer(QString address, QString serverInfo, QSslCertificate serverCert);
explicit NvComputer(QSettings& settings);
bool
update(NvComputer& that);
bool
wake();
QVector<QString>
uniqueAddresses();
void
serialize(QSettings& settings);
enum PairState
{
PS_UNKNOWN,
PS_PAIRED,
PS_NOT_PAIRED
};
enum ComputerState
{
CS_UNKNOWN,
CS_ONLINE,
CS_OFFLINE
};
// Ephemeral traits
ComputerState state;
PairState pairState;
QString activeAddress;
int currentGameId;
QString gfeVersion;
QString appVersion;
QVector<NvDisplayMode> displayModes;
int maxLumaPixelsHEVC;
int serverCodecModeSupport;
QString gpuModel;
// Persisted traits
QString localAddress;
QString remoteAddress;
QString manualAddress;
QByteArray macAddress;
QString name;
QString uuid;
QSslCertificate serverCert;
QVector<NvApp> appList;
// Synchronization
QReadWriteLock lock;
};
+100 -40
View File
@@ -10,11 +10,16 @@
#include <QSslKey>
#include <QImageReader>
#include <QtEndian>
#include <QNetworkProxy>
#define REQUEST_TIMEOUT_MS 5000
#define LAUNCH_TIMEOUT_MS 120000
#define RESUME_TIMEOUT_MS 30000
#define QUIT_TIMEOUT_MS 30000
NvHTTP::NvHTTP(QString address) :
m_Address(address)
NvHTTP::NvHTTP(QString address, QSslCertificate serverCert) :
m_Address(address),
m_ServerCert(serverCert)
{
Q_ASSERT(!address.isEmpty());
@@ -24,6 +29,15 @@ NvHTTP::NvHTTP(QString address) :
m_BaseUrlHttps.setHost(address);
m_BaseUrlHttp.setPort(47989);
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>
@@ -68,36 +82,50 @@ NvHTTP::getServerInfo(NvLogLevel logLevel)
{
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
// 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)
try
{
// Certificate validation error, fallback to HTTP
serverInfo = openConnectionToString(m_BaseUrlHttp,
// Always try HTTPS first, since it properly reports
// pairing status (and a few other attributes).
serverInfo = openConnectionToString(m_BaseUrlHttps,
"serverinfo",
nullptr,
true,
REQUEST_TIMEOUT_MS,
logLevel);
// Throws if the request failed
verifyResponseStatus(serverInfo);
}
else
catch (const GfeHttpResponseException& e)
{
// Rethrow real errors
throw e;
if (e.getStatusCode() == 401)
{
// 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;
}
@@ -155,7 +183,7 @@ NvHTTP::launchApp(int appId,
"&surroundAudioInfo="+getSurroundAudioInfoString(streamConfig->audioConfiguration)+
"&remoteControllersBitmap="+QString::number(gamepadMask)+
"&gcmap="+QString::number(gamepadMask),
false);
LAUNCH_TIMEOUT_MS);
// Throws if the request failed
verifyResponseStatus(response);
@@ -175,7 +203,7 @@ NvHTTP::resumeApp(PSTREAM_CONFIGURATION streamConfig)
"rikey="+QString(QByteArray(streamConfig->remoteInputAesKey, sizeof(streamConfig->remoteInputAesKey)).toHex())+
"&rikeyid="+QString::number(riKeyId)+
"&surroundAudioInfo="+getSurroundAudioInfoString(streamConfig->audioConfiguration),
false);
RESUME_TIMEOUT_MS);
// Throws if the request failed
verifyResponseStatus(response);
@@ -188,7 +216,7 @@ NvHTTP::quitApp()
openConnectionToString(m_BaseUrlHttps,
"cancel",
nullptr,
false);
QUIT_TIMEOUT_MS);
// Throws if the request failed
verifyResponseStatus(response);
@@ -235,7 +263,7 @@ NvHTTP::getAppList()
QString appxml = openConnectionToString(m_BaseUrlHttps,
"applist",
nullptr,
true,
REQUEST_TIMEOUT_MS,
NvLogLevel::ERROR);
verifyResponseStatus(appxml);
@@ -303,7 +331,7 @@ NvHTTP::getBoxArt(int appId)
"appasset",
"appid="+QString::number(appId)+
"&AssetType=2&AssetIdx=0",
true,
REQUEST_TIMEOUT_MS,
NvLogLevel::VERBOSE);
QImage image = QImageReader(reply).read();
delete reply;
@@ -350,10 +378,10 @@ QString
NvHTTP::openConnectionToString(QUrl baseUrl,
QString command,
QString arguments,
bool enableTimeout,
int timeoutMs,
NvLogLevel logLevel)
{
QNetworkReply* reply = openConnection(baseUrl, command, arguments, enableTimeout, logLevel);
QNetworkReply* reply = openConnection(baseUrl, command, arguments, timeoutMs, logLevel);
QString ret;
QTextStream stream(reply);
@@ -368,14 +396,18 @@ QNetworkReply*
NvHTTP::openConnection(QUrl baseUrl,
QString command,
QString arguments,
bool enableTimeout,
int timeoutMs,
NvLogLevel logLevel)
{
// Build a URL for the request
QUrl url(baseUrl);
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) : ""));
QNetworkRequest request = QNetworkRequest(url);
@@ -385,15 +417,28 @@ NvHTTP::openConnection(QUrl baseUrl,
QNetworkReply* reply = m_Nam.get(request);
// Ignore self-signed certificate errors (since GFE uses them)
reply->ignoreSslErrors();
// Assert that we always have a server cert for HTTPS, since
// the request will fail anyway if we do not.
Q_ASSERT(!m_ServerCert.isNull() || baseUrl == m_BaseUrlHttp);
if (!m_ServerCert.isNull()) {
// Pin the server certificate received during pairing
QList<QSslError> expectedSslErrors;
expectedSslErrors.append(QSslError(QSslError::HostNameMismatch, m_ServerCert));
expectedSslErrors.append(QSslError(QSslError::SelfSignedCertificate, m_ServerCert));
// The SecureTransport backend for Qt TLS on macOS throws CertificateUntrusted
// instead of SelfSignedCertificate, so we will need to allow that error too.
expectedSslErrors.append(QSslError(QSslError::CertificateUntrusted, m_ServerCert));
reply->ignoreSslErrors(expectedSslErrors);
}
// Run the request with a timeout if requested
QEventLoop loop;
QObject::connect(reply, SIGNAL(finished()), &loop, SLOT(quit()));
if (enableTimeout)
{
QTimer::singleShot(REQUEST_TIMEOUT_MS, &loop, SLOT(quit()));
if (timeoutMs) {
QTimer::singleShot(timeoutMs, &loop, SLOT(quit()));
}
if (logLevel >= NvLogLevel::VERBOSE) {
qInfo() << "Executing request:" << url.toString();
@@ -419,9 +464,24 @@ NvHTTP::openConnection(QUrl baseUrl,
if (logLevel >= NvLogLevel::ERROR) {
qWarning() << command << " request failed with error " << reply->error();
}
GfeHttpResponseException exception(reply->error(), reply->errorString());
delete reply;
throw exception;
if (reply->error() == QNetworkReply::SslHandshakeFailedError) {
// 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;
+43 -4
View File
@@ -5,7 +5,8 @@
#include <Limelight.h>
#include <QUrl>
#include <QtNetwork/QNetworkAccessManager>
#include <QNetworkAccessManager>
#include <QNetworkReply>
class NvApp
{
@@ -77,6 +78,41 @@ private:
QString m_StatusMessage;
};
class QtNetworkReplyException : public std::exception
{
public:
QtNetworkReplyException(QNetworkReply::NetworkError error, QString errorText) :
m_Error(error),
m_ErrorText(errorText)
{
}
const char* what() const throw()
{
return m_ErrorText.toLatin1();
}
const char* getErrorText() const
{
return m_ErrorText.toLatin1();
}
QNetworkReply::NetworkError getError() const
{
return m_Error;
}
QString toQString() const
{
return m_ErrorText + " (Error " + QString::number(m_Error) + ")";
}
private:
QNetworkReply::NetworkError m_Error;
QString m_ErrorText;
};
class NvHTTP
{
public:
@@ -86,7 +122,7 @@ public:
VERBOSE
};
explicit NvHTTP(QString address);
explicit NvHTTP(QString address, QSslCertificate serverCert);
static
int
@@ -113,9 +149,11 @@ public:
openConnectionToString(QUrl baseUrl,
QString command,
QString arguments,
bool enableTimeout,
int timeoutMs,
NvLogLevel logLevel = NvLogLevel::VERBOSE);
void setServerCert(QSslCertificate serverCert);
static
QVector<int>
parseQuad(QString quad);
@@ -150,9 +188,10 @@ private:
openConnection(QUrl baseUrl,
QString command,
QString arguments,
bool enableTimeout,
int timeoutMs,
NvLogLevel logLevel);
QString m_Address;
QNetworkAccessManager m_Nam;
QSslCertificate m_ServerCert;
};
+32 -18
View File
@@ -8,8 +8,10 @@
#include <openssl/x509.h>
#include <openssl/evp.h>
#define REQUEST_TIMEOUT_MS 5000
NvPairingManager::NvPairingManager(QString address) :
m_Http(address)
m_Http(address, QSslCertificate())
{
QByteArray cert = IdentityManager::get()->getCertificate();
BIO *bio = BIO_new_mem_buf(cert.data(), -1);
@@ -161,7 +163,7 @@ NvPairingManager::saltPin(QByteArray salt, QString pin)
}
NvPairingManager::PairState
NvPairingManager::pair(QString appVersion, QString pin)
NvPairingManager::pair(QString appVersion, QString pin, QSslCertificate& serverCert)
{
int serverMajorVersion = NvHTTP::parseQuad(appVersion).at(0);
qInfo() << "Pairing with server generation:" << serverMajorVersion;
@@ -192,7 +194,7 @@ NvPairingManager::pair(QString appVersion, QString pin)
"pair",
"devicename=roth&updateState=1&phrase=getservercert&salt=" +
salt.toHex() + "&clientcert=" + IdentityManager::get()->getCertificate().toHex(),
false);
0);
NvHTTP::verifyResponseStatus(getCert);
if (NvHTTP::getXmlString(getCert, "paired") != "1")
{
@@ -200,26 +202,38 @@ NvPairingManager::pair(QString appVersion, QString pin)
return PairState::FAILED;
}
QByteArray serverCert = NvHTTP::getXmlStringFromHex(getCert, "plaincert");
if (serverCert == nullptr)
QByteArray serverCertStr = NvHTTP::getXmlStringFromHex(getCert, "plaincert");
if (serverCertStr == nullptr)
{
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;
}
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 encryptedChallenge = encrypt(randomChallenge, &encKey);
QString challengeXml = m_Http.openConnectionToString(m_Http.m_BaseUrlHttp,
"pair",
"devicename=roth&updateState=1&clientchallenge=" +
encryptedChallenge.toHex(),
true);
REQUEST_TIMEOUT_MS);
NvHTTP::verifyResponseStatus(challengeXml);
if (NvHTTP::getXmlString(challengeXml, "paired") != "1")
{
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;
}
@@ -247,12 +261,12 @@ NvPairingManager::pair(QString appVersion, QString pin)
"pair",
"devicename=roth&updateState=1&serverchallengeresp=" +
encryptedChallengeResponseHash.toHex(),
true);
REQUEST_TIMEOUT_MS);
NvHTTP::verifyResponseStatus(respXml);
if (NvHTTP::getXmlString(respXml, "paired") != "1")
{
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;
}
@@ -262,21 +276,21 @@ NvPairingManager::pair(QString appVersion, QString pin)
if (!verifySignature(serverSecret,
serverSignature,
serverCert))
serverCertStr))
{
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;
}
QByteArray expectedResponseData;
expectedResponseData.append(randomChallenge);
expectedResponseData.append(getSignatureFromPemCert(serverCert));
expectedResponseData.append(getSignatureFromPemCert(serverCertStr));
expectedResponseData.append(serverSecret);
if (QCryptographicHash::hash(expectedResponseData, hashAlgo) != serverResponse)
{
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;
}
@@ -288,24 +302,24 @@ NvPairingManager::pair(QString appVersion, QString pin)
"pair",
"devicename=roth&updateState=1&clientpairingsecret=" +
clientPairingSecret.toHex(),
true);
REQUEST_TIMEOUT_MS);
NvHTTP::verifyResponseStatus(secretRespXml);
if (NvHTTP::getXmlString(secretRespXml, "paired") != "1")
{
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;
}
QString pairChallengeXml = m_Http.openConnectionToString(m_Http.m_BaseUrlHttps,
"pair",
"devicename=roth&updateState=1&phrase=pairchallenge",
true);
REQUEST_TIMEOUT_MS);
NvHTTP::verifyResponseStatus(pairChallengeXml);
if (NvHTTP::getXmlString(pairChallengeXml, "paired") != "1")
{
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;
}
+1 -1
View File
@@ -23,7 +23,7 @@ public:
~NvPairingManager();
PairState
pair(QString appVersion, QString pin);
pair(QString appVersion, QString pin, QSslCertificate& serverCert);
private:
QByteArray
+414
View File
@@ -0,0 +1,414 @@
#include "commandlineparser.h"
#include <QCommandLineParser>
#include <QRegularExpression>
#if defined(Q_OS_WIN)
#include <qt_windows.h>
#endif
static bool inRange(int value, int min, int max)
{
return value >= min && value <= max;
}
// This method returns key's value from QMap where the key is a QString.
// Key matching is case insensitive.
template <typename T>
static T mapValue(QMap<QString, T> map, QString key)
{
for(auto& item : map.toStdMap()) {
if (QString::compare(item.first, key, Qt::CaseInsensitive) == 0) {
return item.second;
}
}
return T();
}
class CommandLineParser : public QCommandLineParser
{
public:
enum MessageType {
Info,
Error
};
void setupCommonOptions()
{
setSingleDashWordOptionMode(QCommandLineParser::ParseAsLongOptions);
addHelpOption();
addVersionOption();
}
void handleHelpAndVersionOptions()
{
if (isSet("help")) {
showInfo(helpText());
}
if (isSet("version")) {
showVersion();
}
}
void handleUnknownOptions()
{
if (unknownOptionNames().length()) {
showError(QString("Unknown options: %1").arg(unknownOptionNames().join(", ")));
}
}
void showMessage(QString message, MessageType type) const
{
#if defined(Q_OS_WIN)
UINT flags = MB_OK | MB_TOPMOST | MB_SETFOREGROUND;
flags |= (type == Info ? MB_ICONINFORMATION : MB_ICONERROR);
QString title = "Moonlight";
MessageBoxW(nullptr, reinterpret_cast<const wchar_t *>(message.utf16()),
reinterpret_cast<const wchar_t *>(title.utf16()), flags);
#endif
message = message.endsWith('\n') ? message : message + '\n';
fputs(qPrintable(message), type == Info ? stdout : stderr);
}
[[ noreturn ]] void showInfo(QString message) const
{
showMessage(message, Info);
exit(0);
}
[[ noreturn ]] void showError(QString message) const
{
showMessage(message + "\n\n" + helpText(), Error);
exit(1);
}
int getIntOption(QString name) const
{
bool ok;
int intValue = value(name).toInt(&ok);
if (!ok) {
showError(QString("Invalid %1 value: %2").arg(name, value(name)));
}
return intValue;
}
bool getToggleOptionValue(QString name, bool defaultValue) const
{
QRegularExpression re(QString("^(%1|no-%1)$").arg(name));
QStringList options = optionNames().filter(re);
if (options.isEmpty()) {
return defaultValue;
} else {
return options.last() == name;
}
}
QString getChoiceOptionValue(QString name) const
{
if (!m_Choices[name].contains(value(name), Qt::CaseInsensitive)) {
showError(QString("Invalid %1 choice: %2").arg(name, value(name)));
}
return value(name);
}
QPair<int,int> getResolutionOptionValue(QString name) const
{
QRegularExpression re("^(\\d+)x(\\d+)$", QRegularExpression::CaseInsensitiveOption);
auto match = re.match(value(name));
if (!match.hasMatch()) {
showError(QString("Invalid %1 format: %2").arg(name, value(name)));
}
return qMakePair(match.captured(1).toInt(), match.captured(2).toInt());
}
void addFlagOption(QString name, QString descriptiveName)
{
addOption(QCommandLineOption(name, QString("Use %1.").arg(descriptiveName)));
}
void addToggleOption(QString name, QString descriptiveName)
{
addOption(QCommandLineOption(name, QString("Use %1.").arg(descriptiveName)));
addOption(QCommandLineOption("no-" + name, QString("Do not use %1.").arg(descriptiveName)));
}
void addValueOption(QString name, QString descriptiveName)
{
addOption(QCommandLineOption(name, QString("Specify %1 to use.").arg(descriptiveName), name));
}
void addChoiceOption(QString name, QString descriptiveName, QStringList choices)
{
addOption(QCommandLineOption(name, QString("Select %1: %2.").arg(descriptiveName, choices.join('/')), name));
m_Choices[name] = choices;
}
private:
QMap<QString, QStringList> m_Choices;
};
GlobalCommandLineParser::GlobalCommandLineParser()
{
}
GlobalCommandLineParser::~GlobalCommandLineParser()
{
}
GlobalCommandLineParser::ParseResult GlobalCommandLineParser::parse(const QStringList &args)
{
CommandLineParser parser;
parser.setupCommonOptions();
parser.setApplicationDescription(
"\n"
"Starts Moonlight normally if no arguments are given.\n"
"\n"
"Available actions:\n"
" quit Quit the currently running app\n"
" stream Start streaming an app\n"
"\n"
"See 'moonlight <action> --help' for help of specific action."
);
parser.addPositionalArgument("action", "Action to execute", "<action>");
parser.parse(args);
auto posArgs = parser.positionalArguments();
QString action = posArgs.isEmpty() ? QString() : posArgs.first().toLower();
if (action == "") {
// This method will not return and terminates the process if --version
// or --help is specified
parser.handleHelpAndVersionOptions();
parser.handleUnknownOptions();
return NormalStartRequested;
} else if (action == "quit") {
return QuitRequested;
} else if (action == "stream") {
return StreamRequested;
} else {
parser.showError(QString("Invalid action: %1").arg(action));
}
}
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()
{
m_WindowModeMap = {
{"fullscreen", StreamingPreferences::WM_FULLSCREEN},
{"windowed", StreamingPreferences::WM_WINDOWED},
{"borderless", StreamingPreferences::WM_FULLSCREEN_DESKTOP},
};
m_AudioConfigMap = {
{"stereo", StreamingPreferences::AC_STEREO},
{"5.1-surround", StreamingPreferences::AC_51_SURROUND},
};
m_VideoCodecMap = {
{"auto", StreamingPreferences::VCC_AUTO},
{"H.264", StreamingPreferences::VCC_FORCE_H264},
{"HEVC", StreamingPreferences::VCC_FORCE_HEVC},
};
m_VideoDecoderMap = {
{"auto", StreamingPreferences::VDS_AUTO},
{"software", StreamingPreferences::VDS_FORCE_HARDWARE},
{"hardware", StreamingPreferences::VDS_FORCE_SOFTWARE},
};
}
StreamCommandLineParser::~StreamCommandLineParser()
{
}
void StreamCommandLineParser::parse(const QStringList &args, StreamingPreferences *preferences)
{
CommandLineParser parser;
parser.setupCommonOptions();
parser.setApplicationDescription(
"\n"
"Starts directly streaming a given app."
);
parser.addPositionalArgument("stream", "Start stream");
// Add other arguments and options
parser.addPositionalArgument("host", "Host computer name, UUID, or IP address", "<host>");
parser.addPositionalArgument("app", "App to stream", "\"<app>\"");
parser.addFlagOption("720", "1280x720 resolution");
parser.addFlagOption("1080", "1920x1080 resolution");
parser.addFlagOption("1440", "2560x1440 resolution");
parser.addFlagOption("4K", "3840x2160 resolution");
parser.addValueOption("resolution", "custom <width>x<height> resolution");
parser.addToggleOption("vsync", "V-Sync");
parser.addValueOption("fps", "FPS");
parser.addValueOption("bitrate", "bitrate in Kbps");
parser.addChoiceOption("display-mode", "display mode", m_WindowModeMap.keys());
parser.addChoiceOption("audio-config", "audio config", m_AudioConfigMap.keys());
parser.addToggleOption("multi-controller", "multiple controller support");
parser.addToggleOption("quit-after", "quit app after session");
parser.addToggleOption("mouse-acceleration", "mouse acceleration");
parser.addToggleOption("game-optimization", "game optimizations");
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-decoder", "video decoder", m_VideoDecoderMap.keys());
if (!parser.parse(args)) {
parser.showError(parser.errorText());
}
parser.handleUnknownOptions();
// Resolve display's width and height
QRegularExpression resolutionRexExp("^(720|1080|1440|4K|resolution)$");
QStringList resoOptions = parser.optionNames().filter(resolutionRexExp);
bool displaySet = resoOptions.length();
if (displaySet) {
QString name = resoOptions.last();
if (name == "720") {
preferences->width = 1280;
preferences->height = 720;
displaySet = true;
} else if (name == "1080") {
preferences->width = 1920;
preferences->height = 1080;
displaySet = true;
} else if (name == "1440") {
preferences->width = 2560;
preferences->height = 1440;
displaySet = true;
} else if (name == "4K") {
preferences->width = 3840;
preferences->height = 2160;
displaySet = true;
} else if (name == "resolution") {
auto resolution = parser.getResolutionOptionValue(name);
preferences->width = resolution.first;
preferences->height = resolution.second;
}
}
// Resolve --fps option
if (parser.isSet("fps")) {
preferences->fps = parser.getIntOption("fps");
if (!inRange(preferences->fps, 30, 120)) {
parser.showError("FPS must be in range: 30 - 120");
}
}
// Resolve --bitrate option
if (parser.isSet("bitrate")) {
preferences->bitrateKbps = parser.getIntOption("bitrate");
if (!inRange(preferences->bitrateKbps, 500, 150000)) {
parser.showError("Bitrate must be in range: 500 - 150000");
}
} else if (displaySet || parser.isSet("fps")) {
preferences->bitrateKbps = preferences->getDefaultBitrate(
preferences->width, preferences->height, preferences->fps);
}
// Resolve --display option
if (parser.isSet("display-mode")) {
preferences->windowMode = mapValue(m_WindowModeMap, parser.getChoiceOptionValue("display-mode"));
}
// Resolve --vsync and --no-vsync options
preferences->enableVsync = parser.getToggleOptionValue("vsync", preferences->enableVsync);
// Resolve --audio-config option
if (parser.isSet("audio-config")) {
preferences->audioConfig = mapValue(m_AudioConfigMap, parser.getChoiceOptionValue("audio-config"));
}
// Resolve --multi-controller and --no-multi-controller options
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
preferences->mouseAcceleration = parser.getToggleOptionValue("mouse-acceleration", preferences->mouseAcceleration);
// Resolve --game-optimization and --no-game-optimization options
preferences->gameOptimizations = parser.getToggleOptionValue("game-optimization", preferences->gameOptimizations);
// Resolve --audio-on-host and --no-audio-on-host options
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
if (parser.isSet("video-codec")) {
preferences->videoCodecConfig = mapValue(m_VideoCodecMap, parser.getChoiceOptionValue("video-codec"));
}
// Resolve --video-decoder option
if (parser.isSet("video-decoder")) {
preferences->videoDecoderSelection = mapValue(m_VideoDecoderMap, parser.getChoiceOptionValue("video-decoder"));
}
// This method will not return and terminates the process if --version or
// --help is specified
parser.handleHelpAndVersionOptions();
// Verify that both host and app has been provided
auto posArgs = parser.positionalArguments();
if (posArgs.length() < 2) {
parser.showError("Host not provided");
}
m_Host = parser.positionalArguments().at(1);
if (posArgs.length() < 3) {
parser.showError("App not provided");
}
m_AppName = parser.positionalArguments().at(2);
}
QString StreamCommandLineParser::getHost() const
{
return m_Host;
}
QString StreamCommandLineParser::getAppName() const
{
return m_AppName;
}
+56
View File
@@ -0,0 +1,56 @@
#pragma once
#include "settings/streamingpreferences.h"
#include <QMap>
#include <QString>
class GlobalCommandLineParser
{
public:
enum ParseResult {
NormalStartRequested,
StreamRequested,
QuitRequested,
};
GlobalCommandLineParser();
virtual ~GlobalCommandLineParser();
ParseResult parse(const QStringList &args);
};
class QuitCommandLineParser
{
public:
QuitCommandLineParser();
virtual ~QuitCommandLineParser();
void parse(const QStringList &args);
QString getHost() const;
private:
QString m_Host;
};
class StreamCommandLineParser
{
public:
StreamCommandLineParser();
virtual ~StreamCommandLineParser();
void parse(const QStringList &args, StreamingPreferences *preferences);
QString getHost() const;
QString getAppName() const;
private:
QString m_Host;
QString m_AppName;
QMap<QString, StreamingPreferences::WindowMode> m_WindowModeMap;
QMap<QString, StreamingPreferences::AudioConfig> m_AudioConfigMap;
QMap<QString, StreamingPreferences::VideoCodecConfig> m_VideoCodecMap;
QMap<QString, StreamingPreferences::VideoDecoderSelection> m_VideoDecoderMap;
};
+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;
};
}
+256
View File
@@ -0,0 +1,256 @@
#include "startstream.h"
#include "backend/computermanager.h"
#include "backend/computerseeker.h"
#include "streaming/session.h"
#include <QCoreApplication>
#include <QTimer>
#define COMPUTER_SEEK_TIMEOUT 10000
#define APP_SEEK_TIMEOUT 10000
namespace CliStartStream
{
enum State {
StateInit,
StateSeekComputer,
StateSeekApp,
StateStartSession,
StateFailure,
};
class Event
{
public:
enum Type {
AppQuitCompleted,
AppQuitRequested,
ComputerFound,
ComputerUpdated,
Executed,
Timedout,
};
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);
Session* session;
NvApp app;
switch (event.type) {
// Occurs when CliStartStreamSegue becomes visible and the UI calls launcher's execute()
case Event::Executed:
if (m_State == StateInit) {
m_State = StateSeekComputer;
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, &Launcher::onComputerUpdated);
q->connect(m_ComputerManager, &ComputerManager::quitAppCompleted,
q, &Launcher::onQuitAppCompleted);
emit q->searchingComputer();
}
break;
// Occurs when searched computer is found
case Event::ComputerFound:
if (m_State == StateSeekComputer) {
if (event.computer->pairState == NvComputer::PS_PAIRED) {
m_State = StateSeekApp;
m_Computer = event.computer;
m_TimeoutTimer->start(APP_SEEK_TIMEOUT);
emit q->searchingApp();
} 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 a computer is updated
case Event::ComputerUpdated:
if (m_State == StateSeekApp) {
int index = getAppIndex();
if (-1 != index) {
app = m_Computer->appList[index];
m_TimeoutTimer->stop();
if (isNotStreaming() || isStreamingApp(app)) {
m_State = StateStartSession;
session = new Session(m_Computer, app, m_Preferences);
emit q->sessionCreated(app.name, session);
} else {
emit q->appQuitRequired(getCurrentAppName());
}
}
}
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:
if (m_State == StateSeekComputer) {
m_State = StateFailure;
emit q->failed(QString("Failed to connect to %1").arg(m_ComputerName));
}
if (m_State == StateSeekApp) {
m_State = StateFailure;
emit q->failed(QString("Failed to find application %1").arg(m_AppName));
}
break;
}
}
int getAppIndex() const
{
for (int i = 0; i < m_Computer->appList.length(); i++) {
if (m_Computer->appList[i].name.toLower() == m_AppName.toLower()) {
return i;
}
}
return -1;
}
bool isNotStreaming() const
{
return m_Computer->currentGameId == 0;
}
bool isStreamingApp(NvApp app) const
{
return m_Computer->currentGameId == app.id;
}
QString getCurrentAppName() const
{
for (NvApp app : m_Computer->appList) {
if (m_Computer->currentGameId == app.id) {
return app.name;
}
}
return "<UNKNOWN>";
}
Launcher *q_ptr;
QString m_ComputerName;
QString m_AppName;
StreamingPreferences *m_Preferences;
ComputerManager *m_ComputerManager;
ComputerSeeker *m_ComputerSeeker;
NvComputer *m_Computer;
State m_State;
QTimer *m_TimeoutTimer;
};
Launcher::Launcher(QString computer, QString app,
StreamingPreferences* preferences, QObject *parent)
: QObject(parent),
m_DPtr(new LauncherPrivate(this))
{
Q_D(Launcher);
d->m_ComputerName = computer;
d->m_AppName = app;
d->m_Preferences = preferences;
d->m_State = StateInit;
d->m_TimeoutTimer = new QTimer(this);
d->m_TimeoutTimer->setSingleShot(true);
connect(d->m_TimeoutTimer, &QTimer::timeout,
this, &Launcher::onTimeout);
}
Launcher::~Launcher()
{
}
void Launcher::execute(ComputerManager *manager)
{
Q_D(Launcher);
Event event(Event::Executed);
event.computerManager = manager;
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)
{
Q_D(Launcher);
Event event(Event::ComputerUpdated);
event.computer = computer;
d->handleEvent(event);
}
void Launcher::onTimeout()
{
Q_D(Launcher);
Event event(Event::Timedout);
d->handleEvent(event);
}
void Launcher::onQuitAppCompleted(QVariant error)
{
Q_D(Launcher);
Event event(Event::AppQuitCompleted);
event.errorMessage = error.toString();
d->handleEvent(event);
}
}
+48
View File
@@ -0,0 +1,48 @@
#pragma once
#include <QObject>
#include <QVariant>
class ComputerManager;
class NvComputer;
class Session;
class StreamingPreferences;
namespace CliStartStream
{
class Event;
class LauncherPrivate;
class Launcher : public QObject
{
Q_OBJECT
Q_DECLARE_PRIVATE_D(m_DPtr, Launcher)
public:
explicit Launcher(QString computer, QString app,
StreamingPreferences* preferences,
QObject *parent = nullptr);
~Launcher();
Q_INVOKABLE void execute(ComputerManager *manager);
Q_INVOKABLE void quitRunningApp();
Q_INVOKABLE bool isExecuted() const;
signals:
void searchingComputer();
void searchingApp();
void sessionCreated(QString appName, Session *session);
void failed(QString text);
void appQuitRequired(QString appName);
private slots:
void onComputerFound(NvComputer *computer);
void onComputerUpdated(NvComputer *computer);
void onTimeout();
void onQuitAppCompleted(QVariant error);
private:
QScopedPointer<LauncherPrivate> m_DPtr;
};
}
@@ -4,7 +4,7 @@
<metadata_license>CC0-1.0</metadata_license>
<project_license>GPL-3.0+</project_license>
<name>Moonlight</name>
<summary>Stream games from your NVIDIA GameStream-enabled PC</summary>
<summary>Play games remotely from your NVIDIA GameStream-enabled PC</summary>
<description>
<p>Moonlight can stream games and other applications from a PC with an NVIDIA GeForce GTX 600-series or higher GPU and GeForce Experience installed.</p>
@@ -19,20 +19,149 @@
</ul>
</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="help">https://github.com/moonlight-stream/moonlight-docs/wiki/Setup-Guide</url>
<screenshots>
<screenshot type="default">
<image>https://moonlight-stream.com/appstream_resources/moonlight_screenshot_1.png</image>
<image>https://moonlight-stream.org/appstream_resources/moonlight_screenshot_1.png</image>
</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>
</screenshots>
<releases>
<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">
<description>
<p>New features:</p>
<ul>
<li>Added keyboard and gamepad UI navigation</li>
<li>Added support for mouse X1 and X2 buttons (back and forward)</li>
<li>Added support for mouse movement with touchscreens</li>
<li>Improved HEVC performance at high bitrates</li>
<li>Added support for launching streaming sessions directly from a terminal or script</li>
<li>Added quit shortcut for gamepads: Select+Start+L1+R1</li>
</ul>
<p>Bugfixes:</p>
<ul>
<li>Fixed not saving deletion of PCs</li>
<li>Fixed keys being stuck down on the host PC after Moonlight loses focus or quits</li>
<li>Fixed some warning dialogs being stuck behind the UI window</li>
<li>Updated included gamepad mapping list</li>
</ul>
</description>
</release>
<release version="0.5.0" date="2018-09-15">
<description>
<p>New features:</p>
@@ -137,7 +266,7 @@
</categories>
<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>
-438
View File
@@ -1,438 +0,0 @@
# Game Controller DB for SDL in 2.0.6 format
# Source: https://github.com/gabomdq/SDL_GameControllerDB
# Windows
03000000fa2d00000100000000000000,3DRUDDER,leftx:a0,lefty:a1,rightx:a5,righty:a2,platform:Windows,
03000000022000000090000000000000,8Bitdo NES30 Pro,a:b1,b:b0,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftstick:b13,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:b9,rightx:a3,righty:a4,start:b11,x:b4,y:b3,platform:Windows,
03000000203800000900000000000000,8Bitdo NES30 Pro,a:b1,b:b0,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftstick:b13,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:b9,rightx:a3,righty:a4,start:b11,x:b4,y:b3,platform:Windows,
03000000c82d00000060000000000000,8Bitdo SF30 Pro,a:b1,b:b0,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftstick:b13,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:b9,rightx:a3,righty:a4,start:b11,x:b4,y:b3,platform:Windows,
03000000c82d00000061000000000000,8Bitdo SF30 Pro Wireless,a:b1,b:b0,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b2,leftshoulder:b6,leftstick:b13,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:b9,rightx:a3,righty:a4,start:b11,x:b4,y:b3,platform:Windows,
03000000102800000900000000000000,8Bitdo SFC30 GamePad,a:b1,b:b0,back:b10,leftshoulder:b6,leftx:a0,lefty:a1,rightshoulder:b7,start:b11,x:b4,y:b3,platform:Windows,
03000000a00500003232000000000000,8Bitdo Zero GamePad,a:b0,b:b1,back:b10,dpdown:+a2,dpleft:-a0,dpright:+a0,dpup:-a2,leftshoulder:b6,rightshoulder:b7,start:b11,x:b3,y:b4,platform:Windows,
030000008f0e00001200000000000000,Acme,a:b0,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b5,leftx:a0,lefty:a1,rightshoulder:b6,rightstick:b11,righttrigger:b7,rightx:a3,righty:a2,start:b9,x:b2,y:b3,platform:Windows,
03000000341a00003608000000000000,Afterglow PS3 Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Windows,
03000000c01100001352000000000000,Battalife Joystick,a:b6,b:b7,back:b2,leftshoulder:b0,leftx:a0,lefty:a1,rightshoulder:b1,start:b3,x:b4,y:b5,platform:Windows,
030000006b1400000055000000000000,bigben ps3padstreetnew,a:b0,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b2,y:b3,platform:Windows,
0300000066f700000500000000000000,BrutalLegendTest,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a3,righty:a2,start:b9,x:b0,y:b3,platform:Windows,
03000000d81d00000b00000000000000,BUFFALO BSGP1601 Series ,a:b5,b:b3,back:b12,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b8,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b9,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b13,x:b4,y:b2,platform:Windows,
03000000e82000006058000000000000,Cideko AK08b,a:b2,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b3,y:b0,platform:Windows,
030000005e0400008e02000000000000,Controller (XBOX 360 For Windows),a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b8,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b9,righttrigger:a2,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Windows,
03000000260900008888000000000000,Cyber Gadget GameCube Controller,a:b0,b:b1,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,lefttrigger:a5,leftx:a0,lefty:a1,rightshoulder:b6,righttrigger:a4,rightx:a2,righty:a3~,start:b7,x:b2,y:b3,platform:Windows,
03000000a306000022f6000000000000,Cyborg V.3 Rumble Pad,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:+a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:-a3,rightx:a2,righty:a4,start:b9,x:b0,y:b3,platform:Windows,
03000000791d00000103000000000000,Dual Box WII,a:b2,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b10,leftshoulder:b6,lefttrigger:b4,leftx:a0,lefty:a1,rightshoulder:b7,righttrigger:b5,rightx:a2,righty:a3,start:b9,x:b3,y:b0,platform:Windows,
030000004f04000023b3000000000000,Dual Trigger 3-in-1,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Windows,
03000000341a00000108000000000000,EXEQ RF USB Gamepad 8206,a:b0,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b8,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b7,rightx:a2,righty:a3,start:b9,x:b2,y:b3,platform:Windows,
030000000d0f00008500000000000000,Fighting Commander 2016 PS3,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Windows,
030000000d0f00008400000000000000,Fighting Commander 5,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Windows,
030000000d0f00008700000000000000,Fighting Stick mini 4,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,lefttrigger:b6,rightshoulder:b5,righttrigger:b7,start:b9,x:b0,y:b3,platform:Windows,
030000000d0f00008800000000000000,Fighting Stick mini 4,a:b1,b:b2,back:b9,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,lefttrigger:b6,rightshoulder:b5,righttrigger:b7,start:b8,x:b0,y:b3,platform:Windows,
030000000d0f00002700000000000000,FIGHTING STICK V3,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,lefttrigger:b6,rightshoulder:b5,righttrigger:b7,start:b9,x:b0,y:b3,platform:Windows,
78696e70757403000000000000000000,Fightstick TES,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b10,leftshoulder:b4,lefttrigger:a2,rightshoulder:b5,righttrigger:a5,start:b7,x:b2,y:b3,platform:Windows,
03000000790000000600000000000000,G-Shark GS-GP702,a:b2,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a4,start:b9,x:b3,y:b0,platform:Windows,
03000000260900002625000000000000,Gamecube Controller,a:b0,b:b1,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b6,lefttrigger:a4,leftx:a0,lefty:a1,righttrigger:a5,rightx:a2,righty:a3,start:b7,x:b2,y:b3,platform:Windows,
030000008f0e00000d31000000000000,GAMEPAD 3 TURBO,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Windows,
03000000280400000140000000000000,GamePad Pro USB,a:b1,b:b2,back:b8,leftshoulder:b4,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,righttrigger:b7,start:b9,x:b0,y:b3,platform:Windows,
03000000ffff00000000000000000000,GameStop Gamepad,a:b0,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b2,y:b3,platform:Windows,
03000000451300000010000000000000,Generic USB Joystick,a:b0,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b2,y:b3,platform:Windows,
03000000341a00000302000000000000,Hama Scorpad,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Windows,
030000000d0f00004900000000000000,Hatsune Miku Sho Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Windows,
03000000d81400000862000000000000,HitBox Edition Cthulhu+,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b5,lefttrigger:b4,rightshoulder:b7,righttrigger:b6,start:b9,x:b0,y:b3,platform:Windows,
030000000d0f00005f00000000000000,Hori Fighting Commander 4 (PS3),a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Windows,
030000000d0f00005e00000000000000,Hori Fighting Commander 4 (PS4),a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Windows,
030000000d0f00004000000000000000,Hori Fighting Stick Mini 3,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b5,lefttrigger:b4,rightshoulder:b7,righttrigger:b6,start:b9,x:b0,y:b3,platform:Windows,
030000000d0f00006e00000000000000,HORIPAD 4 (PS3),a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Windows,
030000000d0f00006600000000000000,HORIPAD 4 (PS4),a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Windows,
030000000d0f0000ee00000000000000,HORIPAD mini4,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Windows,
030000000d0f00004d00000000000000,HORIPAD3 A,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Windows,
03000000250900000017000000000000,HRAP2 on PS/SS/N64 Joypad to USB BOX,a:b2,b:b1,back:b9,leftshoulder:b5,lefttrigger:b4,leftx:a0,lefty:a1,rightshoulder:b7,righttrigger:b6,start:b8,x:b3,y:b0,platform:Windows,
030000008f0e00001330000000000000,HuiJia SNES Controller,a:b2,b:b1,back:b8,dpdown:+a1,dpleft:-a0,dpright:+a0,dpup:-a1,leftshoulder:b6,rightshoulder:b7,start:b9,x:b3,y:b0,platform:Windows,
03000000d81d00000f00000000000000,iBUFFALO BSGP1204 Series,a:b2,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftstick:b10,lefttrigger:b4,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b11,righttrigger:b5,rightx:a2,righty:a3,start:b9,x:b3,y:b0,platform:Windows,
03000000d81d00001000000000000000,iBUFFALO BSGP1204P Series,a:b2,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftstick:b10,lefttrigger:b4,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b11,righttrigger:b5,rightx:a2,righty:a3,start:b9,x:b3,y:b0,platform:Windows,
03000000830500006020000000000000,iBuffalo SNES Controller,a:b1,b:b0,back:b6,dpdown:+a1,dpleft:-a0,dpright:+a0,dpup:-a1,leftshoulder:b4,rightshoulder:b5,start:b7,x:b3,y:b2,platform:Windows,
03000000b50700001403000000000000,IMPACT BLACK,a:b2,b:b3,back:b8,leftshoulder:b4,leftstick:b10,lefttrigger:b5,leftx:a0,lefty:a1,rightshoulder:b6,rightstick:b11,righttrigger:b7,rightx:a3,righty:a2,start:b9,x:b0,y:b1,platform:Windows,
030000006f0e00002401000000000000,INJUSTICE FightStick for PS3,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,lefttrigger:b6,rightshoulder:b5,righttrigger:b7,start:b9,x:b0,y:b3,platform:Windows,
03000000491900000204000000000000,Ipega PG-9023,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftstick:b13,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:b9,rightx:a3,righty:a4,start:b11,x:b3,y:b4,platform:Windows,
030000006d04000011c2000000000000,Logitech Cordless Wingman,a:b0,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b9,leftstick:b5,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b10,rightstick:b2,righttrigger:b7,rightx:a3,righty:a4,x:b4,platform:Windows,
030000006d04000016c2000000000000,Logitech Dual Action,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Windows,
030000006d04000018c2000000000000,Logitech F510 Gamepad,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Windows,
030000006d04000019c2000000000000,Logitech F710 Gamepad,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Windows,
03000000380700005032000000000000,Mad Catz FightPad PRO (PS3),a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Windows,
03000000380700005082000000000000,Mad Catz FightPad PRO (PS4),a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Windows,
03000000380700008433000000000000,Mad Catz FightStick TE S+ PS3,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,lefttrigger:b6,rightshoulder:b5,righttrigger:b7,start:b9,x:b0,y:b3,platform:Windows,
03000000380700008483000000000000,Mad Catz FightStick TE S+ PS4,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b6,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Windows,
03000000380700008134000000000000,Mad Catz FightStick TE2+ PS3,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b7,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b4,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Windows,
03000000380700008184000000000000,Mad Catz FightStick TE2+ PS4,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b5,leftstick:b10,lefttrigger:a4,leftx:a0,lefty:a1,rightshoulder:b4,rightstick:b11,righttrigger:b7,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Windows,
03000000380700008034000000000000,Mad Catz TE2 PS3 Fightstick,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Windows,
03000000380700008084000000000000,Mad Catz TE2 PS4 Fightstick,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Windows,
03000000380700008532000000000000,Madcatz Arcade Fightstick TE S PS3,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Windows,
03000000380700003888000000000000,Madcatz Arcade Fightstick TE S+ PS3,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Windows,
03000000380700001888000000000000,MadCatz SFIV FightStick PS3,a:b0,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b5,lefttrigger:b7,leftx:a0,lefty:a1,rightshoulder:b4,righttrigger:b6,rightx:a2,righty:a3,start:b9,x:b2,y:b3,platform:Windows,
03000000380700008081000000000000,MADCATZ SFV Arcade FightStick Alpha PS4,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,righttrigger:b7,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Windows,
030000008305000031b0000000000000,MaxfireBlaze3,a:b0,b:b1,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b2,y:b3,platform:Windows,
03000000250900000128000000000000,Mayflash Arcade Stick,a:b1,b:b2,back:b8,leftshoulder:b0,lefttrigger:b4,leftx:a0,lefty:a1,rightshoulder:b3,righttrigger:b7,start:b9,x:b5,y:b6,platform:Windows,
03000000790000004418000000000000,Mayflash GameCube Controller,a:b1,b:b2,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b7,righttrigger:a4,rightx:a5,righty:a2,start:b9,x:b0,y:b3,platform:Windows,
03000000790000004318000000000000,Mayflash GameCube Controller Adapter,a:b1,b:b2,back:b0,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b0,leftshoulder:b4,leftstick:b0,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b0,righttrigger:a4,rightx:a5,righty:a2,start:b9,x:b0,y:b3,platform:Windows,
030000008f0e00001030000000000000,Mayflash USB Adapter for original Sega Saturn controller,a:b0,b:b1,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,lefttrigger:b5,rightshoulder:b2,righttrigger:b7,start:b9,x:b3,y:b4,platform:Windows,
0300000025090000e803000000000000,Mayflash Wii Classic Controller,a:b1,b:b0,back:b8,dpdown:b13,dpleft:b12,dpright:b14,dpup:b11,guide:b10,leftshoulder:b4,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b3,y:b2,platform:Windows,
03000000790000000018000000000000,Mayflash WiiU Pro Game Controller Adapter (DInput),a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Windows,
03000000250900006688000000000000,MP-8866 Super Dual Box,a:b2,b:b1,back:b9,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftstick:b10,lefttrigger:b4,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b11,righttrigger:b5,rightx:a2,righty:a3,start:b8,x:b3,y:b0,platform:Windows,
030000001008000001e5000000000000,NEXT SNES Controller,a:b2,b:b1,back:b8,dpdown:+a1,dpleft:-a0,dpright:+a0,dpup:-a1,leftshoulder:b4,rightshoulder:b6,start:b9,x:b3,y:b0,platform:Windows,
03000000bd12000015d0000000000000,Nintendo Retrolink USB Super SNES Classic Controller,a:b2,b:b1,back:b8,leftshoulder:b4,leftx:a0,lefty:a1,rightshoulder:b5,start:b9,x:b3,y:b0,platform:Windows,
030000007e0500000920000000000000,Nintendo Switch Pro Controller,a:b0,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b2,y:b3,platform:Windows,
030000004b120000014d000000000000,NYKO AIRFLO,a:b0,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b10,leftshoulder:a3,leftstick:a0,lefttrigger:b6,rightshoulder:b5,rightstick:a2,righttrigger:b7,start:b9,x:b2,y:b3,platform:Windows,
03000000362800000100000000000000,OUYA Game Controller,a:b0,b:b3,dpdown:b9,dpleft:b10,dpright:b11,dpup:b8,guide:b14,leftshoulder:b4,leftstick:b6,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b7,righttrigger:b13,rightx:a3,righty:a4,x:b1,y:b2,platform:Windows,
03000000120c0000f60e000000000000,P4 Wired Gamepad,a:b1,b:b2,back:b12,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b5,lefttrigger:b7,rightshoulder:b4,righttrigger:b6,start:b9,x:b0,y:b3,platform:Windows,
030000008f0e00000300000000000000,Piranha xtreme,a:b2,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftstick:b10,lefttrigger:b4,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b11,righttrigger:b5,rightx:a3,righty:a2,start:b9,x:b3,y:b0,platform:Windows,
03000000d62000006dca000000000000,PowerA Pro Ex,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Windows,
030000008f0e00007530000000000000,PS (R) Gamepad,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b1,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Windows,
03000000e30500009605000000000000,PS to USB convert cable,a:b2,b:b1,back:b9,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftstick:b10,lefttrigger:b4,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b11,righttrigger:b5,rightx:a2,righty:a3,start:b8,x:b3,y:b0,platform:Windows,
03000000100800000100000000000000,PS1 USB,a:b2,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftstick:b10,lefttrigger:b4,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b11,righttrigger:b5,rightx:a3,righty:a2,start:b9,x:b3,y:b0,platform:Windows,
03000000100800000300000000000000,PS2 USB,a:b2,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftstick:b10,lefttrigger:b4,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b11,righttrigger:b5,rightx:a4,righty:a2,start:b9,x:b3,y:b0,platform:Windows,
030000004c0500006802000000000000,PS3 Controller,a:b14,b:b13,back:b0,dpdown:b6,dpleft:b7,dpright:b5,dpup:b4,guide:b16,leftshoulder:b10,leftstick:b1,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b11,rightstick:b2,righttrigger:b9,rightx:a2,righty:a3,start:b3,x:b15,y:b12,platform:Windows,
03000000888800000803000000000000,PS3 Controller,a:b2,b:b1,back:b8,dpdown:h0.8,dpleft:h0.4,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b9,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:b7,rightx:a3,righty:a4,start:b11,x:b0,y:b3,platform:Windows,
03000000250900000500000000000000,PS3 DualShock,a:b2,b:b1,back:b9,dpdown:h0.8,dpleft:h0.4,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftstick:b10,lefttrigger:b4,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b11,righttrigger:b5,rightx:a2,righty:a3,start:b8,x:b0,y:b3,platform:Windows,
03000000100000008200000000000000,PS360+ v1.66,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,lefttrigger:b6,leftx:h0.4,rightshoulder:b5,righttrigger:b7,start:b9,x:b0,y:b3,platform:Windows,
030000004c050000a00b000000000000,PS4 Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Windows,
030000004c050000c405000000000000,PS4 Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Windows,
030000004c050000cc09000000000000,PS4 Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Windows,
03000000300f00000011000000000000,QanBa Arcade JoyStick 1008,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,righttrigger:b7,start:b10,x:b0,y:b3,platform:Windows,
03000000300f00001611000000000000,QanBa Arcade JoyStick 4018,a:b1,b:b2,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b9,leftshoulder:b4,lefttrigger:b6,rightshoulder:b5,righttrigger:b7,start:b8,x:b0,y:b3,platform:Windows,
03000000222c00000020000000000000,QANBA DRONE ARCADE JOYSTICK,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,lefttrigger:a3,rightshoulder:b5,righttrigger:a4,start:b9,x:b0,y:b3,platform:Windows,
03000000300f00001210000000000000,QanBa Joystick Plus,a:b0,b:b1,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,rightshoulder:b5,start:b9,x:b2,y:b3,platform:Windows,
03000000341a00000104000000000000,QanBa Joystick Q4RAF,a:b5,b:b6,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b10,leftshoulder:b0,lefttrigger:b4,leftx:a0,lefty:a1,rightshoulder:b3,righttrigger:b7,start:b9,x:b1,y:b2,platform:Windows,
03000000222c00000223000000000000,Qanba Obsidian Arcade Joystick PS3 Mode,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Windows,
03000000222c00000023000000000000,Qanba Obsidian Arcade Joystick PS4 Mode,a:b1,b:b2,back:b13,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Windows,
03000000321500000003000000000000,Razer Hydra,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b8,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b9,righttrigger:a2,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Windows,
030000000d0f00001100000000000000,REAL ARCADE PRO.3,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,rightshoulder:b5,rightstick:b11,righttrigger:b7,start:b9,x:b0,y:b3,platform:Windows,
030000000d0f00006a00000000000000,Real Arcade Pro.4,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Windows,
030000000d0f00006b00000000000000,Real Arcade Pro.4,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Windows,
030000000d0f00008a00000000000000,Real Arcade Pro.4,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Windows,
030000000d0f00008b00000000000000,Real Arcade Pro.4,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Windows,
030000000d0f00007000000000000000,REAL ARCADE PRO.4 VLX,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,rightshoulder:b5,rightstick:b11,righttrigger:b7,start:b9,x:b0,y:b3,platform:Windows,
030000000d0f00002200000000000000,REAL ARCADE Pro.V3,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Windows,
030000000d0f00005b00000000000000,Real Arcade Pro.V4,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Windows,
030000000d0f00005c00000000000000,Real Arcade Pro.V4,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Windows,
03000000790000001100000000000000,Retrolink SNES Controller,a:b2,b:b1,back:b8,dpdown:+a4,dpleft:-a3,dpright:+a3,dpup:-a4,leftshoulder:b4,rightshoulder:b5,start:b9,x:b3,y:b0,platform:Windows,
0300000000f000000300000000000000,RetroUSB.com RetroPad,a:b1,b:b5,back:b2,leftshoulder:b6,leftx:a0,lefty:a1,rightshoulder:b7,start:b3,x:b0,y:b4,platform:Windows,
0300000000f00000f100000000000000,RetroUSB.com Super RetroPort,a:b1,b:b5,back:b2,leftshoulder:b6,leftx:a0,lefty:a1,rightshoulder:b7,start:b3,x:b0,y:b4,platform:Windows,
030000006b140000010d000000000000,Revolution Pro Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Windows,
030000006f0e00001e01000000000000,Rock Candy Gamepad for PS3,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Windows,
030000004f04000003d0000000000000,run'n'drive,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b7,leftshoulder:a3,leftstick:b10,lefttrigger:b4,leftx:a0,lefty:a1,rightshoulder:a4,rightstick:b11,righttrigger:b5,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Windows,
03000000a30600001af5000000000000,Saitek Cyborg,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a3,righty:a4,start:b9,x:b0,y:b3,platform:Windows,
03000000a306000023f6000000000000,Saitek Cyborg V.1 Game pad,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a4,start:b9,x:b0,y:b3,platform:Windows,
03000000300f00001201000000000000,Saitek Dual Analog Pad,a:b2,b:b3,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b5,leftx:a0,lefty:a1,rightshoulder:b6,rightstick:b11,righttrigger:b7,rightx:a3,righty:a2,start:b9,x:b0,y:b1,platform:Windows,
03000000a30600000cff000000000000,Saitek P2500 Force Rumble Pad,a:b2,b:b3,back:b11,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b10,leftshoulder:b4,leftstick:b8,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b9,righttrigger:b7,rightx:a2,righty:a3,x:b0,y:b1,platform:Windows,
03000000a30600000c04000000000000,Saitek P2900,a:b1,b:b2,back:b12,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a3,righty:a2,start:b9,x:b0,y:b3,platform:Windows,
03000000300f00001001000000000000,Saitek P480 Rumble Pad,a:b2,b:b3,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b5,leftx:a0,lefty:a1,rightshoulder:b6,rightstick:b11,righttrigger:b7,rightx:a3,righty:a2,start:b9,x:b0,y:b1,platform:Windows,
03000000a30600000b04000000000000,Saitek P990,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a3,righty:a2,start:b9,x:b0,y:b3,platform:Windows,
03000000a30600000b04000000010000,Saitek P990 Dual Analog Pad,a:b1,b:b2,back:b9,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a3,righty:a2,start:b8,x:b0,y:b3,platform:Windows,
03000000300f00001101000000000000,saitek rumble pad,a:b2,b:b3,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b10,lefttrigger:b5,leftx:a0,lefty:a1,rightshoulder:b6,rightstick:b11,righttrigger:b7,rightx:a3,righty:a2,start:b9,x:b0,y:b1,platform:Windows,
0300000000050000289b000000000000,Saturn_Adapter_2.0,a:b1,b:b2,leftshoulder:b6,lefttrigger:b4,leftx:a0,lefty:a1,rightshoulder:b7,righttrigger:b5,start:b9,x:b0,y:b3,platform:Windows,
030000009b2800000500000000000000,Saturn_Adapter_2.0,a:b1,b:b2,leftshoulder:b6,lefttrigger:b4,leftx:a0,lefty:a1,rightshoulder:b7,righttrigger:b5,start:b9,x:b0,y:b3,platform:Windows,
03000000341a00000208000000000000,SL-6555-SBK,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b8,lefttrigger:-a4,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b9,righttrigger:a4,rightx:a3,righty:a2,start:b7,x:b2,y:b3,platform:Windows,
030000008f0e00000800000000000000,SpeedLink Strike FX Wireless,a:b2,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b3,y:b0,platform:Windows,
03000000ff1100003133000000000000,SVEN X-PAD,a:b2,b:b3,back:b4,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b7,righttrigger:b9,rightx:a2,righty:a4,start:b5,x:b0,y:b1,platform:Windows,
03000000fa1900000706000000000000,Team 5,a:b2,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b3,y:b0,platform:Windows,
03000000b50700001203000000000000,Techmobility X6-38V,a:b2,b:b3,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b5,leftx:a0,lefty:a1,rightshoulder:b6,rightstick:b11,righttrigger:b7,rightx:a3,righty:a2,start:b9,x:b0,y:b1,platform:Windows,
030000004f04000015b3000000000000,Thrustmaster Dual Analog 2,a:b0,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b5,leftx:a0,lefty:a1,rightshoulder:b6,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b1,y:b3,platform:Windows,
030000004f04000000b3000000000000,Thrustmaster Firestorm Dual Power,a:b0,b:b2,back:b9,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b11,lefttrigger:b5,leftx:a0,lefty:a1,rightshoulder:b6,rightstick:b12,righttrigger:b7,rightx:a2,righty:a3,start:b10,x:b1,y:b3,platform:Windows,
030000004f04000004b3000000000000,Thrustmaster Firestorm Dual Power 3,a:b0,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b5,leftx:a0,lefty:a1,rightshoulder:b6,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b1,y:b3,platform:Windows,
03000000666600000488000000000000,TigerGame PS/PS2 Game Controller Adapter,a:b2,b:b1,back:b9,dpdown:b14,dpleft:b15,dpright:b13,dpup:b12,leftshoulder:b6,leftstick:b10,lefttrigger:b4,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b11,righttrigger:b5,rightx:a2,righty:a3,start:b8,x:b3,y:b0,platform:Windows,
03000000d90400000200000000000000,TwinShock PS2,a:b2,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftstick:b10,lefttrigger:b4,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b11,righttrigger:b5,rightx:a3,righty:a2,start:b9,x:b3,y:b0,platform:Windows,
03000000380700006652000000000000,UnKnown,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a3,righty:a4,start:b9,x:b0,y:b3,platform:Windows,
03000000632500002305000000000000,USB Vibration Joystick (BM),a:b2,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b3,y:b0,platform:Windows,
03000000790000001b18000000000000,Venom Arcade Joystick,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,lefttrigger:b6,rightshoulder:b5,righttrigger:b7,start:b9,x:b0,y:b3,platform:Windows,
03000000450c00002043000000000000,XEOX Gamepad SL-6556-BK,a:b0,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b2,y:b3,platform:Windows,
03000000172700004431000000000000,XiaoMi Game Controller,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b20,leftshoulder:b6,leftstick:b13,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:a7,rightx:a2,righty:a5,start:b11,x:b3,y:b4,platform:Windows,
03000000786901006e70000000000000,XInput Controller,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b10,leftshoulder:b4,leftstick:b8,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b9,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Windows,
xinput,XInput Controller,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b10,leftshoulder:b4,leftstick:b8,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b9,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Windows,
# Mac OS X
03000000022000000090000001000000,8Bitdo NES30 Pro,a:b1,b:b0,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftstick:b13,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:b9,rightx:a2,righty:a3,start:b11,x:b4,y:b3,platform:Mac OS X,
03000000203800000900000000010000,8Bitdo NES30 Pro,a:b1,b:b0,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftstick:b13,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:b9,rightx:a2,righty:a3,start:b11,x:b4,y:b3,platform:Mac OS X,
03000000102800000900000000000000,8Bitdo SFC30 GamePad Joystick,a:b1,b:b0,back:b10,leftshoulder:b6,leftx:a0,lefty:a1,rightshoulder:b7,start:b11,x:b4,y:b3,platform:Mac OS X,
03000000a00500003232000008010000,8Bitdo Zero GamePad,a:b0,b:b1,back:b10,dpdown:+a1,dpleft:-a0,dpright:+a0,dpup:-a1,leftshoulder:b6,rightshoulder:b7,start:b11,x:b3,y:b4,platform:Mac OS X,
03000000a00500003232000009010000,8Bitdo Zero GamePad,a:b0,b:b1,back:b10,dpdown:+a1,dpleft:-a0,dpright:+a0,dpup:-a1,leftshoulder:b6,rightshoulder:b7,start:b11,x:b3,y:b4,platform:Mac OS X,
030000008305000031b0000000000000,Cideko AK08b,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Mac OS X,
03000000260900008888000088020000,Cyber Gadget GameCube Controller,a:b0,b:b1,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,lefttrigger:a4,leftx:a0,lefty:a1,rightshoulder:b6,righttrigger:a5,rightx:a2,righty:a3~,start:b7,x:b2,y:b3,platform:Mac OS X,
03000000a306000022f6000001030000,Cyborg V.3 Rumble Pad,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:+a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:-a3,rightx:a2,righty:a4,start:b9,x:b0,y:b3,platform:Mac OS X,
03000000790000000600000000000000,G-Shark GP-702,a:b2,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a3,righty:a4,start:b9,x:b3,y:b0,platform:Mac OS X,
03000000ad1b000001f9000000000000,Gamestop BB-070 X360 Controller,a:b0,b:b1,back:b9,dpdown:b12,dpleft:b13,dpright:b14,dpup:b11,guide:b10,leftshoulder:b4,leftstick:b6,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b7,righttrigger:a5,rightx:a3,righty:a4,start:b8,x:b2,y:b3,platform:Mac OS X,
0500000047532047616d657061640000,GameStop Gamepad,a:b0,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b2,y:b3,platform:Mac OS X,
030000000d0f00005f00000000010000,Hori Fighting Commander 4 (PS3),a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Mac OS X,
030000000d0f00005e00000000010000,Hori Fighting Commander 4 (PS4),a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Mac OS X,
030000000d0f00005f00000000000000,HORI Fighting Commander 4 PS3,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Mac OS X,
030000000d0f00005e00000000000000,HORI Fighting Commander 4 PS4,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Mac OS X,
030000000d0f00004d00000000000000,HORI Gem Pad 3,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Mac OS X,
030000000d0f00006e00000000010000,HORIPAD 4 (PS3),a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Mac OS X,
030000000d0f00006600000000010000,HORIPAD 4 (PS4),a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Mac OS X,
030000000d0f00006600000000000000,HORIPAD FPS PLUS 4,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Mac OS X,
030000008f0e00001330000011010000,HuiJia SNES Controller,a:b4,b:b2,back:b16,dpdown:+a2,dpleft:-a0,dpright:+a0,dpup:-a2,leftshoulder:b12,rightshoulder:b14,start:b18,x:b6,y:b0,platform:Mac OS X,
03000000830500006020000000010000,iBuffalo SNES Controller,a:b1,b:b0,back:b6,dpdown:+a1,dpleft:-a0,dpright:+a0,dpup:-a1,leftshoulder:b4,rightshoulder:b5,start:b7,x:b3,y:b2,platform:Mac OS X,
03000000830500006020000000000000,iBuffalo USB 2-axis 8-button Gamepad,a:b1,b:b0,back:b6,leftshoulder:b4,leftx:a0,lefty:a1,rightshoulder:b5,start:b7,x:b3,y:b2,platform:Mac OS X,
030000007e0500000620000001000000,Joy-Con (L),+leftx:h0.2,+lefty:h0.4,-leftx:h0.8,-lefty:h0.1,a:b0,b:b1,leftshoulder:b4,leftstick:b10,rightshoulder:b5,start:b8,x:b2,y:b3,platform:Mac OS X,
030000006d04000016c2000000020000,Logitech Dual Action,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Mac OS X,
030000006d04000016c2000000030000,Logitech Dual Action,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Mac OS X,
030000006d04000016c2000014040000,Logitech Dual Action,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Mac OS X,
030000006d04000016c2000000000000,Logitech F310 Gamepad (DInput),a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Mac OS X,
030000006d04000018c2000000000000,Logitech F510 Gamepad (DInput),a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Mac OS X,
030000006d0400001fc2000000000000,Logitech F710 Gamepad (XInput),a:b0,b:b1,back:b9,dpdown:b12,dpleft:b13,dpright:b14,dpup:b11,guide:b10,leftshoulder:b4,leftstick:b6,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b7,righttrigger:a5,rightx:a3,righty:a4,start:b8,x:b2,y:b3,platform:Mac OS X,
030000006d04000019c2000000000000,Logitech Wireless Gamepad (DInput),a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Mac OS X,
03000000380700005032000000010000,Mad Catz FightPad PRO (PS3),a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Mac OS X,
03000000380700005082000000010000,Mad Catz FightPad PRO (PS4),a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Mac OS X,
03000000790000004418000000010000,Mayflash GameCube Controller,a:b1,b:b2,dpdown:b14,dpleft:b15,dpright:b13,dpup:b12,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b7,righttrigger:a4,rightx:a5,righty:a2,start:b9,x:b0,y:b3,platform:Mac OS X,
0300000025090000e803000000000000,Mayflash Wii Classic Controller,a:b1,b:b0,back:b8,dpdown:b13,dpleft:b12,dpright:b14,dpup:b11,guide:b10,leftshoulder:b4,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b3,y:b2,platform:Mac OS X,
03000000790000000018000000000000,Mayflash WiiU Pro Game Controller Adapter (DInput),a:b4,b:b8,back:b32,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b16,leftstick:b40,lefttrigger:b24,leftx:a0,lefty:a4,rightshoulder:b20,rightstick:b44,righttrigger:b28,rightx:a8,righty:a12,start:b36,x:b0,y:b12,platform:Mac OS X,
03000000d8140000cecf000000000000,MC Cthulhu,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,lefttrigger:b6,rightshoulder:b5,righttrigger:b7,start:b9,x:b0,y:b3,platform:Mac OS X,
030000001008000001e5000006010000,NEXT SNES Controller,a:b2,b:b1,back:b8,dpdown:+a1,dpleft:-a0,dpright:+a0,dpup:-a1,leftshoulder:b4,rightshoulder:b6,start:b9,x:b3,y:b0,platform:Mac OS X,
030000007e0500000920000000000000,Nintendo Switch Pro Controller,a:b0,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b2,y:b3,platform:Mac OS X,
030000008f0e00000300000000000000,Piranha xtreme,a:b2,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftstick:b10,lefttrigger:b4,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b11,righttrigger:b5,rightx:a3,righty:a2,start:b9,x:b3,y:b0,platform:Mac OS X,
03000000d62000006dca000000010000,PowerA Pro Ex,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Mac OS X,
030000004c0500006802000000000000,PS3 Controller,a:b14,b:b13,back:b0,dpdown:b6,dpleft:b7,dpright:b5,dpup:b4,guide:b16,leftshoulder:b10,leftstick:b1,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b11,rightstick:b2,righttrigger:b9,rightx:a2,righty:a3,start:b3,x:b15,y:b12,platform:Mac OS X,
030000004c0500006802000000010000,PS3 Controller,a:b14,b:b13,back:b0,dpdown:b6,dpleft:b7,dpright:b5,dpup:b4,guide:b16,leftshoulder:b10,leftstick:b1,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b11,rightstick:b2,righttrigger:b9,rightx:a2,righty:a3,start:b3,x:b15,y:b12,platform:Mac OS X,
030000004c050000a00b000000010000,PS4 Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Mac OS X,
030000004c050000c405000000000000,PS4 Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Mac OS X,
030000004c050000c405000000010000,PS4 Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Mac OS X,
030000004c050000cc09000000010000,PS4 Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Mac OS X,
030000008916000000fd000000000000,Razer Onza TE,a:b0,b:b1,back:b9,dpdown:b12,dpleft:b13,dpright:b14,dpup:b11,guide:b10,leftshoulder:b4,leftstick:b6,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b7,righttrigger:a5,rightx:a3,righty:a4,start:b8,x:b2,y:b3,platform:Mac OS X,
03000000321500000010000000010000,Razer RAIJU,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Mac OS X,
0300000032150000030a000000000000,Razer Wildcat,a:b0,b:b1,back:b9,dpdown:b12,dpleft:b13,dpright:b14,dpup:b11,guide:b10,leftshoulder:b4,leftstick:b6,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b7,righttrigger:a5,rightx:a3,righty:a4,start:b8,x:b2,y:b3,platform:Mac OS X,
03000000790000001100000000000000,Retrolink Classic Controller,a:b2,b:b1,back:b8,leftshoulder:b4,leftx:a3,lefty:a4,rightshoulder:b5,start:b9,x:b3,y:b0,platform:Mac OS X,
03000000790000001100000006010000,Retrolink SNES Controller,a:b2,b:b1,back:b8,dpdown:+a4,dpleft:-a3,dpright:+a3,dpup:-a4,leftshoulder:b4,rightshoulder:b5,start:b9,x:b3,y:b0,platform:Mac OS X,
030000006b140000010d000000010000,Revolution Pro Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Mac OS X,
03000000c6240000fefa000000000000,Rock Candy Gamepad for PS3,a:b0,b:b1,back:b9,dpdown:b12,dpleft:b13,dpright:b14,dpup:b11,guide:b10,leftshoulder:b4,leftstick:b6,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b7,righttrigger:a5,rightx:a3,righty:a4,start:b8,x:b2,y:b3,platform:Mac OS X,
03000000811700007e05000000000000,Sega Saturn,a:b2,b:b4,dpdown:b16,dpleft:b15,dpright:b14,dpup:b17,leftshoulder:b8,lefttrigger:a5,leftx:a0,lefty:a2,rightshoulder:b9,righttrigger:a4,start:b13,x:b0,y:b6,platform:Mac OS X,
03000000b40400000a01000000000000,Sega Saturn USB Gamepad,a:b0,b:b1,back:b5,guide:b2,leftshoulder:b6,leftx:a0,lefty:a1,rightshoulder:b7,start:b8,x:b3,y:b4,platform:Mac OS X,
030000003512000021ab000000000000,SFC30 Joystick,a:b1,b:b0,back:b10,leftshoulder:b6,leftx:a0,lefty:a1,rightshoulder:b7,start:b11,x:b4,y:b3,platform:Mac OS X,
030000004c050000cc09000000000000,Sony DualShock 4 V2,a:b1,b:b2,back:b13,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Mac OS X,
030000004c050000a00b000000000000,Sony DualShock 4 Wireless Adaptor,a:b1,b:b2,back:b13,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Mac OS X,
030000005e0400008e02000001000000,Steam Virtual GamePad,a:b0,b:b1,back:b9,dpdown:b12,dpleft:b13,dpright:b14,dpup:b11,leftshoulder:b4,leftstick:b6,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b7,righttrigger:a5,rightx:a3,righty:a4,start:b8,x:b2,y:b3,platform:Mac OS X,
03000000110100002014000000000000,SteelSeries Nimbus,a:b0,b:b1,dpdown:b9,dpleft:b11,dpright:b10,dpup:b8,leftshoulder:b4,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,righttrigger:b7,rightx:a2,righty:a3,start:b12,x:b2,y:b3,platform:Mac OS X,
03000000110100002014000001000000,SteelSeries Nimbus,a:b0,b:b1,dpdown:b9,dpleft:b11,dpright:b10,dpup:b8,guide:b12,leftshoulder:b4,lefttrigger:b6,leftx:a0,lefty:a1~,rightshoulder:b5,righttrigger:b7,rightx:a2,righty:a3~,x:b2,y:b3,platform:Mac OS X,
03000000381000002014000001000000,SteelSeries Nimbus,a:b0,b:b1,dpdown:b9,dpleft:b11,dpright:b10,dpup:b8,guide:b12,leftshoulder:b4,lefttrigger:b6,leftx:a0,lefty:a1~,rightshoulder:b5,righttrigger:b7,rightx:a2,righty:a3~,x:b2,y:b3,platform:Mac OS X,
03000000110100001714000000000000,SteelSeries Stratus XL,a:b0,b:b1,dpdown:b9,dpleft:b11,dpright:b10,dpup:b8,leftshoulder:b4,lefttrigger:b6,leftx:a0,lefty:a1~,rightshoulder:b5,righttrigger:b7,rightx:a2,righty:a3~,start:b12,x:b2,y:b3,platform:Mac OS X,
03000000110100001714000020010000,SteelSeries Stratus XL,a:b0,b:b1,dpdown:b9,dpleft:b11,dpright:b10,dpup:b8,leftshoulder:b4,lefttrigger:b6,leftx:a0,lefty:a1~,rightshoulder:b5,righttrigger:b7,rightx:a2,righty:a3~,start:b12,x:b2,y:b3,platform:Mac OS X,
030000004f04000015b3000000000000,Thrustmaster Dual Analog 3.2,a:b0,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b5,leftx:a0,lefty:a1,rightshoulder:b6,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b1,y:b3,platform:Mac OS X,
030000004f04000000b3000000000000,Thrustmaster Firestorm Dual Power,a:b0,b:b2,back:b9,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b11,lefttrigger:b5,leftx:a0,lefty:a1,rightshoulder:b6,righttrigger:b7,rightx:a2,righty:a3,start:b10,x:b1,y:b3,platform:Mac OS X,
03000000bd12000015d0000000000000,Tomee SNES USB Controller,a:b2,b:b1,back:b8,leftshoulder:b4,leftx:a0,lefty:a1,rightshoulder:b5,start:b9,x:b3,y:b0,platform:Mac OS X,
03000000bd12000015d0000000010000,Tomee SNES USB Controller,a:b2,b:b1,back:b8,dpdown:+a1,dpleft:-a0,dpright:+a0,dpup:-a1,leftshoulder:b4,rightshoulder:b5,start:b9,x:b3,y:b0,platform:Mac OS X,
03000000100800000100000000000000,Twin USB Joystick,a:b4,b:b2,back:b16,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b12,leftstick:b20,lefttrigger:b8,leftx:a0,lefty:a2,rightshoulder:b14,rightstick:b22,righttrigger:b10,rightx:a6,righty:a4,start:b18,x:b6,y:b0,platform:Mac OS X,
050000005769696d6f74652028303000,Wii Remote,a:b4,b:b5,back:b7,dpdown:b3,dpleft:b0,dpright:b1,dpup:b2,guide:b8,leftshoulder:b11,lefttrigger:b12,leftx:a0,lefty:a1,start:b6,x:b10,y:b9,platform:Mac OS X,
050000005769696d6f74652028313800,Wii U Pro Controller,a:b16,b:b15,back:b7,dpdown:b12,dpleft:b13,dpright:b14,dpup:b11,guide:b8,leftshoulder:b19,leftstick:b23,lefttrigger:b21,leftx:a0,lefty:a1,rightshoulder:b20,rightstick:b24,righttrigger:b22,rightx:a2,righty:a3,start:b6,x:b18,y:b17,platform:Mac OS X,
030000005e0400008e02000000000000,X360 Controller,a:b0,b:b1,back:b9,dpdown:b12,dpleft:b13,dpright:b14,dpup:b11,guide:b10,leftshoulder:b4,leftstick:b6,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b7,righttrigger:a5,rightx:a3,righty:a4,start:b8,x:b2,y:b3,platform:Mac OS X,
03000000c6240000045d000000000000,Xbox 360 Wired Controller,a:b0,b:b1,back:b9,dpdown:b12,dpleft:b13,dpright:b14,dpup:b11,guide:b10,leftshoulder:b4,leftstick:b6,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b7,righttrigger:a5,rightx:a3,righty:a4,start:b8,x:b2,y:b3,platform:Mac OS X,
030000005e040000d102000000000000,Xbox One Wired Controller,a:b0,b:b1,back:b9,dpdown:b12,dpleft:b13,dpright:b14,dpup:b11,guide:b10,leftshoulder:b4,leftstick:b6,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b7,righttrigger:a5,rightx:a3,righty:a4,start:b8,x:b2,y:b3,platform:Mac OS X,
030000005e040000dd02000000000000,Xbox One Wired Controller,a:b0,b:b1,back:b9,dpdown:b12,dpleft:b13,dpright:b14,dpup:b11,guide:b10,leftshoulder:b4,leftstick:b6,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b7,righttrigger:a5,rightx:a3,righty:a4,start:b8,x:b2,y:b3,platform:Mac OS X,
030000005e040000e302000000000000,Xbox One Wired Controller,a:b0,b:b1,back:b9,dpdown:b12,dpleft:b13,dpright:b14,dpup:b11,guide:b10,leftshoulder:b4,leftstick:b6,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b7,righttrigger:a5,rightx:a3,righty:a4,start:b8,x:b2,y:b3,platform:Mac OS X,
030000005e040000e002000000000000,Xbox Wireless Controller,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b10,leftshoulder:b4,leftstick:b8,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b9,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Mac OS X,
030000005e040000e002000003090000,Xbox Wireless Controller,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b10,leftshoulder:b4,leftstick:b8,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b9,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Mac OS X,
030000005e040000ea02000000000000,Xbox Wireless Controller,a:b0,b:b1,back:b9,dpdown:b12,dpleft:b13,dpright:b14,dpup:b11,guide:b10,leftshoulder:b4,leftstick:b6,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b7,righttrigger:a5,rightx:a3,righty:a4,start:b8,x:b2,y:b3,platform:Mac OS X,
030000005e040000fd02000003090000,Xbox Wireless Controller,a:b0,b:b1,back:b16,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b15,leftshoulder:b6,leftstick:b13,lefttrigger:a5,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:a4,rightx:a2,righty:a3,start:b11,x:b3,y:b4,platform:Mac OS X,
03000000172700004431000029010000,XiaoMi Game Controller,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b15,leftshoulder:b6,leftstick:b13,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:a6,rightx:a2,righty:a5,start:b11,x:b3,y:b4,platform:Mac OS X,
03000000120c0000100e000000010000,ZEROPLUS P4 Gamepad,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Mac OS X,
# Linux
03000000022000000090000011010000,8Bitdo NES30 Pro,a:b1,b:b0,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftstick:b13,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:b9,rightx:a2,righty:a3,start:b11,x:b4,y:b3,platform:Linux,
05000000203800000900000000010000,8Bitdo NES30 Pro,a:b1,b:b0,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftstick:b13,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:b9,rightx:a2,righty:a3,start:b11,x:b4,y:b3,platform:Linux,
05000000c82d00002038000000010000,8Bitdo NES30 Pro,a:b1,b:b0,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b2,leftshoulder:b6,leftstick:b13,lefttrigger:a5,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:a4,rightx:a2,righty:a3,start:b11,x:b4,y:b3,platform:Linux,
03000000c82d00000190000011010000,8Bitdo NES30 Pro 8Bitdo NES30 Pro,a:b1,b:b0,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftstick:b13,lefttrigger:a4,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:a5,rightx:a2,righty:a3,start:b11,x:b4,y:b3,platform:Linux,
05000000102800000900000000010000,8Bitdo SFC30 GamePad,a:b1,b:b0,back:b10,leftshoulder:b6,leftx:a0,lefty:a1,rightshoulder:b7,start:b11,x:b4,y:b3,platform:Linux,
05000000c82d00003028000000010000,8Bitdo SFC30 GamePad,a:b1,b:b0,back:b10,leftshoulder:b6,leftx:a0,lefty:a1,rightshoulder:b7,start:b11,x:b4,y:b3,platform:Linux,
05000000a00500003232000001000000,8Bitdo Zero GamePad,a:b0,b:b1,back:b10,leftshoulder:b6,leftx:a0,lefty:a1,rightshoulder:b7,start:b11,x:b3,y:b4,platform:Linux,
05000000a00500003232000008010000,8Bitdo Zero GamePad,a:b0,b:b1,back:b10,dpdown:+a1,dpleft:-a0,dpright:+a0,dpup:-a1,leftshoulder:b6,rightshoulder:b7,start:b11,x:b3,y:b4,platform:Linux,
030000006f0e00003901000000430000,Afterglow Prismatic Wired Controller,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux,
030000006f0e00003901000020060000,Afterglow Wired Controller for Xbox One,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux,
03000000100000008200000011010000,Akishop Customs PS360+ v1.66,a:b1,b:b2,back:b12,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,lefttrigger:b6,rightshoulder:b5,righttrigger:b7,start:b9,x:b0,y:b3,platform:Linux,
05000000050b00000045000031000000,ASUS Gamepad,a:b0,b:b1,back:b9,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b6,leftshoulder:b4,leftstick:b7,lefttrigger:a5,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b8,righttrigger:a4,rightx:a2,righty:a3,start:b10,x:b2,y:b3,platform:Linux,
03000000666600006706000000010000,boom PSX to PC Converter,a:b2,b:b1,back:b8,dpdown:b14,dpleft:b15,dpright:b13,dpup:b12,leftshoulder:b6,leftstick:b9,lefttrigger:b4,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b10,righttrigger:b5,rightx:a2,righty:a3,start:b11,x:b3,y:b0,platform:Linux,
03000000e82000006058000001010000,Cideko AK08b,a:b2,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b3,y:b0,platform:Linux,
03000000260900008888000000010000,Cyber Gadget GameCube Controller,a:b0,b:b1,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,lefttrigger:a4,leftx:a0,lefty:a1,rightshoulder:b6,righttrigger:a5,rightx:a2,righty:a3~,start:b7,x:b2,y:b3,platform:Linux,
03000000a306000022f6000011010000,Cyborg V.3 Rumble Pad,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:+a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:-a3,rightx:a2,righty:a4,start:b9,x:b0,y:b3,platform:Linux,
03000000b40400000a01000000010000,CYPRESS USB Gamepad,a:b0,b:b1,back:b5,guide:b2,leftshoulder:b6,leftx:a0,lefty:a1,rightshoulder:b7,start:b8,x:b3,y:b4,platform:Linux,
03000000790000000600000010010000,DragonRise Inc. Generic USB Joystick,a:b2,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a3,righty:a4,start:b9,x:b3,y:b0,platform:Linux,
030000006f0e00003001000001010000,EA Sports PS3 Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Linux,
03000000341a000005f7000010010000,GameCube {HuiJia USB box},a:b1,b:b2,dpdown:b14,dpleft:b15,dpright:b13,dpup:b12,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b7,righttrigger:a4,rightx:a5,righty:a2,start:b9,x:b0,y:b3,platform:Linux,
0500000047532047616d657061640000,GameStop Gamepad,a:b0,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b2,y:b3,platform:Linux,
030000006f0e00000104000000010000,Gamestop Logic3 Controller,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux,
030000006f0e00001304000000010000,Generic X-Box pad,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:a0,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:a3,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux,
030000006f0e00001f01000000010000,Generic X-Box pad,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux,
03000000f0250000c183000010010000,Goodbetterbest Ltd USB Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Linux,
0300000079000000d418000000010000,GPD Win 2 Controller,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux,
03000000280400000140000000010000,Gravis GamePad Pro USB ,a:b1,b:b2,back:b8,leftshoulder:b4,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,righttrigger:b7,start:b9,x:b0,y:b3,platform:Linux,
030000008f0e00000610000000010000,GreenAsia Electronics 4Axes 12Keys GamePad ,a:b2,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftstick:b9,lefttrigger:b4,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b10,righttrigger:b5,rightx:a3,righty:a2,start:b11,x:b3,y:b0,platform:Linux,
030000008f0e00000300000010010000,GreenAsia Inc. USB Joystick,a:b2,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftstick:b10,lefttrigger:b4,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b11,righttrigger:b5,rightx:a3,righty:a2,start:b9,x:b3,y:b0,platform:Linux,
030000008f0e00001200000010010000,GreenAsia Inc. USB Joystick,a:b0,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b5,leftx:a0,lefty:a1,rightshoulder:b6,rightstick:b11,righttrigger:b7,rightx:a3,righty:a2,start:b9,x:b2,y:b3,platform:Linux,
0500000047532067616d657061640000,GS gamepad,a:b0,b:b1,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b2,y:b3,platform:Linux,
06000000adde0000efbe000002010000,Hidromancer Game Controller,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux,
03000000d81400000862000011010000,HitBox (PS3/PC) Analog Mode,a:b1,b:b2,back:b8,guide:b9,leftshoulder:b4,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,righttrigger:b7,start:b12,x:b0,y:b3,platform:Linux,
03000000c9110000f055000011010000,HJC Game GAMEPAD,a:b0,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b2,y:b3,platform:Linux,
030000000d0f00000d00000000010000,hori,a:b0,b:b6,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b10,leftshoulder:b3,leftx:b4,lefty:b5,rightshoulder:b7,start:b9,x:b1,y:b2,platform:Linux,
030000000d0f00001000000011010000,HORI CO. LTD. FIGHTING STICK 3,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,lefttrigger:b6,rightshoulder:b5,righttrigger:b7,start:b9,x:b0,y:b3,platform:Linux,
030000000d0f00006a00000011010000,HORI CO. LTD. Real Arcade Pro.4,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Linux,
030000000d0f00006b00000011010000,HORI CO. LTD. Real Arcade Pro.4,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Linux,
030000000d0f00002200000011010000,HORI CO. LTD. REAL ARCADE Pro.V3,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,lefttrigger:b6,rightshoulder:b5,righttrigger:b7,start:b9,x:b0,y:b3,platform:Linux,
030000000d0f00005f00000011010000,Hori Fighting Commander 4 (PS3),a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Linux,
030000000d0f00005e00000011010000,Hori Fighting Commander 4 (PS4),a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Linux,
03000000ad1b000001f5000033050000,Hori Pad EX Turbo 2,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux,
030000000d0f00006e00000011010000,HORIPAD 4 (PS3),a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Linux,
030000000d0f00006600000011010000,HORIPAD 4 (PS4),a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Linux,
030000000d0f00006700000001010000,HORIPAD ONE,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux,
030000008f0e00001330000010010000,HuiJia SNES Controller,a:b2,b:b1,back:b8,dpdown:+a1,dpleft:-a0,dpright:+a0,dpup:-a1,leftshoulder:b6,rightshoulder:b7,start:b9,x:b3,y:b0,platform:Linux,
03000000830500006020000010010000,iBuffalo SNES Controller,a:b1,b:b0,back:b6,dpdown:+a1,dpleft:-a0,dpright:+a0,dpup:-a1,leftshoulder:b4,rightshoulder:b5,start:b7,x:b3,y:b2,platform:Linux,
050000006964726f69643a636f6e0000,idroid:con,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Linux,
03000000b50700001503000010010000,impact,a:b2,b:b3,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b5,leftx:a0,lefty:a1,rightshoulder:b6,rightstick:b11,righttrigger:b7,rightx:a3,righty:a2,start:b9,x:b0,y:b1,platform:Linux,
03000000fd0500000030000000010000,InterAct GoPad I-73000 (Fighting Game Layout),a:b3,b:b4,back:b6,leftx:a0,lefty:a1,rightshoulder:b2,righttrigger:b5,start:b7,x:b0,y:b1,platform:Linux,
0500000049190000020400001b010000,Ipega PG-9069 - Bluetooth Gamepad,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b161,leftshoulder:b6,leftstick:b13,lefttrigger:a5,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:a4,rightx:a2,righty:a3,start:b11,x:b3,y:b4,platform:Linux,
030000006e0500000320000010010000,JC-U3613M - DirectInput Mode,a:b2,b:b3,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b8,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b9,righttrigger:b7,rightx:a2,righty:a3,start:b11,x:b0,y:b1,platform:Linux,
03000000300f00001001000010010000,Jess Tech Dual Analog Rumble Pad,a:b2,b:b3,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b5,leftx:a0,lefty:a1,rightshoulder:b6,rightstick:b11,righttrigger:b7,rightx:a3,righty:a2,start:b9,x:b0,y:b1,platform:Linux,
03000000ba2200002010000001010000,Jess Technology USB Game Controller,a:b2,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,righttrigger:b7,rightx:a3,righty:a2,start:b9,x:b3,y:b0,platform:Linux,
030000006f0e00000103000000020000,Logic3 Controller,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux,
030000006d04000019c2000010010000,Logitech Cordless RumblePad 2,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Linux,
030000006d04000016c2000010010000,Logitech Dual Action,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Linux,
030000006d04000016c2000011010000,Logitech Dual Action,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Linux,
030000006d0400001dc2000014400000,Logitech F310 Gamepad (XInput),a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux,
030000006d0400001ec2000020200000,Logitech F510 Gamepad (XInput),a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux,
030000006d04000019c2000011010000,Logitech F710 Gamepad (DInput),a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Linux,
030000006d0400001fc2000005030000,Logitech F710 Gamepad (XInput),a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux,
030000006d04000015c2000010010000,Logitech Logitech Extreme 3D,a:b0,b:b4,back:b6,guide:b8,leftshoulder:b9,leftstick:h0.8,leftx:a0,lefty:a1,rightshoulder:b10,rightstick:h0.2,start:b7,x:b2,y:b5,platform:Linux,
030000006d04000018c2000010010000,Logitech RumblePad 2,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Linux,
030000006d04000011c2000010010000,Logitech WingMan Cordless RumblePad,a:b0,b:b1,back:b2,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b5,leftshoulder:b6,lefttrigger:b9,leftx:a0,lefty:a1,rightshoulder:b7,righttrigger:b10,rightx:a3,righty:a4,start:b8,x:b3,y:b4,platform:Linux,
05000000380700006652000025010000,Mad Catz C.T.R.L.R ,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Linux,
03000000380700005032000011010000,Mad Catz FightPad PRO (PS3),a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Linux,
03000000380700005082000011010000,Mad Catz FightPad PRO (PS4),a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Linux,
03000000ad1b00002ef0000090040000,Mad Catz Fightpad SFxT,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,lefttrigger:a2,rightshoulder:b5,righttrigger:a5,start:b7,x:b2,y:b3,platform:Linux,
03000000380700008034000011010000,Mad Catz fightstick (PS3),a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Linux,
03000000380700008084000011010000,Mad Catz fightstick (PS4),a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Linux,
03000000380700008433000011010000,Mad Catz FightStick TE S+ PS3,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Linux,
03000000380700008483000011010000,Mad Catz FightStick TE S+ PS4,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Linux,
03000000380700001647000010040000,Mad Catz Wired Xbox 360 Controller,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux,
03000000380700003847000090040000,Mad Catz Wired Xbox 360 Controller (SFIV),a:b0,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b10,leftshoulder:b4,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b2,y:b3,platform:Linux,
03000000ad1b000016f0000090040000,Mad Catz Xbox 360 Controller,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux,
03000000380700001888000010010000,MadCatz PC USB Wired Stick 8818,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Linux,
03000000380700003888000010010000,MadCatz PC USB Wired Stick 8838,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:a0,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Linux,
0300000079000000d218000011010000,MAGIC-NS,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Linux,
03000000790000004418000010010000,Mayflash GameCube Controller,a:b1,b:b2,dpdown:b14,dpleft:b15,dpright:b13,dpup:b12,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b7,righttrigger:a4,rightx:a5,righty:a2,start:b9,x:b0,y:b3,platform:Linux,
03000000780000000600000010010000,Microntek USB Joystick,a:b2,b:b1,back:b8,leftshoulder:b6,lefttrigger:b4,leftx:a0,lefty:a1,rightshoulder:b7,righttrigger:b5,start:b9,x:b3,y:b0,platform:Linux,
030000005e0400000e00000000010000,Microsoft SideWinder,a:b0,b:b1,back:b9,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,rightshoulder:b7,start:b8,x:b3,y:b4,platform:Linux,
030000005e0400008e02000004010000,Microsoft X-Box 360 pad,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux,
030000005e0400008e02000062230000,Microsoft X-Box 360 pad,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux,
030000005e040000e302000003020000,Microsoft X-Box One Elite pad,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux,
030000005e040000d102000001010000,Microsoft X-Box One pad,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux,
030000005e040000d102000003020000,Microsoft X-Box One pad v2,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux,
030000005e0400008502000000010000,Microsoft X-Box pad (Japan),a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b5,leftstick:b8,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b2,rightstick:b9,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b3,y:b4,platform:Linux,
030000005e0400008902000021010000,Microsoft X-Box pad v2 (US),a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b5,leftstick:b8,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b2,rightstick:b9,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b3,y:b4,platform:Linux,
05000000d6200000ad0d000001000000,Moga Pro,a:b0,b:b1,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b7,lefttrigger:a5,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b8,righttrigger:a4,rightx:a2,righty:a3,start:b6,x:b2,y:b3,platform:Linux,
03000000250900006688000000010000,MP-8866 Super Dual Box,a:b2,b:b1,back:b9,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftstick:b10,lefttrigger:b4,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b11,righttrigger:b5,rightx:a2,righty:a3,start:b8,x:b3,y:b0,platform:Linux,
030000001008000001e5000010010000,NEXT SNES Controller,a:b2,b:b1,back:b8,dpdown:+a1,dpleft:-a0,dpright:+a0,dpup:-a1,leftshoulder:b4,rightshoulder:b6,start:b9,x:b3,y:b0,platform:Linux,
050000007e0500000920000001000000,Nintendo Switch Pro Controller,a:b0,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b2,y:b3,platform:Linux,
050000007e0500003003000001000000,Nintendo Wii Remote Pro Controller,a:b0,b:b1,back:b8,dpdown:b14,dpleft:b15,dpright:b16,dpup:b13,guide:b10,leftshoulder:b4,leftstick:b11,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b12,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b3,y:b2,platform:Linux,
05000000010000000100000003000000,Nintendo Wiimote,a:b0,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b10,leftshoulder:b4,leftstick:b11,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b12,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b2,y:b3,platform:Linux,
030000000d0500000308000010010000,Nostromo n45 Dual Analog Gamepad,a:b0,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b9,leftshoulder:b4,leftstick:b12,lefttrigger:b5,leftx:a0,lefty:a1,rightshoulder:b6,rightstick:b11,righttrigger:b7,rightx:a3,righty:a2,start:b10,x:b2,y:b3,platform:Linux,
03000000550900001072000011010000,NVIDIA Controller,a:b0,b:b1,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b13,leftshoulder:b4,leftstick:b8,lefttrigger:a5,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b9,righttrigger:a4,rightx:a2,righty:a3,start:b7,x:b2,y:b3,platform:Linux,
03000000451300000830000010010000,NYKO CORE,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Linux,
030000005e0400000202000000010000,Old Xbox pad,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b5,leftstick:b8,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b2,rightstick:b9,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b3,y:b4,platform:Linux,
05000000362800000100000002010000,OUYA Game Controller,a:b0,b:b3,dpdown:b9,dpleft:b10,dpright:b11,dpup:b8,guide:b14,leftshoulder:b4,leftstick:b6,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b7,righttrigger:a5,rightx:a3,righty:a4,x:b1,y:b2,platform:Linux,
05000000362800000100000003010000,OUYA Game Controller,a:b0,b:b3,dpdown:b9,dpleft:b10,dpright:b11,dpup:b8,guide:b14,leftshoulder:b4,leftstick:b6,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b7,righttrigger:a5,rightx:a3,righty:a4,x:b1,y:b2,platform:Linux,
03000000ff1100003133000010010000,PC Game Controller,a:b2,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b3,y:b0,platform:Linux,
030000006f0e00006401000001010000,PDP Battlefield One,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux,
03000000d62000006dca000011010000,PowerA Pro Ex,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Linux,
03000000ff1100004133000010010000,PS2 Controller,a:b2,b:b1,back:b8,leftshoulder:b6,lefttrigger:b4,leftx:a0,lefty:a1,rightshoulder:b7,righttrigger:b5,start:b9,x:b3,y:b0,platform:Linux,
03000000341a00003608000011010000,PS3 Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Linux,
030000004c0500006802000010010000,PS3 Controller,a:b14,b:b13,back:b0,dpdown:b6,dpleft:b7,dpright:b5,dpup:b4,guide:b16,leftshoulder:b10,leftstick:b1,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b11,rightstick:b2,righttrigger:b9,rightx:a2,righty:a3,start:b3,x:b15,y:b12,platform:Linux,
030000004c0500006802000010810000,PS3 Controller,a:b0,b:b1,back:b8,dpdown:b14,dpleft:b15,dpright:b16,dpup:b13,guide:b10,leftshoulder:b4,leftstick:b11,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b12,righttrigger:a5,rightx:a3,righty:a4,start:b9,x:b3,y:b2,platform:Linux,
030000004c0500006802000011010000,PS3 Controller,a:b14,b:b13,back:b0,dpdown:b6,dpleft:b7,dpright:b5,dpup:b4,guide:b16,leftshoulder:b10,leftstick:b1,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b11,rightstick:b2,righttrigger:b9,rightx:a2,righty:a3,start:b3,x:b15,y:b12,platform:Linux,
030000004c0500006802000011810000,PS3 Controller,a:b0,b:b1,back:b8,dpdown:b14,dpleft:b15,dpright:b16,dpup:b13,guide:b10,leftshoulder:b4,leftstick:b11,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b12,righttrigger:a5,rightx:a3,righty:a4,start:b9,x:b3,y:b2,platform:Linux,
050000004c0500006802000000010000,PS3 Controller,a:b14,b:b13,back:b0,dpdown:b6,dpleft:b7,dpright:b5,dpup:b4,guide:b16,leftshoulder:b10,leftstick:b1,lefttrigger:a12,leftx:a0,lefty:a1,rightshoulder:b11,rightstick:b2,righttrigger:a13,rightx:a2,righty:a3,start:b3,x:b15,y:b12,platform:Linux,
050000004c0500006802000000810000,PS3 Controller,a:b0,b:b1,back:b8,dpdown:b14,dpleft:b15,dpright:b16,dpup:b13,guide:b10,leftshoulder:b4,leftstick:b11,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b12,righttrigger:a5,rightx:a3,righty:a4,start:b9,x:b3,y:b2,platform:Linux,
050000004c0500006802000000800000,PS3 Controller (Bluetooth),a:b0,b:b1,back:b8,dpdown:b14,dpleft:b15,dpright:b16,dpup:b13,guide:b10,leftshoulder:b4,leftstick:b11,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b12,righttrigger:a5,rightx:a3,righty:a4,start:b9,x:b3,y:b2,platform:Linux,
05000000504c415953544154494f4e00,PS3 Controller (Bluetooth),a:b14,b:b13,back:b0,dpdown:b6,dpleft:b7,dpright:b5,dpup:b4,guide:b16,leftshoulder:b10,leftstick:b1,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b11,rightstick:b2,righttrigger:b9,rightx:a2,righty:a3,start:b3,x:b15,y:b12,platform:Linux,
060000004c0500006802000000010000,PS3 Controller (Bluetooth),a:b14,b:b13,back:b0,dpdown:b6,dpleft:b7,dpright:b5,dpup:b4,guide:b16,leftshoulder:b10,leftstick:b1,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b11,rightstick:b2,righttrigger:b9,rightx:a2,righty:a3,start:b3,x:b15,y:b12,platform:Linux,
030000004c050000a00b000011010000,PS4 Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Linux,
030000004c050000a00b000011810000,PS4 Controller,a:b0,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b10,leftshoulder:b4,leftstick:b11,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b12,righttrigger:a5,rightx:a3,righty:a4,start:b9,x:b3,y:b2,platform:Linux,
030000004c050000c405000011010000,PS4 Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Linux,
030000004c050000c405000011810000,PS4 Controller,a:b0,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b10,leftshoulder:b4,leftstick:b11,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b12,righttrigger:a5,rightx:a3,righty:a4,start:b9,x:b3,y:b2,platform:Linux,
030000004c050000cc09000011010000,PS4 Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Linux,
030000004c050000cc09000011810000,PS4 Controller,a:b0,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b10,leftshoulder:b4,leftstick:b11,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b12,righttrigger:a5,rightx:a3,righty:a4,start:b9,x:b3,y:b2,platform:Linux,
050000004c050000c405000000010000,PS4 Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Linux,
050000004c050000c405000000810000,PS4 Controller,a:b0,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b10,leftshoulder:b4,leftstick:b11,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b12,righttrigger:a5,rightx:a3,righty:a4,start:b9,x:b3,y:b2,platform:Linux,
050000004c050000cc09000000010000,PS4 Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Linux,
050000004c050000cc09000000810000,PS4 Controller,a:b0,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b10,leftshoulder:b4,leftstick:b11,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b12,righttrigger:a5,rightx:a3,righty:a4,start:b9,x:b3,y:b2,platform:Linux,
03000000300f00001211000011010000,QanBa Arcade JoyStick,a:b2,b:b0,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b5,lefttrigger:b4,leftx:a0,lefty:a1,rightshoulder:b7,righttrigger:b6,start:b9,x:b1,y:b3,platform:Linux,
030000009b2800000300000001010000,raphnet.net 4nes4snes v1.5,a:b0,b:b4,back:b2,leftshoulder:b6,leftx:a0,lefty:a1,rightshoulder:b7,start:b3,x:b1,y:b5,platform:Linux,
030000008916000001fd000024010000,Razer Onza Classic Edition,a:b0,b:b1,back:b6,dpdown:b14,dpleft:b11,dpright:b12,dpup:b13,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux,
030000008916000000fd000024010000,Razer Onza Tournament,a:b0,b:b1,back:b6,dpdown:b14,dpleft:b11,dpright:b12,dpup:b13,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux,
03000000321500000010000011010000,Razer RAIJU,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Linux,
03000000c6240000045d000025010000,Razer Sabertooth,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux,
03000000321500000009000011010000,Razer Serval,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a5,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a4,rightx:a2,righty:a3,start:b7,x:b2,y:b3,platform:Linux,
050000003215000000090000163a0000,Razer Serval,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a5,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a4,rightx:a2,righty:a3,start:b7,x:b2,y:b3,platform:Linux,
0300000032150000030a000001010000,Razer Wildcat,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux,
03000000790000001100000010010000,Retrolink SNES Controller,a:b2,b:b1,back:b8,dpdown:+a1,dpleft:-a0,dpright:+a0,dpup:-a1,leftshoulder:b4,rightshoulder:b5,start:b9,x:b3,y:b0,platform:Linux,
0300000000f000000300000000010000,RetroUSB.com RetroPad,a:b1,b:b5,back:b2,leftshoulder:b6,leftx:a0,lefty:a1,rightshoulder:b7,start:b3,x:b0,y:b4,platform:Linux,
0300000000f00000f100000000010000,RetroUSB.com Super RetroPort,a:b1,b:b5,back:b2,leftshoulder:b6,leftx:a0,lefty:a1,rightshoulder:b7,start:b3,x:b0,y:b4,platform:Linux,
030000006b140000010d000011010000,Revolution Pro Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Linux,
030000006f0e00001e01000011010000,Rock Candy Gamepad for PS3,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Linux,
030000006f0e00004601000001010000,Rock Candy Wired Controller for Xbox One,a:b0,b:b1,back:b6,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux,
03000000a306000023f6000011010000,Saitek Cyborg V.1 Game Pad,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a4,start:b9,x:b0,y:b3,platform:Linux,
03000000a30600000cff000010010000,Saitek P2500 Force Rumble Pad,a:b2,b:b3,back:b11,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b10,leftshoulder:b4,leftstick:b8,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b9,righttrigger:b7,rightx:a3,righty:a2,x:b0,y:b1,platform:Linux,
03000000a30600000c04000011010000,Saitek P2900 Wireless Pad,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b9,leftshoulder:b6,leftstick:b10,lefttrigger:b4,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b11,righttrigger:b5,rightx:a3,righty:a2,start:b12,x:b0,y:b3,platform:Linux,
03000000a30600000901000000010000,Saitek P880,a:b2,b:b3,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b8,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b9,righttrigger:b7,rightx:a3,righty:a2,x:b0,y:b1,platform:Linux,
03000000a30600000b04000000010000,Saitek P990 Dual Analog Pad,a:b1,b:b2,back:b9,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a3,righty:a2,start:b8,x:b0,y:b3,platform:Linux,
03000000a306000018f5000010010000,Saitek PLC Saitek P3200 Rumble Pad,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a3,righty:a4,start:b9,x:b0,y:b3,platform:Linux,
03000000c01600008704000011010000,Serial/Keyboard/Mouse/Joystick,a:b12,b:b10,back:b4,dpdown:b2,dpleft:b3,dpright:b1,dpup:b0,leftshoulder:b9,leftstick:b14,lefttrigger:b6,leftx:a1,lefty:a0,rightshoulder:b8,rightstick:b15,righttrigger:b7,rightx:a2,righty:a3,start:b5,x:b13,y:b11,platform:Linux,
03000000f025000021c1000010010000,ShanWan Gioteck PS3 Wired Controller,a:b2,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b3,y:b0,platform:Linux,
03000000632500007505000010010000,SHANWAN PS3/PC Gamepad,a:b2,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b3,y:b0,platform:Linux,
03000000250900000500000000010000,Sony PS2 pad with SmartJoy adapter,a:b2,b:b1,back:b9,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftstick:b10,lefttrigger:b4,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b11,righttrigger:b5,rightx:a2,righty:a3,start:b8,x:b3,y:b0,platform:Linux,
030000005e0400008e02000073050000,Speedlink TORID Wireless Gamepad,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux,
030000005e0400008e02000020200000,SpeedLink XEOX Pro Analog Gamepad pad,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux,
03000000de2800000112000001000000,Steam Controller,a:b0,b:b1,back:b6,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,righttrigger:a3,start:b7,x:b2,y:b3,platform:Linux,
03000000de2800000211000001000000,Steam Controller,a:b0,b:b1,back:b6,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,righttrigger:a3,start:b7,x:b2,y:b3,platform:Linux,
03000000de2800004211000001000000,Steam Controller,a:b0,b:b1,back:b6,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,righttrigger:a3,start:b7,x:b2,y:b3,platform:Linux,
03000000de280000fc11000001000000,Steam Controller,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux,
05000000de2800000212000001000000,Steam Controller,a:b0,b:b1,back:b6,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,righttrigger:a3,start:b7,x:b2,y:b3,platform:Linux,
05000000de2800000511000001000000,Steam Controller,a:b0,b:b1,back:b6,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,righttrigger:a3,start:b7,x:b2,y:b3,platform:Linux,
03000000de280000ff11000001000000,Steam Virtual Gamepad,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux,
03000000ad1b000038f0000090040000,Street Fighter IV FightStick TE,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux,
03000000666600000488000000010000,Super Joy Box 5 Pro,a:b2,b:b1,back:b9,dpdown:b14,dpleft:b15,dpright:b13,dpup:b12,leftshoulder:b6,leftstick:b10,lefttrigger:b4,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b11,righttrigger:b5,rightx:a2,righty:a3,start:b8,x:b3,y:b0,platform:Linux,
030000004f04000020b3000010010000,Thrustmaster 2 in 1 DT,a:b0,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b5,leftx:a0,lefty:a1,rightshoulder:b6,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b1,y:b3,platform:Linux,
030000004f04000015b3000010010000,Thrustmaster Dual Analog 4,a:b0,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b5,leftx:a0,lefty:a1,rightshoulder:b6,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b1,y:b3,platform:Linux,
030000004f04000023b3000000010000,Thrustmaster Dual Trigger 3-in-1,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Linux,
030000004f04000000b3000010010000,Thrustmaster Firestorm Dual Power,a:b0,b:b2,back:b9,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b11,lefttrigger:b5,leftx:a0,lefty:a1,rightshoulder:b6,rightstick:b12,righttrigger:b7,rightx:a2,righty:a3,start:b10,x:b1,y:b3,platform:Linux,
030000004f04000008d0000000010000,Thrustmaster Run N Drive Wireless,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Linux,
030000004f04000009d0000000010000,Thrustmaster Run N Drive Wireless PS3,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Linux,
03000000bd12000015d0000010010000,Tomee SNES USB Controller,a:b2,b:b1,back:b8,dpdown:+a1,dpleft:-a0,dpright:+a0,dpup:-a1,leftshoulder:b4,rightshoulder:b5,start:b9,x:b3,y:b0,platform:Linux,
03000000d814000007cd000011010000,Toodles 2008 Chimp PC/PS3,a:b0,b:b1,back:b8,leftshoulder:b4,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,righttrigger:b7,start:b9,x:b3,y:b2,platform:Linux,
03000000100800000100000010010000,Twin USB PS2 Adapter,a:b2,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftstick:b10,lefttrigger:b4,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b11,righttrigger:b5,rightx:a3,righty:a2,start:b9,x:b3,y:b0,platform:Linux,
03000000100800000300000010010000,USB Gamepad,a:b2,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftstick:b10,lefttrigger:b4,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b11,righttrigger:b5,rightx:a3,righty:a2,start:b9,x:b3,y:b0,platform:Linux,
03000000790000001100000000010000,USB Gamepad1,a:b2,b:b1,back:b8,dpdown:a0,dpleft:a1,dpright:a2,dpup:a4,start:b9,platform:Linux,
05000000ac0500003232000001000000,VR-BOX,a:b0,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftstick:b10,lefttrigger:b4,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b11,righttrigger:b5,rightx:a3,righty:a2,start:b9,x:b2,y:b3,platform:Linux,
030000005e0400008e02000010010000,X360 Controller,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux,
030000005e0400008e02000014010000,X360 Controller,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux,
030000005e0400001907000000010000,X360 Wireless Controller,a:b0,b:b1,back:b6,dpdown:b14,dpleft:b11,dpright:b12,dpup:b13,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux,
030000005e0400009102000007010000,X360 Wireless Controller,a:b0,b:b1,back:b6,dpdown:b14,dpleft:b11,dpright:b12,dpup:b13,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux,
030000005e040000a102000000010000,X360 Wireless Controller,a:b0,b:b1,back:b6,dpdown:b14,dpleft:b11,dpright:b12,dpup:b13,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux,
030000005e040000a102000007010000,X360 Wireless Controller,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux,
0000000058626f782033363020576900,Xbox 360 Wireless Controller,a:b0,b:b1,back:b14,dpdown:b11,dpleft:b12,dpright:b13,dpup:b10,guide:b7,leftshoulder:b4,leftstick:b8,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b9,righttrigger:a5,rightx:a3,righty:a4,start:b6,x:b2,y:b3,platform:Linux,
030000005e040000a102000014010000,Xbox 360 Wireless Receiver (XBOX),a:b0,b:b1,back:b6,dpdown:b14,dpleft:b11,dpright:b12,dpup:b13,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux,
0000000058626f782047616d65706100,Xbox Gamepad (userspace driver),a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a5,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a4,rightx:a2,righty:a3,start:b7,x:b2,y:b3,platform:Linux,
050000005e040000e002000003090000,Xbox One Wireless Controller,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b10,leftshoulder:b4,leftstick:b8,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b9,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux,
050000005e040000fd02000003090000,Xbox One Wireless Controller,a:b0,b:b1,back:b15,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b16,leftshoulder:b6,leftstick:b13,lefttrigger:a5,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:a4,rightx:a2,righty:a3,start:b11,x:b3,y:b4,platform:Linux,
03000000450c00002043000010010000,XEOX Gamepad SL-6556-BK,a:b0,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b2,y:b3,platform:Linux,
05000000172700004431000029010000,XiaoMi Game Controller,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b20,leftshoulder:b6,leftstick:b13,lefttrigger:a7,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:a6,rightx:a2,righty:a5,start:b11,x:b3,y:b4,platform:Linux,
03000000c0160000e105000001010000,Xin-Mo Xin-Mo Dual Arcade,a:b4,b:b3,back:b6,dpdown:b12,dpleft:b13,dpright:b14,dpup:b11,guide:b9,leftshoulder:b2,leftx:a0,lefty:a1,rightshoulder:b5,start:b7,x:b1,y:b0,platform:Linux,
xinput,XInput Controller,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux,
03000000120c0000100e000011010000,ZEROPLUS P4 Gamepad,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Linux,
# Android
64633436313965656664373634323364,Microsoft X-Box 360 pad,a:b0,b:b1,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b9,leftstick:b7,lefttrigger:a4,leftx:a0,lefty:a1,rightshoulder:b10,rightstick:b8,righttrigger:a5,rightx:a2,righty:a3,x:b2,y:b3,platform:Android,
37336435666338653565313731303834,NVIDIA Controller,a:b0,b:b1,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b9,leftstick:b7,lefttrigger:a4,leftx:a0,lefty:a1,rightshoulder:b10,rightstick:b8,righttrigger:a5,rightx:a2,righty:a3,start:b6,x:b2,y:b3,platform:Android,
4e564944494120436f72706f72617469,NVIDIA Controller,a:b0,b:b1,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b9,leftstick:b7,lefttrigger:a4,leftx:a0,lefty:a1,rightshoulder:b10,rightstick:b8,righttrigger:a5,rightx:a2,righty:a3,start:b6,x:b2,y:b3,platform:Android,
61363931656135336130663561616264,NVIDIA Controller,a:b0,b:b1,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b9,leftstick:b7,lefttrigger:a4,leftx:a0,lefty:a1,rightshoulder:b10,rightstick:b8,righttrigger:a5,rightx:a2,righty:a3,start:b6,x:b2,y:b3,platform:Android,
35643031303033326130316330353564,PS4 Controller,a:b1,b:b17,back:b15,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b5,leftshoulder:b3,leftstick:b4,lefttrigger:+a3,leftx:a0,lefty:a1,rightshoulder:b18,rightstick:b6,righttrigger:+a4,rightx:a2,righty:a5,start:b16,x:b0,y:b2,platform:Android,
05000000de2800000511000001000000,Steam Controller,a:b0,b:b1,back:b6,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,righttrigger:a3,start:b7,x:b2,y:b3,platform:Android,
5477696e20555342204a6f7973746963,Twin USB Joystick,a:b22,b:b21,back:b28,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b26,leftstick:b30,lefttrigger:b24,leftx:a0,lefty:a1,rightshoulder:b27,rightstick:b31,righttrigger:b25,rightx:a3,righty:a2,start:b29,x:b23,y:b20,platform:Android,
34356136633366613530316338376136,Xbox Wireless Controller,a:b0,b:b1,back:b9,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b10,leftshoulder:b3,leftstick:b15,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b18,rightstick:b16,righttrigger:a5,rightx:a3,righty:a4,x:b17,y:b2,platform:Android,
# iOS
4d466947616d65706164010000000000,MFi Extended Gamepad,a:b0,b:b1,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,righttrigger:a5,rightx:a3,righty:a4,start:b6,x:b2,y:b3,platform:iOS,
4d466947616d65706164020000000000,MFi Gamepad,a:b0,b:b1,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,rightshoulder:b5,start:b6,x:b2,y:b3,platform:iOS,
05000000de2800000511000001000000,Steam Controller,a:b0,b:b1,back:b6,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,righttrigger:a3,start:b7,x:b2,y:b3,platform:iOS,
+49 -11
View File
@@ -3,14 +3,16 @@ import QtQuick.Dialogs 1.2
import QtQuick.Controls 2.2
import AppModel 1.0
import ComputerManager 1.0
import SdlGamepadKeyNavigation 1.0
GridView {
property int computerIndex
property AppModel appModel : createModel()
id: appGrid
focus: true
activeFocusOnTab: true
anchors.fill: parent
anchors.leftMargin: (parent.width % (cellWidth + anchors.rightMargin)) / 2
anchors.topMargin: 20
@@ -29,12 +31,23 @@ GridView {
stackView.pop()
}
Component.onCompleted: {
// Don't show any highlighted item until interacting with them
currentIndex = -1
}
SdlGamepadKeyNavigation {
id: gamepadKeyNav
}
onVisibleChanged: {
if (visible) {
appModel.computerLost.connect(computerLost)
gamepadKeyNav.enable()
}
else {
appModel.computerLost.disconnect(computerLost)
gamepadKeyNav.disable()
}
}
@@ -47,8 +60,9 @@ GridView {
model: appModel
delegate: ItemDelegate {
delegate: NavigableItemDelegate {
width: 200; height: 335;
grid: appGrid
Image {
id: appIcon
@@ -59,6 +73,9 @@ GridView {
width: 150
height: 200
}
width: sourceSize.width
height: sourceSize.height
fillMode: Image.Pad
}
Image {
@@ -72,10 +89,9 @@ GridView {
}
}
Text {
Label {
id: appNameText
text: model.name
color: "white"
width: parent.width
height: 125
anchors.top: appIcon.bottom
@@ -83,6 +99,12 @@ GridView {
horizontalAlignment: Text.AlignHCenter
wrapMode: Text.Wrap
elide: Text.ElideRight
// Display a tooltip with the full name if it's truncated
ToolTip.text: model.name
ToolTip.delay: 1000
ToolTip.timeout: 5000
ToolTip.visible: (parent.hovered || parent.highlighted) && truncated
}
function launchOrResumeSelectedApp()
@@ -111,22 +133,33 @@ GridView {
anchors.fill: parent
acceptedButtons: Qt.RightButton
onClicked: {
// Right click
appContextMenu.open()
// popup() ensures the menu appears under the mouse cursor
if (appContextMenu.popup) {
appContextMenu.popup()
}
else {
// Qt 5.9 doesn't have popup()
appContextMenu.open()
}
}
}
Keys.onMenuPressed: {
// We must use open() here so the menu is positioned on
// the ItemDelegate and not where the mouse cursor is
appContextMenu.open()
}
Menu {
id: appContextMenu
MenuItem {
NavigableMenuItem {
text: model.running ? "Resume Game" : "Launch Game"
onTriggered: {
appContextMenu.close()
launchOrResumeSelectedApp()
}
height: visible ? implicitHeight : 0
}
MenuItem {
NavigableMenuItem {
text: "Quit Game"
onTriggered: {
quitAppDialog.appName = appModel.getRunningAppName()
@@ -134,7 +167,6 @@ GridView {
quitAppDialog.open()
}
visible: model.running
height: visible ? implicitHeight : 0
}
}
}
@@ -148,7 +180,8 @@ GridView {
property int nextAppIndex: 0
text:"Are you sure you want to quit " + appName +"? Any unsaved progress will be lost."
standardButtons: StandardButton.Yes | StandardButton.No
onYes: {
function quitApp() {
var component = Qt.createComponent("QuitSegue.qml")
var params = {"appName": appName}
if (segueToStream) {
@@ -167,6 +200,11 @@ GridView {
// Trigger the quit after pushing the quit segue on screen
appModel.quitRunningApp()
}
onYes: quitApp()
// For keyboard/gamepad navigation
onAccepted: quitApp()
}
ScrollBar.vertical: ScrollBar {
+31
View File
@@ -0,0 +1,31 @@
import QtQuick 2.9
import QtQuick.Controls 2.2
// https://stackoverflow.com/questions/45029968/how-do-i-set-the-combobox-width-to-fit-the-largest-item
ComboBox {
property int textWidth
implicitWidth: leftPadding + textWidth + indicator.width + rightPadding
TextMetrics {
id: popupMetrics
}
TextMetrics {
id: textMetrics
}
// We call this every time the options change (and init)
// so we can adjust the combo box width here too
onActivated: {
textMetrics.font = font
popupMetrics.font = popup.font
textWidth = 0
for (var i = 0; i < count; i++){
textMetrics.text = textAt(i)
popupMetrics.text = textAt(i)
textWidth = Math.max(textMetrics.width, textWidth)
textWidth = Math.max(popupMetrics.width, textWidth)
}
}
}
+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()
}
}
}
}
+124
View File
@@ -0,0 +1,124 @@
import QtQml 2.2
import QtQuick 2.0
import QtQuick.Controls 2.2
import QtQuick.Dialogs 1.2
import ComputerManager 1.0
Item {
visible: false
function onSearchingComputer() {
stageLabel.text = "Establishing connection to PC..."
}
function onSearchingApp() {
stageLabel.text = "Loading app list..."
}
function onSessionCreated(appName, session) {
var component = Qt.createComponent("StreamSegue.qml")
var segue = component.createObject(stackView, {
"appName": appName,
"session": session,
"quitAfter": true
})
stackView.push(segue)
}
function onLaunchFailed(message) {
errorDialog.text = message
errorDialog.open()
}
function onAppQuitRequired(appName) {
quitAppDialog.appName = appName
quitAppDialog.open()
}
onVisibleChanged: {
if (visible && !launcher.isExecuted()) {
toolBar.visible = false
launcher.searchingComputer.connect(onSearchingComputer)
launcher.searchingApp.connect(onSearchingApp)
launcher.sessionCreated.connect(onSessionCreated)
launcher.failed.connect(onLaunchFailed)
launcher.appQuitRequired.connect(onAppQuitRequired)
launcher.execute(ComputerManager)
}
}
Row {
anchors.centerIn: parent
spacing: 5
BusyIndicator {
id: stageSpinner
}
Label {
id: stageLabel
height: stageSpinner.height
font.pointSize: 20
verticalAlignment: Text.AlignVCenter
wrapMode: Text.Wrap
}
}
MessageDialog {
id: errorDialog
modality:Qt.WindowModal
icon: StandardIcon.Critical
standardButtons: StandardButton.Ok | StandardButton.Help
onHelp: {
Qt.openUrlExternally("https://github.com/moonlight-stream/moonlight-docs/wiki/Troubleshooting");
}
onVisibleChanged: {
if (!visible) {
Qt.quit();
}
}
}
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()
}
}
+24
View File
@@ -0,0 +1,24 @@
import QtQuick 2.0
import QtQuick.Controls 2.2
ItemDelegate {
property GridView grid
highlighted: grid.activeFocus && grid.currentItem === this
Keys.onLeftPressed: {
grid.moveCurrentIndexLeft()
}
Keys.onRightPressed: {
grid.moveCurrentIndexRight()
}
Keys.onDownPressed: {
grid.moveCurrentIndexDown()
}
Keys.onUpPressed: {
grid.moveCurrentIndexUp()
}
Keys.onReturnPressed: {
clicked()
}
}
+12
View File
@@ -0,0 +1,12 @@
import QtQuick 2.0
import QtQuick.Controls 2.2
MenuItem {
// Ensure focus can't be given to an invisible item
enabled: visible
height: visible ? implicitHeight : 0
Keys.onReturnPressed: {
triggered()
}
}
+18
View File
@@ -0,0 +1,18 @@
import QtQuick 2.0
import QtQuick.Controls 2.2
ToolButton {
activeFocusOnTab: true
Keys.onReturnPressed: {
clicked()
}
Keys.onRightPressed: {
nextItemInFocusChain(true).forceActiveFocus(Qt.TabFocus)
}
Keys.onLeftPressed: {
nextItemInFocusChain(false).forceActiveFocus(Qt.TabFocus)
}
}
+48 -41
View File
@@ -8,11 +8,14 @@ import ComputerModel 1.0
import ComputerManager 1.0
import StreamingPreferences 1.0
import SdlGamepadKeyNavigation 1.0
GridView {
property ComputerModel computerModel : createModel()
id: pcGrid
focus: true
activeFocusOnTab: true
anchors.fill: parent
anchors.leftMargin: (parent.width % (cellWidth + anchors.rightMargin)) / 2
anchors.topMargin: 20
@@ -30,16 +33,25 @@ GridView {
id: prefs
}
SdlGamepadKeyNavigation {
id: gamepadKeyNav
}
onVisibleChanged: {
if (visible) {
gamepadKeyNav.enable()
}
else {
gamepadKeyNav.disable()
}
}
Component.onCompleted: {
// Setup signals on CM
ComputerManager.computerAddCompleted.connect(addComplete)
if (prefs.isRunningWayland()) {
waylandDialog.open()
}
else if (!prefs.hasAnyHardwareAcceleration()) {
noHwDecoderDialog.open()
}
// Don't show any highlighted item until interacting with them
currentIndex = -1
}
function pairingComplete(error)
@@ -72,8 +84,9 @@ GridView {
model: computerModel
delegate: ItemDelegate {
delegate: NavigableItemDelegate {
width: 300; height: 300;
grid: pcGrid
Image {
id: pcIcon
@@ -111,10 +124,9 @@ GridView {
visible: !model.addPc && model.statusUnknown
}
Text {
Label {
id: pcNameText
text: model.name
color: "white"
width: parent.width
anchors.top: pcIcon.bottom
@@ -125,13 +137,12 @@ GridView {
Menu {
id: pcContextMenu
MenuItem {
NavigableMenuItem {
text: "Wake PC"
onTriggered: computerModel.wakeComputer(index)
visible: !model.addPc && !model.online && model.wakeable
height: visible ? implicitHeight : 0
}
MenuItem {
NavigableMenuItem {
text: "Delete PC"
onTriggered: {
deletePcDialog.pcIndex = index
@@ -170,6 +181,7 @@ GridView {
}
}
} else if (!model.online) {
// Using open() here because it may be activated by keyboard
pcContextMenu.open()
}
}
@@ -178,12 +190,26 @@ GridView {
anchors.fill: parent
acceptedButtons: Qt.RightButton;
onClicked: {
// right click
if (!model.addPc) { // but only for actual PCs, not the add-pc option
pcContextMenu.open()
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()
}
}
}
}
Keys.onMenuPressed: {
if (!model.addPc) {
// We must use open() here so the menu is positioned on
// the ItemDelegate and not where the mouse cursor is
pcContextMenu.open()
}
}
}
MessageDialog {
@@ -199,31 +225,6 @@ GridView {
}
}
MessageDialog {
id: noHwDecoderDialog
modality:Qt.WindowModal
icon: StandardIcon.Warning
standardButtons: StandardButton.Ok | StandardButton.Help
text: "No functioning hardware accelerated H.264 video decoder was detected by Moonlight. " +
"Your streaming performance may be severely degraded in this configuration. " +
"Click the Help button for more information on solving this problem."
onHelp: {
Qt.openUrlExternally("https://github.com/moonlight-stream/moonlight-docs/wiki/Fixing-Hardware-Decoding-Problems");
}
}
MessageDialog {
id: waylandDialog
modality:Qt.WindowModal
icon: StandardIcon.Warning
standardButtons: StandardButton.Ok | StandardButton.Help
text: "Moonlight does not support hardware acceleration on Wayland. Continuing on Wayland may result in poor streaming performance. " +
"Please switch to an X session for optimal performance."
onHelp: {
Qt.openUrlExternally("https://github.com/moonlight-stream/moonlight-docs/wiki/Fixing-Hardware-Decoding-Problems");
}
}
MessageDialog {
id: pairDialog
// don't allow edits to the rest of the window while open
@@ -243,10 +244,16 @@ GridView {
property int pcIndex : -1;
text:"Are you sure you want to remove this PC?"
standardButtons: StandardButton.Yes | StandardButton.No
onYes: {
function deletePc() {
console.log("deleting PC pairing for PC at index: " + pcIndex)
computerModel.deleteComputer(pcIndex);
}
onYes: deletePc()
// For keyboard/gamepad activation
onAccepted: deletePc()
}
Dialog {
-1
View File
@@ -65,7 +65,6 @@ Item {
verticalAlignment: Text.AlignVCenter
wrapMode: Text.Wrap
color: "white"
}
}
+145 -49
View File
@@ -3,15 +3,50 @@ import QtQuick.Controls 2.2
import StreamingPreferences 1.0
import ComputerManager 1.0
import SdlGamepadKeyNavigation 1.0
ScrollView {
Flickable {
id: settingsPage
objectName: "Settings"
anchors.fill: parent
contentWidth: settingsColumn1.width > settingsColumn2.width ? settingsColumn1.width : settingsColumn2.width
contentHeight: settingsColumn1.height > settingsColumn2.height ? settingsColumn1.height : settingsColumn2.height
ScrollBar.vertical: ScrollBar {
parent: settingsPage.parent
anchors {
top: settingsPage.top
left: settingsPage.right
bottom: settingsPage.bottom
leftMargin: -10
}
}
StreamingPreferences {
id: prefs
}
// The StackView will trigger a visibility change when
// we're pushed onto it, causing our onVisibleChanged
// routine to run, but only if we start as invisible
visible: false
SdlGamepadKeyNavigation {
id: gamepadKeyNav
}
onVisibleChanged: {
if (visible) {
gamepadKeyNav.setSettingsMode(true)
gamepadKeyNav.enable()
}
else {
gamepadKeyNav.disable()
}
}
Component.onDestruction: {
prefs.save()
}
@@ -37,23 +72,21 @@ ScrollView {
id: resFPStitle
text: qsTr("Resolution and FPS")
font.pointSize: 12
wrapMode: Text.Wrap
color: "white"
wrapMode: Text.Wrap
}
Label {
width: parent.width
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
wrapMode: Text.Wrap
color: "white"
}
Row {
spacing: 5
ComboBox {
AutoResizingComboBox {
// ignore setting the index at first, and actually set it when the component is loaded
Component.onCompleted: {
// Add native resolutions for all attached displays
@@ -125,7 +158,6 @@ ScrollView {
}
id: resolutionComboBox
width: 200
textRole: "text"
model: ListModel {
id: resolutionListModel
@@ -168,7 +200,7 @@ ScrollView {
}
}
ComboBox {
AutoResizingComboBox {
function createModel() {
var fpsListModel = Qt.createQmlObject('import QtQuick 2.0; ListModel {}', parent, '')
@@ -192,7 +224,16 @@ ScrollView {
// Use 64 as the cutoff for adding a separate option to
// handle wonky displays that report just over 60 Hz.
if (max_fps > 64) {
fpsListModel.append({"text": max_fps+" FPS", "video_fps": ""+max_fps})
// 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
@@ -233,7 +274,6 @@ ScrollView {
id: fpsComboBox
textRole: "text"
width: 250
// ::onActivated must be used, as it only listens for when the index is changed by a human
onActivated : {
var selectedFps = parseInt(model.get(currentIndex).video_fps)
@@ -255,21 +295,18 @@ ScrollView {
text: qsTr("Video bitrate: ")
font.pointSize: 12
wrapMode: Text.Wrap
color: "white"
}
Label {
width: parent.width
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
wrapMode: Text.Wrap
color: "white"
}
Slider {
id: slider
wheelEnabled: true
value: prefs.bitrateKbps
@@ -292,10 +329,9 @@ ScrollView {
text: qsTr("Display mode")
font.pointSize: 12
wrapMode: Text.Wrap
color: "white"
}
ComboBox {
AutoResizingComboBox {
// ignore setting the index at first, and actually set it when the component is loaded
Component.onCompleted: {
var savedWm = prefs.windowMode
@@ -311,7 +347,7 @@ ScrollView {
}
id: windowModeComboBox
width: Math.min(bitrateDesc.implicitWidth, parent.width)
hoverEnabled: true
textRole: "text"
model: ListModel {
id: windowModeListModel
@@ -331,16 +367,43 @@ ScrollView {
onActivated: {
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 {
id: vsyncCheck
text: "<font color=\"white\">Enable V-Sync</font>"
hoverEnabled: true
text: "V-Sync"
font.pointSize: 12
checked: prefs.enableVsync
onCheckedChanged: {
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"
}
}
}
@@ -363,10 +426,9 @@ ScrollView {
text: qsTr("Audio configuration")
font.pointSize: 12
wrapMode: Text.Wrap
color: "white"
}
ComboBox {
AutoResizingComboBox {
// ignore setting the index at first, and actually set it when the component is loaded
Component.onCompleted: {
var saved_audio = prefs.audioConfig
@@ -382,21 +444,16 @@ ScrollView {
}
id: audioComboBox
width: Math.min(bitrateDesc.implicitWidth, parent.width)
textRole: "text"
model: ListModel {
id: audioListModel
ListElement {
text: "Autodetect"
val: StreamingPreferences.AC_AUTO
}
ListElement {
text: "Stereo"
val: StreamingPreferences.AC_FORCE_STEREO
val: StreamingPreferences.AC_STEREO
}
ListElement {
text: "5.1 surround sound"
val: StreamingPreferences.AC_FORCE_SURROUND
val: StreamingPreferences.AC_51_SURROUND
}
}
// ::onActivated must be used, as it only listens for when the index is changed by a human
@@ -407,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 {
@@ -422,28 +502,40 @@ ScrollView {
title: "<font color=\"skyblue\">Input Settings</font>"
font.pointSize: 12
Row {
Column {
anchors.fill: parent
spacing: 5
CheckBox {
id: multiControllerCheck
text: "<font color=\"white\">Multiple controller support</font>"
id: singleControllerCheck
text: "Force gamepad #1 always present"
font.pointSize: 12
checked: prefs.multiController
checked: !prefs.multiController
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 {
id: mouseAccelerationCheck
text: "<font color=\"white\">Enable mouse acceleration</font>"
id: rawInputCheck
hoverEnabled: true
text: "Raw mouse input"
font.pointSize: 12
checked: prefs.mouseAcceleration
checked: !prefs.mouseAcceleration
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"
}
}
}
@@ -455,15 +547,13 @@ ScrollView {
title: "<font color=\"skyblue\">Host Settings</font>"
font.pointSize: 12
Row {
Column {
anchors.fill: parent
spacing: 5
CheckBox {
id: optimizeGameSettingsCheck
text: "<font color=\"white\">Optimize game settings</font>"
// HACK: Match width of the other checkbox to make the UI not look bad
width: multiControllerCheck.width
text: "Optimize game settings"
font.pointSize: 12
checked: prefs.gameOptimizations
onCheckedChanged: {
@@ -473,7 +563,7 @@ ScrollView {
CheckBox {
id: audioPcCheck
text: "<font color=\"white\">Play audio on host PC</font>"
text: "Play audio on host PC"
font.pointSize: 12
checked: prefs.playAudioOnHost
onCheckedChanged: {
@@ -500,10 +590,9 @@ ScrollView {
text: qsTr("Video decoder")
font.pointSize: 12
wrapMode: Text.Wrap
color: "white"
}
ComboBox {
AutoResizingComboBox {
// ignore setting the index at first, and actually set it when the component is loaded
Component.onCompleted: {
var saved_vds = prefs.videoDecoderSelection
@@ -519,7 +608,6 @@ ScrollView {
}
id: decoderComboBox
width: Math.min(bitrateDesc.implicitWidth, parent.width)
textRole: "text"
model: ListModel {
id: decoderListModel
@@ -548,10 +636,9 @@ ScrollView {
text: qsTr("Video codec")
font.pointSize: 12
wrapMode: Text.Wrap
color: "white"
}
ComboBox {
AutoResizingComboBox {
// ignore setting the index at first, and actually set it when the component is loaded
Component.onCompleted: {
var saved_vcc = prefs.videoCodecConfig
@@ -567,7 +654,6 @@ ScrollView {
}
id: codecComboBox
width: Math.min(bitrateDesc.implicitWidth, parent.width)
textRole: "text"
model: ListModel {
id: codecListModel
@@ -599,7 +685,7 @@ ScrollView {
CheckBox {
id: unlockUnsupportedFps
text: "<font color=\"white\">Unlock unsupported FPS options</font>"
text: "Unlock unsupported FPS options"
font.pointSize: 12
checked: prefs.unsupportedFps
onCheckedChanged: {
@@ -613,7 +699,7 @@ ScrollView {
CheckBox {
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
checked: prefs.enableMdns
onCheckedChanged: {
@@ -630,6 +716,16 @@ ScrollView {
}
}
}
CheckBox {
id: quitAppAfter
text: "Quit app after quitting session"
font.pointSize: 12
checked: prefs.quitAppAfter
onCheckedChanged: {
prefs.quitAppAfter = checked
}
}
}
}
}
+72 -11
View File
@@ -3,12 +3,15 @@ import QtQuick.Controls 2.2
import QtQuick.Dialogs 1.2
import QtQuick.Window 2.2
import ComputerManager 1.0
import SdlGamepadKeyNavigation 1.0
import Session 1.0
Item {
property Session session
property string appName
property string stageText : "Starting " + appName + "..."
property bool quitAfter : false
anchors.fill: parent
@@ -42,8 +45,8 @@ Item {
function displayLaunchError(text)
{
// Display the error dialog after Session::exec() returns
errorDialog.text = text
errorDialog.open()
}
function displayLaunchWarning(text)
@@ -57,28 +60,77 @@ Item {
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: {
if (visible) {
// Hide the toolbar before we start loading
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
session.stageStarting.connect(stageStarting)
session.stageFailed.connect(stageFailed)
session.connectionStarted.connect(connectionStarted)
session.displayLaunchError.connect(displayLaunchError)
session.displayLaunchWarning.connect(displayLaunchWarning)
session.quitStarting.connect(quitStarting)
session.sessionFinished.connect(sessionFinished)
// Run the streaming session to completion
session.exec(Screen.virtualX, Screen.virtualY)
// Show the Qt window again after streaming
window.visible = true
// Exit this view
stackView.pop()
}
else {
else if (!quitAfter) {
// Show the toolbar again when we become hidden
toolBar.visible = true
}
@@ -100,12 +152,11 @@ Item {
verticalAlignment: Text.AlignVCenter
wrapMode: Text.Wrap
color: "white"
}
}
Label {
text: "Tip: Press Ctrl+Alt+Shift+Q to disconnect your session"
id: hintText
anchors.bottom: parent.bottom
anchors.bottomMargin: 50
anchors.horizontalCenter: parent.horizontalCenter
@@ -113,7 +164,6 @@ Item {
verticalAlignment: Text.AlignVCenter
wrapMode: Text.Wrap
color: "white"
}
MessageDialog {
@@ -121,8 +171,19 @@ Item {
modality:Qt.WindowModal
icon: StandardIcon.Critical
standardButtons: StandardButton.Ok | StandardButton.Help
onAccepted: {
if (quitAfter) {
Qt.quit()
}
}
onHelp: {
Qt.openUrlExternally("https://github.com/moonlight-stream/moonlight-docs/wiki/Troubleshooting");
if (quitAfter) {
Qt.quit()
}
}
}
}
+1 -1
View File
@@ -2,7 +2,7 @@
#include "backend/boxartmanager.h"
#include "backend/computermanager.h"
#include "streaming/session.hpp"
#include "streaming/session.h"
#include <QAbstractListModel>
+1 -1
View File
@@ -1,5 +1,5 @@
#include "backend/computermanager.h"
#include "streaming/session.hpp"
#include "streaming/session.h"
#include <QAbstractListModel>
+243 -27
View File
@@ -1,5 +1,6 @@
import QtQuick 2.9
import QtQuick.Controls 2.2
import QtQuick.Dialogs 1.2
import QtQuick.Layouts 1.3
import QtQuick.Window 2.2
@@ -7,6 +8,7 @@ import QtQuick.Controls.Material 2.1
import ComputerManager 1.0
import AutoUpdateChecker 1.0
import StreamingPreferences 1.0
ApplicationWindow {
property bool pollingActive: false
@@ -16,30 +18,145 @@ ApplicationWindow {
width: 1280
height: 600
visibility: prefs.startWindowed ? "Windowed" : "Maximized"
Material.theme: Material.Dark
Material.accent: Material.Purple
StackView {
id: stackView
initialItem: "PcView.qml"
anchors.fill: parent
StreamingPreferences {
id: prefs
}
onVisibilityChanged: {
// We don't want to just use 'active' here because that will stop polling if
// we lose focus, which might be overzealous for users with multiple screens
// where we may be clearly visible on the other display. Ideally we'll poll
// only if the window is visible to the user (not if obscured by other windows),
// but it seems difficult to do this portably.
var shouldPoll = visibility !== Window.Minimized && visibility !== Window.Hidden
StackView {
id: stackView
initialItem: initialView
anchors.fill: parent
focus: true
if (shouldPoll && !pollingActive) {
ComputerManager.startPolling()
pollingActive = true
onCurrentItemChanged: {
// Ensure focus travels to the next view when going back
if (currentItem) {
currentItem.forceActiveFocus()
}
}
else if (!shouldPoll && pollingActive) {
ComputerManager.stopPollingAsync()
pollingActive = false
Keys.onEscapePressed: {
if (depth > 1) {
stackView.pop()
}
else {
quitConfirmationDialog.open()
}
}
Keys.onBackPressed: {
if (depth > 1) {
stackView.pop()
}
else {
quitConfirmationDialog.open()
}
}
Keys.onMenuPressed: {
settingsButton.clicked()
}
// This is a keypress we've reserved for letting the
// SdlGamepadKeyNavigation object tell us to show settings
// when Menu is consumed by a focused control.
Keys.onHangupPressed: {
settingsButton.clicked()
}
}
// This timer keeps us polling for 5 minutes of inactivity
// to allow the user to work with Moonlight on a second display
// while dealing with configuration issues. This will ensure
// machines come online even if the input focus isn't on Moonlight.
Timer {
id: inactivityTimer
interval: 5 * 60000
onTriggered: {
if (!active && 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()
}
}
property bool initialized: false
// BUG: Using onAfterSynchronizing: here causes very strange
// failures on Linux. Many shaders fail to compile and we
// eventually segfault deep inside the Qt OpenGL code.
onAfterRendering: {
// We use this callback to trigger dialog display because
// it only happens once the window is fully constructed.
// Doing it earlier can lead to the dialog appearing behind
// the window or otherwise without input focus.
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()) {
waylandDialog.open()
}
else if (prefs.isWow64()) {
wow64Dialog.open()
}
else if (!prefs.hasAnyHardwareAcceleration()) {
noHwDecoderDialog.open()
}
var unmappedGamepads = prefs.getUnmappedGamepads()
if (unmappedGamepads) {
unmappedGamepadDialog.unmappedGamepads = unmappedGamepads
unmappedGamepadDialog.open()
}
}
}
@@ -68,7 +185,7 @@ ApplicationWindow {
spacing: 20
anchors.fill: parent
ToolButton {
NavigableToolButton {
// Only make the button visible if the user has navigated somewhere.
visible: stackView.depth > 1
@@ -85,6 +202,10 @@ ApplicationWindow {
}
onClicked: stackView.pop()
Keys.onDownPressed: {
stackView.currentItem.forceActiveFocus(Qt.TabFocus)
}
}
Label {
@@ -97,7 +218,7 @@ ApplicationWindow {
Layout.fillWidth: true
}
ToolButton {
NavigableToolButton {
property string browserUrl: ""
id: updateButton
@@ -114,7 +235,6 @@ ApplicationWindow {
ToolTip.delay: 1000
ToolTip.timeout: 3000
ToolTip.visible: hovered
ToolTip.text: "Update available for Moonlight"
// Invisible until we get a callback notifying us that
// an update is available
@@ -122,9 +242,9 @@ ApplicationWindow {
onClicked: Qt.openUrlExternally(browserUrl);
function updateAvailable(url)
function updateAvailable(version, url)
{
ToolTip.text = "Update available for Moonlight: Version " + version
updateButton.browserUrl = url
updateButton.visible = true
}
@@ -133,9 +253,15 @@ ApplicationWindow {
AutoUpdateChecker.onUpdateAvailable.connect(updateAvailable)
AutoUpdateChecker.start()
}
Keys.onDownPressed: {
stackView.currentItem.forceActiveFocus(Qt.TabFocus)
}
}
ToolButton {
NavigableToolButton {
id: helpButton
Image {
source: "qrc:/res/question_mark.svg"
anchors.centerIn: parent
@@ -148,13 +274,23 @@ ApplicationWindow {
ToolTip.delay: 1000
ToolTip.timeout: 3000
ToolTip.visible: hovered
ToolTip.text: "Help"
ToolTip.text: "Help" + (helpShortcut.nativeText ? (" ("+helpShortcut.nativeText+")") : "")
Shortcut {
id: helpShortcut
sequence: StandardKey.HelpContents
onActivated: helpButton.clicked()
}
// TODO need to make sure browser is brought to foreground.
onClicked: Qt.openUrlExternally("https://github.com/moonlight-stream/moonlight-docs/wiki/Setup-Guide");
Keys.onDownPressed: {
stackView.currentItem.forceActiveFocus(Qt.TabFocus)
}
}
ToolButton {
NavigableToolButton {
// TODO: Implement gamepad mapping then unhide this button
visible: false
@@ -173,9 +309,15 @@ ApplicationWindow {
}
onClicked: navigateTo("qrc:/gui/GamepadMapper.qml", "Gamepad Mapping")
Keys.onDownPressed: {
stackView.currentItem.forceActiveFocus(Qt.TabFocus)
}
}
ToolButton {
NavigableToolButton {
id: settingsButton
Image {
source: "qrc:/res/settings.svg"
anchors.centerIn: parent
@@ -187,11 +329,85 @@ ApplicationWindow {
onClicked: navigateTo("qrc:/gui/SettingsView.qml", "Settings")
Keys.onDownPressed: {
stackView.currentItem.forceActiveFocus(Qt.TabFocus)
}
Shortcut {
id: settingsShortcut
sequence: StandardKey.Preferences
onActivated: settingsButton.clicked()
}
ToolTip.delay: 1000
ToolTip.timeout: 3000
ToolTip.visible: hovered
ToolTip.text: "Settings"
ToolTip.text: "Settings" + (settingsShortcut.nativeText ? (" ("+settingsShortcut.nativeText+")") : "")
}
}
}
MessageDialog {
id: noHwDecoderDialog
modality:Qt.WindowModal
icon: StandardIcon.Warning
standardButtons: StandardButton.Ok | StandardButton.Help
text: "No functioning hardware accelerated H.264 video decoder was detected by Moonlight. " +
"Your streaming performance may be severely degraded in this configuration. " +
"Click the Help button for more information on solving this problem."
onHelp: {
Qt.openUrlExternally("https://github.com/moonlight-stream/moonlight-docs/wiki/Fixing-Hardware-Decoding-Problems");
}
}
MessageDialog {
id: waylandDialog
modality:Qt.WindowModal
icon: StandardIcon.Warning
standardButtons: StandardButton.Ok | StandardButton.Help
text: "Moonlight does not support hardware acceleration on Wayland. Continuing on Wayland may result in poor streaming performance. " +
"Please switch to an X session for optimal performance."
onHelp: {
Qt.openUrlExternally("https://github.com/moonlight-stream/moonlight-docs/wiki/Fixing-Hardware-Decoding-Problems");
}
}
MessageDialog {
id: wow64Dialog
modality:Qt.WindowModal
icon: StandardIcon.Warning
standardButtons: StandardButton.Ok | StandardButton.Cancel
text: "This PC is running a 64-bit version of Windows. Please download the x64 version of Moonlight for the best streaming performance."
onAccepted: {
Qt.openUrlExternally("https://github.com/moonlight-stream/moonlight-qt/releases");
}
}
MessageDialog {
id: unmappedGamepadDialog
property string unmappedGamepads : ""
modality:Qt.WindowModal
icon: StandardIcon.Warning
standardButtons: StandardButton.Ok | StandardButton.Help
text: "Moonlight detected gamepads without a proper mapping. " +
"The following gamepads will not function until this is resolved: " + unmappedGamepads + "\n\n" +
"Click the Help button for information on how to map your gamepads."
onHelp: {
Qt.openUrlExternally("https://github.com/moonlight-stream/moonlight-docs/wiki/Gamepad-Mapping");
}
}
// 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()
}
}
+266
View File
@@ -0,0 +1,266 @@
#include "sdlgamepadkeynavigation.h"
#include <QKeyEvent>
#include <QGuiApplication>
#include <QWindow>
#include "settings/mappingmanager.h"
#define AXIS_NAVIGATION_REPEAT_DELAY 150
SdlGamepadKeyNavigation::SdlGamepadKeyNavigation()
: m_Enabled(false),
m_SettingsMode(false),
m_LastAxisNavigationEventTime(0)
{
m_PollingTimer = new QTimer(this);
connect(m_PollingTimer, SIGNAL(timeout()), this, SLOT(onPollingTimerFired()));
}
SdlGamepadKeyNavigation::~SdlGamepadKeyNavigation()
{
disable();
}
void SdlGamepadKeyNavigation::enable()
{
if (m_Enabled) {
return;
}
// We have to initialize and uninitialize this in enable()/disable()
// because we need to get out of the way of the Session class. If it
// doesn't get to reinitialize the GC subsystem, it won't get initial
// arrival events. Additionally, there's a race condition between
// our QML objects being destroyed and SDL being deinitialized that
// this solves too.
if (SDL_InitSubSystem(SDL_INIT_GAMECONTROLLER) != 0) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"SDL_InitSubSystem(SDL_INIT_GAMECONTROLLER) failed: %s",
SDL_GetError());
return;
}
MappingManager mappingManager;
mappingManager.applyMappings();
// Drop all pending gamepad add events. SDL will generate these for us
// on first init of the GC subsystem. We can't depend on them due to
// overlapping lifetimes of SdlGamepadKeyNavigation instances, so we
// will attach ourselves.
SDL_PumpEvents();
SDL_FlushEvent(SDL_CONTROLLERDEVICEADDED);
// Open all currently attached game controllers
for (int i = 0; i < SDL_NumJoysticks(); i++) {
if (SDL_IsGameController(i)) {
SDL_GameController* gc = SDL_GameControllerOpen(i);
if (gc != nullptr) {
m_Gamepads.append(gc);
}
}
}
// Poll every 50 ms for a new joystick event
m_PollingTimer->start(50);
m_Enabled = true;
}
void SdlGamepadKeyNavigation::disable()
{
if (!m_Enabled) {
return;
}
m_PollingTimer->stop();
while (!m_Gamepads.isEmpty()) {
SDL_GameControllerClose(m_Gamepads[0]);
m_Gamepads.removeAt(0);
}
SDL_QuitSubSystem(SDL_INIT_GAMECONTROLLER);
m_Enabled = false;
}
void SdlGamepadKeyNavigation::onPollingTimerFired()
{
SDL_Event event;
while (SDL_PollEvent(&event)) {
switch (event.type) {
case SDL_CONTROLLERBUTTONDOWN:
case SDL_CONTROLLERBUTTONUP:
{
QEvent::Type type =
event.type == SDL_CONTROLLERBUTTONDOWN ?
QEvent::Type::KeyPress : QEvent::Type::KeyRelease;
switch (event.cbutton.button) {
case SDL_CONTROLLER_BUTTON_DPAD_UP:
if (m_SettingsMode) {
// Back-tab
sendKey(type, Qt::Key_Tab, Qt::ShiftModifier);
}
else {
sendKey(type, Qt::Key_Up);
}
break;
case SDL_CONTROLLER_BUTTON_DPAD_DOWN:
if (m_SettingsMode) {
sendKey(type, Qt::Key_Tab);
}
else {
sendKey(type, Qt::Key_Down);
}
break;
case SDL_CONTROLLER_BUTTON_DPAD_LEFT:
sendKey(type, Qt::Key_Left);
if (m_SettingsMode) {
// Some settings controls respond to left/right (like the slider)
// and others respond to up/down (like combo boxes). They seem to
// be mutually exclusive though so let's just send both.
sendKey(type, Qt::Key_Up);
}
break;
case SDL_CONTROLLER_BUTTON_DPAD_RIGHT:
sendKey(type, Qt::Key_Right);
if (m_SettingsMode) {
// Some settings controls respond to left/right (like the slider)
// and others respond to up/down (like combo boxes). They seem to
// be mutually exclusive though so let's just send both.
sendKey(type, Qt::Key_Down);
}
break;
case SDL_CONTROLLER_BUTTON_A:
if (m_SettingsMode) {
sendKey(type, Qt::Key_Space);
}
else {
sendKey(type, Qt::Key_Return);
}
break;
case SDL_CONTROLLER_BUTTON_B:
sendKey(type, Qt::Key_Escape);
break;
case SDL_CONTROLLER_BUTTON_X:
sendKey(type, Qt::Key_Menu);
break;
case SDL_CONTROLLER_BUTTON_Y:
case SDL_CONTROLLER_BUTTON_START:
// HACK: We use this keycode to inform main.qml
// to show the settings when Key_Menu is handled
// by the control in focus.
sendKey(type, Qt::Key_Hangup);
break;
default:
break;
}
break;
}
case SDL_CONTROLLERDEVICEADDED:
SDL_GameController* gc = SDL_GameControllerOpen(event.cdevice.which);
if (gc != nullptr) {
m_Gamepads.append(gc);
}
break;
}
}
// Handle analog sticks by polling
for (auto gc : m_Gamepads) {
short leftX = SDL_GameControllerGetAxis(gc, SDL_CONTROLLER_AXIS_LEFTX);
short leftY = SDL_GameControllerGetAxis(gc, SDL_CONTROLLER_AXIS_LEFTY);
if (SDL_GetTicks() - m_LastAxisNavigationEventTime < AXIS_NAVIGATION_REPEAT_DELAY) {
// Do nothing
}
else if (leftY < -30000) {
if (m_SettingsMode) {
// Back-tab
sendKey(QEvent::Type::KeyPress, Qt::Key_Tab, Qt::ShiftModifier);
sendKey(QEvent::Type::KeyRelease, Qt::Key_Tab, Qt::ShiftModifier);
}
else {
sendKey(QEvent::Type::KeyPress, Qt::Key_Up);
sendKey(QEvent::Type::KeyRelease, Qt::Key_Up);
}
m_LastAxisNavigationEventTime = SDL_GetTicks();
}
else if (leftY > 30000) {
if (m_SettingsMode) {
sendKey(QEvent::Type::KeyPress, Qt::Key_Tab);
sendKey(QEvent::Type::KeyRelease, Qt::Key_Tab);
}
else {
sendKey(QEvent::Type::KeyPress, Qt::Key_Down);
sendKey(QEvent::Type::KeyRelease, Qt::Key_Down);
}
m_LastAxisNavigationEventTime = SDL_GetTicks();
}
else if (leftX < -30000) {
sendKey(QEvent::Type::KeyPress, Qt::Key_Left);
sendKey(QEvent::Type::KeyRelease, Qt::Key_Left);
if (m_SettingsMode) {
// Some settings controls respond to left/right (like the slider)
// and others respond to up/down (like combo boxes). They seem to
// be mutually exclusive though so let's just send both.
sendKey(QEvent::Type::KeyPress, Qt::Key_Up);
sendKey(QEvent::Type::KeyRelease, Qt::Key_Up);
}
m_LastAxisNavigationEventTime = SDL_GetTicks();
}
else if (leftX > 30000) {
sendKey(QEvent::Type::KeyPress, Qt::Key_Right);
sendKey(QEvent::Type::KeyRelease, Qt::Key_Right);
if (m_SettingsMode) {
// Some settings controls respond to left/right (like the slider)
// and others respond to up/down (like combo boxes). They seem to
// be mutually exclusive though so let's just send both.
sendKey(QEvent::Type::KeyPress, Qt::Key_Down);
sendKey(QEvent::Type::KeyRelease, Qt::Key_Down);
}
m_LastAxisNavigationEventTime = SDL_GetTicks();
}
}
}
void SdlGamepadKeyNavigation::sendKey(QEvent::Type type, Qt::Key key, Qt::KeyboardModifiers modifiers)
{
QGuiApplication* app = static_cast<QGuiApplication*>(QGuiApplication::instance());
QWindow* focusWindow = app->focusWindow();
if (focusWindow != nullptr) {
QKeyEvent keyPressEvent(type, key, modifiers);
app->sendEvent(focusWindow, &keyPressEvent);
}
}
void SdlGamepadKeyNavigation::setSettingsMode(bool 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;
}
int count = 0;
for (int i = 0; i < SDL_NumJoysticks(); i++) {
if (SDL_IsGameController(i)) {
count++;
}
}
SDL_QuitSubSystem(SDL_INIT_GAMECONTROLLER);
return count;
}
+37
View File
@@ -0,0 +1,37 @@
#pragma once
#include <QTimer>
#include <QEvent>
#include <SDL.h>
class SdlGamepadKeyNavigation : public QObject
{
Q_OBJECT
public:
SdlGamepadKeyNavigation();
~SdlGamepadKeyNavigation();
Q_INVOKABLE void enable();
Q_INVOKABLE void disable();
Q_INVOKABLE void setSettingsMode(bool settingsMode);
Q_INVOKABLE int getConnectedGamepads();
private:
void sendKey(QEvent::Type type, Qt::Key key, Qt::KeyboardModifiers modifiers = Qt::NoModifier);
private slots:
void onPollingTimerFired();
private:
QTimer* m_PollingTimer;
QList<SDL_GameController*> m_Gamepads;
bool m_Enabled;
bool m_SettingsMode;
Uint32 m_LastAxisNavigationEventTime;
};
+83 -7
View File
@@ -1,9 +1,11 @@
#include <QGuiApplication>
#include <QQmlApplicationEngine>
#include <QQmlContext>
#include <QIcon>
#include <QQuickStyle>
#include <QMutex>
#include <QtDebug>
#include <QNetworkProxyFactory>
// Don't let SDL hook our main function, since Qt is already
// doing the same thing. This needs to be before any headers
@@ -15,12 +17,20 @@
#include "streaming/video/ffmpeg.h"
#endif
#ifdef Q_OS_WIN32
#include "antihookingprotection.h"
#endif
#include "cli/quitstream.h"
#include "cli/startstream.h"
#include "cli/commandlineparser.h"
#include "path.h"
#include "gui/computermodel.h"
#include "gui/appmodel.h"
#include "backend/autoupdatechecker.h"
#include "streaming/session.hpp"
#include "streaming/session.h"
#include "settings/streamingpreferences.h"
#include "gui/sdlgamepadkeynavigation.h"
#if !defined(QT_DEBUG) && defined(Q_OS_WIN32)
// Log to file for release Windows builds
@@ -126,6 +136,14 @@ void qtLogToDiskHandler(QtMsgType type, const QMessageLogContext&, const QString
break;
}
// HACK: Avoid printing this internal Qt warning which seems to print every time we
// start our event loop for the QNetworkReply. I have found this warning in many
// unrelated bug reports, but none actually tackle this warning itself. It seems to be
// new in Qt 5.12.
if (msg.startsWith("QNetworkReplyHttpImplPrivate::_q_startOperation was called more than once")) {
return;
}
QTime logTime = QTime::fromMSecsSinceStartOfDay(s_LoggerTime.elapsed());
QString txt = QString("%1 - Qt %2: %3\n").arg(logTime.toString()).arg(typeTxt).arg(msg);
@@ -213,6 +231,9 @@ LONG WINAPI UnhandledExceptionHandler(struct _EXCEPTION_POINTERS *ExceptionInfo)
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.
// These also ensure that our cache directory is named correctly. As such,
// it is critical that these be called before Path::initialize().
@@ -259,6 +280,12 @@ int main(int argc, char *argv[])
SetUnhandledExceptionFilter(UnhandledExceptionHandler);
#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);
// This avoids using the default keychain for SSL, which may cause
@@ -266,11 +293,21 @@ int main(int argc, char *argv[])
qputenv("QT_SSL_USE_TEMPORARY_KEYCHAIN", QByteArray("1"));
#ifdef Q_OS_WIN32
// On Windows, use ANGLE so we don't have to load both DX and OGL
// user-mode drivers into our app.
qputenv("QT_OPENGL", "angle");
if (!qEnvironmentVariableIsSet("QT_OPENGL")) {
// On Windows, use ANGLE so we don't have to load OpenGL
// user-mode drivers into our app. OGL drivers (especially Intel)
// seem to crash Moonlight far more often than DirectX.
qputenv("QT_OPENGL", "angle");
}
#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
qRegisterMetaType<NvApp>("NvApp");
@@ -282,6 +319,7 @@ int main(int argc, char *argv[])
qmlRegisterType<ComputerModel>("ComputerModel", 1, 0, "ComputerModel");
qmlRegisterType<AppModel>("AppModel", 1, 0, "AppModel");
qmlRegisterType<StreamingPreferences>("StreamingPreferences", 1, 0, "StreamingPreferences");
qmlRegisterType<SdlGamepadKeyNavigation>("SdlGamepadKeyNavigation", 1, 0, "SdlGamepadKeyNavigation");
qmlRegisterUncreatableType<Session>("Session", 1, 0, "Session", "Session cannot be created from QML");
qmlRegisterSingletonType<ComputerManager>("ComputerManager", 1, 0,
"ComputerManager",
@@ -296,19 +334,55 @@ int main(int argc, char *argv[])
QQuickStyle::setStyle("Material");
// Load the main.qml file
QQmlApplicationEngine engine;
QString initialView;
GlobalCommandLineParser parser;
switch (parser.parse(app.arguments())) {
case GlobalCommandLineParser::NormalStartRequested:
initialView = "PcView.qml";
break;
case GlobalCommandLineParser::StreamRequested:
{
initialView = "CliStartStreamSegue.qml";
StreamingPreferences* preferences = new StreamingPreferences(&app);
StreamCommandLineParser streamParser;
streamParser.parse(app.arguments(), preferences);
QString host = streamParser.getHost();
QString appName = streamParser.getAppName();
auto launcher = new CliStartStream::Launcher(host, appName, preferences, &app);
engine.rootContext()->setContextProperty("launcher", launcher);
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);
// Load the main.qml file
engine.load(QUrl(QStringLiteral("qrc:/gui/main.qml")));
if (engine.rootObjects().isEmpty())
return -1;
SDL_SetMainReady();
if (SDL_Init(0) != 0) {
if (SDL_Init(SDL_INIT_TIMER) != 0) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"SDL_Init() failed: %s",
SDL_GetError());
}
// Use atexit() to ensure SDL_Quit() is called. This avoids
// racing with object destruction where SDL may be used.
atexit(SDL_Quit);
// Avoid the default behavior of changing the timer resolution to 1 ms.
// We don't want this all the time that Moonlight is open. We will set
// it manually when we start streaming.
@@ -316,7 +390,9 @@ int main(int argc, char *argv[])
int err = app.exec();
SDL_Quit();
// Give worker tasks time to properly exit. Fixes PendingQuitTask
// sometimes freezing and blocking process exit.
QThreadPool::globalInstance()->waitForDone(30000);
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
View File
@@ -7,5 +7,11 @@
<file>gui/StreamSegue.qml</file>
<file>gui/GamepadMapper.qml</file>
<file>gui/QuitSegue.qml</file>
<file>gui/NavigableToolButton.qml</file>
<file>gui/NavigableItemDelegate.qml</file>
<file>gui/NavigableMenuItem.qml</file>
<file>gui/CliQuitStreamSegue.qml</file>
<file>gui/CliStartStreamSegue.qml</file>
<file>gui/AutoResizingComboBox.qml</file>
</qresource>
</RCC>
+1 -69
View File
@@ -1,69 +1 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
id="svg3761"
viewBox="0 0 680 680"
height="680"
width="680"
version="1.0">
<metadata
id="metadata3767">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<defs
id="defs3765" />
<g
id="g3759"
transform="translate(-135,491) scale(0.100000,-0.100000)">
<circle
id="circle3741"
fill="#565C64"
r="3400"
cy="1505"
cx="4755" />
<path
id="path3743"
style="fill:#ffffff"
d="M4550 4230 c-582 -48 -1123 -271 -1568 -646 l-84 -70 869 -869 c477 -477 871 -868 876 -868 4 0 6 552 5 1228 l-3 1227 -25 1 c-14 0 -45 -1 -70 -3z" />
<path
id="path3745"
style="fill:#ffffff"
d="M4867 4230 c-10 -9 -9 -2453 1 -2453 4 0 398 391 875 868 l869 869 -84 70 c-344 290 -749 492 -1178 587 -166 36 -469 74 -483 59z" />
<path
id="path3747"
style="fill:#ffffff"
d="M2673 3275 c-241 -285 -412 -595 -526 -953 -63 -196 -111 -458 -119 -640 l-3 -70 1228 -3 c675 -1 1227 1 1227 5 0 5 -391 399 -868 876 l-869 869 -70 -84z" />
<path
id="path3749"
style="fill:#ffffff"
d="M5898 2490 c-478 -477 -868 -871 -868 -876 0 -4 552 -6 1228 -5 l1227 3 -3 70 c-14 326 -124 729 -287 1055 -91 182 -259 430 -387 573 l-43 48 -867 -868z" />
<path
id="path3751"
style="fill:#ffffff"
d="M2026 1388 c-7 -11 10 -198 30 -321 76 -486 291 -952 617 -1338 l70 -84 869 869 c477 477 868 871 868 875 0 11 -2448 10 -2454 -1z" />
<path
id="path3753"
style="fill:#ffffff"
d="M5030 1390 c0 -5 391 -399 868 -876 l869 -869 70 84 c241 285 412 595 526 953 63 196 111 458 119 640 l3 70 -1227 3 c-676 1 -1228 -1 -1228 -5z" />
<path
id="path3755"
style="fill:#ffffff"
d="M3767 359 l-869 -869 84 -70 c285 -241 595 -412 953 -526 196 -63 458 -111 640 -119 l70 -3 3 1228 c1 675 -1 1227 -5 1227 -5 0 -399 -391 -876 -868z" />
<path
id="path3757"
style="fill:#ffffff"
d="M4862 0 l3 -1228 70 3 c182 8 444 56 640 119 358 114 668 285 953 526 l84 70 -869 869 c-477 477 -871 868 -876 868 -4 0 -6 -552 -5 -1227z" />
</g>
</svg>
<?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>

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" />
</svg>

Before

Width:  |  Height:  |  Size: 499 B

After

Width:  |  Height:  |  Size: 499 B

+108
View File
@@ -0,0 +1,108 @@
#include "mappingmanager.h"
#include "path.h"
#include <QDir>
#include <SDL.h>
#define SER_GAMEPADMAPPING "gcmapping"
#define SER_GUID "guid"
#define SER_MAPPING "mapping"
MappingManager::MappingManager()
{
QSettings settings;
// First load existing saved mappings. This ensures the user's
// hints can always override the old data.
int mappingCount = settings.beginReadArray(SER_GAMEPADMAPPING);
for (int i = 0; i < mappingCount; i++) {
settings.setArrayIndex(i);
SdlGamepadMapping mapping(settings.value(SER_GUID).toString(), settings.value(SER_MAPPING).toString());
addMapping(mapping);
}
settings.endArray();
// Finally load mappings from SDL_HINT_GAMECONTROLLERCONFIG
QStringList sdlMappings = QString::fromLocal8Bit(SDL_GetHint(SDL_HINT_GAMECONTROLLERCONFIG)).split('\n', QString::SkipEmptyParts);
for (QString sdlMapping : sdlMappings) {
SdlGamepadMapping mapping(sdlMapping);
addMapping(mapping);
}
// Save the updated mappings to settings
save();
}
void MappingManager::save()
{
QSettings settings;
settings.remove(SER_GAMEPADMAPPING);
settings.beginWriteArray(SER_GAMEPADMAPPING);
QList<SdlGamepadMapping> mappings = m_Mappings.values();
for (int i = 0; i < mappings.count(); i++) {
settings.setArrayIndex(i);
settings.setValue(SER_GUID, mappings[i].getGuid());
settings.setValue(SER_MAPPING, mappings[i].getMapping());
}
settings.endArray();
}
void MappingManager::applyMappings()
{
QString mappingFile = Path::getGamepadMappingFile();
if (!mappingFile.isEmpty()) {
std::string mappingFileNative = QDir::toNativeSeparators(mappingFile).toStdString();
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Loading gamepad mappings from: %s",
mappingFileNative.c_str());
int newMappings = SDL_GameControllerAddMappingsFromFile(mappingFileNative.c_str());
if (newMappings < 0) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Error loading gamepad mappings: %s",
SDL_GetError());
}
else {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Loaded %d new gamepad mappings",
newMappings);
}
}
else {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"No gamepad mapping file found");
}
QList<SdlGamepadMapping> mappings = m_Mappings.values();
for (const SdlGamepadMapping& mapping : mappings) {
QString sdlMappingString = mapping.getSdlMappingString();
int ret = SDL_GameControllerAddMapping(qPrintable(sdlMappingString));
if (ret < 0) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"Unable to add mapping: %s",
qPrintable(sdlMappingString));
}
else if (ret == 1) {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Loaded saved user mapping: %s",
qPrintable(sdlMappingString));
}
}
}
void MappingManager::addMapping(QString mappingString)
{
SdlGamepadMapping mapping(mappingString);
addMapping(mapping);
}
void MappingManager::addMapping(SdlGamepadMapping& mapping)
{
m_Mappings[mapping.getGuid()] = mapping;
}
+74
View File
@@ -0,0 +1,74 @@
#pragma once
#include <QSettings>
class SdlGamepadMapping
{
public:
SdlGamepadMapping() {}
SdlGamepadMapping(QString string)
{
QStringList mapping = string.split(",");
if (!mapping.isEmpty()) {
m_Guid = mapping[0];
string.remove(0, m_Guid.length() + 1);
m_Mapping = string;
}
}
SdlGamepadMapping(QString guid, QString mapping)
: m_Guid(guid),
m_Mapping(mapping)
{
}
bool operator==(const SdlGamepadMapping& other) const
{
return m_Guid == other.m_Guid && m_Mapping == other.m_Mapping;
}
QString getGuid() const
{
return m_Guid;
}
QString getMapping() const
{
return m_Mapping;
}
QString getSdlMappingString() const
{
if (m_Guid.isEmpty() || m_Mapping.isEmpty()) {
return "";
}
else {
return m_Guid + "," + m_Mapping;
}
}
private:
QString m_Guid;
QString m_Mapping;
};
class MappingManager
{
public:
MappingManager();
void addMapping(QString gamepadString);
void addMapping(SdlGamepadMapping& gamepadMapping);
void applyMappings();
void save();
private:
QMap<QString, SdlGamepadMapping> m_Mappings;
};
+28 -6
View File
@@ -1,5 +1,5 @@
#include "streamingpreferences.h"
#include "streaming/session.hpp"
#include "streaming/session.h"
#include "streaming/streamutils.h"
#include <QSettings>
@@ -20,9 +20,13 @@
#define SER_WINDOWMODE "windowmode"
#define SER_UNSUPPORTEDFPS "unsupportedfps"
#define SER_MDNS "mdns"
#define SER_QUITAPPAFTER "quitAppAfter"
#define SER_MOUSEACCELERATION "mouseacceleration"
#define SER_STARTWINDOWED "startwindowed"
#define SER_FRAMEPACING "framepacing"
StreamingPreferences::StreamingPreferences()
StreamingPreferences::StreamingPreferences(QObject *parent)
: QObject(parent)
{
reload();
}
@@ -41,9 +45,12 @@ void StreamingPreferences::reload()
multiController = settings.value(SER_MULTICONT, true).toBool();
unsupportedFps = settings.value(SER_UNSUPPORTEDFPS, false).toBool();
enableMdns = settings.value(SER_MDNS, true).toBool();
quitAppAfter = settings.value(SER_QUITAPPAFTER, 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,
static_cast<int>(AudioConfig::AC_AUTO)).toInt());
static_cast<int>(AudioConfig::AC_STEREO)).toInt());
videoCodecConfig = static_cast<VideoCodecConfig>(settings.value(SER_VIDEOCFG,
static_cast<int>(VideoCodecConfig::VCC_AUTO)).toInt());
videoDecoderSelection = static_cast<VideoDecoderSelection>(settings.value(SER_VIDEODEC,
@@ -68,7 +75,10 @@ void StreamingPreferences::save()
settings.setValue(SER_MULTICONT, multiController);
settings.setValue(SER_UNSUPPORTEDFPS, unsupportedFps);
settings.setValue(SER_MDNS, enableMdns);
settings.setValue(SER_QUITAPPAFTER, quitAppAfter);
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_VIDEOCFG, static_cast<int>(videoCodecConfig));
settings.setValue(SER_VIDEODEC, static_cast<int>(videoDecoderSelection));
@@ -89,6 +99,20 @@ bool StreamingPreferences::isRunningWayland()
return qgetenv("XDG_SESSION_TYPE") == QByteArray("wayland");
}
bool StreamingPreferences::isWow64()
{
#ifdef Q_OS_WIN32
return QSysInfo::currentCpuArchitecture() != QSysInfo::buildCpuArchitecture();
#else
return false;
#endif
}
QString StreamingPreferences::getUnmappedGamepads()
{
return SdlInputHandler::getUnmappedGamepads();
}
int StreamingPreferences::getMaximumStreamingFrameRate()
{
// Never let the maximum drop below 60 FPS
@@ -118,9 +142,7 @@ int StreamingPreferences::getMaximumStreamingFrameRate()
}
}
// Cap the frame rate at 120 FPS. Past this, the encoders start
// to max out and drop frames.
maxFrameRate = qMax(maxFrameRate, qMin(120, bestMode.refresh_rate));
maxFrameRate = qMax(maxFrameRate, bestMode.refresh_rate);
}
}
+16 -4
View File
@@ -8,7 +8,7 @@ class StreamingPreferences : public QObject
Q_OBJECT
public:
StreamingPreferences();
StreamingPreferences(QObject *parent = nullptr);
Q_INVOKABLE static int
getDefaultBitrate(int width, int height, int fps);
@@ -19,19 +19,22 @@ public:
Q_INVOKABLE static bool isRunningWayland();
Q_INVOKABLE static bool isWow64();
Q_INVOKABLE static int getMaximumStreamingFrameRate();
Q_INVOKABLE QRect getDesktopResolution(int displayIndex);
Q_INVOKABLE QRect getNativeResolution(int displayIndex);
Q_INVOKABLE QString getUnmappedGamepads();
void reload();
enum AudioConfig
{
AC_AUTO,
AC_FORCE_STEREO,
AC_FORCE_SURROUND
AC_STEREO,
AC_51_SURROUND
};
Q_ENUM(AudioConfig)
@@ -70,7 +73,10 @@ public:
Q_PROPERTY(bool multiController MEMBER multiController NOTIFY multiControllerChanged)
Q_PROPERTY(bool unsupportedFps MEMBER unsupportedFps NOTIFY unsupportedFpsChanged)
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 startWindowed MEMBER startWindowed NOTIFY startWindowedChanged)
Q_PROPERTY(bool framePacing MEMBER framePacing NOTIFY framePacingChanged)
Q_PROPERTY(AudioConfig audioConfig MEMBER audioConfig NOTIFY audioConfigChanged)
Q_PROPERTY(VideoCodecConfig videoCodecConfig MEMBER videoCodecConfig NOTIFY videoCodecConfigChanged)
Q_PROPERTY(VideoDecoderSelection videoDecoderSelection MEMBER videoDecoderSelection NOTIFY videoDecoderSelectionChanged)
@@ -87,7 +93,10 @@ public:
bool multiController;
bool unsupportedFps;
bool enableMdns;
bool quitAppAfter;
bool mouseAcceleration;
bool startWindowed;
bool framePacing;
AudioConfig audioConfig;
VideoCodecConfig videoCodecConfig;
VideoDecoderSelection videoDecoderSelection;
@@ -102,10 +111,13 @@ signals:
void multiControllerChanged();
void unsupportedFpsChanged();
void enableMdnsChanged();
void quitAppAfterChanged();
void mouseAccelerationChanged();
void audioConfigChanged();
void videoCodecConfigChanged();
void videoDecoderSelectionChanged();
void windowModeChanged();
void startWindowedChanged();
void framePacingChanged();
};
+80 -50
View File
@@ -1,13 +1,25 @@
#include "../session.hpp"
#include "../session.h"
#include "renderers/renderer.h"
#ifdef HAVE_SOUNDIO
#include "renderers/soundioaudiorenderer.h"
#endif
#include "renderers/sdl.h"
#include "renderers/qtaud.h"
#include <Limelight.h>
IAudioRenderer* Session::createAudioRenderer()
{
#ifdef HAVE_SOUNDIO
if (qgetenv("ML_AUDIO") == "SDL") {
return new SdlAudioRenderer();
}
return new SoundIoAudioRenderer();
#else
return new SdlAudioRenderer();
#endif
}
bool Session::testAudio(int audioConfiguration)
@@ -19,30 +31,31 @@ bool Session::testAudio(int audioConfiguration)
return false;
}
bool ret = audioRenderer->testAudio(audioConfiguration);
// Build a fake OPUS_MULTISTREAM_CONFIGURATION to give
// the renderer the channel count and sample rate.
OPUS_MULTISTREAM_CONFIGURATION opusConfig = {};
opusConfig.sampleRate = 48000;
switch (audioConfiguration)
{
case AUDIO_CONFIGURATION_STEREO:
opusConfig.channelCount = 2;
break;
case AUDIO_CONFIGURATION_51_SURROUND:
opusConfig.channelCount = 6;
break;
default:
SDL_assert(false);
return false;
}
bool ret = audioRenderer->prepareForPlayback(&opusConfig);
delete audioRenderer;
return ret;
}
int Session::detectAudioConfiguration()
{
IAudioRenderer* audioRenderer;
audioRenderer = createAudioRenderer();
if (audioRenderer == nullptr) {
// Hope for the best
return AUDIO_CONFIGURATION_STEREO;
}
int audioConfig = audioRenderer->detectAudioConfiguration();
delete audioRenderer;
return audioConfig;
}
int Session::arInit(int /* audioConfiguration */,
const POPUS_MULTISTREAM_CONFIGURATION opusConfig,
void* /* arContext */, int /* arFlags */)
@@ -66,15 +79,10 @@ int Session::arInit(int /* audioConfiguration */,
}
s_ActiveSession->m_AudioRenderer = s_ActiveSession->createAudioRenderer();
if (s_ActiveSession->m_AudioRenderer == nullptr) {
opus_multistream_decoder_destroy(s_ActiveSession->m_OpusDecoder);
return -2;
}
if (!s_ActiveSession->m_AudioRenderer->prepareForPlayback(opusConfig)) {
delete s_ActiveSession->m_AudioRenderer;
opus_multistream_decoder_destroy(s_ActiveSession->m_OpusDecoder);
return -3;
return -2;
}
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
@@ -86,39 +94,61 @@ int Session::arInit(int /* audioConfiguration */,
void Session::arCleanup()
{
// m_AudioRenderer is deleted in cleanupAudioRenderer()
delete s_ActiveSession->m_AudioRenderer;
s_ActiveSession->m_AudioRenderer = nullptr;
opus_multistream_decoder_destroy(s_ActiveSession->m_OpusDecoder);
s_ActiveSession->m_OpusDecoder = nullptr;
}
// This is called on the main thread
void Session::cleanupAudioRendererOnMainThread()
{
SDL_AtomicLock(&m_AudioRendererLock);
delete m_AudioRenderer;
m_AudioRenderer = nullptr;
SDL_AtomicUnlock(&m_AudioRendererLock);
}
void Session::arDecodeAndPlaySample(char* sampleData, int sampleLength)
{
int samplesDecoded;
samplesDecoded = opus_multistream_decode(s_ActiveSession->m_OpusDecoder,
(unsigned char*)sampleData,
sampleLength,
s_ActiveSession->m_OpusDecodeBuffer,
SAMPLES_PER_FRAME,
0);
if (samplesDecoded > 0) {
SDL_AtomicLock(&s_ActiveSession->m_AudioRendererLock);
if (s_ActiveSession->m_AudioRenderer != nullptr) {
s_ActiveSession->m_AudioRenderer->submitAudio(s_ActiveSession->m_OpusDecodeBuffer,
static_cast<int>(sizeof(short) *
samplesDecoded *
s_ActiveSession->m_AudioConfig.channelCount));
// Set this thread to high priority to reduce
// the chance of missing our sample delivery time
if (s_ActiveSession->m_AudioSampleCount == 0) {
if (SDL_SetThreadPriority(SDL_THREAD_PRIORITY_HIGH) < 0) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"Unable to set audio thread to high priority: %s",
SDL_GetError());
}
}
s_ActiveSession->m_AudioSampleCount++;
if (s_ActiveSession->m_AudioRenderer != nullptr) {
samplesDecoded = opus_multistream_decode(s_ActiveSession->m_OpusDecoder,
(unsigned char*)sampleData,
sampleLength,
s_ActiveSession->m_OpusDecodeBuffer,
SAMPLES_PER_FRAME,
0);
if (samplesDecoded > 0) {
if (!s_ActiveSession->m_AudioRenderer->submitAudio(s_ActiveSession->m_OpusDecodeBuffer,
static_cast<int>(
sizeof(short) *
samplesDecoded *
s_ActiveSession->m_AudioConfig.channelCount))) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"Reinitializing audio renderer after failure");
delete s_ActiveSession->m_AudioRenderer;
s_ActiveSession->m_AudioRenderer = nullptr;
}
}
}
// Only try to recreate the audio renderer every 200 samples (1 second)
// to avoid thrashing if the audio device is unavailable. It is
// safe to reinitialize here because we can't be torn down while
// the audio decoder/playback thread is still alive.
if (s_ActiveSession->m_AudioRenderer == nullptr && (s_ActiveSession->m_AudioSampleCount % 200) == 0) {
s_ActiveSession->m_AudioRenderer = s_ActiveSession->createAudioRenderer();
if (!s_ActiveSession->m_AudioRenderer->prepareForPlayback(&s_ActiveSession->m_AudioConfig)) {
delete s_ActiveSession->m_AudioRenderer;
s_ActiveSession->m_AudioRenderer = nullptr;
}
SDL_AtomicUnlock(&s_ActiveSession->m_AudioRendererLock);
}
}
+2 -5
View File
@@ -12,9 +12,6 @@ public:
virtual bool prepareForPlayback(const OPUS_MULTISTREAM_CONFIGURATION* opusConfig) = 0;
virtual void submitAudio(short* audioBuffer, int audioSize) = 0;
virtual bool testAudio(int audioConfiguration) = 0;
virtual int detectAudioConfiguration() = 0;
// Return false if an unrecoverable error has occurred and the renderer must be reinitialized
virtual bool submitAudio(short* audioBuffer, int audioSize) = 0;
};
+1 -10
View File
@@ -12,17 +12,8 @@ public:
virtual bool prepareForPlayback(const OPUS_MULTISTREAM_CONFIGURATION* opusConfig);
virtual void submitAudio(short* audioBuffer, int audioSize);
virtual bool testAudio(int audioConfiguration);
virtual int detectAudioConfiguration();
virtual bool submitAudio(short* audioBuffer, int audioSize);
private:
SDL_AudioDeviceID m_AudioDevice;
Uint32 m_ChannelCount;
Uint32 m_PendingDrops;
Uint32 m_PendingHardDrops;
Uint32 m_SampleIndex;
Uint32 m_BaselinePendingData;
};
+4 -141
View File
@@ -3,81 +3,10 @@
#include <Limelight.h>
#include <SDL.h>
#include <QAudioDeviceInfo>
#include <QtGlobal>
#define MIN_QUEUED_FRAMES 2
#define MAX_QUEUED_FRAMES 4
#define STOP_THE_WORLD_LIMIT 20
#define DROP_RATIO_DENOM 32
// Detecting this with SDL is quite problematic, so we'll use Qt's
// multimedia framework to do so. It appears to be actually
// accurate on Linux and macOS, unlike using SDL and relying
// on a channel change in the format received.
int SdlAudioRenderer::detectAudioConfiguration()
{
int preferredChannelCount = QAudioDeviceInfo::defaultOutputDevice().preferredFormat().channelCount();
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Audio output device prefers %d channel configuration",
preferredChannelCount);
// We can better downmix 5.1 to quad than we can upmix stereo
if (preferredChannelCount > 2) {
return AUDIO_CONFIGURATION_51_SURROUND;
}
else {
return AUDIO_CONFIGURATION_STEREO;
}
}
bool SdlAudioRenderer::testAudio(int audioConfiguration)
{
SDL_AudioSpec want, have;
SDL_AudioDeviceID dev;
SDL_zero(want);
want.freq = 48000;
want.format = AUDIO_S16;
want.samples = SAMPLES_PER_FRAME;
switch (audioConfiguration) {
case AUDIO_CONFIGURATION_STEREO:
want.channels = 2;
break;
case AUDIO_CONFIGURATION_51_SURROUND:
want.channels = 6;
break;
default:
SDL_assert(false);
return false;
}
// Test audio device for functionality
dev = SDL_OpenAudioDevice(NULL, 0, &want, &have, 0);
if (dev == 0) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Audio test - Failed to open audio device: %s",
SDL_GetError());
return false;
}
SDL_CloseAudioDevice(dev);
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Audio test - Successful with %d channels",
want.channels);
return true;
}
SdlAudioRenderer::SdlAudioRenderer()
: m_AudioDevice(0),
m_ChannelCount(0),
m_PendingDrops(0),
m_PendingHardDrops(0),
m_SampleIndex(0),
m_BaselinePendingData(0)
: m_AudioDevice(0)
{
SDL_assert(!SDL_WasInit(SDL_INIT_AUDIO));
if (SDL_InitSubSystem(SDL_INIT_AUDIO) != 0) {
@@ -122,27 +51,6 @@ bool SdlAudioRenderer::prepareForPlayback(const OPUS_MULTISTREAM_CONFIGURATION*
have.samples,
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
SDL_PauseAudioDevice(m_AudioDevice, 0);
@@ -161,58 +69,13 @@ SdlAudioRenderer::~SdlAudioRenderer()
SDL_assert(!SDL_WasInit(SDL_INIT_AUDIO));
}
void 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;
}
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;
}
if (SDL_QueueAudio(m_AudioDevice, audioBuffer, audioSize) < 0) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Failed to queue audio sample: %s",
SDL_GetError());
}
return true;
}
@@ -0,0 +1,463 @@
#include "soundioaudiorenderer.h"
#include <SDL.h>
#include <QtGlobal>
// GFE sends us packets in 5 ms chunks
const double SoundIoAudioRenderer::k_RawSampleLengthSec = 0.005;
#ifdef Q_OS_LINUX
// PulseAudio and ALSA require more than just 5 ms samples
// for some reason, so write a minimum of 25 ms each time to
// prevent underruns on Bluetooth.
// https://github.com/moonlight-stream/moonlight-qt/issues/147
// https://github.com/moonlight-stream/moonlight-qt/issues/157
const double SoundIoAudioRenderer::k_MinSampleLengthSec = 0.025;
#else
// This determines the size of the buffers we'll
// get from CoreAudio. It is also the minimum
// size that we will write when called to fill a buffer.
const double SoundIoAudioRenderer::k_MinSampleLengthSec = k_RawSampleLengthSec;
#endif
SoundIoAudioRenderer::SoundIoAudioRenderer()
: m_OpusChannelCount(0),
m_SoundIo(nullptr),
m_Device(nullptr),
m_OutputStream(nullptr),
m_RingBuffer(nullptr),
m_Latency(0),
m_Errored(false)
{
}
SoundIoAudioRenderer::~SoundIoAudioRenderer()
{
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Audio latency: %f",
m_Latency);
if (m_OutputStream != nullptr) {
soundio_outstream_destroy(m_OutputStream);
}
// Must be destroyed after the stream is stopped
// or we could still get sioWriteCallback() calls.
if (m_RingBuffer != nullptr) {
soundio_ring_buffer_destroy(m_RingBuffer);
}
if (m_Device != nullptr) {
soundio_device_unref(m_Device);
}
if (m_SoundIo != nullptr) {
soundio_destroy(m_SoundIo);
}
}
int SoundIoAudioRenderer::scoreChannelLayout(const struct SoundIoChannelLayout* layout, const OPUS_MULTISTREAM_CONFIGURATION* opusConfig)
{
int score = 50;
// Compute a score for this layout based on how many matching channels
// we find (or acceptable alternatives).
for (int i = 0; i < layout->channel_count; i++) {
if (opusConfig->channelCount >= 2) {
switch (layout->channels[i]) {
case SoundIoChannelIdFrontLeft:
case SoundIoChannelIdFrontRight:
score += 2;
break;
default:
break;
}
}
if (opusConfig->channelCount >= 6) {
switch (layout->channels[i]) {
case SoundIoChannelIdFrontCenter:
case SoundIoChannelIdLfe:
score += 2;
break;
case SoundIoChannelIdSideLeft:
case SoundIoChannelIdSideRight:
score++;
break;
// Score layouts using the back L/R as higher
// value than those using side L/R.
case SoundIoChannelIdBackLeft:
case SoundIoChannelIdBackRight:
score += 2;
break;
default:
break;
}
}
}
// Now subtract the difference between the desired and actual channel count
// to punish layouts that have extra unused speakers.
if (opusConfig->channelCount < layout->channel_count) {
score -= layout->channel_count - opusConfig->channelCount;
}
return score;
}
bool SoundIoAudioRenderer::prepareForPlayback(const OPUS_MULTISTREAM_CONFIGURATION* opusConfig)
{
m_SoundIo = soundio_create();
if (m_SoundIo == nullptr) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"soundio_create() failed");
return false;
}
m_SoundIo->app_name = "Moonlight";
m_SoundIo->userdata = this;
m_SoundIo->on_backend_disconnect = sioBackendDisconnect;
m_SoundIo->on_devices_change = sioDevicesChanged;
int err = soundio_connect(m_SoundIo);
if (err != SoundIoErrorNone) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"soundio_connect() failed: %s",
soundio_strerror(err));
return false;
}
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Audio backend: %s",
soundio_backend_name(m_SoundIo->current_backend));
// Don't continue if we could only open the dummy backend
if (m_SoundIo->current_backend == SoundIoBackendDummy) {
return false;
}
// Flush events to update with new device arrivals
soundio_flush_events(m_SoundIo);
// Remember the actual channel count for later
m_OpusChannelCount = opusConfig->channelCount;
int outputDeviceIndex = soundio_default_output_device_index(m_SoundIo);
if (outputDeviceIndex < 0) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"No output device found");
return false;
}
m_Device = soundio_get_output_device(m_SoundIo, outputDeviceIndex);
if (m_Device == nullptr) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"soundio_get_output_device() failed");
return false;
}
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Selected audio device: %s",
m_Device->name);
m_OutputStream = soundio_outstream_create(m_Device);
if (m_OutputStream == nullptr) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"soundio_outstream_create() failed");
return false;
}
m_OutputStream->format = SoundIoFormatS16NE;
m_OutputStream->sample_rate = opusConfig->sampleRate;
m_OutputStream->software_latency = k_MinSampleLengthSec;
m_OutputStream->name = "Moonlight";
m_OutputStream->userdata = this;
m_OutputStream->error_callback = sioErrorCallback;
m_OutputStream->write_callback = sioWriteCallback;
SoundIoChannelLayout bestLayout = m_Device->current_layout;
for (int i = 0; i < m_Device->layout_count; i++) {
if (scoreChannelLayout(&bestLayout, opusConfig) <
scoreChannelLayout(&m_Device->layouts[i], opusConfig)) {
bestLayout = m_Device->layouts[i];
}
}
if (bestLayout.channel_count < opusConfig->channelCount) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"No compatible channel layouts found. Some channels may not be played!");
}
m_OutputStream->layout = bestLayout;
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Native layout: %s (%d channels)",
m_OutputStream->layout.name ?
m_OutputStream->layout.name : "<UNKNOWN>",
m_OutputStream->layout.channel_count);
err = soundio_outstream_open(m_OutputStream);
if (err != SoundIoErrorNone) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"soundio_outstream_open() failed: %s",
soundio_strerror(err));
return false;
}
if (m_OutputStream->layout_error) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"Channel layout failed: %s",
soundio_strerror(m_OutputStream->layout_error));
// ALSA through PulseAudio appears to fail snd_pcm_set_chmap()
// even after claiming the layout is supported (and even on totally
// standard layouts like Stereo). We'll just ignore this for ALSA
// and only bail if we get an actual failure out of one of these APIs.
if (m_SoundIo->current_backend != SoundIoBackendAlsa) {
return false;
}
}
m_EffectiveLayout = m_OutputStream->layout;
for (int i = 0; i < m_EffectiveLayout.channel_count; i++) {
// Fixup the layout to use back L/R so our channel position
// logic in sioWriteCallback() works.
if (m_EffectiveLayout.channels[i] == SoundIoChannelIdSideLeft) {
m_EffectiveLayout.channels[i] = SoundIoChannelIdBackLeft;
}
if (m_EffectiveLayout.channels[i] == SoundIoChannelIdSideRight) {
m_EffectiveLayout.channels[i] = SoundIoChannelIdBackRight;
}
}
int packetsToBuffer;
#ifdef Q_OS_LINUX
// PulseAudio and ALSA need the large buffer (see comment on k_MinSampleLengthSec),
// so we need a buffer at least double that size to allow packets to arrive
// while we're writing to the sink.
packetsToBuffer = (int)(k_MinSampleLengthSec / k_RawSampleLengthSec) * 2;
#else
if (m_SoundIo->current_backend == SoundIoBackendWasapi) {
// 15 ms buffer seems to be fine for WASAPI
packetsToBuffer = 3;
}
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_OutputStream->bytes_per_sample *
m_OpusChannelCount *
SAMPLES_PER_FRAME *
packetsToBuffer);
if (m_RingBuffer == nullptr) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"soundio_ring_buffer_create() failed");
return false;
}
err = soundio_outstream_start(m_OutputStream);
if (err != SoundIoErrorNone) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"soundio_outstream_start() failed: %s",
soundio_strerror(err));
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;
}
bool SoundIoAudioRenderer::submitAudio(short* audioBuffer, int audioSize)
{
if (m_Errored) {
return false;
}
// Flush events to update with new device arrivals
soundio_flush_events(m_SoundIo);
// We must always write a full frame of audio. If we don't,
// the reader will get out of sync with the writer and our
// channels will get all mixed up. To ensure this is always
// the case, round our bytes free down to the next multiple
// of our frame size.
int bytesFree = soundio_ring_buffer_free_count(m_RingBuffer);
int bytesPerFrame = m_OpusChannelCount * m_OutputStream->bytes_per_sample;
int bytesToWrite = qMin(audioSize, (bytesFree / bytesPerFrame) * bytesPerFrame);
memcpy(soundio_ring_buffer_write_ptr(m_RingBuffer), audioBuffer, bytesToWrite);
soundio_ring_buffer_advance_write_ptr(m_RingBuffer, bytesToWrite);
return true;
}
void SoundIoAudioRenderer::sioErrorCallback(SoundIoOutStream* stream, int err)
{
auto me = reinterpret_cast<SoundIoAudioRenderer*>(stream->userdata);
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Audio rendering error: %s",
soundio_strerror(err));
// Trigger reinitialization
me->m_Errored = true;
}
void SoundIoAudioRenderer::sioBackendDisconnect(SoundIo* soundio, int err)
{
auto me = reinterpret_cast<SoundIoAudioRenderer*>(soundio->userdata);
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"Audio backend disconnected: %s",
soundio_strerror(err));
// Trigger reinitialization
me->m_Errored = true;
}
void SoundIoAudioRenderer::sioDevicesChanged(SoundIo* soundio)
{
auto me = reinterpret_cast<SoundIoAudioRenderer*>(soundio->userdata);
if (me->m_Device == nullptr) {
// Ignore calls that take place during initialization
return;
}
int outputDeviceIndex = soundio_default_output_device_index(soundio);
if (outputDeviceIndex >= 0) {
struct SoundIoDevice* outputDevice = soundio_get_output_device(soundio, outputDeviceIndex);
if (outputDevice == nullptr) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"soundio_get_output_device() failed");
return;
}
if (!soundio_device_equal(outputDevice, me->m_Device)) {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Default audio output device changed");
// Trigger reinitialization
me->m_Errored = true;
}
soundio_device_unref(outputDevice);
}
}
// bytes_per_frame should never be used on the ring buffer! It's not always
// the same number of bytes per frames as the output stream!
void SoundIoAudioRenderer::sioWriteCallback(SoundIoOutStream* stream, int frameCountMin, int frameCountMax)
{
auto me = reinterpret_cast<SoundIoAudioRenderer*>(stream->userdata);
char* readPtr = soundio_ring_buffer_read_ptr(me->m_RingBuffer);
int framesLeft = soundio_ring_buffer_fill_count(me->m_RingBuffer) /
(me->m_OpusChannelCount * stream->bytes_per_sample);
int bytesRead = 0;
// Clamp framesLeft to frameCountMax
framesLeft = qMin(framesLeft, frameCountMax);
// Ensure we always write at least a buffer, even if it's silence, to avoid
// busy looping when no audio data is available while libsoundio tries to keep
// us from starving the output device.
frameCountMin = qMax(frameCountMin, (int)(stream->sample_rate * k_MinSampleLengthSec));
frameCountMin = qMin(frameCountMin, frameCountMax);
// Track latency on queueing-based backends
if (me->m_SoundIo->current_backend != SoundIoBackendCoreAudio && me->m_SoundIo->current_backend != SoundIoBackendJack) {
soundio_outstream_get_latency(stream, &me->m_Latency);
}
for (;;) {
int frameCount;
int err;
struct SoundIoChannelArea* areas;
// Always meet the minimum but don't write more than that
// if we'll have to insert silence
frameCount = qMax(framesLeft, frameCountMin);
if (frameCount == 0) {
// Nothing more to write
break;
}
err = soundio_outstream_begin_write(stream, &areas, &frameCount);
if (err != SoundIoErrorNone) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"soundio_outstream_begin_write() failed: %s",
soundio_strerror(err));
break;
}
for (int frame = 0; frame < frameCount; frame++) {
for (int ch = 0; ch < me->m_EffectiveLayout.channel_count; ch++) {
// SoundIoChannelId - 1 happens to match Moonlight's channel layout.
// For side L/R, this logic depends on us fixing those up
// in m_EffectiveLayout to back L/R.
int readPtrChannel = me->m_EffectiveLayout.channels[ch] - 1;
if (frame >= framesLeft || readPtrChannel >= me->m_OpusChannelCount) {
// Write silence if we have no buffered frames left or
// nothing in the audio stream for this channel
memset(areas[ch].ptr, 0, stream->bytes_per_sample);
}
else {
// Write audio data from our ring buffer
memcpy(areas[ch].ptr,
&readPtr[readPtrChannel * stream->bytes_per_sample],
stream->bytes_per_sample);
}
areas[ch].ptr += areas[ch].step;
}
// Move on to the next frame if we aren't inserting silence
if (frame < framesLeft) {
readPtr += stream->bytes_per_sample * me->m_OpusChannelCount;
bytesRead += stream->bytes_per_sample * me->m_OpusChannelCount;
}
}
err = soundio_outstream_end_write(stream);
if (err != SoundIoErrorNone && err != SoundIoErrorUnderflow) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"soundio_outstream_end_write() failed: %s",
soundio_strerror(err));
break;
}
if (framesLeft >= frameCount) {
framesLeft -= frameCount;
}
else {
framesLeft = 0;
}
if (frameCountMin >= frameCount) {
frameCountMin -= frameCount;
}
else {
frameCountMin = 0;
}
}
soundio_ring_buffer_advance_read_ptr(me->m_RingBuffer, bytesRead);
}
@@ -0,0 +1,40 @@
#pragma once
#include "renderer.h"
#include <soundio/soundio.h>
class SoundIoAudioRenderer : public IAudioRenderer
{
public:
SoundIoAudioRenderer();
~SoundIoAudioRenderer();
virtual bool prepareForPlayback(const OPUS_MULTISTREAM_CONFIGURATION* opusConfig);
virtual bool submitAudio(short* audioBuffer, int audioSize);
private:
int scoreChannelLayout(const struct SoundIoChannelLayout* layout, const OPUS_MULTISTREAM_CONFIGURATION* opusConfig);
static void sioErrorCallback(struct SoundIoOutStream* stream, int err);
static void sioWriteCallback(struct SoundIoOutStream* stream, int frameCountMin, int frameCountMax);
static void sioBackendDisconnect(struct SoundIo* soundio, int err);
static void sioDevicesChanged(SoundIo* soundio);
int m_OpusChannelCount;
struct SoundIo* m_SoundIo;
struct SoundIoDevice* m_Device;
struct SoundIoOutStream* m_OutputStream;
struct SoundIoRingBuffer* m_RingBuffer;
struct SoundIoChannelLayout m_EffectiveLayout;
double m_Latency;
bool m_Errored;
static const double k_RawSampleLengthSec;
static const double k_MinSampleLengthSec;
};
+420 -113
View File
@@ -1,6 +1,7 @@
#include <Limelight.h>
#include <SDL.h>
#include "streaming/session.hpp"
#include "streaming/session.h"
#include "settings/mappingmanager.h"
#include "path.h"
#include <QtGlobal>
@@ -16,6 +17,17 @@
#define VK_NUMPAD0 0x60
#endif
#define MOUSE_POLLING_INTERVAL 5
// How long the mouse button will be pressed for a tap to click gesture
#define TAP_BUTTON_RELEASE_DELAY 100
// How long the fingers must be stationary to start a drag
#define DRAG_ACTIVATION_DELAY 650
// How far the finger can move before it cancels a drag or tap
#define DEAD_ZONE_DELTA 0.1f
const int SdlInputHandler::k_ButtonMap[] = {
A_FLAG, B_FLAG, X_FLAG, Y_FLAG,
BACK_FLAG, SPECIAL_FLAG, PLAY_FLAG,
@@ -24,10 +36,16 @@ const int SdlInputHandler::k_ButtonMap[] = {
UP_FLAG, DOWN_FLAG, LEFT_FLAG, RIGHT_FLAG
};
SdlInputHandler::SdlInputHandler(StreamingPreferences& prefs, NvComputer* computer)
: m_LastMouseMotionTime(0),
m_MultiController(prefs.multiController),
m_NeedsInputDelay(false)
SdlInputHandler::SdlInputHandler(StreamingPreferences& prefs, NvComputer*, int streamWidth, int streamHeight)
: m_MultiController(prefs.multiController),
m_MouseMoveTimer(0),
m_LeftButtonReleaseTimer(0),
m_RightButtonReleaseTimer(0),
m_DragTimer(0),
m_DragButton(0),
m_NumFingersDown(0),
m_StreamWidth(streamWidth),
m_StreamHeight(streamHeight)
{
// Allow gamepad input when the app doesn't have focus
SDL_SetHint(SDL_HINT_JOYSTICK_ALLOW_BACKGROUND_EVENTS, "1");
@@ -48,54 +66,22 @@ SdlInputHandler::SdlInputHandler(StreamingPreferences& prefs, NvComputer* comput
SDL_GetError());
}
QString mappingFile = Path::getGamepadMappingFile();
if (!mappingFile.isEmpty()) {
std::string mappingFileNative = QDir::toNativeSeparators(mappingFile).toStdString();
MappingManager mappingManager;
mappingManager.applyMappings();
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Loading gamepad mappings from: %s",
mappingFileNative.c_str());
int newMappings = SDL_GameControllerAddMappingsFromFile(mappingFileNative.c_str());
if (newMappings < 0) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Error loading gamepad mappings: %s",
SDL_GetError());
}
else {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Loaded %d new gamepad mappings",
newMappings);
}
}
else {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"No gamepad mapping file found");
}
if (!m_MultiController) {
// Player 1 is always present in non-MC mode
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
// GFE to choke and input latency to increase significantly. We will
// artificially throttle them to avoid this situation.
QVector<int> gfeVersion = NvHTTP::parseQuad(computer->gfeVersion);
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;
}
// Initialize the gamepad mask with currently attached gamepads to avoid
// causing gamepads to unexpectedly disappear and reappear on the host
// during stream startup as we detect currently attached gamepads one at a time.
m_GamepadMask = getAttachedGamepadMask();
SDL_zero(m_GamepadState);
SDL_zero(m_TouchDownEvent);
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()
@@ -106,6 +92,11 @@ SdlInputHandler::~SdlInputHandler()
}
}
SDL_RemoveTimer(m_MouseMoveTimer);
SDL_RemoveTimer(m_LeftButtonReleaseTimer);
SDL_RemoveTimer(m_RightButtonReleaseTimer);
SDL_RemoveTimer(m_DragTimer);
SDL_QuitSubSystem(SDL_INIT_GAMECONTROLLER);
SDL_assert(!SDL_WasInit(SDL_INIT_GAMECONTROLLER));
}
@@ -120,28 +111,13 @@ void SdlInputHandler::handleKeyEvent(SDL_KeyboardEvent* event)
(event->keysym.mod & KMOD_CTRL) &&
(event->keysym.mod & KMOD_ALT) &&
(event->keysym.mod & KMOD_SHIFT)) {
// Force raise all keys in the combo to avoid
// leaving them down after disconnecting
LiSendKeyboardEvent(0xA0, KEY_ACTION_UP, 0);
LiSendKeyboardEvent(0xA1, KEY_ACTION_UP, 0);
LiSendKeyboardEvent(0xA2, KEY_ACTION_UP, 0);
LiSendKeyboardEvent(0xA3, KEY_ACTION_UP, 0);
LiSendKeyboardEvent(0xA4, KEY_ACTION_UP, 0);
LiSendKeyboardEvent(0xA5, KEY_ACTION_UP, 0);
// Check for quit combo (Ctrl+Alt+Shift+Q)
if (event->keysym.sym == SDLK_q) {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Detected quit key combo");
// Uncapture the mouse to avoid processing any
// further keyboard input
SDL_SetRelativeMouseMode(SDL_FALSE);
SDL_Event event;
// Push a quit event to the main loop
SDL_Event event;
event.type = SDL_QUIT;
event.quit.timestamp = SDL_GetTicks();
SDL_PushEvent(&event);
@@ -151,7 +127,13 @@ void SdlInputHandler::handleKeyEvent(SDL_KeyboardEvent* event)
else if (event->keysym.sym == SDLK_z) {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Detected mouse capture toggle combo");
// 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;
}
// Check for the full-screen combo (Ctrl+Alt+Shift+X)
@@ -159,6 +141,10 @@ void SdlInputHandler::handleKeyEvent(SDL_KeyboardEvent* event)
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Detected full-screen toggle combo");
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;
}
}
@@ -384,6 +370,14 @@ void SdlInputHandler::handleKeyEvent(SDL_KeyboardEvent* event)
}
}
// Track the key state so we always know which keys are down
if (event->state == SDL_PRESSED) {
m_KeysDown.insert(keyCode);
}
else {
m_KeysDown.remove(keyCode);
}
LiSendKeyboardEvent(keyCode,
event->state == SDL_PRESSED ?
KEY_ACTION_DOWN : KEY_ACTION_UP,
@@ -409,6 +403,10 @@ void SdlInputHandler::handleMouseButtonEvent(SDL_MouseButtonEvent* event)
// Not capturing
return;
}
else if (event->which == SDL_TOUCH_MOUSEID) {
// Ignore synthetic mouse events
return;
}
switch (event->button)
{
@@ -422,9 +420,11 @@ void SdlInputHandler::handleMouseButtonEvent(SDL_MouseButtonEvent* event)
button = BUTTON_RIGHT;
break;
case SDL_BUTTON_X1:
button = BUTTON_X1;
break;
case SDL_BUTTON_X2:
// Unsupported by GameStream
return;
button = BUTTON_X2;
break;
default:
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Unhandled button event: %d",
@@ -444,40 +444,15 @@ void SdlInputHandler::handleMouseMotionEvent(SDL_MouseMotionEvent* event)
// Not capturing
return;
}
short xdelta = (short)event->xrel;
short ydelta = (short)event->yrel;
// If we're sending more than one motion event per millisecond,
// 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;
else if (event->which == SDL_TOUCH_MOUSEID) {
// Ignore synthetic mouse events
return;
}
if (xdelta != 0 || ydelta != 0) {
LiSendMouseMoveEvent(xdelta, ydelta);
}
// Batch until the next mouse polling window or we'll get awful
// input lag everything except GFE 3.14 and 3.15.
SDL_AtomicAdd(&m_MouseDeltaX, event->xrel);
SDL_AtomicAdd(&m_MouseDeltaY, event->yrel);
}
void SdlInputHandler::handleMouseWheelEvent(SDL_MouseWheelEvent* event)
@@ -486,6 +461,10 @@ void SdlInputHandler::handleMouseWheelEvent(SDL_MouseWheelEvent* event)
// Not capturing
return;
}
else if (event->which == SDL_TOUCH_MOUSEID) {
// Ignore synthetic mouse events
return;
}
if (event->y != 0) {
LiSendScrollEvent((signed char)event->y);
@@ -523,6 +502,50 @@ void SdlInputHandler::sendGamepadState(GamepadState* state)
state->rsY);
}
Uint32 SdlInputHandler::releaseLeftButtonTimerCallback(Uint32, void*)
{
LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_LEFT);
return 0;
}
Uint32 SdlInputHandler::releaseRightButtonTimerCallback(Uint32, void*)
{
LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_RIGHT);
return 0;
}
Uint32 SdlInputHandler::dragTimerCallback(Uint32, void *param)
{
auto me = reinterpret_cast<SdlInputHandler*>(param);
// Check how many fingers are down now to decide
// which button to hold down
if (me->m_NumFingersDown == 2) {
me->m_DragButton = BUTTON_RIGHT;
}
else if (me->m_NumFingersDown == 1) {
me->m_DragButton = BUTTON_LEFT;
}
LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, me->m_DragButton);
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)
{
GamepadState* state = findStateForGamepad(event->which);
@@ -579,6 +602,23 @@ void SdlInputHandler::handleControllerButtonEvent(SDL_ControllerButtonEvent* eve
state->buttons &= ~k_ButtonMap[event->button];
}
// Handle Start+Select+L1+R1 as a gamepad quit combo
if (state->buttons == (PLAY_FLAG | BACK_FLAG | LB_FLAG | RB_FLAG)) {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Detected quit gamepad button combo");
// Push a quit event to the main loop
SDL_Event event;
event.type = SDL_QUIT;
event.quit.timestamp = SDL_GetTicks();
SDL_PushEvent(&event);
// Clear buttons down on this gameapd
LiSendMultiControllerEvent(state->index, m_GamepadMask,
0, 0, 0, 0, 0, 0, 0);
return;
}
sendGamepadState(state);
}
@@ -589,8 +629,31 @@ void SdlInputHandler::handleControllerDeviceEvent(SDL_ControllerDeviceEvent* eve
if (event->type == SDL_CONTROLLERDEVICEADDED) {
int i;
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;
}
#if SDL_VERSION_ATLEAST(2, 0, 9)
// Determine this player's preferred index
i = SDL_GameControllerGetPlayerIndex(controller);
if (i < 0 || i >= MAX_GAMEPADS || m_GamepadState[i].controller != NULL) {
// If the player index is unavailable or invalid, start searching from 0
i = 0;
}
#else
// SDL 2.0.8 and earlier has no player number hints
i = 0;
#endif
for (; i < MAX_GAMEPADS; i++) {
if (m_GamepadState[i].controller == NULL) {
// Found an empty slot
break;
@@ -600,6 +663,7 @@ void SdlInputHandler::handleControllerDeviceEvent(SDL_ControllerDeviceEvent* eve
if (i == MAX_GAMEPADS) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"No open gamepad slots found!");
SDL_GameControllerClose(controller);
return;
}
@@ -611,26 +675,30 @@ void SdlInputHandler::handleControllerDeviceEvent(SDL_ControllerDeviceEvent* eve
// Always player 1 in single controller mode
state->index = 0;
}
state->controller = SDL_GameControllerOpen(event->which);
if (state->controller == NULL) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Failed to open gamepad: %s",
SDL_GetError());
return;
}
state->controller = controller;
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);
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Gamepad %d (player %d) is: %s",
"Gamepad %d (player %d) is: %s (%s -> %s)",
i,
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
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);
}
else {
@@ -668,9 +736,172 @@ void SdlInputHandler::handleControllerDeviceEvent(SDL_ControllerDeviceEvent* eve
}
}
void SdlInputHandler::handleJoystickArrivalEvent(SDL_JoyDeviceEvent* event)
{
SDL_assert(event->type == SDL_JOYDEVICEADDED);
if (!SDL_IsGameController(event->which)) {
char guidStr[33];
SDL_JoystickGetGUIDString(SDL_JoystickGetDeviceGUID(event->which),
guidStr, sizeof(guidStr));
const char* name = SDL_JoystickNameForIndex(event->which);
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"Joystick discovered with no mapping: %s %s",
name ? name : "<UNKNOWN>",
guidStr);
SDL_Joystick* joy = SDL_JoystickOpen(event->which);
if (joy != nullptr) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"Number of axes: %d | Number of buttons: %d | Number of hats: %d",
SDL_JoystickNumAxes(joy), SDL_JoystickNumButtons(joy),
SDL_JoystickNumHats(joy));
SDL_JoystickClose(joy);
}
else {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"Unable to open joystick for query: %s",
SDL_GetError());
}
}
}
void SdlInputHandler::handleTouchFingerEvent(SDL_TouchFingerEvent* event)
{
int fingerIndex = -1;
// Observations on Windows 10: x and y appear to be relative to 0,0 of the window client area.
// Although SDL documentation states they are 0.0 - 1.0 float values, they can actually be higher
// or lower than those values as touch events continue for touches started within the client area that
// leave the client area during a drag motion.
// dx and dy are deltas from the last touch event, not the first touch down.
// Determine the index of this finger using our list
// of fingers that are currently active on screen.
// This is also required to handle finger up which
// where the finger will not be in SDL_GetTouchFinger()
// anymore.
if (event->type != SDL_FINGERDOWN) {
for (int i = 0; i < MAX_FINGERS; i++) {
if (event->fingerId == m_TouchDownEvent[i].fingerId) {
fingerIndex = i;
break;
}
}
}
else {
// Resolve the new finger by determining the ID of each
// finger on the display.
int numTouchFingers = SDL_GetNumTouchFingers(event->touchId);
for (int i = 0; i < numTouchFingers; i++) {
SDL_Finger* finger = SDL_GetTouchFinger(event->touchId, i);
SDL_assert(finger != nullptr);
if (finger != nullptr) {
if (finger->id == event->fingerId) {
fingerIndex = i;
break;
}
}
}
}
if (fingerIndex < 0 || fingerIndex >= MAX_FINGERS) {
// Too many fingers
return;
}
// Handle cursor motion based on the position of the
// primary finger on screen
if (fingerIndex == 0) {
// The event x and y values are relative to our window width
// and height. However, we want to scale them to be relative
// to the host resolution. Fortunately this is easy since we
// already have normalized values. We'll just multiply them
// by the stream dimensions to get real X and Y values rather
// than the client window dimensions.
short deltaX = static_cast<short>(event->dx * m_StreamWidth);
short deltaY = static_cast<short>(event->dy * m_StreamHeight);
if (deltaX != 0 || deltaY != 0) {
LiSendMouseMoveEvent(deltaX, deltaY);
}
}
// Start a drag timer when primary or secondary
// fingers go down
if (event->type == SDL_FINGERDOWN &&
(fingerIndex == 0 || fingerIndex == 1)) {
SDL_RemoveTimer(m_DragTimer);
m_DragTimer = SDL_AddTimer(DRAG_ACTIVATION_DELAY,
dragTimerCallback,
this);
}
if (event->type == SDL_FINGERMOTION) {
// Count the total cumulative dx/dy that the finger
// has moved.
m_CumulativeDelta[fingerIndex] += qAbs(event->x);
m_CumulativeDelta[fingerIndex] += qAbs(event->y);
// If it's outside the deadzone delta, cancel drags and taps
if (m_CumulativeDelta[fingerIndex] > DEAD_ZONE_DELTA) {
SDL_RemoveTimer(m_DragTimer);
m_DragTimer = 0;
// This effectively cancels the tap logic below
m_TouchDownEvent[fingerIndex].timestamp = 0;
}
}
if (event->type == SDL_FINGERUP) {
// Cancel the drag timer on finger up
SDL_RemoveTimer(m_DragTimer);
m_DragTimer = 0;
// Release any drag
if (m_DragButton != 0) {
LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, m_DragButton);
m_DragButton = 0;
}
// 2 finger tap
else if (event->timestamp - m_TouchDownEvent[1].timestamp < 250) {
// Zero timestamp of the primary finger to ensure we won't
// generate a left click if the primary finger comes up soon.
m_TouchDownEvent[0].timestamp = 0;
// Press down the right mouse button
LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, BUTTON_RIGHT);
// Queue a timer to release it in 100 ms
SDL_RemoveTimer(m_RightButtonReleaseTimer);
m_RightButtonReleaseTimer = SDL_AddTimer(TAP_BUTTON_RELEASE_DELAY,
releaseRightButtonTimerCallback,
nullptr);
}
// 1 finger tap
else if (event->timestamp - m_TouchDownEvent[0].timestamp < 250) {
// Press down the left mouse button
LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, BUTTON_LEFT);
// Queue a timer to release it in 100 ms
SDL_RemoveTimer(m_LeftButtonReleaseTimer);
m_LeftButtonReleaseTimer = SDL_AddTimer(TAP_BUTTON_RELEASE_DELAY,
releaseLeftButtonTimerCallback,
nullptr);
}
}
m_NumFingersDown = SDL_GetNumTouchFingers(event->touchId);
if (event->type == SDL_FINGERDOWN) {
m_TouchDownEvent[fingerIndex] = *event;
m_CumulativeDelta[fingerIndex] = 0;
}
else if (event->type == SDL_FINGERUP) {
m_TouchDownEvent[fingerIndex] = {};
}
}
int SdlInputHandler::getAttachedGamepadMask()
{
int i;
int count;
int mask;
@@ -679,8 +910,9 @@ int SdlInputHandler::getAttachedGamepadMask()
return 0x1;
}
// TODO: Use SDL_GameControllerGetPlayerIndex() for accurate indexes?
count = mask = 0;
for (i = 0; i < SDL_NumJoysticks(); i++) {
for (int i = 0; i < SDL_NumJoysticks(); i++) {
if (SDL_IsGameController(i)) {
mask |= (1 << count++);
}
@@ -688,3 +920,78 @@ int SdlInputHandler::getAttachedGamepadMask()
return mask;
}
void SdlInputHandler::raiseAllKeys()
{
if (m_KeysDown.isEmpty()) {
return;
}
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Raising %d keys",
m_KeysDown.count());
for (auto keyDown : m_KeysDown) {
LiSendKeyboardEvent(keyDown, KEY_ACTION_UP, 0);
}
m_KeysDown.clear();
}
QString SdlInputHandler::getUnmappedGamepads()
{
QString ret;
if (SDL_InitSubSystem(SDL_INIT_GAMECONTROLLER) != 0) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"SDL_InitSubSystem(SDL_INIT_GAMECONTROLLER) failed: %s",
SDL_GetError());
}
MappingManager mappingManager;
mappingManager.applyMappings();
for (int i = 0; i < SDL_NumJoysticks(); i++) {
if (!SDL_IsGameController(i)) {
char guidStr[33];
SDL_JoystickGetGUIDString(SDL_JoystickGetDeviceGUID(i),
guidStr, sizeof(guidStr));
const char* name = SDL_JoystickNameForIndex(i);
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Unmapped joystick: %s %s",
name ? name : "<UNKNOWN>",
guidStr);
SDL_Joystick* joy = SDL_JoystickOpen(i);
if (joy != nullptr) {
int numButtons = SDL_JoystickNumButtons(joy);
int numHats = SDL_JoystickNumHats(joy);
int numAxes = SDL_JoystickNumAxes(joy);
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Number of axes: %d | Number of buttons: %d | Number of hats: %d",
numAxes, numButtons, numHats);
if ((numAxes >= 4 && numAxes <= 8) && numButtons >= 8 && numHats <= 1) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"Joystick likely to be an unmapped game controller");
if (!ret.isEmpty()) {
ret += ", ";
}
ret += name;
}
SDL_JoystickClose(joy);
}
else {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"Unable to open joystick for query: %s",
SDL_GetError());
}
}
}
SDL_QuitSubSystem(SDL_INIT_GAMECONTROLLER);
return ret;
}
@@ -17,11 +17,13 @@ struct GamepadState {
};
#define MAX_GAMEPADS 4
#define MAX_FINGERS 2
class SdlInputHandler
{
public:
explicit SdlInputHandler(StreamingPreferences& prefs, NvComputer* computer);
explicit SdlInputHandler(StreamingPreferences& prefs, NvComputer* computer,
int streamWidth, int streamHeight);
~SdlInputHandler();
@@ -39,19 +41,52 @@ public:
void handleControllerDeviceEvent(SDL_ControllerDeviceEvent* event);
void handleJoystickArrivalEvent(SDL_JoyDeviceEvent* event);
void handleTouchFingerEvent(SDL_TouchFingerEvent* event);
int getAttachedGamepadMask();
void raiseAllKeys();
static
QString getUnmappedGamepads();
private:
GamepadState*
findStateForGamepad(SDL_JoystickID id);
void sendGamepadState(GamepadState* state);
Uint32 m_LastMouseMotionTime;
static
Uint32 releaseLeftButtonTimerCallback(Uint32 interval, void* param);
static
Uint32 releaseRightButtonTimerCallback(Uint32 interval, void* param);
static
Uint32 dragTimerCallback(Uint32 interval, void* param);
static
Uint32 mouseMoveTimerCallback(Uint32 interval, void* param);
bool m_MultiController;
bool m_NeedsInputDelay;
SDL_TimerID m_MouseMoveTimer;
SDL_atomic_t m_MouseDeltaX;
SDL_atomic_t m_MouseDeltaY;
int m_GamepadMask;
GamepadState m_GamepadState[MAX_GAMEPADS];
QSet<short> m_KeysDown;
SDL_TouchFingerEvent m_TouchDownEvent[MAX_FINGERS];
float m_CumulativeDelta[MAX_FINGERS];
SDL_TimerID m_LeftButtonReleaseTimer;
SDL_TimerID m_RightButtonReleaseTimer;
SDL_TimerID m_DragTimer;
char m_DragButton;
int m_NumFingersDown;
int m_StreamWidth;
int m_StreamHeight;
static const int k_ButtonMap[];
};
+247 -167
View File
@@ -1,4 +1,4 @@
#include "session.hpp"
#include "session.h"
#include "settings/streamingpreferences.h"
#include "streaming/streamutils.h"
@@ -6,12 +6,6 @@
#include <SDL.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
#include "video/ffmpeg.h"
#endif
@@ -79,6 +73,7 @@ void Session::clStageFailed(int stage, long errorCode)
void Session::clConnectionTerminated(long errorCode)
{
s_ActiveSession->m_UnexpectedTermination = true;
emit s_ActiveSession->displayLaunchError("Connection terminated");
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
@@ -104,12 +99,16 @@ void Session::clLogMessage(const char* format, ...)
va_end(ap);
}
#define CALL_INITIALIZE(dec) (dec)->initialize(vds, window, videoFormat, width, height, frameRate, enableVsync)
#define CALL_INITIALIZE(dec) (dec)->initialize(vds, window, videoFormat, width, height, frameRate, enableVsync, enableFramePacing)
bool Session::chooseDecoder(StreamingPreferences::VideoDecoderSelection vds,
SDL_Window* window, int videoFormat, int width, int height,
int frameRate, bool enableVsync, IVideoDecoder*& chosenDecoder)
int frameRate, bool enableVsync, bool enableFramePacing, IVideoDecoder*& chosenDecoder)
{
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"V-sync %s",
enableVsync ? "enabled" : "disabled");
#ifdef HAVE_SLVIDEO
chosenDecoder = new SLVideoDecoder();
if (CALL_INITIALIZE(chosenDecoder)) {
@@ -170,25 +169,33 @@ int Session::drSubmitDecodeUnit(PDECODE_UNIT du)
// Use a lock since we'll be yanking this decoder out
// from underneath the session when we initiate destruction.
// We need to destroy the decoder on the main thread to satisfy
// some API constraints (like DXVA2).
// some API constraints (like DXVA2). If we can't acquire it,
// that means the decoder is about to be destroyed, so we can
// safely return DR_OK and wait for m_NeedsIdr to be set by
// the decoder reinitialization code.
SDL_AtomicLock(&s_ActiveSession->m_DecoderLock);
if (SDL_AtomicTryLock(&s_ActiveSession->m_DecoderLock)) {
if (s_ActiveSession->m_NeedsIdr) {
// If we reset our decoder, we'll need to request an IDR frame
s_ActiveSession->m_NeedsIdr = false;
SDL_AtomicUnlock(&s_ActiveSession->m_DecoderLock);
return DR_NEED_IDR;
}
if (s_ActiveSession->m_NeedsIdr) {
// If we reset our decoder, we'll need to request an IDR frame
s_ActiveSession->m_NeedsIdr = false;
SDL_AtomicUnlock(&s_ActiveSession->m_DecoderLock);
return DR_NEED_IDR;
}
IVideoDecoder* decoder = s_ActiveSession->m_VideoDecoder;
if (decoder != nullptr) {
int ret = decoder->submitDecodeUnit(du);
SDL_AtomicUnlock(&s_ActiveSession->m_DecoderLock);
return ret;
IVideoDecoder* decoder = s_ActiveSession->m_VideoDecoder;
if (decoder != nullptr) {
int ret = decoder->submitDecodeUnit(du);
SDL_AtomicUnlock(&s_ActiveSession->m_DecoderLock);
return ret;
}
else {
SDL_AtomicUnlock(&s_ActiveSession->m_DecoderLock);
return DR_OK;
}
}
else {
SDL_AtomicUnlock(&s_ActiveSession->m_DecoderLock);
// Decoder is going away. Ignore anything coming in until
// the lock is released.
return DR_OK;
}
}
@@ -214,17 +221,20 @@ bool Session::isHardwareDecodeAvailable(StreamingPreferences::VideoDecoderSelect
return false;
}
if (!chooseDecoder(vds, window, videoFormat, width, height, frameRate, true, decoder)) {
if (!chooseDecoder(vds, window, videoFormat, width, height, frameRate, true, false, decoder)) {
SDL_DestroyWindow(window);
SDL_QuitSubSystem(SDL_INIT_VIDEO);
return false;
}
SDL_DestroyWindow(window);
bool ret = decoder->isHardwareAccelerated();
delete decoder;
// This must be called after the decoder is deleted, because
// the renderer may want to interact with the window
SDL_DestroyWindow(window);
SDL_QuitSubSystem(SDL_INIT_VIDEO);
return ret;
@@ -251,24 +261,28 @@ int Session::getDecoderCapabilities(StreamingPreferences::VideoDecoderSelection
return false;
}
if (!chooseDecoder(vds, window, videoFormat, width, height, frameRate, true, decoder)) {
if (!chooseDecoder(vds, window, videoFormat, width, height, frameRate, true, false, decoder)) {
SDL_DestroyWindow(window);
SDL_QuitSubSystem(SDL_INIT_VIDEO);
return false;
}
SDL_DestroyWindow(window);
int caps = decoder->getDecoderCapabilities();
delete decoder;
// This must be called after the decoder is deleted, because
// the renderer may want to interact with the window
SDL_DestroyWindow(window);
SDL_QuitSubSystem(SDL_INIT_VIDEO);
return caps;
}
Session::Session(NvComputer* computer, NvApp& app)
: m_Computer(computer),
Session::Session(NvComputer* computer, NvApp& app, StreamingPreferences *preferences)
: m_Preferences(preferences ? preferences : new StreamingPreferences(this)),
m_Computer(computer),
m_App(app),
m_Window(nullptr),
m_VideoDecoder(nullptr),
@@ -278,11 +292,19 @@ Session::Session(NvComputer* computer, NvApp& app)
m_DisplayOriginX(0),
m_DisplayOriginY(0),
m_PendingWindowedTransition(false),
m_UnexpectedTermination(true), // Failure prior to streaming is unexpected
m_OpusDecoder(nullptr),
m_AudioRenderer(nullptr),
m_AudioRendererLock(0)
m_AudioSampleCount(0)
{
}
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()
@@ -294,9 +316,6 @@ void Session::initialize()
m_VideoCallbacks.setup = drSetup;
m_VideoCallbacks.submitDecodeUnit = drSubmitDecodeUnit;
// Submit for decode without using a separate thread
m_VideoCallbacks.capabilities |= CAPABILITY_DIRECT_SUBMIT;
// Slice up to 4 times for parallel decode, once slice per core
int slices = qMin(MAX_SLICES, SDL_GetCPUCount());
m_VideoCallbacks.capabilities |= CAPABILITY_SLICES_PER_FRAME(slices);
@@ -305,11 +324,13 @@ void Session::initialize()
slices);
LiInitializeStreamConfiguration(&m_StreamConfig);
m_StreamConfig.width = m_Preferences.width;
m_StreamConfig.height = m_Preferences.height;
m_StreamConfig.fps = m_Preferences.fps;
m_StreamConfig.bitrate = m_Preferences.bitrateKbps;
m_StreamConfig.width = m_Preferences->width;
m_StreamConfig.height = m_Preferences->height;
m_StreamConfig.fps = m_Preferences->fps;
m_StreamConfig.bitrate = m_Preferences->bitrateKbps;
m_StreamConfig.hevcBitratePercentageMultiplier = 75;
m_StreamConfig.streamingRemotely = STREAM_CFG_AUTO;
m_StreamConfig.packetSize = 1392;
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Video bitrate: %d kbps",
@@ -321,16 +342,12 @@ void Session::initialize()
// Only the first 4 bytes are populated in the RI key IV
RAND_bytes(reinterpret_cast<unsigned char*>(m_StreamConfig.remoteInputAesIv), 4);
switch (m_Preferences.audioConfig)
switch (m_Preferences->audioConfig)
{
case StreamingPreferences::AC_AUTO:
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Autodetecting audio configuration");
m_StreamConfig.audioConfiguration = detectAudioConfiguration();
break;
case StreamingPreferences::AC_FORCE_STEREO:
case StreamingPreferences::AC_STEREO:
m_StreamConfig.audioConfiguration = AUDIO_CONFIGURATION_STEREO;
break;
case StreamingPreferences::AC_FORCE_SURROUND:
case StreamingPreferences::AC_51_SURROUND:
m_StreamConfig.audioConfiguration = AUDIO_CONFIGURATION_51_SURROUND;
break;
}
@@ -339,26 +356,32 @@ void Session::initialize()
"Audio configuration: %d",
m_StreamConfig.audioConfiguration);
switch (m_Preferences.videoCodecConfig)
switch (m_Preferences->videoCodecConfig)
{
case StreamingPreferences::VCC_AUTO:
// TODO: Determine if HEVC is better depending on the decoder
m_StreamConfig.supportsHevc =
isHardwareDecodeAvailable(m_Preferences.videoDecoderSelection,
isHardwareDecodeAvailable(m_Preferences->videoDecoderSelection,
VIDEO_FORMAT_H265,
m_StreamConfig.width,
m_StreamConfig.height,
m_StreamConfig.fps);
// Oddly enough, the H.264 encoder on my GTX 1080 is actually more
// performant than the HEVC encoder (especially because split frame encode
// is disabled for HEVC by moonlight-common-c). Use H.264 for 1440p 100 FPS and above.
if (m_StreamConfig.width * m_StreamConfig.height * m_StreamConfig.fps >= 2560 * 1440 * 100) {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Preferring H.264 for higher performance");
m_StreamConfig.supportsHevc = false;
#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;
break;
case StreamingPreferences::VCC_FORCE_H264:
@@ -375,21 +398,7 @@ void Session::initialize()
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:
m_FullScreenFlag = SDL_WINDOW_FULLSCREEN_DESKTOP;
@@ -421,28 +430,28 @@ bool Session::validateLaunch()
{
QStringList warningList;
if (m_Preferences.videoDecoderSelection == StreamingPreferences::VDS_FORCE_SOFTWARE) {
if (m_Preferences->videoDecoderSelection == StreamingPreferences::VDS_FORCE_SOFTWARE) {
emitLaunchWarning("Your settings selection to force software decoding may cause poor streaming performance.");
}
if (m_Preferences.unsupportedFps && m_StreamConfig.fps > 60) {
if (m_Preferences->unsupportedFps && m_StreamConfig.fps > 60) {
emitLaunchWarning("Using unsupported FPS options may cause stuttering or lag.");
if (m_Preferences.enableVsync) {
if (m_Preferences->enableVsync) {
emitLaunchWarning("V-sync will be disabled when streaming at a higher frame rate than the display.");
}
}
if (m_StreamConfig.supportsHevc) {
bool hevcForced = m_Preferences.videoCodecConfig == StreamingPreferences::VCC_FORCE_HEVC ||
m_Preferences.videoCodecConfig == StreamingPreferences::VCC_FORCE_HEVC_HDR;
bool hevcForced = m_Preferences->videoCodecConfig == StreamingPreferences::VCC_FORCE_HEVC ||
m_Preferences->videoCodecConfig == StreamingPreferences::VCC_FORCE_HEVC_HDR;
if (!isHardwareDecodeAvailable(m_Preferences.videoDecoderSelection,
if (!isHardwareDecodeAvailable(m_Preferences->videoDecoderSelection,
VIDEO_FORMAT_H265,
m_StreamConfig.width,
m_StreamConfig.height,
m_StreamConfig.fps) &&
m_Preferences.videoDecoderSelection == StreamingPreferences::VDS_AUTO) {
m_Preferences->videoDecoderSelection == StreamingPreferences::VDS_AUTO) {
if (hevcForced) {
emitLaunchWarning("Using software decoding due to your selection to force HEVC without GPU support. This may cause poor streaming performance.");
}
@@ -478,7 +487,7 @@ bool Session::validateLaunch()
emitLaunchWarning("Your host PC GPU doesn't support HDR streaming. "
"A GeForce GTX 1000-series (Pascal) or later GPU is required for HDR streaming.");
}
else if (!isHardwareDecodeAvailable(m_Preferences.videoDecoderSelection,
else if (!isHardwareDecodeAvailable(m_Preferences->videoDecoderSelection,
VIDEO_FORMAT_H265_MAIN10,
m_StreamConfig.width,
m_StreamConfig.height,
@@ -501,30 +510,38 @@ bool Session::validateLaunch()
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.height = 1080;
// Test if audio works at the specified audio configuration
bool audioTestPassed = testAudio(m_StreamConfig.audioConfiguration);
// Gracefully degrade to stereo if 5.1 doesn't work
if (!audioTestPassed && m_StreamConfig.audioConfiguration == AUDIO_CONFIGURATION_51_SURROUND) {
audioTestPassed = testAudio(AUDIO_CONFIGURATION_STEREO);
if (audioTestPassed) {
m_StreamConfig.audioConfiguration = AUDIO_CONFIGURATION_STEREO;
emitLaunchWarning("5.1 surround sound is not supported by the current audio device.");
}
}
// Test that audio hardware is functional
m_AudioDisabled = !testAudio(m_StreamConfig.audioConfiguration);
// If nothing worked, warn the user that audio will not work
m_AudioDisabled = !audioTestPassed;
if (m_AudioDisabled) {
emitLaunchWarning("Failed to open audio device. Audio will be unavailable during this session.");
}
if (m_Preferences.videoDecoderSelection == StreamingPreferences::VDS_FORCE_HARDWARE &&
!isHardwareDecodeAvailable(m_Preferences.videoDecoderSelection,
// Check for unmapped gamepads
if (!SdlInputHandler::getUnmappedGamepads().isEmpty()) {
emitLaunchWarning("An attached gamepad has no mapping and won't be usable. Visit the Moonlight help to resolve this.");
}
if (m_Preferences->videoDecoderSelection == StreamingPreferences::VDS_FORCE_HARDWARE &&
!isHardwareDecodeAvailable(m_Preferences->videoDecoderSelection,
m_StreamConfig.supportsHevc ? VIDEO_FORMAT_H265 : VIDEO_FORMAT_H264,
m_StreamConfig.width,
m_StreamConfig.height,
m_StreamConfig.fps)) {
if (m_Preferences.videoCodecConfig == StreamingPreferences::VCC_AUTO) {
if (m_Preferences->videoCodecConfig == StreamingPreferences::VCC_AUTO) {
emit displayLaunchError("Your selection to force hardware decoding cannot be satisfied due to missing hardware decoding support on this PC's GPU.");
}
else {
@@ -536,7 +553,7 @@ bool Session::validateLaunch()
}
// Add the capability flags from the chosen decoder/renderer
m_VideoCallbacks.capabilities |= getDecoderCapabilities(m_Preferences.videoDecoderSelection,
m_VideoCallbacks.capabilities |= getDecoderCapabilities(m_Preferences->videoDecoderSelection,
m_StreamConfig.supportsHevc ? VIDEO_FORMAT_H265 : VIDEO_FORMAT_H264,
m_StreamConfig.width,
m_StreamConfig.height,
@@ -548,25 +565,65 @@ bool Session::validateLaunch()
class DeferredSessionCleanupTask : public QRunnable
{
void run() override
{
// Finish cleanup of the connection state
LiStopConnection();
public:
DeferredSessionCleanupTask(Session* session) :
m_Session(session) {}
private:
virtual ~DeferredSessionCleanupTask() override
{
// Allow another session to start now that we're cleaned up
Session::s_ActiveSession = nullptr;
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,
int& width, int& height)
{
int displayIndex = 0;
bool fullScreen;
if (m_Window != nullptr) {
displayIndex = SDL_GetWindowDisplayIndex(m_Window);
SDL_assert(displayIndex >= 0);
fullScreen = (SDL_GetWindowFlags(m_Window) & SDL_WINDOW_FULLSCREEN);
}
// Create our window on the same display that Qt's UI
// was being displayed on.
@@ -593,10 +650,16 @@ void Session::getWindowDimensions(int& x, int& y,
i, SDL_GetError());
}
}
fullScreen = (m_Preferences->windowMode != StreamingPreferences::WM_WINDOWED);
}
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;
height = usableBounds.h;
@@ -681,9 +744,13 @@ void Session::updateOptimalWindowDisplayMode()
bestMode = desktopMode;
}
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Chosen best display mode: %dx%dx%d",
bestMode.w, bestMode.h, bestMode.refresh_rate);
if ((SDL_GetWindowFlags(m_Window) & SDL_WINDOW_FULLSCREEN_DESKTOP) == SDL_WINDOW_FULLSCREEN) {
// Only print when the window is actually in full-screen exclusive mode,
// 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);
}
@@ -718,6 +785,7 @@ void Session::exec(int displayOriginX, int displayOriginY)
// Check for validation errors/warnings and emit
// signals for them, if appropriate
if (!validateLaunch()) {
emit sessionFinished();
return;
}
@@ -738,39 +806,33 @@ void Session::exec(int displayOriginX, int displayOriginY)
// Initialize the gamepad code with our preferences
StreamingPreferences prefs;
SdlInputHandler inputHandler(prefs, m_Computer);
SdlInputHandler inputHandler(prefs, m_Computer,
m_StreamConfig.width,
m_StreamConfig.height);
// The UI should have ensured the old game was already quit
// if we decide to stream a different game.
Q_ASSERT(m_Computer->currentGameId == 0 ||
m_Computer->currentGameId == m_App.id);
bool enableGameOptimizations = false;
for (const NvDisplayMode &mode : m_Computer->displayModes) {
if (mode.width == m_StreamConfig.width &&
mode.height == m_StreamConfig.height) {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Found host supported resolution: %dx%d",
mode.width, mode.height);
enableGameOptimizations = prefs.gameOptimizations;
break;
}
}
try {
NvHTTP http(m_Computer->activeAddress);
NvHTTP http(m_Computer->activeAddress, m_Computer->serverCert);
if (m_Computer->currentGameId != 0) {
http.resumeApp(&m_StreamConfig);
}
else {
http.launchApp(m_App.id, &m_StreamConfig,
enableGameOptimizations,
prefs.gameOptimizations,
prefs.playAudioOnHost,
inputHandler.getAttachedGamepadMask());
}
} catch (const GfeHttpResponseException& e) {
emit displayLaunchError(e.toQString());
s_ActiveSessionSemaphore.release();
QThreadPool::globalInstance()->start(new DeferredSessionCleanupTask(this));
return;
} catch (const QtNetworkReplyException& e) {
emit displayLaunchError(e.toQString());
QThreadPool::globalInstance()->start(new DeferredSessionCleanupTask(this));
return;
}
@@ -780,7 +842,7 @@ void Session::exec(int displayOriginX, int displayOriginY)
"SDL_InitSubSystem(SDL_INIT_VIDEO) failed: %s",
SDL_GetError());
emit displayLaunchError(QString::fromLocal8Bit(SDL_GetError()));
s_ActiveSessionSemaphore.release();
QThreadPool::globalInstance()->start(new DeferredSessionCleanupTask(this));
return;
}
@@ -808,7 +870,7 @@ void Session::exec(int displayOriginX, int displayOriginY)
// We already displayed an error dialog in the stage failure
// listener.
SDL_QuitSubSystem(SDL_INIT_VIDEO);
s_ActiveSessionSemaphore.release();
QThreadPool::globalInstance()->start(new DeferredSessionCleanupTask(this));
return;
}
@@ -830,14 +892,35 @@ void Session::exec(int displayOriginX, int displayOriginY)
"SDL_CreateWindow() failed: %s",
SDL_GetError());
SDL_QuitSubSystem(SDL_INIT_VIDEO);
s_ActiveSessionSemaphore.release();
QThreadPool::globalInstance()->start(new DeferredSessionCleanupTask(this));
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()
// again after creating a window to allow it to account
// for window chrome size.
if (m_Preferences.windowMode == StreamingPreferences::WM_WINDOWED) {
if (m_Preferences->windowMode == StreamingPreferences::WM_WINDOWED) {
getWindowDimensions(x, y, width, height);
// We must set the size before the position because centering
@@ -857,30 +940,11 @@ void Session::exec(int displayOriginX, int displayOriginY)
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
// Capture the mouse by default on release builds only.
// This prevents the mouse from becoming trapped inside
// Moonlight when it's halted at a debug break.
if (m_Preferences.windowMode != StreamingPreferences::WM_WINDOWED) {
if (m_Preferences->windowMode != StreamingPreferences::WM_WINDOWED) {
SDL_SetRelativeMouseMode(SDL_TRUE);
}
#endif
@@ -894,13 +958,6 @@ void Session::exec(int displayOriginX, int displayOriginY)
// Disable the screen saver
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
// sleep precision and more accurate callback timing.
SDL_SetHint(SDL_HINT_TIMER_RESOLUTION, "1");
@@ -915,6 +972,11 @@ void Session::exec(int displayOriginX, int displayOriginY)
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
// because we want to suspend all Qt processing until the stream is over.
SDL_Event event;
@@ -978,10 +1040,19 @@ void Session::exec(int displayOriginX, int displayOriginY)
}
#endif
// Release mouse cursor when another window is activated (e.g. by using ALT+TAB).
// This lets user to interact with our window's title bar and with the buttons in it.
if (event.window.event == SDL_WINDOWEVENT_FOCUS_LOST) {
SDL_SetRelativeMouseMode(SDL_FALSE);
// Release mouse cursor when another window is activated (e.g. by using ALT+TAB).
// This lets user to interact with our window's title bar and with the buttons in it.
// Doing this while the window is full-screen breaks the transition out of FS
// (desktop and exclusive), so we must check for that before releasing mouse capture.
if (!(SDL_GetWindowFlags(m_Window) & SDL_WINDOW_FULLSCREEN)) {
SDL_SetRelativeMouseMode(SDL_FALSE);
}
// 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.
inputHandler.raiseAllKeys();
}
// We want to recreate the decoder for resizes (full-screen toggles) and the initial shown event.
@@ -1040,7 +1111,7 @@ void Session::exec(int displayOriginX, int displayOriginY)
// forcefully disable V-sync to allow the stream to render faster
// than the display.
int displayHz = StreamUtils::getDisplayRefreshRate(m_Window);
bool enableVsync = m_Preferences.enableVsync;
bool enableVsync = m_Preferences->enableVsync;
if (displayHz + 5 < m_StreamConfig.fps) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"Disabling V-sync because refresh rate limit exceeded");
@@ -1049,10 +1120,11 @@ void Session::exec(int displayOriginX, int displayOriginY)
// Choose a new decoder (hopefully the same one, but possibly
// not if a GPU was removed or something).
if (!chooseDecoder(m_Preferences.videoDecoderSelection,
if (!chooseDecoder(m_Preferences->videoDecoderSelection,
m_Window, m_ActiveVideoFormat, m_ActiveVideoWidth,
m_ActiveVideoHeight, m_ActiveVideoFrameRate,
enableVsync,
enableVsync && m_Preferences->framePacing,
s_ActiveSession->m_VideoDecoder)) {
SDL_AtomicUnlock(&m_DecoderLock);
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
@@ -1093,35 +1165,43 @@ void Session::exec(int displayOriginX, int displayOriginY)
case SDL_CONTROLLERDEVICEREMOVED:
inputHandler.handleControllerDeviceEvent(&event.cdevice);
break;
case SDL_JOYDEVICEADDED:
inputHandler.handleJoystickArrivalEvent(&event.jdevice);
break;
// SDL2 sends touch events from trackpads by default on
// macOS. This totally screws our actual mouse handling,
// so we must explicitly ignore touch events on macOS.
#ifndef Q_OS_DARWIN
case SDL_FINGERDOWN:
case SDL_FINGERMOTION:
case SDL_FINGERUP:
inputHandler.handleTouchFingerEvent(&event.tfinger);
break;
#endif
}
}
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"SDL_WaitEvent() failed: %s",
SDL_GetError());
DispatchDeferredCleanup:
#ifdef Q_OS_WIN32
// HACK: See comment above
SetThreadExecutionState(ES_CONTINUOUS);
#endif
// Uncapture the mouse and hide the window immediately,
// so we can return to the Qt GUI ASAP.
SDL_SetRelativeMouseMode(SDL_FALSE);
SDL_EnableScreenSaver();
SDL_SetHint(SDL_HINT_TIMER_RESOLUTION, "0");
// Raise any keys that are still down
inputHandler.raiseAllKeys();
// Destroy the decoder, since this must be done on the main thread
SDL_AtomicLock(&m_DecoderLock);
delete m_VideoDecoder;
m_VideoDecoder = nullptr;
SDL_AtomicUnlock(&m_DecoderLock);
// Destroy the audio renderer which must also be done on the main thread
cleanupAudioRendererOnMainThread();
// This must be called after the decoder is deleted, because
// the renderer may want to interact with the window
SDL_DestroyWindow(m_Window);
if (iconSurface != nullptr) {
SDL_FreeSurface(iconSurface);
}
@@ -1131,6 +1211,6 @@ DispatchDeferredCleanup:
// Cleanup can take a while, so dispatch it to a worker thread.
// When it is complete, it will release our s_ActiveSessionSemaphore
// reference.
QThreadPool::globalInstance()->start(new DeferredSessionCleanupTask());
QThreadPool::globalInstance()->start(new DeferredSessionCleanupTask(this));
}
@@ -6,7 +6,7 @@
#include <opus_multistream.h>
#include "backend/computermanager.h"
#include "settings/streamingpreferences.h"
#include "input.hpp"
#include "input.h"
#include "video/decoder.h"
#include "audio/renderers/renderer.h"
@@ -18,7 +18,9 @@ class Session : public QObject
friend class DeferredSessionCleanupTask;
public:
explicit Session(NvComputer* computer, NvApp& app);
explicit Session(NvComputer* computer, NvApp& app, StreamingPreferences *preferences = nullptr);
virtual ~Session();
Q_INVOKABLE void exec(int displayOriginX, int displayOriginY);
@@ -41,6 +43,10 @@ signals:
void displayLaunchWarning(QString text);
void quitStarting();
void sessionFinished();
private:
void initialize();
@@ -54,8 +60,6 @@ private:
int detectAudioConfiguration();
void cleanupAudioRendererOnMainThread();
bool testAudio(int audioConfiguration);
void getWindowDimensions(int& x, int& y,
@@ -68,7 +72,8 @@ private:
static
bool chooseDecoder(StreamingPreferences::VideoDecoderSelection vds,
SDL_Window* window, int videoFormat, int width, int height,
int frameRate, bool enableVsync, IVideoDecoder*& chosenDecoder);
int frameRate, bool enableVsync, bool enableFramePacing,
IVideoDecoder*& chosenDecoder);
static
void clStageStarting(int stage);
@@ -102,7 +107,7 @@ private:
static
int drSubmitDecodeUnit(PDECODE_UNIT du);
StreamingPreferences m_Preferences;
StreamingPreferences* m_Preferences;
STREAM_CONFIGURATION m_StreamConfig;
DECODER_RENDERER_CALLBACKS m_VideoCallbacks;
NvComputer* m_Computer;
@@ -116,6 +121,7 @@ private:
int m_DisplayOriginX;
int m_DisplayOriginY;
bool m_PendingWindowedTransition;
bool m_UnexpectedTermination;
int m_ActiveVideoFormat;
int m_ActiveVideoWidth;
@@ -126,7 +132,7 @@ private:
short m_OpusDecodeBuffer[MAX_CHANNELS * SAMPLES_PER_FRAME];
IAudioRenderer* m_AudioRenderer;
OPUS_MULTISTREAM_CONFIGURATION m_AudioConfig;
SDL_SpinLock m_AudioRendererLock;
int m_AudioSampleCount;
static AUDIO_RENDERER_CALLBACKS k_AudioCallbacks;
static CONNECTION_LISTENER_CALLBACKS k_ConnCallbacks;
+18 -1
View File
@@ -8,6 +8,22 @@
#define MAX_SLICES 4
typedef struct _VIDEO_STATS {
uint32_t receivedFrames;
uint32_t decodedFrames;
uint32_t renderedFrames;
uint32_t networkDroppedFrames;
uint32_t pacerDroppedFrames;
uint32_t totalReassemblyTime;
uint32_t totalDecodeTime;
uint32_t totalPacerTime;
uint32_t totalRenderTime;
float receivedFps;
float decodedFps;
float renderedFps;
uint32_t measurementStartTimestamp;
} VIDEO_STATS, *PVIDEO_STATS;
class IVideoDecoder {
public:
virtual ~IVideoDecoder() {}
@@ -17,7 +33,8 @@ public:
int width,
int height,
int frameRate,
bool enableVsync) = 0;
bool enableVsync,
bool enableFramePacing) = 0;
virtual bool isHardwareAccelerated() = 0;
virtual int getDecoderCapabilities() = 0;
virtual int submitDecodeUnit(PDECODE_UNIT du) = 0;
+38 -60
View File
@@ -266,74 +266,52 @@ bool DXVA2Renderer::initializeRenderer()
return false;
}
UINT guidCount;
GUID* guids;
hr = m_ProcService->GetVideoProcessorDeviceGuids(&m_Desc, &guidCount, &guids);
DXVA2_VideoProcessorCaps caps;
hr = m_ProcService->GetVideoProcessorCaps(DXVA2_VideoProcProgressiveDevice, &m_Desc, renderTargetDesc.Format, &caps);
if (FAILED(hr)) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"GetVideoProcessorDeviceGuids() failed: %x",
"GetVideoProcessorCaps() failed for DXVA2_VideoProcProgressiveDevice: %x",
hr);
return false;
}
UINT i;
for (i = 0; i < guidCount; i++) {
DXVA2_VideoProcessorCaps caps;
hr = m_ProcService->GetVideoProcessorCaps(guids[i], &m_Desc, renderTargetDesc.Format, &caps);
if (SUCCEEDED(hr)) {
if (!(caps.DeviceCaps & DXVA2_VPDev_HardwareDevice)) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"Device %d is not hardware: %x",
i,
caps.DeviceCaps);
continue;
}
else if (!(caps.VideoProcessorOperations & DXVA2_VideoProcess_YUV2RGB) &&
!(caps.VideoProcessorOperations & DXVA2_VideoProcess_YUV2RGBExtended)) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"Device %d can't convert YUV2RGB: %x",
i,
caps.VideoProcessorOperations);
continue;
}
else if (!(caps.VideoProcessorOperations & DXVA2_VideoProcess_StretchX) ||
!(caps.VideoProcessorOperations & DXVA2_VideoProcess_StretchY)) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"Device %d can't stretch video: %x",
i,
caps.VideoProcessorOperations);
continue;
}
m_ProcService->GetProcAmpRange(guids[i], &m_Desc, renderTargetDesc.Format, DXVA2_ProcAmp_Brightness, &m_BrightnessRange);
m_ProcService->GetProcAmpRange(guids[i], &m_Desc, renderTargetDesc.Format, DXVA2_ProcAmp_Contrast, &m_ContrastRange);
m_ProcService->GetProcAmpRange(guids[i], &m_Desc, renderTargetDesc.Format, DXVA2_ProcAmp_Hue, &m_HueRange);
m_ProcService->GetProcAmpRange(guids[i], &m_Desc, renderTargetDesc.Format, DXVA2_ProcAmp_Saturation, &m_SaturationRange);
hr = m_ProcService->CreateVideoProcessor(guids[i], &m_Desc, renderTargetDesc.Format, 0, &m_Processor);
if (FAILED(hr)) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"CreateVideoProcessor() failed for GUID %d: %x",
i,
hr);
continue;
}
break;
}
else {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"GetVideoProcessorCaps() failed for GUID %d: %x",
i,
hr);
}
if (!(caps.DeviceCaps & DXVA2_VPDev_HardwareDevice)) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"DXVA2_VideoProcProgressiveDevice is not hardware: %x",
caps.DeviceCaps);
return false;
}
else if (!(caps.VideoProcessorOperations & DXVA2_VideoProcess_YUV2RGB) &&
!(caps.VideoProcessorOperations & DXVA2_VideoProcess_YUV2RGBExtended)) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"DXVA2_VideoProcProgressiveDevice can't convert YUV2RGB: %x",
caps.VideoProcessorOperations);
return false;
}
else if (!(caps.VideoProcessorOperations & DXVA2_VideoProcess_StretchX) ||
!(caps.VideoProcessorOperations & DXVA2_VideoProcess_StretchY)) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"DXVA2_VideoProcProgressiveDevice can't stretch video: %x",
caps.VideoProcessorOperations);
return false;
}
CoTaskMemFree(guids);
if (caps.DeviceCaps & DXVA2_VPDev_EmulatedDXVA1) {
// DXVA2 over DXVA1 may have bad performance
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"DXVA2_VideoProcProgressiveDevice is DXVA1");
}
if (i == guidCount) {
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_Hue, &m_HueRange);
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);
if (FAILED(hr)) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Unable to find a usable DXVA2 processor");
"CreateVideoProcessor() failed for DXVA2_VideoProcProgressiveDevice: %x",
hr);
return false;
}
@@ -666,9 +644,9 @@ int DXVA2Renderer::getDecoderCapabilities()
return 0;
}
IFFmpegRenderer::VSyncConstraint DXVA2Renderer::getVsyncConstraint()
IFFmpegRenderer::FramePacingConstraint DXVA2Renderer::getFramePacingConstraint()
{
return VSYNC_ANY;
return PACING_ANY;
}
void DXVA2Renderer::renderFrameAtVsync(AVFrame *frame)
+1 -1
View File
@@ -25,7 +25,7 @@ public:
virtual void renderFrameAtVsync(AVFrame* frame);
virtual bool needsTestFrame();
virtual int getDecoderCapabilities();
virtual VSyncConstraint getVsyncConstraint();
virtual FramePacingConstraint getFramePacingConstraint();
private:
bool initializeDecoder();
@@ -20,12 +20,13 @@
// V-sync happens.
#define TIMER_SLACK_MS 3
Pacer::Pacer(IFFmpegRenderer* renderer) :
Pacer::Pacer(IFFmpegRenderer* renderer, PVIDEO_STATS videoStats) :
m_FrameQueueLock(0),
m_VsyncSource(nullptr),
m_VsyncRenderer(renderer),
m_MaxVideoFps(0),
m_DisplayFps(0)
m_DisplayFps(0),
m_VideoStats(videoStats)
{
}
@@ -82,10 +83,10 @@ void Pacer::vsyncCallback(int timeUntilNextVsyncMillis)
// Catch up if we're several frames ahead
while (m_FrameQueue.count() > frameDropTarget) {
AVFrame* frame = m_FrameQueue.dequeue();
m_VideoStats->pacerDroppedFrames++;
av_frame_free(&frame);
}
if (m_FrameQueue.isEmpty()) {
SDL_AtomicUnlock(&m_FrameQueueLock);
@@ -110,22 +111,27 @@ RenderNextFrame:
AVFrame* frame = m_FrameQueue.dequeue();
SDL_AtomicUnlock(&m_FrameQueueLock);
// Count time spent in Pacer's queues
Uint32 beforeRender = SDL_GetTicks();
m_VideoStats->totalPacerTime += beforeRender - frame->pts;
// Render it
m_VsyncRenderer->renderFrameAtVsync(frame);
m_VideoStats->totalRenderTime += SDL_GetTicks() - beforeRender;
m_VideoStats->renderedFrames++;
// Free the frame
av_frame_free(&frame);
}
bool Pacer::initialize(SDL_Window* window, int maxVideoFps, bool enableVsync)
bool Pacer::initialize(SDL_Window* window, int maxVideoFps, bool enablePacing)
{
m_MaxVideoFps = maxVideoFps;
m_EnableVsync = enableVsync;
m_DisplayFps = StreamUtils::getDisplayRefreshRate(window);
if (m_EnableVsync) {
if (enablePacing) {
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);
#if defined(Q_OS_DARWIN)
@@ -143,7 +149,7 @@ bool Pacer::initialize(SDL_Window* window, int maxVideoFps, bool enableVsync)
}
else {
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);
}
@@ -164,7 +170,10 @@ void Pacer::submitFrame(AVFrame* frame)
SDL_AtomicUnlock(&m_FrameQueueLock);
}
else {
Uint32 beforeRender = SDL_GetTicks();
m_VsyncRenderer->renderFrameAtVsync(frame);
m_VideoStats->totalRenderTime += SDL_GetTicks() - beforeRender;
m_VideoStats->renderedFrames++;
av_frame_free(&frame);
}
}
@@ -1,5 +1,6 @@
#pragma once
#include "../../decoder.h"
#include "../renderer.h"
#include <QQueue>
@@ -13,13 +14,13 @@ public:
class Pacer
{
public:
Pacer(IFFmpegRenderer* renderer);
Pacer(IFFmpegRenderer* renderer, PVIDEO_STATS videoStats);
~Pacer();
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);
@@ -34,5 +35,5 @@ private:
IFFmpegRenderer* m_VsyncRenderer;
int m_MaxVideoFps;
int m_DisplayFps;
bool m_EnableVsync;
PVIDEO_STATS m_VideoStats;
};
@@ -8,10 +8,10 @@ extern "C" {
class IFFmpegRenderer {
public:
enum VSyncConstraint {
VSYNC_FORCE_OFF,
VSYNC_FORCE_ON,
VSYNC_ANY
enum FramePacingConstraint {
PACING_FORCE_OFF,
PACING_FORCE_ON,
PACING_ANY
};
virtual ~IFFmpegRenderer() {}
@@ -25,7 +25,7 @@ public:
virtual void renderFrameAtVsync(AVFrame* frame) = 0;
virtual bool needsTestFrame() = 0;
virtual int getDecoderCapabilities() = 0;
virtual VSyncConstraint getVsyncConstraint() = 0;
virtual FramePacingConstraint getFramePacingConstraint() = 0;
};
class SdlRenderer : public IFFmpegRenderer {
@@ -42,7 +42,7 @@ public:
virtual void renderFrameAtVsync(AVFrame* frame);
virtual bool needsTestFrame();
virtual int getDecoderCapabilities();
virtual VSyncConstraint getVsyncConstraint();
virtual FramePacingConstraint getFramePacingConstraint();
private:
SDL_Renderer* m_Renderer;
@@ -43,9 +43,9 @@ int SdlRenderer::getDecoderCapabilities()
return CAPABILITY_REFERENCE_FRAME_INVALIDATION_AVC;
}
IFFmpegRenderer::VSyncConstraint SdlRenderer::getVsyncConstraint()
IFFmpegRenderer::FramePacingConstraint SdlRenderer::getFramePacingConstraint()
{
return VSYNC_ANY;
return PACING_ANY;
}
bool SdlRenderer::initialize(SDL_Window* window,
+24 -2
View File
@@ -1,3 +1,5 @@
#include <QString>
#include "vaapi.h"
#include <streaming/streamutils.h>
@@ -109,6 +111,26 @@ VAAPIRenderer::initialize(SDL_Window* window, int, int width, int height, int, b
"Initialized VAAPI %d.%d",
major, minor);
const char* vendorString = vaQueryVendorString(vaDeviceContext->display);
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Driver: %s",
vendorString ? vendorString : "<unknown>");
// AMD's Gallium VAAPI driver has a nasty memory leak
// that causes memory to be leaked for each submitted frame.
// The Flatpak runtime has a VDPAU driver in place that works
// well, so use that instead on AMD systems.
if (vendorString && qgetenv("FORCE_VAAPI") != "1") {
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
err = av_hwdevice_ctx_init(m_HwContext);
if (err < 0) {
@@ -155,9 +177,9 @@ VAAPIRenderer::getDecoderCapabilities()
return 0;
}
IFFmpegRenderer::VSyncConstraint VAAPIRenderer::getVsyncConstraint()
IFFmpegRenderer::FramePacingConstraint VAAPIRenderer::getFramePacingConstraint()
{
return VSYNC_ANY;
return PACING_ANY;
}
void
+1 -1
View File
@@ -40,7 +40,7 @@ public:
virtual void renderFrameAtVsync(AVFrame* frame);
virtual bool needsTestFrame();
virtual int getDecoderCapabilities();
virtual VSyncConstraint getVsyncConstraint();
virtual FramePacingConstraint getFramePacingConstraint();
private:
int m_WindowSystem;
+10 -2
View File
@@ -91,6 +91,7 @@ bool VDPAURenderer::initialize(SDL_Window* window, int, int width, int height, i
GET_PROC_ADDRESS(VDP_FUNC_ID_OUTPUT_SURFACE_DESTROY, &m_VdpOutputSurfaceDestroy);
GET_PROC_ADDRESS(VDP_FUNC_ID_OUTPUT_SURFACE_QUERY_CAPABILITIES, &m_VdpOutputSurfaceQueryCapabilities);
GET_PROC_ADDRESS(VDP_FUNC_ID_VIDEO_SURFACE_GET_PARAMETERS, &m_VdpVideoSurfaceGetParameters);
GET_PROC_ADDRESS(VDP_FUNC_ID_GET_INFORMATION_STRING, &m_VdpGetInformationString);
SDL_GetWindowSize(window, (int*)&m_DisplayWidth, (int*)&m_DisplayHeight);
@@ -130,6 +131,13 @@ bool VDPAURenderer::initialize(SDL_Window* window, int, int width, int height, i
return false;
}
const char* infoString;
if (m_VdpGetInformationString(&infoString) == VDP_STATUS_OK) {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Driver: %s",
infoString);
}
// Try our available output formats to find something the GPU supports
bool foundFormat = false;
for (int i = 0; i < OUTPUT_SURFACE_FORMAT_COUNT; i++) {
@@ -238,9 +246,9 @@ int VDPAURenderer::getDecoderCapabilities()
return 0;
}
IFFmpegRenderer::VSyncConstraint VDPAURenderer::getVsyncConstraint()
IFFmpegRenderer::FramePacingConstraint VDPAURenderer::getFramePacingConstraint()
{
return VSYNC_ANY;
return PACING_ANY;
}
void VDPAURenderer::renderFrameAtVsync(AVFrame* frame)
+2 -1
View File
@@ -23,7 +23,7 @@ public:
virtual void renderFrameAtVsync(AVFrame* frame);
virtual bool needsTestFrame();
virtual int getDecoderCapabilities();
virtual VSyncConstraint getVsyncConstraint();
virtual FramePacingConstraint getFramePacingConstraint();
private:
uint32_t m_VideoWidth, m_VideoHeight;
@@ -56,6 +56,7 @@ private:
VdpOutputSurfaceDestroy* m_VdpOutputSurfaceDestroy;
VdpOutputSurfaceQueryCapabilities* m_VdpOutputSurfaceQueryCapabilities;
VdpVideoSurfaceGetParameters* m_VdpVideoSurfaceGetParameters;
VdpGetInformationString* m_VdpGetInformationString;
// X11 stuff
VdpPresentationQueueTargetCreateX11* m_VdpPresentationQueueTargetCreateX11;
+15 -23
View File
@@ -160,10 +160,20 @@ public:
// We need to add a subview for our display layer.
NSView* contentView = info.info.cocoa.window.contentView;
m_View = [[NSView alloc] initWithFrame:contentView.bounds];
m_View.wantsLayer = YES;
[contentView addSubview: m_View];
setupDisplayLayer();
m_DisplayLayer = [[AVSampleBufferDisplayLayer alloc] init];
m_DisplayLayer.bounds = m_View.bounds;
m_DisplayLayer.position = CGPointMake(CGRectGetMidX(m_View.bounds), CGRectGetMidY(m_View.bounds));
m_DisplayLayer.videoGravity = AVLayerVideoGravityResizeAspect;
// Create a layer-hosted view by setting the layer before wantsLayer
// This avoids us having to add our AVSampleBufferDisplayLayer as a
// sublayer of a layer-backed view which leaves a useless layer in
// the middle.
m_View.layer = m_DisplayLayer;
m_View.wantsLayer = YES;
[contentView addSubview: m_View];
err = av_hwdevice_ctx_create(&m_HwContext,
AV_HWDEVICE_TYPE_VIDEOTOOLBOX,
@@ -201,33 +211,15 @@ public:
return 0;
}
virtual IFFmpegRenderer::VSyncConstraint getVsyncConstraint() override
virtual IFFmpegRenderer::FramePacingConstraint getFramePacingConstraint() override
{
// This renderer is inherently tied to V-sync due how we're
// rendering with AVSampleBufferDisplay layer. Running without
// the V-Sync source leads to massive stuttering.
return VSYNC_FORCE_ON;
return PACING_FORCE_ON;
}
private:
void setupDisplayLayer()
{
CALayer* oldLayer = m_DisplayLayer;
m_DisplayLayer = [[AVSampleBufferDisplayLayer alloc] init];
m_DisplayLayer.bounds = m_View.bounds;
m_DisplayLayer.position = CGPointMake(CGRectGetMidX(m_View.bounds), CGRectGetMidY(m_View.bounds));
m_DisplayLayer.videoGravity = AVLayerVideoGravityResizeAspect;
CALayer* viewLayer = m_View.layer;
if (oldLayer != nil) {
[viewLayer replaceSublayer:oldLayer with:m_DisplayLayer];
}
else {
[viewLayer addSublayer:m_DisplayLayer];
}
}
AVBufferRef* m_HwContext;
AVSampleBufferDisplayLayer* m_DisplayLayer;
CMVideoFormatDescriptionRef m_FormatDesc;
+218 -24
View File
@@ -1,6 +1,7 @@
#include <Limelight.h>
#include "ffmpeg.h"
#include "streaming/streamutils.h"
#include <h264_stream.h>
#ifdef Q_OS_WIN32
#include "ffmpeg-renderers/dxva2.h"
@@ -21,6 +22,8 @@
// This is gross but it allows us to use sizeof()
#include "ffmpeg_videosamples.cpp"
#define MAX_SPS_EXTRA_SIZE 16
#define FAILED_DECODES_RESET_THRESHOLD 20
bool FFmpegVideoDecoder::isHardwareAccelerated()
@@ -60,11 +63,18 @@ FFmpegVideoDecoder::FFmpegVideoDecoder()
m_HwDecodeCfg(nullptr),
m_Renderer(nullptr),
m_ConsecutiveFailedDecodes(0),
m_Pacer(nullptr)
m_Pacer(nullptr),
m_LastFrameNumber(0),
m_StreamFps(0),
m_NeedsSpsFixup(false)
{
av_init_packet(&m_Pkt);
SDL_AtomicSet(&m_QueuedFrames, 0);
SDL_zero(m_ActiveWndVideoStats);
SDL_zero(m_LastWndVideoStats);
SDL_zero(m_GlobalVideoStats);
// Use linear filtering when renderer scaling is required
SDL_SetHint(SDL_HINT_RENDER_SCALE_QUALITY, "1");
}
@@ -115,30 +125,33 @@ void FFmpegVideoDecoder::reset()
delete m_Renderer;
m_Renderer = nullptr;
logVideoStats(m_GlobalVideoStats, "Global video stats");
}
bool FFmpegVideoDecoder::completeInitialization(AVCodec* decoder, SDL_Window* window,
int videoFormat, int width, int height,
int maxFps, bool enableVsync, bool testOnly)
int maxFps, bool enableFramePacing, bool testOnly)
{
auto vsyncConstraint = m_Renderer->getVsyncConstraint();
if (vsyncConstraint == IFFmpegRenderer::VSYNC_FORCE_OFF && enableVsync) {
auto vsyncConstraint = m_Renderer->getFramePacingConstraint();
if (vsyncConstraint == IFFmpegRenderer::PACING_FORCE_OFF && enableFramePacing) {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"V-sync is forcefully disabled by the active renderer");
enableVsync = false;
"Frame pacing is forcefully disabled by the active renderer");
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
int displayHz = StreamUtils::getDisplayRefreshRate(window);
if (displayHz + 5 >= maxFps) {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"V-sync is forcefully enabled by the active renderer");
enableVsync = true;
"Frame pacing is forcefully enabled by the active renderer");
enableFramePacing = true;
}
}
m_Pacer = new Pacer(m_Renderer);
if (!m_Pacer->initialize(window, maxFps, enableVsync)) {
m_StreamFps = maxFps;
m_Pacer = new Pacer(m_Renderer, &m_ActiveWndVideoStats);
if (!m_Pacer->initialize(window, maxFps, enableFramePacing)) {
return false;
}
@@ -216,6 +229,17 @@ bool FFmpegVideoDecoder::completeInitialization(AVCodec* decoder, SDL_Window* wi
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
// Restore default log level before streaming
@@ -225,6 +249,84 @@ bool FFmpegVideoDecoder::completeInitialization(AVCodec* decoder, SDL_Window* wi
return true;
}
void FFmpegVideoDecoder::addVideoStats(VIDEO_STATS& src, VIDEO_STATS& dst)
{
dst.receivedFrames += src.receivedFrames;
dst.decodedFrames += src.decodedFrames;
dst.renderedFrames += src.renderedFrames;
dst.networkDroppedFrames += src.networkDroppedFrames;
dst.pacerDroppedFrames += src.pacerDroppedFrames;
dst.totalReassemblyTime += src.totalReassemblyTime;
dst.totalDecodeTime += src.totalDecodeTime;
dst.totalPacerTime += src.totalPacerTime;
dst.totalRenderTime += src.totalRenderTime;
Uint32 now = SDL_GetTicks();
// Initialize the measurement start point if this is the first video stat window
if (!dst.measurementStartTimestamp) {
dst.measurementStartTimestamp = src.measurementStartTimestamp;
}
// The following code assumes the global measure was already started first
SDL_assert(dst.measurementStartTimestamp <= src.measurementStartTimestamp);
dst.receivedFps = (float)dst.receivedFrames / ((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);
}
void FFmpegVideoDecoder::logVideoStats(VIDEO_STATS& stats, const char* title)
{
if (stats.renderedFps > 0 || stats.renderedFrames != 0) {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"%s", title);
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"----------------------------------------------------------");
}
if (stats.renderedFps > 0) {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Incoming frame rate from network: %.2f FPS",
stats.receivedFps);
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Decoding frame rate: %.2f FPS",
stats.decodedFps);
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Rendering frame rate: %.2f FPS",
stats.renderedFps);
}
if (stats.renderedFrames != 0) {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Total frames received: %u",
stats.receivedFrames);
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Total frames decoded: %u",
stats.decodedFrames);
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Total frames rendered: %u",
stats.renderedFrames);
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Average reassembly time: %.2f ms",
(float)stats.totalReassemblyTime / stats.decodedFrames);
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Average decode time: %.2f ms",
(float)stats.totalDecodeTime / stats.decodedFrames);
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Average frame pacing delay: %.2f ms",
(float)stats.totalPacerTime / stats.renderedFrames);
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Average render time: %.2f ms",
(float)stats.totalRenderTime / stats.renderedFrames);
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Frames lost during network transmission: %.2f%%",
(float)stats.networkDroppedFrames / stats.decodedFrames);
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Frames dropped by frame pacing: %.2f%%",
(float)stats.pacerDroppedFrames / stats.decodedFrames);
}
}
IFFmpegRenderer* FFmpegVideoDecoder::createAcceleratedRenderer(const AVCodecHWConfig* hwDecodeCfg)
{
if (!(hwDecodeCfg->methods & AV_CODEC_HW_CONFIG_METHOD_HW_DEVICE_CTX)) {
@@ -261,7 +363,8 @@ bool FFmpegVideoDecoder::initialize(
int width,
int height,
int maxFps,
bool enableVsync)
bool enableVsync,
bool enableFramePacing)
{
AVCodec* decoder;
@@ -297,7 +400,7 @@ bool FFmpegVideoDecoder::initialize(
m_Renderer = new SdlRenderer();
if (vds != StreamingPreferences::VDS_FORCE_HARDWARE &&
m_Renderer->initialize(window, videoFormat, width, height, maxFps, enableVsync) &&
completeInitialization(decoder, window, videoFormat, width, height, maxFps, enableVsync, false)) {
completeInitialization(decoder, window, videoFormat, width, height, maxFps, enableFramePacing, false)) {
return true;
}
else {
@@ -314,13 +417,13 @@ bool FFmpegVideoDecoder::initialize(
m_HwDecodeCfg = config;
// Initialize the hardware codec and submit a test frame if the renderer needs it
if (m_Renderer->initialize(window, videoFormat, width, height, maxFps, enableVsync) &&
completeInitialization(decoder, window, videoFormat, width, height, maxFps, enableVsync, m_Renderer->needsTestFrame())) {
completeInitialization(decoder, window, videoFormat, width, height, maxFps, enableFramePacing, m_Renderer->needsTestFrame())) {
if (m_Renderer->needsTestFrame()) {
// The test worked, so now let's initialize it for real
reset();
if ((m_Renderer = createAcceleratedRenderer(config)) != nullptr &&
m_Renderer->initialize(window, videoFormat, width, height, maxFps, enableVsync) &&
completeInitialization(decoder, window, videoFormat, width, height, maxFps, enableVsync, false)) {
completeInitialization(decoder, window, videoFormat, width, height, maxFps, enableFramePacing, false)) {
return true;
}
else {
@@ -341,28 +444,107 @@ 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)
{
PLENTRY entry = du->bufferList;
int err;
if (du->fullLength + AV_INPUT_BUFFER_PADDING_SIZE > m_DecodeBuffer.length()) {
m_DecodeBuffer = QByteArray(du->fullLength + AV_INPUT_BUFFER_PADDING_SIZE, 0);
// Flip stats windows roughly every second
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
logVideoStats(lastTwoWndStats, "Periodic video stats");
#endif
// Accumulate these values into the global stats
addVideoStats(m_ActiveWndVideoStats, m_GlobalVideoStats);
// Move this window into the last window slot and clear it for next window
SDL_memcpy(&m_LastWndVideoStats, &m_ActiveWndVideoStats, sizeof(m_ActiveWndVideoStats));
SDL_zero(m_ActiveWndVideoStats);
m_ActiveWndVideoStats.measurementStartTimestamp = SDL_GetTicks();
}
m_ActiveWndVideoStats.receivedFrames++;
if (!m_LastFrameNumber) {
m_ActiveWndVideoStats.measurementStartTimestamp = SDL_GetTicks();
m_LastFrameNumber = du->frameNumber;
}
else {
// Any frame number greater than m_LastFrameNumber + 1 represents a dropped frame
m_ActiveWndVideoStats.networkDroppedFrames += du->frameNumber - (m_LastFrameNumber + 1);
m_LastFrameNumber = du->frameNumber;
}
int requiredBufferSize = du->fullLength;
if (du->frameType == FRAME_TYPE_IDR) {
// Add some extra space in case we need to do an SPS fixup
requiredBufferSize += MAX_SPS_EXTRA_SIZE;
}
if (requiredBufferSize + AV_INPUT_BUFFER_PADDING_SIZE > m_DecodeBuffer.length()) {
m_DecodeBuffer = QByteArray(requiredBufferSize + AV_INPUT_BUFFER_PADDING_SIZE, 0);
}
int offset = 0;
while (entry != nullptr) {
memcpy(&m_DecodeBuffer.data()[offset],
entry->data,
entry->length);
offset += entry->length;
writeBuffer(entry, offset);
entry = entry->next;
}
SDL_assert(offset == du->fullLength);
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;
Uint32 beforeDecode = SDL_GetTicks();
err = avcodec_send_packet(m_VideoDecoderCtx, &m_Pkt);
if (err < 0) {
@@ -400,9 +582,17 @@ int FFmpegVideoDecoder::submitDecodeUnit(PDECODE_UNIT du)
// Reset failed decodes count if we reached this far
m_ConsecutiveFailedDecodes = 0;
// Capture a frame timestamp to measuring pacing delay
frame->pts = SDL_GetTicks();
// Queue the frame for rendering from the main thread
SDL_AtomicIncRef(&m_QueuedFrames);
queueFrame(frame);
// Count time in avcodec_send_packet() and avcodec_receive_frame()
// as time spent decoding
m_ActiveWndVideoStats.totalDecodeTime += SDL_GetTicks() - beforeDecode;
m_ActiveWndVideoStats.decodedFrames++;
}
else {
av_frame_free(&frame);
@@ -446,6 +636,10 @@ void FFmpegVideoDecoder::dropFrame(SDL_UserEvent* event)
}
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);
}
+15 -2
View File
@@ -18,7 +18,8 @@ public:
int width,
int height,
int maxFps,
bool enableVsync) override;
bool enableVsync,
bool enableFramePacing) override;
virtual bool isHardwareAccelerated() override;
virtual int getDecoderCapabilities() override;
virtual int submitDecodeUnit(PDECODE_UNIT du) override;
@@ -30,12 +31,18 @@ public:
private:
bool completeInitialization(AVCodec* decoder, SDL_Window* window,
int videoFormat, int width, int height,
int maxFps, bool enableVsync, bool testOnly);
int maxFps, bool enableFramePacing, bool testOnly);
void logVideoStats(VIDEO_STATS& stats, const char* title);
void addVideoStats(VIDEO_STATS& src, VIDEO_STATS& dst);
IFFmpegRenderer* createAcceleratedRenderer(const AVCodecHWConfig* hwDecodeCfg);
void reset();
void writeBuffer(PLENTRY entry, int& offset);
static
enum AVPixelFormat ffGetFormat(AVCodecContext* context,
const enum AVPixelFormat* pixFmts);
@@ -48,6 +55,12 @@ private:
SDL_atomic_t m_QueuedFrames;
int m_ConsecutiveFailedDecodes;
Pacer* m_Pacer;
VIDEO_STATS m_ActiveWndVideoStats;
VIDEO_STATS m_LastWndVideoStats;
VIDEO_STATS m_GlobalVideoStats;
int m_LastFrameNumber;
int m_StreamFps;
bool m_NeedsSpsFixup;
static const uint8_t k_H264TestFrame[];
static const uint8_t k_HEVCTestFrame[];
+1 -1
View File
@@ -34,7 +34,7 @@ SLVideoDecoder::getDecoderCapabilities()
bool
SLVideoDecoder::initialize(StreamingPreferences::VideoDecoderSelection vds,
SDL_Window*,
int videoFormat, int, int, int frameRate, bool)
int videoFormat, int, int, int frameRate, bool, bool)
{
// SLVideo only supports hardware decoding
if (vds == StreamingPreferences::VDS_FORCE_SOFTWARE) {
+2 -1
View File
@@ -15,7 +15,8 @@ public:
int width,
int height,
int frameRate,
bool enableVsync);
bool enableVsync,
bool enableFramePacing);
virtual bool isHardwareAccelerated();
virtual int getDecoderCapabilities();
virtual int submitDecodeUnit(PDECODE_UNIT du);
+1
View File
@@ -0,0 +1 @@
0.8.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
+20
View File
@@ -409,6 +409,7 @@ enum AVCodecID {
AV_CODEC_ID_DXV,
AV_CODEC_ID_SCREENPRESSO,
AV_CODEC_ID_RSCC,
AV_CODEC_ID_AVS2,
AV_CODEC_ID_Y41P = 0x8000,
AV_CODEC_ID_AVRP,
@@ -446,6 +447,11 @@ enum AVCodecID {
AV_CODEC_ID_SVG,
AV_CODEC_ID_GDV,
AV_CODEC_ID_FITS,
AV_CODEC_ID_IMM4,
AV_CODEC_ID_PROSUMER,
AV_CODEC_ID_MWSC,
AV_CODEC_ID_WCMV,
AV_CODEC_ID_RASC,
/* various PCM "codecs" */
AV_CODEC_ID_FIRST_AUDIO = 0x10000, ///< A dummy id pointing at the start of audio codecs
@@ -485,6 +491,7 @@ enum AVCodecID {
AV_CODEC_ID_PCM_S64BE,
AV_CODEC_ID_PCM_F16LE,
AV_CODEC_ID_PCM_F24LE,
AV_CODEC_ID_PCM_VIDC,
/* various ADPCM codecs */
AV_CODEC_ID_ADPCM_IMA_QT = 0x11000,
@@ -1359,6 +1366,12 @@ enum AVPacketSideDataType {
*/
AV_PKT_DATA_ENCRYPTION_INFO,
/**
* Active Format Description data consisting of a single byte as specified
* in ETSI TS 101 154 using AVActiveFormatDescription enum.
*/
AV_PKT_DATA_AFD,
/**
* The number of side data types.
* This is not part of the public API/ABI in the sense that it may
@@ -1614,6 +1627,7 @@ typedef struct AVCodecContext {
* The allocated memory should be AV_INPUT_BUFFER_PADDING_SIZE bytes larger
* than extradata_size to avoid problems if it is read with the bitstream reader.
* The bytewise contents of extradata must not depend on the architecture or CPU endianness.
* Must be allocated with the av_malloc() family of functions.
* - encoding: Set/allocated/freed by libavcodec.
* - decoding: Set/allocated/freed by user.
*/
@@ -5768,6 +5782,7 @@ typedef struct AVBitStreamFilter {
int (*init)(AVBSFContext *ctx);
int (*filter)(AVBSFContext *ctx, AVPacket *pkt);
void (*close)(AVBSFContext *ctx);
void (*flush)(AVBSFContext *ctx);
} AVBitStreamFilter;
#if FF_API_OLD_BSF
@@ -5894,6 +5909,11 @@ int av_bsf_send_packet(AVBSFContext *ctx, AVPacket *pkt);
*/
int av_bsf_receive_packet(AVBSFContext *ctx, AVPacket *pkt);
/**
* Reset the internal bitstream filter state / flush internal buffers.
*/
void av_bsf_flush(AVBSFContext *ctx);
/**
* Free a bitstream filter context and everything associated with it; write NULL
* into the supplied pointer.
+13
View File
@@ -85,4 +85,17 @@ typedef struct MediaCodecBuffer AVMediaCodecBuffer;
*/
int av_mediacodec_release_buffer(AVMediaCodecBuffer *buffer, int render);
/**
* Release a MediaCodec buffer and render it at the given time to the surface
* that is associated with the decoder. The timestamp must be within one second
* of the current java/lang/System#nanoTime() (which is implemented using
* CLOCK_MONOTONIC on Android). See the Android MediaCodec documentation
* of android/media/MediaCodec#releaseOutputBuffer(int,long) for more details.
*
* @param buffer the buffer to render
* @param time timestamp in nanoseconds of when to render the buffer
* @return 0 on success, < 0 otherwise
*/
int av_mediacodec_render_buffer_at_time(AVMediaCodecBuffer *buffer, int64_t time);
#endif /* AVCODEC_MEDIACODEC_H */
+2 -2
View File
@@ -28,8 +28,8 @@
#include "libavutil/version.h"
#define LIBAVCODEC_VERSION_MAJOR 58
#define LIBAVCODEC_VERSION_MINOR 21
#define LIBAVCODEC_VERSION_MICRO 104
#define LIBAVCODEC_VERSION_MINOR 35
#define LIBAVCODEC_VERSION_MICRO 100
#define LIBAVCODEC_VERSION_INT AV_VERSION_INT(LIBAVCODEC_VERSION_MAJOR, \
LIBAVCODEC_VERSION_MINOR, \
+1 -1
View File
@@ -66,7 +66,7 @@
#endif
/**
* Assert that floating point opperations can be executed.
* Assert that floating point operations can be executed.
*
* This will av_assert0() that the cpu is not in MMX state on X86
*/
+1 -1
View File
@@ -1,5 +1,5 @@
/* Automatically generated by version.sh, do not manually edit! */
#ifndef AVUTIL_FFVERSION_H
#define AVUTIL_FFVERSION_H
#define FFMPEG_VERSION "N-91434-gcced03dd66"
#define FFMPEG_VERSION "4.1"
#endif /* AVUTIL_FFVERSION_H */
+2
View File
@@ -33,6 +33,8 @@
* allocated buffer or map it with mmap() when available.
* In case of success set *bufptr to the read or mmapped buffer, and
* *size to the size in bytes of the buffer in *bufptr.
* Unlike mmap this function succeeds with zero sized files, in this
* case *bufptr will be set to NULL and *size will be set to 0.
* The returned buffer must be released with av_file_unmap().
*
* @param log_offset loglevel offset used for logging
+8
View File
@@ -158,6 +158,14 @@ enum AVFrameSideDataType {
*/
AV_FRAME_DATA_QP_TABLE_DATA,
#endif
/**
* Timecode which conforms to SMPTE ST 12-1. The data is an array of 4 uint32_t
* where the first uint32_t describes how many (1-3) of the other timecodes are used.
* The timecode format is described in the av_timecode_get_smpte_from_framenum()
* function in libavutil/timecode.c.
*/
AV_FRAME_DATA_S12M_TIMECODE,
};
enum AVActiveFormatDescription {
+11
View File
@@ -343,7 +343,13 @@ char *av_get_pix_fmt_string(char *buf, int buf_size,
* format writes the values corresponding to the palette
* component c in data[1] to dst, rather than the palette indexes in
* data[0]. The behavior is undefined if the format is not paletted.
* @param dst_element_size size of elements in dst array (2 or 4 byte)
*/
void av_read_image_line2(void *dst, const uint8_t *data[4],
const int linesize[4], const AVPixFmtDescriptor *desc,
int x, int y, int c, int w, int read_pal_component,
int dst_element_size);
void av_read_image_line(uint16_t *dst, const uint8_t *data[4],
const int linesize[4], const AVPixFmtDescriptor *desc,
int x, int y, int c, int w, int read_pal_component);
@@ -361,7 +367,12 @@ void av_read_image_line(uint16_t *dst, const uint8_t *data[4],
* @param y the vertical coordinate of the first pixel to write
* @param w the width of the line to write, that is the number of
* values to write to the image line
* @param src_element_size size of elements in src array (2 or 4 byte)
*/
void av_write_image_line2(const void *src, uint8_t *data[4],
const int linesize[4], const AVPixFmtDescriptor *desc,
int x, int y, int c, int w, int src_element_size);
void av_write_image_line(const uint16_t *src, uint8_t *data[4],
const int linesize[4], const AVPixFmtDescriptor *desc,
int x, int y, int c, int w);
+9
View File
@@ -42,6 +42,10 @@
* This is stored as BGRA on little-endian CPU architectures and ARGB on
* big-endian CPUs.
*
* @note
* If the resolution is not a multiple of the chroma subsampling factor
* then the chroma plane resolution must be rounded up.
*
* @par
* When the pixel format is palettized RGB32 (AV_PIX_FMT_PAL8), the palettized
* image data is stored in AVFrame.data[0]. The palette is transported in
@@ -333,6 +337,9 @@ enum AVPixelFormat {
AV_PIX_FMT_GRAY14BE, ///< Y , 14bpp, big-endian
AV_PIX_FMT_GRAY14LE, ///< Y , 14bpp, little-endian
AV_PIX_FMT_GRAYF32BE, ///< IEEE-754 single precision Y, 32bpp, big-endian
AV_PIX_FMT_GRAYF32LE, ///< IEEE-754 single precision Y, 32bpp, little-endian
AV_PIX_FMT_NB ///< number of pixel formats, DO NOT USE THIS if you want to link with shared libav* because the number of formats might differ between versions
};
@@ -401,6 +408,8 @@ enum AVPixelFormat {
#define AV_PIX_FMT_GBRPF32 AV_PIX_FMT_NE(GBRPF32BE, GBRPF32LE)
#define AV_PIX_FMT_GBRAPF32 AV_PIX_FMT_NE(GBRAPF32BE, GBRAPF32LE)
#define AV_PIX_FMT_GRAYF32 AV_PIX_FMT_NE(GRAYF32BE, GRAYF32LE)
#define AV_PIX_FMT_YUVA420P9 AV_PIX_FMT_NE(YUVA420P9BE , YUVA420P9LE)
#define AV_PIX_FMT_YUVA422P9 AV_PIX_FMT_NE(YUVA422P9BE , YUVA422P9LE)
#define AV_PIX_FMT_YUVA444P9 AV_PIX_FMT_NE(YUVA444P9BE , YUVA444P9LE)
+2 -2
View File
@@ -79,8 +79,8 @@
*/
#define LIBAVUTIL_VERSION_MAJOR 56
#define LIBAVUTIL_VERSION_MINOR 18
#define LIBAVUTIL_VERSION_MICRO 102
#define LIBAVUTIL_VERSION_MINOR 22
#define LIBAVUTIL_VERSION_MICRO 100
#define LIBAVUTIL_VERSION_INT AV_VERSION_INT(LIBAVUTIL_VERSION_MAJOR, \
LIBAVUTIL_VERSION_MINOR, \
Binary file not shown.
Binary file not shown.

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