Compare commits

...
171 Commits
Author SHA1 Message Date
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
92 changed files with 6120 additions and 1731 deletions
+6
View File
@@ -4,3 +4,9 @@
[submodule "qmdnsengine/qmdnsengine"]
path = qmdnsengine/qmdnsengine
url = https://github.com/cgutman/qmdnsengine.git
[submodule "app/SDL_GameControllerDB"]
path = app/SDL_GameControllerDB
url = https://github.com/gabomdq/SDL_GameControllerDB.git
[submodule "soundio/libsoundio"]
path = soundio/libsoundio
url = https://github.com/andrewrk/libsoundio.git
+1
View File
@@ -20,6 +20,7 @@ matrix:
- qt59base
- qt59quickcontrols2
- qt59svg
- qt59multimedia
- libesd0-dev
- libgl-dev
- libegl1-mesa-dev
+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.3.0</string>
<string>0.6.0</string>
<key>CFBundleShortVersionString</key>
<string>0.3.0</string>
<string>0.6.0</string>
<key>CFBundleDisplayName</key>
<string>Moonlight</string>
</dict>
+78 -18
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 gdi32.lib user32.lib d3d9.lib dwmapi.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,47 +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 \
path.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 \
path.h
path.h \
settings/mappingmanager.h \
gui/sdlgamepadkeynavigation.h
# Platform-specific renderers and decoders
ffmpeg {
@@ -115,7 +150,7 @@ 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
@@ -158,6 +193,7 @@ config_SLVideo {
DEFINES += HAVE_SLVIDEO
LIBS += -lSLVideo
SOURCES += streaming/video/sl.cpp
HEADERS += streaming/video/sl.h
}
@@ -177,12 +213,19 @@ macx {
SOURCES += \
streaming/video/ffmpeg-renderers/vt.mm \
streaming/video/ffmpeg-renderers/pacer/displaylinkvsyncsource.cpp
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 += \
resources.qrc \
@@ -208,6 +251,13 @@ 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
unix:!macx: {
isEmpty(PREFIX) {
PREFIX = /usr/local
@@ -215,32 +265,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.3.0
DEFINES += VERSION_STR=\\\"0.3.0\\\"
VERSION = 0.6.0
DEFINES += VERSION_STR=\\\"0.6.0\\\"
+43 -2
View File
@@ -6,8 +6,14 @@
BoxArtManager::BoxArtManager(QObject *parent) :
QObject(parent),
m_BoxArtDir(Path::getBoxArtCacheDir())
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(".");
}
@@ -30,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
@@ -41,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.
@@ -75,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;
};
+21 -14
View File
@@ -29,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());
@@ -58,7 +64,7 @@ NvHTTP::getCurrentGame(QString serverInfo)
}
QString
NvHTTP::getServerInfo()
NvHTTP::getServerInfo(NvLogLevel logLevel)
{
QString serverInfo;
@@ -70,7 +76,7 @@ NvHTTP::getServerInfo()
"serverinfo",
nullptr,
true,
true);
logLevel);
// Throws if the request failed
verifyResponseStatus(serverInfo);
}
@@ -83,7 +89,7 @@ NvHTTP::getServerInfo()
"serverinfo",
nullptr,
true,
true);
logLevel);
verifyResponseStatus(serverInfo);
}
else
@@ -189,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, "");
@@ -230,7 +236,7 @@ NvHTTP::getAppList()
"applist",
nullptr,
true,
true);
NvLogLevel::ERROR);
verifyResponseStatus(appxml);
QXmlStreamReader xmlReader(appxml);
@@ -297,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;
@@ -344,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);
@@ -362,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);
@@ -388,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);
@@ -396,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();
@@ -409,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,66 @@
</screenshots>
<releases>
<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>
+75 -49
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,12 +31,23 @@ GridView {
stackView.pop()
}
Component.onCompleted: {
// Don't show any highlighted item until interacting with them
currentIndex = -1
}
SdlGamepadKeyNavigation {
id: gamepadKeyNav
}
onVisibleChanged: {
if (visible) {
appModel.computerLost.connect(computerLost)
gamepadKeyNav.enable()
}
else {
appModel.computerLost.disconnect(computerLost)
gamepadKeyNav.disable()
}
}
@@ -52,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 {
@@ -95,6 +108,8 @@ GridView {
if (runningIndex >= 0 && runningIndex !== index) {
quitAppDialog.appName = appModel.getRunningAppName()
quitAppDialog.segueToStream = true
quitAppDialog.nextAppName = model.name
quitAppDialog.nextAppIndex = index
quitAppDialog.open()
return
}
@@ -104,60 +119,36 @@ GridView {
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 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 = model.name
params.nextSession = appModel.createSessionForApp(index)
}
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()
}
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()
@@ -165,11 +156,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.11
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
font: popup.font
}
TextMetrics {
id: textMetrics
font: parent.font
}
// We call this every time the options change (and init)
// so we can adjust the combo box width here too
onActivated: {
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)
}
}
+81 -76
View File
@@ -1,6 +1,6 @@
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
@@ -8,18 +8,20 @@ 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
@@ -31,16 +33,25 @@ GridView {
id: prefs
}
SdlGamepadKeyNavigation {
id: gamepadKeyNav
}
onVisibleChanged: {
if (visible) {
gamepadKeyNav.enable()
}
else {
gamepadKeyNav.disable()
}
}
Component.onCompleted: {
// Setup signals on CM
ComputerManager.computerAddCompleted.connect(addComplete)
if (prefs.isRunningWayland()) {
waylandDialog.open()
}
else if (!prefs.hasAnyHardwareAcceleration()) {
noHwDecoderDialog.open()
}
// Don't show any highlighted item until interacting with them
currentIndex = -1
}
function pairingComplete(error)
@@ -49,7 +60,7 @@ GridView {
pairDialog.close()
// Display a failed dialog if we got an error
if (error !== null) {
if (error !== undefined) {
errorDialog.text = error
errorDialog.open()
}
@@ -58,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()
}
}
@@ -73,8 +84,9 @@ GridView {
model: computerModel
delegate: Item {
delegate: NavigableItemDelegate {
width: 300; height: 300;
grid: pcGrid
Image {
id: pcIcon
@@ -126,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
@@ -142,50 +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, {"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) {
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 {
@@ -193,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");
}
}
@@ -240,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 {
+1 -1
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
+248 -97
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,26 +304,59 @@ ScrollView {
}
}
Row {
CheckBox {
id: fullScreenCheck
text: "<font color=\"white\">Full-screen</font>"
font.pointSize: 12
checked: prefs.fullScreen
onCheckedChanged: {
prefs.fullScreen = checked
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)
}
CheckBox {
id: vsyncCheck
text: "<font color=\"white\">Enable V-Sync</font>"
font.pointSize: 12
visible: fullScreenCheck.checked
checked: prefs.enableVsync
onCheckedChanged: {
prefs.enableVsync = checked
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: vsyncCheck
text: "<font color=\"white\">Enable V-Sync</font>"
font.pointSize: 12
checked: prefs.enableVsync
onCheckedChanged: {
prefs.enableVsync = checked
}
}
}
@@ -276,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
@@ -327,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
@@ -343,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
}
}
}
}
@@ -353,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: {
@@ -399,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
@@ -445,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
@@ -489,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);
+211 -35
View File
@@ -1,5 +1,6 @@
import QtQuick 2.9
import QtQuick.Controls 2.2
import QtQuick.Dialogs 1.2
import QtQuick.Layouts 1.3
import QtQuick.Window 2.2
@@ -7,6 +8,7 @@ import QtQuick.Controls.Material 2.1
import ComputerManager 1.0
import AutoUpdateChecker 1.0
import StreamingPreferences 1.0
ApplicationWindow {
property bool pollingActive: false
@@ -16,13 +18,54 @@ ApplicationWindow {
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()
}
}
Keys.onBackPressed: {
if (depth > 1) {
stackView.pop()
}
}
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()
}
}
onVisibilityChanged: {
@@ -43,6 +86,37 @@ ApplicationWindow {
}
}
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)
{
var existingItem = stackView.find(function(item, index) {
@@ -61,22 +135,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)
}
}
@@ -90,12 +175,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
@@ -108,12 +200,6 @@ ApplicationWindow {
onClicked: Qt.openUrlExternally(browserUrl);
Menu {
x: parent.width
transformOrigin: Menu.TopRight
}
function updateAvailable(url)
{
updateButton.browserUrl = url
@@ -124,26 +210,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
@@ -152,29 +256,101 @@ 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");
}
}
}
+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;
};
+144 -11
View File
@@ -1,5 +1,6 @@
#include <QGuiApplication>
#include <QQmlApplicationEngine>
#include <QQmlContext>
#include <QIcon>
#include <QQuickStyle>
#include <QMutex>
@@ -11,12 +12,19 @@
#define SDL_MAIN_HANDLED
#include <SDL.h>
#ifdef HAVE_FFMPEG
#include "streaming/video/ffmpeg.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
@@ -62,7 +70,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)
@@ -94,7 +103,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);
}
@@ -122,15 +131,99 @@ 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);
@@ -158,6 +251,16 @@ 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
QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
@@ -166,10 +269,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");
@@ -182,6 +289,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",
@@ -196,8 +304,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;
@@ -209,6 +340,10 @@ int main(int argc, char *argv[])
SDL_GetError());
}
// Use atexit() to ensure SDL_Quit() is called. This avoids
// racing with object destruction where SDL may be used.
atexit(SDL_Quit);
// Avoid the default behavior of changing the timer resolution to 1 ms.
// We don't want this all the time that Moonlight is open. We will set
// it manually when we start streaming.
@@ -216,7 +351,5 @@ int main(int argc, char *argv[])
int err = app.exec();
SDL_Quit();
return err;
}
+38
View File
@@ -4,9 +4,11 @@
#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()
{
@@ -20,6 +22,42 @@ QString Path::getBoxArtCacheDir()
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) {
+3
View File
@@ -9,9 +9,12 @@ public:
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;
};
+81 -12
View File
@@ -1,5 +1,6 @@
#include "streamingpreferences.h"
#include "streaming/session.hpp"
#include "streaming/session.h"
#include "streaming/streamutils.h"
#include <QSettings>
@@ -16,8 +17,13 @@
#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();
}
@@ -30,17 +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()
@@ -51,14 +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()
@@ -75,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
@@ -87,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));
}
}
@@ -101,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,
@@ -116,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) {
@@ -124,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
+30 -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,43 +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();
};
-230
View File
@@ -1,230 +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));
// We must check this prior to the below checks to ensure we don't
// underflow if framesQueued - s_PendingHardDrops < 0.
if (framesQueued <= MIN_QUEUED_FRAMES) {
s_PendingDrops = s_PendingHardDrops = 0;
}
// Pend enough drops to get us back to MIN_QUEUED_FRAMES
else 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 (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[];
};
+401 -146
View File
@@ -1,5 +1,6 @@
#include "session.hpp"
#include "session.h"
#include "settings/streamingpreferences.h"
#include "streaming/streamutils.h"
#include <Limelight.h>
#include <SDL.h>
@@ -19,13 +20,6 @@
#include "video/sl.h"
#endif
#if defined(Q_OS_DARWIN)
// Using full-screen desktop allows 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
@@ -54,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
};
@@ -85,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);
@@ -108,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, 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;
@@ -129,7 +127,7 @@ bool Session::chooseDecoder(StreamingPreferences::VideoDecoderSelection vds,
#ifdef HAVE_FFMPEG
chosenDecoder = new FFmpegVideoDecoder();
if (chosenDecoder->initialize(vds, window, videoFormat, width, height, frameRate, enableVsync)) {
if (CALL_INITIALIZE(chosenDecoder)) {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"FFmpeg-based video decoder chosen");
return true;
@@ -172,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;
}
}
@@ -232,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);
@@ -257,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,
@@ -273,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;
}
@@ -291,12 +340,12 @@ 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,
@@ -317,19 +366,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)
@@ -352,20 +412,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.");
}
@@ -401,7 +469,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,
@@ -418,7 +486,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;
@@ -435,13 +503,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 {
@@ -452,6 +543,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;
}
@@ -469,8 +567,7 @@ 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;
@@ -479,20 +576,29 @@ void Session::getWindowDimensions(bool fullScreen,
displayIndex = SDL_GetWindowDisplayIndex(m_Window);
SDL_assert(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
else if (fullScreen) {
// Create our window on the same display that Qt's UI
// was being displayed on.
else {
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_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;
SDL_Rect displayBounds;
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());
}
}
}
@@ -501,23 +607,19 @@ void Session::getWindowDimensions(bool fullScreen,
if (SDL_GetDisplayUsableBounds(displayIndex, &usableBounds) == 0) {
width = usableBounds.w;
height = usableBounds.h;
x = usableBounds.x;
y = usableBounds.y;
if (m_Window != nullptr) {
int top, left, bottom, right;
if (SDL_GetWindowBordersSize(m_Window, &top, &left, &bottom, &right) < 0) {
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");
return;
"Unable to get window border size: %s",
SDL_GetError());
}
x += left;
y += top;
width -= left + right;
height -= top + bottom;
// If the stream window can fit within the usable drawing area with 1:1
// scaling, do that rather than filling the screen.
@@ -534,44 +636,107 @@ void Session::getWindowDimensions(bool fullScreen,
width = m_StreamConfig.width;
height = m_StreamConfig.height;
x = y = SDL_WINDOWPOS_UNDEFINED_DISPLAY(displayIndex);
}
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;
}
}
}
}
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;
}
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();
@@ -581,7 +746,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.
@@ -615,6 +782,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));
@@ -636,15 +807,16 @@ 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
@@ -659,15 +831,14 @@ 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,
0);
SDL_WINDOW_ALLOW_HIGHDPI);
if (!m_Window) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"SDL_CreateWindow() failed: %s",
@@ -680,11 +851,13 @@ void Session::exec()
// 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);
if (m_Preferences->windowMode == StreamingPreferences::WM_WINDOWED) {
getWindowDimensions(x, y, width, height);
SDL_SetWindowPosition(m_Window, x, y);
// 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
@@ -692,13 +865,10 @@ void Session::exec()
}
else {
// Update the window display mode based on our current monitor
SDL_DisplayMode mode;
if (SDL_GetDesktopDisplayMode(SDL_GetWindowDisplayIndex(m_Window), &mode) == 0) {
SDL_SetWindowDisplayMode(m_Window, &mode);
}
updateOptimalWindowDisplayMode();
// Enter full screen
SDL_SetWindowFullscreen(m_Window, SDL_OS_FULLSCREEN_FLAG);
SDL_SetWindowFullscreen(m_Window, m_FullScreenFlag);
}
QSvgRenderer svgIconRenderer(QString(":/res/moonlight.svg"));
@@ -724,7 +894,7 @@ void Session::exec()
// 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
@@ -757,6 +927,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;
@@ -795,11 +967,70 @@ 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
@@ -817,10 +1048,8 @@ void Session::exec()
SDL_FlushEvent(SDL_WINDOWEVENT);
// Update the window display mode based on our current monitor
SDL_DisplayMode mode;
if (SDL_GetDesktopDisplayMode(SDL_GetWindowDisplayIndex(m_Window), &mode) == 0) {
SDL_SetWindowDisplayMode(m_Window, &mode);
}
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
@@ -829,18 +1058,31 @@ void Session::exec()
SDL_FlushEvent(SDL_RENDER_DEVICE_RESET);
SDL_FlushEvent(SDL_RENDER_TARGETS_RESET);
// 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,
m_Preferences.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;
{
// 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
@@ -874,13 +1116,23 @@ 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
@@ -893,6 +1145,9 @@ DispatchDeferredCleanup:
SDL_EnableScreenSaver();
SDL_SetHint(SDL_HINT_TIMER_RESOLUTION, "0");
// Raise any keys that are still down
inputHandler.raiseAllKeys();
// Destroy the decoder, since this must be done on the main thread
SDL_AtomicLock(&m_DecoderLock);
delete m_VideoDecoder;
@@ -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,20 +42,27 @@ 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,
@@ -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);
};
+16
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() {}
@@ -19,6 +34,7 @@ public:
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;
+97 -67
View File
@@ -266,74 +266,52 @@ bool DXVA2Renderer::initializeRenderer()
return false;
}
UINT guidCount;
GUID* guids;
hr = m_ProcService->GetVideoProcessorDeviceGuids(&m_Desc, &guidCount, &guids);
DXVA2_VideoProcessorCaps caps;
hr = m_ProcService->GetVideoProcessorCaps(DXVA2_VideoProcProgressiveDevice, &m_Desc, renderTargetDesc.Format, &caps);
if (FAILED(hr)) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"GetVideoProcessorDeviceGuids() failed: %x",
"GetVideoProcessorCaps() failed for DXVA2_VideoProcProgressiveDevice: %x",
hr);
return false;
}
UINT i;
for (i = 0; i < guidCount; i++) {
DXVA2_VideoProcessorCaps caps;
hr = m_ProcService->GetVideoProcessorCaps(guids[i], &m_Desc, renderTargetDesc.Format, &caps);
if (SUCCEEDED(hr)) {
if (!(caps.DeviceCaps & DXVA2_VPDev_HardwareDevice)) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"Device %d is not hardware: %x",
i,
caps.DeviceCaps);
continue;
}
else if (!(caps.VideoProcessorOperations & DXVA2_VideoProcess_YUV2RGB) &&
!(caps.VideoProcessorOperations & DXVA2_VideoProcess_YUV2RGBExtended)) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"Device %d can't convert YUV2RGB: %x",
i,
caps.VideoProcessorOperations);
continue;
}
else if (!(caps.VideoProcessorOperations & DXVA2_VideoProcess_StretchX) ||
!(caps.VideoProcessorOperations & DXVA2_VideoProcess_StretchY)) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"Device %d can't stretch video: %x",
i,
caps.VideoProcessorOperations);
continue;
}
m_ProcService->GetProcAmpRange(guids[i], &m_Desc, renderTargetDesc.Format, DXVA2_ProcAmp_Brightness, &m_BrightnessRange);
m_ProcService->GetProcAmpRange(guids[i], &m_Desc, renderTargetDesc.Format, DXVA2_ProcAmp_Contrast, &m_ContrastRange);
m_ProcService->GetProcAmpRange(guids[i], &m_Desc, renderTargetDesc.Format, DXVA2_ProcAmp_Hue, &m_HueRange);
m_ProcService->GetProcAmpRange(guids[i], &m_Desc, renderTargetDesc.Format, DXVA2_ProcAmp_Saturation, &m_SaturationRange);
hr = m_ProcService->CreateVideoProcessor(guids[i], &m_Desc, renderTargetDesc.Format, 0, &m_Processor);
if (FAILED(hr)) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"CreateVideoProcessor() failed for GUID %d: %x",
i,
hr);
continue;
}
break;
}
else {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"GetVideoProcessorCaps() failed for GUID %d: %x",
i,
hr);
}
if (!(caps.DeviceCaps & DXVA2_VPDev_HardwareDevice)) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"DXVA2_VideoProcProgressiveDevice is not hardware: %x",
caps.DeviceCaps);
return false;
}
else if (!(caps.VideoProcessorOperations & DXVA2_VideoProcess_YUV2RGB) &&
!(caps.VideoProcessorOperations & DXVA2_VideoProcess_YUV2RGBExtended)) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"DXVA2_VideoProcProgressiveDevice can't convert YUV2RGB: %x",
caps.VideoProcessorOperations);
return false;
}
else if (!(caps.VideoProcessorOperations & DXVA2_VideoProcess_StretchX) ||
!(caps.VideoProcessorOperations & DXVA2_VideoProcess_StretchY)) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"DXVA2_VideoProcProgressiveDevice can't stretch video: %x",
caps.VideoProcessorOperations);
return false;
}
CoTaskMemFree(guids);
if (caps.DeviceCaps & DXVA2_VPDev_EmulatedDXVA1) {
// DXVA2 over DXVA1 may have bad performance
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"DXVA2_VideoProcProgressiveDevice is DXVA1");
}
if (i == guidCount) {
m_ProcService->GetProcAmpRange(DXVA2_VideoProcProgressiveDevice, &m_Desc, renderTargetDesc.Format, DXVA2_ProcAmp_Brightness, &m_BrightnessRange);
m_ProcService->GetProcAmpRange(DXVA2_VideoProcProgressiveDevice, &m_Desc, renderTargetDesc.Format, DXVA2_ProcAmp_Contrast, &m_ContrastRange);
m_ProcService->GetProcAmpRange(DXVA2_VideoProcProgressiveDevice, &m_Desc, renderTargetDesc.Format, DXVA2_ProcAmp_Hue, &m_HueRange);
m_ProcService->GetProcAmpRange(DXVA2_VideoProcProgressiveDevice, &m_Desc, renderTargetDesc.Format, DXVA2_ProcAmp_Saturation, &m_SaturationRange);
hr = m_ProcService->CreateVideoProcessor(DXVA2_VideoProcProgressiveDevice, &m_Desc, renderTargetDesc.Format, 0, &m_Processor);
if (FAILED(hr)) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Unable to find a usable DXVA2 processor");
"CreateVideoProcessor() failed for DXVA2_VideoProcProgressiveDevice: %x",
hr);
return false;
}
@@ -364,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
@@ -379,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 {
@@ -466,7 +475,6 @@ bool DXVA2Renderer::initializeDevice(SDL_Window* window, bool enableVsync)
D3DPRESENT_PARAMETERS d3dpp = {};
d3dpp.hDeviceWindow = info.info.win.window;
d3dpp.Flags = D3DPRESENTFLAG_VIDEO;
d3dpp.SwapEffect = D3DSWAPEFFECT_DISCARD;
if ((windowFlags & SDL_WINDOW_FULLSCREEN_DESKTOP) == SDL_WINDOW_FULLSCREEN) {
d3dpp.Windowed = false;
@@ -489,10 +497,20 @@ bool DXVA2Renderer::initializeDevice(SDL_Window* window, bool enableVsync)
// to reduce latency by avoiding double v-syncing.
d3dpp.PresentationInterval = D3DPRESENT_INTERVAL_IMMEDIATE;
// 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;
// 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");
@@ -501,6 +519,7 @@ bool DXVA2Renderer::initializeDevice(SDL_Window* window, bool 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,
@@ -510,6 +529,7 @@ bool DXVA2Renderer::initializeDevice(SDL_Window* window, bool enableVsync)
// 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,
@@ -619,6 +639,16 @@ bool DXVA2Renderer::needsTestFrame()
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]);
@@ -24,6 +24,8 @@ public:
virtual bool prepareDecoderContext(AVCodecContext* context);
virtual void renderFrameAtVsync(AVFrame* frame);
virtual bool needsTestFrame();
virtual int getDecoderCapabilities();
virtual VSyncConstraint getVsyncConstraint();
private:
bool initializeDecoder();
@@ -1,50 +0,0 @@
#include "displaylinkvsyncsource.h"
DisplayLinkVsyncSource::DisplayLinkVsyncSource(Pacer* pacer)
: m_Pacer(pacer),
m_DisplayLink(nullptr)
{
}
DisplayLinkVsyncSource::~DisplayLinkVsyncSource()
{
if (m_DisplayLink != nullptr) {
CVDisplayLinkStop(m_DisplayLink);
CVDisplayLinkRelease(m_DisplayLink);
}
}
CVReturn
DisplayLinkVsyncSource::displayLinkOutputCallback(
CVDisplayLinkRef,
const CVTimeStamp* /* now */,
const CVTimeStamp* /* vsyncTime */,
CVOptionFlags,
CVOptionFlags*,
void *displayLinkContext)
{
auto me = reinterpret_cast<DisplayLinkVsyncSource*>(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->m_Pacer->vsyncCallback(500 / me->m_DisplayFps);
return kCVReturnSuccess;
}
bool DisplayLinkVsyncSource::initialize(SDL_Window*, int displayFps)
{
m_DisplayFps = displayFps;
CVDisplayLinkCreateWithActiveCGDisplays(&m_DisplayLink);
CVDisplayLinkSetOutputCallback(m_DisplayLink, displayLinkOutputCallback, this);
CVDisplayLinkStart(m_DisplayLink);
return true;
}
@@ -1,31 +1,11 @@
#pragma once
#include <CoreVideo/CoreVideo.h>
#include "pacer.h"
class DisplayLinkVsyncSource : public IVsyncSource
class DisplayLinkVsyncSourceFactory
{
public:
DisplayLinkVsyncSource(Pacer* pacer);
virtual ~DisplayLinkVsyncSource();
virtual bool initialize(SDL_Window* window, int displayFps);
private:
static
CVReturn
displayLinkOutputCallback(
CVDisplayLinkRef,
const CVTimeStamp* now,
const CVTimeStamp* /* vsyncTime */,
CVOptionFlags,
CVOptionFlags*,
void *displayLinkContext);
Pacer* m_Pacer;
CVDisplayLinkRef m_DisplayLink;
int m_DisplayFps;
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);
}
@@ -1,4 +1,5 @@
#include "pacer.h"
#include "streaming/streamutils.h"
#include "nullthreadedvsyncsource.h"
@@ -19,12 +20,13 @@
// V-sync happens.
#define TIMER_SLACK_MS 3
Pacer::Pacer(IFFmpegRenderer* renderer) :
Pacer::Pacer(IFFmpegRenderer* renderer, PVIDEO_STATS videoStats) :
m_FrameQueueLock(0),
m_VsyncSource(nullptr),
m_VsyncRenderer(renderer),
m_MaxVideoFps(0),
m_DisplayFps(0)
m_DisplayFps(0),
m_VideoStats(videoStats)
{
}
@@ -81,6 +83,7 @@ void Pacer::vsyncCallback(int timeUntilNextVsyncMillis)
// Catch up if we're several frames ahead
while (m_FrameQueue.count() > frameDropTarget) {
AVFrame* frame = m_FrameQueue.dequeue();
m_VideoStats->pacerDroppedFrames++;
av_frame_free(&frame);
}
@@ -110,7 +113,10 @@ RenderNextFrame:
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);
@@ -120,22 +126,7 @@ bool Pacer::initialize(SDL_Window* window, int maxVideoFps, bool enableVsync)
{
m_MaxVideoFps = maxVideoFps;
m_EnableVsync = enableVsync;
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_GetCurrentDisplayMode(displayIndex, &mode) == 0) {
// May be zero if undefined
m_DisplayFps = mode.refresh_rate;
}
m_DisplayFps = StreamUtils::getDisplayRefreshRate(window);
if (m_EnableVsync) {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
@@ -143,7 +134,7 @@ bool Pacer::initialize(SDL_Window* window, int maxVideoFps, bool enableVsync)
m_DisplayFps, m_MaxVideoFps);
#if defined(Q_OS_DARWIN)
m_VsyncSource = new DisplayLinkVsyncSource(this);
m_VsyncSource = DisplayLinkVsyncSourceFactory::createVsyncSource(this);
#elif defined(Q_OS_WIN32)
m_VsyncSource = new DxVsyncSource(this);
#else
@@ -178,7 +169,10 @@ void Pacer::submitFrame(AVFrame* frame)
SDL_AtomicUnlock(&m_FrameQueueLock);
}
else {
Uint32 beforeRender = SDL_GetTicks();
m_VsyncRenderer->renderFrameAtVsync(frame);
m_VideoStats->totalRenderTime += SDL_GetTicks() - beforeRender;
m_VideoStats->renderedFrames++;
av_frame_free(&frame);
}
}
@@ -1,5 +1,6 @@
#pragma once
#include "../../decoder.h"
#include "../renderer.h"
#include <QQueue>
@@ -13,7 +14,7 @@ public:
class Pacer
{
public:
Pacer(IFFmpegRenderer* renderer);
Pacer(IFFmpegRenderer* renderer, PVIDEO_STATS videoStats);
~Pacer();
@@ -35,4 +36,5 @@ private:
int m_MaxVideoFps;
int m_DisplayFps;
bool m_EnableVsync;
PVIDEO_STATS m_VideoStats;
};
@@ -8,6 +8,12 @@ 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,
@@ -18,6 +24,8 @@ public:
virtual bool prepareDecoderContext(AVCodecContext* context) = 0;
virtual void renderFrameAtVsync(AVFrame* frame) = 0;
virtual bool needsTestFrame() = 0;
virtual int getDecoderCapabilities() = 0;
virtual VSyncConstraint getVsyncConstraint() = 0;
};
class SdlRenderer : public IFFmpegRenderer {
@@ -33,6 +41,8 @@ public:
virtual bool prepareDecoderContext(AVCodecContext* context);
virtual void renderFrameAtVsync(AVFrame* frame);
virtual bool needsTestFrame();
virtual int getDecoderCapabilities();
virtual VSyncConstraint getVsyncConstraint();
private:
SDL_Renderer* m_Renderer;
@@ -36,6 +36,18 @@ bool SdlRenderer::needsTestFrame()
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,
@@ -109,6 +109,11 @@ VAAPIRenderer::initialize(SDL_Window* window, int, int width, int height, int, b
"Initialized VAAPI %d.%d",
major, minor);
const char* vendorString = vaQueryVendorString(vaDeviceContext->display);
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Driver: %s",
vendorString ? vendorString : "<unknown>");
// This will populate the driver_quirks
err = av_hwdevice_ctx_init(m_HwContext);
if (err < 0) {
@@ -149,6 +154,17 @@ VAAPIRenderer::needsTestFrame()
return true;
}
int
VAAPIRenderer::getDecoderCapabilities()
{
return 0;
}
IFFmpegRenderer::VSyncConstraint VAAPIRenderer::getVsyncConstraint()
{
return VSYNC_ANY;
}
void
VAAPIRenderer::renderFrameAtVsync(AVFrame* frame)
{
@@ -39,6 +39,8 @@ public:
virtual bool prepareDecoderContext(AVCodecContext* context);
virtual void renderFrameAtVsync(AVFrame* frame);
virtual bool needsTestFrame();
virtual int getDecoderCapabilities();
virtual VSyncConstraint getVsyncConstraint();
private:
int m_WindowSystem;
@@ -91,6 +91,7 @@ bool VDPAURenderer::initialize(SDL_Window* window, int, int width, int height, i
GET_PROC_ADDRESS(VDP_FUNC_ID_OUTPUT_SURFACE_DESTROY, &m_VdpOutputSurfaceDestroy);
GET_PROC_ADDRESS(VDP_FUNC_ID_OUTPUT_SURFACE_QUERY_CAPABILITIES, &m_VdpOutputSurfaceQueryCapabilities);
GET_PROC_ADDRESS(VDP_FUNC_ID_VIDEO_SURFACE_GET_PARAMETERS, &m_VdpVideoSurfaceGetParameters);
GET_PROC_ADDRESS(VDP_FUNC_ID_GET_INFORMATION_STRING, &m_VdpGetInformationString);
SDL_GetWindowSize(window, (int*)&m_DisplayWidth, (int*)&m_DisplayHeight);
@@ -130,6 +131,13 @@ bool VDPAURenderer::initialize(SDL_Window* window, int, int width, int height, i
return false;
}
const char* infoString;
if (m_VdpGetInformationString(&infoString) == VDP_STATUS_OK) {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Driver: %s",
infoString);
}
// Try our available output formats to find something the GPU supports
bool foundFormat = false;
for (int i = 0; i < OUTPUT_SURFACE_FORMAT_COUNT; i++) {
@@ -217,6 +225,9 @@ 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");
@@ -230,6 +241,16 @@ bool VDPAURenderer::needsTestFrame()
return true;
}
int VDPAURenderer::getDecoderCapabilities()
{
return 0;
}
IFFmpegRenderer::VSyncConstraint VDPAURenderer::getVsyncConstraint()
{
return VSYNC_ANY;
}
void VDPAURenderer::renderFrameAtVsync(AVFrame* frame)
{
VdpStatus status;
@@ -22,6 +22,8 @@ public:
virtual bool prepareDecoderContext(AVCodecContext* context);
virtual void renderFrameAtVsync(AVFrame* frame);
virtual bool needsTestFrame();
virtual int getDecoderCapabilities();
virtual VSyncConstraint getVsyncConstraint();
private:
uint32_t m_VideoWidth, m_VideoHeight;
@@ -54,6 +56,7 @@ private:
VdpOutputSurfaceDestroy* m_VdpOutputSurfaceDestroy;
VdpOutputSurfaceQueryCapabilities* m_VdpOutputSurfaceQueryCapabilities;
VdpVideoSurfaceGetParameters* m_VdpVideoSurfaceGetParameters;
VdpGetInformationString* m_VdpGetInformationString;
// X11 stuff
VdpPresentationQueueTargetCreateX11* m_VdpPresentationQueueTargetCreateX11;
+24 -19
View File
@@ -160,10 +160,20 @@ public:
// We need to add a subview for our display layer.
NSView* contentView = info.info.cocoa.window.contentView;
m_View = [[NSView alloc] initWithFrame:contentView.bounds];
m_View.wantsLayer = YES;
[contentView addSubview: m_View];
setupDisplayLayer();
m_DisplayLayer = [[AVSampleBufferDisplayLayer alloc] init];
m_DisplayLayer.bounds = m_View.bounds;
m_DisplayLayer.position = CGPointMake(CGRectGetMidX(m_View.bounds), CGRectGetMidY(m_View.bounds));
m_DisplayLayer.videoGravity = AVLayerVideoGravityResizeAspect;
// Create a layer-hosted view by setting the layer before wantsLayer
// This avoids us having to add our AVSampleBufferDisplayLayer as a
// sublayer of a layer-backed view which leaves a useless layer in
// the middle.
m_View.layer = m_DisplayLayer;
m_View.wantsLayer = YES;
[contentView addSubview: m_View];
err = av_hwdevice_ctx_create(&m_HwContext,
AV_HWDEVICE_TYPE_VIDEOTOOLBOX,
@@ -196,25 +206,20 @@ public:
return false;
}
private:
void setupDisplayLayer()
virtual int getDecoderCapabilities() override
{
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];
}
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:
AVBufferRef* m_HwContext;
AVSampleBufferDisplayLayer* m_DisplayLayer;
CMVideoFormatDescriptionRef m_FormatDesc;
+162 -3
View File
@@ -1,5 +1,6 @@
#include <Limelight.h>
#include "ffmpeg.h"
#include "streaming/streamutils.h"
#ifdef Q_OS_WIN32
#include "ffmpeg-renderers/dxva2.h"
@@ -27,6 +28,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)
{
@@ -54,11 +60,17 @@ FFmpegVideoDecoder::FFmpegVideoDecoder()
m_HwDecodeCfg(nullptr),
m_Renderer(nullptr),
m_ConsecutiveFailedDecodes(0),
m_Pacer(nullptr)
m_Pacer(nullptr),
m_LastFrameNumber(0),
m_StreamFps(0)
{
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");
}
@@ -100,17 +112,41 @@ void FFmpegVideoDecoder::reset()
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;
avcodec_free_context(&m_VideoDecoderCtx);
logVideoStats(m_GlobalVideoStats, "Global video stats");
}
bool FFmpegVideoDecoder::completeInitialization(AVCodec* decoder, SDL_Window* window,
int videoFormat, int width, int height,
int maxFps, bool enableVsync, bool testOnly)
{
m_Pacer = new Pacer(m_Renderer);
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;
}
@@ -125,6 +161,10 @@ bool FFmpegVideoDecoder::completeInitialization(AVCodec* decoder, SDL_Window* wi
// 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;
@@ -194,6 +234,80 @@ bool FFmpegVideoDecoder::completeInitialization(AVCodec* decoder, SDL_Window* wi
return true;
}
void FFmpegVideoDecoder::addVideoStats(VIDEO_STATS& src, VIDEO_STATS& dst)
{
dst.receivedFrames += src.receivedFrames;
dst.decodedFrames += src.decodedFrames;
dst.renderedFrames += src.renderedFrames;
dst.networkDroppedFrames += src.networkDroppedFrames;
dst.pacerDroppedFrames += src.pacerDroppedFrames;
dst.totalReassemblyTime += src.totalReassemblyTime;
dst.totalDecodeTime += src.totalDecodeTime;
dst.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)) {
@@ -315,6 +429,38 @@ int FFmpegVideoDecoder::submitDecodeUnit(PDECODE_UNIT du)
PLENTRY entry = du->bufferList;
int err;
// 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;
}
if (du->fullLength + AV_INPUT_BUFFER_PADDING_SIZE > m_DecodeBuffer.length()) {
m_DecodeBuffer = QByteArray(du->fullLength + AV_INPUT_BUFFER_PADDING_SIZE, 0);
}
@@ -333,6 +479,10 @@ int FFmpegVideoDecoder::submitDecodeUnit(PDECODE_UNIT du)
m_Pkt.data = reinterpret_cast<uint8_t*>(m_DecodeBuffer.data());
m_Pkt.size = du->fullLength;
m_ActiveWndVideoStats.totalReassemblyTime += LiGetMillis() - du->receiveTimeMs;
Uint32 beforeDecode = SDL_GetTicks();
err = avcodec_send_packet(m_VideoDecoderCtx, &m_Pkt);
if (err < 0) {
char errorstring[512];
@@ -372,6 +522,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);
@@ -415,6 +570,10 @@ void FFmpegVideoDecoder::dropFrame(SDL_UserEvent* event)
}
else {
av_frame_free(&frame);
// Since Pacer won't see this frame, we'll mark it as a drop
// on its behalf
m_ActiveWndVideoStats.pacerDroppedFrames++;
}
SDL_AtomicDecRef(&m_QueuedFrames);
}
+10
View File
@@ -20,6 +20,7 @@ public:
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;
@@ -31,6 +32,10 @@ private:
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();
@@ -47,6 +52,11 @@ private:
SDL_atomic_t m_QueuedFrames;
int m_ConsecutiveFailedDecodes;
Pacer* m_Pacer;
VIDEO_STATS m_ActiveWndVideoStats;
VIDEO_STATS m_LastWndVideoStats;
VIDEO_STATS m_GlobalVideoStats;
int m_LastFrameNumber;
int m_StreamFps;
static const uint8_t k_H264TestFrame[];
static const uint8_t k_HEVCTestFrame[];
+6
View File
@@ -25,6 +25,12 @@ SLVideoDecoder::isHardwareAccelerated()
return true;
}
int
SLVideoDecoder::getDecoderCapabilities()
{
return 0;
}
bool
SLVideoDecoder::initialize(StreamingPreferences::VideoDecoderSelection vds,
SDL_Window*,
+1
View File
@@ -17,6 +17,7 @@ public:
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
@@ -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
+3 -2
View File
@@ -2,10 +2,11 @@ TEMPLATE = subdirs
SUBDIRS = \
moonlight-common-c \
qmdnsengine \
app
app \
soundio
# Build the dependencies in parallel before the final app
app.depends = qmdnsengine moonlight-common-c
app.depends = qmdnsengine moonlight-common-c soundio
# 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!"
+33 -4
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,35 @@ 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 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
@@ -99,7 +127,7 @@ if "%SIGN%"=="1" (
)
echo Building bundle
set VCREDIST_INSTALLER=%VCREDIST_PATH%\vcredist_%ARCH%.exe
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
@@ -119,7 +147,7 @@ if "%SIGN%"=="1" (
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%
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
@@ -132,6 +160,7 @@ if /i "%APPVEYOR%"=="true" (
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!
@@ -139,4 +168,4 @@ exit /b 0
:Error
echo Build failed!
exit /b !ERRORLEVEL!
exit /b !ERRORLEVEL!
View File
Submodule soundio/libsoundio added at 2bb21ad417
+101
View File
@@ -0,0 +1,101 @@
#-------------------------------------------------
#
# 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
}
# Disable warnings
CONFIG += warn_off
unix:!macx {
CONFIG += link_pkgconfig
packagesExist(libpulse) {
PKGCONFIG += libpulse
CONFIG += pulseaudio
}
packagesExist(alsa) {
PKGCONFIG += alsa
CONFIG += alsa
}
}
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
}
+29 -5
View File
@@ -4,7 +4,7 @@
<?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"
@@ -39,7 +39,7 @@
</Directory>
</Directory>
<Property Id="APPDATAFOLDER">%LOCALAPPDATA%\Moonlight Game Streaming</Property>
<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 -->
@@ -60,16 +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" />
<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" />
<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">
@@ -85,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>
+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>