Compare commits

...
233 Commits
Author SHA1 Message Date
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
Cameron Gutman 1f759577ce Version 0.5.0 2018-09-15 16:23:10 -07:00
Cameron Gutman 04376ca047 Fix Clang format string signness mismatch warnings 2018-09-15 16:00:36 -07:00
Cameron Gutman 7ac108f495 Reduce delay for PCs appearing online 2018-09-15 13:43:02 -07:00
Cameron Gutman a2eece0f5c Log audio buffer size 2018-09-15 13:27:57 -07:00
Cameron Gutman a03279df3b Fix errant mouse button event being sent on mouse capture 2018-09-14 22:09:45 -07:00
Cameron Gutman bef6c2b665 Fix the underflow caused by the underflow fix :( 2018-09-14 21:03:06 -07:00
Cameron Gutman 8002c65693 Block Intel drivers earlier than 4836 from using HEVC due to artifacting issues. Fixes #32 2018-09-14 20:20:47 -07:00
Cameron Gutman 5c7247b8b5 Archive symbols from prebuilt libraries too 2018-09-14 19:32:23 -07:00
Cameron Gutman 1a18e68441 Make Windows shortcut description consistent with Mac and Linux 2018-09-14 19:23:02 -07:00
Cameron Gutman 72afa55897 Add tip text to the StreamSegue telling users about the quit shortcut. Fixes #77 2018-09-14 19:11:06 -07:00
Cameron Gutman eed9fca10c Mass conversion of isNull() -> isEmpty() 2018-09-14 18:36:15 -07:00
Cameron Gutman 25d97e187a Fix large mouse input delay on GFE 3.14.0 and earlier. Fixes #68 2018-09-14 18:35:28 -07:00
Cameron Gutman 7ca2ca8045 Print the GPU driver version on Windows 2018-09-13 14:54:30 -07:00
Cameron Gutman 54885c3e5f Revert "Delay Session::exec() for 250 ms to allow the StackView transition to complete" due to regressing spinner performance on Windows and Linux
This reverts commit b8ffb6e53d.
2018-09-13 14:39:33 -07:00
Cameron Gutman b8ffb6e53d Delay Session::exec() for 250 ms to allow the StackView transition to complete 2018-09-13 14:33:38 -07:00
Cameron Gutman 7bc43c226d Fix underflow in audio drop code 2018-09-13 14:09:03 -07:00
Cameron Gutman da4697794d Increase buffer size for glitch-free playback on macOS 2018-09-13 13:02:21 -07:00
Cameron Gutman 879c524658 Revert "Process Qt events in the SDL loop" to fix input issues on Windows and Mac
This reverts commit c8e00195ed.
2018-09-13 12:14:56 -07:00
Cameron Gutman e3a8f010c3 Fixes for experimental Qt audio renderer 2018-09-13 12:00:51 -07:00
Cameron Gutman f2f8f92172 Add gamepad mapping DB from https://github.com/gabomdq/SDL_GameControllerDB 2018-09-13 11:58:32 -07:00
Cameron Gutman 265ec96c6f Fix grammar mistake in shortcut description 2018-09-13 08:18:01 -07:00
Cameron Gutman 6e06174db6 Add experimental Qt audio renderer 2018-09-13 08:17:26 -07:00
Cameron Gutman 9677569293 Fix Travis CI build 2018-09-13 08:05:54 -07:00
Cameron Gutman c8e00195ed Process Qt events in the SDL loop 2018-09-13 08:03:06 -07:00
Cameron Gutman 64bc189010 Fixes to SDL audio renderer and autodetection support 2018-09-13 07:46:01 -07:00
Cameron Gutman 12496e4432 Refactor audio rendering into a separate class 2018-09-13 06:23:06 -07:00
Cameron Gutman 9e2fd67487 Add option to enable mouse acceleration for remote desktop usage 2018-09-09 13:21:11 -07:00
Cameron Gutman af17d56cbd Add option to disable mDNS PC discovery. Fixes #50 2018-09-09 12:33:19 -07:00
Cameron Gutman 6e0a657b76 Update readme and metadata for Beta 1 features 2018-09-09 11:53:58 -07:00
Cameron Gutman b0a9fc7085 Add Help option to connection failure dialog 2018-09-09 11:53:14 -07:00
Cameron Gutman 5e47b8a835 Update README.md 2018-09-09 11:40:13 -07:00
Cameron Gutman 0b94ad1fef Add help link in Add PC failure dialog 2018-09-09 11:19:33 -07:00
Cameron Gutman 9dcd856099 Blacklist Kepler GPUs for HEVC due to hybrid decode support present in some drivers 2018-09-09 10:48:01 -07:00
Cameron Gutman cfbcc4093c Reduce UI jank when transitioning to the stream segue 2018-09-09 10:17:32 -07:00
Cameron Gutman b8169ae08b Fix broken quit dialog transition to next app 2018-09-09 10:08:23 -07:00
Cameron Gutman 103625fbdd Version v0.4.0 2018-09-08 16:58:03 -07:00
Cameron Gutman 4712080a1b Fix centering of streaming window and size after toggling full-screen on Windows 2018-09-08 16:01:35 -07:00
Cameron Gutman 93c683be3a Allow renderers that force V-sync on to run without it when streaming higher than the display refresh rate 2018-09-08 15:27:21 -07:00
Cameron Gutman c9a7c15f98 Improve unsupported FPS options and performance 2018-09-08 15:09:46 -07:00
Cameron Gutman 64a08f0533 Allow higher than display refresh rate streaming with unsupported FPS option 2018-09-08 14:34:11 -07:00
Cameron Gutman 52c6455ca5 Avoid sending a click event on focus gain on Linux 2018-09-08 14:05:42 -07:00
Cameron Gutman 238681d72f Merge pull request #67 from jhakonen/capture-on-activated-window
Capture mouse cursor when clicking inactive window
2018-09-08 14:01:27 -07:00
Cameron Gutman dbcc6feb3f Merge pull request #66 from jhakonen/fix-key-not-working
Fixed < key in keyboard not working
2018-09-08 11:05:34 -07:00
Janne Hakonen 300b53f28c Capture mouse cursor when clicking inactive window 2018-09-08 16:39:20 +03:00
Janne Hakonen 3dabb1c891 Fixed < key in keyboard not working 2018-09-08 14:16:27 +03:00
Cameron Gutman 4381f7a973 Apply changes to preferences when loading the settings page 2018-09-07 23:16:13 -07:00
Cameron Gutman 77b767ae5b Hide autodetect option for audio configuration since it's broken on macOS and Linux (and to some extent, Windows) 2018-09-07 22:51:56 -07:00
Cameron Gutman 2c4f78133f Avoid inflating MessageDialogs for each app in the grid. Fixes scrolling jank on app grid. 2018-09-07 17:33:34 -07:00
Cameron Gutman ec2c181f4e Use 4 threads for fetching box art 2018-09-07 17:30:43 -07:00
Cameron Gutman fae98eb13a Limit box art fetching to a single thread to improve UI responsiveness 2018-09-07 17:02:52 -07:00
Cameron Gutman f1d117d5d7 Ensure FFmpeg log messages are written to the debug log 2018-09-07 15:53:10 -07:00
Cameron Gutman 529ac8041a Only write a dump for the first unhandled exception 2018-09-07 15:45:35 -07:00
Cameron Gutman 391e84e0f4 Create a crash dump when Moonlight crashes on Windows 2018-09-07 14:16:59 -07:00
Cameron Gutman 6a2b717ff1 Run in native resolution on Retina displays when streaming above scaled resolution 2018-09-07 01:12:18 -07:00
Cameron Gutman 475a39cb73 Recreate the decoder when moving to a new display to allow Pacer to get the new refresh rate 2018-09-06 19:42:53 -07:00
Cameron Gutman 4dba74e2f9 Fix handling of undefined and non-matching refresh rate 2018-09-06 19:16:59 -07:00
Cameron Gutman 587d783955 Use SDL_WINDOWPOS_CENTERED_DISPLAY() to fix incorrect positioning on multi-display system on macOS 2018-09-06 18:53:00 -07:00
Cameron Gutman 3bfe87d516 Merge pull request #63 from jhakonen/optional-desktop-shortcut
Changed desktop shortcut as optional in Windows installer. Fixes #17
2018-09-06 16:20:50 -07:00
Janne Hakonen d3e43012fb Changed desktop shortcut as optional in installer. Fixes #17 2018-09-06 20:09:00 +03:00
Cameron Gutman 5da156b433 Prefer H.264 for higher decoding performance in very high load situations 2018-09-05 18:57:40 -07:00
Cameron Gutman b2eeb63b6f Mark the full-screen display mode as recommended due to lower latency and better frame pacing 2018-09-05 17:46:29 -07:00
Cameron Gutman a7517dace3 Switch to ItemDelegate for PCs and Apps for proper hover and click behavior 2018-09-05 17:08:27 -07:00
Cameron Gutman bd9f2d2009 Remove unused Menu items in ToolButtons 2018-09-05 16:38:52 -07:00
Cameron Gutman 78730f7235 Wake host on a separate worker thread to avoid hanging the UI for DNS resolution 2018-09-05 16:10:32 -07:00
Cameron Gutman 8a2491d955 Populate RC strings for Windows builds 2018-09-05 15:52:50 -07:00
Cameron Gutman 0d68faf40c Remove unused parameter 2018-09-05 15:45:36 -07:00
Cameron Gutman 957a2b2169 Fix box art caching location and cleanup of cache directory by the uninstaller 2018-09-05 15:45:10 -07:00
Cameron Gutman 856318f947 Start the SDL window on the same display as the Qt UI was displayed on 2018-09-05 15:15:53 -07:00
Cameron Gutman 4df9d6b20a Update common-c with FEC fixes 2018-09-05 14:49:07 -07:00
Cameron Gutman 8519d09141 Extract dSYM file for debugging Mac builds 2018-09-04 02:20:00 -07:00
Cameron Gutman 1b4fe83739 Fix AppVeyor symbol artifact name 2018-09-04 01:09:10 -07:00
Cameron Gutman 8fc825b2eb Produce PDBs for Windows builds 2018-09-04 01:03:01 -07:00
Cameron Gutman 6d0fc22ab5 Ensure symbols are always generated for debugging purposes 2018-09-04 00:18:02 -07:00
Cameron Gutman 9a71b4c237 Improve mouse batching to only batch if the last event was less than 1 millisecond ago 2018-09-04 00:09:35 -07:00
Cameron Gutman 9aa487418f Force V-Sync enabled for DXVA2 windowed with DWM composition enabled 2018-09-03 23:34:15 -07:00
Cameron Gutman 203b2fcaca Batch mouse move events for high DPI mice 2018-09-04 00:37:21 -04:00
Cameron Gutman dae21f2638 Detect native panel resolution on Retina displays. Fixes #59 2018-09-04 00:21:37 -04:00
Cameron Gutman af6e99cbac Select the highest full-screen refresh rate that our stream FPS evenly divides 2018-09-03 22:54:41 -04:00
Cameron Gutman 6d6acf4308 Fix release builds 2018-09-03 22:47:31 -04:00
Cameron Gutman 7dd4815edf Allow selection between full-screen exclusive mode and borderless windowed mode 2018-09-03 22:17:34 -04:00
Cameron Gutman 2c068a99a3 Allow FFmpeg renderers to enforce V-sync being enabled or disabled 2018-09-03 20:57:09 -04:00
Cameron Gutman fa93364ddd Ensure the CVDisplayLink is synchronized to the display that our window is on 2018-09-03 20:19:24 -04:00
Cameron Gutman 9b1eef3f63 Pass AV_HWACCEL_FLAG_IGNORE_LEVEL for VDPAU per FFmpeg documentation recommendation 2018-09-03 17:06:21 -04:00
Cameron Gutman ed6c07fd4b Display all frames even if they are corrupt or missing references (as will happen during reference frame invalidation) 2018-09-02 18:45:29 -04:00
Cameron Gutman 5ad96f5287 Display a dialog when the connection is terminated by the remote PC 2018-09-02 18:34:10 -04:00
Cameron Gutman fa8845c7e2 Merge pull request #58 from jhakonen/release-cursor
Release mouse cursor when window focus is lost
2018-09-02 16:51:56 -04:00
Janne Hakonen b96ace7f04 Release mouse cursor when window focus is lost 2018-09-02 00:31:37 +03:00
Cameron Gutman 234a8dc3d5 Fix build with HAVE_SLVIDEO 2018-08-31 10:40:25 -04:00
Cameron Gutman 0467c2f893 Don't rely on Qt 5.10 icon feature 2018-08-31 02:28:26 -04:00
Cameron Gutman 762fe92933 Allow streaming with no audio device. Fixes #52 2018-08-31 00:09:31 -04:00
Cameron Gutman 16d105d462 Avoid hardcoding VCRedist version 2018-08-30 20:47:44 -07:00
Cameron Gutman 8a8b624f47 Add logging for adding a PC and offline/online state transitions 2018-08-30 23:15:06 -04:00
Cameron Gutman 9470615c63 Use QtQuick.Dialogs 1.2 for Qt 5.9 support 2018-08-30 00:00:05 -04:00
Cameron Gutman 8f0ea700a5 Fix QML error when pairing successfully 2018-08-29 23:59:19 -04:00
Cameron Gutman b7cbec0b1c Pass RFI flags to decoder 2018-08-25 13:36:54 -07:00
Cameron Gutman 9be9934b8c Add reference frame invalidation for the software decoder 2018-08-25 12:38:04 -07:00
Cameron Gutman 55f0e1e1d5 Use QT_OPENGL to set ANGLE 2018-08-25 11:59:32 -07:00
Cameron Gutman bbd784fc85 Use ANGLE for rendering on Windows 2018-08-25 11:35:55 -07:00
Cameron Gutman 408f93732b Fix 'invalid VADisplay' errors on renderer destruction 2018-08-25 11:21:52 -07:00
Cameron Gutman fb53c95207 Version 0.3.0 2018-08-21 01:18:55 -07:00
Cameron Gutman 960e083152 Add manifest for Windows 2018-08-21 01:07:30 -07:00
Cameron Gutman 7e560aa09e Use MMCSS when streaming with the DXVA renderer 2018-08-21 00:53:41 -07:00
Cameron Gutman 7d9478b774 Fix FlipEx on Windows 7 2018-08-21 00:49:29 -07:00
Cameron Gutman 0fa0ab43c9 Fix broken Alt+Tab behavior on Windows 2018-08-20 22:48:48 -07:00
Cameron Gutman f9cdd49d2e Add settings option to disable V-sync for lowest possible latency on Windows 2018-08-20 22:25:19 -07:00
Cameron Gutman 7661f5a3d7 Use SetThreadExecutionState() to disable the screensaver since SDL doesn't currently call that. Fixes #46 2018-08-20 21:36:23 -07:00
Cameron Gutman 395f337bbb Switch full-screen mode back to full-screen exclusive on Windows to reduce latency by 1 frame 2018-08-20 21:19:00 -07:00
Cameron Gutman 9e9fe2a2cf Work around DWM bug on Windows 7 causing horrible performance 2018-08-20 21:17:25 -07:00
Cameron Gutman 5bf88cfe96 Fix Mac build 2018-08-20 18:57:11 -07:00
Cameron Gutman f7d3c10c9d Plumb the ability to disable V-sync through the video renderers 2018-08-20 18:19:42 -07:00
Cameron Gutman 6b395c816f Allow Pacer to wait for frames up until a few ms before v-sync for better smoothness and lower latency 2018-08-20 17:53:35 -07:00
Cameron Gutman d6e7173af0 Add a VsyncSource for renderers that already block for V-sync 2018-08-20 17:24:47 -07:00
Cameron Gutman 1a60484abc Use hardware vertex processing if available 2018-08-19 22:45:17 -07:00
Cameron Gutman 62f765b2b4 Switch DXVA2 to IDirect3D9Ex APIs for more detailed control of rendering 2018-08-19 21:53:39 -07:00
Cameron Gutman 57f2fb07f0 Speed up build times by allow dependencies to be built in parallel 2018-08-19 14:33:37 -07:00
Cameron Gutman 1b8c3aae73 Speed up resize/full-screen by avoiding excessive decoder resets 2018-08-19 01:19:23 -07:00
Cameron Gutman 845e84adb7 Avoid the test frame for DXVA2 and VT APIs to address flickering in full-screen on Win7 2018-08-19 00:59:04 -07:00
Cameron Gutman 91c0429e2c Remove cache directories on uninstall 2018-08-18 15:43:24 -07:00
Cameron Gutman 30f673efe1 Create Qt components with all parameters specified otherwise it will be instantiated with default properties 2018-08-16 23:29:46 -07:00
Cameron Gutman c1b1719914 Recreate the VT decoder if the display layer fails. Minor fix to Path class. 2018-08-16 23:09:40 -07:00
Cameron Gutman 9fbdd794b9 Improve support for full-screen exclusive mode 2018-08-16 22:25:14 -07:00
Cameron Gutman 38ff2bf5cb Store all files in the current directory for portable installations. Fixes #43 2018-08-16 21:04:47 -07:00
Cameron Gutman 345e800abd Always submit frames to Pacer if it's using a frame queue 2018-08-16 17:59:33 -07:00
Cameron Gutman 404eaa44e4 Implement frame pacing for Windows using D3DKMTWaitForVerticalBlankEvent 2018-08-16 01:29:53 -07:00
Cameron Gutman 8e257ab241 Remove unused function 2018-08-16 00:04:28 -07:00
Cameron Gutman fabbad5370 Fix build error on Windows and warnings 2018-08-16 00:02:43 -07:00
Cameron Gutman 7d61acb2a8 Use Pacer to drive all rendering 2018-08-15 23:57:03 -07:00
Cameron Gutman f6a451d6e7 Add PoC VsyncSource for Windows using DwmFlush(). Soon to be replaced with D3DKMTWaitForVerticalBlankEvent(). 2018-08-15 23:20:56 -07:00
Cameron Gutman e68a15c825 Move the Vsync logic from VTRenderer into a VsyncSource 2018-08-15 22:02:15 -07:00
Cameron Gutman f929cffce7 Let Pacer know what the display Hz and stream FPS are so it can make better drop decisions 2018-08-15 21:10:35 -07:00
Cameron Gutman f171588616 Pass the maximum frame rate into FFmpeg renderers 2018-08-15 20:41:19 -07:00
Cameron Gutman e467ce7ce8 Uninitialize video subsystem before allowing a racing session to start to prevent an assert from firing 2018-08-15 20:35:11 -07:00
Cameron Gutman 1fda7550da Sign the MSI within the bundle too 2018-08-15 20:29:59 -07:00
Cameron Gutman f714a5d0cb Refactor frame pacing from VT renderer into a separate class for sharing with other renderers 2018-08-14 19:13:17 -07:00
Cameron Gutman 2a3d5a27a8 Add issue templates 2018-08-14 18:03:10 -07:00
Cameron Gutman c9bf5ae79d Prevent underflow during audio frame drop calculation 2018-08-14 17:39:29 -07:00
Cameron Gutman cf2d8f71d6 Avoid setting timer resolution to 1 ms while Moonlight is not streaming 2018-08-13 22:23:05 -07:00
Cameron Gutman 611d9ac1ba Pass the chroma type to the VDPAU video mixer 2018-08-13 20:02:55 -07:00
Cameron Gutman f3309087ab Avoid polling if the Qt window is minimized 2018-08-13 19:47:42 -07:00
110 changed files with 8002 additions and 1961 deletions
+31
View File
@@ -0,0 +1,31 @@
---
name: Bug report
about: Create a report to help us address broken functionality
---
**Describe the bug**
A clear and concise description of what the bug is.
**Steps to reproduce**
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.
**Moonlight settings (please complete the following information)**
- Have any settings been adjusted from defaults?
- If so, which settings have been changed?
- Does the problem still occur after reverting settings back to default?
**Client PC details (please complete the following information)**
- OS: [e.g. Windows 10 1803]
- Moonlight Version: [e.g. v0.2.0]
**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.
+17
View File
@@ -0,0 +1,17 @@
---
name: Feature request
about: Suggest an idea for this project
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.
+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/cgutman/libsoundio.git
[submodule "h264bitstream/h264bitstream"]
path = h264bitstream/h264bitstream
url = https://github.com/aizvorski/h264bitstream.git
+1
View File
@@ -20,6 +20,7 @@ matrix:
- qt59base
- qt59quickcontrols2
- qt59svg
- qt59multimedia
- libesd0-dev
- libgl-dev
- libegl1-mesa-dev
+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 LoadLibraryAHook(LPCSTR lpLibFileName)
{
if (lpLibFileName && isImageBlacklistedA(lpLibFileName)) {
SetLastError(ERROR_ACCESS_DISABLED_BY_POLICY);
return nullptr;
}
return s_RealLoadLibraryA(lpLibFileName);
}
static HMODULE LoadLibraryWHook(LPCWSTR lpLibFileName)
{
if (lpLibFileName && isImageBlacklistedW(lpLibFileName)) {
SetLastError(ERROR_ACCESS_DISABLED_BY_POLICY);
return nullptr;
}
return s_RealLoadLibraryW(lpLibFileName);
}
static HMODULE 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 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();
+28 -6
View File
@@ -13,20 +13,42 @@ You can follow development on our [Discord server](https://discord.gg/6ERtzFY).
## Features
- Hardware accelerated video decoding on Windows, Mac, and Linux
- Supports streaming at up to 120 FPS on high refresh rate monitors
- Supports streaming at 720p, 1080p, 1440p, or 4K
- Supports streaming at up to 120 FPS (high refresh rate monitor recommended)
- Supports streaming at 720p, 1080p, 1440p, 4K, and the client PC's native screen resolution
- 5.1 surround sound audio
- HEVC support for better image quality at reduced bandwidth
- Keyboard and mouse support
- Gamepad support with SDL gamepad mappings
## Building
### 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 El Capitan (10.11) or later
* Xcode with High Sierra (10.13) SDK (Mojave SDK not yet supported until Qt 5.12 later this year)
* [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`
* 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 may also use Homebrew on macOS or your Linux distro's package manager for the Qt SDK as long as the packages are Qt 5.9 or later.
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`.
## Contribute
1. Fork us
+2 -2
View File
@@ -23,9 +23,9 @@
<key>NSSupportsAutomaticGraphicsSwitching</key>
<true/>
<key>CFBundleVersion</key>
<string>0.2.0</string>
<string>0.6.1</string>
<key>CFBundleShortVersionString</key>
<string>0.2.0</string>
<string>0.6.1</string>
<key>CFBundleDisplayName</key>
<string>Moonlight</string>
</dict>
+31
View File
@@ -0,0 +1,31 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" xmlns:asmv3="urn:schemas-microsoft-com:asm.v3" manifestVersion="1.0">
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
<security>
<requestedPrivileges>
<requestedExecutionLevel level="asInvoker" uiAccess="false" />
</requestedPrivileges>
</security>
</trustInfo>
<description>Moonlight Game Streaming</description>
<asmv3:application>
<asmv3:windowsSettings>
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true/pm</dpiAware>
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">permonitor</dpiAwareness>
</asmv3:windowsSettings>
</asmv3:application>
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
<application>
<!-- Windows 10 -->
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}"/>
<!-- Windows 8.1 -->
<supportedOS Id="{1f676c76-80e1-4239-95bb-83d0f6d0da78}"/>
<!-- Windows 8 -->
<supportedOS Id="{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}"/>
<!-- Windows 7 -->
<supportedOS Id="{35138b9a-5d96-4fbd-8e2d-a2440225f93a}"/>
<!-- Windows Vista -->
<supportedOS Id="{e2011457-1546-43c5-a5fe-008deee3d3f0}"/>
</application>
</compatibility>
</assembly>
+115 -22
View File
@@ -11,6 +11,16 @@ unix:!macx {
# Support debug and release builds from command line for CI
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 += qtquickcompiler
}
TEMPLATE = app
# The following define makes your compiler emit warnings if you use
@@ -34,7 +44,7 @@ win32 {
LIBS += -L$$PWD/../libs/windows/lib/x64
}
LIBS += ws2_32.lib winmm.lib dxva2.lib ole32.lib
LIBS += ws2_32.lib winmm.lib dxva2.lib ole32.lib gdi32.lib user32.lib d3d9.lib dwmapi.lib dbghelp.lib
}
macx {
INCLUDEPATH += $$PWD/../libs/mac/include $$PWD/../libs/mac/Frameworks/SDL2.framework/Versions/A/Headers
@@ -45,6 +55,14 @@ unix:!macx {
CONFIG += link_pkgconfig
PKGCONFIG += openssl sdl2 opus
# For libsoundio
packagesExist(libpulse) {
PKGCONFIG += libpulse
}
packagesExist(alsa) {
PKGCONFIG += alsa
}
packagesExist(libavcodec) {
PKGCONFIG += libavcodec libavutil
CONFIG += ffmpeg
@@ -66,45 +84,64 @@ 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/identitymanager.cpp \
backend/nvcomputer.cpp \
backend/nvhttp.cpp \
backend/nvpairingmanager.cpp \
backend/computermanager.cpp \
backend/boxartmanager.cpp \
cli/commandlineparser.cpp \
cli/startstream.cpp \
settings/streamingpreferences.cpp \
streaming/input.cpp \
streaming/session.cpp \
streaming/audio.cpp \
streaming/audio/audio.cpp \
streaming/audio/renderers/sdlaud.cpp \
gui/computermodel.cpp \
gui/appmodel.cpp \
streaming/streamutils.cpp \
backend/autoupdatechecker.cpp
backend/autoupdatechecker.cpp \
path.cpp \
settings/mappingmanager.cpp \
gui/sdlgamepadkeynavigation.cpp
HEADERS += \
utils.h \
backend/identitymanager.h \
backend/nvcomputer.h \
backend/nvhttp.h \
backend/nvpairingmanager.h \
backend/computermanager.h \
backend/boxartmanager.h \
cli/commandlineparser.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 \
gui/computermodel.h \
gui/appmodel.h \
streaming/video/decoder.h \
streaming/streamutils.h \
backend/autoupdatechecker.h
backend/autoupdatechecker.h \
path.h \
settings/mappingmanager.h \
gui/sdlgamepadkeynavigation.h
# Platform-specific renderers and decoders
ffmpeg {
@@ -113,10 +150,16 @@ ffmpeg {
DEFINES += HAVE_FFMPEG
SOURCES += \
streaming/video/ffmpeg.cpp \
streaming/video/ffmpeg-renderers/sdl.cpp
streaming/video/ffmpeg-renderers/sdlvid.cpp \
streaming/video/ffmpeg-renderers/pacer/pacer.cpp \
streaming/video/ffmpeg-renderers/pacer/nullthreadedvsyncsource.cpp
HEADERS += \
streaming/video/ffmpeg.h \
streaming/video/ffmpeg-renderers/renderer.h
streaming/video/ffmpeg-renderers/renderer.h \
streaming/video/ffmpeg-renderers/pacer/pacer.h \
streaming/video/ffmpeg-renderers/pacer/nullthreadedvsyncsource.h
}
libva {
message(VAAPI renderer selected)
@@ -150,20 +193,38 @@ config_SLVideo {
DEFINES += HAVE_SLVIDEO
LIBS += -lSLVideo
SOURCES += streaming/video/sl.cpp
HEADERS += streaming/video/sl.h
}
win32 {
message(DXVA2 renderer selected)
SOURCES += streaming/video/ffmpeg-renderers/dxva2.cpp
HEADERS += streaming/video/ffmpeg-renderers/dxva2.h
SOURCES += \
streaming/video/ffmpeg-renderers/dxva2.cpp \
streaming/video/ffmpeg-renderers/pacer/dxvsyncsource.cpp
HEADERS += \
streaming/video/ffmpeg-renderers/dxva2.h \
streaming/video/ffmpeg-renderers/pacer/dxvsyncsource.h
}
macx {
message(VideoToolbox renderer selected)
SOURCES += streaming/video/ffmpeg-renderers/vt.mm
HEADERS += streaming/video/ffmpeg-renderers/vt.h
SOURCES += \
streaming/video/ffmpeg-renderers/vt.mm \
streaming/video/ffmpeg-renderers/pacer/displaylinkvsyncsource.mm
HEADERS += \
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 += \
@@ -190,6 +251,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
@@ -197,30 +278,42 @@ unix:!macx: {
isEmpty(BINDIR) {
BINDIR = bin
}
isEmpty(DATADIR) {
DATADIR = share
}
target.path = $$PREFIX/$$BINDIR/
desktop.files = deploy/linux/com.moonlight_stream.Moonlight.desktop
desktop.path = $$PREFIX/share/applications/
desktop.path = $$PREFIX/$$DATADIR/applications/
icons.files = res/moonlight.svg
icons.path = $$PREFIX/share/icons/hicolor/scalable/apps/
icons.path = $$PREFIX/$$DATADIR/icons/hicolor/scalable/apps/
appdata.files = deploy/linux/com.moonlight_stream.Moonlight.appdata.xml
appdata.path = $$PREFIX/share/metainfo/
appstream.files = deploy/linux/com.moonlight_stream.Moonlight.appdata.xml
appstream.path = $$PREFIX/$$DATADIR/metainfo/
INSTALLS += target desktop icons appdata
appdata.files = SDL_GameControllerDB/gamecontrollerdb.txt
appdata.path = "$$PREFIX/$$DATADIR/Moonlight Game Streaming Project/Moonlight/"
INSTALLS += target appdata desktop icons appstream
}
win32 {
RC_ICONS = moonlight.ico
QMAKE_TARGET_COMPANY = Moonlight Game Streaming Project
QMAKE_TARGET_DESCRIPTION = Moonlight Game Streaming Client
QMAKE_TARGET_PRODUCT = Moonlight
CONFIG -= embed_manifest_exe
QMAKE_LFLAGS += /MANIFEST:embed /MANIFESTINPUT:$${PWD}/Moonlight.exe.manifest
}
macx {
QMAKE_INFO_PLIST = $$PWD/Info.plist
APP_BUNDLE_RESOURCES.files = moonlight.icns
APP_BUNDLE_RESOURCES.files = moonlight.icns SDL_GameControllerDB/gamecontrollerdb.txt
APP_BUNDLE_RESOURCES.path = Contents/Resources
QMAKE_BUNDLE_DATA += APP_BUNDLE_RESOURCES
}
VERSION = 0.2.0
DEFINES += VERSION_STR=\\\"0.2.0\\\"
VERSION = 0.6.1
DEFINES += VERSION_STR=\\\"0.6.1\\\"
+44 -4
View File
@@ -1,14 +1,19 @@
#include "boxartmanager.h"
#include "../path.h"
#include <QStandardPaths>
#include <QImageReader>
#include <QImageWriter>
BoxArtManager::BoxArtManager(QObject *parent) :
QObject(parent),
m_BoxArtDir(
QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + "/boxart")
m_BoxArtDir(Path::getBoxArtCacheDir()),
m_ThreadPool(this)
{
// 4 is a good balance between fast loading for large
// app grids and not crushing GFE with tons of requests
// and causing UI jank from constantly stalling to decode
// new images.
m_ThreadPool.setMaxThreadCount(4);
if (!m_BoxArtDir.exists()) {
m_BoxArtDir.mkpath(".");
}
@@ -31,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
@@ -42,7 +80,7 @@ QUrl BoxArtManager::loadBoxArt(NvComputer* computer, NvApp& app)
// If we get here, we need to fetch asynchronously.
// Kick off a worker on our thread pool to do just that.
NetworkBoxArtLoadTask* netLoadTask = new NetworkBoxArtLoadTask(this, computer, app);
QThreadPool::globalInstance()->start(netLoadTask);
m_ThreadPool.start(netLoadTask);
// Return the placeholder then we can notify the caller
// later when the real image is ready.
@@ -76,3 +114,5 @@ QUrl BoxArtManager::loadBoxArtFromNetwork(NvComputer* computer, int appId)
return QUrl();
}
#include "boxartmanager.moc"
+1 -33
View File
@@ -36,37 +36,5 @@ private:
getFilePathForBoxArt(NvComputer* computer, int appId);
QDir m_BoxArtDir;
};
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;
QThreadPool m_ThreadPool;
};
+384 -290
View File
@@ -1,278 +1,143 @@
#include "computermanager.h"
#include "nvhttp.h"
#include "settings/streamingpreferences.h"
#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);
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);
NvComputer::NvComputer(QString address, QString serverInfo)
{
this->name = NvHTTP::getXmlString(serverInfo, "hostname");
if (this->name.isNull()) {
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.isNull()) {
this->serverCodecModeSupport = codecSupport.toInt();
}
else {
this->serverCodecModeSupport = 0;
}
QString maxLumaPixelsHEVC = NvHTTP::getXmlString(serverInfo, "MaxLumaPixelsHEVC");
if (!maxLumaPixelsHEVC.isNull()) {
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.isNull()) {
qWarning() << name << "has no MAC address stored";
return false;
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;
}
const quint16 WOL_PORTS[] = {
7, 9, // Standard WOL ports
47998, 47999, 48000, // Ports opened by GFE
};
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;
}
// 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 all unique address strings or host names
bool success = false;
for (QString& addressString : addressList) {
QHostInfo hostInfo = QHostInfo::fromName(addressString);
// Try all IP addresses that this string resolves to
for (QHostAddress& address : hostInfo.addresses()) {
QUdpSocket sock;
// Bind to any address on the correct protocol
if (sock.bind(address.protocol() == QUdpSocket::IPv4Protocol ?
QHostAddress::AnyIPv4 : QHostAddress::AnyIPv6)) {
// 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;
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[i].isNull()) {
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),
@@ -363,17 +228,24 @@ void ComputerManager::startPolling()
return;
}
// Start an MDNS query for GameStream hosts
m_MdnsBrowser = new QMdnsEngine::Browser(&m_MdnsServer, "_nvstream._tcp.local.", &m_MdnsCache);
connect(m_MdnsBrowser, &QMdnsEngine::Browser::serviceAdded,
this, [this](const QMdnsEngine::Service& service) {
qInfo() << "Discovered mDNS host:" << service.hostname();
StreamingPreferences prefs;
MdnsPendingComputer* pendingComputer = new MdnsPendingComputer(&m_MdnsServer, &m_MdnsCache, service);
connect(pendingComputer, SIGNAL(resolvedv4(MdnsPendingComputer*,QHostAddress)),
this, SLOT(handleMdnsServiceResolved(MdnsPendingComputer*,QHostAddress)));
m_PendingResolution.append(pendingComputer);
});
if (prefs.enableMdns) {
// Start an MDNS query for GameStream hosts
m_MdnsBrowser = new QMdnsEngine::Browser(&m_MdnsServer, "_nvstream._tcp.local.", &m_MdnsCache);
connect(m_MdnsBrowser, &QMdnsEngine::Browser::serviceAdded,
this, [this](const QMdnsEngine::Service& service) {
qInfo() << "Discovered mDNS host:" << service.hostname();
MdnsPendingComputer* pendingComputer = new MdnsPendingComputer(&m_MdnsServer, &m_MdnsCache, service);
connect(pendingComputer, SIGNAL(resolvedv4(MdnsPendingComputer*,QHostAddress)),
this, SLOT(handleMdnsServiceResolved(MdnsPendingComputer*,QHostAddress)));
m_PendingResolution.append(pendingComputer);
});
}
else {
qWarning() << "mDNS is disabled by user preference";
}
// Start polling threads for each known host
QMapIterator<QString, NvComputer*> i(m_KnownHosts);
@@ -383,6 +255,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[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)
{
@@ -394,6 +285,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);
@@ -410,9 +314,23 @@ 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[m_Computer->uuid];
m_ComputerManager->m_PollThreads.remove(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();
@@ -425,9 +343,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;
}
@@ -443,6 +360,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);
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
@@ -451,6 +417,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);
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);
@@ -498,6 +513,118 @@ 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:
void run()
{
NvHTTP http(m_Address);
qInfo() << "Processing new PC at" << m_Address << "from" << (m_Mdns ? "mDNS" : "user");
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;
}
}
} 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;
};
void ComputerManager::addNewHost(QString address, bool mdns)
{
// Punt to a worker thread to avoid stalling the
@@ -506,37 +633,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 -372
View File
@@ -1,5 +1,6 @@
#pragma once
#include "nvhttp.h"
#include "nvcomputer.h"
#include "nvpairingmanager.h"
#include <qmdnsengine/server.h>
@@ -13,195 +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();
} 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;
for (int i = 0; i < TRIES_BEFORE_OFFLINING && !online; i++) {
for (auto& address : m_Computer->uniqueAddresses()) {
if (isInterruptionRequested()) {
return;
}
if (tryPollComputer(address, stateChanged)) {
online = true;
break;
}
}
}
// 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) {
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)) {
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
@@ -294,185 +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);
QString serverInfo;
try {
serverInfo = http.getServerInfo();
} 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) {
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;
};
+2 -2
View File
@@ -185,12 +185,12 @@ IdentityManager::getSslConfig()
QString
IdentityManager::getUniqueId()
{
if (m_CachedUniqueId.isNull()) {
if (m_CachedUniqueId.isEmpty()) {
QSettings settings;
// Load the unique ID from settings
m_CachedUniqueId = settings.value(SER_UNIQUEID).toString();
if (!m_CachedUniqueId.isEmpty() && !m_CachedUniqueId.isNull()) {
if (!m_CachedUniqueId.isEmpty()) {
qInfo() << "Loaded unique ID from settings:" << m_CachedUniqueId;
}
else {
+275
View File
@@ -0,0 +1,275 @@
#include "nvcomputer.h"
#include <QUdpSocket>
#include <QHostInfo>
#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)
{
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();
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);
// 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)
{
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 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;
// Bind to any address on the correct protocol
if (sock.bind(address.protocol() == QUdpSocket::IPv4Protocol ?
QHostAddress::AnyIPv4 : QHostAddress::AnyIPv6)) {
// 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;
}
}
}
}
}
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; \
}
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;
}
+75
View File
@@ -0,0 +1,75 @@
#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);
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;
};
+23 -14
View File
@@ -16,6 +16,8 @@
NvHTTP::NvHTTP(QString address) :
m_Address(address)
{
Q_ASSERT(!address.isEmpty());
m_BaseUrlHttp.setScheme("http");
m_BaseUrlHttps.setScheme("https");
m_BaseUrlHttp.setHost(address);
@@ -27,9 +29,15 @@ NvHTTP::NvHTTP(QString address) :
QVector<int>
NvHTTP::parseQuad(QString quad)
{
QStringList parts = quad.split(".");
QVector<int> ret;
// Return an empty vector for old GFE versions
// that were missing GfeVersion.
if (quad.isEmpty()) {
return ret;
}
QStringList parts = quad.split(".");
for (int i = 0; i < 4; i++)
{
ret.append(parts.at(i).toInt());
@@ -56,7 +64,7 @@ NvHTTP::getCurrentGame(QString serverInfo)
}
QString
NvHTTP::getServerInfo()
NvHTTP::getServerInfo(NvLogLevel logLevel)
{
QString serverInfo;
@@ -68,7 +76,7 @@ NvHTTP::getServerInfo()
"serverinfo",
nullptr,
true,
true);
logLevel);
// Throws if the request failed
verifyResponseStatus(serverInfo);
}
@@ -81,7 +89,7 @@ NvHTTP::getServerInfo()
"serverinfo",
nullptr,
true,
true);
logLevel);
verifyResponseStatus(serverInfo);
}
else
@@ -187,7 +195,7 @@ NvHTTP::quitApp()
// Newer GFE versions will just return success even if quitting fails
// if we're not the original requestor.
if (getCurrentGame(getServerInfo()) != 0) {
if (getCurrentGame(getServerInfo(NvHTTP::ERROR)) != 0) {
// Generate a synthetic GfeResponseException letting the caller know
// that they can't kill someone else's stream.
throw GfeHttpResponseException(599, "");
@@ -228,7 +236,7 @@ NvHTTP::getAppList()
"applist",
nullptr,
true,
true);
NvLogLevel::ERROR);
verifyResponseStatus(appxml);
QXmlStreamReader xmlReader(appxml);
@@ -295,7 +303,8 @@ NvHTTP::getBoxArt(int appId)
"appasset",
"appid="+QString::number(appId)+
"&AssetType=2&AssetIdx=0",
true);
true,
NvLogLevel::VERBOSE);
QImage image = QImageReader(reply).read();
delete reply;
@@ -342,9 +351,9 @@ NvHTTP::openConnectionToString(QUrl baseUrl,
QString command,
QString arguments,
bool enableTimeout,
bool suppressLogging)
NvLogLevel logLevel)
{
QNetworkReply* reply = openConnection(baseUrl, command, arguments, enableTimeout, suppressLogging);
QNetworkReply* reply = openConnection(baseUrl, command, arguments, enableTimeout, logLevel);
QString ret;
QTextStream stream(reply);
@@ -360,7 +369,7 @@ NvHTTP::openConnection(QUrl baseUrl,
QString command,
QString arguments,
bool enableTimeout,
bool suppressLogging)
NvLogLevel logLevel)
{
// Build a URL for the request
QUrl url(baseUrl);
@@ -386,7 +395,7 @@ NvHTTP::openConnection(QUrl baseUrl,
{
QTimer::singleShot(REQUEST_TIMEOUT_MS, &loop, SLOT(quit()));
}
if (!suppressLogging) {
if (logLevel >= NvLogLevel::VERBOSE) {
qInfo() << "Executing request:" << url.toString();
}
loop.exec(QEventLoop::ExcludeUserInputEvents);
@@ -394,7 +403,7 @@ NvHTTP::openConnection(QUrl baseUrl,
// Abort the request if it timed out
if (!reply->isFinished())
{
if (!suppressLogging) {
if (logLevel >= NvLogLevel::ERROR) {
qWarning() << "Aborting timed out request for" << url.toString();
}
reply->abort();
@@ -407,10 +416,10 @@ NvHTTP::openConnection(QUrl baseUrl,
// Handle error
if (reply->error() != QNetworkReply::NoError)
{
if (!suppressLogging) {
if (logLevel >= NvLogLevel::ERROR) {
qWarning() << command << " request failed with error " << reply->error();
}
GfeHttpResponseException exception(reply->error(), reply->errorString());
QtNetworkReplyException exception(reply->error(), reply->errorString());
delete reply;
throw exception;
}
+47 -5
View File
@@ -5,7 +5,8 @@
#include <Limelight.h>
#include <QUrl>
#include <QtNetwork/QNetworkAccessManager>
#include <QNetworkAccessManager>
#include <QNetworkReply>
class NvApp
{
@@ -17,7 +18,7 @@ public:
bool isInitialized()
{
return id != 0 && !name.isNull();
return id != 0 && !name.isEmpty();
}
int id;
@@ -77,9 +78,50 @@ 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:
enum NvLogLevel {
NONE,
ERROR,
VERBOSE
};
explicit NvHTTP(QString address);
static
@@ -87,7 +129,7 @@ public:
getCurrentGame(QString serverInfo);
QString
getServerInfo();
getServerInfo(NvLogLevel logLevel);
static
void
@@ -108,7 +150,7 @@ public:
QString command,
QString arguments,
bool enableTimeout,
bool suppressLogging = false);
NvLogLevel logLevel = NvLogLevel::VERBOSE);
static
QVector<int>
@@ -145,7 +187,7 @@ private:
QString command,
QString arguments,
bool enableTimeout,
bool suppressLogging = false);
NvLogLevel logLevel);
QString m_Address;
QNetworkAccessManager m_Nam;
+362
View File
@@ -0,0 +1,362 @@
#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"
" 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 == "stream") {
return StreamRequested;
} else {
parser.showError(QString("Invalid action: %1").arg(action));
}
}
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("mouse-acceleration", "mouse acceleration");
parser.addToggleOption("game-optimization", "game optimizations");
parser.addToggleOption("audio-on-host", "audio on host PC");
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 --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 --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;
}
+41
View File
@@ -0,0 +1,41 @@
#pragma once
#include "settings/streamingpreferences.h"
#include <QMap>
#include <QString>
class GlobalCommandLineParser
{
public:
enum ParseResult {
NormalStartRequested,
StreamRequested,
};
GlobalCommandLineParser();
virtual ~GlobalCommandLineParser();
ParseResult parse(const QStringList &args);
};
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;
};
+220
View File
@@ -0,0 +1,220 @@
#include "startstream.h"
#include "backend/computermanager.h"
#include "streaming/session.h"
#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 {
ComputerUpdated,
Executed,
Timedout,
};
Event(Type type)
: type(type), computerManager(nullptr), computer(nullptr) {}
Type type;
ComputerManager *computerManager;
NvComputer *computer;
};
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) {
case Event::Executed:
if (m_State == StateInit) {
m_State = StateSeekComputer;
m_TimeoutTimer->start(COMPUTER_SEEK_TIMEOUT);
m_ComputerManager = event.computerManager;
q->connect(m_ComputerManager, &ComputerManager::computerStateChanged,
q, &Launcher::onComputerUpdated);
// 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();
emit q->searchingComputer();
}
break;
case Event::ComputerUpdated:
if (m_State == StateSeekComputer) {
if (matchComputer(event.computer) && isOnline(event.computer)) {
if (isPaired(event.computer)) {
m_State = StateSeekApp;
m_TimeoutTimer->start(APP_SEEK_TIMEOUT);
m_Computer = event.computer;
m_ComputerManager->stopPollingAsync();
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);
}
}
}
if (m_State == StateSeekApp) {
int index = getAppIndex();
if (-1 != index) {
app = m_Computer->appList[index];
if (isNotStreaming() || isStreamingApp(app)) {
m_State = StateStartSession;
m_TimeoutTimer->stop();
session = new Session(m_Computer, app, m_Preferences);
emit q->sessionCreated(app.name, session);
} else {
m_State = StateFailure;
QString msg = QString("%1 is already running. Please quit %1 to stream %2.")
.arg(getCurrentAppName(), app.name);
emit q->failed(msg);
}
}
}
break;
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;
}
}
bool matchComputer(NvComputer *computer) const
{
QString value = m_ComputerName.toLower();
return computer->name.toLower() == value ||
computer->localAddress.toLower() == value ||
computer->remoteAddress.toLower() == value ||
computer->manualAddress.toLower() == value ||
computer->uuid.toLower() == value;
}
bool isOnline(NvComputer *computer) const
{
return computer->state == NvComputer::CS_ONLINE;
}
bool isPaired(NvComputer *computer) const
{
return computer->pairState == NvComputer::PS_PAIRED;
}
int getAppIndex() const
{
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;
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::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);
}
}
+42
View File
@@ -0,0 +1,42 @@
#pragma once
#include <QObject>
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);
signals:
void searchingComputer();
void searchingApp();
void sessionCreated(QString appName, Session *session);
void failed(QString text);
private slots:
void onComputerUpdated(NvComputer *computer);
void onTimeout();
private:
QScopedPointer<LauncherPrivate> m_DPtr;
};
}
@@ -11,7 +11,7 @@
<p>Features include:</p>
<ul>
<li>Streaming at up to 4K resolution</li>
<li>Support for up to 120 FPS streaming on high refresh rate monitors</li>
<li>Support for up to 120 FPS streaming (high refresh rate monitor recommended)</li>
<li>Hardware accelerated video decoding with VAAPI and VDPAU support</li>
<li>5.1 surround sound audio support</li>
<li>HEVC support for better video compression efficiency</li>
@@ -33,6 +33,90 @@
</screenshots>
<releases>
<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>
<ul>
<li>Added option to enable mouse acceleration for remote desktop usage</li>
<li>Added option to disable automatic PC discovery</li>
<li>Added gamepad mappings from gabomdq's SDL_GameControllerDB</li>
<li>Added help links to error dialogs</li>
<li>Added quit shortcut tip to stream loading page</li>
<li>Restored the autodetect audio configuration option</li>
</ul>
<p>Bugfixes:</p>
<ul>
<li>Fixed quit app dialog starting the wrong app</li>
<li>Fixed very high input lag on GFE 3.14.0 and earlier</li>
<li>Fixed sending errant mouse click when capturing the mouse</li>
</ul>
</description>
</release>
<release version="0.4.0" date="2018-09-08">
<description>
<p>New features:</p>
<ul>
<li>Added option to enable (unsupported) 90/120 FPS streaming on 60 Hz monitors</li>
<li>Added refresh rate matching for better frame pacing on high refresh rate monitors</li>
<li>The streaming window now appears on the same monitor the UI was displayed on</li>
<li>The streaming window is now centered on the target monitor</li>
<li>Display a dialog when the connection is terminated by the remote PC</li>
<li>Added option to select between full-screen exclusive and borderless windowed mode</li>
</ul>
<p>Bugfixes:</p>
<ul>
<li>Fixed streaming with no audio device</li>
<li>Fixed mouse lag with some high polling rate mice</li>
<li>Improved UI performance on the app grid page</li>
<li>1440p and 4K 120 FPS performance is improved to avoid encoder bottlenecks</li>
<li>Autodetect audio configuration option removed because it was broken</li>
</ul>
</description>
</release>
<release version="0.3.0" date="2018-08-21">
<description>
<p>Bugfixes:</p>
<ul>
<li>Reduced power consumption when the app is minimized</li>
<li>Fixed malformed asset requests being sent when the first host in the grid is offline</li>
</ul>
</description>
</release>
<release version="0.2.0" date="2018-08-12">
<description>
<p>Bugfixes:</p>
+82 -59
View File
@@ -1,27 +1,24 @@
import QtQuick 2.9
import QtQuick.Dialogs 1.3
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
anchors.rightMargin: 5
anchors.bottomMargin: 5
cellWidth: 225; cellHeight: 350;
focus: true
// Cache delegates for 1000px in both directions to improve
// scrolling and resizing performance
cacheBuffer: 1000
cellWidth: 225; cellHeight: 385;
// The StackView will trigger a visibility change when
// we're pushed onto it, causing our onVisibleChanged
@@ -34,18 +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) {
// Start polling when this view is shown
ComputerManager.startPolling()
appModel.computerLost.connect(computerLost)
gamepadKeyNav.enable()
}
else {
// Stop polling when this view is not on top
ComputerManager.stopPollingAsync()
appModel.computerLost.disconnect(computerLost)
gamepadKeyNav.disable()
}
}
@@ -58,17 +60,22 @@ GridView {
model: appModel
delegate: Item {
width: 200; height: 300;
delegate: NavigableItemDelegate {
width: 200; height: 335;
grid: appGrid
Image {
id: appIcon
anchors.horizontalCenter: parent.horizontalCenter;
y: 20
source: model.boxart
sourceSize {
width: 150
height: 200
}
width: sourceSize.width
height: sourceSize.height
fillMode: Image.Pad
}
Image {
@@ -93,6 +100,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()
@@ -101,72 +114,47 @@ GridView {
if (runningIndex >= 0 && runningIndex !== index) {
quitAppDialog.appName = appModel.getRunningAppName()
quitAppDialog.segueToStream = true
quitAppDialog.nextAppName = model.name
quitAppDialog.nextAppIndex = index
quitAppDialog.open()
return
}
var component = Qt.createComponent("StreamSegue.qml")
var segue = component.createObject(stackView)
segue.appName = model.name
segue.session = appModel.createSessionForApp(index)
var segue = component.createObject(stackView, {"appName": model.name, "session": appModel.createSessionForApp(index)})
stackView.push(segue)
}
MessageDialog {
id: quitAppDialog
modality:Qt.WindowModal
property string appName : "";
property bool segueToStream : false
text:"Are you sure you want to quit " + appName +"? Any unsaved progress will be lost."
standardButtons: StandardButton.Yes | StandardButton.No
onYes: {
var component = Qt.createComponent("QuitSegue.qml")
var segue = component.createObject(stackView)
segue.appName = appName
if (segueToStream) {
// Store the session and app name if we're going to stream after
// successfully quitting the old app.
segue.nextAppName = model.name
segue.nextSession = appModel.createSessionForApp(index)
}
else {
segue.nextAppName = null
segue.nextSession = null
}
stackView.push(segue)
// Trigger the quit after pushing the quit segue on screen
appModel.quitRunningApp()
}
onClicked: {
// Nothing is running or this app is running
launchOrResumeSelectedApp()
}
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.LeftButton | Qt.RightButton
acceptedButtons: Qt.RightButton
onClicked: {
if (mouse.button === Qt.LeftButton) {
// Nothing is running or this app is running
launchOrResumeSelectedApp()
}
else {
// Right click
appContextMenu.open()
}
// popup() ensures the menu appears under the mouse cursor
appContextMenu.popup()
}
}
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()
@@ -174,11 +162,46 @@ GridView {
quitAppDialog.open()
}
visible: model.running
height: visible ? implicitHeight : 0
}
}
}
MessageDialog {
id: quitAppDialog
modality:Qt.WindowModal
property string appName : ""
property bool segueToStream : false
property string nextAppName: ""
property int nextAppIndex: 0
text:"Are you sure you want to quit " + appName +"? Any unsaved progress will be lost."
standardButtons: StandardButton.Yes | StandardButton.No
function quitApp() {
var component = Qt.createComponent("QuitSegue.qml")
var params = {"appName": appName}
if (segueToStream) {
// Store the session and app name if we're going to stream after
// successfully quitting the old app.
params.nextAppName = nextAppName
params.nextSession = appModel.createSessionForApp(nextAppIndex)
}
else {
params.nextAppName = null
params.nextSession = null
}
stackView.push(component.createObject(stackView, params))
// Trigger the quit after pushing the quit segue on screen
appModel.quitRunningApp()
}
onYes: quitApp()
// For keyboard/gamepad navigation
onAccepted: quitApp()
}
ScrollBar.vertical: ScrollBar {
parent: appGrid.parent
anchors {
+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)
}
}
}
+78
View File
@@ -0,0 +1,78 @@
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()
}
onVisibleChanged: {
if (visible) {
toolBar.visible = false
launcher.searchingComputer.connect(onSearchingComputer)
launcher.searchingApp.connect(onSearchingApp)
launcher.sessionCreated.connect(onSessionCreated)
launcher.failed.connect(onLaunchFailed)
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
color: "white"
}
}
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();
}
}
}
}
+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)
}
}
+79 -92
View File
@@ -1,24 +1,27 @@
import QtQuick 2.9
import QtQuick.Controls 2.2
import QtQuick.Dialogs 1.3
import QtQuick.Dialogs 1.2
import QtQuick.Layouts 1.3
import QtQuick.Window 2.2
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
anchors.rightMargin: 5
anchors.bottomMargin: 5
cellWidth: 350; cellHeight: 350;
focus: true
objectName: "Computers"
// The StackView will trigger a visibility change when
@@ -30,39 +33,34 @@ GridView {
id: prefs
}
Component.onCompleted: {
// Setup signals on CM
ComputerManager.computerAddCompleted.connect(addComplete)
if (prefs.isRunningWayland()) {
waylandDialog.open()
}
else if (!prefs.hasAnyHardwareAcceleration()) {
noHwDecoderDialog.open()
}
SdlGamepadKeyNavigation {
id: gamepadKeyNav
}
onVisibleChanged: {
if (visible) {
// Start polling when this view is shown
ComputerManager.startPolling()
gamepadKeyNav.enable()
}
else {
// Stop polling when this view is not top-most
ComputerManager.stopPollingAsync()
gamepadKeyNav.disable()
}
}
Component.onCompleted: {
// Setup signals on CM
ComputerManager.computerAddCompleted.connect(addComplete)
// Don't show any highlighted item until interacting with them
currentIndex = -1
}
function pairingComplete(error)
{
// Close the PIN dialog
pairDialog.close()
// Start polling again
ComputerManager.startPolling()
// Display a failed dialog if we got an error
if (error !== null) {
if (error !== undefined) {
errorDialog.text = error
errorDialog.open()
}
@@ -71,7 +69,7 @@ GridView {
function addComplete(success)
{
if (!success) {
errorDialog.text = "Unable to connect to the specified PC. Ensure GameStream is enabled in GeForce Experience."
errorDialog.text = "Unable to connect to the specified PC. Click the Help button for possible solutions."
errorDialog.open()
}
}
@@ -86,8 +84,9 @@ GridView {
model: computerModel
delegate: Item {
delegate: NavigableItemDelegate {
width: 300; height: 300;
grid: pcGrid
Image {
id: pcIcon
@@ -139,13 +138,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
@@ -155,55 +153,58 @@ GridView {
}
}
onClicked: {
if (model.addPc) {
addPcDialog.open()
}
else if (model.online) {
if (model.paired) {
// go to game view
var component = Qt.createComponent("AppView.qml")
var appView = component.createObject(stackView, {"computerIndex": index, "objectName": model.name})
stackView.push(appView)
}
else {
if (!model.busy) {
var pin = ("0000" + Math.floor(Math.random() * 10000)).slice(-4)
// Kick off pairing in the background
computerModel.pairComputer(index, pin)
// Display the pairing dialog
pairDialog.pin = pin
pairDialog.open()
}
else {
// cannot pair while something is streaming or attempting to pair
errorDialog.text = "This PC is currently busy. Make sure to quit any running games and try again."
errorDialog.open()
}
}
} else if (!model.online) {
// Using open() here because it may be activated by keyboard
pcContextMenu.open()
}
}
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.LeftButton | Qt.RightButton;
acceptedButtons: Qt.RightButton;
onClicked: {
if(mouse.button === Qt.LeftButton) {
if (model.addPc) {
addPcDialog.open()
}
else if (model.online) {
if (model.paired) {
// go to game view
var component = Qt.createComponent("AppView.qml")
var appView = component.createObject(stackView)
appView.computerIndex = index
appView.objectName = model.name
stackView.push(appView)
}
else {
if (!model.busy) {
var pin = ("0000" + Math.floor(Math.random() * 10000)).slice(-4)
// Stop polling, since pairing may make GFE unresponsive
ComputerManager.stopPollingAsync()
// Kick off pairing in the background
computerModel.pairComputer(index, pin)
// Display the pairing dialog
pairDialog.pin = pin
pairDialog.open()
}
else {
// cannot pair while something is streaming or attempting to pair
errorDialog.text = "This PC is currently busy. Make sure to quit any running games and try again."
errorDialog.open()
}
}
} else if(!model.online) {
pcContextMenu.open()
}
}
else { // 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
pcContextMenu.popup()
}
}
}
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 {
@@ -211,31 +212,11 @@ GridView {
// don't allow edits to the rest of the window while open
modality:Qt.WindowModal
icon: StandardIcon.Critical
standardButtons: StandardButton.Ok
}
MessageDialog {
id: noHwDecoderDialog
modality:Qt.WindowModal
icon: StandardIcon.Warning
standardButtons: StandardButton.Ok | StandardButton.Help
text: "No functioning hardware accelerated H.264 video decoder was detected by Moonlight. " +
"Your streaming performance may be severely degraded in this configuration. " +
"Click the Help button for more information on solving this problem."
onHelp: {
Qt.openUrlExternally("https://github.com/moonlight-stream/moonlight-docs/wiki/Fixing-Hardware-Decoding-Problems");
}
}
MessageDialog {
id: 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");
// Using Setup-Guide here instead of Troubleshooting because it's likely that users
// will arrive here by forgetting to enable GameStream or not forwarding ports.
Qt.openUrlExternally("https://github.com/moonlight-stream/moonlight-docs/wiki/Setup-Guide");
}
}
@@ -258,10 +239,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 {
+2 -10
View File
@@ -1,6 +1,6 @@
import QtQuick 2.0
import QtQuick.Controls 2.2
import QtQuick.Dialogs 1.3
import QtQuick.Dialogs 1.2
import ComputerManager 1.0
import Session 1.0
@@ -33,9 +33,7 @@ Item {
// If we're supposed to launch another game after this, do so now
if (error === undefined && nextSession !== null) {
var component = Qt.createComponent("StreamSegue.qml")
var segue = component.createObject(stackView)
segue.appName = nextAppName
segue.session = nextSession
var segue = component.createObject(stackView, {"appName": nextAppName, "session": nextSession})
stackView.push(segue)
}
}
@@ -44,16 +42,10 @@ Item {
if (visible) {
// Connect the quit completion signal
ComputerManager.quitAppCompleted.connect(quitAppCompleted)
// We must be polling or we won't get the quitAppCompleted() callback
ComputerManager.startPolling()
}
else {
// Disconnect the signal
ComputerManager.quitAppCompleted.disconnect(quitAppCompleted)
// Stop polling when this view is not on top
ComputerManager.stopPollingAsync()
}
}
+249 -85
View File
@@ -2,6 +2,8 @@ import QtQuick 2.9
import QtQuick.Controls 2.2
import StreamingPreferences 1.0
import ComputerManager 1.0
import SdlGamepadKeyNavigation 1.0
ScrollView {
id: settingsPage
@@ -11,6 +13,25 @@ ScrollView {
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()
}
@@ -52,41 +73,55 @@ ScrollView {
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
for (var displayIndex = 0;; displayIndex++) {
var screenRect = prefs.getDisplayResolution(displayIndex)
if (screenRect.width === 0) {
// Exceeded max count of displays
break
}
var done = false
for (var displayIndex = 0; !done; displayIndex++) {
for (var displayResIndex = 0; displayResIndex < 2; displayResIndex++) {
var screenRect;
var indexToAdd = 0
for (var j = 0; j < resolutionComboBox.count; j++) {
var existing_width = parseInt(resolutionListModel.get(j).video_width);
var existing_height = parseInt(resolutionListModel.get(j).video_height);
// Some platforms have different desktop resolutions
// and native resolutions (like macOS with Retina displays)
if (displayResIndex == 0) {
screenRect = prefs.getDesktopResolution(displayIndex)
}
else {
screenRect = prefs.getNativeResolution(displayIndex)
}
if (screenRect.width === existing_width && screenRect.height === existing_height) {
// Duplicate entry, skip
indexToAdd = -1
if (screenRect.width === 0) {
// Exceeded max count of displays
done = true
break
}
else if (screenRect.width * screenRect.height > existing_width * existing_height) {
// Candidate entrypoint after this entry
indexToAdd = j + 1
}
}
// Insert this display's resolution if it's not a duplicate
if (indexToAdd >= 0) {
resolutionListModel.insert(indexToAdd,
{
"text": "Native ("+screenRect.width+"x"+screenRect.height+")",
"video_width": ""+screenRect.width,
"video_height": ""+screenRect.height
})
var indexToAdd = 0
for (var j = 0; j < resolutionComboBox.count; j++) {
var existing_width = parseInt(resolutionListModel.get(j).video_width);
var existing_height = parseInt(resolutionListModel.get(j).video_height);
if (screenRect.width === existing_width && screenRect.height === existing_height) {
// Duplicate entry, skip
indexToAdd = -1
break
}
else if (screenRect.width * screenRect.height > existing_width * existing_height) {
// Candidate entrypoint after this entry
indexToAdd = j + 1
}
}
// Insert this display's resolution if it's not a duplicate
if (indexToAdd >= 0) {
resolutionListModel.insert(indexToAdd,
{
"text": "Native ("+screenRect.width+"x"+screenRect.height+")",
"video_width": ""+screenRect.width,
"video_height": ""+screenRect.height
})
}
}
}
@@ -95,7 +130,7 @@ ScrollView {
var saved_width = prefs.width
var saved_height = prefs.height
currentIndex = 0
for (var i = 0; i < resolutionComboBox.count; i++) {
for (var i = 0; i < resolutionListModel.count; i++) {
var el_width = parseInt(resolutionListModel.get(i).video_width);
var el_height = parseInt(resolutionListModel.get(i).video_height);
@@ -104,10 +139,12 @@ ScrollView {
currentIndex = i
}
}
// Persist the selected value
activated(currentIndex)
}
id: resolutionComboBox
width: 200
textRole: "text"
model: ListModel {
id: resolutionListModel
@@ -136,59 +173,96 @@ ScrollView {
}
// ::onActivated must be used, as it only listens for when the index is changed by a human
onActivated : {
prefs.width = parseInt(resolutionListModel.get(currentIndex).video_width)
prefs.height = parseInt(resolutionListModel.get(currentIndex).video_height)
var selectedWidth = parseInt(resolutionListModel.get(currentIndex).video_width)
var selectedHeight = parseInt(resolutionListModel.get(currentIndex).video_height)
prefs.bitrateKbps = prefs.getDefaultBitrate(prefs.width, prefs.height, prefs.fps);
slider.value = prefs.bitrateKbps
// Only modify the bitrate if the values actually changed
if (prefs.width !== selectedWidth || prefs.height !== selectedHeight) {
prefs.width = selectedWidth
prefs.height = selectedHeight
prefs.bitrateKbps = prefs.getDefaultBitrate(prefs.width, prefs.height, prefs.fps);
slider.value = prefs.bitrateKbps
}
}
}
ComboBox {
// ignore setting the index at first, and actually set it when the component is loaded
Component.onCompleted: {
AutoResizingComboBox {
function createModel() {
var fpsListModel = Qt.createQmlObject('import QtQuick 2.0; ListModel {}', parent, '')
// Get the max supported FPS on this system
var max_fps = prefs.getMaximumStreamingFrameRate();
// Default entries
fpsListModel.append({"text": "30 FPS", "video_fps": "30"})
fpsListModel.append({"text": "60 FPS", "video_fps": "60"})
// Add unsupported FPS values that come before the display max FPS
if (prefs.unsupportedFps) {
if (max_fps > 90) {
fpsListModel.append({"text": "90 FPS (Unsupported)", "video_fps": "90"})
}
if (max_fps > 120) {
fpsListModel.append({"text": "120 FPS (Unsupported)", "video_fps": "120"})
}
}
// 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})
}
// Add unsupported FPS values that come after the display max FPS
if (prefs.unsupportedFps) {
if (max_fps < 90) {
fpsListModel.append({"text": "90 FPS (Unsupported)", "video_fps": "90"})
}
if (max_fps < 120) {
fpsListModel.append({"text": "120 FPS (Unsupported)", "video_fps": "120"})
}
}
return fpsListModel
}
function reinitialize() {
model = createModel()
var saved_fps = prefs.fps
currentIndex = 0
for (var i = 0; i < fpsComboBox.count; i++) {
var el_fps = parseInt(fpsListModel.get(i).video_fps);
for (var i = 0; i < model.count; i++) {
var el_fps = parseInt(model.get(i).video_fps);
// Pick the highest value lesser or equal to the saved FPS
if (saved_fps >= el_fps) {
currentIndex = i
}
}
// Persist the selected value
activated(currentIndex)
}
// ignore setting the index at first, and actually set it when the component is loaded
Component.onCompleted: {
reinitialize()
}
id: fpsComboBox
textRole: "text"
model: ListModel {
id: fpsListModel
ListElement {
text: "30 FPS"
video_fps: "30"
}
ListElement {
text: "60 FPS"
video_fps: "60"
}
// A higher value may be added at runtime
// based on the attached display refresh rate
}
// ::onActivated must be used, as it only listens for when the index is changed by a human
onActivated : {
prefs.fps = parseInt(fpsListModel.get(currentIndex).video_fps)
var selectedFps = parseInt(model.get(currentIndex).video_fps)
prefs.bitrateKbps = prefs.getDefaultBitrate(prefs.width, prefs.height, prefs.fps);
slider.value = prefs.bitrateKbps
// Only modify the bitrate if the values actually changed
if (prefs.fps !== selectedFps) {
prefs.fps = selectedFps
prefs.bitrateKbps = prefs.getDefaultBitrate(prefs.width, prefs.height, prefs.fps);
slider.value = prefs.bitrateKbps
}
}
}
}
@@ -230,13 +304,59 @@ ScrollView {
}
}
Label {
width: parent.width
id: windowModeTitle
text: qsTr("Display mode")
font.pointSize: 12
wrapMode: Text.Wrap
color: "white"
}
AutoResizingComboBox {
// ignore setting the index at first, and actually set it when the component is loaded
Component.onCompleted: {
var savedWm = prefs.windowMode
currentIndex = 0
for (var i = 0; i < windowModeListModel.count; i++) {
var thisWm = windowModeListModel.get(i).val;
if (savedWm === thisWm) {
currentIndex = i
break
}
}
activated(currentIndex)
}
id: windowModeComboBox
textRole: "text"
model: ListModel {
id: windowModeListModel
ListElement {
text: "Full-screen (Recommended)"
val: StreamingPreferences.WM_FULLSCREEN
}
ListElement {
text: "Borderless windowed"
val: StreamingPreferences.WM_FULLSCREEN_DESKTOP
}
ListElement {
text: "Windowed"
val: StreamingPreferences.WM_WINDOWED
}
}
onActivated: {
prefs.windowMode = windowModeListModel.get(currentIndex).val
}
}
CheckBox {
id: fullScreenCheck
text: "<font color=\"white\">Full-screen</font>"
id: vsyncCheck
text: "<font color=\"white\">Enable V-Sync</font>"
font.pointSize: 12
checked: prefs.fullScreen
checked: prefs.enableVsync
onCheckedChanged: {
prefs.fullScreen = checked
prefs.enableVsync = checked
}
}
}
@@ -263,35 +383,32 @@ ScrollView {
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
currentIndex = 0
for(var i = 0; i < audioListModel.count; i++) {
var el_audio= audioListModel.get(i).val;
if(saved_audio === el_audio) {
currentIndex = i
}
for (var i = 0; i < audioListModel.count; i++) {
var el_audio = audioListModel.get(i).val;
if (saved_audio === el_audio) {
currentIndex = i
break
}
}
activated(currentIndex)
}
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
@@ -314,10 +431,10 @@ ScrollView {
id: gamepadSettingsGroupBox
width: (parent.width - parent.padding)
padding: 12
title: "<font color=\"skyblue\">Gamepad Settings</font>"
title: "<font color=\"skyblue\">Input Settings</font>"
font.pointSize: 12
Column {
Row {
anchors.fill: parent
spacing: 5
@@ -330,6 +447,16 @@ ScrollView {
prefs.multiController = checked
}
}
CheckBox {
id: mouseAccelerationCheck
text: "<font color=\"white\">Enable mouse acceleration</font>"
font.pointSize: 12
checked: prefs.mouseAcceleration
onCheckedChanged: {
prefs.mouseAcceleration = checked
}
}
}
}
@@ -340,13 +467,15 @@ ScrollView {
title: "<font color=\"skyblue\">Host Settings</font>"
font.pointSize: 12
Column {
Row {
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
font.pointSize: 12
checked: prefs.gameOptimizations
onCheckedChanged: {
@@ -386,21 +515,22 @@ ScrollView {
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
currentIndex = 0
for(var i = 0; i < decoderListModel.count; i++) {
for (var i = 0; i < decoderListModel.count; i++) {
var el_vds = decoderListModel.get(i).val;
if(saved_vds === el_vds) {
currentIndex = i
}
if (saved_vds === el_vds) {
currentIndex = i
break
}
}
activated(currentIndex)
}
id: decoderComboBox
width: Math.min(bitrateDesc.implicitWidth, parent.width)
textRole: "text"
model: ListModel {
id: decoderListModel
@@ -432,21 +562,22 @@ ScrollView {
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
currentIndex = 0
for(var i = 0; i < codecListModel.count; i++) {
var el_vcc = codecListModel.get(i).val;
if(saved_vcc === el_vcc) {
currentIndex = i
}
if (saved_vcc === el_vcc) {
currentIndex = i
break
}
}
activated(currentIndex)
}
id: codecComboBox
width: Math.min(bitrateDesc.implicitWidth, parent.width)
textRole: "text"
model: ListModel {
id: codecListModel
@@ -476,6 +607,39 @@ ScrollView {
}
}
CheckBox {
id: unlockUnsupportedFps
text: "<font color=\"white\">Unlock unsupported FPS options</font>"
font.pointSize: 12
checked: prefs.unsupportedFps
onCheckedChanged: {
prefs.unsupportedFps = checked
// The selectable FPS values depend on whether
// this option is enabled or not
fpsComboBox.reinitialize()
}
}
CheckBox {
id: enableMdns
text: "<font color=\"white\">Automatically find PCs on the local network (Recommended)</font>"
font.pointSize: 12
checked: prefs.enableMdns
onCheckedChanged: {
prefs.enableMdns = checked
// We must save the updated preference to ensure
// ComputerManager can observe the change internally.
prefs.save()
// Restart polling so the mDNS change takes effect
if (window.pollingActive) {
ComputerManager.stopPollingAsync()
ComputerManager.startPolling()
}
}
}
}
}
}
+37 -8
View File
@@ -1,6 +1,7 @@
import QtQuick 2.0
import QtQuick.Controls 2.2
import QtQuick.Dialogs 1.3
import QtQuick.Dialogs 1.2
import QtQuick.Window 2.2
import Session 1.0
@@ -8,6 +9,7 @@ Item {
property Session session
property string appName
property string stageText : "Starting " + appName + "..."
property bool quitAfter : false
anchors.fill: parent
@@ -41,8 +43,8 @@ Item {
function displayLaunchError(text)
{
// Display the error dialog after Session::exec() returns
errorDialog.text = text
errorDialog.open()
}
function displayLaunchWarning(text)
@@ -69,13 +71,25 @@ Item {
session.displayLaunchWarning.connect(displayLaunchWarning)
// Run the streaming session to completion
session.exec()
session.exec(Screen.virtualX, Screen.virtualY)
// Show the Qt window again after streaming
window.visible = true
if (quitAfter) {
Qt.quit()
} else {
// Show the Qt window again after streaming
window.visible = true
// Exit this view
stackView.pop()
// Exit this view
stackView.pop()
// Display any launch errors. We do this after
// the Qt UI is visible again to prevent losing
// focus on the dialog which would impact gamepad
// users.
if (errorDialog.text) {
errorDialog.open()
}
}
}
else {
// Show the toolbar again when we become hidden
@@ -103,10 +117,25 @@ Item {
}
}
Label {
text: "Tip: Press Ctrl+Alt+Shift+Q to disconnect your session"
anchors.bottom: parent.bottom
anchors.bottomMargin: 50
anchors.horizontalCenter: parent.horizontalCenter
font.pointSize: 18
verticalAlignment: Text.AlignVCenter
wrapMode: Text.Wrap
color: "white"
}
MessageDialog {
id: errorDialog
modality:Qt.WindowModal
icon: StandardIcon.Critical
standardButtons: StandardButton.Ok
standardButtons: StandardButton.Ok | StandardButton.Help
onHelp: {
Qt.openUrlExternally("https://github.com/moonlight-stream/moonlight-docs/wiki/Troubleshooting");
}
}
}
+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>
+21 -3
View File
@@ -1,5 +1,7 @@
#include "computermodel.h"
#include <QThreadPool>
ComputerModel::ComputerModel(QObject* object)
: QAbstractListModel(object) {}
@@ -119,11 +121,27 @@ void ComputerModel::deleteComputer(int computerIndex)
endRemoveRows();
}
bool ComputerModel::wakeComputer(int computerIndex)
class DeferredWakeHostTask : public QRunnable
{
public:
DeferredWakeHostTask(NvComputer* computer)
: m_Computer(computer) {}
void run()
{
m_Computer->wake();
}
private:
NvComputer* m_Computer;
};
void ComputerModel::wakeComputer(int computerIndex)
{
Q_ASSERT(computerIndex < m_Computers.count());
return m_Computers[computerIndex]->wake();
DeferredWakeHostTask* wakeTask = new DeferredWakeHostTask(m_Computers[computerIndex]);
QThreadPool::globalInstance()->start(wakeTask);
}
void ComputerModel::pairComputer(int computerIndex, QString pin)
@@ -135,7 +153,7 @@ void ComputerModel::pairComputer(int computerIndex, QString pin)
void ComputerModel::handlePairingCompleted(NvComputer*, QString error)
{
emit pairingCompleted(error.isNull() ? QVariant() : error);
emit pairingCompleted(error.isEmpty() ? QVariant() : error);
}
void ComputerModel::handleComputerStateChanged(NvComputer* computer)
+2 -2
View File
@@ -1,5 +1,5 @@
#include "backend/computermanager.h"
#include "streaming/session.hpp"
#include "streaming/session.h"
#include <QAbstractListModel>
@@ -34,7 +34,7 @@ public:
Q_INVOKABLE void pairComputer(int computerIndex, QString pin);
Q_INVOKABLE bool wakeComputer(int computerIndex);
Q_INVOKABLE void wakeComputer(int computerIndex);
Q_INVOKABLE Session* createSessionForCurrentGame(int computerIndex);
+268 -35
View File
@@ -1,24 +1,141 @@
import QtQuick 2.9
import QtQuick.Controls 2.2
import QtQuick.Dialogs 1.2
import QtQuick.Layouts 1.3
import QtQuick.Window 2.2
import QtQuick.Controls.Material 2.1
import ComputerManager 1.0
import AutoUpdateChecker 1.0
import StreamingPreferences 1.0
ApplicationWindow {
property bool pollingActive: false
id: window
visible: true
width: 1280
height: 600
// Maximize the window by default when the stream is configured
// for full-screen or borderless windowed. This is ideal for TV
// setups where the user doesn't want a tiny window in the middle
// of their screen when starting Moonlight.
visibility: prefs.windowMode != StreamingPreferences.WM_WINDOWED ? "Maximized" : "Windowed"
Material.theme: Material.Dark
Material.accent: Material.Purple
StreamingPreferences {
id: prefs
}
StackView {
id: stackView
initialItem: "PcView.qml"
initialItem: initialView
anchors.fill: parent
focus: true
onCurrentItemChanged: {
// Ensure focus travels to the next view when going back
if (currentItem) {
currentItem.forceActiveFocus()
}
}
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
}
}
}
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) {
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()
}
initialized = true;
}
}
function navigateTo(url, objectName)
@@ -39,22 +156,33 @@ ApplicationWindow {
header: ToolBar {
id: toolBar
anchors.topMargin: 5
anchors.bottomMargin: 5
RowLayout {
spacing: 20
anchors.fill: parent
ToolButton {
NavigableToolButton {
// Only make the button visible if the user has navigated somewhere.
visible: stackView.depth > 1
icon.source: "qrc:/res/arrow_left.svg"
// FIXME: We're using an Image here rather than icon.source because
// icons don't work on Qt 5.9 LTS.
Image {
source: "qrc:/res/arrow_left.svg"
anchors.centerIn: parent
sourceSize {
// The icon should be square so use the height as the width too
width: toolBar.height - toolBar.anchors.bottomMargin - toolBar.anchors.topMargin
height: toolBar.height - toolBar.anchors.bottomMargin - toolBar.anchors.topMargin
}
}
onClicked: stackView.pop()
Menu {
id: backButton
x: parent.width - width
transformOrigin: Menu.TopRight
Keys.onDownPressed: {
stackView.currentItem.forceActiveFocus(Qt.TabFocus)
}
}
@@ -68,12 +196,19 @@ ApplicationWindow {
Layout.fillWidth: true
}
ToolButton {
NavigableToolButton {
property string browserUrl: ""
id: updateButton
icon.source: "qrc:/res/update.svg"
icon.color: "#0BF200"
Image {
source: "qrc:/res/update.svg"
anchors.centerIn: parent
sourceSize {
width: toolBar.height - toolBar.anchors.bottomMargin - toolBar.anchors.topMargin
height: toolBar.height - toolBar.anchors.bottomMargin - toolBar.anchors.topMargin
}
}
ToolTip.delay: 1000
ToolTip.timeout: 3000
@@ -86,12 +221,6 @@ ApplicationWindow {
onClicked: Qt.openUrlExternally(browserUrl);
Menu {
x: parent.width
transformOrigin: Menu.TopRight
}
function updateAvailable(url)
{
updateButton.browserUrl = url
@@ -102,26 +231,44 @@ ApplicationWindow {
AutoUpdateChecker.onUpdateAvailable.connect(updateAvailable)
AutoUpdateChecker.start()
}
Keys.onDownPressed: {
stackView.currentItem.forceActiveFocus(Qt.TabFocus)
}
}
ToolButton {
icon.source: "qrc:/res/question_mark.svg"
NavigableToolButton {
id: helpButton
Image {
source: "qrc:/res/question_mark.svg"
anchors.centerIn: parent
sourceSize {
width: toolBar.height - toolBar.anchors.bottomMargin - toolBar.anchors.topMargin
height: toolBar.height - toolBar.anchors.bottomMargin - toolBar.anchors.topMargin
}
}
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");
Menu {
x: parent.width
transformOrigin: Menu.TopRight
Keys.onDownPressed: {
stackView.currentItem.forceActiveFocus(Qt.TabFocus)
}
}
ToolButton {
NavigableToolButton {
// TODO: Implement gamepad mapping then unhide this button
visible: false
@@ -130,29 +277,115 @@ ApplicationWindow {
ToolTip.visible: hovered
ToolTip.text: "Gamepad Mapper"
icon.source: "qrc:/res/ic_videogame_asset_white_48px.svg"
Image {
source: "qrc:/res/ic_videogame_asset_white_48px.svg"
anchors.centerIn: parent
sourceSize {
width: toolBar.height - toolBar.anchors.bottomMargin - toolBar.anchors.topMargin
height: toolBar.height - toolBar.anchors.bottomMargin - toolBar.anchors.topMargin
}
}
onClicked: navigateTo("qrc:/gui/GamepadMapper.qml", "Gamepad Mapping")
Menu {
x: parent.width
transformOrigin: Menu.TopRight
Keys.onDownPressed: {
stackView.currentItem.forceActiveFocus(Qt.TabFocus)
}
}
ToolButton {
icon.source: "qrc:/res/settings.svg"
NavigableToolButton {
id: settingsButton
Image {
source: "qrc:/res/settings.svg"
anchors.centerIn: parent
sourceSize {
width: toolBar.height - toolBar.anchors.bottomMargin - toolBar.anchors.topMargin
height: toolBar.height - toolBar.anchors.bottomMargin - toolBar.anchors.topMargin
}
}
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"
Menu {
x: parent.width
transformOrigin: Menu.TopRight
}
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()
}
}
+246
View File
@@ -0,0 +1,246 @@
#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;
}
+35
View File
@@ -0,0 +1,35 @@
#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);
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;
};
+175 -18
View File
@@ -1,5 +1,6 @@
#include <QGuiApplication>
#include <QQmlApplicationEngine>
#include <QQmlContext>
#include <QIcon>
#include <QQuickStyle>
#include <QMutex>
@@ -11,11 +12,23 @@
#define SDL_MAIN_HANDLED
#include <SDL.h>
#ifdef HAVE_FFMPEG
#include "streaming/video/ffmpeg.h"
#endif
#ifdef Q_OS_WIN32
#include "antihookingprotection.h"
#endif
#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
@@ -61,7 +74,8 @@ void logToLoggerStream(QString& message)
}
#endif
s_LoggerStream << message << endl;
s_LoggerStream << message;
s_LoggerStream.flush();
}
void sdlLogToDiskHandler(void*, int category, SDL_LogPriority priority, const char* message)
@@ -93,7 +107,7 @@ void sdlLogToDiskHandler(void*, int category, SDL_LogPriority priority, const ch
}
QTime logTime = QTime::fromMSecsSinceStartOfDay(s_LoggerTime.elapsed());
QString txt = QString("%1 - SDL %2 (%3): %4").arg(logTime.toString()).arg(priorityTxt).arg(category).arg(message);
QString txt = QString("%1 - SDL %2 (%3): %4\n").arg(logTime.toString()).arg(priorityTxt).arg(category).arg(message);
logToLoggerStream(txt);
}
@@ -121,24 +135,116 @@ void qtLogToDiskHandler(QtMsgType type, const QMessageLogContext&, const QString
}
QTime logTime = QTime::fromMSecsSinceStartOfDay(s_LoggerTime.elapsed());
QString txt = QString("%1 - Qt %2: %3").arg(logTime.toString()).arg(typeTxt).arg(msg);
QString txt = QString("%1 - Qt %2: %3\n").arg(logTime.toString()).arg(typeTxt).arg(msg);
logToLoggerStream(txt);
}
#ifdef HAVE_FFMPEG
void ffmpegLogToDiskHandler(void* ptr, int level, const char* fmt, va_list vl)
{
char lineBuffer[1024];
static int printPrefix = 1;
if ((level & 0xFF) > av_log_get_level()) {
return;
}
av_log_format_line(ptr, level, fmt, vl, lineBuffer, sizeof(lineBuffer), &printPrefix);
QTime logTime = QTime::fromMSecsSinceStartOfDay(s_LoggerTime.elapsed());
QString txt = QString("%1 - FFmpeg: %2").arg(logTime.toString()).arg(lineBuffer);
logToLoggerStream(txt);
}
#endif
#endif
#ifdef Q_OS_WIN32
#define WIN32_LEAN_AND_MEAN
#include <Windows.h>
#include <DbgHelp.h>
static UINT s_HitUnhandledException = 0;
LONG WINAPI UnhandledExceptionHandler(struct _EXCEPTION_POINTERS *ExceptionInfo)
{
// Only write a dump for the first unhandled exception
if (InterlockedCompareExchange(&s_HitUnhandledException, 1, 0) != 0) {
return EXCEPTION_CONTINUE_SEARCH;
}
WCHAR dmpFileName[MAX_PATH];
swprintf_s(dmpFileName, L"%ls\\Moonlight-%I64u.dmp",
(PWCHAR)QDir::toNativeSeparators(Path::getLogDir()).utf16(), QDateTime::currentSecsSinceEpoch());
QString qDmpFileName = QString::fromUtf16((unsigned short*)dmpFileName);
HANDLE dumpHandle = CreateFileW(dmpFileName, GENERIC_WRITE, 0, nullptr, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr);
if (dumpHandle != INVALID_HANDLE_VALUE) {
MINIDUMP_EXCEPTION_INFORMATION info;
info.ThreadId = GetCurrentThreadId();
info.ExceptionPointers = ExceptionInfo;
info.ClientPointers = FALSE;
DWORD typeFlags = MiniDumpWithIndirectlyReferencedMemory |
MiniDumpIgnoreInaccessibleMemory |
MiniDumpWithUnloadedModules |
MiniDumpWithThreadInfo;
if (MiniDumpWriteDump(GetCurrentProcess(),
GetCurrentProcessId(),
dumpHandle,
(MINIDUMP_TYPE)typeFlags,
&info,
nullptr,
nullptr)) {
qCritical() << "Unhandled exception! Minidump written to:" << qDmpFileName;
}
else {
qCritical() << "Unhandled exception! Failed to write dump:" << GetLastError();
}
CloseHandle(dumpHandle);
}
else {
qCritical() << "Unhandled exception! Failed to open dump file:" << qDmpFileName << "with error" << GetLastError();
}
// Let the program crash and WER collect a dump
return EXCEPTION_CONTINUE_SEARCH;
}
#endif
int main(int argc, char *argv[])
{
// 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().
QCoreApplication::setOrganizationName("Moonlight Game Streaming Project");
QCoreApplication::setOrganizationDomain("moonlight-stream.com");
QCoreApplication::setApplicationName("Moonlight");
if (QFile(QDir::currentPath() + "/portable.dat").exists()) {
qInfo() << "Running in portable mode from:" << QDir::currentPath();
QSettings::setDefaultFormat(QSettings::IniFormat);
QSettings::setPath(QSettings::IniFormat, QSettings::UserScope, QDir::currentPath());
QSettings::setPath(QSettings::IniFormat, QSettings::SystemScope, QDir::currentPath());
// Initialize paths for portable mode
Path::initialize(true);
}
else {
// Initialize paths for standard installation
Path::initialize(false);
}
#ifdef USE_CUSTOM_LOGGER
#ifdef LOG_TO_FILE
#ifdef Q_OS_DARWIN
// On macOS, $TMPDIR is some random folder under /var/folders/ that nobody can
// easily find, so use the system's global tmp directory instead.
QDir tempDir("/tmp");
#else
QDir tempDir(QDir::tempPath());
#endif
QDir tempDir(Path::getLogDir());
s_LoggerFile = new QFile(tempDir.filePath(QString("Moonlight-%1.log").arg(QDateTime::currentSecsSinceEpoch())));
if (s_LoggerFile->open(QIODevice::WriteOnly)) {
qInfo() << "Redirecting log output to " << s_LoggerFile->fileName();
@@ -149,6 +255,22 @@ int main(int argc, char *argv[])
s_LoggerTime.start();
qInstallMessageHandler(qtLogToDiskHandler);
SDL_LogSetOutputFunction(sdlLogToDiskHandler, nullptr);
#ifdef HAVE_FFMPEG
av_log_set_callback(ffmpegLogToDiskHandler);
#endif
#endif
#ifdef Q_OS_WIN32
// Create a crash dump when we crash on Windows
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);
@@ -157,10 +279,14 @@ int main(int argc, char *argv[])
// password prompts on macOS.
qputenv("QT_SSL_USE_TEMPORARY_KEYCHAIN", QByteArray("1"));
// Set these here to allow us to use the default QSettings constructor
QCoreApplication::setOrganizationName("Moonlight Game Streaming Project");
QCoreApplication::setOrganizationDomain("moonlight-stream.com");
QCoreApplication::setApplicationName("Moonlight");
#ifdef Q_OS_WIN32
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
// Register custom metatypes for use in signals
qRegisterMetaType<NvApp>("NvApp");
@@ -173,6 +299,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",
@@ -187,8 +314,31 @@ 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;
}
}
engine.rootContext()->setContextProperty("initialView", initialView);
// Load the main.qml file
engine.load(QUrl(QStringLiteral("qrc:/gui/main.qml")));
if (engine.rootObjects().isEmpty())
return -1;
@@ -200,9 +350,16 @@ int main(int argc, char *argv[])
SDL_GetError());
}
int err = app.exec();
// Use atexit() to ensure SDL_Quit() is called. This avoids
// racing with object destruction where SDL may be used.
atexit(SDL_Quit);
SDL_Quit();
// Avoid the default behavior of changing the timer resolution to 1 ms.
// We don't want this all the time that Moonlight is open. We will set
// it manually when we start streaming.
SDL_SetHint(SDL_HINT_TIMER_RESOLUTION, "0");
int err = app.exec();
return err;
}
+77
View File
@@ -0,0 +1,77 @@
#include "path.h"
#include <QtDebug>
#include <QDir>
#include <QStandardPaths>
#include <QSettings>
#include <QCoreApplication>
QString Path::s_LogDir;
QString Path::s_BoxArtCacheDir;
const QString Path::k_GameControllerMappingFile("gamecontrollerdb.txt");
QString Path::getLogDir()
{
Q_ASSERT(!s_LogDir.isEmpty());
return s_LogDir;
}
QString Path::getBoxArtCacheDir()
{
Q_ASSERT(!s_BoxArtCacheDir.isEmpty());
return s_BoxArtCacheDir;
}
QString Path::getGamepadMappingFile()
{
QString candidatePath;
// Check the current directory first
candidatePath = QDir(QDir::currentPath()).absoluteFilePath(k_GameControllerMappingFile);
if (QFile::exists(candidatePath)) {
return candidatePath;
}
// Now check the data directories (for Linux, in particular)
candidatePath = QStandardPaths::locate(QStandardPaths::DataLocation, k_GameControllerMappingFile);
if (!candidatePath.isEmpty() && QFile::exists(candidatePath)) {
return candidatePath;
}
// Now try the directory of our app installation (for Windows, if current dir doesn't find it)
candidatePath = QDir(QCoreApplication::applicationDirPath()).absoluteFilePath(k_GameControllerMappingFile);
if (QFile::exists(candidatePath)) {
return candidatePath;
}
// Now try the Resources folder in our app bundle for macOS
QDir dir = QDir(QCoreApplication::applicationDirPath());
dir.cdUp();
dir.cd("Resources");
dir.filePath(k_GameControllerMappingFile);
candidatePath = dir.absoluteFilePath(k_GameControllerMappingFile);
if (QFile::exists(candidatePath)) {
return candidatePath;
}
// Couldn't find it
return QString();
}
void Path::initialize(bool portable)
{
if (portable) {
s_LogDir = QDir::currentPath();
s_BoxArtCacheDir = QDir::currentPath() + "/boxart";
}
else {
#ifdef Q_OS_DARWIN
// On macOS, $TMPDIR is some random folder under /var/folders/ that nobody can
// easily find, so use the system's global tmp directory instead.
s_LogDir = "/tmp";
#else
s_LogDir = QDir::tempPath();
#endif
s_BoxArtCacheDir = QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + "/boxart";
}
}
+20
View File
@@ -0,0 +1,20 @@
#pragma once
#include <QString>
class Path
{
public:
static QString getLogDir();
static QString getBoxArtCacheDir();
static QString getGamepadMappingFile();
static void initialize(bool portable);
private:
static QString s_LogDir;
static QString s_BoxArtCacheDir;
static const QString k_GameControllerMappingFile;
};
+5
View File
@@ -7,5 +7,10 @@
<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/CliStartStreamSegue.qml</file>
<file>gui/AutoResizingComboBox.qml</file>
</qresource>
</RCC>
+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">
<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">
<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: 484 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;
};
+84 -12
View File
@@ -1,5 +1,6 @@
#include "streamingpreferences.h"
#include "streaming/session.hpp"
#include "streaming/session.h"
#include "streaming/streamutils.h"
#include <QSettings>
@@ -9,14 +10,20 @@
#define SER_FPS "fps"
#define SER_BITRATE "bitrate"
#define SER_FULLSCREEN "fullscreen"
#define SER_VSYNC "vsync"
#define SER_GAMEOPTS "gameopts"
#define SER_HOSTAUDIO "hostaudio"
#define SER_MULTICONT "multicontroller"
#define SER_AUDIOCFG "audiocfg"
#define SER_VIDEOCFG "videocfg"
#define SER_VIDEODEC "videodec"
#define SER_WINDOWMODE "windowmode"
#define SER_UNSUPPORTEDFPS "unsupportedfps"
#define SER_MDNS "mdns"
#define SER_MOUSEACCELERATION "mouseacceleration"
StreamingPreferences::StreamingPreferences()
StreamingPreferences::StreamingPreferences(QObject *parent)
: QObject(parent)
{
reload();
}
@@ -29,16 +36,23 @@ void StreamingPreferences::reload()
height = settings.value(SER_HEIGHT, 720).toInt();
fps = settings.value(SER_FPS, 60).toInt();
bitrateKbps = settings.value(SER_BITRATE, getDefaultBitrate(width, height, fps)).toInt();
fullScreen = settings.value(SER_FULLSCREEN, true).toBool();
enableVsync = settings.value(SER_VSYNC, true).toBool();
gameOptimizations = settings.value(SER_GAMEOPTS, true).toBool();
playAudioOnHost = settings.value(SER_HOSTAUDIO, false).toBool();
multiController = settings.value(SER_MULTICONT, true).toBool();
unsupportedFps = settings.value(SER_UNSUPPORTEDFPS, false).toBool();
enableMdns = settings.value(SER_MDNS, true).toBool();
mouseAcceleration = settings.value(SER_MOUSEACCELERATION, 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,
static_cast<int>(VideoDecoderSelection::VDS_AUTO)).toInt());
windowMode = static_cast<WindowMode>(settings.value(SER_WINDOWMODE,
// Try to load from the old preference value too
static_cast<int>(settings.value(SER_FULLSCREEN, true).toBool() ?
WindowMode::WM_FULLSCREEN : WindowMode::WM_WINDOWED)).toInt());
}
void StreamingPreferences::save()
@@ -49,13 +63,17 @@ void StreamingPreferences::save()
settings.setValue(SER_HEIGHT, height);
settings.setValue(SER_FPS, fps);
settings.setValue(SER_BITRATE, bitrateKbps);
settings.setValue(SER_FULLSCREEN, fullScreen);
settings.setValue(SER_VSYNC, enableVsync);
settings.setValue(SER_GAMEOPTS, gameOptimizations);
settings.setValue(SER_HOSTAUDIO, playAudioOnHost);
settings.setValue(SER_MULTICONT, multiController);
settings.setValue(SER_UNSUPPORTEDFPS, unsupportedFps);
settings.setValue(SER_MDNS, enableMdns);
settings.setValue(SER_MOUSEACCELERATION, mouseAcceleration);
settings.setValue(SER_AUDIOCFG, static_cast<int>(audioConfig));
settings.setValue(SER_VIDEOCFG, static_cast<int>(videoCodecConfig));
settings.setValue(SER_VIDEODEC, static_cast<int>(videoDecoderSelection));
settings.setValue(SER_WINDOWMODE, static_cast<int>(windowMode));
}
bool StreamingPreferences::hasAnyHardwareAcceleration()
@@ -72,6 +90,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
@@ -84,12 +116,26 @@ int StreamingPreferences::getMaximumStreamingFrameRate()
return maxFrameRate;
}
for (int i = 0; i < SDL_GetNumVideoDisplays(); i++) {
SDL_DisplayMode mode;
if (SDL_GetCurrentDisplayMode(i, &mode) == 0) {
SDL_DisplayMode mode, bestMode, desktopMode;
for (int displayIndex = 0; displayIndex < SDL_GetNumVideoDisplays(); displayIndex++) {
// Get the native desktop resolution
if (StreamUtils::getRealDesktopMode(displayIndex, &desktopMode)) {
// Start at desktop mode and work our way up
bestMode = desktopMode;
for (int i = 0; i < SDL_GetNumDisplayModes(displayIndex); i++) {
if (SDL_GetDisplayMode(displayIndex, i, &mode) == 0) {
if (mode.w == desktopMode.w && mode.h == desktopMode.h) {
if (mode.refresh_rate > bestMode.refresh_rate) {
bestMode = mode;
}
}
}
}
// Cap the frame rate at 120 FPS. Past this, the encoders start
// to max out and drop frames.
maxFrameRate = qMax(maxFrameRate, qMin(120, mode.refresh_rate));
maxFrameRate = qMax(maxFrameRate, qMin(120, bestMode.refresh_rate));
}
}
@@ -98,7 +144,7 @@ int StreamingPreferences::getMaximumStreamingFrameRate()
return maxFrameRate;
}
QRect StreamingPreferences::getDisplayResolution(int displayIndex)
QRect StreamingPreferences::getDesktopResolution(int displayIndex)
{
if (SDL_InitSubSystem(SDL_INIT_VIDEO) != 0) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
@@ -113,7 +159,7 @@ QRect StreamingPreferences::getDisplayResolution(int displayIndex)
}
SDL_DisplayMode mode;
int err = SDL_GetCurrentDisplayMode(displayIndex, &mode);
int err = SDL_GetDesktopDisplayMode(displayIndex, &mode);
SDL_QuitSubSystem(SDL_INIT_VIDEO);
if (err == 0) {
@@ -121,12 +167,38 @@ QRect StreamingPreferences::getDisplayResolution(int displayIndex)
}
else {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"SDL_GetCurrentDisplayMode() failed: %s",
"SDL_GetDesktopDisplayMode() failed: %s",
SDL_GetError());
return QRect();
}
}
QRect StreamingPreferences::getNativeResolution(int displayIndex)
{
if (SDL_InitSubSystem(SDL_INIT_VIDEO) != 0) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"SDL_InitSubSystem(SDL_INIT_VIDEO) failed: %s",
SDL_GetError());
return QRect();
}
if (displayIndex >= SDL_GetNumVideoDisplays()) {
SDL_QuitSubSystem(SDL_INIT_VIDEO);
return QRect();
}
SDL_DisplayMode mode;
bool success = StreamUtils::getRealDesktopMode(displayIndex, &mode);
SDL_QuitSubSystem(SDL_INIT_VIDEO);
if (success) {
return QRect(0, 0, mode.w, mode.h);
}
else {
return QRect();
}
}
int StreamingPreferences::getDefaultBitrate(int width, int height, int fps)
{
// This table prefers 16:10 resolutions because they are
+33 -8
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,17 +19,22 @@ public:
Q_INVOKABLE static bool isRunningWayland();
Q_INVOKABLE static bool isWow64();
Q_INVOKABLE static int getMaximumStreamingFrameRate();
Q_INVOKABLE QRect getDisplayResolution(int displayIndex);
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)
@@ -50,40 +55,60 @@ public:
};
Q_ENUM(VideoDecoderSelection)
enum WindowMode
{
WM_FULLSCREEN,
WM_FULLSCREEN_DESKTOP,
WM_WINDOWED
};
Q_ENUM(WindowMode)
Q_PROPERTY(int width MEMBER width NOTIFY displayModeChanged)
Q_PROPERTY(int height MEMBER height NOTIFY displayModeChanged)
Q_PROPERTY(int fps MEMBER fps NOTIFY displayModeChanged)
Q_PROPERTY(int bitrateKbps MEMBER bitrateKbps NOTIFY bitrateChanged)
Q_PROPERTY(bool fullScreen MEMBER fullScreen NOTIFY fullScreenChanged)
Q_PROPERTY(bool enableVsync MEMBER enableVsync NOTIFY enableVsyncChanged)
Q_PROPERTY(bool gameOptimizations MEMBER gameOptimizations NOTIFY gameOptimizationsChanged)
Q_PROPERTY(bool playAudioOnHost MEMBER playAudioOnHost NOTIFY playAudioOnHostChanged)
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 mouseAcceleration MEMBER mouseAcceleration NOTIFY mouseAccelerationChanged)
Q_PROPERTY(AudioConfig audioConfig MEMBER audioConfig NOTIFY audioConfigChanged)
Q_PROPERTY(VideoCodecConfig videoCodecConfig MEMBER videoCodecConfig NOTIFY videoCodecConfigChanged)
Q_PROPERTY(VideoDecoderSelection videoDecoderSelection MEMBER videoDecoderSelection NOTIFY videoDecoderSelectionChanged)
Q_PROPERTY(WindowMode windowMode MEMBER windowMode NOTIFY windowModeChanged)
// Directly accessible members for preferences
int width;
int height;
int fps;
int bitrateKbps;
bool fullScreen;
bool enableVsync;
bool gameOptimizations;
bool playAudioOnHost;
bool multiController;
bool unsupportedFps;
bool enableMdns;
bool mouseAcceleration;
AudioConfig audioConfig;
VideoCodecConfig videoCodecConfig;
VideoDecoderSelection videoDecoderSelection;
WindowMode windowMode;
signals:
void displayModeChanged();
void bitrateChanged();
void fullScreenChanged();
void enableVsyncChanged();
void gameOptimizationsChanged();
void playAudioOnHostChanged();
void multiControllerChanged();
void unsupportedFpsChanged();
void enableMdnsChanged();
void mouseAccelerationChanged();
void audioConfigChanged();
void videoCodecConfigChanged();
void videoDecoderSelectionChanged();
void windowModeChanged();
};
-228
View File
@@ -1,228 +0,0 @@
#include "session.hpp"
#include <Limelight.h>
#include <SDL.h>
#define MAX_CHANNELS 6
#define SAMPLES_PER_FRAME 240
#define MIN_QUEUED_FRAMES 2
#define MAX_QUEUED_FRAMES 4
#define STOP_THE_WORLD_LIMIT 20
#define DROP_RATIO_DENOM 32
SDL_AudioDeviceID Session::s_AudioDevice;
OpusMSDecoder* Session::s_OpusDecoder;
short Session::s_OpusDecodeBuffer[MAX_CHANNELS * SAMPLES_PER_FRAME];
int Session::s_ChannelCount;
int Session::s_PendingDrops;
int Session::s_PendingHardDrops;
unsigned int Session::s_SampleIndex;
Uint32 Session::s_BaselinePendingData;
int Session::sdlDetermineAudioConfiguration()
{
SDL_AudioSpec want, have;
SDL_AudioDeviceID dev;
int ret;
if (SDL_InitSubSystem(SDL_INIT_AUDIO) != 0) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"SDL_InitSubSystem(SDL_INIT_AUDIO) failed: %s",
SDL_GetError());
return AUDIO_CONFIGURATION_STEREO;
}
SDL_zero(want);
want.freq = 48000;
want.format = AUDIO_S16;
want.channels = 6;
want.samples = 1024;
// Try to open for 5.1 surround sound, but allow SDL to tell us that's
// not available.
dev = SDL_OpenAudioDevice(NULL, 0, &want, &have, SDL_AUDIO_ALLOW_CHANNELS_CHANGE);
if (dev == 0) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Failed to open audio device");
// We'll probably have issues during audio stream init, but we'll
// try anyway
ret = AUDIO_CONFIGURATION_STEREO;
goto Exit;
}
SDL_CloseAudioDevice(dev);
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Audio device has %d channels", have.channels);
if (have.channels > 2) {
// We don't support quadraphonic or 7.1 surround, but SDL
// should be able to downmix or upmix better from 5.1 than
// from stereo, so use 5.1 in non-stereo cases.
ret = AUDIO_CONFIGURATION_51_SURROUND;
}
else {
ret = AUDIO_CONFIGURATION_STEREO;
}
Exit:
SDL_QuitSubSystem(SDL_INIT_AUDIO);
return ret;
}
int Session::sdlAudioInit(int /* audioConfiguration */,
POPUS_MULTISTREAM_CONFIGURATION opusConfig,
void* /* arContext */, int /* arFlags */)
{
SDL_AudioSpec want, have;
int error;
SDL_assert(!SDL_WasInit(SDL_INIT_AUDIO));
if (SDL_InitSubSystem(SDL_INIT_AUDIO) != 0) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"SDL_InitSubSystem(SDL_INIT_AUDIO) failed: %s",
SDL_GetError());
return AUDIO_CONFIGURATION_STEREO;
}
SDL_zero(want);
want.freq = opusConfig->sampleRate;
want.format = AUDIO_S16;
want.channels = opusConfig->channelCount;
// This is supposed to be a power of 2, but our
// frames contain a non-power of 2 number of samples,
// so the slop would require buffering another full frame.
// Specifying non-Po2 seems to work for our supported platforms.
want.samples = SAMPLES_PER_FRAME;
s_AudioDevice = SDL_OpenAudioDevice(NULL, 0, &want, &have, 0);
if (s_AudioDevice == 0) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Failed to open audio device: %s",
SDL_GetError());
SDL_QuitSubSystem(SDL_INIT_AUDIO);
return -1;
}
s_OpusDecoder = opus_multistream_decoder_create(opusConfig->sampleRate,
opusConfig->channelCount,
opusConfig->streams,
opusConfig->coupledStreams,
opusConfig->mapping,
&error);
if (s_OpusDecoder == NULL) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Failed to create decoder: %d",
error);
SDL_CloseAudioDevice(s_AudioDevice);
s_AudioDevice = 0;
SDL_QuitSubSystem(SDL_INIT_AUDIO);
return -2;
}
// 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.
s_BaselinePendingData = 0;
#ifdef Q_OS_WIN32
for (int i = 0; i < 100; i++) {
s_BaselinePendingData = qMax(s_BaselinePendingData, SDL_GetQueuedAudioSize(s_AudioDevice));
SDL_Delay(10);
}
#endif
s_BaselinePendingData *= 2;
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Baseline pending audio data: %d bytes",
s_BaselinePendingData);
s_ChannelCount = opusConfig->channelCount;
s_SampleIndex = 0;
s_PendingDrops = s_PendingHardDrops = 0;
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Audio stream has %d channels",
opusConfig->channelCount);
return 0;
}
void Session::sdlAudioStart()
{
// Unpause the audio device
SDL_PauseAudioDevice(s_AudioDevice, 0);
}
void Session::sdlAudioStop()
{
// Pause the audio device
SDL_PauseAudioDevice(s_AudioDevice, 1);
}
void Session::sdlAudioCleanup()
{
SDL_CloseAudioDevice(s_AudioDevice);
s_AudioDevice = 0;
opus_multistream_decoder_destroy(s_OpusDecoder);
s_OpusDecoder = nullptr;
SDL_QuitSubSystem(SDL_INIT_AUDIO);
SDL_assert(!SDL_WasInit(SDL_INIT_AUDIO));
}
void Session::sdlAudioDecodeAndPlaySample(char* sampleData, int sampleLength)
{
int samplesDecoded;
s_SampleIndex++;
Uint32 queuedAudio = qMax((int)SDL_GetQueuedAudioSize(s_AudioDevice) - (int)s_BaselinePendingData, 0);
Uint32 framesQueued = queuedAudio / (SAMPLES_PER_FRAME * s_ChannelCount * sizeof(short));
// Pend enough drops to get us back to MIN_QUEUED_FRAMES
if (framesQueued - s_PendingHardDrops > STOP_THE_WORLD_LIMIT) {
s_PendingHardDrops = framesQueued - MIN_QUEUED_FRAMES;
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Pending hard drop of %u audio frames",
s_PendingHardDrops);
}
else if (framesQueued - s_PendingHardDrops - s_PendingDrops > MAX_QUEUED_FRAMES) {
s_PendingDrops = framesQueued - MIN_QUEUED_FRAMES;
}
// Determine if this frame should be dropped
if (framesQueued <= MIN_QUEUED_FRAMES) {
s_PendingDrops = s_PendingHardDrops = 0;
}
else if (s_PendingHardDrops != 0) {
// Hard drops happen all at once to forcefully
// resync with the source.
s_PendingHardDrops--;
return;
}
else if (s_PendingDrops != 0 && s_SampleIndex % DROP_RATIO_DENOM == 0) {
// Normal drops are interspersed with the audio data
// to hide the glitches.
s_PendingDrops--;
return;
}
samplesDecoded = opus_multistream_decode(s_OpusDecoder,
(unsigned char*)sampleData,
sampleLength,
s_OpusDecodeBuffer,
SAMPLES_PER_FRAME,
0);
if (samplesDecoded > 0) {
if (SDL_QueueAudio(s_AudioDevice,
s_OpusDecodeBuffer,
sizeof(short) * samplesDecoded * s_ChannelCount) < 0) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Failed to queue audio sample: %s",
SDL_GetError());
}
}
}
+150
View File
@@ -0,0 +1,150 @@
#include "../session.h"
#include "renderers/renderer.h"
#ifdef HAVE_SOUNDIO
#include "renderers/soundioaudiorenderer.h"
#else
#include "renderers/sdl.h"
#endif
#include <Limelight.h>
IAudioRenderer* Session::createAudioRenderer()
{
#ifdef HAVE_SOUNDIO
return new SoundIoAudioRenderer();
#else
return new SdlAudioRenderer();
#endif
}
bool Session::testAudio(int audioConfiguration)
{
IAudioRenderer* audioRenderer;
audioRenderer = createAudioRenderer();
if (audioRenderer == nullptr) {
return false;
}
// 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::arInit(int /* audioConfiguration */,
const POPUS_MULTISTREAM_CONFIGURATION opusConfig,
void* /* arContext */, int /* arFlags */)
{
int error;
SDL_memcpy(&s_ActiveSession->m_AudioConfig, opusConfig, sizeof(*opusConfig));
s_ActiveSession->m_OpusDecoder =
opus_multistream_decoder_create(opusConfig->sampleRate,
opusConfig->channelCount,
opusConfig->streams,
opusConfig->coupledStreams,
opusConfig->mapping,
&error);
if (s_ActiveSession->m_OpusDecoder == NULL) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Failed to create decoder: %d",
error);
return -1;
}
s_ActiveSession->m_AudioRenderer = s_ActiveSession->createAudioRenderer();
if (!s_ActiveSession->m_AudioRenderer->prepareForPlayback(opusConfig)) {
delete s_ActiveSession->m_AudioRenderer;
opus_multistream_decoder_destroy(s_ActiveSession->m_OpusDecoder);
return -2;
}
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Audio stream has %d channels",
opusConfig->channelCount);
return 0;
}
void Session::arCleanup()
{
delete s_ActiveSession->m_AudioRenderer;
s_ActiveSession->m_AudioRenderer = nullptr;
opus_multistream_decoder_destroy(s_ActiveSession->m_OpusDecoder);
s_ActiveSession->m_OpusDecoder = nullptr;
}
void Session::arDecodeAndPlaySample(char* sampleData, int sampleLength)
{
int samplesDecoded;
// Set this thread to high priority to reduce
// the chance of missing our sample delivery time
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;
}
}
}
+96
View File
@@ -0,0 +1,96 @@
#include "qtaud.h"
#include <QDebug>
QtAudioRenderer::QtAudioRenderer()
: m_AudioOutput(nullptr),
m_OutputDevice(nullptr)
{
}
QtAudioRenderer::~QtAudioRenderer()
{
delete m_AudioOutput;
}
bool QtAudioRenderer::prepareForPlayback(const OPUS_MULTISTREAM_CONFIGURATION* opusConfig)
{
QAudioFormat format;
// testAudio() assumes this sample rate
Q_ASSERT(opusConfig->sampleRate == 48000);
format.setSampleRate(opusConfig->sampleRate);
format.setChannelCount(opusConfig->channelCount);
format.setSampleSize(16);
format.setSampleType(QAudioFormat::SignedInt);
format.setCodec("audio/pcm");
m_AudioOutput = new QAudioOutput(format);
// Buffer 40 ms of audio data
m_AudioOutput->setBufferSize(format.bytesForDuration(40000));
m_OutputDevice = m_AudioOutput->start();
if (m_OutputDevice == nullptr) {
qWarning() << "Audio start failed:" << m_AudioOutput->error();
return false;
}
// Check what buffer size was really used after starting
qInfo() << "Audio stream buffer:" << m_AudioOutput->bufferSize() << "bytes";
return true;
}
void QtAudioRenderer::submitAudio(short* audioBuffer, int audioSize)
{
m_OutputDevice->write((const char *)audioBuffer, audioSize);
}
bool QtAudioRenderer::testAudio(int audioConfiguration)
{
QAudioFormat format;
format.setSampleRate(48000);
format.setSampleSize(16);
format.setSampleType(QAudioFormat::SignedInt);
format.setCodec("audio/pcm");
switch (audioConfiguration) {
case AUDIO_CONFIGURATION_STEREO:
format.setChannelCount(2);
break;
case AUDIO_CONFIGURATION_51_SURROUND:
format.setChannelCount(6);
break;
default:
Q_ASSERT(false);
return false;
}
if (QAudioDeviceInfo::defaultOutputDevice().isFormatSupported(format)) {
qInfo() << "Audio test successful with" << format.channelCount() << "channels";
return true;
}
else {
qWarning() << "Audio test failed with" << format.channelCount() << "channels";
return false;
}
}
int QtAudioRenderer::detectAudioConfiguration()
{
int preferredChannelCount = QAudioDeviceInfo::defaultOutputDevice().preferredFormat().channelCount();
qInfo() << "Audio output device prefers" << preferredChannelCount << "channel configuration";
// 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;
}
}
+24
View File
@@ -0,0 +1,24 @@
#pragma once
#include "renderer.h"
#include <QAudioOutput>
class QtAudioRenderer : public IAudioRenderer
{
public:
QtAudioRenderer();
virtual ~QtAudioRenderer();
virtual bool prepareForPlayback(const OPUS_MULTISTREAM_CONFIGURATION* opusConfig);
virtual void submitAudio(short* audioBuffer, int audioSize);
virtual bool testAudio(int audioConfiguration);
virtual int detectAudioConfiguration();
private:
QAudioOutput* m_AudioOutput;
QIODevice* m_OutputDevice;
};
+17
View File
@@ -0,0 +1,17 @@
#pragma once
#include <Limelight.h>
#define MAX_CHANNELS 6
#define SAMPLES_PER_FRAME 240
class IAudioRenderer
{
public:
virtual ~IAudioRenderer() {}
virtual bool prepareForPlayback(const OPUS_MULTISTREAM_CONFIGURATION* opusConfig) = 0;
// Return false if an unrecoverable error has occurred and the renderer must be reinitialized
virtual bool submitAudio(short* audioBuffer, int audioSize) = 0;
};
+24
View File
@@ -0,0 +1,24 @@
#pragma once
#include "renderer.h"
#include <SDL.h>
class SdlAudioRenderer : public IAudioRenderer
{
public:
SdlAudioRenderer();
virtual ~SdlAudioRenderer();
virtual bool prepareForPlayback(const OPUS_MULTISTREAM_CONFIGURATION* opusConfig);
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;
};
+159
View File
@@ -0,0 +1,159 @@
#include "sdl.h"
#include <Limelight.h>
#include <SDL.h>
#include <QtGlobal>
#define MIN_QUEUED_FRAMES 2
#define MAX_QUEUED_FRAMES 4
#define STOP_THE_WORLD_LIMIT 20
#define DROP_RATIO_DENOM 32
SdlAudioRenderer::SdlAudioRenderer()
: m_AudioDevice(0),
m_ChannelCount(0),
m_PendingDrops(0),
m_PendingHardDrops(0),
m_SampleIndex(0),
m_BaselinePendingData(0)
{
SDL_assert(!SDL_WasInit(SDL_INIT_AUDIO));
if (SDL_InitSubSystem(SDL_INIT_AUDIO) != 0) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"SDL_InitSubSystem(SDL_INIT_AUDIO) failed: %s",
SDL_GetError());
SDL_assert(SDL_WasInit(SDL_INIT_AUDIO));
}
}
bool SdlAudioRenderer::prepareForPlayback(const OPUS_MULTISTREAM_CONFIGURATION* opusConfig)
{
SDL_AudioSpec want, have;
SDL_zero(want);
want.freq = opusConfig->sampleRate;
want.format = AUDIO_S16;
want.channels = opusConfig->channelCount;
// This is supposed to be a power of 2, but our
// frames contain a non-power of 2 number of samples,
// so the slop would require buffering another full frame.
// Specifying non-Po2 seems to work for our supported platforms.
want.samples = SAMPLES_PER_FRAME;
m_AudioDevice = SDL_OpenAudioDevice(NULL, 0, &want, &have, 0);
if (m_AudioDevice == 0) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Failed to open audio device: %s",
SDL_GetError());
SDL_QuitSubSystem(SDL_INIT_AUDIO);
return false;
}
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Desired audio buffer: %u samples (%lu bytes)",
want.samples,
want.samples * sizeof(short) * want.channels);
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Obtained audio buffer: %u samples (%u bytes)",
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);
return true;
}
SdlAudioRenderer::~SdlAudioRenderer()
{
if (m_AudioDevice != 0) {
// Stop playback
SDL_PauseAudioDevice(m_AudioDevice, 1);
SDL_CloseAudioDevice(m_AudioDevice);
}
SDL_QuitSubSystem(SDL_INIT_AUDIO);
SDL_assert(!SDL_WasInit(SDL_INIT_AUDIO));
}
bool SdlAudioRenderer::submitAudio(short* audioBuffer, int audioSize)
{
m_SampleIndex++;
Uint32 queuedAudio = SDL_GetQueuedAudioSize(m_AudioDevice);
if (queuedAudio > m_BaselinePendingData) {
queuedAudio -= m_BaselinePendingData;
}
else {
queuedAudio = 0;
}
Uint32 framesQueued = queuedAudio / (SAMPLES_PER_FRAME * m_ChannelCount * sizeof(short));
// We must check this prior to the below checks to ensure we don't
// underflow if framesQueued - m_PendingHardDrops < 0.
if (framesQueued <= MIN_QUEUED_FRAMES) {
m_PendingDrops = m_PendingHardDrops = 0;
}
// Pend enough drops to get us back to MIN_QUEUED_FRAMES, checking first
// to ensure we don't underflow.
else if (framesQueued > m_PendingHardDrops &&
framesQueued - m_PendingHardDrops > STOP_THE_WORLD_LIMIT) {
m_PendingHardDrops = framesQueued - MIN_QUEUED_FRAMES;
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Pending hard drop of %u audio frames",
m_PendingHardDrops);
}
// If we're under the stop the world limit, we can drop samples
// gracefully over the next little while.
else if (framesQueued > m_PendingHardDrops + m_PendingDrops &&
framesQueued - m_PendingHardDrops - m_PendingDrops > MAX_QUEUED_FRAMES) {
m_PendingDrops = framesQueued - MIN_QUEUED_FRAMES;
}
// Determine if this frame should be dropped
if (m_PendingHardDrops != 0) {
// Hard drops happen all at once to forcefully
// resync with the source.
m_PendingHardDrops--;
return true;
}
else if (m_PendingDrops != 0 && m_SampleIndex % DROP_RATIO_DENOM == 0) {
// Normal drops are interspersed with the audio data
// to hide the glitches.
m_PendingDrops--;
return true;
}
if (SDL_QueueAudio(m_AudioDevice, audioBuffer, audioSize) < 0) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Failed to queue audio sample: %s",
SDL_GetError());
}
return true;
}
@@ -0,0 +1,436 @@
#include "soundioaudiorenderer.h"
#include <SDL.h>
#include <QtGlobal>
SoundIoAudioRenderer::SoundIoAudioRenderer()
: m_OpusChannelCount(0),
m_SoundIo(nullptr),
m_Device(nullptr),
m_OutputStream(nullptr),
m_RingBuffer(nullptr),
m_Errored(false)
{
}
SoundIoAudioRenderer::~SoundIoAudioRenderer()
{
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->name = "Moonlight";
m_OutputStream->userdata = this;
m_OutputStream->error_callback = sioErrorCallback;
m_OutputStream->write_callback = sioWriteCallback;
// This determines the size of the buffers we'll
// get from CoreAudio. Since GFE sends us packets
// in 5 ms chunks, we'll give them to the OS in
// buffers of the same size.
m_OutputStream->software_latency = 0.005;
SoundIoChannelLayout bestLayout = m_Device->current_layout;
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;
}
}
// Buffer up to 6 packets of audio (30 ms) to smooth
// out network packet delivery jitter
m_RingBuffer = soundio_ring_buffer_create(m_SoundIo,
m_OutputStream->bytes_per_sample *
m_OpusChannelCount *
SAMPLES_PER_FRAME * 6);
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;
}
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);
// Place an upper-bound on audio stream latency to
// avoid accumulating packets in queue-based backends
// like WASAPI. This bound was set by testing on several
// Windows machines. The highest latency was found on
// a XPS 9343 running Windows 7 in Steam Big Picture
// and the 5.1 audio test clip from Fraunhofer.
if (me->m_SoundIo->current_backend == SoundIoBackendWasapi) {
double latency;
if (soundio_outstream_get_latency(stream, &latency) == SoundIoErrorNone) {
if (latency > 0.050) {
// If our latency is higher than desired, drop these samples to gracefully lower
// the latency without glitching too much. Dropping the whole buffer causes
// a much more noticeable glitch. This approach also ensures that we don't
// accidentally underflow if the driver/kernel side is delayed and isn't
// consuming data fast enough. Dropping a frame at a time and re-evaluating
// each time ensures that we'll stop dropping if latency returns to normal.
readPtr += framesLeft * stream->bytes_per_sample * me->m_OpusChannelCount;
bytesRead += framesLeft * stream->bytes_per_sample * me->m_OpusChannelCount;
framesLeft = 0;
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Latency exceeded drop cap: %f",
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,36 @@
#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;
bool m_Errored;
};
+417 -28
View File
@@ -1,8 +1,11 @@
#include <Limelight.h>
#include <SDL.h>
#include "streaming/session.hpp"
#include "streaming/session.h"
#include "settings/mappingmanager.h"
#include "path.h"
#include <QtGlobal>
#include <QDir>
#define VK_0 0x30
#define VK_A 0x41
@@ -14,6 +17,15 @@
#define VK_NUMPAD0 0x60
#endif
// 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,
@@ -22,12 +34,28 @@ const int SdlInputHandler::k_ButtonMap[] = {
UP_FLAG, DOWN_FLAG, LEFT_FLAG, RIGHT_FLAG
};
SdlInputHandler::SdlInputHandler(bool multiController)
: m_MultiController(multiController)
SdlInputHandler::SdlInputHandler(StreamingPreferences& prefs, NvComputer* computer, int streamWidth, int streamHeight)
: m_LastMouseMotionTime(0),
m_MultiController(prefs.multiController),
m_NeedsInputDelay(false),
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");
// If mouse acceleration is enabled, use relative mode warp (which
// is via normal motion events that are influenced by mouse acceleration).
// Otherwise, we'll use raw input capture which is straight from the device
// without modification by the OS.
SDL_SetHint(SDL_HINT_MOUSE_RELATIVE_MODE_WARP,
prefs.mouseAcceleration ? "1" : "0");
// We need to reinit this each time, since you only get
// an initial set of gamepad arrival events once per init.
SDL_assert(!SDL_WasInit(SDL_INIT_GAMECONTROLLER));
@@ -37,6 +65,9 @@ SdlInputHandler::SdlInputHandler(bool multiController)
SDL_GetError());
}
MappingManager mappingManager;
mappingManager.applyMappings();
if (!m_MultiController) {
// Player 1 is always present in non-MC mode
m_GamepadMask = 0x1;
@@ -46,7 +77,22 @@ SdlInputHandler::SdlInputHandler(bool multiController)
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;
}
SDL_zero(m_GamepadState);
SDL_zero(m_TouchDownEvent);
SDL_zero(m_CumulativeDelta);
}
SdlInputHandler::~SdlInputHandler()
@@ -57,6 +103,10 @@ SdlInputHandler::~SdlInputHandler()
}
}
SDL_RemoveTimer(m_LeftButtonReleaseTimer);
SDL_RemoveTimer(m_RightButtonReleaseTimer);
SDL_RemoveTimer(m_DragTimer);
SDL_QuitSubSystem(SDL_INIT_GAMECONTROLLER);
SDL_assert(!SDL_WasInit(SDL_INIT_GAMECONTROLLER));
}
@@ -71,28 +121,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);
@@ -102,7 +137,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)
@@ -110,6 +151,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;
}
}
@@ -324,6 +369,9 @@ void SdlInputHandler::handleKeyEvent(SDL_KeyboardEvent* event)
case SDL_SCANCODE_APOSTROPHE:
keyCode = 0xDE;
break;
case SDL_SCANCODE_NONUSBACKSLASH:
keyCode = 0xE2;
break;
default:
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Unhandled button event: %d",
@@ -332,6 +380,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,
@@ -342,9 +398,13 @@ void SdlInputHandler::handleMouseButtonEvent(SDL_MouseButtonEvent* event)
{
int button;
// Capture the mouse again if clicked when unbound
// Capture the mouse again if clicked when unbound.
// We start capture on left button released instead of
// pressed to avoid sending an errant mouse button released
// event to the host when clicking into our window (since
// the pressed event was consumed by this code).
if (event->button == SDL_BUTTON_LEFT &&
event->state == SDL_PRESSED &&
event->state == SDL_RELEASED &&
!SDL_GetRelativeMouseMode()) {
SDL_SetRelativeMouseMode(SDL_TRUE);
return;
@@ -353,6 +413,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)
{
@@ -366,9 +430,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",
@@ -388,10 +454,44 @@ void SdlInputHandler::handleMouseMotionEvent(SDL_MouseMotionEvent* event)
// Not capturing
return;
}
else if (event->which == SDL_TOUCH_MOUSEID) {
// Ignore synthetic mouse events
return;
}
if (event->xrel != 0 || event->yrel != 0) {
LiSendMouseMoveEvent((short)event->xrel,
(short)event->yrel);
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;
}
if (xdelta != 0 || ydelta != 0) {
LiSendMouseMoveEvent(xdelta, ydelta);
}
}
@@ -401,6 +501,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);
@@ -438,6 +542,36 @@ 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;
}
void SdlInputHandler::handleControllerAxisEvent(SDL_ControllerAxisEvent* event)
{
GamepadState* state = findStateForGamepad(event->which);
@@ -494,6 +628,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);
}
@@ -583,9 +734,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;
@@ -595,7 +909,7 @@ int SdlInputHandler::getAttachedGamepadMask()
}
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++);
}
@@ -603,3 +917,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 && 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;
}
+88
View File
@@ -0,0 +1,88 @@
#pragma once
#include "settings/streamingpreferences.h"
#include "backend/computermanager.h"
#include <SDL.h>
struct GamepadState {
SDL_GameController* controller;
SDL_JoystickID jsId;
short index;
short buttons;
short lsX, lsY;
short rsX, rsY;
unsigned char lt, rt;
};
#define MAX_GAMEPADS 4
#define MAX_FINGERS 2
class SdlInputHandler
{
public:
explicit SdlInputHandler(StreamingPreferences& prefs, NvComputer* computer,
int streamWidth, int streamHeight);
~SdlInputHandler();
void handleKeyEvent(SDL_KeyboardEvent* event);
void handleMouseButtonEvent(SDL_MouseButtonEvent* event);
void handleMouseMotionEvent(SDL_MouseMotionEvent* event);
void handleMouseWheelEvent(SDL_MouseWheelEvent* event);
void handleControllerAxisEvent(SDL_ControllerAxisEvent* event);
void handleControllerButtonEvent(SDL_ControllerButtonEvent* event);
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);
static
Uint32 releaseLeftButtonTimerCallback(Uint32 interval, void* param);
static
Uint32 releaseRightButtonTimerCallback(Uint32 interval, void* param);
static
Uint32 dragTimerCallback(Uint32 interval, void* param);
Uint32 m_LastMouseMotionTime;
bool m_MultiController;
bool m_NeedsInputDelay;
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[];
};
-52
View File
@@ -1,52 +0,0 @@
#pragma once
#include <SDL.h>
struct GamepadState {
SDL_GameController* controller;
SDL_JoystickID jsId;
short index;
short buttons;
short lsX, lsY;
short rsX, rsY;
unsigned char lt, rt;
};
#define MAX_GAMEPADS 4
class SdlInputHandler
{
public:
explicit SdlInputHandler(bool multiController);
~SdlInputHandler();
void handleKeyEvent(SDL_KeyboardEvent* event);
void handleMouseButtonEvent(SDL_MouseButtonEvent* event);
void handleMouseMotionEvent(SDL_MouseMotionEvent* event);
void handleMouseWheelEvent(SDL_MouseWheelEvent* event);
void handleControllerAxisEvent(SDL_ControllerAxisEvent* event);
void handleControllerButtonEvent(SDL_ControllerButtonEvent* event);
void handleControllerDeviceEvent(SDL_ControllerDeviceEvent* event);
int getAttachedGamepadMask();
private:
GamepadState*
findStateForGamepad(SDL_JoystickID id);
void sendGamepadState(GamepadState* state);
bool m_MultiController;
int m_GamepadMask;
GamepadState m_GamepadState[MAX_GAMEPADS];
static const int k_ButtonMap[];
};
+503 -207
View File
@@ -1,10 +1,17 @@
#include "session.hpp"
#include "session.h"
#include "settings/streamingpreferences.h"
#include "streaming/streamutils.h"
#include <Limelight.h>
#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
@@ -13,15 +20,6 @@
#include "video/sl.h"
#endif
#if defined(Q_OS_WIN32) || defined(Q_OS_DARWIN)
// Using full-screen desktop allows us to avoid needing to enable V-sync on Windows
// and it also avoids some strange flickering issues on my Win7 test machine
// with Intel HD 5500 graphics. It also allows Spaces to work on macOS.
#define SDL_OS_FULLSCREEN_FLAG SDL_WINDOW_FULLSCREEN_DESKTOP
#else
#define SDL_OS_FULLSCREEN_FLAG SDL_WINDOW_FULLSCREEN
#endif
#ifdef Q_OS_WIN32
// Scaling the icon down on Win32 looks dreadful, so render at lower res
#define ICON_SIZE 32
@@ -50,11 +48,11 @@ CONNECTION_LISTENER_CALLBACKS Session::k_ConnCallbacks = {
};
AUDIO_RENDERER_CALLBACKS Session::k_AudioCallbacks = {
Session::sdlAudioInit,
Session::sdlAudioStart,
Session::sdlAudioStop,
Session::sdlAudioCleanup,
Session::sdlAudioDecodeAndPlaySample,
Session::arInit,
nullptr,
nullptr,
Session::arCleanup,
Session::arDecodeAndPlaySample,
CAPABILITY_DIRECT_SUBMIT
};
@@ -81,6 +79,8 @@ void Session::clStageFailed(int stage, long errorCode)
void Session::clConnectionTerminated(long errorCode)
{
emit s_ActiveSession->displayLaunchError("Connection terminated");
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Connection terminated: %ld",
errorCode);
@@ -104,13 +104,15 @@ void Session::clLogMessage(const char* format, ...)
va_end(ap);
}
#define CALL_INITIALIZE(dec) (dec)->initialize(vds, window, videoFormat, width, height, frameRate, enableVsync)
bool Session::chooseDecoder(StreamingPreferences::VideoDecoderSelection vds,
SDL_Window* window, int videoFormat, int width, int height,
int frameRate, IVideoDecoder*& chosenDecoder)
int frameRate, bool enableVsync, IVideoDecoder*& chosenDecoder)
{
#ifdef HAVE_SLVIDEO
chosenDecoder = new SLVideoDecoder();
if (chosenDecoder->initialize(vds, window, videoFormat, width, height, frameRate)) {
if (CALL_INITIALIZE(chosenDecoder)) {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"SLVideo video decoder chosen");
return true;
@@ -125,7 +127,7 @@ bool Session::chooseDecoder(StreamingPreferences::VideoDecoderSelection vds,
#ifdef HAVE_FFMPEG
chosenDecoder = new FFmpegVideoDecoder();
if (chosenDecoder->initialize(vds, window, videoFormat, width, height, frameRate)) {
if (CALL_INITIALIZE(chosenDecoder)) {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"FFmpeg-based video decoder chosen");
return true;
@@ -168,25 +170,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;
}
}
@@ -212,7 +222,7 @@ bool Session::isHardwareDecodeAvailable(StreamingPreferences::VideoDecoderSelect
return false;
}
if (!chooseDecoder(vds, window, videoFormat, width, height, frameRate, decoder)) {
if (!chooseDecoder(vds, window, videoFormat, width, height, frameRate, true, decoder)) {
SDL_DestroyWindow(window);
SDL_QuitSubSystem(SDL_INIT_VIDEO);
return false;
@@ -228,23 +238,70 @@ bool Session::isHardwareDecodeAvailable(StreamingPreferences::VideoDecoderSelect
return ret;
}
Session::Session(NvComputer* computer, NvApp& app)
: m_Computer(computer),
int Session::getDecoderCapabilities(StreamingPreferences::VideoDecoderSelection vds,
int videoFormat, int width, int height, int frameRate)
{
IVideoDecoder* decoder;
if (SDL_InitSubSystem(SDL_INIT_VIDEO) != 0) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"SDL_InitSubSystem(SDL_INIT_VIDEO) failed: %s",
SDL_GetError());
return false;
}
SDL_Window* window = SDL_CreateWindow("", 0, 0, width, height, SDL_WINDOW_HIDDEN);
if (!window) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Failed to create window for hardware decode test: %s",
SDL_GetError());
SDL_QuitSubSystem(SDL_INIT_VIDEO);
return false;
}
if (!chooseDecoder(vds, window, videoFormat, width, height, frameRate, true, decoder)) {
SDL_DestroyWindow(window);
SDL_QuitSubSystem(SDL_INIT_VIDEO);
return false;
}
SDL_DestroyWindow(window);
int caps = decoder->getDecoderCapabilities();
delete decoder;
SDL_QuitSubSystem(SDL_INIT_VIDEO);
return caps;
}
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),
m_DecoderLock(0),
m_NeedsIdr(false)
m_NeedsIdr(false),
m_AudioDisabled(false),
m_DisplayOriginX(0),
m_DisplayOriginY(0),
m_PendingWindowedTransition(false),
m_OpusDecoder(nullptr),
m_AudioRenderer(nullptr),
m_AudioSampleCount(0)
{
qDebug() << "Server GPU:" << m_Computer->gpuModel;
}
void Session::initialize()
{
qInfo() << "Server GPU:" << m_Computer->gpuModel;
qInfo() << "Server GFE version:" << m_Computer->gfeVersion;
LiInitializeVideoCallbacks(&m_VideoCallbacks);
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);
@@ -253,10 +310,10 @@ Session::Session(NvComputer* computer, NvApp& app)
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;
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
@@ -269,16 +326,12 @@ Session::Session(NvComputer* computer, NvApp& app)
// 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 = sdlDetermineAudioConfiguration();
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;
}
@@ -287,16 +340,32 @@ Session::Session(NvComputer* computer, NvApp& app)
"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);
#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:
@@ -313,19 +382,30 @@ Session::Session(NvComputer* computer, NvApp& app)
break;
}
if (computer->activeAddress == computer->remoteAddress) {
if (m_Computer->activeAddress == m_Computer->remoteAddress) {
m_StreamConfig.streamingRemotely = 1;
}
else {
m_StreamConfig.streamingRemotely = 0;
}
if (computer->activeAddress == computer->localAddress) {
if (m_Computer->activeAddress == m_Computer->localAddress) {
m_StreamConfig.packetSize = 1392;
}
else {
m_StreamConfig.packetSize = 1024;
}
switch (m_Preferences->windowMode)
{
case StreamingPreferences::WM_FULLSCREEN_DESKTOP:
m_FullScreenFlag = SDL_WINDOW_FULLSCREEN_DESKTOP;
break;
case StreamingPreferences::WM_FULLSCREEN:
default:
m_FullScreenFlag = SDL_WINDOW_FULLSCREEN;
break;
}
}
void Session::emitLaunchWarning(QString text)
@@ -348,20 +428,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_StreamConfig.supportsHevc) {
bool hevcForced = m_Preferences.videoCodecConfig == StreamingPreferences::VCC_FORCE_HEVC ||
m_Preferences.videoCodecConfig == StreamingPreferences::VCC_FORCE_HEVC_HDR;
if (m_Preferences->unsupportedFps && m_StreamConfig.fps > 60) {
emitLaunchWarning("Using unsupported FPS options may cause stuttering or lag.");
if (!isHardwareDecodeAvailable(m_Preferences.videoDecoderSelection,
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;
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.");
}
@@ -397,7 +485,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,
@@ -414,7 +502,7 @@ bool Session::validateLaunch()
if (m_StreamConfig.width >= 3840) {
// Only allow 4K on GFE 3.x+
if (m_Computer->gfeVersion.isNull() || m_Computer->gfeVersion.startsWith("2.")) {
if (m_Computer->gfeVersion.isEmpty() || m_Computer->gfeVersion.startsWith("2.")) {
emitLaunchWarning("GeForce Experience 3.0 or higher is required for 4K streaming.");
m_StreamConfig.width = 1920;
@@ -431,13 +519,36 @@ bool Session::validateLaunch()
}
}
if (m_Preferences.videoDecoderSelection == StreamingPreferences::VDS_FORCE_HARDWARE &&
!isHardwareDecodeAvailable(m_Preferences.videoDecoderSelection,
// 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.");
}
}
// 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.");
}
// 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 {
@@ -448,6 +559,13 @@ bool Session::validateLaunch()
return false;
}
// Add the capability flags from the chosen decoder/renderer
m_VideoCallbacks.capabilities |= getDecoderCapabilities(m_Preferences->videoDecoderSelection,
m_StreamConfig.supportsHevc ? VIDEO_FORMAT_H265 : VIDEO_FORMAT_H264,
m_StreamConfig.width,
m_StreamConfig.height,
m_StreamConfig.fps);
return true;
}
@@ -465,140 +583,180 @@ class DeferredSessionCleanupTask : public QRunnable
}
};
void Session::getWindowDimensions(bool fullScreen,
int& x, int& y,
void Session::getWindowDimensions(int& x, int& y,
int& width, int& height)
{
int displayIndex = 0;
// If there's a display matching this exact resolution, pick that
// one (for native full-screen streaming). Otherwise, assume
// display 0 for now. TODO: Default to the screen that the Qt window is on
if (fullScreen) {
for (int i = 0; i < SDL_GetNumVideoDisplays(); i++) {
SDL_DisplayMode mode;
if (SDL_GetCurrentDisplayMode(i, &mode) == 0 &&
m_ActiveVideoWidth == mode.w &&
m_ActiveVideoHeight == mode.h) {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Found exact resolution match on display: %d",
i);
displayIndex = i;
break;
}
}
}
if (m_Window != nullptr) {
displayIndex = SDL_GetWindowDisplayIndex(m_Window);
if (displayIndex < 0) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"SDL_GetWindowDisplayIndex() failed: %s",
SDL_GetError());
// Assume display 0
displayIndex = 0;
}
}
if (fullScreen) {
SDL_DisplayMode currentMode;
if (SDL_GetCurrentDisplayMode(displayIndex, &currentMode) == 0) {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Using current display mode: %dx%dx%d",
currentMode.w, currentMode.h, currentMode.refresh_rate);
width = currentMode.w;
height = currentMode.h;
}
else {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Unable to get current display mode: %s",
SDL_GetError());
width = m_StreamConfig.width;
height = m_StreamConfig.height;
}
// Full-screen modes always start at the origin
x = y = SDL_WINDOWPOS_UNDEFINED_DISPLAY(displayIndex);
SDL_assert(displayIndex >= 0);
}
// Create our window on the same display that Qt's UI
// was being displayed on.
else {
SDL_Rect usableBounds;
if (SDL_GetDisplayUsableBounds(displayIndex, &usableBounds) == 0) {
width = usableBounds.w;
height = usableBounds.h;
x = usableBounds.x;
y = usableBounds.y;
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Qt UI screen is at (%d,%d)",
m_DisplayOriginX, m_DisplayOriginY);
for (int i = 0; i < SDL_GetNumVideoDisplays(); i++) {
SDL_Rect displayBounds;
if (m_Window != nullptr) {
int top, left, bottom, right;
if (SDL_GetWindowBordersSize(m_Window, &top, &left, &bottom, &right) < 0) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"Unable to get window border size");
return;
if (SDL_GetDisplayBounds(i, &displayBounds) == 0) {
if (displayBounds.x == m_DisplayOriginX &&
displayBounds.y == m_DisplayOriginY) {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"SDL found matching display %d",
i);
displayIndex = i;
break;
}
}
else {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"SDL_GetDisplayBounds(%d) failed: %s",
i, SDL_GetError());
}
}
}
x += left;
y += top;
SDL_Rect usableBounds;
if (SDL_GetDisplayUsableBounds(displayIndex, &usableBounds) == 0) {
width = usableBounds.w;
height = usableBounds.h;
if (m_Window != nullptr) {
int top, left, bottom, right;
if (SDL_GetWindowBordersSize(m_Window, &top, &left, &bottom, &right) == 0) {
width -= left + right;
height -= top + bottom;
}
else {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"Unable to get window border size: %s",
SDL_GetError());
}
// If the stream window can fit within the usable drawing area with 1:1
// scaling, do that rather than filling the screen.
if (m_StreamConfig.width < width && m_StreamConfig.height < height) {
width = m_StreamConfig.width;
height = m_StreamConfig.height;
// If the stream window can fit within the usable drawing area with 1:1
// scaling, do that rather than filling the screen.
if (m_StreamConfig.width < width && m_StreamConfig.height < height) {
width = m_StreamConfig.width;
height = m_StreamConfig.height;
}
}
}
else {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"SDL_GetDisplayUsableBounds() failed: %s",
SDL_GetError());
width = m_StreamConfig.width;
height = m_StreamConfig.height;
}
x = y = SDL_WINDOWPOS_CENTERED_DISPLAY(displayIndex);
}
void Session::updateOptimalWindowDisplayMode()
{
SDL_DisplayMode desktopMode, bestMode, mode;
int displayIndex = SDL_GetWindowDisplayIndex(m_Window);
// Try the current display mode first. On macOS, this will be the normal
// scaled desktop resolution setting.
if (SDL_GetDesktopDisplayMode(displayIndex, &desktopMode) == 0) {
// If this doesn't fit the selected resolution, use the native
// resolution of the panel (unscaled).
if (desktopMode.w < m_ActiveVideoWidth || desktopMode.h < m_ActiveVideoHeight) {
if (!StreamUtils::getRealDesktopMode(displayIndex, &desktopMode)) {
return;
}
}
}
else {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"SDL_GetDesktopDisplayMode() failed: %s",
SDL_GetError());
return;
}
// Start with the native desktop resolution and try to find
// the highest refresh rate that our stream FPS evenly divides.
bestMode = desktopMode;
bestMode.refresh_rate = 0;
for (int i = 0; i < SDL_GetNumDisplayModes(displayIndex); i++) {
if (SDL_GetDisplayMode(displayIndex, i, &mode) == 0) {
if (mode.w == desktopMode.w && mode.h == desktopMode.h &&
mode.refresh_rate % m_StreamConfig.fps == 0) {
if (mode.refresh_rate > bestMode.refresh_rate) {
bestMode = mode;
}
}
}
else {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"SDL_GetDisplayUsableBounds() failed: %s",
SDL_GetError());
width = m_StreamConfig.width;
height = m_StreamConfig.height;
x = y = SDL_WINDOWPOS_UNDEFINED_DISPLAY(displayIndex);
}
}
if (bestMode.refresh_rate == 0) {
// We may find no match if the user has moved a 120 FPS
// stream onto a 60 Hz monitor (since no refresh rate can
// divide our FPS setting). We'll stick to the default in
// this case.
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"No matching refresh rate found; using desktop mode");
bestMode = desktopMode;
}
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);
}
void Session::toggleFullscreen()
{
bool fullScreen = !(SDL_GetWindowFlags(m_Window) & SDL_OS_FULLSCREEN_FLAG);
int x, y, width, height;
// If we're leaving full screen, toggle out before setting our size and position
// that way we have accurate display size metrics (not the size our stream changed it to).
if (!fullScreen) {
SDL_SetWindowFullscreen(m_Window, 0);
SDL_SetWindowResizable(m_Window, SDL_TRUE);
}
getWindowDimensions(fullScreen, x, y, width, height);
SDL_SetWindowPosition(m_Window, x, y);
SDL_SetWindowSize(m_Window, width, height);
bool fullScreen = !(SDL_GetWindowFlags(m_Window) & m_FullScreenFlag);
if (fullScreen) {
SDL_SetWindowResizable(m_Window, SDL_FALSE);
SDL_SetWindowFullscreen(m_Window, SDL_OS_FULLSCREEN_FLAG);
SDL_SetWindowFullscreen(m_Window, m_FullScreenFlag);
}
else {
SDL_SetWindowFullscreen(m_Window, 0);
SDL_SetWindowResizable(m_Window, SDL_TRUE);
// Reposition the window when the resize is complete
m_PendingWindowedTransition = true;
}
}
void Session::exec()
void Session::exec(int displayOriginX, int displayOriginY)
{
m_DisplayOriginX = displayOriginX;
m_DisplayOriginY = displayOriginY;
// Complete initialization in this deferred context to avoid
// calling expensive functions in the constructor (during the
// process of loading the StreamSegue).
initialize();
// Check for validation errors/warnings and emit
// signals for them, if appropriate
if (!validateLaunch()) {
return;
}
// Manually pump the UI thread for the view
QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents);
// Wait 1.5 seconds before connecting to let the user
// have time to read any messages present on the segue
uint32_t start = SDL_GetTicks();
while (!SDL_TICKS_PASSED(SDL_GetTicks(), start + 1500)) {
// Pump the UI loop while we wait
SDL_Delay(5);
QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents);
}
// Wait for any old session to finish cleanup
s_ActiveSessionSemaphore.acquire();
@@ -608,7 +766,9 @@ void Session::exec()
// Initialize the gamepad code with our preferences
StreamingPreferences prefs;
SdlInputHandler inputHandler(prefs.multiController);
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.
@@ -642,6 +802,10 @@ void Session::exec()
emit displayLaunchError(e.toQString());
s_ActiveSessionSemaphore.release();
return;
} catch (const QtNetworkReplyException& e) {
emit displayLaunchError(e.toQString());
s_ActiveSessionSemaphore.release();
return;
}
SDL_assert(!SDL_WasInit(SDL_INIT_VIDEO));
@@ -663,21 +827,22 @@ void Session::exec()
// Older GFE versions didn't have this field
QByteArray siGfeVersion;
if (!m_Computer->gfeVersion.isNull()) {
if (!m_Computer->gfeVersion.isEmpty()) {
siGfeVersion = m_Computer->gfeVersion.toLatin1();
}
if (!siGfeVersion.isNull()) {
if (!siGfeVersion.isEmpty()) {
hostInfo.serverInfoGfeVersion = siGfeVersion.data();
}
int err = LiStartConnection(&hostInfo, &m_StreamConfig, &k_ConnCallbacks,
&m_VideoCallbacks, &k_AudioCallbacks,
&m_VideoCallbacks,
m_AudioDisabled ? nullptr : &k_AudioCallbacks,
NULL, 0, NULL, 0);
if (err != 0) {
// We already displayed an error dialog in the stage failure
// listener.
s_ActiveSessionSemaphore.release();
SDL_QuitSubSystem(SDL_INIT_VIDEO);
s_ActiveSessionSemaphore.release();
return;
}
@@ -686,39 +851,23 @@ void Session::exec()
QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents);
int x, y, width, height;
getWindowDimensions(m_Preferences.fullScreen,
x, y, width, height);
getWindowDimensions(x, y, width, height);
m_Window = SDL_CreateWindow("Moonlight",
x,
y,
width,
height,
m_Preferences.fullScreen ?
SDL_OS_FULLSCREEN_FLAG : 0);
SDL_WINDOW_ALLOW_HIGHDPI);
if (!m_Window) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"SDL_CreateWindow() failed: %s",
SDL_GetError());
s_ActiveSessionSemaphore.release();
SDL_QuitSubSystem(SDL_INIT_VIDEO);
s_ActiveSessionSemaphore.release();
return;
}
// For non-full screen windows, call getWindowDimensions()
// again after creating a window to allow it to account
// for window chrome size.
if (!m_Preferences.fullScreen) {
getWindowDimensions(false, x, y, width, height);
SDL_SetWindowPosition(m_Window, x, y);
SDL_SetWindowSize(m_Window, width, height);
// Passing SDL_WINDOW_RESIZABLE to set this during window
// creation causes our window to be full screen for some reason
SDL_SetWindowResizable(m_Window, SDL_TRUE);
}
QSvgRenderer svgIconRenderer(QString(":/res/moonlight.svg"));
QImage svgImage(ICON_SIZE, ICON_SIZE, QImage::Format_RGBA8888);
svgImage.fill(0);
@@ -732,17 +881,42 @@ void Session::exec()
4 * svgImage.width(),
SDL_PIXELFORMAT_RGBA32);
#ifndef Q_OS_DARWIN
// Other platforms seem to preserve our Qt icon when creating a new window
// 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) {
getWindowDimensions(x, y, width, height);
// We must set the size before the position because centering
// won't work unless it knows the final size of the window.
SDL_SetWindowSize(m_Window, width, height);
SDL_SetWindowPosition(m_Window, x, y);
// Passing SDL_WINDOW_RESIZABLE to set this during window
// creation causes our window to be full screen for some reason
SDL_SetWindowResizable(m_Window, SDL_TRUE);
}
else {
// Update the window display mode based on our current monitor
updateOptimalWindowDisplayMode();
// Enter full screen
SDL_SetWindowFullscreen(m_Window, m_FullScreenFlag);
}
#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.fullScreen) {
if (m_Preferences->windowMode != StreamingPreferences::WM_WINDOWED) {
SDL_SetRelativeMouseMode(SDL_TRUE);
}
#endif
@@ -756,6 +930,17 @@ void Session::exec()
// 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");
// Raise the priority of the main thread, since it handles
// time-sensitive video rendering
if (SDL_SetThreadPriority(SDL_THREAD_PRIORITY_HIGH) < 0) {
@@ -764,6 +949,8 @@ void Session::exec()
SDL_GetError());
}
int currentDisplayIndex = SDL_GetWindowDisplayIndex(m_Window);
// 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;
@@ -802,32 +989,122 @@ void Session::exec()
}
case SDL_WINDOWEVENT:
// Capture mouse cursor when user actives the window by clicking on
// window's client area (borders and title bar excluded).
// Without this you would have to click the window twice (once to
// activate it, second time to enable capture). With this you need to
// click it only once.
// On Linux, the button press event is delivered after the focus gain
// so this is not neccessary (and leads to a click sent to the host
// when focusing the window by clicking).
// By excluding window's borders and title bar out, lets user still
// interact with them without mouse capture kicking in.
#if defined(Q_OS_WIN32) || defined(Q_OS_DARWIN)
if (event.window.event == SDL_WINDOWEVENT_FOCUS_GAINED) {
int mouseX, mouseY;
Uint32 mouseState = SDL_GetGlobalMouseState(&mouseX, &mouseY);
if (mouseState & SDL_BUTTON(SDL_BUTTON_LEFT)) {
int x, y, width, height;
SDL_GetWindowPosition(m_Window, &x, &y);
SDL_GetWindowSize(m_Window, &width, &height);
if (mouseX > x && mouseX < x+width && mouseY > y && mouseY < y+height) {
SDL_SetRelativeMouseMode(SDL_TRUE);
}
}
}
#endif
if (event.window.event == SDL_WINDOWEVENT_FOCUS_LOST) {
// Release mouse cursor when another window is activated (e.g. by using ALT+TAB).
// This lets user to interact with our window's title bar and with the buttons in it.
// Doing this while the window is full-screen breaks the transition out of FS
// (desktop and exclusive), so we must check for that before releasing mouse capture.
if (!(SDL_GetWindowFlags(m_Window) & SDL_WINDOW_FULLSCREEN)) {
SDL_SetRelativeMouseMode(SDL_FALSE);
}
// 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.
// We use SDL_WINDOWEVENT_SIZE_CHANGED rather than SDL_WINDOWEVENT_RESIZED because the latter doesn't
// seem to fire when switching from windowed to full-screen on X11.
if (event.window.event != SDL_WINDOWEVENT_SIZE_CHANGED && event.window.event != SDL_WINDOWEVENT_SHOWN) {
break;
// Check that the window display hasn't changed. If it has, we want
// to recreate the decoder to allow it to adapt to the new display.
// This will allow Pacer to pull the new display refresh rate.
if (SDL_GetWindowDisplayIndex(m_Window) == currentDisplayIndex) {
break;
}
}
// Complete any repositioning that was deferred until
// the resize from full-screen to windowed had completed.
// If we try to do this immediately, the resize won't take effect
// properly on Windows.
if (m_PendingWindowedTransition) {
m_PendingWindowedTransition = false;
int x, y, width, height;
getWindowDimensions(x, y, width, height);
SDL_SetWindowSize(m_Window, width, height);
SDL_SetWindowPosition(m_Window, x, y);
}
// Fall through
case SDL_RENDER_DEVICE_RESET:
case SDL_RENDER_TARGETS_RESET:
SDL_AtomicLock(&m_DecoderLock);
// Destroy the old decoder
delete m_VideoDecoder;
// Chose a new decoder (hopefully the same one, but possibly
// not if a GPU was removed or something).
if (!chooseDecoder(m_Preferences.videoDecoderSelection,
m_Window, m_ActiveVideoFormat, m_ActiveVideoWidth,
m_ActiveVideoHeight, m_ActiveVideoFrameRate,
s_ActiveSession->m_VideoDecoder)) {
SDL_AtomicUnlock(&m_DecoderLock);
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Failed to recreate decoder after reset");
emit displayLaunchError("Unable to initialize video decoder. Please check your streaming settings and try again.");
goto DispatchDeferredCleanup;
// Flush any other pending window events that could
// send us back here immediately
SDL_PumpEvents();
SDL_FlushEvent(SDL_WINDOWEVENT);
// Update the window display mode based on our current monitor
currentDisplayIndex = SDL_GetWindowDisplayIndex(m_Window);
updateOptimalWindowDisplayMode();
// Now that the old decoder is dead, flush any events it may
// have queued to reset itself (if this reset was the result
// of state loss).
SDL_PumpEvents();
SDL_FlushEvent(SDL_RENDER_DEVICE_RESET);
SDL_FlushEvent(SDL_RENDER_TARGETS_RESET);
{
// If the stream exceeds the display refresh rate (plus some slack),
// 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;
if (displayHz + 5 < m_StreamConfig.fps) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"Disabling V-sync because refresh rate limit exceeded");
enableVsync = false;
}
// Choose a new decoder (hopefully the same one, but possibly
// not if a GPU was removed or something).
if (!chooseDecoder(m_Preferences->videoDecoderSelection,
m_Window, m_ActiveVideoFormat, m_ActiveVideoWidth,
m_ActiveVideoHeight, m_ActiveVideoFrameRate,
enableVsync,
s_ActiveSession->m_VideoDecoder)) {
SDL_AtomicUnlock(&m_DecoderLock);
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Failed to recreate decoder after reset");
emit displayLaunchError("Unable to initialize video decoder. Please check your streaming settings and try again.");
goto DispatchDeferredCleanup;
}
}
// Request an IDR frame to complete the reset
@@ -861,18 +1138,37 @@ void Session::exec()
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);
@@ -6,8 +6,9 @@
#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"
class Session : public QObject
{
@@ -17,14 +18,18 @@ class Session : public QObject
friend class DeferredSessionCleanupTask;
public:
explicit Session(NvComputer* computer, NvApp& app);
explicit Session(NvComputer* computer, NvApp& app, StreamingPreferences *preferences = nullptr);
Q_INVOKABLE void exec();
Q_INVOKABLE void exec(int displayOriginX, int displayOriginY);
static
bool isHardwareDecodeAvailable(StreamingPreferences::VideoDecoderSelection vds,
int videoFormat, int width, int height, int frameRate);
static
int getDecoderCapabilities(StreamingPreferences::VideoDecoderSelection vds,
int videoFormat, int width, int height, int frameRate);
signals:
void stageStarting(QString stage);
@@ -37,24 +42,31 @@ signals:
void displayLaunchWarning(QString text);
private:
void initialize();
bool validateLaunch();
void emitLaunchWarning(QString text);
int getDecoderCapabilities();
int sdlDetermineAudioConfiguration();
IAudioRenderer* createAudioRenderer();
void getWindowDimensions(bool fullScreen,
int& x, int& y,
int detectAudioConfiguration();
bool testAudio(int audioConfiguration);
void getWindowDimensions(int& x, int& y,
int& width, int& height);
void toggleFullscreen();
void updateOptimalWindowDisplayMode();
static
bool chooseDecoder(StreamingPreferences::VideoDecoderSelection vds,
SDL_Window* window, int videoFormat, int width, int height,
int frameRate, IVideoDecoder*& chosenDecoder);
int frameRate, bool enableVsync, IVideoDecoder*& chosenDecoder);
static
void clStageStarting(int stage);
@@ -68,6 +80,17 @@ private:
static
void clLogMessage(const char* format, ...);
static
int arInit(int audioConfiguration,
const POPUS_MULTISTREAM_CONFIGURATION opusConfig,
void* arContext, int arFlags);
static
void arCleanup();
static
void arDecodeAndPlaySample(char* sampleData, int sampleLength);
static
int drSetup(int videoFormat, int width, int height, int frameRate, void*, int);
@@ -77,24 +100,7 @@ private:
static
int drSubmitDecodeUnit(PDECODE_UNIT du);
static
int sdlAudioInit(int audioConfiguration,
POPUS_MULTISTREAM_CONFIGURATION opusConfig,
void* arContext, int arFlags);
static
void sdlAudioStart();
static
void sdlAudioStop();
static
void sdlAudioCleanup();
static
void sdlAudioDecodeAndPlaySample(char* sampleData, int sampleLength);
StreamingPreferences m_Preferences;
StreamingPreferences* m_Preferences;
STREAM_CONFIGURATION m_StreamConfig;
DECODER_RENDERER_CALLBACKS m_VideoCallbacks;
NvComputer* m_Computer;
@@ -103,20 +109,22 @@ private:
IVideoDecoder* m_VideoDecoder;
SDL_SpinLock m_DecoderLock;
bool m_NeedsIdr;
bool m_AudioDisabled;
Uint32 m_FullScreenFlag;
int m_DisplayOriginX;
int m_DisplayOriginY;
bool m_PendingWindowedTransition;
int m_ActiveVideoFormat;
int m_ActiveVideoWidth;
int m_ActiveVideoHeight;
int m_ActiveVideoFrameRate;
static SDL_AudioDeviceID s_AudioDevice;
static OpusMSDecoder* s_OpusDecoder;
static short s_OpusDecodeBuffer[];
static int s_ChannelCount;
static int s_PendingDrops;
static int s_PendingHardDrops;
static unsigned int s_SampleIndex;
static Uint32 s_BaselinePendingData;
OpusMSDecoder* m_OpusDecoder;
short m_OpusDecodeBuffer[MAX_CHANNELS * SAMPLES_PER_FRAME];
IAudioRenderer* m_AudioRenderer;
OPUS_MULTISTREAM_CONFIGURATION m_AudioConfig;
int m_AudioSampleCount;
static AUDIO_RENDERER_CALLBACKS k_AudioCallbacks;
static CONNECTION_LISTENER_CALLBACKS k_ConnCallbacks;
+107
View File
@@ -1,5 +1,11 @@
#include "streamutils.h"
#include <Qt>
#ifdef Q_OS_DARWIN
#include <ApplicationServices/ApplicationServices.h>
#endif
void StreamUtils::scaleSourceToDestinationSurface(SDL_Rect* src, SDL_Rect* dst)
{
int dstH = dst->w * src->h / src->w;
@@ -18,3 +24,104 @@ void StreamUtils::scaleSourceToDestinationSurface(SDL_Rect* src, SDL_Rect* dst)
SDL_assert(dst->h * src->w / src->h <= dst->w);
}
}
int StreamUtils::getDisplayRefreshRate(SDL_Window* window)
{
int displayIndex = SDL_GetWindowDisplayIndex(window);
if (displayIndex < 0) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Failed to get current display: %s",
SDL_GetError());
// Assume display 0 if it fails
displayIndex = 0;
}
SDL_DisplayMode mode;
if ((SDL_GetWindowFlags(window) & SDL_WINDOW_FULLSCREEN_DESKTOP) == SDL_WINDOW_FULLSCREEN) {
// Use the window display mode for full-screen exclusive mode
if (SDL_GetWindowDisplayMode(window, &mode) != 0) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"SDL_GetWindowDisplayMode() failed: %s",
SDL_GetError());
// Assume 60 Hz
return 60;
}
}
else {
// Use the current display mode for windowed and borderless
if (SDL_GetCurrentDisplayMode(displayIndex, &mode) != 0) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"SDL_GetCurrentDisplayMode() failed: %s",
SDL_GetError());
// Assume 60 Hz
return 60;
}
}
// May be zero if undefined
if (mode.refresh_rate == 0) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"Refresh rate unknown; assuming 60 Hz");
mode.refresh_rate = 60;
}
return mode.refresh_rate;
}
bool StreamUtils::getRealDesktopMode(int displayIndex, SDL_DisplayMode* mode)
{
#ifdef Q_OS_DARWIN
#define MAX_DISPLAYS 16
CGDirectDisplayID displayIds[MAX_DISPLAYS];
uint32_t displayCount = 0;
CGGetActiveDisplayList(MAX_DISPLAYS, displayIds, &displayCount);
if (displayIndex >= displayCount) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Too many displays: %d vs %d",
displayIndex, displayCount);
return false;
}
SDL_zerop(mode);
// Retina displays have non-native resolutions both below and above (!) their
// native resolution, so it's impossible for us to figure out what's actually
// native on macOS using the SDL API alone. We'll talk to CoreGraphics to
// find the correct resolution and match it in our SDL list.
CFArrayRef modeList = CGDisplayCopyAllDisplayModes(displayIds[displayIndex], nullptr);
CFIndex count = CFArrayGetCount(modeList);
for (CFIndex i = 0; i < count; i++) {
auto cgMode = (CGDisplayModeRef)(CFArrayGetValueAtIndex(modeList, i));
if ((CGDisplayModeGetIOFlags(cgMode) & kDisplayModeNativeFlag) != 0) {
mode->w = static_cast<int>(CGDisplayModeGetWidth(cgMode));
mode->h = static_cast<int>(CGDisplayModeGetHeight(cgMode));
break;
}
}
CFRelease(modeList);
// Now find the SDL mode that matches the CG native mode
for (int i = 0; i < SDL_GetNumDisplayModes(displayIndex); i++) {
SDL_DisplayMode thisMode;
if (SDL_GetDisplayMode(displayIndex, i, &thisMode) == 0) {
if (thisMode.w == mode->w && thisMode.h == mode->h &&
thisMode.refresh_rate >= mode->refresh_rate) {
*mode = thisMode;
break;
}
}
}
#else
if (SDL_GetDesktopDisplayMode(displayIndex, mode) != 0) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"SDL_GetDesktopDisplayMode() failed: %s",
SDL_GetError());
return false;
}
#endif
return true;
}
+6
View File
@@ -7,4 +7,10 @@ class StreamUtils
public:
static
void scaleSourceToDestinationSurface(SDL_Rect* src, SDL_Rect* dst);
static
bool getRealDesktopMode(int displayIndex, SDL_DisplayMode* mode);
static
int getDisplayRefreshRate(SDL_Window* window);
};
+18 -1
View File
@@ -8,6 +8,21 @@
#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 totalRenderTime;
float receivedFps;
float decodedFps;
float renderedFps;
uint32_t measurementStartTimestamp;
} VIDEO_STATS, *PVIDEO_STATS;
class IVideoDecoder {
public:
virtual ~IVideoDecoder() {}
@@ -16,8 +31,10 @@ public:
int videoFormat,
int width,
int height,
int frameRate) = 0;
int frameRate,
bool enableVsync) = 0;
virtual bool isHardwareAccelerated() = 0;
virtual int getDecoderCapabilities() = 0;
virtual int submitDecodeUnit(PDECODE_UNIT du) = 0;
virtual void renderFrame(SDL_UserEvent* event) = 0;
virtual void dropFrame(SDL_UserEvent* event) = 0;
+263 -112
View File
@@ -3,6 +3,13 @@
#include "../ffmpeg.h"
#include <streaming/streamutils.h>
#include <SDL_syswm.h>
#define WIN32_LEAN_AND_MEAN
#include <Windows.h>
#include <VersionHelpers.h>
#include <dwmapi.h>
#include <Limelight.h>
DEFINE_GUID(DXVADDI_Intel_ModeH264_E, 0x604F8E68,0x4951,0x4C54,0x88,0xFE,0xAB,0xD2,0x5C,0x15,0xB3,0xD6);
@@ -10,7 +17,6 @@ DEFINE_GUID(DXVADDI_Intel_ModeH264_E, 0x604F8E68,0x4951,0x4C54,0x88,0xFE,0xAB,0x
#define SAFE_COM_RELEASE(x) if (x) { (x)->Release(); }
DXVA2Renderer::DXVA2Renderer() :
m_SdlRenderer(nullptr),
m_DecService(nullptr),
m_Decoder(nullptr),
m_SurfacesUsed(0),
@@ -23,10 +29,15 @@ DXVA2Renderer::DXVA2Renderer() :
{
RtlZeroMemory(m_DecSurfaces, sizeof(m_DecSurfaces));
RtlZeroMemory(&m_DXVAContext, sizeof(m_DXVAContext));
// Use MMCSS scheduling for lower scheduling latency while we're streaming
DwmEnableMMCSS(TRUE);
}
DXVA2Renderer::~DXVA2Renderer()
{
DwmEnableMMCSS(FALSE);
SAFE_COM_RELEASE(m_DecService);
SAFE_COM_RELEASE(m_Decoder);
SAFE_COM_RELEASE(m_Device);
@@ -41,10 +52,6 @@ DXVA2Renderer::~DXVA2Renderer()
if (m_Pool != nullptr) {
av_buffer_pool_uninit(&m_Pool);
}
if (m_SdlRenderer != nullptr) {
SDL_DestroyRenderer(m_SdlRenderer);
}
}
void DXVA2Renderer::ffPoolDummyDelete(void*, uint8_t*)
@@ -259,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;
}
@@ -357,6 +342,13 @@ bool DXVA2Renderer::isDecoderBlacklisted()
id.Description,
id.VendorId,
id.DeviceId);
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"GPU driver: %s %d.%d.%d.%d",
id.Driver,
HIWORD(id.DriverVersion.HighPart),
LOWORD(id.DriverVersion.HighPart),
HIWORD(id.DriverVersion.LowPart),
LOWORD(id.DriverVersion.LowPart));
if (id.VendorId == 0x8086) {
// Intel seems to encode the series in the high byte of
@@ -372,26 +364,50 @@ bool DXVA2Renderer::isDecoderBlacklisted()
case 0x1600: // Broadwell
case 0x2200: // Cherry Trail and Braswell
// Blacklist these for HEVC to avoid hybrid decode
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"GPU blacklisted for HEVC due to hybrid decode");
result = (m_VideoFormat & VIDEO_FORMAT_MASK_H265) != 0;
break;
default:
// Everything else is fine with whatever it says it supports
result = false;
// Intel drivers from before late-2017 had a bug that caused some strange artifacts
// when decoding HEVC. Avoid HEVC on drivers prior to build 4836 which I confirmed
// is not affected on my Intel HD 515.
// https://github.com/moonlight-stream/moonlight-qt/issues/32
// https://www.intel.com/content/www/us/en/support/articles/000005654/graphics-drivers.html
if (LOWORD(id.DriverVersion.LowPart) < 4836) {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Intel driver version blacklisted for HEVC");
result = (m_VideoFormat & VIDEO_FORMAT_MASK_H265) != 0;
}
else {
// Everything else is fine with whatever it says it supports
result = false;
}
break;
}
}
else if (id.VendorId == 0x10DE) {
// For NVIDIA, we wait to avoid those GPUs with Feature Set E
// for HEVC decoding, since that's hybrid.
// for HEVC decoding, since that's hybrid. It appears that Kepler GPUs
// also had some hybrid decode support (per DXVA2 Checker) so we'll
// blacklist those too.
// https://en.wikipedia.org/wiki/Nvidia_PureVideo
// https://bluesky23.yukishigure.com/en/dxvac/deviceInfo/decoder.html
// http://envytools.readthedocs.io/en/latest/hw/pciid.html (missing GM200)
if ((id.DeviceId >= 0x1340 && id.DeviceId <= 0x137F) || // GM108
if ((id.DeviceId >= 0x1180 && id.DeviceId <= 0x11BF) || // GK104
(id.DeviceId >= 0x11C0 && id.DeviceId <= 0x11FF) || // GK106
(id.DeviceId >= 0x0FC0 && id.DeviceId <= 0x0FFF) || // GK107
(id.DeviceId >= 0x1000 && id.DeviceId <= 0x103F) || // GK110/GK110B
(id.DeviceId >= 0x1280 && id.DeviceId <= 0x12BF) || // GK208
(id.DeviceId >= 0x1340 && id.DeviceId <= 0x137F) || // GM108
(id.DeviceId >= 0x1380 && id.DeviceId <= 0x13BF) || // GM107
(id.DeviceId >= 0x13C0 && id.DeviceId <= 0x13FF) || // GM204
(id.DeviceId >= 0x1617 && id.DeviceId <= 0x161A) || // GM204
(id.DeviceId == 0x1667) || // GM204
(id.DeviceId >= 0x17C0 && id.DeviceId <= 0x17FF)) { // GM200
// Avoid HEVC on Feature Set E GPUs
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"GPU blacklisted for HEVC due to hybrid decode");
result = (m_VideoFormat & VIDEO_FORMAT_MASK_H265) != 0;
}
else {
@@ -430,34 +446,147 @@ bool DXVA2Renderer::isDecoderBlacklisted()
return result;
}
bool DXVA2Renderer::initialize(SDL_Window* window, int videoFormat, int width, int height)
bool DXVA2Renderer::initializeDevice(SDL_Window* window, bool enableVsync)
{
SDL_SysWMinfo info;
SDL_VERSION(&info.version);
SDL_GetWindowWMInfo(window, &info);
IDirect3D9Ex* d3d9ex;
HRESULT hr = Direct3DCreate9Ex(D3D_SDK_VERSION, &d3d9ex);
if (FAILED(hr)) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Direct3DCreate9Ex() failed: %x",
hr);
return false;
}
int adapterIndex = SDL_Direct3D9GetAdapterIndex(SDL_GetWindowDisplayIndex(window));
Uint32 windowFlags = SDL_GetWindowFlags(window);
D3DCAPS9 deviceCaps;
d3d9ex->GetDeviceCaps(adapterIndex, D3DDEVTYPE_HAL, &deviceCaps);
D3DDISPLAYMODEEX currentMode;
currentMode.Size = sizeof(currentMode);
d3d9ex->GetAdapterDisplayModeEx(adapterIndex, &currentMode, nullptr);
D3DPRESENT_PARAMETERS d3dpp = {};
d3dpp.hDeviceWindow = info.info.win.window;
d3dpp.Flags = D3DPRESENTFLAG_VIDEO;
if ((windowFlags & SDL_WINDOW_FULLSCREEN_DESKTOP) == SDL_WINDOW_FULLSCREEN) {
d3dpp.Windowed = false;
d3dpp.BackBufferWidth = currentMode.Width;
d3dpp.BackBufferHeight = currentMode.Height;
d3dpp.FullScreen_RefreshRateInHz = currentMode.RefreshRate;
d3dpp.BackBufferFormat = currentMode.Format;
}
else {
d3dpp.Windowed = true;
d3dpp.BackBufferFormat = D3DFMT_UNKNOWN;
SDL_GetWindowSize(window, (int*)&d3dpp.BackBufferWidth, (int*)&d3dpp.BackBufferHeight);
}
BOOL dwmEnabled;
DwmIsCompositionEnabled(&dwmEnabled);
if (d3dpp.Windowed && dwmEnabled) {
// If composition enabled, disable v-sync and let DWM manage things
// to reduce latency by avoiding double v-syncing.
d3dpp.PresentationInterval = D3DPRESENT_INTERVAL_IMMEDIATE;
// If V-sync is enabled (not rendering faster than display),
// we can use FlipEx for more efficient swapping.
if (enableVsync) {
// D3DSWAPEFFECT_FLIPEX requires at least 2 back buffers to allow us to
// continue while DWM is waiting to render the surface to the display.
d3dpp.SwapEffect = D3DSWAPEFFECT_FLIPEX;
d3dpp.BackBufferCount = 2;
}
else {
// With V-sync off, we won't use FlipEx because that will block while
// DWM is waiting to render our surface (effectively behaving like V-Sync).
d3dpp.SwapEffect = D3DSWAPEFFECT_DISCARD;
d3dpp.BackBufferCount = 1;
}
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Windowed mode with DWM running");
}
else if (enableVsync) {
// Uncomposited desktop or full-screen exclusive mode with V-sync enabled
// We will enable V-sync in this scenario to avoid tearing.
d3dpp.PresentationInterval = D3DPRESENT_INTERVAL_ONE;
d3dpp.SwapEffect = D3DSWAPEFFECT_DISCARD;
d3dpp.BackBufferCount = 1;
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"V-Sync enabled");
}
else {
// Uncomposited desktop or full-screen exclusive mode with V-sync disabled
// We will allowing tearing for lowest latency.
d3dpp.PresentationInterval = D3DPRESENT_INTERVAL_IMMEDIATE;
d3dpp.SwapEffect = D3DSWAPEFFECT_DISCARD;
d3dpp.BackBufferCount = 1;
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"V-Sync disabled in tearing mode");
}
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Windowed: %d | Present Interval: %x",
d3dpp.Windowed, d3dpp.PresentationInterval);
// FFmpeg requires this attribute for doing asynchronous decoding
// in a separate thread with this device.
int deviceFlags = D3DCREATE_MULTITHREADED;
if (deviceCaps.DevCaps & D3DDEVCAPS_HWTRANSFORMANDLIGHT) {
deviceFlags |= D3DCREATE_HARDWARE_VERTEXPROCESSING;
}
else {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"No hardware vertex processing support!");
deviceFlags |= D3DCREATE_SOFTWARE_VERTEXPROCESSING;
}
hr = d3d9ex->CreateDeviceEx(adapterIndex,
D3DDEVTYPE_HAL,
d3dpp.hDeviceWindow,
deviceFlags,
&d3dpp,
d3dpp.Windowed ? nullptr : &currentMode,
&m_Device);
d3d9ex->Release();
if (FAILED(hr)) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"CreateDeviceEx() failed: %x",
hr);
return false;
}
hr = m_Device->SetMaximumFrameLatency(1);
if (FAILED(hr)) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"SetMaximumFrameLatency() failed: %x",
hr);
return false;
}
return true;
}
bool DXVA2Renderer::initialize(SDL_Window* window, int videoFormat, int width, int height, int, bool enableVsync)
{
m_VideoFormat = videoFormat;
m_VideoWidth = width;
m_VideoHeight = height;
// FFmpeg will be decoding on different threads than the main thread that we're
// currently running on right now. We must set this hint so SDL will pass
// D3DCREATE_MULTITHREADED to IDirect3D9::CreateDevice().
SDL_SetHint(SDL_HINT_RENDER_DIRECT3D_THREADSAFE, "1");
// We require full-screen desktop mode to avoid having to enable V-sync
// to synchronize frame delivery (which has a much higher latency penalty).
m_SdlRenderer = SDL_CreateRenderer(window, -1, SDL_RENDERER_ACCELERATED);
if (!m_SdlRenderer) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"SDL_CreateRenderer() failed: %s",
SDL_GetError());
return false;
}
m_Device = SDL_RenderGetD3D9Device(m_SdlRenderer);
// Draw a black frame until the video stream starts rendering
SDL_SetRenderDrawColor(m_SdlRenderer, 0, 0, 0, SDL_ALPHA_OPAQUE);
SDL_RenderClear(m_SdlRenderer);
SDL_RenderPresent(m_SdlRenderer);
RtlZeroMemory(&m_Desc, sizeof(m_Desc));
int alignment;
@@ -481,6 +610,10 @@ bool DXVA2Renderer::initialize(SDL_Window* window, int videoFormat, int width, i
m_Desc.SampleFormat.SampleFormat = DXVA2_SampleProgressiveFrame;
m_Desc.Format = (D3DFORMAT)MAKEFOURCC('N','V','1','2');
if (!initializeDevice(window, enableVsync)) {
return false;
}
if (!initializeDecoder()) {
return false;
}
@@ -489,10 +622,34 @@ bool DXVA2Renderer::initialize(SDL_Window* window, int videoFormat, int width, i
return false;
}
// For some reason, using Direct3D9Ex breaks this with multi-monitor setups.
// When focus is lost, the window is minimized then immediately restored without
// input focus. This glitches out the renderer and a bunch of other stuff.
// Direct3D9Ex itself seems to have this minimize on focus loss behavior on its
// own, so just disable SDL's handling of the focus loss event.
SDL_SetHint(SDL_HINT_VIDEO_MINIMIZE_ON_FOCUS_LOSS, "0");
return true;
}
void DXVA2Renderer::renderFrame(AVFrame* frame)
bool DXVA2Renderer::needsTestFrame()
{
// We validate the DXVA2 profiles are supported
// in initialize() so no test frame is required
return false;
}
int DXVA2Renderer::getDecoderCapabilities()
{
return 0;
}
IFFmpegRenderer::VSyncConstraint DXVA2Renderer::getVsyncConstraint()
{
return VSYNC_ANY;
}
void DXVA2Renderer::renderFrameAtVsync(AVFrame *frame)
{
IDirect3DSurface9* surface = reinterpret_cast<IDirect3DSurface9*>(frame->data[3]);
HRESULT hr;
@@ -628,33 +785,27 @@ void DXVA2Renderer::renderFrame(AVFrame* frame)
bltParams.Alpha = DXVA2_Fixed32OpaqueAlpha();
if (SDL_RenderClear(m_SdlRenderer) != 0) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"SDL_RenderClear() failed: %s",
SDL_GetError());
av_frame_free(&frame);
// We're going to cheat a little bit here. It seems SDL's
// renderer may flake out in scenarios like moving the window
// between monitors, so generate a synthetic reset event for
// the main loop to consume.
SDL_Event event;
event.type = SDL_RENDER_TARGETS_RESET;
SDL_PushEvent(&event);
return;
}
m_Device->Clear(0, nullptr, D3DCLEAR_TARGET, D3DCOLOR_ARGB(255, 0, 0, 0), 0.0f, 0);
hr = m_Processor->VideoProcessBlt(m_RenderTarget, &bltParams, &sample, 1, nullptr);
av_frame_free(&frame);
if (FAILED(hr)) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"VideoProcessBlt() failed: %x",
hr);
SDL_Event event;
event.type = SDL_RENDER_TARGETS_RESET;
SDL_PushEvent(&event);
return;
}
// We must try to present to trigger SDL's logic to recover the render target,
// even if VideoProcessBlt() fails.
SDL_RenderPresent(m_SdlRenderer);
hr = m_Device->PresentEx(nullptr, nullptr, nullptr, nullptr, 0);
if (FAILED(hr)) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"PresentEx() failed: %x",
hr);
SDL_Event event;
event.type = SDL_RENDER_TARGETS_RESET;
SDL_PushEvent(&event);
return;
}
}
+10 -5
View File
@@ -1,6 +1,7 @@
#pragma once
#include "renderer.h"
#include "pacer/pacer.h"
#include <d3d9.h>
#include <dxva2api.h>
@@ -17,13 +18,19 @@ public:
virtual bool initialize(SDL_Window* window,
int videoFormat,
int width,
int height);
int height,
int maxFps,
bool enableVsync);
virtual bool prepareDecoderContext(AVCodecContext* context);
virtual void renderFrame(AVFrame* frame);
virtual void renderFrameAtVsync(AVFrame* frame);
virtual bool needsTestFrame();
virtual int getDecoderCapabilities();
virtual VSyncConstraint getVsyncConstraint();
private:
bool initializeDecoder();
bool initializeRenderer();
bool initializeDevice(SDL_Window* window, bool enableVsync);
bool isDecoderBlacklisted();
static
@@ -42,8 +49,6 @@ private:
int m_DisplayWidth;
int m_DisplayHeight;
SDL_Renderer* m_SdlRenderer;
struct dxva_context m_DXVAContext;
IDirect3DSurface9* m_DecSurfaces[19];
DXVA2_ConfigPictureDecode m_Config;
@@ -52,7 +57,7 @@ private:
int m_SurfacesUsed;
AVBufferPool* m_Pool;
IDirect3DDevice9* m_Device;
IDirect3DDevice9Ex* m_Device;
IDirect3DSurface9* m_RenderTarget;
IDirectXVideoProcessorService* m_ProcService;
IDirectXVideoProcessor* m_Processor;
@@ -0,0 +1,11 @@
#pragma once
#include "pacer.h"
class DisplayLinkVsyncSourceFactory
{
public:
static
IVsyncSource* createVsyncSource(Pacer* pacer);
};
@@ -0,0 +1,132 @@
#include "displaylinkvsyncsource.h"
#include <SDL_syswm.h>
#include <CoreVideo/CoreVideo.h>
#import <Cocoa/Cocoa.h>
class DisplayLinkVsyncSource : public IVsyncSource
{
public:
DisplayLinkVsyncSource(Pacer* pacer)
: m_Pacer(pacer),
m_DisplayLink(nullptr)
{
}
virtual ~DisplayLinkVsyncSource() override
{
if (m_DisplayLink != nullptr) {
CVDisplayLinkStop(m_DisplayLink);
CVDisplayLinkRelease(m_DisplayLink);
}
}
static
CGDirectDisplayID
getDisplayID(NSScreen* screen)
{
NSNumber* screenNumber = [screen deviceDescription][@"NSScreenNumber"];
return [screenNumber unsignedIntValue];
}
static
CVReturn
displayLinkOutputCallback(
CVDisplayLinkRef displayLink,
const CVTimeStamp* /* now */,
const CVTimeStamp* /* vsyncTime */,
CVOptionFlags,
CVOptionFlags*,
void *displayLinkContext)
{
auto me = reinterpret_cast<DisplayLinkVsyncSource*>(displayLinkContext);
SDL_assert(displayLink == me->m_DisplayLink);
// In my testing on macOS 10.13, this callback is invoked about 24 ms
// prior to the specified v-sync time (now - vsyncTime). Since this is
// greater than the standard v-sync interval (16 ms = 60 FPS), we will
// draw using the current host time, rather than the actual v-sync target
// time. Because the CVDisplayLink is in sync with the actual v-sync
// interval, even if many ms prior, we can safely use the current host time
// and get a consistent callback for each v-sync. This reduces video latency
// by at least 1 frame vs. rendering with the actual vsyncTime.
me->m_Pacer->vsyncCallback(500 / me->m_DisplayFps);
return kCVReturnSuccess;
}
virtual bool initialize(SDL_Window* window, int displayFps) override
{
SDL_SysWMinfo info;
SDL_VERSION(&info.version);
if (!SDL_GetWindowWMInfo(window, &info)) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"SDL_GetWindowWMInfo() failed: %s",
SDL_GetError());
return false;
}
SDL_assert(info.subsystem == SDL_SYSWM_COCOA);
m_DisplayFps = displayFps;
NSScreen* screen = [info.info.cocoa.window screen];
CVReturn status;
if (screen == nullptr) {
// Window not visible on any display, so use a
// CVDisplayLink that can work with all active displays.
// When we become visible, we'll recreate ourselves
// and associate with the new screen.
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"NSWindow is not visible on any display");
status = CVDisplayLinkCreateWithActiveCGDisplays(&m_DisplayLink);
}
else {
CGDirectDisplayID displayId;
displayId = getDisplayID(screen);
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"NSWindow on display: %x",
displayId);
status = CVDisplayLinkCreateWithCGDisplay(displayId, &m_DisplayLink);
}
if (status != kCVReturnSuccess) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Failed to create CVDisplayLink: %d",
status);
return false;
}
status = CVDisplayLinkSetOutputCallback(m_DisplayLink, displayLinkOutputCallback, this);
if (status != kCVReturnSuccess) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"CVDisplayLinkSetOutputCallback() failed: %d",
status);
return false;
}
status = CVDisplayLinkStart(m_DisplayLink);
if (status != kCVReturnSuccess) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"CVDisplayLinkStart() failed: %d",
status);
return false;
}
return true;
}
private:
Pacer* m_Pacer;
CVDisplayLinkRef m_DisplayLink;
int m_DisplayFps;
};
IVsyncSource* DisplayLinkVsyncSourceFactory::createVsyncSource(Pacer* pacer) {
return new DisplayLinkVsyncSource(pacer);
}
@@ -0,0 +1,168 @@
#include "dxvsyncsource.h"
// Useful references:
// https://bugs.chromium.org/p/chromium/issues/detail?id=467617
// https://chromium.googlesource.com/chromium/src.git/+/c564f2fe339b2b2abb0c8773c90c83215670ea71/gpu/ipc/service/gpu_vsync_provider_win.cc
DxVsyncSource::DxVsyncSource(Pacer* pacer) :
m_Pacer(pacer),
m_Thread(nullptr),
m_Gdi32Handle(nullptr)
{
SDL_AtomicSet(&m_Stopping, 0);
}
DxVsyncSource::~DxVsyncSource()
{
if (m_Thread != nullptr) {
SDL_AtomicSet(&m_Stopping, 1);
SDL_WaitThread(m_Thread, nullptr);
}
if (m_Gdi32Handle != nullptr) {
FreeLibrary(m_Gdi32Handle);
}
}
bool DxVsyncSource::initialize(SDL_Window* window, int displayFps)
{
m_DisplayFps = displayFps;
m_Gdi32Handle = LoadLibraryA("gdi32.dll");
if (m_Gdi32Handle == nullptr) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Failed to load gdi32.dll: %d",
GetLastError());
return false;
}
m_D3DKMTOpenAdapterFromHdc = (PFND3DKMTOPENADAPTERFROMHDC)GetProcAddress(m_Gdi32Handle, "D3DKMTOpenAdapterFromHdc");
m_D3DKMTCloseAdapter = (PFND3DKMTCLOSEADAPTER)GetProcAddress(m_Gdi32Handle, "D3DKMTCloseAdapter");
m_D3DKMTWaitForVerticalBlankEvent = (PFND3DKMTWAITFORVERTICALBLANKEVENT)GetProcAddress(m_Gdi32Handle, "D3DKMTWaitForVerticalBlankEvent");
if (m_D3DKMTOpenAdapterFromHdc == nullptr ||
m_D3DKMTCloseAdapter == nullptr ||
m_D3DKMTWaitForVerticalBlankEvent == nullptr) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Missing required function in gdi32.dll");
return false;
}
SDL_SysWMinfo info;
SDL_VERSION(&info.version);
if (!SDL_GetWindowWMInfo(window, &info)) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"SDL_GetWindowWMInfo() failed: %s",
SDL_GetError());
return false;
}
m_Window = info.info.win.window;
m_Thread = SDL_CreateThread(vsyncThread, "DX Vsync Thread", this);
if (m_Thread == nullptr) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Unable to create DX V-sync thread: %s",
SDL_GetError());
return false;
}
return true;
}
int DxVsyncSource::vsyncThread(void* context)
{
DxVsyncSource* me = reinterpret_cast<DxVsyncSource*>(context);
SDL_SetThreadPriority(SDL_THREAD_PRIORITY_HIGH);
D3DKMT_OPENADAPTERFROMHDC openAdapterParams = {};
HMONITOR lastMonitor = nullptr;
DEVMODEA monitorMode;
monitorMode.dmSize = sizeof(monitorMode);
while (SDL_AtomicGet(&me->m_Stopping) == 0) {
D3DKMT_WAITFORVERTICALBLANKEVENT waitForVblankEventParams;
NTSTATUS status;
// If the monitor has changed from last time, open the new adapter
HMONITOR currentMonitor = MonitorFromWindow(me->m_Window, MONITOR_DEFAULTTONEAREST);
if (currentMonitor != lastMonitor) {
MONITORINFOEXA monitorInfo = {};
monitorInfo.cbSize = sizeof(monitorInfo);
if (!GetMonitorInfoA(currentMonitor, &monitorInfo)) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"GetMonitorInfo() failed: %d",
GetLastError());
SDL_Delay(10);
continue;
}
if (!EnumDisplaySettingsA(monitorInfo.szDevice, ENUM_CURRENT_SETTINGS, &monitorMode)) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"EnumDisplaySettings() failed: %d",
GetLastError());
SDL_Delay(10);
continue;
}
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Monitor changed: %s %d Hz",
monitorInfo.szDevice,
monitorMode.dmDisplayFrequency);
if (openAdapterParams.hAdapter != 0) {
D3DKMT_CLOSEADAPTER closeAdapterParams = {};
closeAdapterParams.hAdapter = openAdapterParams.hAdapter;
me->m_D3DKMTCloseAdapter(&closeAdapterParams);
}
openAdapterParams.hDc = CreateDCA(nullptr, monitorInfo.szDevice, nullptr, nullptr);
if (!openAdapterParams.hDc) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"CreateDC() failed: %d",
GetLastError());
SDL_Delay(10);
continue;
}
status = me->m_D3DKMTOpenAdapterFromHdc(&openAdapterParams);
DeleteDC(openAdapterParams.hDc);
if (status != STATUS_SUCCESS) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"D3DKMTOpenAdapterFromHdc() failed: %x",
status);
SDL_Delay(10);
continue;
}
lastMonitor = currentMonitor;
}
waitForVblankEventParams.hAdapter = openAdapterParams.hAdapter;
waitForVblankEventParams.hDevice = 0;
waitForVblankEventParams.VidPnSourceId = openAdapterParams.VidPnSourceId;
status = me->m_D3DKMTWaitForVerticalBlankEvent(&waitForVblankEventParams);
if (status != STATUS_SUCCESS) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"D3DKMTWaitForVerticalBlankEvent() failed: %x",
status);
SDL_Delay(10);
continue;
}
me->m_Pacer->vsyncCallback(1000 / me->m_DisplayFps);
}
if (openAdapterParams.hAdapter != 0) {
D3DKMT_CLOSEADAPTER closeAdapterParams = {};
closeAdapterParams.hAdapter = openAdapterParams.hAdapter;
me->m_D3DKMTCloseAdapter(&closeAdapterParams);
}
return 0;
}
@@ -0,0 +1,53 @@
#pragma once
#include "pacer.h"
#include <SDL_syswm.h>
// from <D3dkmthk.h>
typedef LONG NTSTATUS;
typedef UINT D3DKMT_HANDLE;
typedef UINT D3DDDI_VIDEO_PRESENT_SOURCE_ID;
#define STATUS_SUCCESS ((NTSTATUS)0x00000000L)
#define STATUS_GRAPHICS_PRESENT_OCCLUDED ((NTSTATUS)0xC01E0006L)
typedef struct _D3DKMT_OPENADAPTERFROMHDC {
HDC hDc;
D3DKMT_HANDLE hAdapter;
LUID AdapterLuid;
D3DDDI_VIDEO_PRESENT_SOURCE_ID VidPnSourceId;
} D3DKMT_OPENADAPTERFROMHDC;
typedef struct _D3DKMT_CLOSEADAPTER {
D3DKMT_HANDLE hAdapter;
} D3DKMT_CLOSEADAPTER;
typedef struct _D3DKMT_WAITFORVERTICALBLANKEVENT {
D3DKMT_HANDLE hAdapter;
D3DKMT_HANDLE hDevice;
D3DDDI_VIDEO_PRESENT_SOURCE_ID VidPnSourceId;
} D3DKMT_WAITFORVERTICALBLANKEVENT;
typedef NTSTATUS(APIENTRY* PFND3DKMTOPENADAPTERFROMHDC)(D3DKMT_OPENADAPTERFROMHDC*);
typedef NTSTATUS(APIENTRY* PFND3DKMTCLOSEADAPTER)(D3DKMT_CLOSEADAPTER*);
typedef NTSTATUS(APIENTRY* PFND3DKMTWAITFORVERTICALBLANKEVENT)(D3DKMT_WAITFORVERTICALBLANKEVENT*);
class DxVsyncSource : public IVsyncSource
{
public:
DxVsyncSource(Pacer* pacer);
virtual ~DxVsyncSource();
virtual bool initialize(SDL_Window* window, int displayFps);
private:
static int vsyncThread(void* context);
Pacer* m_Pacer;
SDL_Thread* m_Thread;
SDL_atomic_t m_Stopping;
HMODULE m_Gdi32Handle;
HWND m_Window;
int m_DisplayFps;
PFND3DKMTOPENADAPTERFROMHDC m_D3DKMTOpenAdapterFromHdc;
PFND3DKMTCLOSEADAPTER m_D3DKMTCloseAdapter;
PFND3DKMTWAITFORVERTICALBLANKEVENT m_D3DKMTWaitForVerticalBlankEvent;
};
@@ -0,0 +1,43 @@
#include "nullthreadedvsyncsource.h"
NullThreadedVsyncSource::NullThreadedVsyncSource(Pacer* pacer) :
m_Pacer(pacer),
m_Thread(nullptr)
{
SDL_AtomicSet(&m_Stopping, 0);
}
NullThreadedVsyncSource::~NullThreadedVsyncSource()
{
if (m_Thread != nullptr) {
SDL_AtomicSet(&m_Stopping, 1);
SDL_WaitThread(m_Thread, nullptr);
}
}
bool NullThreadedVsyncSource::initialize(SDL_Window*, int displayFps)
{
m_DisplayFps = displayFps;
m_Thread = SDL_CreateThread(vsyncThread, "Null Vsync Thread", this);
if (m_Thread == nullptr) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Unable to create DX V-sync thread: %s",
SDL_GetError());
return false;
}
return true;
}
int NullThreadedVsyncSource::vsyncThread(void* context)
{
NullThreadedVsyncSource* me = reinterpret_cast<NullThreadedVsyncSource*>(context);
SDL_SetThreadPriority(SDL_THREAD_PRIORITY_HIGH);
while (SDL_AtomicGet(&me->m_Stopping) == 0) {
me->m_Pacer->vsyncCallback(1000 / me->m_DisplayFps);
}
return 0;
}
@@ -0,0 +1,21 @@
#pragma once
#include "pacer.h"
class NullThreadedVsyncSource : public IVsyncSource
{
public:
NullThreadedVsyncSource(Pacer* pacer);
virtual ~NullThreadedVsyncSource();
virtual bool initialize(SDL_Window* window, int displayFps);
private:
static int vsyncThread(void* context);
Pacer* m_Pacer;
SDL_Thread* m_Thread;
SDL_atomic_t m_Stopping;
int m_DisplayFps;
};
@@ -0,0 +1,183 @@
#include "pacer.h"
#include "streaming/streamutils.h"
#include "nullthreadedvsyncsource.h"
#ifdef Q_OS_DARWIN
#include "displaylinkvsyncsource.h"
#endif
#ifdef Q_OS_WIN32
#include "dxvsyncsource.h"
#endif
#define FRAME_HISTORY_ENTRIES 8
// We may be woken up slightly late so don't go all the way
// up to the next V-sync since we may accidentally step into
// the next V-sync period. It also takes some amount of time
// to do the render itself, so we can't render right before
// V-sync happens.
#define TIMER_SLACK_MS 3
Pacer::Pacer(IFFmpegRenderer* renderer, PVIDEO_STATS videoStats) :
m_FrameQueueLock(0),
m_VsyncSource(nullptr),
m_VsyncRenderer(renderer),
m_MaxVideoFps(0),
m_DisplayFps(0),
m_VideoStats(videoStats)
{
}
Pacer::~Pacer()
{
// Stop V-sync callbacks
delete m_VsyncSource;
m_VsyncSource = nullptr;
while (!m_FrameQueue.isEmpty()) {
AVFrame* frame = m_FrameQueue.dequeue();
av_frame_free(&frame);
}
}
// Called in an arbitrary thread by the IVsyncSource on V-sync
// or an event synchronized with V-sync
void Pacer::vsyncCallback(int timeUntilNextVsyncMillis)
{
// Make sure initialize() has been called
SDL_assert(m_MaxVideoFps != 0);
SDL_assert(timeUntilNextVsyncMillis >= TIMER_SLACK_MS);
Uint32 vsyncCallbackStartTime = SDL_GetTicks();
SDL_AtomicLock(&m_FrameQueueLock);
// If the queue length history entries are large, be strict
// about dropping excess frames.
int frameDropTarget = 1;
// If we may get more frames per second than we can display, use
// frame history to drop frames only if consistently above the
// one queued frame mark.
if (m_MaxVideoFps >= m_DisplayFps) {
for (int i = 0; i < m_FrameQueueHistory.count(); i++) {
if (m_FrameQueueHistory[i] <= 1) {
// Be lenient as long as the queue length
// resolves before the end of frame history
frameDropTarget = 3;
break;
}
}
if (m_FrameQueueHistory.count() == FRAME_HISTORY_ENTRIES) {
m_FrameQueueHistory.dequeue();
}
m_FrameQueueHistory.enqueue(m_FrameQueue.count());
}
// Catch up if we're several frames ahead
while (m_FrameQueue.count() > frameDropTarget) {
AVFrame* frame = m_FrameQueue.dequeue();
m_VideoStats->pacerDroppedFrames++;
av_frame_free(&frame);
}
if (m_FrameQueue.isEmpty()) {
SDL_AtomicUnlock(&m_FrameQueueLock);
while (!SDL_TICKS_PASSED(SDL_GetTicks(),
vsyncCallbackStartTime + timeUntilNextVsyncMillis - TIMER_SLACK_MS)) {
SDL_Delay(1);
SDL_AtomicLock(&m_FrameQueueLock);
if (!m_FrameQueue.isEmpty()) {
// Don't release the lock
goto RenderNextFrame;
}
SDL_AtomicUnlock(&m_FrameQueueLock);
}
// Nothing to render at this time
return;
}
RenderNextFrame:
// Grab the first frame
AVFrame* frame = m_FrameQueue.dequeue();
SDL_AtomicUnlock(&m_FrameQueueLock);
// Render it
Uint32 beforeRender = SDL_GetTicks();
m_VsyncRenderer->renderFrameAtVsync(frame);
m_VideoStats->totalRenderTime += SDL_GetTicks() - beforeRender;
m_VideoStats->renderedFrames++;
// Free the frame
av_frame_free(&frame);
}
bool Pacer::initialize(SDL_Window* window, int maxVideoFps, bool enableVsync)
{
m_MaxVideoFps = maxVideoFps;
m_EnableVsync = enableVsync;
m_DisplayFps = StreamUtils::getDisplayRefreshRate(window);
if (m_EnableVsync) {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Frame pacing in tear-free mode: target %d Hz with %d FPS stream",
m_DisplayFps, m_MaxVideoFps);
#if defined(Q_OS_DARWIN)
m_VsyncSource = DisplayLinkVsyncSourceFactory::createVsyncSource(this);
#elif defined(Q_OS_WIN32)
m_VsyncSource = new DxVsyncSource(this);
#else
// Platforms without a VsyncSource will just render frames
// immediately like they used to.
#endif
if (m_VsyncSource != nullptr && !m_VsyncSource->initialize(window, m_DisplayFps)) {
return false;
}
}
else {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Minimal latency tearing mode: target %d Hz with %d FPS stream",
m_DisplayFps, m_MaxVideoFps);
}
return true;
}
void Pacer::submitFrame(AVFrame* frame)
{
// Make sure initialize() has been called
SDL_assert(m_MaxVideoFps != 0);
// Queue the frame until the V-sync callback if
// we have a V-sync source, otherwise deliver it
// immediately and hope for the best.
if (isUsingFrameQueue()) {
SDL_AtomicLock(&m_FrameQueueLock);
m_FrameQueue.enqueue(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);
}
}
bool Pacer::isUsingFrameQueue()
{
return m_VsyncSource != nullptr;
}
@@ -0,0 +1,40 @@
#pragma once
#include "../../decoder.h"
#include "../renderer.h"
#include <QQueue>
class IVsyncSource {
public:
virtual ~IVsyncSource() {}
virtual bool initialize(SDL_Window* window, int displayFps) = 0;
};
class Pacer
{
public:
Pacer(IFFmpegRenderer* renderer, PVIDEO_STATS videoStats);
~Pacer();
void submitFrame(AVFrame* frame);
bool initialize(SDL_Window* window, int maxVideoFps, bool enableVsync);
void vsyncCallback(int timeUntilNextVsyncMillis);
bool isUsingFrameQueue();
private:
QQueue<AVFrame*> m_FrameQueue;
QQueue<int> m_FrameQueueHistory;
SDL_SpinLock m_FrameQueueLock;
IVsyncSource* m_VsyncSource;
IFFmpegRenderer* m_VsyncRenderer;
int m_MaxVideoFps;
int m_DisplayFps;
bool m_EnableVsync;
PVIDEO_STATS m_VideoStats;
};
@@ -8,13 +8,24 @@ extern "C" {
class IFFmpegRenderer {
public:
enum VSyncConstraint {
VSYNC_FORCE_OFF,
VSYNC_FORCE_ON,
VSYNC_ANY
};
virtual ~IFFmpegRenderer() {}
virtual bool initialize(SDL_Window* window,
int videoFormat,
int width,
int height) = 0;
int height,
int maxFps,
bool enableVsync) = 0;
virtual bool prepareDecoderContext(AVCodecContext* context) = 0;
virtual void renderFrame(AVFrame* frame) = 0;
virtual void renderFrameAtVsync(AVFrame* frame) = 0;
virtual bool needsTestFrame() = 0;
virtual int getDecoderCapabilities() = 0;
virtual VSyncConstraint getVsyncConstraint() = 0;
};
class SdlRenderer : public IFFmpegRenderer {
@@ -24,9 +35,14 @@ public:
virtual bool initialize(SDL_Window* window,
int videoFormat,
int width,
int height);
int height,
int maxFps,
bool enableVsync);
virtual bool prepareDecoderContext(AVCodecContext* context);
virtual void renderFrame(AVFrame* frame);
virtual void renderFrameAtVsync(AVFrame* frame);
virtual bool needsTestFrame();
virtual int getDecoderCapabilities();
virtual VSyncConstraint getVsyncConstraint();
private:
SDL_Renderer* m_Renderer;
@@ -30,12 +30,43 @@ bool SdlRenderer::prepareDecoderContext(AVCodecContext*)
return true;
}
bool SdlRenderer::needsTestFrame()
{
// This renderer should always work
return false;
}
int SdlRenderer::getDecoderCapabilities()
{
// The FFmpeg CPU decoder can handle reference frame invalidation,
// but only for H.264.
return CAPABILITY_REFERENCE_FRAME_INVALIDATION_AVC;
}
IFFmpegRenderer::VSyncConstraint SdlRenderer::getVsyncConstraint()
{
return VSYNC_ANY;
}
bool SdlRenderer::initialize(SDL_Window* window,
int,
int width,
int height)
int height,
int,
bool enableVsync)
{
m_Renderer = SDL_CreateRenderer(window, -1, SDL_RENDERER_ACCELERATED);
Uint32 rendererFlags = SDL_RENDERER_ACCELERATED;
if ((SDL_GetWindowFlags(window) & SDL_WINDOW_FULLSCREEN_DESKTOP) == SDL_WINDOW_FULLSCREEN) {
// In full-screen exclusive mode, we enable V-sync if requested. For other modes, Windows and Mac
// have compositors that make rendering tear-free. Linux compositor varies by distro and user
// configuration but doesn't seem feasible to detect here.
if (enableVsync) {
rendererFlags |= SDL_RENDERER_PRESENTVSYNC;
}
}
m_Renderer = SDL_CreateRenderer(window, -1, rendererFlags);
if (!m_Renderer) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"SDL_CreateRenderer() failed: %s",
@@ -67,7 +98,7 @@ bool SdlRenderer::initialize(SDL_Window* window,
return true;
}
void SdlRenderer::renderFrame(AVFrame* frame)
void SdlRenderer::renderFrameAtVsync(AVFrame* frame)
{
SDL_UpdateYUVTexture(m_Texture, nullptr,
frame->data[0],
@@ -77,9 +108,6 @@ void SdlRenderer::renderFrame(AVFrame* frame)
frame->data[2],
frame->linesize[2]);
// Done with the frame now
av_frame_free(&frame);
SDL_RenderClear(m_Renderer);
SDL_RenderCopy(m_Renderer, m_Texture, nullptr, nullptr);
SDL_RenderPresent(m_Renderer);
+43 -4
View File
@@ -1,3 +1,5 @@
#include <QString>
#include "vaapi.h"
#include <streaming/streamutils.h>
@@ -27,7 +29,7 @@ VAAPIRenderer::~VAAPIRenderer()
}
bool
VAAPIRenderer::initialize(SDL_Window* window, int, int width, int height)
VAAPIRenderer::initialize(SDL_Window* window, int, int width, int height, int, bool)
{
int err;
SDL_SysWMinfo info;
@@ -109,6 +111,26 @@ VAAPIRenderer::initialize(SDL_Window* window, int, int width, int height)
"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) {
@@ -141,8 +163,27 @@ VAAPIRenderer::prepareDecoderContext(AVCodecContext* context)
return true;
}
bool
VAAPIRenderer::needsTestFrame()
{
// We need a test frame to see if this VAAPI driver
// supports the profile used for streaming
return true;
}
int
VAAPIRenderer::getDecoderCapabilities()
{
return 0;
}
IFFmpegRenderer::VSyncConstraint VAAPIRenderer::getVsyncConstraint()
{
return VSYNC_ANY;
}
void
VAAPIRenderer::renderFrame(AVFrame* frame)
VAAPIRenderer::renderFrameAtVsync(AVFrame* frame)
{
VASurfaceID surface = (VASurfaceID)(uintptr_t)frame->data[3];
AVHWDeviceContext* deviceContext = (AVHWDeviceContext*)m_HwContext->data;
@@ -197,6 +238,4 @@ VAAPIRenderer::renderFrame(AVFrame* frame)
// We don't accept anything else in initialize().
SDL_assert(false);
}
av_frame_free(&frame);
}
+7 -2
View File
@@ -33,9 +33,14 @@ public:
virtual bool initialize(SDL_Window* window,
int videoFormat,
int width,
int height);
int height,
int maxFps,
bool enableVsync);
virtual bool prepareDecoderContext(AVCodecContext* context);
virtual void renderFrame(AVFrame* frame);
virtual void renderFrameAtVsync(AVFrame* frame);
virtual bool needsTestFrame();
virtual int getDecoderCapabilities();
virtual VSyncConstraint getVsyncConstraint();
private:
int m_WindowSystem;
+36 -11
View File
@@ -54,7 +54,7 @@ VDPAURenderer::~VDPAURenderer()
}
}
bool VDPAURenderer::initialize(SDL_Window* window, int, int width, int height)
bool VDPAURenderer::initialize(SDL_Window* window, int, int width, int height, int, bool)
{
int err;
VdpStatus status;
@@ -91,6 +91,7 @@ bool VDPAURenderer::initialize(SDL_Window* window, int, int width, int height)
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)
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++) {
@@ -217,13 +225,33 @@ bool VDPAURenderer::prepareDecoderContext(AVCodecContext* context)
// https://github.com/FFmpeg/FFmpeg/commit/64ecb78b7179cab2dbdf835463104679dbb7c895
context->hwaccel_flags |= AV_HWACCEL_FLAG_ALLOW_PROFILE_MISMATCH;
// This flag is recommended due to hardware underreporting supported levels
context->hwaccel_flags |= AV_HWACCEL_FLAG_IGNORE_LEVEL;
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Using VDPAU accelerated renderer");
return true;
}
void VDPAURenderer::renderFrame(AVFrame* frame)
bool VDPAURenderer::needsTestFrame()
{
// We need a test frame to see if this VDPAU driver
// supports the profile used for streaming
return true;
}
int VDPAURenderer::getDecoderCapabilities()
{
return 0;
}
IFFmpegRenderer::VSyncConstraint VDPAURenderer::getVsyncConstraint()
{
return VSYNC_ANY;
}
void VDPAURenderer::renderFrameAtVsync(AVFrame* frame)
{
VdpStatus status;
VdpVideoSurface videoSurface = (VdpVideoSurface)(uintptr_t)frame->data[3];
@@ -235,14 +263,14 @@ void VDPAURenderer::renderFrame(AVFrame* frame)
// We need to create the mixer on the fly, because we don't know the dimensions
// of our video surfaces in advance of decoding
if (m_VideoMixer == 0) {
VdpChromaType chroma;
VdpChromaType videoSurfaceChroma;
uint32_t videoSurfaceWidth, videoSurfaceHeight;
status = m_VdpVideoSurfaceGetParameters(videoSurface, &chroma, &videoSurfaceWidth, &videoSurfaceHeight);
status = m_VdpVideoSurfaceGetParameters(videoSurface, &videoSurfaceChroma,
&videoSurfaceWidth, &videoSurfaceHeight);
if (status != VDP_STATUS_OK) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"VdpVideoSurfaceGetParameters() failed: %s",
m_VdpGetErrorString(status));
av_frame_free(&frame);
return;
}
@@ -250,14 +278,16 @@ void VDPAURenderer::renderFrame(AVFrame* frame)
"VDPAU surface size: %dx%d",
videoSurfaceWidth, videoSurfaceHeight);
#define PARAM_COUNT 2
#define PARAM_COUNT 3
const VdpVideoMixerParameter params[PARAM_COUNT] = {
VDP_VIDEO_MIXER_PARAMETER_VIDEO_SURFACE_WIDTH,
VDP_VIDEO_MIXER_PARAMETER_VIDEO_SURFACE_HEIGHT,
VDP_VIDEO_MIXER_PARAMETER_CHROMA_TYPE,
};
const void* const paramValues[PARAM_COUNT] = {
&videoSurfaceWidth,
&videoSurfaceHeight,
&videoSurfaceChroma,
};
status = m_VdpVideoMixerCreate(m_Device, 0, nullptr,
@@ -267,7 +297,6 @@ void VDPAURenderer::renderFrame(AVFrame* frame)
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"VdpVideoMixerCreate() failed: %s",
m_VdpGetErrorString(status));
av_frame_free(&frame);
return;
}
}
@@ -310,10 +339,6 @@ void VDPAURenderer::renderFrame(AVFrame* frame)
nullptr,
0,
nullptr);
// The decoder can have this surface back now
av_frame_free(&frame);
if (status != VDP_STATUS_OK) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"VdpVideoMixerRender() failed: %s",
+8 -2
View File
@@ -16,9 +16,14 @@ public:
virtual bool initialize(SDL_Window* window,
int videoFormat,
int width,
int height);
int height,
int maxFps,
bool enableVsync);
virtual bool prepareDecoderContext(AVCodecContext* context);
virtual void renderFrame(AVFrame* frame);
virtual void renderFrameAtVsync(AVFrame* frame);
virtual bool needsTestFrame();
virtual int getDecoderCapabilities();
virtual VSyncConstraint getVsyncConstraint();
private:
uint32_t m_VideoWidth, m_VideoHeight;
@@ -51,6 +56,7 @@ private:
VdpOutputSurfaceDestroy* m_VdpOutputSurfaceDestroy;
VdpOutputSurfaceQueryCapabilities* m_VdpOutputSurfaceQueryCapabilities;
VdpVideoSurfaceGetParameters* m_VdpVideoSurfaceGetParameters;
VdpGetInformationString* m_VdpGetInformationString;
// X11 stuff
VdpPresentationQueueTargetCreateX11* m_VdpPresentationQueueTargetCreateX11;
+45 -121
View File
@@ -2,19 +2,16 @@
// libavutil both defining AVMediaType
#define AVMediaType AVMediaType_FFmpeg
#include "vt.h"
#include "pacer/pacer.h"
#undef AVMediaType
#include <SDL_syswm.h>
#include <Limelight.h>
#include <QQueue>
#include <mach/mach_time.h>
#import <Cocoa/Cocoa.h>
#import <VideoToolbox/VideoToolbox.h>
#import <AVFoundation/AVFoundation.h>
#import <CoreVideo/CoreVideo.h>
#define FRAME_HISTORY_ENTRIES 8
class VTRenderer : public IFFmpegRenderer
{
@@ -23,9 +20,7 @@ public:
: m_HwContext(nullptr),
m_DisplayLayer(nullptr),
m_FormatDesc(nullptr),
m_View(nullptr),
m_DisplayLink(nullptr),
m_FrameQueueLock(0)
m_View(nullptr)
{
}
@@ -39,63 +34,28 @@ public:
CFRelease(m_FormatDesc);
}
if (m_DisplayLink != nullptr) {
CVDisplayLinkStop(m_DisplayLink);
CVDisplayLinkRelease(m_DisplayLink);
}
while (!m_FrameQueue.isEmpty()) {
AVFrame* frame = m_FrameQueue.dequeue();
av_frame_free(&frame);
}
if (m_View != nullptr) {
[m_View removeFromSuperview];
}
}
void drawFrame(uint64_t vsyncTime)
// Caller frees frame after we return
virtual void renderFrameAtVsync(AVFrame* frame) override
{
OSStatus status;
CVPixelBufferRef pixBuf = reinterpret_cast<CVPixelBufferRef>(frame->data[3]);
SDL_AtomicLock(&m_FrameQueueLock);
if (m_DisplayLayer.status == AVQueuedSampleBufferRenderingStatusFailed) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"Resetting failed AVSampleBufferDisplay layer");
int frameDropTarget;
// If the queue length history entries are large, be strict
// about dropping excess frames.
frameDropTarget = 1;
for (int i = 0; i < m_FrameQueueHistory.count(); i++) {
if (m_FrameQueueHistory[i] <= 1) {
// Be lenient as long as the queue length
// resolves before the end of frame history
frameDropTarget = 3;
}
}
if (m_FrameQueueHistory.count() == FRAME_HISTORY_ENTRIES) {
m_FrameQueueHistory.dequeue();
}
m_FrameQueueHistory.enqueue(m_FrameQueue.count());
// Catch up if we're several frames ahead
while (m_FrameQueue.count() > frameDropTarget) {
AVFrame* frame = m_FrameQueue.dequeue();
av_frame_free(&frame);
}
if (m_FrameQueue.isEmpty()) {
SDL_AtomicUnlock(&m_FrameQueueLock);
// Trigger the main thread to recreate the decoder
SDL_Event event;
event.type = SDL_RENDER_TARGETS_RESET;
SDL_PushEvent(&event);
return;
}
// Grab the first frame
AVFrame* frame = m_FrameQueue.dequeue();
SDL_AtomicUnlock(&m_FrameQueueLock);
CVPixelBufferRef pixBuf = reinterpret_cast<CVPixelBufferRef>(frame->data[3]);
// If the format has changed or doesn't exist yet, construct it with the
// pixel buffer data
if (!m_FormatDesc || !CMVideoFormatDescriptionMatchesImageBuffer(m_FormatDesc, pixBuf)) {
@@ -108,7 +68,6 @@ public:
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"CMVideoFormatDescriptionCreateForImageBuffer() failed: %d",
status);
av_frame_free(&frame);
return;
}
}
@@ -117,7 +76,7 @@ public:
CMSampleTimingInfo timingInfo = {
.duration = kCMTimeInvalid,
.decodeTimeStamp = kCMTimeInvalid,
.presentationTimeStamp = CMTimeMake(vsyncTime, 1000 * 1000 * 1000)
.presentationTimeStamp = CMTimeMake(mach_absolute_time(), 1000 * 1000 * 1000)
};
CMSampleBufferRef sampleBuffer;
@@ -130,45 +89,20 @@ public:
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"CMSampleBufferCreateReadyWithImageBuffer() failed: %d",
status);
av_frame_free(&frame);
return;
}
[m_DisplayLayer enqueueSampleBuffer:sampleBuffer];
CFRelease(sampleBuffer);
av_frame_free(&frame);
}
static
CVReturn
displayLinkOutputCallback(
CVDisplayLinkRef,
const CVTimeStamp* now,
const CVTimeStamp* /* vsyncTime */,
CVOptionFlags,
CVOptionFlags*,
void *displayLinkContext)
{
VTRenderer* me = reinterpret_cast<VTRenderer*>(displayLinkContext);
// In my testing on macOS 10.13, this callback is invoked about 24 ms
// prior to the specified v-sync time (now - vsyncTime). Since this is
// greater than the standard v-sync interval (16 ms = 60 FPS), we will
// draw using the current host time, rather than the actual v-sync target
// time. Because the CVDisplayLink is in sync with the actual v-sync
// interval, even if many ms prior, we can safely use the current host time
// and get a consistent callback for each v-sync. This reduces video latency
// by at least 1 frame vs. rendering with the actual vsyncTime.
me->drawFrame(now->hostTime);
return kCVReturnSuccess;
}
virtual bool initialize(SDL_Window* window,
int videoFormat,
int,
int) override
int,
int,
bool) override
{
int err;
@@ -226,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,
@@ -243,10 +187,6 @@ public:
return false;
}
CVDisplayLinkCreateWithActiveCGDisplays(&m_DisplayLink);
CVDisplayLinkSetOutputCallback(m_DisplayLink, displayLinkOutputCallback, this);
CVDisplayLinkStart(m_DisplayLink);
return true;
}
@@ -260,46 +200,30 @@ public:
return true;
}
virtual void renderFrame(AVFrame* frame) override
virtual bool needsTestFrame() override
{
if (m_DisplayLayer.status == AVQueuedSampleBufferRenderingStatusFailed) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"Resetting failed AVSampleBufferDisplay layer");
setupDisplayLayer();
}
// We query VT to determine whether the codec is supported
return false;
}
SDL_AtomicLock(&m_FrameQueueLock);
m_FrameQueue.enqueue(frame);
SDL_AtomicUnlock(&m_FrameQueueLock);
virtual int getDecoderCapabilities() override
{
return 0;
}
virtual IFFmpegRenderer::VSyncConstraint getVsyncConstraint() 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;
}
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;
NSView* m_View;
CVDisplayLinkRef m_DisplayLink;
QQueue<AVFrame*> m_FrameQueue;
QQueue<int> m_FrameQueueHistory;
SDL_SpinLock m_FrameQueueLock;
};
IFFmpegRenderer* VTRendererFactory::createRenderer() {
+273 -29
View File
@@ -1,5 +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"
@@ -20,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()
@@ -27,6 +31,11 @@ bool FFmpegVideoDecoder::isHardwareAccelerated()
return m_HwDecodeCfg != nullptr;
}
int FFmpegVideoDecoder::getDecoderCapabilities()
{
return m_Renderer->getDecoderCapabilities();
}
enum AVPixelFormat FFmpegVideoDecoder::ffGetFormat(AVCodecContext* context,
const enum AVPixelFormat* pixFmts)
{
@@ -53,11 +62,19 @@ FFmpegVideoDecoder::FFmpegVideoDecoder()
m_DecodeBuffer(1024 * 1024, 0),
m_HwDecodeCfg(nullptr),
m_Renderer(nullptr),
m_ConsecutiveFailedDecodes(0)
m_ConsecutiveFailedDecodes(0),
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");
}
@@ -91,19 +108,53 @@ void FFmpegVideoDecoder::reset()
dropFrame(&event.user);
}
else {
SDL_Delay(100);
SDL_Delay(10);
SDL_PumpEvents();
}
}
delete m_Pacer;
m_Pacer = nullptr;
// This must be called after deleting Pacer because it
// may be holding AVFrames to free in its destructor.
// However, it must be called before deleting the IFFmpegRenderer
// since the codec context may be referencing objects that we
// need to delete in the renderer destructor.
avcodec_free_context(&m_VideoDecoderCtx);
delete m_Renderer;
m_Renderer = nullptr;
logVideoStats(m_GlobalVideoStats, "Global video stats");
}
bool FFmpegVideoDecoder::completeInitialization(AVCodec* decoder, int videoFormat, int width, int height, bool testOnly)
bool FFmpegVideoDecoder::completeInitialization(AVCodec* decoder, SDL_Window* window,
int videoFormat, int width, int height,
int maxFps, bool enableVsync, bool testOnly)
{
auto vsyncConstraint = m_Renderer->getVsyncConstraint();
if (vsyncConstraint == IFFmpegRenderer::VSYNC_FORCE_OFF && enableVsync) {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"V-sync is forcefully disabled by the active renderer");
enableVsync = false;
}
else if (vsyncConstraint == IFFmpegRenderer::VSYNC_FORCE_ON && !enableVsync) {
// 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;
}
}
m_StreamFps = maxFps;
m_Pacer = new Pacer(m_Renderer, &m_ActiveWndVideoStats);
if (!m_Pacer->initialize(window, maxFps, enableVsync)) {
return false;
}
m_VideoDecoderCtx = avcodec_alloc_context3(decoder);
if (!m_VideoDecoderCtx) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
@@ -114,6 +165,10 @@ bool FFmpegVideoDecoder::completeInitialization(AVCodec* decoder, int videoForma
// Always request low delay decoding
m_VideoDecoderCtx->flags |= AV_CODEC_FLAG_LOW_DELAY;
// Allow display of corrupt frames and frames missing references
m_VideoDecoderCtx->flags |= AV_CODEC_FLAG_OUTPUT_CORRUPT;
m_VideoDecoderCtx->flags2 |= AV_CODEC_FLAG2_SHOW_ALL;
// Enable slice multi-threading for software decoding
if (!m_HwDecodeCfg) {
m_VideoDecoderCtx->thread_type = FF_THREAD_SLICE;
@@ -174,6 +229,17 @@ bool FFmpegVideoDecoder::completeInitialization(AVCodec* decoder, int videoForma
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
@@ -183,6 +249,80 @@ bool FFmpegVideoDecoder::completeInitialization(AVCodec* decoder, int videoForma
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.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 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)) {
@@ -218,7 +358,8 @@ bool FFmpegVideoDecoder::initialize(
int videoFormat,
int width,
int height,
int)
int maxFps,
bool enableVsync)
{
AVCodec* decoder;
@@ -253,8 +394,8 @@ bool FFmpegVideoDecoder::initialize(
m_HwDecodeCfg = nullptr;
m_Renderer = new SdlRenderer();
if (vds != StreamingPreferences::VDS_FORCE_HARDWARE &&
m_Renderer->initialize(window, videoFormat, width, height) &&
completeInitialization(decoder, videoFormat, width, height, false)) {
m_Renderer->initialize(window, videoFormat, width, height, maxFps, enableVsync) &&
completeInitialization(decoder, window, videoFormat, width, height, maxFps, enableVsync, false)) {
return true;
}
else {
@@ -269,20 +410,26 @@ bool FFmpegVideoDecoder::initialize(
}
m_HwDecodeCfg = config;
// Submit test frame to ensure this codec really works
if (m_Renderer->initialize(window, videoFormat, width, height) &&
completeInitialization(decoder, videoFormat, width, height, true)) {
// OK, it worked, so now let's initialize it for real
reset();
if ((m_Renderer = createAcceleratedRenderer(config)) != nullptr &&
m_Renderer->initialize(window, videoFormat, width, height) &&
completeInitialization(decoder, videoFormat, width, height, false)) {
return true;
// 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())) {
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)) {
return true;
}
else {
SDL_LogCritical(SDL_LOG_CATEGORY_APPLICATION,
"Decoder failed to initialize after successful test");
reset();
}
}
else {
SDL_LogCritical(SDL_LOG_CATEGORY_APPLICATION,
"Decoder failed to initialize after successful test");
reset();
// No test required. Good to go now.
return true;
}
}
else {
@@ -292,28 +439,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) {
@@ -354,6 +580,11 @@ int FFmpegVideoDecoder::submitDecodeUnit(PDECODE_UNIT du)
// 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);
@@ -380,7 +611,7 @@ int FFmpegVideoDecoder::submitDecodeUnit(PDECODE_UNIT du)
void FFmpegVideoDecoder::renderFrame(SDL_UserEvent* event)
{
AVFrame* frame = reinterpret_cast<AVFrame*>(event->data1);
m_Renderer->renderFrame(frame);
m_Pacer->submitFrame(frame);
SDL_AtomicDecRef(&m_QueuedFrames);
}
@@ -388,6 +619,19 @@ void FFmpegVideoDecoder::renderFrame(SDL_UserEvent* event)
void FFmpegVideoDecoder::dropFrame(SDL_UserEvent* event)
{
AVFrame* frame = reinterpret_cast<AVFrame*>(event->data1);
av_frame_free(&frame);
// If Pacer is using a frame queue, we can just queue the
// frame and let it decide whether to drop or not. If Pacer
// is submitting directly for rendering, we drop the frame
// ourselves.
if (m_Pacer->isUsingFrameQueue()) {
m_Pacer->submitFrame(frame);
}
else {
av_frame_free(&frame);
// Since Pacer won't see this frame, we'll mark it as a drop
// on its behalf
m_ActiveWndVideoStats.pacerDroppedFrames++;
}
SDL_AtomicDecRef(&m_QueuedFrames);
}
+20 -3
View File
@@ -2,6 +2,7 @@
#include "decoder.h"
#include "ffmpeg-renderers/renderer.h"
#include "ffmpeg-renderers/pacer/pacer.h"
extern "C" {
#include <libavcodec/avcodec.h>
@@ -16,8 +17,10 @@ public:
int videoFormat,
int width,
int height,
int frameRate) override;
int maxFps,
bool enableVsync) override;
virtual bool isHardwareAccelerated() override;
virtual int getDecoderCapabilities() override;
virtual int submitDecodeUnit(PDECODE_UNIT du) override;
virtual void renderFrame(SDL_UserEvent* event) override;
virtual void dropFrame(SDL_UserEvent* event) override;
@@ -25,13 +28,20 @@ public:
virtual IFFmpegRenderer* getRenderer();
private:
bool completeInitialization(AVCodec* decoder, int videoFormat,
int width, int height, bool testOnly);
bool completeInitialization(AVCodec* decoder, SDL_Window* window,
int videoFormat, int width, int height,
int maxFps, bool enableVsync, 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);
@@ -43,6 +53,13 @@ private:
IFFmpegRenderer* m_Renderer;
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[];
+7 -1
View File
@@ -25,10 +25,16 @@ SLVideoDecoder::isHardwareAccelerated()
return true;
}
int
SLVideoDecoder::getDecoderCapabilities()
{
return 0;
}
bool
SLVideoDecoder::initialize(StreamingPreferences::VideoDecoderSelection vds,
SDL_Window*,
int videoFormat, int, int, int frameRate)
int videoFormat, int, int, int frameRate, bool)
{
// SLVideo only supports hardware decoding
if (vds == StreamingPreferences::VDS_FORCE_SOFTWARE) {
+3 -1
View File
@@ -14,8 +14,10 @@ public:
int videoFormat,
int width,
int height,
int frameRate);
int frameRate,
bool enableVsync);
virtual bool isHardwareAccelerated();
virtual int getDecoderCapabilities();
virtual int submitDecodeUnit(PDECODE_UNIT du);
// Unused since rendering is done directly from the decode thread
+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
+271
View File
@@ -0,0 +1,271 @@
/*
* Copyright (C) 2010-2013 Nektra S.A., Buenos Aires, Argentina.
* All rights reserved. Contact: http://www.nektra.com
*
*
* This file is part of Deviare In-Proc
*
*
* Commercial License Usage
* ------------------------
* Licensees holding valid commercial Deviare In-Proc licenses may use this
* file in accordance with the commercial license agreement provided with the
* Software or, alternatively, in accordance with the terms contained in
* a written agreement between you and Nektra. For licensing terms and
* conditions see http://www.nektra.com/licensing/. For further information
* use the contact form at http://www.nektra.com/contact/.
*
*
* GNU General Public License Usage
* --------------------------------
* Alternatively, this file may be used under the terms of the GNU
* General Public License version 3.0 as published by the Free Software
* Foundation and appearing in the file LICENSE.GPL included in the
* packaging of this file. Please review the following information to
* ensure the GNU General Public License version 3.0 requirements will be
* met: http://www.gnu.org/copyleft/gpl.html.
*
**/
#ifndef _NKTHOOKLIB
#define _NKTHOOKLIB
#include <windows.h>
//-----------------------------------------------------------
#define NKTHOOKLIB_DontSkipInitialJumps 0x0001
#define NKTHOOKLIB_DontRemoveOnUnhook 0x0002
#define NKTHOOKLIB_DontSkipAnyJumps 0x0004
#define NKTHOOKLIB_SkipNullProcsToHook 0x0008
#define NKTHOOKLIB_UseAbsoluteIndirectJumps 0x0010
#define NKTHOOKLIB_DisallowReentrancy 0x0020
#define NKTHOOKLIB_DontEnableHooks 0x0040
#define NKTHOOKLIB_ProcessPlatformX86 1
#define NKTHOOKLIB_ProcessPlatformX64 2
#define NKTHOOKLIB_CurrentProcess ((HANDLE)(LONG_PTR)-1)
#define NKTHOOKLIB_CurrentThread ((HANDLE)(LONG_PTR)-2)
//-----------------------------------------------------------
class CNktHookLib
{
public:
typedef struct tagHOOK_INFO {
SIZE_T nHookId;
LPVOID lpProcToHook;
LPVOID lpNewProcAddr;
//----
LPVOID lpCallOriginal;
} HOOK_INFO, *LPHOOK_INFO;
CNktHookLib();
~CNktHookLib();
DWORD Hook(__out SIZE_T *lpnHookId, __out LPVOID *lplpCallOriginal, __in LPVOID lpProcToHook,
__in LPVOID lpNewProcAddr, __in DWORD dwFlags=0);
DWORD Hook(__inout HOOK_INFO aHookInfo[], __in SIZE_T nCount, __in DWORD dwFlags=0);
DWORD Hook(__inout LPHOOK_INFO aHookInfo[], __in SIZE_T nCount, __in DWORD dwFlags = 0);
DWORD RemoteHook(__out SIZE_T *lpnHookId, __out LPVOID *lplpCallOriginal, __in DWORD dwPid,
__in LPVOID lpProcToHook, __in LPVOID lpNewProcAddr, __in DWORD dwFlags);
DWORD RemoteHook(__inout HOOK_INFO aHookInfo[], __in SIZE_T nCount, __in DWORD dwPid, __in DWORD dwFlags);
DWORD RemoteHook(__inout LPHOOK_INFO aHookInfo[], __in SIZE_T nCount, __in DWORD dwPid, __in DWORD dwFlags);
DWORD RemoteHook(__out SIZE_T *lpnHookId, __out LPVOID *lplpCallOriginal, __in HANDLE hProcess,
__in LPVOID lpProcToHook, __in LPVOID lpNewProcAddr, __in DWORD dwFlags);
DWORD RemoteHook(__inout HOOK_INFO aHookInfo[], __in SIZE_T nCount, __in HANDLE hProcess, __in DWORD dwFlags);
DWORD RemoteHook(__inout LPHOOK_INFO aHookInfo[], __in SIZE_T nCount, __in HANDLE hProcess, __in DWORD dwFlags);
DWORD Unhook(__in SIZE_T nHookId);
DWORD Unhook(__in HOOK_INFO aHookInfo[], __in SIZE_T nCount);
DWORD Unhook(__in LPHOOK_INFO aHookInfo[], __in SIZE_T nCount);
VOID UnhookProcess(__in DWORD dwPid);
VOID UnhookAll();
//NOTE: The following 2 (two) methods will remove the hooks from the internal list of hooks but the original
// hook(s) will remain active.
DWORD RemoveHook(__in SIZE_T nHookId, BOOL bDisable);
DWORD RemoveHook(__in HOOK_INFO aHookInfo[], __in SIZE_T nCount, __in BOOL bDisable);
DWORD RemoveHook(__in LPHOOK_INFO aHookInfo[], __in SIZE_T nCount, __in BOOL bDisable);
DWORD EnableHook(__in SIZE_T nHookId, __in BOOL bEnable);
DWORD EnableHook(__in HOOK_INFO aHookInfo[], __in SIZE_T nCount, __in BOOL bEnable);
DWORD EnableHook(__in LPHOOK_INFO aHookInfo[], __in SIZE_T nCount, __in BOOL bEnable);
DWORD SetSuspendThreadsWhileHooking(__in BOOL bEnable);
BOOL GetSuspendThreadsWhileHooking();
DWORD SetEnableDebugOutput(__in BOOL bEnable);
BOOL GetEnableDebugOutput();
void* __cdecl operator new(__in size_t nSize);
void* __cdecl operator new[](__in size_t nSize);
void* __cdecl operator new(__in size_t nSize, __inout void* lpInPlace);
void __cdecl operator delete(__inout void* p);
void __cdecl operator delete[](__inout void* p);
#if _MSC_VER >= 1200
void __cdecl operator delete(__inout void* p, __inout void* lpPlace);
#endif //_MSC_VER >= 1200
private:
DWORD HookCommon(__in LPVOID lpInfo, __in SIZE_T nCount, __in DWORD dwPid, __in DWORD dwFlags);
DWORD UnhookCommon(__in LPVOID lpInfo, __in SIZE_T nCount, __in DWORD dwFlags);
DWORD RemoveHookCommon(__in LPVOID lpInfo, __in SIZE_T nCount, __in BOOL bDisable, __in DWORD dwFlags);
DWORD EnableHookCommon(__in LPVOID lpInfo, __in SIZE_T nCount, __in BOOL bEnable, __in DWORD dwFlags);
private:
LPVOID lpInternals;
};
//-----------------------------------------------------------
namespace NktHookLibHelpers {
//NOTE: See "BuildNtSysCalls" below
typedef struct tagSYSCALLDEF {
LPSTR szNtApiNameA;
SIZE_T nOffset;
} SYSCALLDEF, *LPSYSCALLDEF;
//--------------------------------
//NOTE: See "SetApiResolverCallback" below
typedef LPVOID (__stdcall *lpfnInternalApiResolver)(__in_z LPCSTR szApiNameA, __in LPVOID lpUserParam);
//--------------------------------
HINSTANCE GetModuleBaseAddress(__in_z LPCWSTR szDllNameW);
LPVOID GetProcedureAddress(__in HINSTANCE hDll, __in LPCSTR szProcNameA);
HINSTANCE GetRemoteModuleBaseAddress(__in HANDLE hProcess, __in_z LPCWSTR szDllNameW, __in BOOL bScanMappedImages);
LPVOID GetRemoteProcedureAddress(__in HANDLE hProcess, __in HINSTANCE hDll, __in_z LPCSTR szProcNameA);
//--------------------------------
int sprintf_s(__out_z char *lpDest, __in size_t nMaxCount, __in_z const char *szFormatA, ...);
int vsnprintf(__out_z char *lpDest, __in size_t nMaxCount, __in_z const char *szFormatA, __in va_list lpArgList);
//only on XP or later
int swprintf_s(__out_z wchar_t *lpDest, __in size_t nMaxCount, __in_z const wchar_t *szFormatW, ...);
int vsnwprintf(__out_z wchar_t *lpDest, __in size_t nMaxCount, __in_z const wchar_t *szFormatW, __in va_list lpArgList);
//--------------------------------
//Returns a PROCESSOR_ARCHITECTURE_xxx value or -1 on error.
LONG GetProcessorArchitecture();
HANDLE OpenProcess(__in DWORD dwDesiredAccess, __in BOOL bInheritHandle, __in DWORD dwProcessId);
HANDLE OpenThread(__in DWORD dwDesiredAccess, __in BOOL bInheritHandle, __in DWORD dwThreadId);
LONG GetProcessPlatform(__in HANDLE hProcess);
SIZE_T ReadMem(__in HANDLE hProcess, __out LPVOID lpDest, __in LPVOID lpSrc, __in SIZE_T nBytesCount);
BOOL WriteMem(__in HANDLE hProcess, __out LPVOID lpDest, __in LPVOID lpSrc, __in SIZE_T nBytesCount);
LONG GetThreadPriority(__in HANDLE hThread, __out int *lpnPriority);
LONG SetThreadPriority(__in HANDLE hThread, __in int nPriority);
DWORD GetCurrentThreadId();
DWORD GetCurrentProcessId();
HANDLE GetProcessHeap();
LPVOID MemAlloc(__in SIZE_T nSize);
VOID MemFree(__in LPVOID lpPtr);
VOID MemSet(__out void *lpDest, __in int nVal, __in SIZE_T nCount);
VOID MemCopy(__out void *lpDest, __in const void *lpSrc, __in SIZE_T nCount);
SIZE_T TryMemCopy(__out void *lpDest, __in const void *lpSrc, __in SIZE_T nCount);
VOID MemMove(__out void *lpDest, __in const void *lpSrc, __in SIZE_T nCount);
int MemCompare(__in const void *lpBuf1, __in const void *lpBuf2, __in SIZE_T nCount);
//--------------------------------
VOID DebugPrint(__in LPCSTR szFormatA, ...);
VOID DebugVPrint(__in LPCSTR szFormatA, __in va_list argptr);
//--------------------------------
SIZE_T GetInstructionLength(__in LPVOID lpAddr, __in SIZE_T nSize, __in BYTE nPlatformBits,
__out_opt BOOL *lpbIsMemOp=NULL, __out_z_opt LPSTR szBufA=NULL, __in SIZE_T nBufLen=0);
//NOTE: When NktHookLib is initialized, it tries to locate needed ntdll's apis by scanning process' modules.
// If you want to override an api call, use this method to set the resolver address. LPVOID returned by
// the callback must have the same definition and calling convention than the one in ntdll. Return NULL
// if you want NktHookLib to use the real ntdll api.
VOID SetInternalApiResolverCallback(__in lpfnInternalApiResolver fnInternalApiResolver, __in LPVOID lpUserParam);
//This function generates a relocatable byte code with a copy of the original SysCall routine for each passes
//ntdll api. The code is created based on the original ntdll.dll image file on disk located on System32 or SysWow64,
//depending on the target platform.
//
//Useful for doing direct calls to low level apis bypassing third party hooks (like Chrome navigator does). Also, you
//can pass the generated code to another process. You can generate the code with this method, pass it to another
//process and then use SetApiResolverCallback above.
//
//NOTE: Not all NtXXX apis are SysCalls. Trying to generate code for a non-syscall api may generate an unexpected
// behavior.
// If 'lpCode' is NULL, the needed space is returned. Although syscalls uses less than 32 bytes, a maximum of 256
// bytes are supported for each requested api. You can safety allocate a block of 256*nDefsCount bytes to hold
// the generated code.
DWORD BuildNtSysCalls(__in LPSYSCALLDEF lpDefs, __in SIZE_T nDefsCount, __in SIZE_T nPlatform,
__out_opt LPVOID lpCode, __out SIZE_T *lpnCodeSize);
//--------------------------------
//NOTE: Return 0xFFFFFFFF if remote thread is not accessible
DWORD GetWin32LastError(__in_opt HANDLE hThread=NULL);
BOOL SetWin32LastError(__in DWORD dwErrorCode, __in_opt HANDLE hThread=NULL);
//--------------------------------
BOOL GetOsVersion(__out_opt LPDWORD lpdwVerMajor=NULL, __out_opt LPDWORD lpdwVerMinor=NULL,
__out_opt LPDWORD lpdwBuildNumber=NULL);
//--------------------------------
//NOTE: CreateProcessWithDllW and related functions returns the Win32 error code directly. NOERROR => Success.
//
// If "szDllNameW" string ends with 'x86.dll', 'x64.dll', '32.dll', '64.dll', the dll name will be adjusted
// in order to match the process platform. I.e.: "mydll_x86.dll" will become "mydll_x64.dll" on 64-bit processes.
DWORD CreateProcessWithDllW(__in_z_opt LPCWSTR lpApplicationName, __inout_z_opt LPWSTR lpCommandLine,
__in_opt LPSECURITY_ATTRIBUTES lpProcessAttributes,
__in_opt LPSECURITY_ATTRIBUTES lpThreadAttributes, __in BOOL bInheritHandles,
__in DWORD dwCreationFlags, __in_z_opt LPCWSTR lpEnvironment,
__in_z_opt LPCWSTR lpCurrentDirectory, __in LPSTARTUPINFOW lpStartupInfo,
__out LPPROCESS_INFORMATION lpProcessInformation, __in_z LPCWSTR szDllNameW,
__in_opt HANDLE hSignalCompleted=NULL, __in_z_opt LPCSTR szInitFunctionA=NULL,
__in_opt LPVOID lpInitFuncParams=NULL, __in_opt ULONG nInitFuncParamsSize=0);
DWORD CreateProcessWithLogonAndDllW(__in_z LPCWSTR lpUsername, __in_z_opt LPCWSTR lpDomain, __in_z LPCWSTR lpPassword,
__in DWORD dwLogonFlags, __in_opt LPCWSTR lpApplicationName,
__inout_opt LPWSTR lpCommandLine, __in DWORD dwCreationFlags,
__in_z_opt LPCWSTR lpEnvironment, __in_z_opt LPCWSTR lpCurrentDirectory,
__in LPSTARTUPINFOW lpStartupInfo, __out LPPROCESS_INFORMATION lpProcessInformation,
__in_z LPCWSTR szDllNameW, __in_opt HANDLE hSignalCompleted=NULL,
__in_z_opt LPCSTR szInitFunctionA=NULL, __in_opt LPVOID lpInitFuncParams=NULL,
__in_opt ULONG nInitFuncParamsSize=0);
DWORD CreateProcessWithTokenAndDllW(__in HANDLE hToken, __in DWORD dwLogonFlags, __in_z_opt LPCWSTR lpApplicationName,
__inout_opt LPWSTR lpCommandLine, __in DWORD dwCreationFlags,
__in_z_opt LPCWSTR lpEnvironment, __in_z_opt LPCWSTR lpCurrentDirectory,
__in LPSTARTUPINFOW lpStartupInfo, __out LPPROCESS_INFORMATION lpProcessInformation,
__in_z LPCWSTR szDllNameW, __in_opt HANDLE hSignalCompleted=NULL,
__in_z_opt LPCSTR szInitFunctionA=NULL, __in_opt LPVOID lpInitFuncParams=NULL,
__in_opt ULONG nInitFuncParamsSize=0);
DWORD InjectDllByPidW(__in DWORD dwPid, __in_z LPCWSTR szDllNameW, __in_z_opt LPCSTR szInitFunctionA=NULL,
__in_opt DWORD dwProcessInitWaitTimeoutMs=5000, __out_opt LPHANDLE lphInjectorThread=NULL,
__in_opt LPVOID lpInitFuncParams=NULL, __in_opt ULONG nInitFuncParamsSize=0);
DWORD InjectDllByHandleW(__in HANDLE hProcess, __in_z LPCWSTR szDllNameW, __in_z_opt LPCSTR szInitFunctionA=NULL,
__in_opt DWORD dwProcessInitWaitTimeoutMs=5000, __out_opt LPHANDLE lphInjectorThread=NULL,
__in_opt LPVOID lpInitFuncParams=NULL, __in_opt ULONG nInitFuncParamsSize=0);
} //NktHookLibHelpers
//-----------------------------------------------------------
#endif //_NKTHOOKLIB
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+5 -1
View File
@@ -12,6 +12,9 @@ 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
win32 {
INCLUDEPATH += $$PWD/../libs/windows/include
}
@@ -21,6 +24,7 @@ macx {
unix:!macx {
CONFIG += link_pkgconfig
PKGCONFIG += openssl
DEFINES += HAVE_CLOCK_GETTIME=1
}
COMMON_C_DIR = $$PWD/moonlight-common-c
@@ -63,7 +67,7 @@ INCLUDEPATH += \
CONFIG += warn_off staticlib
DEFINES += HAS_SOCKLEN_T
debug {
CONFIG(debug, debug|release) {
# Enable asserts on debug builds
DEFINES += LC_DEBUG
}
+9 -2
View File
@@ -2,9 +2,16 @@ TEMPLATE = subdirs
SUBDIRS = \
moonlight-common-c \
qmdnsengine \
app
app \
soundio \
h264bitstream
CONFIG += ordered
# Build the dependencies in parallel before the final app
app.depends = qmdnsengine moonlight-common-c soundio h264bitstream
win32 {
SUBDIRS += AntiHooking
app.depends += AntiHooking
}
# Support debug and release builds from command line for CI
CONFIG += debug_and_release
+3
View File
@@ -7,6 +7,9 @@ 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
QMDNSE_DIR = $$PWD/qmdnsengine/src
DEFINES += \
QT_NO_SIGNALS_SLOTS_KEYWORDS
+6
View File
@@ -33,6 +33,12 @@ pushd $BUILD_FOLDER
make $(echo "$BUILD_CONFIG" | tr '[:upper:]' '[:lower:]') || fail "Make failed!"
popd
echo Saving dSYM file
pushd $BUILD_FOLDER
dsymutil app/Moonlight.app/Contents/MacOS/Moonlight -o Moonlight.dsym || fail "dSYM creation failed!"
cp -R Moonlight.dsym $INSTALLER_FOLDER || fail "dSYM copy failed!"
popd
echo Copying dylib dependencies
mkdir $BUILD_FOLDER/app/Moonlight.app/Contents/lib
cp $SOURCE_ROOT/libs/mac/lib/*.dylib $BUILD_FOLDER/app/Moonlight.app/Contents/lib/ || fail "Dylib copy failed!"
+64 -16
View File
@@ -16,6 +16,7 @@ if /I "%BUILD_CONFIG%"=="debug" (
if /I "%BUILD_CONFIG%"=="signed-release" (
set BUILD_CONFIG=release
set SIGN=1
set MUST_DEPLOY_SYMBOLS=1
) else (
echo Invalid build configuration
exit /b 1
@@ -31,7 +32,6 @@ if /I "%ARCH%" NEQ "x86" (
)
set VS_PATH=%ProgramFiles(x86)%\Microsoft Visual Studio\2017\Community
set VCREDIST_PATH=%VS_PATH%\VC\Redist\MSVC\14.14.26405
set SIGNTOOL_PARAMS=sign /tr http://timestamp.digicert.com /td sha256 /fd sha256 /sha1 1B3C676E831A94EC0327C3347EB32C68C26B3A67 /v
call "%VS_PATH%\VC\Auxiliary\Build\vcvarsall.bat" %ARCH%
@@ -42,15 +42,18 @@ set SOURCE_ROOT=%cd%
set BUILD_FOLDER=%BUILD_ROOT%\build-%ARCH%-%BUILD_CONFIG%
set DEPLOY_FOLDER=%BUILD_ROOT%\deploy-%ARCH%-%BUILD_CONFIG%
set INSTALLER_FOLDER=%BUILD_ROOT%\installer-%ARCH%-%BUILD_CONFIG%
set SYMBOLS_FOLDER=%BUILD_ROOT%\symbols-%ARCH%-%BUILD_CONFIG%
echo Cleaning output directories
rmdir /s /q %DEPLOY_FOLDER%
rmdir /s /q %BUILD_FOLDER%
rmdir /s /q %INSTALLER_FOLDER%
rmdir /s /q %SYMBOLS_FOLDER%
mkdir %BUILD_ROOT%
mkdir %DEPLOY_FOLDER%
mkdir %BUILD_FOLDER%
mkdir %INSTALLER_FOLDER%
mkdir %SYMBOLS_FOLDER%
echo Configuring the project
pushd %BUILD_FOLDER%
@@ -64,10 +67,39 @@ nmake %BUILD_CONFIG%
if !ERRORLEVEL! NEQ 0 goto Error
popd
echo Saving PDBs
for /r "%BUILD_FOLDER%" %%f in (*.pdb) do (
copy "%%f" %SYMBOLS_FOLDER%
if !ERRORLEVEL! NEQ 0 goto Error
)
copy %SOURCE_ROOT%\libs\windows\lib\%ARCH%\*.pdb %SYMBOLS_FOLDER%
if !ERRORLEVEL! NEQ 0 goto Error
7z a %SYMBOLS_FOLDER%\MoonlightDebuggingSymbols.zip %SYMBOLS_FOLDER%\*.pdb
if !ERRORLEVEL! NEQ 0 goto Error
if "%ML_SYMBOL_STORE%" NEQ "" (
echo Publishing PDBs to symbol store: %ML_SYMBOL_STORE%
symstore add /f %SYMBOLS_FOLDER%\*.pdb /s %ML_SYMBOL_STORE% /t Moonlight
if !ERRORLEVEL! NEQ 0 goto Error
) else (
if "%MUST_DEPLOY_SYMBOLS%"=="1" (
echo "A symbol server must be specified in ML_SYMBOL_STORE for signed release builds"
exit /b 1
)
)
echo Copying DLL dependencies
copy %SOURCE_ROOT%\libs\windows\lib\%ARCH%\*.dll %DEPLOY_FOLDER%
if !ERRORLEVEL! NEQ 0 goto Error
echo Copying AntiHooking.dll
copy %BUILD_FOLDER%\AntiHooking\%BUILD_CONFIG%\AntiHooking.dll %DEPLOY_FOLDER%
if !ERRORLEVEL! NEQ 0 goto Error
echo Copying GC mapping list
copy %SOURCE_ROOT%\app\SDL_GameControllerDB\gamecontrollerdb.txt %DEPLOY_FOLDER%
if !ERRORLEVEL! NEQ 0 goto Error
echo Deploying Qt dependencies
windeployqt.exe --dir %DEPLOY_FOLDER% --%BUILD_CONFIG% --qmldir %SOURCE_ROOT%\app\gui --no-opengl-sw --no-compiler-runtime %BUILD_FOLDER%\app\%BUILD_CONFIG%\Moonlight.exe
if !ERRORLEVEL! NEQ 0 goto Error
@@ -82,27 +114,32 @@ if !ERRORLEVEL! NEQ 0 goto Error
if "%SIGN%"=="1" (
echo Signing deployed binaries
set FILES_TO_SIGN=
for /r "%DEPLOY_FOLDER%" %%f in (*.dll *.exe) do (
signtool %SIGNTOOL_PARAMS% %%f
if !ERRORLEVEL! NEQ 0 goto Error
set FILES_TO_SIGN=!FILES_TO_SIGN! %%f
)
signtool %SIGNTOOL_PARAMS% !FILES_TO_SIGN!
if !ERRORLEVEL! NEQ 0 goto Error
)
echo Building installer
set VCREDIST_INSTALLER=%VCREDIST_PATH%\vcredist_%ARCH%.exe
msbuild %SOURCE_ROOT%\wix\Moonlight.sln /p:Configuration=%BUILD_CONFIG% /p:Platform=%ARCH%
if !ERRORLEVEL! NEQ 0 goto Error
echo Building portable package
rem This must be done after WiX harvesting and signing, since the VCRT dlls are MS signed
rem and should not be harvested for inclusion in the full installer
copy "%VCREDIST_PATH%\%ARCH%\Microsoft.VC141.CRT\*.dll" %DEPLOY_FOLDER%
if !ERRORLEVEL! NEQ 0 goto Error
7z a %INSTALLER_FOLDER%\MoonlightPortable.zip %DEPLOY_FOLDER%\*
echo Building MSI
msbuild %SOURCE_ROOT%\wix\Moonlight\Moonlight.wixproj /p:Configuration=%BUILD_CONFIG% /p:Platform=%ARCH%
if !ERRORLEVEL! NEQ 0 goto Error
if "%SIGN%"=="1" (
echo Signing installer binary
echo Signing MSI
signtool %SIGNTOOL_PARAMS% %BUILD_FOLDER%\Moonlight.msi
if !ERRORLEVEL! NEQ 0 goto Error
)
echo Building bundle
set VCREDIST_INSTALLER=%VCToolsRedistDir%vcredist_%ARCH%.exe
rem Bundles are always x86 binaries
msbuild %SOURCE_ROOT%\wix\MoonlightSetup\MoonlightSetup.wixproj /p:Configuration=%BUILD_CONFIG% /p:Platform=x86
if !ERRORLEVEL! NEQ 0 goto Error
if "%SIGN%"=="1" (
echo Signing bundle
"%WIX%\bin\insignia" -ib %INSTALLER_FOLDER%\MoonlightSetup.exe -o %BUILD_FOLDER%\engine.exe
if !ERRORLEVEL! NEQ 0 goto Error
signtool %SIGNTOOL_PARAMS% %BUILD_FOLDER%\engine.exe
@@ -113,12 +150,23 @@ if "%SIGN%"=="1" (
if !ERRORLEVEL! NEQ 0 goto Error
)
echo Building portable package
rem This must be done after WiX harvesting and signing, since the VCRT dlls are MS signed
rem and should not be harvested for inclusion in the full installer
copy "%VCToolsRedistDir%%ARCH%\Microsoft.VC141.CRT\*.dll" %DEPLOY_FOLDER%
rem This file tells Moonlight that it's a portable installation
echo. > %DEPLOY_FOLDER%\portable.dat
if !ERRORLEVEL! NEQ 0 goto Error
7z a %INSTALLER_FOLDER%\MoonlightPortable.zip %DEPLOY_FOLDER%\*
if !ERRORLEVEL! NEQ 0 goto Error
if /i "%APPVEYOR%"=="true" (
echo Pushing artifacts
appveyor PushArtifact %INSTALLER_FOLDER%\MoonlightSetup.exe -FileName MoonlightSetup-%ARCH%-%BUILD_CONFIG%.exe
if !ERRORLEVEL! NEQ 0 goto Error
appveyor PushArtifact %INSTALLER_FOLDER%\MoonlightPortable.zip -FileName MoonlightPortable-%ARCH%-%BUILD_CONFIG%.zip
if !ERRORLEVEL! NEQ 0 goto Error
appveyor PushArtifact %SYMBOLS_FOLDER%\MoonlightDebuggingSymbols.zip -FileName MoonlightDebuggingSymbols-%ARCH%-%BUILD_CONFIG%.zip
)
echo Build successful!
@@ -126,4 +174,4 @@ exit /b 0
:Error
echo Build failed!
exit /b !ERRORLEVEL!
exit /b !ERRORLEVEL!
View File
Submodule soundio/libsoundio added at 5d6a7210fa
+111
View File
@@ -0,0 +1,111 @@
#-------------------------------------------------
#
# Project created by QtCreator 2018-10-02T15:27:07
#
#-------------------------------------------------
QT -= core gui
TARGET = soundio
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
# Force MSVC to compile C as C++ for atomic support
*-msvc {
QMAKE_CFLAGS += /TP
}
# Older GCC versions defaulted to GNU89
*-g++ {
QMAKE_CFLAGS += -std=gnu99
}
# Disable warnings
CONFIG += warn_off
unix:!macx {
CONFIG += link_pkgconfig
packagesExist(libpulse) {
PKGCONFIG += libpulse
CONFIG += pulseaudio
}
packagesExist(alsa) {
PKGCONFIG += alsa
CONFIG += alsa
}
}
CONFIG(release, debug|release) {
# Disable asserts on release builds
DEFINES += NDEBUG
}
DEFINES += \
SOUNDIO_STATIC_LIBRARY \
SOUNDIO_VERSION_MAJOR=1 \
SOUNDIO_VERSION_MINOR=1 \
SOUNDIO_VERSION_PATCH=0 \
SOUNDIO_VERSION_STRING=\\\"1.1.0\\\"
SRC_DIR = $$PWD/libsoundio/src
INC_DIR = $$PWD/libsoundio
SOURCES += \
$$SRC_DIR/channel_layout.c \
$$SRC_DIR/dummy.c \
$$SRC_DIR/os.c \
$$SRC_DIR/ring_buffer.c \
$$SRC_DIR/soundio.c \
$$SRC_DIR/util.c
HEADERS += \
$$SRC_DIR/atomics.h \
$$SRC_DIR/dummy.h \
$$SRC_DIR/list.h \
$$SRC_DIR/os.h \
$$SRC_DIR/ring_buffer.h \
$$SRC_DIR/soundio_internal.h \
$$SRC_DIR/soundio_private.h \
$$SRC_DIR/util.h \
$$INC_DIR/soundio/soundio.h \
$$INC_DIR/soundio/endian.h
INCLUDEPATH += $$INC_DIR
win32 {
message(WASAPI backend selected)
DEFINES += SOUNDIO_HAVE_WASAPI
SOURCES += $$SRC_DIR/wasapi.c
HEADERS += $$SRC_DIR/wasapi.h
}
macx {
message(CoreAudio backend selected)
DEFINES += SOUNDIO_HAVE_COREAUDIO
SOURCES += $$SRC_DIR/coreaudio.c
HEADERS += $$SRC_DIR/coreaudio.h
}
pulseaudio {
message(PulseAudio backend selected)
DEFINES += SOUNDIO_HAVE_PULSEAUDIO
SOURCES += $$SRC_DIR/pulseaudio.c
HEADERS += $$SRC_DIR/pulseaudio.h
}
alsa {
message(ALSA backend selected)
DEFINES += SOUNDIO_HAVE_ALSA
SOURCES += $$SRC_DIR/alsa.c
HEADERS += $$SRC_DIR/alsa.h
}
+5
View File
@@ -6,6 +6,7 @@
<SchemaVersion>2.0</SchemaVersion>
<OutputName>Moonlight</OutputName>
<OutputType>Package</OutputType>
<DefineSolutionProperties>false</DefineSolutionProperties>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
<DefineConstants>Debug</DefineConstants>
@@ -26,6 +27,10 @@
<HintPath>$(WixExtDir)\WixFirewallExtension.dll</HintPath>
<Name>WixFirewallExtension</Name>
</WixExtension>
<WixExtension Include="WixUtilExtension">
<HintPath>$(WixExtDir)\WixUtilExtension.dll</HintPath>
<Name>WixUtilExtension</Name>
</WixExtension>
</ItemGroup>
<Import Project="$(WixTargetsPath)" Condition=" '$(WixTargetsPath)' != '' " />
<Import Project="$(MSBuildExtensionsPath32)\Microsoft\WiX\v3.x\Wix.targets" Condition=" '$(WixTargetsPath)' == '' AND Exists('$(MSBuildExtensionsPath32)\Microsoft\WiX\v3.x\Wix.targets') " />
+33 -5
View File
@@ -4,11 +4,12 @@
<?define FullName = "Moonlight Game Streaming Client" ?>
<?define ShortcutName = "$(var.ShortName)" ?>
<?define ShortcutDesc = "Stream games from a NVIDIA GameStream-compatible PC" ?>
<?define ShortcutDesc = "Stream games from your NVIDIA GameStream-enabled PC" ?>
<?define InstallFolder = "Moonlight Game Streaming" ?>
<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi"
xmlns:fire="http://schemas.microsoft.com/wix/FirewallExtension">
xmlns:fire="http://schemas.microsoft.com/wix/FirewallExtension"
xmlns:util="http://schemas.microsoft.com/wix/UtilExtension">
<Product Id="*"
Name="$(var.FullName)"
@@ -20,7 +21,7 @@
<Package InstallerVersion="500" Compressed="yes" InstallScope="perMachine" />
<MajorUpgrade DowngradeErrorMessage="A newer version of [ProductName] is already installed." Schedule="afterInstallInitialize" />
<MediaTemplate CompressionLevel="high" />
<MediaTemplate CompressionLevel="high" EmbedCab="yes" />
<?if $(var.Platform) = x64 ?>
<?define PlatformProgramFilesFolder = "ProgramFiles64Folder" ?>
@@ -38,6 +39,8 @@
</Directory>
</Directory>
<Property Id="APPDATAFOLDER">%LOCALAPPDATA%\Moonlight Game Streaming Project</Property>
<!-- There's no way to delete a registry key on uninstall but not major upgrade, so
we have to roll our own deletion via custom action -->
<CustomAction Id="DeleteRegistryKey"
@@ -57,15 +60,38 @@
Target="[#MoonlightExe]"
Directory="ApplicationProgramsFolder"
WorkingDirectory="INSTALLFOLDER" />
<RemoveFolder Id="CleanupStartMenuShortcut" Directory="ApplicationProgramsFolder" On="uninstall" />
<util:RemoveFolderEx Id="CleanupAppDataFolder" On="uninstall" Property="APPDATAFOLDER" />
<RegistryValue Root="HKCU" Key="Software\Moonlight Game Streaming Project" Name="Installed" Type="integer" Value="1" KeyPath="yes" />
</Component>
<Component Id="MoonlightDesktopShortcut" Guid="*" Directory="INSTALLFOLDER">
<Shortcut Id="DesktopShortcut"
Name="$(var.ShortcutName)"
Description="$(var.ShortcutDesc)"
Target="[#MoonlightExe]"
Directory="DesktopFolder"
WorkingDirectory="INSTALLFOLDER" />
<RemoveFolder Id="CleanupStartMenuShortcut" Directory="ApplicationProgramsFolder" On="uninstall" />
<RemoveFolder Id="CleanupDesktopShortcut" Directory="DesktopFolder" On="uninstall" />
<RegistryValue Root="HKCU" Key="Software\Moonlight Game Streaming Project" Name="Installed" Type="integer" Value="1" KeyPath="yes" />
<RegistryValue Root="HKCU"
Key="Software\Moonlight Game Streaming Project"
Name="DesktopShortcutInstalled"
Type="integer"
Value="1"
KeyPath="yes" />
<Condition>ADDDESKTOPSHORTCUT=1</Condition>
</Component>
<!-- Persist desktop shortcut's installed state to let Bundle.wxs know if
the desktop shortcut should installed by default when upgrading the
product -->
<Component Id="MoonlightDesktopShortcutState" Guid="*" Directory="INSTALLFOLDER">
<RegistryValue Root="HKCU"
Key="Software\Moonlight Game Streaming Project"
Name="DesktopShortcutInstallState"
Type="integer"
Value="[ADDDESKTOPSHORTCUT]"
KeyPath="yes" />
</Component>
<DirectoryRef Id="INSTALLFOLDER">
@@ -81,6 +107,8 @@
<Feature Id="ProductFeature" Title="Moonlight" Level="1" ConfigurableDirectory="INSTALLFOLDER">
<ComponentRef Id="Moonlight" />
<ComponentRef Id="MoonlightShortcuts" />
<ComponentRef Id="MoonlightDesktopShortcutState" />
<ComponentRef Id="MoonlightDesktopShortcut" />
<ComponentGroupRef Id="MoonlightDependencies" />
</Feature>
</Product>
+21 -1
View File
@@ -51,13 +51,32 @@
<Variable Name="InstallFolder" Type="string" Value="[ProgramFilesFolder]Moonlight Game Streaming" />
<?endif ?>
<!-- Define "Add desktop shortcut" -checkbox's state by defining a variable
which has same name as the checkbox has. Value 1 means that checkbox
is checked, 0 means that is unchecked-->
<!-- Set checkbox's state as checked by default -->
<Variable Name="AddDesktopShortcutCheckbox" Type="numeric" Value="1" />
<!-- Get checkbox's state from registry if present. The registry value
"DesktopShortcutInstallState" is set in Product.wxs. -->
<util:RegistrySearch Variable="HasDesktopShortcutInstallStateRegKey"
Root="HKCU"
Key="Software\Moonlight Game Streaming Project"
Value="DesktopShortcutInstallState"
Result="exists" />
<util:RegistrySearch Variable="AddDesktopShortcutCheckbox"
Root="HKCU"
Key="Software\Moonlight Game Streaming Project"
Value="DesktopShortcutInstallState"
Condition="HasDesktopShortcutInstallStateRegKey" />
<BootstrapperApplicationRef Id="WixStandardBootstrapperApplication.RtfLicense">
<bal:WixStandardBootstrapperApplication
ShowVersion="yes"
LicenseFile="license.rtf"
LogoFile="..\..\app\moonlight_wix.png"
ShowFilesInUse="yes"
LaunchTarget="[InstallFolder]\Moonlight.exe" />
LaunchTarget="[InstallFolder]\Moonlight.exe"
ThemeFile="RtfTheme.xml" />
</BootstrapperApplicationRef>
<Chain>
@@ -75,6 +94,7 @@
<MsiPackage Id="Moonlight" SourceFile="$(var.Moonlight.TargetPath)" Vital="yes">
<MsiProperty Name="INSTALLFOLDER" Value="[InstallFolder]" />
<MsiProperty Name="REINSTALLMODE" Value="[REINSTALLMODE]" />
<MsiProperty Name="ADDDESKTOPSHORTCUT" Value="[AddDesktopShortcutCheckbox]" />
</MsiPackage>
</Chain>
@@ -6,6 +6,7 @@
<SchemaVersion>2.0</SchemaVersion>
<OutputName>MoonlightSetup</OutputName>
<OutputType>Bundle</OutputType>
<DefineSolutionProperties>false</DefineSolutionProperties>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
<DefineConstants>Debug</DefineConstants>
+88
View File
@@ -0,0 +1,88 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information. -->
<!-- This file has been copied from C:\Program Files (x86)\WiX Toolset v3.11\SDK\themes\RtfTheme.xml and then altered. -->
<Theme xmlns="http://wixtoolset.org/schemas/thmutil/2010">
<Window Width="485" Height="300" HexStyle="100a0000" FontId="0">#(loc.Caption)</Window>
<Font Id="0" Height="-12" Weight="500" Foreground="000000" Background="FFFFFF">Segoe UI</Font>
<Font Id="1" Height="-24" Weight="500" Foreground="000000">Segoe UI</Font>
<Font Id="2" Height="-22" Weight="500" Foreground="666666">Segoe UI</Font>
<Font Id="3" Height="-12" Weight="500" Foreground="000000" Background="FFFFFF">Segoe UI</Font>
<Font Id="4" Height="-12" Weight="500" Foreground="ff0000" Background="FFFFFF" Underline="yes">Segoe UI</Font>
<Image X="11" Y="11" Width="64" Height="64" ImageFile="logo.png" Visible="yes"/>
<Text X="80" Y="11" Width="-11" Height="64" FontId="1" Visible="yes" DisablePrefix="yes">#(loc.Title)</Text>
<Page Name="Help">
<Text X="11" Y="80" Width="-11" Height="30" FontId="2" DisablePrefix="yes">#(loc.HelpHeader)</Text>
<Text X="11" Y="112" Width="-11" Height="-35" FontId="3" DisablePrefix="yes">#(loc.HelpText)</Text>
<Button Name="HelpCancelButton" X="-11" Y="-11" Width="75" Height="23" TabStop="yes" FontId="0">#(loc.HelpCloseButton)</Button>
</Page>
<Page Name="Install">
<Richedit Name="EulaRichedit" X="11" Y="80" Width="-11" Height="-70" TabStop="yes" FontId="0" HexStyle="0x800000" />
<Checkbox Name="EulaAcceptCheckbox" X="-11" Y="-41" Width="260" Height="17" TabStop="yes" FontId="3" HideWhenDisabled="yes">#(loc.InstallAcceptCheckbox)</Checkbox>
<Button Name="OptionsButton" X="-171" Y="-11" Width="75" Height="23" TabStop="yes" FontId="0" HideWhenDisabled="yes">#(loc.InstallOptionsButton)</Button>
<Button Name="InstallButton" X="-91" Y="-11" Width="75" Height="23" TabStop="yes" FontId="0">#(loc.InstallInstallButton)</Button>
<Button Name="WelcomeCancelButton" X="-11" Y="-11" Width="75" Height="23" TabStop="yes" FontId="0">#(loc.InstallCloseButton)</Button>
</Page>
<Page Name="Options">
<Text X="11" Y="80" Width="-11" Height="30" FontId="2" DisablePrefix="yes">#(loc.OptionsHeader)</Text>
<Text X="11" Y="121" Width="-11" Height="17" FontId="3" DisablePrefix="yes">#(loc.OptionsLocationLabel)</Text>
<Editbox Name="FolderEditbox" X="11" Y="143" Width="-91" Height="21" TabStop="yes" FontId="3" FileSystemAutoComplete="yes" />
<Button Name="BrowseButton" X="-11" Y="142" Width="75" Height="23" TabStop="yes" FontId="3">#(loc.OptionsBrowseButton)</Button>
<!-- Added a checkbox for user to choose if shortcut is placed on
desktop or not. Checkbox's current state (checked/unchecked) is
read from a variable which has same name as this checkbox. The
variable is defined in Bundle.wxs. -->
<Checkbox Name="AddDesktopShortcutCheckbox" X="11" Y="178" Width="250" Height="17" TabStop="yes" FontId="3" HideWhenDisabled="yes">Add desktop shortcut</Checkbox>
<Button Name="OptionsOkButton" X="-91" Y="-11" Width="75" Height="23" TabStop="yes" FontId="0">#(loc.OptionsOkButton)</Button>
<Button Name="OptionsCancelButton" X="-11" Y="-11" Width="75" Height="23" TabStop="yes" FontId="0">#(loc.OptionsCancelButton)</Button>
</Page>
<Page Name="FilesInUse">
<Text X="11" Y="80" Width="-11" Height="30" FontId="2" DisablePrefix="yes">#(loc.FilesInUseHeader)</Text>
<Text X="11" Y="121" Width="-11" Height="34" FontId="3" DisablePrefix="yes">#(loc.FilesInUseLabel)</Text>
<Text Name="FilesInUseText" X="11" Y="150" Width="-11" Height="-86" FontId="3" DisablePrefix="yes" HexStyle="0x0000C000"></Text>
<Button Name="FilesInUseCloseRadioButton" X="11" Y="-60" Width="-11" Height="23" TabStop="yes" FontId="0" HideWhenDisabled="yes" HexStyle="0x000009">#(loc.FilesInUseCloseRadioButton)</Button>
<Button Name="FilesInUseDontCloseRadioButton" X="11" Y="-40" Width="-11" Height="23" TabStop="yes" FontId="0" HideWhenDisabled="yes" HexStyle="0x000009">#(loc.FilesInUseDontCloseRadioButton)</Button>
<Button Name="FilesInUseOkButton" X="-91" Y="-11" Width="75" Height="23" TabStop="yes" FontId="0" HideWhenDisabled="yes">#(loc.FilesInUseOkButton)</Button>
<Button Name="FilesInUseCancelButton" X="-11" Y="-11" Width="75" Height="23" TabStop="yes" FontId="0">#(loc.FilesInUseCancelButton)</Button>
</Page>
<Page Name="Progress">
<Text X="11" Y="80" Width="-11" Height="30" FontId="2" DisablePrefix="yes">#(loc.ProgressHeader)</Text>
<Text X="11" Y="121" Width="70" Height="17" FontId="3" DisablePrefix="yes">#(loc.ProgressLabel)</Text>
<Text Name="OverallProgressPackageText" X="85" Y="121" Width="-11" Height="17" FontId="3" DisablePrefix="yes">#(loc.OverallProgressPackageText)</Text>
<Progressbar Name="OverallCalculatedProgressbar" X="11" Y="143" Width="-11" Height="15" />
<Button Name="ProgressCancelButton" X="-11" Y="-11" Width="75" Height="23" TabStop="yes" FontId="0">#(loc.ProgressCancelButton)</Button>
</Page>
<Page Name="Modify">
<Text X="11" Y="80" Width="-11" Height="30" FontId="2" DisablePrefix="yes">#(loc.ModifyHeader)</Text>
<Button Name="RepairButton" X="-171" Y="-11" Width="75" Height="23" TabStop="yes" FontId="0" HideWhenDisabled="yes">#(loc.ModifyRepairButton)</Button>
<Button Name="UninstallButton" X="-91" Y="-11" Width="75" Height="23" TabStop="yes" FontId="0">#(loc.ModifyUninstallButton)</Button>
<Button Name="ModifyCancelButton" X="-11" Y="-11" Width="75" Height="23" TabStop="yes" FontId="0">#(loc.ModifyCloseButton)</Button>
</Page>
<Page Name="Success">
<Text Name="SuccessHeader" X="11" Y="80" Width="-11" Height="30" FontId="2" HideWhenDisabled="yes" DisablePrefix="yes">#(loc.SuccessHeader)</Text>
<Text Name="SuccessInstallHeader" X="11" Y="80" Width="-11" Height="30" FontId="2" HideWhenDisabled="yes" DisablePrefix="yes">#(loc.SuccessInstallHeader)</Text>
<Text Name="SuccessRepairHeader" X="11" Y="80" Width="-11" Height="30" FontId="2" HideWhenDisabled="yes" DisablePrefix="yes">#(loc.SuccessRepairHeader)</Text>
<Text Name="SuccessUninstallHeader" X="11" Y="80" Width="-11" Height="30" FontId="2" HideWhenDisabled="yes" DisablePrefix="yes">#(loc.SuccessUninstallHeader)</Text>
<Button Name="LaunchButton" X="-91" Y="-11" Width="75" Height="23" TabStop="yes" FontId="0" HideWhenDisabled="yes">#(loc.SuccessLaunchButton)</Button>
<Text Name="SuccessRestartText" X="-11" Y="-51" Width="400" Height="34" FontId="3" HideWhenDisabled="yes" DisablePrefix="yes">#(loc.SuccessRestartText)</Text>
<Button Name="SuccessRestartButton" X="-91" Y="-11" Width="75" Height="23" TabStop="yes" FontId="0" HideWhenDisabled="yes">#(loc.SuccessRestartButton)</Button>
<Button Name="SuccessCancelButton" X="-11" Y="-11" Width="75" Height="23" TabStop="yes" FontId="0">#(loc.SuccessCloseButton)</Button>
</Page>
<Page Name="Failure">
<Text Name="FailureHeader" X="11" Y="80" Width="-11" Height="30" FontId="2" HideWhenDisabled="yes" DisablePrefix="yes">#(loc.FailureHeader)</Text>
<Text Name="FailureInstallHeader" X="11" Y="80" Width="-11" Height="30" FontId="2" HideWhenDisabled="yes" DisablePrefix="yes">#(loc.FailureInstallHeader)</Text>
<Text Name="FailureUninstallHeader" X="11" Y="80" Width="-11" Height="30" FontId="2" HideWhenDisabled="yes" DisablePrefix="yes">#(loc.FailureUninstallHeader)</Text>
<Text Name="FailureRepairHeader" X="11" Y="80" Width="-11" Height="30" FontId="2" HideWhenDisabled="yes" DisablePrefix="yes">#(loc.FailureRepairHeader)</Text>
<Hypertext Name="FailureLogFileLink" X="11" Y="121" Width="-11" Height="42" FontId="3" TabStop="yes" HideWhenDisabled="yes">#(loc.FailureHyperlinkLogText)</Hypertext>
<Hypertext Name="FailureMessageText" X="22" Y="163" Width="-11" Height="51" FontId="3" TabStop="yes" HideWhenDisabled="yes" />
<Text Name="FailureRestartText" X="-11" Y="-51" Width="400" Height="34" FontId="3" HideWhenDisabled="yes" DisablePrefix="yes">#(loc.FailureRestartText)</Text>
<Button Name="FailureRestartButton" X="-91" Y="-11" Width="75" Height="23" TabStop="yes" FontId="0" HideWhenDisabled="yes">#(loc.FailureRestartButton)</Button>
<Button Name="FailureCloseButton" X="-11" Y="-11" Width="75" Height="23" TabStop="yes" FontId="0">#(loc.FailureCloseButton)</Button>
</Page>
</Theme>