Compare commits

..
397 Commits
Author SHA1 Message Date
Cameron Gutman f2283b56b9 Version 2.1.0 2020-05-16 23:45:09 -07:00
Cameron Gutman b515f2a599 Update SDL_GameControllerDB 2020-05-16 22:45:29 -07:00
Cameron Gutman 4354b49496 Fix CLI video decoder options being reversed 2020-05-16 17:44:04 -07:00
Cameron Gutman 994312c212 Force GL to block after the current frame instead during the next frame 2020-05-16 16:25:13 -07:00
Cameron Gutman 5f25e0d4e2 Add limited support for frame pacing to EGLRenderer 2020-05-16 15:59:10 -07:00
Cameron Gutman 4bbe571f5f Fix build without FFmpeg and with older EGL 1.5 headers 2020-05-16 13:25:10 -07:00
Cameron Gutman c58d21375c EGL 1.4 doesn't have EGLAttrib either 2020-05-16 12:20:52 -07:00
Cameron Gutman ed128c558f Add support for EGLRenderer on EGL 1.4 2020-05-16 12:13:00 -07:00
Cameron Gutman 828932d95c Fix EGL detection on RPi 2020-05-16 11:10:45 -07:00
Cameron Gutman 42ced309d3 Restore the original GL attributes after EGLRenderer is destroyed 2020-05-15 20:22:11 -07:00
Cameron Gutman 1fcd306879 Fix EGLRenderer when SDL isn't built with X11 or Wayland backends 2020-05-15 20:01:22 -07:00
Cameron Gutman 26c9ad7f06 Use eglGetProcAddress() for all GLES 2.0 extensions 2020-05-15 19:39:41 -07:00
Cameron Gutman 4b7b01ce0a Fix EGL build 2020-05-15 17:54:45 -07:00
Cameron Gutman 6632154906 Remove some unnecessary headers and definitions 2020-05-15 16:59:29 -07:00
Cameron Gutman ca67c7ef43 Provide a main function for the compile test 2020-05-15 16:57:09 -07:00
Cameron Gutman c4039a929c Use a compile test for EGL support 2020-05-15 16:46:03 -07:00
Cameron Gutman a1b74e1220 Fix vaExportSurfaceHandle() detection with i965 VAAPI driver 2020-05-14 20:56:24 -07:00
Cameron Gutman 01f6188824 Add VAAPI_FORCE_DIRECT and VAAPI_FORCE_INDIRECT env vars to control rendering technique 2020-05-13 19:45:30 -07:00
Cameron Gutman 283f31a734 Test if vaExportSurfaceHandle() works before claiming EGL export is supported 2020-05-13 19:39:45 -07:00
Cameron Gutman b800ce9ebe Allow use of VA drivers without VAEntrypointVideoProc 2020-05-13 19:31:25 -07:00
Cameron Gutman eb9d54465c Don't use EGLRenderer if not built with SDL 2.0.10+ 2020-05-13 19:19:11 -07:00
Cameron Gutman eb9e1f81d4 Enable the EGLRenderer to use the render thread 2020-05-13 19:09:20 -07:00
Cameron Gutman ddfbd70b55 Remove Wayland performance warning message 2020-05-13 18:56:18 -07:00
Cameron Gutman 51e9ab83ad Send a null AVFrame to indicate end of stream to allow renderers to do render thread cleanup 2020-05-13 18:55:21 -07:00
Cameron Gutman 5ed7e03b2e Merge pull request #365 from xdbob/wayland_for_upstream
Introduce a new FFMPEG frontend renderer: EGLRenderer
2020-05-13 18:39:18 -07:00
Cameron Gutman 0cb8d5b12f Add the older dList DLLs for the 390 drivers to the blacklist
Fixes #386
2020-05-13 04:25:53 -07:00
Cameron Gutman b4548c1af2 Flush all window events after SDL_CreateRenderer() 2020-05-12 18:41:36 -07:00
Antoine Damhet 0a396f3112 Introduce a new FFMPEG frontend renderer: EGLRenderer
Right now this renderer works on X11 & Wayland with VAAPI as a backend.

Some rendering latency benchmarks on my `i7-10510U` (with
`intel-media-driver` 20.1.1 which cause a *huge* regression with the
SDL_Renderer):
|             | X11    | Wayland |
| Before      | 6.78ms | 22.50ms |
| EGLRenderer | 0.76ms | 00.77ms |

Signed-off-by: Antoine Damhet <antoine.damhet@lse.epita.fr>
2020-05-12 11:11:35 +02:00
Cameron Gutman 2aa18dc88f Add a bit of extra padding on the right for the scrollbar 2020-05-10 13:02:10 -07:00
Cameron Gutman 8d89983eeb Parent ScrollBars to their Flingables, not the StackView
This ensures they disappear when navigating to a new page
2020-05-10 12:53:27 -07:00
Cameron Gutman 573c470965 Ensure the title label doesn't get drawn over when the window is small 2020-05-10 12:38:56 -07:00
Cameron Gutman daa0c751e2 Fix settings layout spacing and centering 2020-05-10 12:30:15 -07:00
Cameron Gutman e1babefc80 Center toolbar text 2020-05-10 11:48:24 -07:00
Cameron Gutman ce99b0e162 Improve settings page behavior when the window is too small 2020-05-10 11:12:37 -07:00
Cameron Gutman 4c3a23b1bd Resize toolbar text 2020-05-09 23:02:09 -07:00
Cameron Gutman 12016af326 Display version number in toolbar on the settings page
Fixes #341
2020-05-09 22:20:39 -07:00
Cameron Gutman fc9871b075 Delay mouse capture until after SDL_RecreateWindow() has been called 2020-05-09 19:48:13 -07:00
Cameron Gutman 3cb3c1c6d8 Revert "Use the physical desktop size in pixels rather than the native resolution on macOS"
Let's wait until functionality is confirmed on a 5K iMac before doing this.

This reverts commit 8fe3474589.
2020-05-09 16:52:18 -07:00
Cameron Gutman 8fe3474589 Use the physical desktop size in pixels rather than the native resolution on macOS
It seems like some Macs (5K iMacs at least) don't have kDisplayModeNativeFlag set
2020-05-09 16:34:48 -07:00
Cameron Gutman dc3c565ec0 Move serialization and deserialization into NvApp class 2020-05-07 19:54:36 -07:00
Cameron Gutman 350c7d7081 Persist the IsAppCollectorGame attribute 2020-05-07 19:37:10 -07:00
Cameron Gutman 0428bc8fe3 Generate synthetic mouse button up if the mouse has left the window and the button is raised 2020-05-07 19:26:02 -07:00
Cameron Gutman 514a8575a2 Use the viewport size for computing the overlay position 2020-05-07 18:56:53 -07:00
Cameron Gutman fc11377552 Fix build on Qt 5.11 and earlier 2020-05-07 17:13:16 -07:00
Cameron Gutman fbaa70a2ae Enable the right stick to scroll the settings page. Fixes #220 2020-05-06 22:38:52 -07:00
Cameron Gutman f30c11ddf4 Properly fix SDL_HINT_MOUSE_FOCUS_CLICKTHROUGH on macOS VTRenderer 2020-05-06 22:20:21 -07:00
Cameron Gutman 51c4ecc879 Revert "Partially unrevert 05e82c24 for macOS which still needs it"
This reverts commit 080421f2fb.
2020-05-06 21:41:57 -07:00
Cameron Gutman 080421f2fb Partially unrevert 05e82c24 for macOS which still needs it 2020-05-06 21:18:23 -07:00
Cameron Gutman 8cc61ca8b8 Use SDL_HINT_MOUSE_FOCUS_CLICKTHROUGH to get click events on focus gain 2020-05-06 20:43:41 -07:00
Cameron Gutman 400754c5b3 Revert "Pass through focus-gain clicks to the host in absolute mouse mode"
This reverts commit 05e82c24f8.
2020-05-06 20:04:07 -07:00
Cameron Gutman 05e82c24f8 Pass through focus-gain clicks to the host in absolute mouse mode 2020-05-06 18:58:43 -07:00
Cameron Gutman 97c15c7876 Ensure the Info.plist is included for builds in the IDE
It is required for SDL_WINDOW_ALLOW_HIGHDPI to work
2020-05-04 20:25:19 -07:00
Cameron Gutman 36e7cc8e89 Use a smaller MaximumUncompressedMediaSize for unofficial builds to speed up compression a bit 2020-05-04 18:32:38 -07:00
Cameron Gutman ad2ce4de53 Update moonlight-common-c 2020-05-04 18:29:55 -07:00
Cameron Gutman 2a28f7c4d2 Use jom to parallelize command-line builds on Windows 2020-05-04 18:06:32 -07:00
Cameron Gutman 0cdbe080a6 Don't archive installers on Windows AppVeyor build
They interact poorly with installed official versions.
2020-05-04 17:51:57 -07:00
Cameron Gutman 96fe1a7da1 Revert "Allow side-by-side installation of nightly and release builds"
This reverts commit 33e814122e.
2020-05-04 17:51:57 -07:00
Cameron Gutman 4f73af2920 Revert "Fix AppVeyor build archiving"
This reverts commit 402e8c1897.
2020-05-04 17:51:57 -07:00
Cameron Gutman 7ca4e49bf5 Compile in parallel on macOS 2020-05-04 17:49:24 -07:00
Cameron Gutman dfa63297fc Fix incorrectly scaled output from the SDL renderer on macOS 2020-05-04 17:48:32 -07:00
Cameron Gutman 402e8c1897 Fix AppVeyor build archiving 2020-05-02 20:59:22 -07:00
Cameron Gutman 33e814122e Allow side-by-side installation of nightly and release builds 2020-05-02 17:56:55 -07:00
Cameron Gutman f28a4df5ee Restore the full QTBUG-80947 hack as it appears to still be required sometimes 2020-05-02 10:14:54 -07:00
Cameron Gutman 417aa2815c Add a friendly error message when no video traffic is received 2020-05-01 21:28:48 -07:00
Cameron Gutman 2fdabe37aa Raise the window before capturing in relative mode 2020-05-01 18:44:22 -07:00
Cameron Gutman d8270dfb6a Capture mouse by default on debug builds too 2020-05-01 18:43:54 -07:00
Cameron Gutman b75f662c41 Add the ability to rename PCs 2020-05-01 18:34:15 -07:00
Cameron Gutman 0d9d0845f5 Add --touchscreen-trackpad options 2020-04-30 21:10:53 -07:00
Cameron Gutman 5d66a1f298 Fix absolute mouse mode help text 2020-04-30 21:05:53 -07:00
Cameron Gutman 9bf52eb63a Improve relative touch deadzone behavior 2020-04-30 21:04:51 -07:00
Cameron Gutman 0c9d72a9a1 Restore the option for relative touch mode 2020-04-30 20:55:15 -07:00
Cameron Gutman c2c2bd2df6 Split input.cpp into smaller files 2020-04-30 20:22:43 -07:00
Cameron Gutman 78fc594d84 Move input code into a subfolder 2020-04-30 20:09:36 -07:00
Cameron Gutman 53b1e7c573 Update moonlight-common-c 2020-04-29 20:22:02 -07:00
Cameron Gutman c14404a7c1 Restore the QTBUG-80947 hack for support for direct connections without a router 2020-04-29 20:20:17 -07:00
Cameron Gutman 96e6750f9c Don't confine the cursor to the full-screen window when not capturing input 2020-04-28 20:37:00 -07:00
Cameron Gutman dbafd05a4e Store a pointer to the window inside the input handler 2020-04-28 20:24:27 -07:00
Cameron Gutman d7fd578cc3 Don't set MODIFIER_META when meta key is disabled 2020-04-28 20:06:30 -07:00
Cameron Gutman 0e8572e1db Version 2.0.0 2020-04-28 02:08:10 -07:00
Cameron Gutman 98a997bd8e Update AppImage to Qt 5.14.2 2020-04-27 17:09:52 -07:00
Cameron Gutman 6fbc95cc41 Update readme with links for new platforms 2020-04-26 21:31:29 -07:00
Cameron Gutman 1852f74e8b Add missing return statement 2020-04-26 20:42:01 -07:00
Cameron Gutman 55e3e16860 Try radeonsi if other VAAPI drivers fail 2020-04-26 20:18:46 -07:00
Cameron Gutman 7cf2a55b43 Allow radeonsi to direct render on XWayland 2020-04-26 20:13:00 -07:00
Cameron Gutman c41cbbd2da Allow the radeonsi VAAPI driver on XWayland 2020-04-26 20:06:53 -07:00
Cameron Gutman 7283bb046b Disable initial mouse capture on XWayland 2020-04-26 19:33:25 -07:00
Cameron Gutman dd2df6cbcf Default to starting in a non-maximized window 2020-04-26 17:57:28 -07:00
Cameron Gutman 01f1cac811 Update readme and metadata 2020-04-26 16:12:10 -07:00
Cameron Gutman aaa112c3f3 Don't use VAAPI drivers that lack VPP support
intel-media-va-driver (free) is one example of such a driver which
is apparently missing the shaders to perform colorspace conversion.
2020-04-26 15:38:05 -07:00
Cameron Gutman dc0fa9c64d Update AppImage's libva to 2.7.1 for compatibility with VA-API 1.7 drivers 2020-04-26 11:38:31 -07:00
Cameron Gutman f749b38aa2 Warn when starting a game with remote desktop mouse mode enabled 2020-04-25 23:50:12 -07:00
Cameron Gutman 43d79673e1 Rebuild SDL2 for Windows
changeset:   13787:a564e72e3b2d
2020-04-25 21:04:44 -07:00
Cameron Gutman fc841f572c Rebuild SDL2 for macOS
changeset: 13787:a564e72e3b2d

with https://bugzilla-attachments.libsdl.org/attachment.cgi?id=4322 for bug #5111
2020-04-25 19:53:00 -07:00
Cameron Gutman df9f117a08 Update SDL_GameControllerDB 2020-04-25 19:02:24 -07:00
Cameron Gutman bedc46a1be Rebuild SDL2 for macOS
changeset:   13787:a564e72e3b2d
2020-04-25 16:56:25 -07:00
Cameron Gutman a7c6095be5 Add special error text for the -1 launch error code 2020-04-25 16:05:39 -07:00
Cameron Gutman f2d5cf785e Don't cancel mouse capture on focus loss in absolute mode 2020-04-25 15:42:13 -07:00
Cameron Gutman f926ad9b5e Fix inverted capture-by-default setting 2020-04-25 15:41:45 -07:00
Cameron Gutman 68acf969f2 Move focus handling into the input code 2020-04-25 15:37:33 -07:00
Cameron Gutman 65d53e800b Confine the cursor to the window in full-screen exclusive mode 2020-04-25 13:15:14 -07:00
Cameron Gutman e44d8ac7e9 Only enable the absolute mouse checkbox if the platform has a window manager 2020-04-25 13:04:22 -07:00
Cameron Gutman d1823dd6e1 Capture the mouse by default always on release builds and in absolute mode for debug builds 2020-04-25 13:01:15 -07:00
Cameron Gutman 80bfd3b8e9 Change raw input checkbox to adjust absolute vs relative mouse mode 2020-04-25 13:00:39 -07:00
Cameron Gutman 07128cc7fe Allow keyboard input when the mouse is not captured 2020-04-24 21:35:38 -07:00
Cameron Gutman 0892f0b0bb Don't capture the cursor on a touch event 2020-04-24 21:26:59 -07:00
Cameron Gutman 8cf7f3ac08 Fix absolute mouse positioning on SDL renderer 2020-04-24 19:37:15 -07:00
Cameron Gutman ab2bb51879 Clamp mouse motion to the video region rather than dropping it 2020-04-24 17:35:35 -07:00
Cameron Gutman 8cb98b5365 Rewrite touchscreen support for direct input 2020-04-23 21:34:03 -07:00
Cameron Gutman 782c5f8a34 Fix parsing rare GFE status code of 0xFFFFFFFF 2020-04-23 18:34:14 -07:00
Cameron Gutman d6b5c5f63e Disable SOPS for streams over 60 FPS for GFE 3.20.3 2020-04-12 11:39:25 -07:00
Cameron Gutman ef6dfa6d91 Add basic support for absolute mouse input 2020-04-11 21:28:11 -07:00
Cameron Gutman 3840ee8ee7 Add MSVC ASan support 2020-04-10 17:21:17 -07:00
Cameron Gutman f47e40fa2a Blacklist more broken A-Volute DLLs 2020-04-10 17:20:21 -07:00
Cameron Gutman 49a4a61876 Enable QoS for ENet on Windows and fix disconnect delay on GFE 3.20.3 2020-04-09 18:02:49 -07:00
Khalid Jebbari 669c1a6f0d Add comment that describe kb shortcut of overlay (#358) 2020-04-06 18:31:02 -07:00
Cameron Gutman 15342190fa Make audio debug logs more readable 2020-04-04 12:53:03 -07:00
Cameron Gutman c98ef6e8a9 Display a friendly error when trying to stream over 4K with H.264 2020-04-04 12:47:44 -07:00
Cameron Gutman d36d5936da Don't include resolutions over 8K in settings list 2020-04-04 12:46:42 -07:00
Cameron Gutman 70de75ae3d Use EF instead of CS7 for DSCP on ENet traffic 2020-04-04 10:59:03 -07:00
Cameron Gutman ea62e33e51 Add 7.1 surround sound support to libsoundio audio renderer 2020-04-04 09:49:10 -07:00
Cameron Gutman 524e92c81a Add 7.1 surround sound support to SLAudio 2020-04-03 19:04:11 -07:00
Cameron Gutman d5a26b1d70 Enable auto-update for AppImages 2020-04-03 17:03:12 -07:00
Cameron Gutman cd7649a666 Add 7.1 surround sound support for SDL audio output 2020-04-03 00:12:52 -07:00
Cameron Gutman 847cc0a38a Migrate to the new audio configuration macros 2020-03-31 21:36:16 -07:00
Cameron Gutman 3588eb9bf8 Reduce retransmission delay on packet loss and enable QoS marking on ENet traffic 2020-03-29 15:49:52 -07:00
Cameron Gutman 296e22d3ca Remove the Wayland post-cleanup hack
It seems to do more harm than good on Fedora 32, causing deadlocks
when the stream is closed. The original bug it was trying to avoid
seems to have already been fixed.
2020-03-28 00:46:04 -07:00
Cameron Gutman 923e590da1 Delay the spinner on the stream segue to prevent animation glitching on Windows 2020-03-27 23:32:46 -07:00
Cameron Gutman e56329d05a Don't render with vaPutSurface() on XWayland 2020-03-27 19:11:03 -07:00
Cameron Gutman 2452e80e57 Add VDPAU driver path fallbacks for AppImage 2020-03-26 19:06:24 -07:00
Cameron Gutman b9019831a9 Fix failure to try the default VAAPI driver with the modified LIBVA_DRIVERS_PATH 2020-03-26 18:25:52 -07:00
Cameron Gutman b3b49b440e Add VAAPI driver path fallbacks for AppImage 2020-03-24 00:09:03 -07:00
Cameron Gutman cbedf0f74f Bundle a newer libva, FFmpeg, and Qt Wayland plugin into the AppImage 2020-03-23 23:57:38 -07:00
Cameron Gutman 6c980eba68 Fix build on Qt 5.15 Beta 2
Reverts most of 10dae7482c
2020-03-23 18:30:56 -07:00
Cameron Gutman e9f766904e Build AppImages on AppVeyor 2020-03-23 01:04:08 -07:00
Cameron Gutman 62d76c006d Move all builds to AppVeyor 2020-03-22 21:26:07 -07:00
Cameron Gutman 615b78a612 Update SDL_GameControllerDB to d222918bd6f786a35f9c42fd87921ca29493b785 2020-03-22 18:41:21 -07:00
Cameron Gutman aaf0340d6c Use 10 ms audio samples on low bandwidth connections 2020-03-21 01:04:41 -07:00
Cameron Gutman 93ea437760 Key repeat is handled server-side, so don't send repeat events 2020-03-20 23:56:44 -07:00
Cameron Gutman fe6a1244ed Fix a couple compiler warnings 2020-03-18 22:46:32 -07:00
Cameron Gutman 953858b0e7 Add workaround for Steam Link graphics bug. Fixes #254 2020-03-18 22:26:15 -07:00
Cameron Gutman f3da46833f Revert "Temporarily switch to gamecontrollerdb to fix build after DMCA takedown of SDL_GameControllerDB"
The DMCA takedown has been disputed and the repo is back up.

This reverts commit 6314676b9c.
2020-03-18 19:48:26 -07:00
Cameron Gutman ba2488b6e2 Try the iHD VA driver as a fallback to fix Comet Lake and later 2020-03-18 18:39:43 -07:00
Cameron Gutman 4e7e0d67e5 Add a workaround for Mesa Iris returning the wrong VA driver 2020-03-17 17:34:35 -07:00
Cameron Gutman faa16b207c Implement a more reliable mouse capture workaround for GNOME+Wayland 2020-03-14 00:33:53 -07:00
Cameron Gutman 2d9f62761c Don't pass through the super key to the host 2020-03-13 18:11:31 -07:00
Cameron Gutman 60689f531d Update to SDL 2.0.12 with USE_D3D9EX 2020-03-11 21:27:45 -07:00
Cameron Gutman 3f97692311 Update macOS to SDL 2.0.12 2020-03-11 17:51:46 -07:00
Cameron Gutman 81221336f4 Rec 601 seems to be a bit more accurate on macOS 2020-03-10 23:21:00 -07:00
Cameron Gutman ecfc758ce4 Attach colorspace data to CVPixelBuffer. Fixes #323 2020-03-10 22:21:09 -07:00
Cameron Gutman 2cb3a132c6 Fix false positives in SDL_GameControllerRumble() support detection 2020-03-07 17:00:38 -08:00
Cameron Gutman 519473e498 Enable rumble on Bluetooth PS4 controllers 2020-03-07 16:46:41 -08:00
Cameron Gutman 9b271d918a Use SDL_GameControllerSetPlayerIndex() to indicate player index 2020-03-03 20:15:35 -08:00
Cameron Gutman 5b4d874851 Avoid sending excessively large mDNS queries on networks with lots of mDNS traffic 2020-03-02 02:52:02 -08:00
Cameron Gutman 9dc1dbc002 Update qmdnsengine to address mDNS traffic amplification 2020-03-02 02:33:50 -08:00
Cameron Gutman 7ee323f0d5 Fix deprecation warning on macOS 2020-02-29 15:54:10 -08:00
Cameron Gutman 4bb6f711b9 Avoid using the QTBUG-80947 workaround on Qt 5.14.2 and later 2020-02-27 18:03:28 -08:00
Cameron Gutman 816cec8f5c Remove deprecated listener callbacks 2020-02-25 00:31:05 -08:00
Cameron Gutman 2394dfe88a Change errorCode to an int type 2020-02-24 23:03:34 -08:00
Cameron Gutman dfc0d2303b Fix incorrect #endif location 2020-02-24 20:55:07 -08:00
Cameron Gutman 598a48ec50 Remove the Wayland mouse capture hack that is not necessary on SDL 2.0.11 2020-02-24 20:33:33 -08:00
Cameron Gutman 9f41e98e3f Remove RPi hacks that are not necessary on SDL 2.0.11 2020-02-24 19:55:22 -08:00
Cameron Gutman 489891ff3c Fix build on Windows and macOS with Qt 5.13 and earlier 2020-02-24 18:17:23 -08:00
Cameron Gutman a44ed0b047 Allow initial destination position to be used as an offset with scaleSourceToDestinationSurface() 2020-02-24 18:01:30 -08:00
Cameron Gutman 1aaa97b91d Override SDL 2.0.12 default of mapping buttons to labels rather than position 2020-02-24 17:44:56 -08:00
Cameron Gutman 13d68e789f Fix Clazy warnings 2020-02-24 17:44:21 -08:00
Cameron Gutman 10dae7482c Fix build with Qt 5.15 2020-02-24 17:39:24 -08:00
Cameron Gutman 979de190dc Cap resolution options at 1080p on Steam Link and Raspberry Pi due to hardware limitations 2020-02-23 00:43:43 -08:00
Cameron Gutman 46bcbda972 MMAL is always full-screen 2020-02-23 00:25:01 -08:00
Cameron Gutman 43bdc353fb Fix 720p streaming on MMAL 2020-02-23 00:22:44 -08:00
Cameron Gutman 6314676b9c Temporarily switch to gamecontrollerdb to fix build after DMCA takedown of SDL_GameControllerDB 2020-02-22 19:20:52 -08:00
Cameron Gutman 9f77f3d968 Don't leave 0 byte files if saving the box art image failed and deal with those that may already exist. Fixes #346 2020-02-20 19:06:19 -08:00
Cameron Gutman f4733dbbe8 Fix unused parameter warning on Linux 2020-02-20 14:13:03 -08:00
Cameron Gutman 61fc0e4893 Don't allow Flickables to be dragged beyond bounds 2020-02-20 13:38:01 -08:00
Cameron Gutman 652ad86cad Update SDL_GameControllerDB to cc4b7f603698914fad9a172a5e47bfece93a45f6 2020-02-20 13:29:36 -08:00
Cameron Gutman 60f308bd76 Build dual binary bundles for Steam Link 2020-02-12 18:20:15 -08:00
Cameron Gutman b2113ff95b Raspberry Pi's full KMS driver is broken with MMAL, so just advise fake KMS 2020-02-09 12:52:11 -08:00
Cameron Gutman b8188a46a9 Display a friendly warning when running without a KMS driver 2020-02-09 12:40:07 -08:00
Cameron Gutman e790b0761e Add another RPi hack to fix mouse capture in full-screen in LXDE 2020-02-09 12:00:30 -08:00
Cameron Gutman fa3317f6cd Enable double buffering on SDL backends that support it 2020-02-09 11:54:09 -08:00
Cameron Gutman b080f7a81c Don't display browser links in non-GUI environment 2020-02-09 11:41:44 -08:00
Cameron Gutman 57a1c5eb76 Disable the window mode options for always full-screen renderers 2020-02-09 11:35:05 -08:00
Cameron Gutman 5b7e2521cc Ignore unbind and full-screen key combos on EGLFS 2020-02-09 10:20:41 -08:00
Cameron Gutman fced653c1c Add a hack to avoid PulseAudio on RPi 2020-02-08 23:35:42 -08:00
Cameron Gutman 9253e6ac5e Fix GLES library not being found on RPi when running from console 2020-02-08 22:34:07 -08:00
Cameron Gutman aff2ed22b9 Use EGLFS platform plugin if X and Wayland aren't detected 2020-02-08 21:37:17 -08:00
Cameron Gutman 5520a52379 Force full-screen mode on EGLFS 2020-02-08 21:31:04 -08:00
Cameron Gutman 505050e936 Fix high DPI check in 0437835b 2020-02-08 18:47:59 -08:00
Cameron Gutman 0437835bde Avoid enabling High DPI mode on EGLFS to fix font rendering 2020-02-08 18:11:33 -08:00
Cameron Gutman 77386f8dfc Reduce buffering for MMAL to avoid requiring 128 MB VRAM split 2020-02-08 17:56:30 -08:00
Cameron Gutman 529fdf0341 Add decoder options dictionary to prepareDecoderContext() 2020-02-08 17:47:26 -08:00
Cameron Gutman fa74700ad9 Fix build with new Steam Link SDK 2020-02-05 17:24:05 -08:00
Cameron Gutman caca491943 Add build script to generate AppImage 2020-02-01 15:46:53 -08:00
Cameron Gutman 4794f44a6d Charge decoders for time spent holding on to frames 2020-01-26 20:15:11 -08:00
Cameron Gutman f0119b1231 Add support for the out-of-tree Jetson FFmpeg NVMPI decoders
https://github.com/jocover/jetson-ffmpeg
2020-01-26 19:16:03 -08:00
Cameron Gutman 4c579aa826 Provide another frame if avcodec_receive_frame() returns EAGAIN 2020-01-26 15:36:22 -08:00
Cameron Gutman b9463b3c0e Allow NV12 and NV21 to be selected for non-hwaccel decoders using the SDL renderer 2020-01-26 15:02:29 -08:00
Cameron Gutman cede6ce8a9 Add H264_DECODER_HINT and HEVC_DECODER_HINT envvars to specify a decoder manually 2020-01-26 14:40:48 -08:00
Cameron Gutman 956e6e3638 Avoid slicing for hardware decoders that use SDL as the renderer 2020-01-26 14:13:42 -08:00
Cameron Gutman b282c7d815 Fix parsing CLI flags that accept values when passed before 'action' argument 2020-01-26 13:32:24 -08:00
Cameron Gutman 5b6142e2d9 Always create IdentityManager on the main thread 2020-01-25 19:34:37 -08:00
Cameron Gutman 58313ad8fc Allow default mouse polling interval to be overridden by setting MOUSE_POLLING_INTERVAL 2020-01-21 19:15:17 -08:00
Cameron Gutman 7aff148e9f Add --packet-size command-line option 2020-01-21 19:10:10 -08:00
Cameron Gutman ee5c61fb74 Centralize Discord invite links 2020-01-18 10:53:05 -08:00
Cameron Gutman 91be6c8028 Only hide Qt's mouse cursor on EGLFS 2020-01-15 18:23:02 -08:00
Cameron Gutman 2e0bd2ec28 Invoke avcodec_receive_frame() during decoder testing 2020-01-15 18:09:59 -08:00
Cameron Gutman a0151fbc04 Fix memory leak if 10-bit color is not supported 2020-01-15 17:56:11 -08:00
Cameron Gutman d853782fea Add support for the Meta/Super/Windows key on macOS and Linux 2020-01-15 17:54:57 -08:00
Cameron Gutman c6638a7154 Add support for 10-bit output in full-screen with DXVA2 2020-01-14 17:12:55 -08:00
Cameron Gutman 4abebe3aec Hide the Qt cursor when streaming 2020-01-11 02:06:08 -08:00
Cameron Gutman 5aed8f928a Add V4L2M2M support using SDL renderer
Note: This requires FFmpeg master to work, since it depends on:
https://github.com/FFmpeg/FFmpeg/commit/d61cf1b1ebc2477749d7d7825a072400ed24af9
2020-01-10 20:52:16 -08:00
Cameron Gutman afe6cfc383 Update build dependencies in README 2020-01-05 23:32:48 -08:00
Cameron Gutman 174983ce1e Update Windows builds to FFmpeg 4.2.1 with D3D11VA+DXVA2 enabled 2020-01-01 01:09:33 -06:00
Cameron Gutman 1313c7b3bf Enable HDR as an experimental option 2019-12-30 18:05:43 -06:00
Cameron Gutman bee622bcad Improve speed of initial PC status poll 2019-12-30 17:51:23 -06:00
Cameron Gutman 8cefdeae49 Update macOS builds to FFmpeg 4.2.1 2019-12-30 02:10:26 -06:00
Cameron Gutman 7cf70b9607 Clear Add PC textbox when closing the dialog 2019-12-29 20:13:45 -06:00
Cameron Gutman add4b582b5 Update issue template configuration 2019-12-28 21:02:14 -06:00
Cameron Gutman b419d21940 Only use the QTBUG-80947 hack on Qt 5.14 2019-12-21 13:54:09 -08:00
Cameron Gutman 227ff4e13a Version 1.2.1 (hotfix for Windows and macOS) 2019-12-21 13:01:44 -08:00
Cameron Gutman 8e6e70683e Add workaround for QTBUG-80947 2019-12-21 12:58:45 -08:00
Cameron Gutman 626f0aec6a Rebuild SDL 2.0.10 for macOS with https://bugzilla.libsdl.org/show_bug.cgi?id=4888 patch 2019-12-21 12:49:40 -08:00
Cameron Gutman 973d174a30 Version 1.2.0 (take 2) 2019-12-17 18:25:37 -08:00
Cameron Gutman 9f35690b95 Update DMG signing to support disambiguating identity shortnames 2019-12-17 18:10:39 -08:00
Cameron Gutman d961f2bc13 Switch to QElapsedTimer since QTime is deprecated for our usecase in Qt 5.14 2019-12-16 23:16:31 -08:00
Cameron Gutman 10e8277044 Version 1.2.0 2019-12-16 22:51:11 -08:00
Cameron Gutman b4c40d9b18 Update SDL_GameControllerDB submodule 2019-12-16 22:38:58 -08:00
Cameron Gutman e1603f0808 Explicitly use Rec 601 for VDPAU 2019-12-16 18:02:28 -08:00
Cameron Gutman d4c2350d63 Fix DXVA2 rendering with the wrong colorspace on AMD GPUs 2019-12-15 13:29:19 -08:00
Cameron Gutman 5e5470ae12 Specify video transfer matrix for SLVideo 2019-12-15 11:27:47 -08:00
Cameron Gutman 6230a99360 Fix colorspace conversion with SDL as frontend renderer 2019-12-14 17:17:29 -08:00
Cameron Gutman 801adb6af7 Add colorspace support to VAAPI renderer 2019-12-14 16:58:04 -08:00
Cameron Gutman 8b9b96f422 Only enable slicing when CPU decoding 2019-12-14 15:34:48 -08:00
Cameron Gutman 160684f23e Allow decoders to choose a desired colorspace 2019-12-14 15:25:56 -08:00
Cameron Gutman 88930a9de4 Optimize decoder capability checking 2019-12-14 15:20:44 -08:00
Cameron Gutman af6b8c9b88 Update common-c with colorspace configuration support 2019-12-14 14:40:02 -08:00
Cameron Gutman bf52d3189d Switch to Bionic build image on Travis CI 2019-12-14 13:25:57 -08:00
Cameron Gutman 5d872e8b5b Add auto update check to Steam Link 2019-12-13 20:21:35 -08:00
Cameron Gutman 4832d8007b Print joystick haptic capabilities to log 2019-12-13 18:43:35 -08:00
Cameron Gutman 879e084383 Enable Qt 5.14's new fractional DPI scaling 2019-12-12 20:36:23 -08:00
Cameron Gutman 1ae16bb657 Add source archival script 2019-12-09 18:27:51 -08:00
Cameron Gutman 2177bc68db Add git-archive-all.sh script from https://github.com/meitar/git-archive-all.sh 2019-12-09 18:27:51 -08:00
Cameron Gutman c8b887fe32 Use the new SDL_GameControllerRumble() API on SDL 2.0.9 to fix rumble on HIDAPI gamepads 2019-12-07 16:17:01 -08:00
Cameron Gutman 7205242f38 Improve VPN detection on Windows 2019-12-07 16:00:03 -08:00
Cameron Gutman be33b5b387 Update default window mode on macOS after upgrading 2019-12-07 13:27:47 -08:00
Cameron Gutman 66a123f4b9 Store binaries on the symbol server too 2019-12-03 19:14:19 -08:00
Cameron Gutman 76a4ee22b3 Update SDL_GameControllerDB submodule 2019-12-03 19:02:29 -08:00
Cameron Gutman 3a6bf4d20b Disable arbitrary audio duration for libsoundio until further testing is done 2019-12-03 19:00:43 -08:00
Cameron Gutman 09cf105cdc Block RTSS hooks due to user reported crashes 2019-12-03 18:31:18 -08:00
Cameron Gutman ce02e086a3 Add missing return when no interface address matches 2019-12-02 17:27:57 -08:00
Cameron Gutman e8d4313454 Fix build on Qt 5.10 and earlier 2019-12-02 17:18:29 -08:00
Cameron Gutman 49046f88cf Ignore unknown MTUs 2019-12-01 22:10:07 -08:00
Cameron Gutman 5e2f32e3eb Force remote streaming mode if connecting through a VPN 2019-12-01 21:50:36 -08:00
Cameron Gutman b4a1393bf6 Update common-c with PMTUD and low bitrate audio fixes 2019-12-01 19:45:15 -08:00
Cameron Gutman 0ccee9d806 Support arbitrary audio duration in libsoundio renderer 2019-12-01 19:43:22 -08:00
Cameron Gutman 5700272dc4 Improve low bandwidth audio performance for SDL and SLAudio and fix RTSP issues with broken PMTUD 2019-11-30 22:24:28 -06:00
Cameron Gutman 7e5a3925b3 Fix typo in channel mapping 2019-11-17 14:35:37 -08:00
Cameron Gutman 1febe32e73 Fix 5.1 channel mapping on Steam Link 2019-11-17 12:22:57 -08:00
Cameron Gutman 6059c982cf Fix auto-comment line breaks 2019-11-16 11:58:07 -08:00
Cameron Gutman de7b973239 Plumb presentation time from server into AVFrame.pts field 2019-11-11 18:03:47 -08:00
Cameron Gutman 741113a607 Add configuration for auto-comment bot 2019-11-09 15:04:28 -08:00
Cameron Gutman c20daa8867 Add compatibility checks for HEVC Main10 in VT backend 2019-11-05 18:36:43 -08:00
Cameron Gutman 5e57a7d5c0 Prevent SDL renderer from trying to render 10-bit output 2019-11-05 17:29:21 -08:00
Cameron Gutman 7c8d27700e Add 10-bit output support to VDPAU renderer 2019-11-05 17:09:44 -08:00
Cameron Gutman f8621be5ee Add an HEVC Main10 test frame 2019-11-05 17:08:25 -08:00
Cameron Gutman 8ed87e10b4 Change PC icon to be consistent with iOS and Android 2019-11-05 17:06:57 -08:00
Cameron Gutman ca26fa2dd0 Add basic HEVC Main10 support in DXVA2 renderer 2019-11-04 22:57:12 -08:00
Cameron Gutman 1669095fb1 Prevent double-clicks from triggering pairing twice. Fixes #286 2019-11-04 19:30:38 -08:00
Cameron Gutman 13eed48877 Update moonlight-common-c submodule 2019-11-02 21:49:00 -07:00
Cameron Gutman 07494e6eff Fix Linux build 2019-10-12 19:44:59 -07:00
Cameron Gutman 41b1d72825 Update moonlight-common-c submodule 2019-10-12 19:22:19 -07:00
Cameron Gutman 4d2965e74f Always install the VC redistributable 2019-09-30 00:11:37 -07:00
Cameron Gutman 12526bfca4 Version 1.1.1 2019-09-29 22:11:39 -07:00
Cameron Gutman 3e2c344140 Display the Moonlight icon in Discord Rich Presence info 2019-09-29 22:06:12 -07:00
Cameron Gutman 33730dd584 Display the update tooltip on launch if an update is available 2019-09-29 21:32:24 -07:00
Cameron Gutman 1cb03b1882 Add padding to app labels 2019-09-29 21:26:07 -07:00
Cameron Gutman 2f0a6db414 Add hacks for SDL+Mutter interaction bugs 2019-09-29 21:05:31 -07:00
Cameron Gutman a42328d971 Use VSWhere to locate Visual Studio and redistributables 2019-09-29 15:01:53 -07:00
Cameron Gutman 96ff51d598 Use SDL audio for platforms that libsoundio doesn't support 2019-09-22 12:03:53 -07:00
Cameron Gutman 7c3d3af681 Fix control stream connection error on multi-homed hosts 2019-09-14 14:08:38 -07:00
Cameron Gutman 6942bca5a7 Link to download page from download badge 2019-08-25 23:24:34 -07:00
Cameron Gutman 9eac9a190e Add Downloads badge 2019-08-25 23:20:55 -07:00
Cameron Gutman 40e4fbd9a6 Update common-c for parallel STUN requests 2019-08-20 19:05:54 -07:00
Cameron Gutman 7b6996ed80 Add a little help to the build scripts 2019-08-13 18:07:42 -07:00
Cameron Gutman ef2827a9d8 Update common-c for STUN fallback 2019-08-07 23:14:07 -07:00
Cameron Gutman dbf98ce3a6 Require keyframes even for software H.264 decoding 2019-08-07 22:38:31 -07:00
Cameron Gutman ee7cb5123d Add NVDEC to list of supported video acceleration APIs 2019-08-05 18:31:38 -07:00
Cameron Gutman 77e1d6ab8a Version 1.1 2019-08-05 18:28:26 -07:00
Cameron Gutman 1640bed7a5 Improve reliability of mDNS discovery 2019-08-04 18:04:33 -07:00
Cameron Gutman c6fd5cbbce Update common-c to fix high quality surround sound over the Internet 2019-08-04 15:15:51 -07:00
Cameron Gutman 5288041da5 Prevent us from consuming more than 4 samples per write callback to bound latency 2019-08-03 12:47:44 -07:00
Cameron Gutman a848fb1dd1 Add Steam Link to Travis CI 2019-08-02 21:31:07 -07:00
Cameron Gutman 172945434c Build the full DMG on Travis CI 2019-08-02 20:52:23 -07:00
Cameron Gutman a0967e0281 Fix building a dmg with no code signing identities 2019-08-02 20:40:20 -07:00
Cameron Gutman deab717c73 Remove AppVeyor OpenSSL hack 2019-08-02 20:21:10 -07:00
Cameron Gutman 5db5a088ea Fix NVDEC artifacts if the renderer is delayed for a bit 2019-08-01 20:25:58 -07:00
Cameron Gutman 5e106d2f19 Only store the local address from mDNS if it actually worked 2019-07-31 22:16:11 -07:00
Cameron Gutman 940da6bc73 Fix discovery of IPv6-only hosts 2019-07-31 22:07:20 -07:00
Cameron Gutman d110bff253 Update qmdnsengine to improve reliability of mDNS on multi-homed systems 2019-07-31 22:03:27 -07:00
Cameron Gutman 4caa329c36 Update Windows to SDL 2.0.10 with USE_D3D9EX 2019-07-28 12:01:12 -07:00
Cameron Gutman 7c7d279337 Update SDL_GameControllerDB to dca1a629d9e094baa17fff8ba1642cabc6125eed 2019-07-26 16:43:20 -04:00
Cameron Gutman 831e6ee76d Update moonlight-common-c to fix termination error code on GFE 3.19.0.107 2019-07-26 16:43:20 -04:00
Cameron Gutman b01842ad02 Update macOS to SDL 2.0.10 2019-07-26 16:43:20 -04:00
Cameron Gutman 7acf91883a Rework audio renderer selection to fall back and treat ML_AUDIO as a hard selection 2019-07-26 12:50:45 -04:00
Cameron Gutman 897f6fdd3e Improve handling of IPv6-only cases 2019-07-15 01:07:31 -07:00
Cameron Gutman 6f9c3fbc38 Add support for zero configuration IPv6 streaming 2019-07-14 17:56:57 -07:00
Cameron Gutman bcbd1a5b4b Fix subsystem print in VAAPI 2019-07-07 15:41:26 -07:00
Cameron Gutman 6468efd7e4 Add VAAPI DRM support 2019-07-07 15:32:38 -07:00
Cameron Gutman 66863789c7 Update common-c for compatibility with legacy servers 2019-07-05 22:52:42 -07:00
Cameron Gutman b30058fc29 Fix pairing to Gen 3-5 servers 2019-07-05 21:04:04 -07:00
Cameron Gutman a41cd0b070 Generate SHA-256 client certificates instead of SHA-1 2019-07-05 20:57:21 -07:00
Cameron Gutman ea8b905f02 Missed a call to SDL_SetRelativeMouseMode 2019-07-02 22:39:09 -07:00
Cameron Gutman 16b301236b Add a mouse capture fallback if SDL_SetRelativeMouseMode() fails 2019-07-02 22:17:38 -07:00
Cameron Gutman 22162dda83 Fix runtime SDL version report 2019-06-30 18:48:11 -07:00
Cameron Gutman 024af89be3 Add an option to disable the gamepad mouse button toggle 2019-06-29 18:24:59 -07:00
Cameron Gutman e1a697658e Add Discord Rich Presence for macOS 2019-06-29 17:55:21 -07:00
Cameron Gutman 0a5051f959 Add Discord Rich Presence integration for Windows 2019-06-29 17:40:30 -07:00
Cameron Gutman 82b6b60b31 Take advantage of SDL 2.0.10 bugfixes and new APIs. Fixes #133 2019-06-29 16:08:04 -07:00
Cameron Gutman 06b3c3636a Print compile-time and run-time SDL versions 2019-06-29 15:43:45 -07:00
Cameron Gutman e5712a9474 Move dylibs into the Frameworks folder in the app bundle 2019-06-29 15:39:20 -07:00
Cameron Gutman 6ff845c53b Use borderless windowed mode as the default on macOS. Fixes #246 2019-06-29 14:58:27 -07:00
Cameron Gutman 1c225ed04d Use CUDA acceleration if other hwaccels are unavailable 2019-06-26 21:54:29 -07:00
Cameron Gutman 612b738968 Use the preferred sw_format when reading frames back 2019-06-26 21:25:41 -07:00
Cameron Gutman 87356ef594 Add CUDA acceleration support 2019-06-26 21:02:33 -07:00
Cameron Gutman 20a4dfa6d6 Update bug report template 2019-06-25 21:41:02 -07:00
Cameron Gutman 524cc97f51 Use borderless windowed mode when toggling to full-screen from an initially windowed stream. Fixes #248 2019-06-23 13:29:57 -07:00
Cameron Gutman fc849fb51a Switch back to SDL for audio on Linux. Fixes #245 2019-06-23 13:16:00 -07:00
Cameron Gutman c88651939a Re-add latency bounding on the SDL audio renderer 2019-06-23 13:14:55 -07:00
Cameron Gutman 01254f67f0 Allow audio renderers to specify capabilities 2019-06-23 12:49:37 -07:00
Cameron Gutman 212ed33b21 Block Optimus from using the dGPU due to driver bugs. Fixes #240. Fixes #235 2019-06-22 19:49:05 -07:00
Cameron Gutman bd3f8aef21 Remove OpenSSL 1.0.2 for Qt 5.12.4/5.13 2019-06-18 19:54:35 -07:00
Cameron Gutman 3785acea29 Update to OpenSSL 1.1.1c in preparation for Qt 5.12.4/5.13 2019-06-16 15:20:04 -07:00
Cameron Gutman 866e2f1762 Version 1.0.1 2019-05-27 09:31:01 -07:00
Cameron Gutman 0a705d6752 Fix video on GFE 3.19 2019-05-27 02:24:55 -07:00
Cameron Gutman 4f74fd5354 Fix uninitialized variables and a race condition with CVDisplayLinkStart() 2019-05-21 09:09:16 -07:00
Cameron Gutman 3df9a5d01c Version 1.0.0 2019-05-21 00:14:12 -07:00
Cameron Gutman 2682829bd3 Fix Steam Link audio latency cap 2019-05-20 23:57:27 -07:00
Cameron Gutman 30cfa2607d Hide mouse cursor after streaming on Steam Link 2019-05-19 17:43:46 -07:00
Cameron Gutman 4467c2e6ad Update SDL_GameControllerDB 2019-05-19 13:46:05 -07:00
Cameron Gutman 2c0e8a0ddf Send WoL packet to 48002 and 48010 to workaround ISP blocking of 7 and 9 2019-05-19 13:36:05 -07:00
Cameron Gutman 4c17f32a2e Fix extra non-working gamepad appearing to the host 2019-05-19 13:24:06 -07:00
Cameron Gutman 674220087f Finish mouse emulation support with overlay and bugfixes 2019-05-19 13:10:42 -07:00
Cameron Gutman fc8d5d5799 Add gamepad mouse emulation support 2019-05-19 12:17:23 -07:00
Cameron Gutman 4e4f04c174 Focus on first item when a gamepad is connected 2019-05-19 11:08:23 -07:00
Cameron Gutman 65c21f3392 Improve gamepad navigation on settings page 2019-05-19 10:16:54 -07:00
Cameron Gutman 97fb30cdf1 Fix a few bugs in CVDisplayLink integration in VTRenderer 2019-05-19 09:52:59 -07:00
Cameron Gutman c975279589 Only enable exception-based thread naming on debug builds 2019-05-13 17:54:03 -07:00
Cameron Gutman b3ee7a635f Cap queued audio at 40 ms on Steam Link 2019-05-11 19:09:59 -07:00
Cameron Gutman c2b12868bb Move DisplayLinkVsyncSource back into VTRenderer to reduce latency 2019-05-11 18:33:12 -07:00
Cameron Gutman bdbb03e16f Request 20 ms audio frames on Steam Link to reduce CPU overhead 2019-05-04 15:46:11 -07:00
Cameron Gutman 53138d7c16 Name threads for easier debugging 2019-05-03 21:18:58 -07:00
Cameron Gutman 18d1d35104 Use QByteArray::reserve() rather than reallocating each time we must resize 2019-05-02 22:54:18 -07:00
Cameron Gutman 99d9de35de Add NV21 pixel format support to SDL renderer 2019-05-02 22:51:28 -07:00
Cameron Gutman e6a48481a5 Restore the bulk submission optimization for Steam Link 2019-05-01 22:31:52 -07:00
Cameron Gutman 21f2b1224a Decode directly into the audio renderer's buffer to avoid a copy 2019-05-01 21:27:41 -07:00
Cameron Gutman 187f47a353 Update common-c to fix receive time corruption 2019-04-30 23:23:02 -07:00
Cameron Gutman cb75b3d6d2 Version 0.10.1 2019-04-28 19:27:30 -07:00
Cameron Gutman 27708b9ecb Disable HIDAPI for Xbox gamepads on macOS 2019-04-28 18:45:56 -07:00
Cameron Gutman 5443165fce Update macOS to SDL 2.0.9 with https://bugzilla.libsdl.org/show_bug.cgi?id=4614 patch 2019-04-28 18:28:06 -07:00
Cameron Gutman dfb006d264 More Steam Link performance optimizations 2019-04-28 15:57:57 -07:00
Cameron Gutman 4f00f62674 Batch controller axis events for reduced CPU usage on Steam Link 2019-04-28 15:16:04 -07:00
Cameron Gutman cec527ffd5 Update common-c with performance optimizations and audio bandwidth reduction 2019-04-28 13:17:22 -07:00
Cameron Gutman 4f2fe5c988 Reduce polling rate on SL due to CPU performance limitations 2019-04-28 13:04:58 -07:00
Cameron Gutman 2bf552f801 Batch audio frames on Steam Link to save CPU cycles 2019-04-28 12:55:04 -07:00
Cameron Gutman 477fa8fedf Add logging callbacks for SLAudio and SLVideo 2019-04-28 11:01:54 -07:00
Cameron Gutman f1c7c5f02c Don't emit boxArtLoadComplete on box art load failure to prevent an infinite loop 2019-04-27 18:48:06 -07:00
Cameron Gutman 4e936193c7 Request a test frame for MMAL decoder 2019-04-24 21:31:52 -07:00
Cameron Gutman f9895c22b6 Update SDL_GameControllerDB to cbebd79740bc1360978bdf6d9d4d13d189e420a3 2019-04-24 20:27:23 -07:00
Cameron Gutman 6e46537112 Fix highlights for toolbar buttons to be more visible 2019-04-24 20:22:39 -07:00
Cameron Gutman 92b3918b26 Correct the path used by forfiles commands 2019-04-23 23:25:10 -07:00
Cameron Gutman 34c615a5fd Only include pre-compiled QML files in the installer and portable zips 2019-04-23 22:28:28 -07:00
Cameron Gutman fa29260e09 Work around Qt 5.12 dialog crash on disconnect 2019-04-23 19:40:21 -07:00
cgutman1 a05816b495 Pre-generate QML cache for our QtQuick dependences. Fixes #217 2019-04-22 21:04:21 -07:00
Cameron Gutman 9c5d847dae Autodetect a Wayland environment and configure SDL correctly. Fixes #211 2019-04-21 18:31:11 -07:00
Cameron Gutman 082e330f9d Partially revert fa4c0e82bd because we'll need Qt initialized first to detect Wayland 2019-04-21 17:43:38 -07:00
Cameron Gutman 8878c4a136 Use VAAPI decoder for AMD on Wayland 2019-04-21 15:33:21 -07:00
Cameron Gutman 00926bac81 Fix build warning in SDL renderer 2019-04-21 11:09:11 -07:00
Cameron Gutman 67b0907e52 Add globaldefs.pri for setting qmake values in each project 2019-04-21 11:08:33 -07:00
Cameron Gutman dba479774b Add DRM renderer for Rockchip devices 2019-04-21 05:22:37 +00:00
Cameron Gutman 5c8a1e632b Add support for rendering NV12 hwframes with SDL 2019-04-19 22:16:06 -07:00
Cameron Gutman d65e29111f Fix some codec selection bugs 2019-04-19 20:57:21 -07:00
Cameron Gutman ed170b8092 Prevent the computer name from overflowing the PC grid item 2019-04-18 21:03:37 -07:00
Cameron Gutman ac36fef968 Fix incorrect placeholder status after loading box art causing app label to be absent 2019-04-18 20:58:40 -07:00
Cameron Gutman 10f6b73cad Revert "Avoid using ANGLE on debug builds due to assert issues on Qt 5.12.2"
Qt 5.12.3 fixes the bug. https://bugreports.qt.io/browse/QTBUG-74467

This reverts commit 26bdc3e1d5.
2019-04-18 20:57:39 -07:00
Cameron Gutman 13d933fc07 Until Qt 5.13, VS 2019 is not supported 2019-04-18 20:08:27 -07:00
Cameron Gutman 3ba98e86f2 Remove QC1 style variable since QC1 dialogs are no longer used 2019-04-18 20:04:24 -07:00
Cameron Gutman 1dfca525cf Refactor renderer initialization to reduce duplication and inconsistency 2019-04-18 20:02:14 -07:00
Cameron Gutman e51ad1a68a Fix use-after-free due to stale m_FrontendRenderer pointer. Fixes #214 2019-04-18 19:26:13 -07:00
Cameron Gutman 9c76700f74 Add MMAL renderer for Raspberry Pi 2019-04-16 01:20:21 -07:00
Cameron Gutman 773b1af6f1 Minor refactoring in preparation for non-hwaccel decoder support 2019-04-15 21:56:01 -07:00
Cameron Gutman b8b633a6cc Use SDL to render on Wayland for VAAPI 2019-04-12 22:54:21 -07:00
Cameron Gutman ada2270bd1 Reduce code duplication in renderers 2019-04-12 22:12:53 -07:00
Cameron Gutman 25e5175c54 Refactor parameter passing to decoders and split backend/decode-only and frontend renderers 2019-04-11 22:27:20 -07:00
Cameron Gutman 9dcd770df2 Fix alloc-dealloc mismatch 2019-04-09 21:51:39 -07:00
Cameron Gutman 00a1948b73 Treat AV_CODEC_CAP_HARDWARE codecs as hardware accelerated too 2019-04-09 21:50:22 -07:00
Cameron Gutman 859a5a5e0c Allow a renderer to opt-out of the render thread and use that for SDL on OGL 2019-04-09 21:46:14 -07:00
Cameron Gutman 6783cf57da TTF_OpenFontRW retains the SDL_RWops on success, so we must keep the font data around to back it 2019-04-09 21:26:47 -07:00
Cameron Gutman a8dc5ac14a Add App Notarization and Hardened Runtime for macOS builds 2019-04-09 18:30:25 -07:00
431 changed files with 21341 additions and 8311 deletions
+6 -1
View File
@@ -1,8 +1,10 @@
---
name: Bug report
about: Create a report to help us address broken functionality
about: Ask for help on Discord before reporting a bug
---
**READ ME FIRST!**
This issue tracker is not the right place to ask for troubleshooting help if you're not able to connect or stream at all. For those requests, please use the #pc-help channel on our Discord server where there are many more people who can help (or direct you back here if it looks like a Moonlight bug after all). https://moonlight-stream.org/discord
**Describe the bug**
A clear and concise description of what the bug is.
@@ -16,6 +18,9 @@ If applicable, add screenshots to help explain your problem. If the issue is rel
**Affected games**
List the games you've tried that exhibit the issue. To see if the issue is game-specific, try streaming Steam Big Picture with Moonlight and see if the issue persists there.
**Other Moonlight clients**
- Does the issue occur when using Moonlight on iOS or Android?
**Moonlight settings (please complete the following information)**
- Have any settings been adjusted from defaults?
- If so, which settings have been changed?
+1
View File
@@ -0,0 +1 @@
blank_issues_enabled: false
+4
View File
@@ -0,0 +1,4 @@
issuesOpened: >
If this is a question about Moonlight or you need help troubleshooting a streaming problem, please use the help channels on our [Discord server](https://moonlight-stream.org/discord) instead of GitHub issues. There are many more people available on Discord to help you and answer your questions.<br /><br />
This issue tracker should only be used for specific bugs or feature requests.<br /><br />
Thank you, and happy streaming!
-53
View File
@@ -1,53 +0,0 @@
language: cpp
git:
depth: 1
env:
- NJOBS=2
matrix:
include:
- os: osx
compiler: clang
- os: linux
sudo: required
dist: xenial
compiler: gcc
addons:
apt:
sources:
- sourceline: 'ppa:beineri/opt-qt597-xenial'
- sourceline: 'ppa:jonathonf/ffmpeg-4'
packages:
- qt59base
- qt59quickcontrols2
- qt59svg
- libesd0-dev
- libgl-dev
- libegl1-mesa-dev
- libxkbcommon-dev
- wayland-protocols
- libopus-dev
- libavcodec-dev
- libavutil-dev
- libva-dev
- libvdpau-dev
install:
- '[ "$TRAVIS_OS_NAME" != osx ] || brew install qt5'
- '[ "$TRAVIS_OS_NAME" != osx ] || brew link --force qt5'
- if [[ "$TRAVIS_OS_NAME" = "linux" ]]; then export SDL2_VER=2.0.9 && wget https://www.libsdl.org/release/SDL2-$SDL2_VER.tar.gz && tar -xvf SDL2-$SDL2_VER.tar.gz && cd SDL2-$SDL2_VER && ./configure && make -j$NJOBS && sudo make install && cd ..; fi
- if [[ "$TRAVIS_OS_NAME" = "linux" ]]; then export SDL2_TTF_VER=2.0.15 && wget https://www.libsdl.org/projects/SDL_ttf/release/SDL2_ttf-$SDL2_TTF_VER.tar.gz && tar -xvf SDL2_ttf-$SDL2_TTF_VER.tar.gz && cd SDL2_ttf-$SDL2_TTF_VER && ./configure && make -j$NJOBS && sudo make install && cd ..; fi
before_script:
- '[ "$TRAVIS_OS_NAME" != "linux" ] || source /opt/qt59/bin/qt59-env.sh'
- qmake -v
- '[[ "$TRAVIS_OS_NAME" != linux || "$CXX" != clang++ ]] || export QMAKESPEC=linux-clang'
- '[[ "$TRAVIS_OS_NAME" != linux || "$CXX" != g++ ]] || export QMAKESPEC=linux-g++'
- '[ "$TRAVIS_OS_NAME" != osx ] || export QMAKESPEC=macx-clang'
script:
- qmake moonlight-qt.pro -spec $QMAKESPEC
- make -j$NJOBS debug
- make -j$NJOBS release
+1 -5
View File
@@ -3,11 +3,7 @@ QT -= core gui
TARGET = AntiHooking
TEMPLATE = lib
# Support debug and release builds from command line for CI
CONFIG += debug_and_release
# Ensure symbols are always generated
CONFIG += force_debug_info
include(../globaldefs.pri)
INCLUDEPATH += $$PWD/../libs/windows/include
contains(QT_ARCH, i386) {
+71 -4
View File
@@ -131,14 +131,81 @@ private:
static LoadLibraryExWFunc s_RealLoadLibraryExW;
static constexpr LPCWSTR k_BlacklistedDlls[] = {
// This DLL shipped with ASUS Sonic Radar 3 improperly handles
// D3D9 exclusive fullscreen in a way that causes CreateDeviceEx()
// to deadlock. https://github.com/moonlight-stream/moonlight-qt/issues/102
L"NahimicOSD.dll",
// These A-Volute DLLs shipped with various audio driver packages improperly handle
// D3D9 exclusive fullscreen in a way that causes CreateDeviceEx() to deadlock.
// https://github.com/moonlight-stream/moonlight-qt/issues/102
L"NahimicOSD.dll", // ASUS Sonic Radar 3
L"SSAudioOSD.dll", // SteelSeries headsets
L"SS2OSD.dll", // ASUS Sonic Studio 2
L"Nahimic2OSD.dll",
L"NahimicMSIOSD.dll",
L"nhAsusPhoebusOSD.dll" // ASUS Phoebus
// This DLL has been seen in several crash reports. Some Googling
// suggests it's highly unstable and causes issues in many games.
L"EZFRD32.dll",
L"EZFRD64.dll",
// These are the newer dList DLLs for Optimus hybrid graphics DDI.
// https://docs.microsoft.com/en-us/windows-hardware/drivers/display/hybrid-system-ddi
//
// We forcefully block them from loading because Optimus has a bug that
// deadlocks DXVA2 when we present with D3DPRESENT_DONOTWAIT. This will prevent
// Optimus from ever using the dGPU even if the user has requested it.
// https://github.com/moonlight-stream/moonlight-qt/issues/240
// https://github.com/moonlight-stream/moonlight-qt/issues/235
L"nvdlist.dll",
L"nvdlistx.dll",
// These are the older dList/AppInit DLLs for Optimus hybrid graphics DDI.
// https://docs.microsoft.com/en-us/windows-hardware/drivers/display/hybrid-system-ddi
//
// These seem to cause a crash in PresentEx() in full-screen exclusive mode.
// This block will prevent Optimus from ever using the dGPU even if the user has requested it.
// https://github.com/moonlight-stream/moonlight-qt/issues/386
//
// d3d9!CSwapChain::BltToHybridPrimary+0x200:
// 00007ffa`23f37e58 488b01 mov rax,qword ptr [rcx] ds:00000000`00000038=????????????????
// 00 0000004e`496ff4e0 00007ffa`23f39e2c d3d9!CSwapChain::BltToHybridPrimary+0x200
// 01 0000004e`496ff880 00007ffa`23ee39ce d3d9!CSwapChain::FlipToSurface+0x15c
// 02 0000004e`496ff900 00007ffa`23f4dd75 d3d9!CSwapChain::PresentMain+0x3e13e
// 03 0000004e`496ffab0 00007ffa`23f4dccd d3d9!CBaseDevice::PresentMain+0x9d
// 04 0000004e`496ffb00 00007ff7`8e31016f d3d9!CBaseDevice::PresentEx+0xbd
// 05 0000004e`496ffb50 00007ff7`8e30df1e Moonlight!DXVA2Renderer::renderFrame+0x61f [C:\moonlight-qt\app\streaming\video\ffmpeg-renderers\dxva2.cpp @ 1035]
// 06 0000004e`496ffd50 00007ff7`8e30e46a Moonlight!Pacer::renderFrame+0x3e [C:\moonlight-qt\app\streaming\video\ffmpeg-renderers\pacer\pacer.cpp @ 265]
// 07 (Inline Function) --------`-------- Moonlight!Pacer::renderLastFrameAndUnlock+0x197 [C:\moonlight-qt\app\streaming\video\ffmpeg-renderers\pacer\pacer.cpp @ 156]
// 08 0000004e`496ffda0 00007ffa`14476978 Moonlight!Pacer::renderThread+0x1ca [C:\moonlight-qt\app\streaming\video\ffmpeg-renderers\pacer\pacer.cpp @ 88]
// 09 0000004e`496ffde0 00007ffa`14476ee2 SDL2!SDL_RunThread+0x38 [C:\Users\camer\SDL\src\thread\SDL_thread.c @ 276]
// 0a 0000004e`496ffe10 00007ffa`2aae0e82 SDL2!RunThread+0x12 [C:\Users\camer\SDL\src\thread\windows\SDL_systhread.c @ 83]
// 0b 0000004e`496ffe40 00007ffa`2d627bd4 ucrtbase!thread_start<unsigned int (__cdecl*)(void *),1>+0x42
// 0c 0000004e`496ffe70 00007ffa`2da4ce51 kernel32!BaseThreadInitThunk+0x14
// 0d 0000004e`496ffea0 00000000`00000000 ntdll!RtlUserThreadStart+0x21
//
L"nvinit.dll",
L"nvinitx.dll",
// In some unknown circumstances, RTSS tries to hook in the middle of an instruction, leaving garbage
// code inside d3d9.dll that causes a crash when executed:
//
// 0:000> u
// d3d9!D3D9GetCurrentOwnershipMode+0x5d:
// 00007ff8`95b95861 9b wait
// 00007ff8`95b95862 a7 cmps dword ptr [rsi],dword ptr [rdi] <--- crash happens here
// 00007ff8`95b95863 ff ???
// 00007ff8`95b95864 bfe8ca8a00 mov edi,8ACAE8h
// 00007ff8`95b95869 00eb add bl,ch
// 00007ff8`95b9586b f1 ???
// 00007ff8`95b9586c b808000000 mov eax,8
// 00007ff8`95b95871 ebe6 jmp d3d9!D3D9GetCurrentOwnershipMode+0x55 (00007ff8`95b95859)
//
// Disassembling starting at the exact address of the attempted hook yields the intended jmp instruction
//
// 0:000> u d3d9!D3D9GetCurrentOwnershipMode+0x5c:
// 00007ff8`95b95860 e99ba7ffbf jmp 00007ff8`55b90000
//
// Since the RTSS OSD doesn't even work with DXVA2, we'll just block the hooks entirely.
L"RTSSHooks.dll",
L"RTSSHooks64.dll",
};
};
+19 -14
View File
@@ -4,48 +4,53 @@
Moonlight also has mobile versions for [Android](https://github.com/moonlight-stream/moonlight-android) and [iOS](https://github.com/moonlight-stream/moonlight-ios).
You can follow development on our [Discord server](https://discord.gg/6ERtzFY).
You can follow development on our [Discord server](https://moonlight-stream.org/discord).
[![Windows AppVeyor Status](https://ci.appveyor.com/api/projects/status/glj5cxqwy2w3bglv/branch/master?svg=true)](https://ci.appveyor.com/project/cgutman/moonlight-qt/branch/master)
[![Mac and Linux Travis CI Status](https://travis-ci.org/moonlight-stream/moonlight-qt.svg?branch=master)](https://travis-ci.org/moonlight-stream/moonlight-qt)
[![AppVeyor Build Status](https://ci.appveyor.com/api/projects/status/glj5cxqwy2w3bglv/branch/master?svg=true)](https://ci.appveyor.com/project/cgutman/moonlight-qt/branch/master)
[![Downloads](https://img.shields.io/github/downloads/moonlight-stream/moonlight-qt/total)](https://github.com/moonlight-stream/moonlight-qt/releases)
## Features
- Hardware accelerated video decoding on Windows, Mac, and Linux
- 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
- 7.1 surround sound audio support
- Support for both pointer capture (for games) and direct mouse control (for remote desktop)
- Direct touchscreen mouse control
- HEVC support for better image quality at reduced bandwidth
- Keyboard and mouse support
- Gamepad support with force feedback for up to 4 players
## Downloads
- [Windows, macOS, and Steam Link](https://github.com/moonlight-stream/moonlight-qt/releases)
- [Snap (for Ubuntu-based Linux distros)](https://snapcraft.io/moonlight)
- [Flatpak (for other Linux distros)](https://flathub.org/apps/details/com.moonlight_stream.Moonlight)
- [AppImage](https://github.com/moonlight-stream/moonlight-qt/releases)
- [Raspberry Pi 4](https://github.com/moonlight-stream/moonlight-docs/wiki/Installing-Moonlight-Qt-on-Raspberry-Pi-4)
- [NVIDIA Jetson and Nintendo Switch (Ubuntu L4T)](https://github.com/moonlight-stream/moonlight-docs/wiki/Installing-Moonlight-Qt-on-Linux4Tegra-(L4T)-Ubuntu)
## Building
### General Build Requirements
* Qt 5.9 SDK or later
### Windows-specific Build Requirements
### Windows Build Requirements
* Qt 5.12.4 SDK or later
* Windows 7 or later
* [Visual Studio](https://visualstudio.microsoft.com/downloads/) 2015 or later (Community edition is fine)
* [Visual Studio](https://visualstudio.microsoft.com/downloads/) 2017 (Community edition is fine)
* Select MSVC Desktop toolchain during Qt installation
* [DirectX SDK](https://www.microsoft.com/en-us/download/details.aspx?id=6812)
* [7-Zip](https://www.7-zip.org/) (only if building installers for non-development PCs)
* [WiX Toolset](http://wixtoolset.org/releases/) v3.11 or later (only if building installers for non-development PCs)
### Mac-specific Build Requirements
* macOS Sierra (10.12) or later
* Xcode 10
### macOS Build Requirements
* Qt 5.14 SDK or later
* macOS High Sierra (10.13) or later
* Xcode 11
* [create-dmg](https://github.com/sindresorhus/create-dmg) (only if building DMGs for use on non-development Macs)
### Linux-specific Build Requirements
### Linux/Unix Build Requirements
* Qt 5.9 SDK or later
* GCC or Clang
* Install your distro equivalents of: `openssl-devel qt5-devel SDL2-devel ffmpeg-devel qt5-qtquickcontrols2-devel libva-devel libvdpau-devel opus-devel pulseaudio-libs-devel alsa-lib-devel SDL2_ttf-devel`
* FFmpeg 4.0 is required to build. If your distro doesn't package FFmpeg 4.0, you can build and install it from source on http://ffmpeg.org/
### Steam Link-specific Build Requirements
### Steam Link Build Requirements
* [Steam Link SDK](https://github.com/ValveSoftware/steamlink-sdk) cloned on your build system
* STEAMLINK_SDK_PATH environment variable set to the Steam Link SDK path
+86 -18
View File
@@ -8,11 +8,7 @@ unix:!macx {
TARGET = Moonlight
}
# Support debug and release builds from command line for CI
CONFIG += debug_and_release
# Ensure symbols are always generated
CONFIG += force_debug_info
include(../globaldefs.pri)
# Precompile QML files to avoid writing qmlcache on portable versions.
# Since this binds the app against the Qt runtime version, we will only
@@ -50,7 +46,7 @@ win32 {
LIBS += -L$$(DXSDK_DIR)/Lib/x64
}
LIBS += ws2_32.lib winmm.lib dxva2.lib ole32.lib gdi32.lib user32.lib d3d9.lib dwmapi.lib dbghelp.lib
LIBS += ws2_32.lib winmm.lib dxva2.lib ole32.lib gdi32.lib user32.lib d3d9.lib dwmapi.lib dbghelp.lib qwave.lib
}
macx {
INCLUDEPATH += $$PWD/../libs/mac/include
@@ -88,12 +84,35 @@ unix:!macx {
packagesExist(libva-wayland) {
CONFIG += libva-wayland
}
packagesExist(libva-drm) {
CONFIG += libva-drm
}
CONFIG += libva
}
packagesExist(vdpau) {
CONFIG += libvdpau
}
packagesExist(mmal) {
PKGCONFIG += mmal
CONFIG += mmal
}
packagesExist(libdrm) {
PKGCONFIG += libdrm
CONFIG += libdrm
}
}
packagesExist(wayland-client) {
DEFINES += HAS_WAYLAND
PKGCONFIG += wayland-client
}
packagesExist(x11) {
DEFINES += HAS_X11
PKGCONFIG += x11
}
}
win32 {
@@ -101,19 +120,20 @@ win32 {
CONFIG += ffmpeg
}
win32:!winrt {
CONFIG += soundio
CONFIG += soundio discord-rpc
}
macx {
LIBS += -lssl -lcrypto -lavcodec.58 -lavutil.56 -lopus -framework SDL2 -framework SDL2_ttf
LIBS += -lobjc -framework VideoToolbox -framework AVFoundation -framework CoreVideo -framework CoreGraphics -framework CoreMedia -framework AppKit
LIBS += -lobjc -framework VideoToolbox -framework AVFoundation -framework CoreVideo -framework CoreGraphics -framework CoreMedia -framework AppKit -framework Metal
# For libsoundio
LIBS += -framework CoreAudio -framework AudioUnit
CONFIG += ffmpeg soundio
CONFIG += ffmpeg soundio discord-rpc
}
SOURCES += \
backend/nvapp.cpp \
main.cpp \
backend/computerseeker.cpp \
backend/identitymanager.cpp \
@@ -122,11 +142,17 @@ SOURCES += \
backend/nvpairingmanager.cpp \
backend/computermanager.cpp \
backend/boxartmanager.cpp \
backend/richpresencemanager.cpp \
cli/commandlineparser.cpp \
cli/quitstream.cpp \
cli/startstream.cpp \
settings/streamingpreferences.cpp \
streaming/input.cpp \
streaming/input/abstouch.cpp \
streaming/input/gamepad.cpp \
streaming/input/input.cpp \
streaming/input/keyboard.cpp \
streaming/input/mouse.cpp \
streaming/input/reltouch.cpp \
streaming/session.cpp \
streaming/audio/audio.cpp \
streaming/audio/renderers/sdlaud.cpp \
@@ -138,9 +164,11 @@ SOURCES += \
settings/mappingmanager.cpp \
gui/sdlgamepadkeynavigation.cpp \
streaming/video/overlaymanager.cpp \
backend/systemproperties.cpp
backend/systemproperties.cpp \
wm.cpp
HEADERS += \
backend/nvapp.h \
utils.h \
backend/computerseeker.h \
backend/identitymanager.h \
@@ -149,11 +177,12 @@ HEADERS += \
backend/nvpairingmanager.h \
backend/computermanager.h \
backend/boxartmanager.h \
backend/richpresencemanager.h \
cli/commandlineparser.h \
cli/quitstream.h \
cli/startstream.h \
settings/streamingpreferences.h \
streaming/input.h \
streaming/input/input.h \
streaming/session.h \
streaming/audio/renderers/renderer.h \
streaming/audio/renderers/sdl.h \
@@ -176,6 +205,7 @@ ffmpeg {
SOURCES += \
streaming/video/ffmpeg.cpp \
streaming/video/ffmpeg-renderers/sdlvid.cpp \
streaming/video/ffmpeg-renderers/cuda.cpp \
streaming/video/ffmpeg-renderers/pacer/pacer.cpp \
streaming/video/ffmpeg-renderers/pacer/nullthreadedvsyncsource.cpp
@@ -183,6 +213,7 @@ ffmpeg {
streaming/video/ffmpeg.h \
streaming/video/ffmpeg-renderers/renderer.h \
streaming/video/ffmpeg-renderers/sdlvid.h \
streaming/video/ffmpeg-renderers/cuda.h \
streaming/video/ffmpeg-renderers/pacer/pacer.h \
streaming/video/ffmpeg-renderers/pacer/nullthreadedvsyncsource.h
}
@@ -206,6 +237,12 @@ libva-wayland {
PKGCONFIG += libva-wayland
DEFINES += HAVE_LIBVA_WAYLAND
}
libva-wayland {
message(VAAPI DRM support enabled)
PKGCONFIG += libva-drm
DEFINES += HAVE_LIBVA_DRM
}
libvdpau {
message(VDPAU renderer selected)
@@ -213,6 +250,30 @@ libvdpau {
SOURCES += streaming/video/ffmpeg-renderers/vdpau.cpp
HEADERS += streaming/video/ffmpeg-renderers/vdpau.h
}
mmal {
message(MMAL renderer selected)
DEFINES += HAVE_MMAL
SOURCES += streaming/video/ffmpeg-renderers/mmal.cpp
HEADERS += streaming/video/ffmpeg-renderers/mmal.h
}
libdrm {
message(DRM renderer selected)
DEFINES += HAVE_DRM
SOURCES += streaming/video/ffmpeg-renderers/drm.cpp
HEADERS += streaming/video/ffmpeg-renderers/drm.h
}
config_EGL {
message(EGL renderer selected)
CONFIG += egl
DEFINES += HAVE_EGL
SOURCES += \
streaming/video/ffmpeg-renderers/eglvid.cpp \
streaming/video/ffmpeg-renderers/egl_extensions.cpp
HEADERS += streaming/video/ffmpeg-renderers/eglvid.h
}
config_SL {
message(Steam Link build configuration selected)
@@ -241,12 +302,10 @@ macx {
message(VideoToolbox renderer selected)
SOURCES += \
streaming/video/ffmpeg-renderers/vt.mm \
streaming/video/ffmpeg-renderers/pacer/displaylinkvsyncsource.mm
streaming/video/ffmpeg-renderers/vt.mm
HEADERS += \
streaming/video/ffmpeg-renderers/vt.h \
streaming/video/ffmpeg-renderers/pacer/displaylinkvsyncsource.h
streaming/video/ffmpeg-renderers/vt.h
}
soundio {
message(libsoundio audio renderer selected)
@@ -255,6 +314,12 @@ soundio {
SOURCES += streaming/audio/renderers/soundioaudiorenderer.cpp
HEADERS += streaming/audio/renderers/soundioaudiorenderer.h
}
discord-rpc {
message(Discord integration enabled)
LIBS += -ldiscord-rpc
DEFINES += HAVE_DISCORD
}
RESOURCES += \
resources.qrc \
@@ -347,10 +412,13 @@ macx {
APP_BUNDLE_RESOURCES.files = moonlight.icns
APP_BUNDLE_RESOURCES.path = Contents/Resources
APP_BUNDLE_FRAMEWORKS.files = $$files(../libs/mac/Frameworks/*.framework, true)
APP_BUNDLE_FRAMEWORKS.files = $$files(../libs/mac/Frameworks/*.framework, true) $$files(../libs/mac/lib/*.dylib, true)
APP_BUNDLE_FRAMEWORKS.path = Contents/Frameworks
QMAKE_BUNDLE_DATA += APP_BUNDLE_RESOURCES APP_BUNDLE_FRAMEWORKS
APP_BUNDLE_PLIST.files = $$OUT_PWD/Info.plist
APP_BUNDLE_PLIST.path = Contents
QMAKE_BUNDLE_DATA += APP_BUNDLE_RESOURCES APP_BUNDLE_FRAMEWORKS APP_BUNDLE_PLIST
QMAKE_RPATHDIR += @executable_path/../Frameworks
}
+25 -3
View File
@@ -28,7 +28,18 @@ AutoUpdateChecker::AutoUpdateChecker(QObject *parent) :
void AutoUpdateChecker::start()
{
#if defined(Q_OS_WIN32) || defined(Q_OS_DARWIN) // Only run update checker on platforms without auto-update
#if defined(Q_OS_WIN32) || defined(Q_OS_DARWIN) || defined(STEAM_LINK) || defined(APP_IMAGE) // Only run update checker on platforms without auto-update
#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0) && !defined(QT_NO_BEARERMANAGEMENT)
// HACK: Set network accessibility to work around QTBUG-80947.
// Even though it was fixed in 5.14.2, it still breaks for users attempting to
// directly connect their computers without a router using APIPA and in some cases
// using OpenVPN with IPv6 enabled. https://github.com/moonlight-stream/moonlight-qt/issues/375
QT_WARNING_PUSH
QT_WARNING_DISABLE_DEPRECATED
m_Nam.setNetworkAccessible(QNetworkAccessManager::Accessible);
QT_WARNING_POP
#endif
// We'll get a callback when this is finished
QUrl url("https://moonlight-stream.org/updates/qt.json");
m_Nam.get(QNetworkRequest(url));
@@ -43,6 +54,17 @@ void AutoUpdateChecker::parseStringToVersionQuad(QString& string, QVector<int>&
}
}
QString AutoUpdateChecker::getPlatform()
{
#if defined(STEAM_LINK)
return QStringLiteral("steamlink");
#elif defined(APP_IMAGE)
return QStringLiteral("appimage");
#else
return QSysInfo::productType();
#endif
}
void AutoUpdateChecker::handleUpdateCheckRequestFinished(QNetworkReply* reply)
{
Q_ASSERT(reply->isFinished());
@@ -88,7 +110,7 @@ void AutoUpdateChecker::handleUpdateCheckRequestFinished(QNetworkReply* reply)
}
if (updateObj["arch"] == QSysInfo::buildCpuArchitecture() &&
updateObj["platform"] == QSysInfo::productType()) {
updateObj["platform"] == getPlatform()) {
qDebug() << "Found update manifest match for current platform";
QString latestVersion = updateObj["version"].toString();
@@ -137,7 +159,7 @@ void AutoUpdateChecker::handleUpdateCheckRequestFinished(QNetworkReply* reply)
}
qWarning() << "No entry in update manifest found for current platform: "
<< QSysInfo::buildCpuArchitecture() << QSysInfo::productType();
<< QSysInfo::buildCpuArchitecture() << getPlatform();
}
else {
qWarning() << "Update checking failed with error: " << reply->error();
+2
View File
@@ -20,6 +20,8 @@ private slots:
private:
void parseStringToVersionQuad(QString& string, QVector<int>& version);
QString getPlatform();
QVector<int> m_CurrentVersionQuad;
QNetworkAccessManager m_Nam;
};
+10 -7
View File
@@ -71,10 +71,10 @@ private:
QUrl BoxArtManager::loadBoxArt(NvComputer* computer, NvApp& app)
{
// Try to open the cached file
QString cacheFilePath = getFilePathForBoxArt(computer, app.id);
if (QFile::exists(cacheFilePath)) {
return QUrl::fromLocalFile(cacheFilePath);
// Try to open the cached file if it exists and contains data
QFile cacheFile(getFilePathForBoxArt(computer, app.id));
if (cacheFile.exists() && cacheFile.size() > 0) {
return QUrl::fromLocalFile(cacheFile.fileName());
}
// If we get here, we need to fetch asynchronously.
@@ -89,10 +89,9 @@ QUrl BoxArtManager::loadBoxArt(NvComputer* computer, NvApp& app)
void BoxArtManager::handleBoxArtLoadComplete(NvComputer* computer, NvApp app, QUrl image)
{
if (image.isEmpty()) {
image = QUrl("qrc:/res/no_app_image.png");
if (!image.isEmpty()) {
emit boxArtLoadComplete(computer, app, image);
}
emit boxArtLoadComplete(computer, app, image);
}
QUrl BoxArtManager::loadBoxArtFromNetwork(NvComputer* computer, int appId)
@@ -110,6 +109,10 @@ QUrl BoxArtManager::loadBoxArtFromNetwork(NvComputer* computer, int appId)
if (image.save(cachePath)) {
return QUrl::fromLocalFile(cachePath);
}
else {
// A failed save() may leave a zero byte file. Make sure that's removed.
QFile(cachePath).remove();
}
}
return QUrl();
+123 -26
View File
@@ -32,7 +32,7 @@ private:
QString serverInfo;
try {
serverInfo = http.getServerInfo(NvHTTP::NvLogLevel::NVLL_NONE);
serverInfo = http.getServerInfo(NvHTTP::NvLogLevel::NVLL_NONE, true);
} catch (...) {
return false;
}
@@ -84,7 +84,7 @@ private:
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 (int i = 0; i < (wasOnline ? TRIES_BEFORE_OFFLINING : 1) && !online; i++) {
for (auto& address : m_Computer->uniqueAddresses()) {
if (isInterruptionRequested()) {
return;
@@ -209,13 +209,50 @@ void ComputerManager::saveHosts()
settings.remove(SER_HOSTS);
settings.beginWriteArray(SER_HOSTS);
for (int i = 0; i < m_KnownHosts.count(); i++) {
settings.setArrayIndex(i);
m_KnownHosts.value(m_KnownHosts.keys()[i])->serialize(settings);
int i = 0;
for (const NvComputer* computer : m_KnownHosts) {
settings.setArrayIndex(i++);
computer->serialize(settings);
}
settings.endArray();
}
QHostAddress ComputerManager::getBestGlobalAddressV6(QVector<QHostAddress> &addresses)
{
for (const QHostAddress& address : addresses) {
if (address.protocol() == QAbstractSocket::IPv6Protocol) {
if (address.isInSubnet(QHostAddress("fe80::"), 10)) {
// Link-local
continue;
}
if (address.isInSubnet(QHostAddress("fec0::"), 10)) {
qInfo() << "Ignoring site-local address:" << address;
continue;
}
if (address.isInSubnet(QHostAddress("fc00::"), 7)) {
qInfo() << "Ignoring ULA:" << address;
continue;
}
if (address.isInSubnet(QHostAddress("2002::"), 16)) {
qInfo() << "Ignoring 6to4 address:" << address;
continue;
}
if (address.isInSubnet(QHostAddress("2001::"), 32)) {
qInfo() << "Ignoring Teredo address:" << address;
continue;
}
return address;
}
}
return QHostAddress();
}
void ComputerManager::startPolling()
{
QWriteLocker lock(&m_Lock);
@@ -233,9 +270,9 @@ void ComputerManager::startPolling()
this, [this](const QMdnsEngine::Service& service) {
qInfo() << "Discovered mDNS host:" << service.hostname();
MdnsPendingComputer* pendingComputer = new MdnsPendingComputer(&m_MdnsServer, &m_MdnsCache, service);
connect(pendingComputer, SIGNAL(resolvedv4(MdnsPendingComputer*,QHostAddress)),
this, SLOT(handleMdnsServiceResolved(MdnsPendingComputer*,QHostAddress)));
MdnsPendingComputer* pendingComputer = new MdnsPendingComputer(&m_MdnsServer, service);
connect(pendingComputer, &MdnsPendingComputer::resolvedHost,
this, &ComputerManager::handleMdnsServiceResolved);
m_PendingResolution.append(pendingComputer);
});
}
@@ -277,11 +314,39 @@ void ComputerManager::startPollingComputer(NvComputer* computer)
}
void ComputerManager::handleMdnsServiceResolved(MdnsPendingComputer* computer,
const QHostAddress& address)
QVector<QHostAddress>& addresses)
{
qInfo() << "Resolved" << computer->hostname() << "to" << address.toString();
QHostAddress v6Global = getBestGlobalAddressV6(addresses);
bool added = false;
addNewHost(address.toString(), true);
// Add the host using the IPv4 address
for (const QHostAddress& address : addresses) {
if (address.protocol() == QAbstractSocket::IPv4Protocol) {
// NB: We don't just call addNewHost() here with v6Global because the IPv6
// address may not be reachable (if the user hasn't installed the IPv6 helper yet
// or if this host lacks outbound IPv6 capability). We want to add IPv6 even if
// it's not currently reachable.
addNewHost(address.toString(), true, v6Global);
added = true;
break;
}
}
if (!added) {
// If we get here, there wasn't an IPv4 address so we'll do it v6-only
for (const QHostAddress& address : addresses) {
if (address.protocol() == QAbstractSocket::IPv6Protocol) {
// Use a link-local or site-local address for the "local address"
if (address.isInSubnet(QHostAddress("fe80::"), 10) ||
address.isInSubnet(QHostAddress("fec0::"), 10) ||
address.isInSubnet(QHostAddress("fc00::"), 7)) {
addNewHost(address.toString(), true, v6Global);
added = true;
break;
}
}
}
}
m_PendingResolution.removeOne(computer);
computer->deleteLater();
@@ -351,6 +416,19 @@ void ComputerManager::deleteHost(NvComputer* computer)
QThreadPool::globalInstance()->start(new DeferredHostDeletionTask(this, computer));
}
void ComputerManager::renameHost(NvComputer* computer, QString name)
{
{
QWriteLocker(&computer->lock);
computer->name = name;
computer->hasCustomName = true;
}
// Notify the UI of the state change
handleComputerStateChanged(computer);
}
void ComputerManager::handleAboutToQuit()
{
QWriteLocker lock(&m_Lock);
@@ -508,9 +586,10 @@ class PendingAddTask : public QObject, public QRunnable
Q_OBJECT
public:
PendingAddTask(ComputerManager* computerManager, QString address, bool mdns)
PendingAddTask(ComputerManager* computerManager, QString address, QHostAddress mdnsIpv6Address, bool mdns)
: m_ComputerManager(computerManager),
m_Address(address),
m_MdnsIpv6Address(mdnsIpv6Address),
m_Mdns(mdns)
{
connect(this, &PendingAddTask::computerAddCompleted,
@@ -560,16 +639,21 @@ private:
{
NvHTTP http(m_Address, QSslCertificate());
qInfo() << "Processing new PC at" << m_Address << "from" << (m_Mdns ? "mDNS" : "user");
qInfo() << "Processing new PC at" << m_Address << "from" << (m_Mdns ? "mDNS" : "user") << m_MdnsIpv6Address;
// Perform initial serverinfo fetch over HTTP since we don't know which cert to use
QString serverInfo = fetchServerInfo(http);
if (serverInfo.isEmpty() && !m_MdnsIpv6Address.isNull()) {
// Retry using the global IPv6 address if the IPv4 or link-local IPv6 address fails
http.setAddress(m_MdnsIpv6Address.toString());
serverInfo = fetchServerInfo(http);
}
if (serverInfo.isEmpty()) {
return;
}
// Create initial newComputer using HTTP serverinfo with no pinned cert
NvComputer* newComputer = new NvComputer(m_Address, serverInfo, QSslCertificate());
NvComputer* newComputer = new NvComputer(http.address(), serverInfo, QSslCertificate());
// Check if we have a record of this host UUID to pull the pinned cert
NvComputer* existingComputer;
@@ -589,22 +673,34 @@ private:
}
// Update the polled computer with the HTTPS information
NvComputer httpsComputer(m_Address, serverInfo, QSslCertificate());
NvComputer httpsComputer(http.address(), serverInfo, QSslCertificate());
newComputer->update(httpsComputer);
}
// Update addresses depending on the context
if (m_Mdns) {
newComputer->localAddress = m_Address;
// Get the WAN IP address using STUN if we're on mDNS
quint32 addr;
int err = LiFindExternalAddressIP4("stun.moonlight-stream.org", 3478, &addr);
if (err == 0) {
newComputer->remoteAddress = QHostAddress(qFromBigEndian(addr)).toString();
// Only update local address if we actually reached the PC via this address.
// If we reached it via the IPv6 address after the local address failed,
// don't store the non-working local address.
if (http.address() == m_Address) {
newComputer->localAddress = m_Address;
}
else {
qWarning() << "STUN failed to get WAN address:" << err;
// Get the WAN IP address using STUN if we're on mDNS over IPv4
if (QHostAddress(newComputer->localAddress).protocol() == QAbstractSocket::IPv4Protocol) {
quint32 addr;
int err = LiFindExternalAddressIP4("stun.moonlight-stream.org", 3478, &addr);
if (err == 0) {
newComputer->remoteAddress = QHostAddress(qFromBigEndian(addr)).toString();
}
else {
qWarning() << "STUN failed to get WAN address:" << err;
}
}
if (!m_MdnsIpv6Address.isNull()) {
Q_ASSERT(m_MdnsIpv6Address.protocol() == QAbstractSocket::IPv6Protocol);
newComputer->ipv6Address = m_MdnsIpv6Address.toString();
}
}
else {
@@ -657,14 +753,15 @@ private:
ComputerManager* m_ComputerManager;
QString m_Address;
QHostAddress m_MdnsIpv6Address;
bool m_Mdns;
};
void ComputerManager::addNewHost(QString address, bool mdns)
void ComputerManager::addNewHost(QString address, bool mdns, QHostAddress mdnsIpv6Address)
{
// Punt to a worker thread to avoid stalling the
// UI while waiting for serverinfo query to complete
PendingAddTask* addTask = new PendingAddTask(this, address, mdns);
PendingAddTask* addTask = new PendingAddTask(this, address, mdnsIpv6Address, mdns);
QThreadPool::globalInstance()->start(addTask);
}
+44 -12
View File
@@ -13,6 +13,7 @@
#include <QReadWriteLock>
#include <QSettings>
#include <QRunnable>
#include <QTimer>
class MdnsPendingComputer : public QObject
{
@@ -20,13 +21,18 @@ class MdnsPendingComputer : public QObject
public:
explicit MdnsPendingComputer(QMdnsEngine::Server* server,
QMdnsEngine::Cache* cache,
const QMdnsEngine::Service& service)
: m_Hostname(service.hostname()),
m_Resolver(server, m_Hostname, cache)
m_Server(server),
m_Resolver(nullptr)
{
connect(&m_Resolver, SIGNAL(resolved(QHostAddress)),
this, SLOT(handleResolved(QHostAddress)));
// Start resolving
resolve();
}
virtual ~MdnsPendingComputer()
{
delete m_Resolver;
}
QString hostname()
@@ -35,20 +41,42 @@ public:
}
private slots:
void handleResolved(const QHostAddress& address)
void handleResolvedTimeout()
{
if (address.protocol() == QAbstractSocket::IPv4Protocol) {
m_Resolver.disconnect();
emit resolvedv4(this, address);
if (m_Addresses.isEmpty()) {
// Try again
qInfo() << "Resolving" << hostname() << "timed out. Retrying...";
resolve();
}
else {
Q_ASSERT(!m_Addresses.isEmpty());
emit resolvedHost(this, m_Addresses);
}
}
void handleResolvedAddress(const QHostAddress& address)
{
qInfo() << "Resolved" << hostname() << "to" << address;
m_Addresses.push_back(address);
}
signals:
void resolvedv4(MdnsPendingComputer*, const QHostAddress&);
void resolvedHost(MdnsPendingComputer*,QVector<QHostAddress>&);
private:
void resolve()
{
delete m_Resolver;
m_Resolver = new QMdnsEngine::Resolver(m_Server, m_Hostname);
connect(m_Resolver, &QMdnsEngine::Resolver::resolved,
this, &MdnsPendingComputer::handleResolvedAddress);
QTimer::singleShot(2000, this, SLOT(handleResolvedTimeout()));
}
QByteArray m_Hostname;
QMdnsEngine::Resolver m_Resolver;
QMdnsEngine::Server* m_Server;
QMdnsEngine::Resolver* m_Resolver;
QVector<QHostAddress> m_Addresses;
};
class ComputerPollingEntry
@@ -140,7 +168,7 @@ public:
Q_INVOKABLE void stopPollingAsync();
Q_INVOKABLE void addNewHost(QString address, bool mdns);
Q_INVOKABLE void addNewHost(QString address, bool mdns, QHostAddress mdnsIpv6Address = QHostAddress());
void pairHost(NvComputer* computer, QString pin);
@@ -151,6 +179,8 @@ public:
// computer is deleted inside this call
void deleteHost(NvComputer* computer);
void renameHost(NvComputer* computer, QString name);
signals:
void computerStateChanged(NvComputer* computer);
@@ -165,11 +195,13 @@ private slots:
void handleComputerStateChanged(NvComputer* computer);
void handleMdnsServiceResolved(MdnsPendingComputer* computer, const QHostAddress& address);
void handleMdnsServiceResolved(MdnsPendingComputer* computer, QVector<QHostAddress>& addresses);
private:
void saveHosts();
QHostAddress getBestGlobalAddressV6(QVector<QHostAddress>& addresses);
void startPollingComputer(NvComputer* computer);
int m_PollingRef;
+12 -5
View File
@@ -39,11 +39,18 @@ void ComputerSeeker::onComputerUpdated(NvComputer *computer)
bool ComputerSeeker::matchComputer(NvComputer *computer) const
{
QString value = m_ComputerName.toLower();
return computer->name.toLower() == value ||
computer->localAddress.toLower() == value ||
computer->remoteAddress.toLower() == value ||
computer->manualAddress.toLower() == value ||
computer->uuid.toLower() == value;
if (computer->name.toLower() == value || computer->uuid.toLower() == value) {
return true;
}
for (const QString& addr : computer->uniqueAddresses()) {
if (addr.toLower() == value) {
return true;
}
}
return false;
}
bool ComputerSeeker::isOnline(NvComputer *computer) const
+1 -1
View File
@@ -75,7 +75,7 @@ void IdentityManager::createCredentials(QSettings& settings)
-1, -1, 0);
X509_set_issuer_name(cert, name);
X509_sign(cert, pk, EVP_sha1());
X509_sign(cert, pk, EVP_sha256());
BIO* biokey = BIO_new(BIO_s_mem());
THROW_BAD_ALLOC_IF_NULL(biokey);
+22
View File
@@ -0,0 +1,22 @@
#include "nvapp.h"
#define SER_APPNAME "name"
#define SER_APPID "id"
#define SER_APPHDR "hdr"
#define SER_APPCOLLECTOR "appcollector"
NvApp::NvApp(QSettings& settings)
{
name = settings.value(SER_APPNAME).toString();
id = settings.value(SER_APPID).toInt();
hdrSupported = settings.value(SER_APPHDR).toBool();
isAppCollectorGame = settings.value(SER_APPCOLLECTOR).toBool();
}
void NvApp::serialize(QSettings& settings) const
{
settings.setValue(SER_APPNAME, name);
settings.setValue(SER_APPID, id);
settings.setValue(SER_APPHDR, hdrSupported);
settings.setValue(SER_APPCOLLECTOR, isAppCollectorGame);
}
+30
View File
@@ -0,0 +1,30 @@
#pragma once
#include <QSettings>
class NvApp
{
public:
NvApp() {}
explicit NvApp(QSettings& settings);
bool operator==(const NvApp& other) const
{
return id == other.id;
}
bool isInitialized()
{
return id != 0 && !name.isEmpty();
}
void
serialize(QSettings& settings) const;
int id = 0;
QString name;
bool hdrSupported = false;
bool isAppCollectorGame = false;
};
Q_DECLARE_METATYPE(NvApp)
+100 -18
View File
@@ -1,8 +1,10 @@
#include "nvcomputer.h"
#include "nvapp.h"
#include <QUdpSocket>
#include <QHostInfo>
#include <QNetworkInterface>
#include <QNetworkProxy>
#define SER_NAME "hostname"
#define SER_UUID "uuid"
@@ -10,33 +12,28 @@
#define SER_LOCALADDR "localaddress"
#define SER_REMOTEADDR "remoteaddress"
#define SER_MANUALADDR "manualaddress"
#define SER_IPV6ADDR "ipv6address"
#define SER_APPLIST "apps"
#define SER_SRVCERT "srvcert"
#define SER_APPNAME "name"
#define SER_APPID "id"
#define SER_APPHDR "hdr"
#define SER_CUSTOMNAME "customname"
NvComputer::NvComputer(QSettings& settings)
{
this->name = settings.value(SER_NAME).toString();
this->uuid = settings.value(SER_UUID).toString();
this->hasCustomName = settings.value(SER_CUSTOMNAME).toBool();
this->macAddress = settings.value(SER_MAC).toByteArray();
this->localAddress = settings.value(SER_LOCALADDR).toString();
this->remoteAddress = settings.value(SER_REMOTEADDR).toString();
this->ipv6Address = settings.value(SER_IPV6ADDR).toString();
this->manualAddress = settings.value(SER_MANUALADDR).toString();
this->serverCert = QSslCertificate(settings.value(SER_SRVCERT).toByteArray());
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();
NvApp app(settings);
this->appList.append(app);
}
settings.endArray();
@@ -54,15 +51,17 @@ NvComputer::NvComputer(QSettings& settings)
this->gpuModel = nullptr;
}
void NvComputer::serialize(QSettings& settings)
void NvComputer::serialize(QSettings& settings) const
{
QReadLocker lock(&this->lock);
settings.setValue(SER_NAME, name);
settings.setValue(SER_CUSTOMNAME, hasCustomName);
settings.setValue(SER_UUID, uuid);
settings.setValue(SER_MAC, macAddress);
settings.setValue(SER_LOCALADDR, localAddress);
settings.setValue(SER_REMOTEADDR, remoteAddress);
settings.setValue(SER_IPV6ADDR, ipv6Address);
settings.setValue(SER_MANUALADDR, manualAddress);
settings.setValue(SER_SRVCERT, serverCert.toPem());
@@ -72,10 +71,7 @@ void NvComputer::serialize(QSettings& settings)
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);
appList[i].serialize(settings);
}
settings.endArray();
}
@@ -92,6 +88,7 @@ NvComputer::NvComputer(QString address, QString serverInfo, QSslCertificate serv
{
this->serverCert = serverCert;
this->hasCustomName = false;
this->name = NvHTTP::getXmlString(serverInfo, "hostname");
if (this->name.isEmpty()) {
this->name = "UNKNOWN";
@@ -129,7 +126,12 @@ NvComputer::NvComputer(QString address, QString serverInfo, QSslCertificate serv
mode2.width * mode2.height * mode2.refreshRate;
});
// We can get an IPv4 loopback address if we're using the GS IPv6 Forwarder
this->localAddress = NvHTTP::getXmlString(serverInfo, "LocalIP");
if (this->localAddress.startsWith("127.")) {
this->localAddress = QString();
}
this->remoteAddress = NvHTTP::getXmlString(serverInfo, "ExternalIP");
this->pairState = NvHTTP::getXmlString(serverInfo, "PairStatus") == "1" ?
PS_PAIRED : PS_NOT_PAIRED;
@@ -156,7 +158,7 @@ bool NvComputer::wake()
const quint16 WOL_PORTS[] = {
7, 9, // Standard WOL ports
47998, 47999, 48000, // Ports opened by GFE
47998, 47999, 48000, 48002, 48010, // Ports opened by GFE
};
// Create the WoL payload
@@ -229,7 +231,82 @@ bool NvComputer::wake()
return success;
}
QVector<QString> NvComputer::uniqueAddresses()
bool NvComputer::isReachableOverVpn()
{
if (activeAddress.isEmpty()) {
return false;
}
QTcpSocket s;
s.setProxy(QNetworkProxy::NoProxy);
s.connectToHost(activeAddress, 47984);
if (s.waitForConnected(3000)) {
Q_ASSERT(!s.localAddress().isNull());
for (const QNetworkInterface& nic : QNetworkInterface::allInterfaces()) {
// Ensure the interface is up
if ((nic.flags() & QNetworkInterface::IsUp) == 0) {
continue;
}
for (const QNetworkAddressEntry& addr : nic.addressEntries()) {
if (addr.ip() == s.localAddress()) {
qInfo() << "Found matching interface:" << nic.humanReadableName() << nic.hardwareAddress() << nic.flags();
#if QT_VERSION >= QT_VERSION_CHECK(5, 11, 0)
qInfo() << "Interface Type:" << nic.type();
qInfo() << "Interface MTU:" << nic.maximumTransmissionUnit();
if (nic.type() == QNetworkInterface::Virtual ||
nic.type() == QNetworkInterface::Ppp) {
// Treat PPP and virtual interfaces as likely VPNs
return true;
}
if (nic.maximumTransmissionUnit() != 0 && nic.maximumTransmissionUnit() < 1500) {
// Treat MTUs under 1500 as likely VPNs
return true;
}
#endif
if (nic.flags() & QNetworkInterface::IsPointToPoint) {
// Treat point-to-point links as likely VPNs.
// This check detects OpenVPN on Unix-like OSes.
return true;
}
if (nic.hardwareAddress().startsWith("00:FF", Qt::CaseInsensitive)) {
// OpenVPN TAP interfaces have a MAC address starting with 00:FF on Windows
return true;
}
if (nic.humanReadableName().startsWith("ZeroTier")) {
// ZeroTier interfaces always start with "ZeroTier"
return true;
}
if (nic.humanReadableName().contains("VPN")) {
// This one is just a final heuristic if all else fails
return true;
}
// Didn't meet any of our VPN heuristics
return false;
}
}
}
qWarning() << "No match found for address:" << s.localAddress();
return false;
}
else {
// If we fail to connect, just pretend that it's not a VPN
return false;
}
}
QVector<QString> NvComputer::uniqueAddresses() const
{
QVector<QString> uniqueAddressList;
@@ -237,6 +314,7 @@ QVector<QString> NvComputer::uniqueAddresses()
uniqueAddressList.append(activeAddress);
uniqueAddressList.append(localAddress);
uniqueAddressList.append(remoteAddress);
uniqueAddressList.append(ipv6Address);
uniqueAddressList.append(manualAddress);
// Prune duplicates (always giving precedence to the first)
@@ -292,10 +370,14 @@ bool NvComputer::update(NvComputer& that)
changed = true; \
}
ASSIGN_IF_CHANGED(name);
if (!hasCustomName) {
// Only overwrite the name if it's not custom
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(ipv6Address);
ASSIGN_IF_CHANGED_AND_NONEMPTY(manualAddress);
ASSIGN_IF_CHANGED(pairState);
ASSIGN_IF_CHANGED(serverCodecModeSupport);
+8 -3
View File
@@ -29,11 +29,14 @@ public:
bool
wake();
bool
isReachableOverVpn();
QVector<QString>
uniqueAddresses();
uniqueAddresses() const;
void
serialize(QSettings& settings);
serialize(QSettings& settings) const;
enum PairState
{
@@ -64,13 +67,15 @@ public:
// Persisted traits
QString localAddress;
QString remoteAddress;
QString ipv6Address;
QString manualAddress;
QByteArray macAddress;
QString name;
bool hasCustomName;
QString uuid;
QSslCertificate serverCert;
QVector<NvApp> appList;
// Synchronization
QReadWriteLock lock;
mutable QReadWriteLock lock;
};
+57 -48
View File
@@ -12,24 +12,22 @@
#include <QtEndian>
#include <QNetworkProxy>
#define FAST_FAIL_TIMEOUT_MS 2000
#define REQUEST_TIMEOUT_MS 5000
#define LAUNCH_TIMEOUT_MS 120000
#define RESUME_TIMEOUT_MS 30000
#define QUIT_TIMEOUT_MS 30000
NvHTTP::NvHTTP(QString address, QSslCertificate serverCert) :
m_Address(address),
m_ServerCert(serverCert)
{
Q_ASSERT(!address.isEmpty());
m_BaseUrlHttp.setScheme("http");
m_BaseUrlHttps.setScheme("https");
m_BaseUrlHttp.setHost(address);
m_BaseUrlHttps.setHost(address);
m_BaseUrlHttp.setPort(47989);
m_BaseUrlHttps.setPort(47984);
setAddress(address);
// Never use a proxy server
QNetworkProxy noProxy(QNetworkProxy::NoProxy);
m_Nam.setProxy(noProxy);
@@ -42,6 +40,21 @@ void NvHTTP::setServerCert(QSslCertificate serverCert)
m_ServerCert = serverCert;
}
void NvHTTP::setAddress(QString address)
{
Q_ASSERT(!address.isEmpty());
m_Address = address;
m_BaseUrlHttp.setHost(address);
m_BaseUrlHttps.setHost(address);
}
QString NvHTTP::address()
{
return m_Address;
}
QVector<int>
NvHTTP::parseQuad(QString quad)
{
@@ -80,7 +93,7 @@ NvHTTP::getCurrentGame(QString serverInfo)
}
QString
NvHTTP::getServerInfo(NvLogLevel logLevel)
NvHTTP::getServerInfo(NvLogLevel logLevel, bool fastFail)
{
QString serverInfo;
@@ -94,7 +107,7 @@ NvHTTP::getServerInfo(NvLogLevel logLevel)
serverInfo = openConnectionToString(m_BaseUrlHttps,
"serverinfo",
nullptr,
REQUEST_TIMEOUT_MS,
fastFail ? FAST_FAIL_TIMEOUT_MS : REQUEST_TIMEOUT_MS,
logLevel);
// Throws if the request failed
verifyResponseStatus(serverInfo);
@@ -107,7 +120,7 @@ NvHTTP::getServerInfo(NvLogLevel logLevel)
serverInfo = openConnectionToString(m_BaseUrlHttp,
"serverinfo",
nullptr,
REQUEST_TIMEOUT_MS,
fastFail ? FAST_FAIL_TIMEOUT_MS : REQUEST_TIMEOUT_MS,
logLevel);
verifyResponseStatus(serverInfo);
}
@@ -124,7 +137,7 @@ NvHTTP::getServerInfo(NvLogLevel logLevel)
serverInfo = openConnectionToString(m_BaseUrlHttp,
"serverinfo",
nullptr,
REQUEST_TIMEOUT_MS,
fastFail ? FAST_FAIL_TIMEOUT_MS : REQUEST_TIMEOUT_MS,
logLevel);
verifyResponseStatus(serverInfo);
}
@@ -132,30 +145,6 @@ NvHTTP::getServerInfo(NvLogLevel logLevel)
return serverInfo;
}
static QString
getSurroundAudioInfoString(int audioConfig)
{
int channelMask;
int channelCount;
switch (audioConfig)
{
case AUDIO_CONFIGURATION_STEREO:
channelCount = 2;
channelMask = 0x3;
break;
case AUDIO_CONFIGURATION_51_SURROUND:
channelCount = 6;
channelMask = 0xFC;
break;
default:
Q_ASSERT(false);
return 0;
}
return QString::number(channelMask << 16 | channelCount);
}
void
NvHTTP::launchApp(int appId,
PSTREAM_CONFIGURATION streamConfig,
@@ -174,10 +163,7 @@ NvHTTP::launchApp(int appId,
"appid="+QString::number(appId)+
"&mode="+QString::number(streamConfig->width)+"x"+
QString::number(streamConfig->height)+"x"+
// Using an FPS value over 60 causes SOPS to default to 720p60,
// so force it to 60 when starting. This won't impact our ability
// to get > 60 FPS while actually streaming though.
QString::number(streamConfig->fps > 60 ? 60 : streamConfig->fps)+
QString::number(streamConfig->fps)+
"&additionalStates=1&sops="+QString::number(sops ? 1 : 0)+
"&rikey="+QByteArray(streamConfig->remoteInputAesKey, sizeof(streamConfig->remoteInputAesKey)).toHex()+
"&rikeyid="+QString::number(riKeyId)+
@@ -185,7 +171,7 @@ NvHTTP::launchApp(int appId,
"&hdrMode=1&clientHdrCapVersion=0&clientHdrCapSupportedFlagsInUint32=0&clientHdrCapMetaDataId=NV_STATIC_METADATA_TYPE_1&clientHdrCapDisplayData=0x0x0x0x0x0x0x0x0x0x0" :
"")+
"&localAudioPlayMode="+QString::number(localAudio ? 1 : 0)+
"&surroundAudioInfo="+getSurroundAudioInfoString(streamConfig->audioConfiguration)+
"&surroundAudioInfo="+QString::number(SURROUNDAUDIOINFO_FROM_AUDIO_CONFIGURATION(streamConfig->audioConfiguration))+
"&remoteControllersBitmap="+QString::number(gamepadMask)+
"&gcmap="+QString::number(gamepadMask),
LAUNCH_TIMEOUT_MS);
@@ -207,7 +193,7 @@ NvHTTP::resumeApp(PSTREAM_CONFIGURATION streamConfig)
"resume",
"rikey="+QString(QByteArray(streamConfig->remoteInputAesKey, sizeof(streamConfig->remoteInputAesKey)).toHex())+
"&rikeyid="+QString::number(riKeyId)+
"&surroundAudioInfo="+getSurroundAudioInfoString(streamConfig->audioConfiguration),
"&surroundAudioInfo="+QString::number(SURROUNDAUDIOINFO_FROM_AUDIO_CONFIGURATION(streamConfig->audioConfiguration)),
RESUME_TIMEOUT_MS);
// Throws if the request failed
@@ -244,16 +230,16 @@ NvHTTP::getDisplayModeList(QString serverInfo)
while (!xmlReader.atEnd()) {
while (xmlReader.readNextStartElement()) {
QStringRef name = xmlReader.name();
if (xmlReader.name() == "DisplayMode") {
if (name == "DisplayMode") {
modes.append(NvDisplayMode());
}
else if (xmlReader.name() == "Width") {
else if (name == "Width") {
modes.last().width = xmlReader.readElementText().toInt();
}
else if (xmlReader.name() == "Height") {
else if (name == "Height") {
modes.last().height = xmlReader.readElementText().toInt();
}
else if (xmlReader.name() == "RefreshRate") {
else if (name == "RefreshRate") {
modes.last().refreshRate = xmlReader.readElementText().toInt();
}
}
@@ -277,7 +263,7 @@ NvHTTP::getAppList()
while (!xmlReader.atEnd()) {
while (xmlReader.readNextStartElement()) {
QStringRef name = xmlReader.name();
if (xmlReader.name() == "App") {
if (name == "App") {
// We must have a valid app before advancing to the next one
if (!apps.isEmpty() && !apps.last().isInitialized()) {
qWarning() << "Invalid applist XML";
@@ -286,15 +272,18 @@ NvHTTP::getAppList()
}
apps.append(NvApp());
}
else if (xmlReader.name() == "AppTitle") {
else if (name == "AppTitle") {
apps.last().name = xmlReader.readElementText();
}
else if (xmlReader.name() == "ID") {
else if (name == "ID") {
apps.last().id = xmlReader.readElementText().toInt();
}
else if (xmlReader.name() == "IsHdrSupported") {
else if (name == "IsHdrSupported") {
apps.last().hdrSupported = xmlReader.readElementText() == "1";
}
else if (name == "IsAppCollectorGame") {
apps.last().isAppCollectorGame = xmlReader.readElementText() == "1";
}
}
}
@@ -310,7 +299,10 @@ NvHTTP::verifyResponseStatus(QString xml)
{
if (xmlReader.name() == "root")
{
int statusCode = xmlReader.attributes().value("status_code").toInt();
// Status code can be 0xFFFFFFFF in some rare cases on GFE 3.20.3, and
// QString::toInt() will fail in that case, so use QString::toUInt()
// and cast the result to an int instead.
int statusCode = (int)xmlReader.attributes().value("status_code").toUInt();
if (statusCode == 200)
{
// Successful
@@ -323,6 +315,12 @@ NvHTTP::verifyResponseStatus(QString xml)
// 401 is expected for unpaired PCs when we fetch serverinfo over HTTPS
qWarning() << "Request failed:" << statusCode << statusMessage;
}
if (statusCode == -1 && statusMessage == "Invalid") {
// Special case handling an audio capture error which GFE doesn't
// provide any useful status message for.
statusCode = 418;
statusMessage = "Missing audio capture device. Reinstalling GeForce Experience should resolve this error.";
}
throw GfeHttpResponseException(statusCode, statusMessage);
}
}
@@ -442,6 +440,17 @@ NvHTTP::openConnection(QUrl baseUrl,
// Add our client certificate
request.setSslConfiguration(IdentityManager::get()->getSslConfig());
#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0) && !defined(QT_NO_BEARERMANAGEMENT)
// HACK: Set network accessibility to work around QTBUG-80947.
// Even though it was fixed in 5.14.2, it still breaks for users attempting to
// directly connect their computers without a router using APIPA and in some cases
// using OpenVPN with IPv6 enabled. https://github.com/moonlight-stream/moonlight-qt/issues/375
QT_WARNING_PUSH
QT_WARNING_DISABLE_DEPRECATED
m_Nam.setNetworkAccessible(QNetworkAccessManager::Accessible);
QT_WARNING_POP
#endif
QNetworkReply* reply = m_Nam.get(request);
// Run the request with a timeout if requested
+6 -21
View File
@@ -1,6 +1,7 @@
#pragma once
#include "identitymanager.h"
#include "nvapp.h"
#include <Limelight.h>
@@ -8,26 +9,6 @@
#include <QNetworkAccessManager>
#include <QNetworkReply>
class NvApp
{
public:
bool operator==(const NvApp& other) const
{
return id == other.id;
}
bool isInitialized()
{
return id != 0 && !name.isEmpty();
}
int id;
QString name;
bool hdrSupported;
};
Q_DECLARE_METATYPE(NvApp)
class NvDisplayMode
{
public:
@@ -131,7 +112,7 @@ public:
getCurrentGame(QString serverInfo);
QString
getServerInfo(NvLogLevel logLevel);
getServerInfo(NvLogLevel logLevel, bool fastFail = false);
static
void
@@ -156,6 +137,10 @@ public:
void setServerCert(QSslCertificate serverCert);
void setAddress(QString address);
QString address();
static
QVector<int>
parseQuad(QString quad);
+3 -1
View File
@@ -256,7 +256,9 @@ NvPairingManager::pair(QString appVersion, QString pin, QSslCertificate& serverC
challengeResponse.append(reinterpret_cast<char*>(asnSignature->data), asnSignature->length);
challengeResponse.append(clientSecretData);
QByteArray encryptedChallengeResponseHash = encrypt(QCryptographicHash::hash(challengeResponse, hashAlgo), &encKey);
QByteArray paddedHash = QCryptographicHash::hash(challengeResponse, hashAlgo);
paddedHash.resize(32);
QByteArray encryptedChallengeResponseHash = encrypt(paddedHash, &encKey);
QString respXml = m_Http.openConnectionToString(m_Http.m_BaseUrlHttp,
"pair",
"devicename=roth&updateState=1&serverchallengeresp=" +
+67
View File
@@ -0,0 +1,67 @@
#include "richpresencemanager.h"
#include <QDebug>
RichPresenceManager::RichPresenceManager(StreamingPreferences& prefs, QString gameName)
: m_DiscordActive(false)
{
#ifdef HAVE_DISCORD
if (prefs.richPresence) {
DiscordEventHandlers handlers = {};
handlers.ready = discordReady;
handlers.disconnected = discordDisconnected;
handlers.errored = discordErrored;
Discord_Initialize("594668102021677159", &handlers, 0, nullptr);
m_DiscordActive = true;
}
if (m_DiscordActive) {
QByteArray stateStr = (QString("Streaming ") + gameName).toUtf8();
DiscordRichPresence discordPresence = {};
discordPresence.state = stateStr.data();
discordPresence.startTimestamp = time(nullptr);
discordPresence.largeImageKey = "icon";
Discord_UpdatePresence(&discordPresence);
}
#else
Q_UNUSED(prefs)
Q_UNUSED(gameName)
#endif
}
RichPresenceManager::~RichPresenceManager()
{
#ifdef HAVE_DISCORD
if (m_DiscordActive) {
Discord_ClearPresence();
Discord_Shutdown();
}
#endif
}
void RichPresenceManager::runCallbacks()
{
#ifdef HAVE_DISCORD
if (m_DiscordActive) {
Discord_RunCallbacks();
}
#endif
}
#ifdef HAVE_DISCORD
void RichPresenceManager::discordReady(const DiscordUser* request)
{
qInfo() << "Discord integration ready for user:" << request->username;
}
void RichPresenceManager::discordDisconnected(int errorCode, const char *message)
{
qInfo() << "Discord integration disconnected:" << errorCode << message;
}
void RichPresenceManager::discordErrored(int errorCode, const char *message)
{
qWarning() << "Discord integration error:" << errorCode << message;
}
#endif
+26
View File
@@ -0,0 +1,26 @@
#pragma once
#include "settings/streamingpreferences.h"
#ifdef HAVE_DISCORD
#include <discord_rpc.h>
#endif
class RichPresenceManager
{
public:
RichPresenceManager(StreamingPreferences& prefs, QString gameName);
~RichPresenceManager();
void runCallbacks();
private:
#ifdef HAVE_DISCORD
static void discordReady(const DiscordUser* request);
static void discordDisconnected(int errorCode, const char* message);
static void discordErrored(int errorCode, const char* message);
#endif
bool m_DiscordActive;
};
+42 -9
View File
@@ -1,11 +1,17 @@
#include "systemproperties.h"
#include "utils.h"
#include <QGuiApplication>
#include "streaming/session.h"
#include "streaming/streamutils.h"
SystemProperties::SystemProperties()
{
isRunningWayland = qgetenv("XDG_SESSION_TYPE") == "wayland";
versionString = QString(VERSION_STR);
hasWindowManager = WMUtils::isRunningWindowManager();
isRunningWayland = WMUtils::isRunningWayland();
isRunningXWayland = isRunningWayland && QGuiApplication::platformName() == "xcb";
#ifdef Q_OS_WIN32
isWow64 = QSysInfo::currentCpuArchitecture() != QSysInfo::buildCpuArchitecture();
@@ -14,11 +20,18 @@ SystemProperties::SystemProperties()
#endif
#ifndef STEAM_LINK
hasBrowser = true;
// Assume we can probably launch a browser if we're in a GUI environment
hasBrowser = hasWindowManager;
#else
hasBrowser = false;
#endif
#ifdef HAVE_DISCORD
hasDiscordIntegration = true;
#else
hasDiscordIntegration = false;
#endif
unmappedGamepads = SdlInputHandler::getUnmappedGamepads();
// Populate data that requires talking to SDL. We do it all in one shot
@@ -51,6 +64,13 @@ void SystemProperties::querySdlVideoInfo()
// Never let the maximum drop below 60 FPS
maximumStreamingFrameRate = 60;
if (SDL_InitSubSystem(SDL_INIT_VIDEO) != 0) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"SDL_InitSubSystem(SDL_INIT_VIDEO) failed: %s",
SDL_GetError());
return;
}
SDL_DisplayMode bestMode;
for (int displayIndex = 0; displayIndex < SDL_GetNumVideoDisplays(); displayIndex++) {
SDL_DisplayMode desktopMode;
@@ -58,7 +78,14 @@ void SystemProperties::querySdlVideoInfo()
err = SDL_GetDesktopDisplayMode(displayIndex, &desktopMode);
if (err == 0) {
monitorDesktopResolutions.insert(displayIndex, QRect(0, 0, desktopMode.w, desktopMode.h));
if (desktopMode.w <= 8192 && desktopMode.h <= 8192) {
monitorDesktopResolutions.insert(displayIndex, QRect(0, 0, desktopMode.w, desktopMode.h));
}
else {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"Skipping resolution over 8K: %dx%d",
desktopMode.w, desktopMode.h);
}
}
else {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
@@ -67,7 +94,14 @@ void SystemProperties::querySdlVideoInfo()
}
if (StreamUtils::getRealDesktopMode(displayIndex, &desktopMode)) {
monitorNativeResolutions.insert(displayIndex, QRect(0, 0, desktopMode.w, desktopMode.h));
if (desktopMode.w <= 8192 && desktopMode.h <= 8192) {
monitorNativeResolutions.insert(displayIndex, QRect(0, 0, desktopMode.w, desktopMode.h));
}
else {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"Skipping resolution over 8K: %dx%d",
desktopMode.w, desktopMode.h);
}
// Start at desktop mode and work our way up
bestMode = desktopMode;
@@ -91,14 +125,13 @@ void SystemProperties::querySdlVideoInfo()
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Failed to create window for hardware decode test: %s",
SDL_GetError());
SDL_QuitSubSystem(SDL_INIT_VIDEO);
return;
}
hasHardwareAcceleration =
Session::isHardwareDecodeAvailable(testWindow,
StreamingPreferences::VDS_AUTO,
VIDEO_FORMAT_H264,
1920, 1080, 60);
Session::getDecoderInfo(testWindow, hasHardwareAcceleration, rendererAlwaysFullScreen, maximumResolution);
SDL_DestroyWindow(testWindow);
SDL_QuitSubSystem(SDL_INIT_VIDEO);
}
+12
View File
@@ -11,11 +11,17 @@ public:
SystemProperties();
Q_PROPERTY(bool hasHardwareAcceleration MEMBER hasHardwareAcceleration CONSTANT)
Q_PROPERTY(bool rendererAlwaysFullScreen MEMBER rendererAlwaysFullScreen CONSTANT)
Q_PROPERTY(bool isRunningWayland MEMBER isRunningWayland CONSTANT)
Q_PROPERTY(bool isRunningXWayland MEMBER isRunningXWayland CONSTANT)
Q_PROPERTY(bool isWow64 MEMBER isWow64 CONSTANT)
Q_PROPERTY(bool hasWindowManager MEMBER hasWindowManager CONSTANT)
Q_PROPERTY(bool hasBrowser MEMBER hasBrowser CONSTANT)
Q_PROPERTY(bool hasDiscordIntegration MEMBER hasDiscordIntegration CONSTANT)
Q_PROPERTY(QString unmappedGamepads MEMBER unmappedGamepads NOTIFY unmappedGamepadsChanged)
Q_PROPERTY(int maximumStreamingFrameRate MEMBER maximumStreamingFrameRate CONSTANT)
Q_PROPERTY(QSize maximumResolution MEMBER maximumResolution CONSTANT)
Q_PROPERTY(QString versionString MEMBER versionString CONSTANT)
Q_INVOKABLE QRect getDesktopResolution(int displayIndex);
Q_INVOKABLE QRect getNativeResolution(int displayIndex);
@@ -27,12 +33,18 @@ private:
void querySdlVideoInfo();
bool hasHardwareAcceleration;
bool rendererAlwaysFullScreen;
bool isRunningWayland;
bool isRunningXWayland;
bool isWow64;
bool hasWindowManager;
bool hasBrowser;
bool hasDiscordIntegration;
QString unmappedGamepads;
int maximumStreamingFrameRate;
QSize maximumResolution;
QList<QRect> monitorDesktopResolutions;
QList<QRect> monitorNativeResolutions;
QString versionString;
};
+38 -13
View File
@@ -172,20 +172,31 @@ GlobalCommandLineParser::ParseResult GlobalCommandLineParser::parse(const QStrin
parser.addPositionalArgument("action", "Action to execute", "<action>");
parser.parse(args);
auto posArgs = parser.positionalArguments();
QString action = posArgs.isEmpty() ? QString() : posArgs.first().toLower();
if (action == "") {
if (posArgs.isEmpty()) {
// This method will not return and terminates the process if --version
// or --help is specified
parser.handleHelpAndVersionOptions();
parser.handleUnknownOptions();
return NormalStartRequested;
} else if (action == "quit") {
return QuitRequested;
} else if (action == "stream") {
return StreamRequested;
} else {
parser.showError(QString("Invalid action: %1").arg(action));
}
else {
// If users supply arguments that accept values prior to the "quit"
// or "stream" positional arguments, we will not be able to correctly
// parse the value out of the input because this QCommandLineParser
// doesn't know about all of the options that "quit" and "stream"
// commands can accept. To work around this issue, we just look
// for "quit" or "stream" positional arguments anywhere.
for (int i = 0; i < posArgs.size(); i++) {
QString action = posArgs.at(i).toLower();
if (action == "quit") {
return QuitRequested;
} else if (action == "stream") {
return StreamRequested;
}
}
parser.showError(QString("Invalid action"));
}
}
@@ -241,6 +252,7 @@ StreamCommandLineParser::StreamCommandLineParser()
m_AudioConfigMap = {
{"stereo", StreamingPreferences::AC_STEREO},
{"5.1-surround", StreamingPreferences::AC_51_SURROUND},
{"7.1-surround", StreamingPreferences::AC_71_SURROUND},
};
m_VideoCodecMap = {
{"auto", StreamingPreferences::VCC_AUTO},
@@ -249,8 +261,8 @@ StreamCommandLineParser::StreamCommandLineParser()
};
m_VideoDecoderMap = {
{"auto", StreamingPreferences::VDS_AUTO},
{"software", StreamingPreferences::VDS_FORCE_HARDWARE},
{"hardware", StreamingPreferences::VDS_FORCE_SOFTWARE},
{"software", StreamingPreferences::VDS_FORCE_SOFTWARE},
{"hardware", StreamingPreferences::VDS_FORCE_HARDWARE},
};
}
@@ -280,11 +292,13 @@ void StreamCommandLineParser::parse(const QStringList &args, StreamingPreference
parser.addToggleOption("vsync", "V-Sync");
parser.addValueOption("fps", "FPS");
parser.addValueOption("bitrate", "bitrate in Kbps");
parser.addValueOption("packet-size", "video packet size");
parser.addChoiceOption("display-mode", "display mode", m_WindowModeMap.keys());
parser.addChoiceOption("audio-config", "audio config", m_AudioConfigMap.keys());
parser.addToggleOption("multi-controller", "multiple controller support");
parser.addToggleOption("quit-after", "quit app after session");
parser.addToggleOption("mouse-acceleration", "mouse acceleration");
parser.addToggleOption("absolute-mouse", "remote desktop optimized mouse control");
parser.addToggleOption("touchscreen-trackpad", "touchscreen in trackpad mode");
parser.addToggleOption("game-optimization", "game optimizations");
parser.addToggleOption("audio-on-host", "audio on host PC");
parser.addToggleOption("frame-pacing", "frame pacing");
@@ -345,6 +359,14 @@ void StreamCommandLineParser::parse(const QStringList &args, StreamingPreference
preferences->width, preferences->height, preferences->fps);
}
// Resolve --packet-size option
if (parser.isSet("packet-size")) {
preferences->packetSize = parser.getIntOption("packet-size");
if (preferences->packetSize < 1024) {
parser.showError("Packet size must be greater than 1024 bytes");
}
}
// Resolve --display option
if (parser.isSet("display-mode")) {
preferences->windowMode = mapValue(m_WindowModeMap, parser.getChoiceOptionValue("display-mode"));
@@ -364,8 +386,11 @@ void StreamCommandLineParser::parse(const QStringList &args, StreamingPreference
// Resolve --quit-after and --no-quit-after options
preferences->quitAppAfter = parser.getToggleOptionValue("quit-after", preferences->quitAppAfter);
// Resolve --mouse-acceleration and --no-mouse-acceleration options
preferences->mouseAcceleration = parser.getToggleOptionValue("mouse-acceleration", preferences->mouseAcceleration);
// Resolve --absolute-mouse and --no-absolute-mouse options
preferences->absoluteMouseMode = parser.getToggleOptionValue("absolute-mouse", preferences->absoluteMouseMode);
// Resolve --touchscreen-trackpad and --no-touchscreen-trackpad options
preferences->absoluteTouchMode = !parser.getToggleOptionValue("touchscreen-trackpad", !preferences->absoluteTouchMode);
// Resolve --game-optimization and --no-game-optimization options
preferences->gameOptimizations = parser.getToggleOptionValue("game-optimization", preferences->gameOptimizations);
@@ -12,8 +12,10 @@
<ul>
<li>Streaming at up to 4K resolution</li>
<li>Support for up to 120 FPS streaming (high refresh rate monitor recommended)</li>
<li>Hardware accelerated video decoding with VAAPI and VDPAU support</li>
<li>5.1 surround sound audio support</li>
<li>Hardware accelerated video decoding with VAAPI, VDPAU, and NVDEC support</li>
<li>7.1 surround sound audio support</li>
<li>Support for both pointer capture (for games) and direct mouse control (for remote desktop)</li>
<li>Direct touchscreen mouse control</li>
<li>HEVC support for better video compression efficiency</li>
<li>Gamepad support with force feedback for up to 4 players</li>
</ul>
@@ -33,6 +35,140 @@
</screenshots>
<releases>
<release version="2.1.0" date="2020-05-17">
<description>
<p>New features:</p>
<ul>
<li>Remote desktop mode now allows interaction with the host without having to click to focus the Moonlight window first</li>
<li>Added a new EGL renderer for improved performance on Wayland and XWayland</li>
<li>Added the option to toggle between direct touchscreen mode and the old trackpad mode</li>
<li>Added the option to rename host PCs</li>
<li>Added the ability to scroll the settings page with the right analog stick</li>
<li>Various UI improvements</li>
</ul>
<p>Bugfixes:</p>
<ul>
<li>Fixed unreliable mouse capture when the stream begins</li>
<li>Fixed cursor being trapped in full-screen mode even when the mouse wasn't captured</li>
<li>Fixed key presses not being sent to the host if the Super/Meta/Windows key is pressed</li>
<li>Updated included gamepad mappings</li>
</ul>
</description>
</release>
<release version="2.0.0" date="2020-04-28">
<description>
<p>New features:</p>
<ul>
<li>Remote desktop-optimized mouse mode which doesn't require capturing the mouse cursor</li>
<li>Touchscreen input now directly controls the mouse cursor</li>
<li>7.1 surround sound support</li>
</ul>
<p>Bugfixes:</p>
<ul>
<li>Improved performance on some lossy networks</li>
<li>Fixed disconnect delay when streaming from GeForce Experience 3.20.3.63</li>
<li>Fixed frame rate not exceeding 60 when streaming from GeForce Experience 3.20.3.63</li>
<li>Fixed parsing of command-line options when passing flags prior to the action</li>
<li>Fixed an issue that may cause excessive mDNS network traffic while Moonlight is running</li>
<li>Fixed hardware decoding on Intel GPUs running Mesa 20.0 and/or the Iris driver</li>
<li>Fixed hardware decoding on Comet Lake and later Intel GPUs using the iHD VA-API driver</li>
<li>Fixed hardware decoding on AMD Radeon GPUs with XWayland</li>
<li>Fixed a crash when using the iHD VA-API driver on Wayland</li>
<li>Fixed delay after exiting the stream on Wayland</li>
<li>Updated included gamepad mappings</li>
</ul>
</description>
</release>
<release version="1.2.0" date="2019-12-17">
<description>
<p>Bugfixes:</p>
<ul>
<li>Improved video encoding efficiency</li>
<li>Improved performance when streaming over a VPN</li>
<li>Fixed RTSP handshake error on certain networks</li>
<li>Fixed rumble not working on certain gamepads</li>
<li>Fixed pairing error when double-clicking on an unpaired PC</li>
<li>Updated included SDL gamepad mappings</li>
</ul>
</description>
</release>
<release version="1.1.1" date="2019-09-29">
<description>
<p>Bugfixes:</p>
<ul>
<li>Improved reliability of public IP address detection</li>
<li>Fixed connecting to a host PC with multiple network connections</li>
<li>Fixed a hang after exiting a full-screen stream with GNOME on Wayland</li>
<li>Fixed gamepad hotplugging</li>
<li>Updated to SDL 2.0.10</li>
<li>Updated to Qt 5.13.1</li>
</ul>
</description>
</release>
<release version="1.1.0" date="2019-08-05">
<description>
<p>New features:</p>
<ul>
<li>Added support for NVDEC for NVIDIA GPUs on Wayland</li>
<li>Added an option to disable gamepad mouse mode for games that require holding the Start button</li>
<li>Added automatic detection of IPv6 addresses for Internet streaming over IPv6 networks</li>
</ul>
<p>Bugfixes:</p>
<ul>
<li>Improved speed and reliability of automatic PC detection on the local network</li>
<li>Fixed audio latency increasing under certain network conditions</li>
<li>Fixed 5.1 surround sound sometimes causing no audio to play when streaming over the Internet</li>
<li>Fixed graceful termination of games not being detected properly on GFE 3.19</li>
<li>Fixed several audio issues on Linux by switching back to SDL for audio</li>
<li>Changed client certificate signature algorithm from SHA-1 to SHA-256 for Debian Buster compatibility</li>
<li>Updated included gamepad mappings</li>
</ul>
</description>
</release>
<release version="1.0.1" date="2019-05-27">
<description>
<p>Bugfixes:</p>
<ul>
<li>Fixed broken video on GeForce Experience 3.19</li>
</ul>
</description>
</release>
<release version="1.0.0" date="2019-05-21">
<description>
<p>New features:</p>
<ul>
<li>Mouse mode for gamepads (Press and hold Start to toggle)</li>
<li>Improved gamepad UI navigation</li>
</ul>
<p>Bugfixes:</p>
<ul>
<li>Fixed duplicate non-working gamepads appearing on the host</li>
<li>Fixed excessive frame queue delay on macOS</li>
<li>Fixed excessive audio latency on Steam Link</li>
<li>Fixed hiding mouse cursor after streaming on Steam Link</li>
<li>Fixed incorrect receive time stats on 32-bit platforms</li>
<li>Added UDP ports 48002 and 48010 for Wake-on-LAN</li>
<li>Updated included gamepad mappings</li>
</ul>
</description>
</release>
<release version="0.10.1" date="2019-04-28">
<description>
<p>New features:</p>
<ul>
<li>Video performance optimizations</li>
<li>VAAPI support on Wayland</li>
</ul>
<p>Bugfixes:</p>
<ul>
<li>Fixed a crash when attempting to dismiss the connection terminated dialog</li>
<li>Fixed a crash when using the overlay on the software decoder</li>
<li>Fixed software decoder not displaying output in some cases</li>
<li>Fixed some minor UI issues on the PC grid</li>
<li>Improved visibility of toolbar highlights</li>
</ul>
</description>
</release>
<release version="0.10.0" date="2019-04-07">
<description>
<p>New features:</p>
+15 -1
View File
@@ -5,5 +5,19 @@
# pairing data will be lost each reboot.
HOME=/usr/local/moonlight
# For a seamless transition across the Qt 5.9 -> Qt 5.14 boundary,
# we bundle binaries built with both Qt 5.9 and Qt 5.14 and pick
# the correct one at runtime.
if [ -d "/usr/local/Qt-5.14.1" ]; then
BIN="moonlight"
else
BIN="moonlight59"
fi
# Renice PE_Single_CPU which seems to host A/V stuff
renice -10 -p $(pidof PE_Single_CPU)
# Renice Moonlight itself to avoid preemption by background tasks
# Write output to a logfile in /tmp
exec ./bin/moonlight > /tmp/moonlight.log
echo Launching $BIN
exec nice -n -10 ./bin/$BIN > /tmp/moonlight.log
+12 -17
View File
@@ -3,6 +3,7 @@ import QtQuick.Controls 2.2
import AppModel 1.0
import ComputerManager 1.0
import SdlGamepadKeyNavigation 1.0
CenteredGridView {
property int computerIndex
@@ -32,6 +33,11 @@ CenteredGridView {
StackView.onActivated: {
appModel.computerLost.connect(computerLost)
activated = true
// Highlight the first item if a gamepad is connected
if (currentIndex == -1 && SdlGamepadKeyNavigation.getConnectedGamepads() > 0) {
currentIndex = 0
}
}
StackView.onDeactivating: {
@@ -61,9 +67,9 @@ CenteredGridView {
source: model.boxart
onSourceSizeChanged: {
if ((width == 130 && height == 180) || // GFE 2.0 placeholder image
(width == 628 && height == 888) || // GFE 3.0 placeholder image
(width == 200 && height == 266)) // Our no_app_image.png
if ((sourceSize.width == 130 && sourceSize.height == 180) || // GFE 2.0 placeholder image
(sourceSize.width == 628 && sourceSize.height == 888) || // GFE 3.0 placeholder image
(sourceSize.width == 200 && sourceSize.height == 266)) // Our no_app_image.png
{
isPlaceholder = true
}
@@ -145,6 +151,8 @@ CenteredGridView {
text: model.name
width: appIcon.width
height: model.running ? 175 : appIcon.height
leftPadding: 20
rightPadding: 20
anchors.left: appIcon.left
anchors.right: appIcon.right
anchors.bottom: appIcon.bottom
@@ -255,18 +263,5 @@ CenteredGridView {
onAccepted: quitApp()
}
ScrollBar.vertical: ScrollBar {
// Manually hide the scrollbar to prevent it from being drawn on top
// of the StreamSegue during the transition. It can sometimes get stuck
// there since we're not consistently pumping the event loop while
// starting the stream.
visible: activated
parent: appGrid.parent
anchors {
top: parent.top
right: parent.right
bottom: parent.bottom
}
}
ScrollBar.vertical: ScrollBar {}
}
+14 -1
View File
@@ -1,11 +1,15 @@
import QtQuick 2.9
import QtQuick.Controls 2.2
import SdlGamepadKeyNavigation 1.0
// https://stackoverflow.com/questions/45029968/how-do-i-set-the-combobox-width-to-fit-the-largest-item
ComboBox {
property int textWidth
property int desiredWidth : leftPadding + textWidth + indicator.width + rightPadding
property int maximumWidth : parent.width
implicitWidth: leftPadding + textWidth + indicator.width + rightPadding
implicitWidth: desiredWidth < maximumWidth ? desiredWidth : maximumWidth
TextMetrics {
id: popupMetrics
@@ -28,4 +32,13 @@ ComboBox {
textWidth = Math.max(popupMetrics.width, textWidth)
}
}
popup.onAboutToShow: {
// Switch to normal navigation for combo boxes
SdlGamepadKeyNavigation.setUiNavMode(false)
}
popup.onAboutToHide: {
SdlGamepadKeyNavigation.setUiNavMode(true)
}
}
+2
View File
@@ -38,4 +38,6 @@ GridView {
updateMargins()
}
boundsBehavior: Flickable.OvershootBounds
}
+19
View File
@@ -2,8 +2,27 @@ import QtQuick 2.0
import QtQuick.Controls 2.2
ToolButton {
property string iconSource
activeFocusOnTab: true
// FIXME: We're using an Image here rather than icon.source because
// icons don't work on Qt 5.9 LTS.
Image {
id: image
source: iconSource
anchors.centerIn: parent.background
sourceSize {
width: parent.background.width * 1.10
height: parent.background.height * 1.10
}
}
// This determines the size of the Material highlight. We increase it
// from the default because we use larger than normal icons for TV readability.
background.width: (parent.height - parent.anchors.bottomMargin - parent.anchors.topMargin) * 0.60
background.height: (parent.height - parent.anchors.bottomMargin - parent.anchors.topMargin) * 0.60
Keys.onReturnPressed: {
clicked()
}
+75 -11
View File
@@ -1,10 +1,12 @@
import QtQuick 2.9
import QtQuick.Controls 2.2
import QtQuick.Layouts 1.3
import ComputerModel 1.0
import ComputerManager 1.0
import StreamingPreferences 1.0
import SdlGamepadKeyNavigation 1.0
CenteredGridView {
property ComputerModel computerModel : createModel()
@@ -14,7 +16,7 @@ CenteredGridView {
activeFocusOnTab: true
topMargin: 20
bottomMargin: 5
cellWidth: 310; cellHeight: 350;
cellWidth: 310; cellHeight: 330;
objectName: "Computers"
Component.onCompleted: {
@@ -27,6 +29,16 @@ CenteredGridView {
StackView.onActivated: {
// Setup signals on CM
ComputerManager.computerAddCompleted.connect(addComplete)
// This is a bit of a hack to do this here as opposed to main.qml, but
// we need it enabled before calling getConnectedGamepads() and PcView
// is never destroyed, so it should be okay.
SdlGamepadKeyNavigation.enable()
// Highlight the first item if a gamepad is connected
if (currentIndex == -1 && SdlGamepadKeyNavigation.getConnectedGamepads() > 0) {
currentIndex = 0
}
}
StackView.onDeactivating: {
@@ -87,13 +99,13 @@ CenteredGridView {
model: computerModel
delegate: NavigableItemDelegate {
width: 300; height: 300;
width: 300; height: 320;
grid: pcGrid
Image {
id: pcIcon
anchors.horizontalCenter: parent.horizontalCenter
source: "qrc:/res/ic_tv_white_48px.svg"
source: "qrc:/res/desktop_windows-48px.svg"
sourceSize {
width: 200
height: 200
@@ -105,7 +117,7 @@ CenteredGridView {
id: stateIcon
anchors.horizontalCenter: pcIcon.horizontalCenter
anchors.verticalCenter: pcIcon.verticalCenter
anchors.verticalCenterOffset: -10
anchors.verticalCenterOffset: -15
visible: !model.statusUnknown && (!model.online || !model.paired)
source: !model.online ? "qrc:/res/baseline-warning-24px.svg" : "qrc:/res/baseline-lock-24px.svg"
sourceSize {
@@ -118,7 +130,7 @@ CenteredGridView {
id: statusUnknownSpinner
anchors.horizontalCenter: pcIcon.horizontalCenter
anchors.verticalCenter: pcIcon.verticalCenter
anchors.verticalCenterOffset: -10
anchors.verticalCenterOffset: -15
width: 75
height: 75
visible: model.statusUnknown
@@ -130,9 +142,11 @@ CenteredGridView {
width: parent.width
anchors.top: pcIcon.bottom
anchors.bottom: parent.bottom
font.pointSize: 36
horizontalAlignment: Text.AlignHCenter
wrapMode: Text.Wrap
elide: Text.ElideRight
}
NavigableMenu {
@@ -146,6 +160,15 @@ CenteredGridView {
deletePcDialog.open()
}
}
NavigableMenuItem {
parentMenu: pcContextMenu
text: "Rename PC"
onTriggered: {
renamePcDialog.pcIndex = index
renamePcDialog.originalName = model.name
renamePcDialog.open()
}
}
NavigableMenuItem {
parentMenu: pcContextMenu
text: "Wake PC"
@@ -222,6 +245,12 @@ CenteredGridView {
NavigableMessageDialog {
id: pairDialog
// Pairing dialog must be modal to prevent double-clicks from triggering
// pairing twice
modal: true
closePolicy: Popup.CloseOnEscape
// don't allow edits to the rest of the window while open
property string pin : "0000"
text:"Please enter " + pin + " on your GameStream PC. This dialog will close when pairing is completed."
@@ -246,12 +275,47 @@ CenteredGridView {
onAccepted: deletePc()
}
ScrollBar.vertical: ScrollBar {
parent: pcGrid.parent
anchors {
top: parent.top
right: parent.right
bottom: parent.bottom
NavigableDialog {
id: renamePcDialog
property string label: "Enter the new name for this PC:"
property string originalName
property int pcIndex : -1;
standardButtons: Dialog.Ok | Dialog.Cancel
onOpened: {
// Force keyboard focus on the textbox so keyboard navigation works
editText.forceActiveFocus()
}
onClosed: {
editText.clear()
}
onAccepted: {
if (editText.text) {
computerModel.renameComputer(pcIndex, editText.text)
}
}
ColumnLayout {
Label {
text: renamePcDialog.label
font.bold: true
}
TextField {
id: editText
placeholderText: renamePcDialog.originalName
Layout.fillWidth: true
focus: true
Keys.onReturnPressed: {
renamePcDialog.accept()
}
}
}
}
ScrollBar.vertical: ScrollBar {}
}
+141 -35
View File
@@ -10,26 +10,31 @@ Flickable {
id: settingsPage
objectName: "Settings"
boundsBehavior: Flickable.OvershootBounds
contentWidth: settingsColumn1.width > settingsColumn2.width ? settingsColumn1.width : settingsColumn2.width
contentHeight: settingsColumn1.height > settingsColumn2.height ? settingsColumn1.height : settingsColumn2.height
ScrollBar.vertical: ScrollBar {
parent: settingsPage.parent
anchors {
top: settingsPage.top
left: settingsPage.right
bottom: settingsPage.bottom
left: parent.right
leftMargin: -10
}
}
StackView.onActivated: {
SdlGamepadKeyNavigation.setSettingsMode(true)
// This enables Tab and BackTab based navigation rather than arrow keys.
// It is required to shift focus between controls on the settings page.
SdlGamepadKeyNavigation.setUiNavMode(true)
// Highlight the first item if a gamepad is connected
if (SdlGamepadKeyNavigation.getConnectedGamepads() > 0) {
resolutionComboBox.forceActiveFocus(Qt.TabFocus)
}
}
StackView.onDeactivating: {
SdlGamepadKeyNavigation.setSettingsMode(false)
SdlGamepadKeyNavigation.setUiNavMode(false)
// Save the prefs so the Session can observe the changes
StreamingPreferences.save()
@@ -38,11 +43,12 @@ Flickable {
Column {
padding: 10
id: settingsColumn1
width: settingsPage.width / 2 - padding
width: settingsPage.width / 2
spacing: 15
GroupBox {
id: basicSettingsGroupBox
width: (parent.width - 2 * parent.padding)
width: (parent.width - (parent.leftPadding + parent.rightPadding))
padding: 12
title: "<font color=\"skyblue\">Basic Settings</font>"
font.pointSize: 12
@@ -69,6 +75,7 @@ Flickable {
Row {
spacing: 5
width: parent.width
AutoResizingComboBox {
// ignore setting the index at first, and actually set it when the component is loaded
@@ -81,7 +88,7 @@ Flickable {
// Some platforms have different desktop resolutions
// and native resolutions (like macOS with Retina displays)
if (displayResIndex == 0) {
if (displayResIndex === 0) {
screenRect = SystemProperties.getDesktopResolution(displayIndex)
}
else {
@@ -122,6 +129,20 @@ Flickable {
}
}
// Prune resolutions that are over the decoder's maximum
var max_pixels = SystemProperties.maximumResolution.width * SystemProperties.maximumResolution.height;
if (max_pixels > 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 (existing_width * existing_height > max_pixels) {
resolutionListModel.remove(j)
j--
}
}
}
// load the saved width/height, and iterate through the ComboBox until a match is found
// and set it to that index.
var saved_width = StreamingPreferences.width
@@ -142,6 +163,7 @@ Flickable {
}
id: resolutionComboBox
maximumWidth: parent.width / 2
textRole: "text"
model: ListModel {
id: resolutionListModel
@@ -258,6 +280,7 @@ Flickable {
}
id: fpsComboBox
maximumWidth: parent.width / 2
textRole: "text"
// ::onActivated must be used, as it only listens for when the index is changed by a human
onActivated : {
@@ -321,25 +344,40 @@ Flickable {
AutoResizingComboBox {
// ignore setting the index at first, and actually set it when the component is loaded
Component.onCompleted: {
var savedWm = StreamingPreferences.windowMode
currentIndex = 0
// Set the recommended option based on the OS
for (var i = 0; i < windowModeListModel.count; i++) {
var thisWm = windowModeListModel.get(i).val;
if (savedWm === thisWm) {
currentIndex = i
if (thisWm === StreamingPreferences.recommendedFullScreenMode) {
windowModeListModel.get(i).text += " (Recommended)"
windowModeListModel.move(i, 0, 1);
break
}
}
currentIndex = 0
if (SystemProperties.hasWindowManager && !SystemProperties.rendererAlwaysFullScreen) {
var savedWm = StreamingPreferences.windowMode
for (var i = 0; i < windowModeListModel.count; i++) {
var thisWm = windowModeListModel.get(i).val;
if (savedWm === thisWm) {
currentIndex = i
break
}
}
}
activated(currentIndex)
}
id: windowModeComboBox
enabled: SystemProperties.hasWindowManager && !SystemProperties.rendererAlwaysFullScreen
hoverEnabled: true
textRole: "text"
model: ListModel {
id: windowModeListModel
ListElement {
text: "Full-screen (Recommended)"
text: "Full-screen"
val: StreamingPreferences.WM_FULLSCREEN
}
ListElement {
@@ -363,6 +401,7 @@ Flickable {
CheckBox {
id: vsyncCheck
width: parent.width
hoverEnabled: true
text: "V-Sync"
font.pointSize: 12
@@ -379,6 +418,7 @@ Flickable {
CheckBox {
id: framePacingCheck
width: parent.width
hoverEnabled: true
text: "Frame pacing"
font.pointSize: 12
@@ -398,7 +438,7 @@ Flickable {
GroupBox {
id: audioSettingsGroupBox
width: (parent.width - 2 * parent.padding)
width: (parent.width - (parent.leftPadding + parent.rightPadding))
padding: 12
title: "<font color=\"skyblue\">Audio Settings</font>"
font.pointSize: 12
@@ -442,6 +482,10 @@ Flickable {
text: "5.1 surround sound"
val: StreamingPreferences.AC_51_SURROUND
}
ListElement {
text: "7.1 surround sound"
val: StreamingPreferences.AC_71_SURROUND
}
}
// ::onActivated must be used, as it only listens for when the index is changed by a human
onActivated : {
@@ -454,7 +498,7 @@ Flickable {
GroupBox {
id: uiSettingsGroupBox
width: (parent.width - 2 * parent.padding)
width: (parent.width - (parent.leftPadding + parent.rightPadding))
padding: 12
title: "<font color=\"skyblue\">UI Settings</font>"
font.pointSize: 12
@@ -465,9 +509,11 @@ Flickable {
CheckBox {
id: startMaximizedCheck
width: parent.width
text: "Maximize Moonlight window on startup"
font.pointSize: 12
checked: !StreamingPreferences.startWindowed
enabled: SystemProperties.hasWindowManager
checked: !StreamingPreferences.startWindowed || !SystemProperties.hasWindowManager
onCheckedChanged: {
StreamingPreferences.startWindowed = !checked
}
@@ -475,6 +521,7 @@ Flickable {
CheckBox {
id: connectionWarningsCheck
width: parent.width
text: "Show connection quality warnings"
font.pointSize: 12
checked: StreamingPreferences.connectionWarnings
@@ -482,19 +529,38 @@ Flickable {
StreamingPreferences.connectionWarnings = checked
}
}
CheckBox {
visible: SystemProperties.hasDiscordIntegration
id: discordPresenceCheck
width: parent.width
text: "Discord Rich Presence integration"
font.pointSize: 12
checked: StreamingPreferences.richPresence
onCheckedChanged: {
StreamingPreferences.richPresence = checked
}
ToolTip.delay: 1000
ToolTip.timeout: 5000
ToolTip.visible: hovered
ToolTip.text: "Updates your Discord status to display the name of the game you're streaming."
}
}
}
}
Column {
padding: 10
rightPadding: 20
anchors.left: settingsColumn1.right
id: settingsColumn2
width: settingsPage.width / 2 - padding
width: settingsPage.width / 2
spacing: 15
GroupBox {
id: gamepadSettingsGroupBox
width: (parent.width - parent.padding)
width: (parent.width - (parent.leftPadding + parent.rightPadding))
padding: 12
title: "<font color=\"skyblue\">Input Settings</font>"
font.pointSize: 12
@@ -505,6 +571,7 @@ Flickable {
CheckBox {
id: singleControllerCheck
width: parent.width
text: "Force gamepad #1 always present"
font.pointSize: 12
checked: !StreamingPreferences.multiController
@@ -520,26 +587,63 @@ Flickable {
}
CheckBox {
id: rawInputCheck
id: absoluteMouseCheck
hoverEnabled: true
text: "Raw mouse input"
width: parent.width
text: "Optimize mouse for remote desktop instead of games"
font.pointSize: 12
checked: !StreamingPreferences.mouseAcceleration
enabled: SystemProperties.hasWindowManager
checked: StreamingPreferences.absoluteMouseMode && SystemProperties.hasWindowManager
onCheckedChanged: {
StreamingPreferences.mouseAcceleration = !checked
StreamingPreferences.absoluteMouseMode = checked
}
ToolTip.delay: 1000
ToolTip.timeout: 5000
ToolTip.visible: hovered
ToolTip.text: "This enables mouse control without capturing the client's mouse cursor. It will not work in most games.\n
You can toggle this while streaming using Ctrl+Alt+Shift+M."
}
CheckBox {
id: absoluteTouchCheck
hoverEnabled: true
width: parent.width
text: "Use touchscreen as a trackpad"
font.pointSize: 12
checked: !StreamingPreferences.absoluteTouchMode
onCheckedChanged: {
StreamingPreferences.absoluteTouchMode = !checked
}
ToolTip.delay: 1000
ToolTip.timeout: 5000
ToolTip.visible: hovered
ToolTip.text: "When checked, the touchscreen acts like a trackpad. When unchecked, the touchscreen will directly control the mouse pointer."
}
CheckBox {
id: gamepadMouseCheck
hoverEnabled: true
width: parent.width
text: "Gamepad mouse mode support"
font.pointSize: 12
checked: StreamingPreferences.gamepadMouse
onCheckedChanged: {
StreamingPreferences.gamepadMouse = checked
}
ToolTip.delay: 1000
ToolTip.timeout: 3000
ToolTip.visible: hovered
ToolTip.text: "When checked, mouse input is not accelerated or scaled by the OS before passing to Moonlight"
ToolTip.text: "When enabled, holding the Start button will toggle mouse mode"
}
}
}
GroupBox {
id: hostSettingsGroupBox
width: (parent.width - parent.padding)
width: (parent.width - (parent.leftPadding + parent.rightPadding))
padding: 12
title: "<font color=\"skyblue\">Host Settings</font>"
font.pointSize: 12
@@ -550,6 +654,7 @@ Flickable {
CheckBox {
id: optimizeGameSettingsCheck
width: parent.width
text: "Optimize game settings"
font.pointSize: 12
checked: StreamingPreferences.gameOptimizations
@@ -560,6 +665,7 @@ Flickable {
CheckBox {
id: audioPcCheck
width: parent.width
text: "Play audio on host PC"
font.pointSize: 12
checked: StreamingPreferences.playAudioOnHost
@@ -572,7 +678,7 @@ Flickable {
GroupBox {
id: advancedSettingsGroupBox
width: (parent.width - parent.padding)
width: (parent.width - (parent.leftPadding + parent.rightPadding))
padding: 12
title: "<font color=\"skyblue\">Advanced Settings</font>"
font.pointSize: 12
@@ -659,20 +765,17 @@ Flickable {
val: StreamingPreferences.VCC_AUTO
}
ListElement {
text: "Force H.264"
text: "H.264"
val: StreamingPreferences.VCC_FORCE_H264
}
ListElement {
text: "Force HEVC"
text: "HEVC (H.265)"
val: StreamingPreferences.VCC_FORCE_HEVC
}
// HDR seems to be broken in GFE 3.14.1, and even when that's fixed
// we'll probably need to gate this feature on OS support in our
// renderers.
/* ListElement {
text: "Force HEVC HDR"
ListElement {
text: "HEVC HDR (Experimental)"
val: StreamingPreferences.VCC_FORCE_HEVC_HDR
} */
}
}
// ::onActivated must be used, as it only listens for when the index is changed by a human
onActivated : {
@@ -682,6 +785,7 @@ Flickable {
CheckBox {
id: unlockUnsupportedFps
width: parent.width
text: "Unlock unsupported FPS options"
font.pointSize: 12
checked: StreamingPreferences.unsupportedFps
@@ -700,6 +804,7 @@ Flickable {
CheckBox {
id: enableMdns
width: parent.width
text: "Automatically find PCs on the local network (Recommended)"
font.pointSize: 12
checked: StreamingPreferences.enableMdns
@@ -724,6 +829,7 @@ Flickable {
CheckBox {
id: quitAppAfter
width: parent.width
text: "Quit app after quitting session"
font.pointSize: 12
checked: StreamingPreferences.quitAppAfter
+22 -16
View File
@@ -20,7 +20,7 @@ Item {
function stageFailed(stage, errorCode)
{
// Display the error dialog after Session::exec() returns
errorDialog.text = "Starting " + stage + " failed: Error " + errorCode
streamSegueErrorDialog.text = "Starting " + stage + " failed: Error " + errorCode
}
function connectionStarted()
@@ -38,7 +38,7 @@ Item {
function displayLaunchError(text)
{
// Display the error dialog after Session::exec() returns
errorDialog.text = text
streamSegueErrorDialog.text = text
}
function displayLaunchWarning(text)
@@ -68,9 +68,10 @@ Item {
SdlGamepadKeyNavigation.enable()
if (quitAfter) {
if (errorDialog.text) {
if (streamSegueErrorDialog.text) {
// Quit when the error dialog is acknowledged
errorDialog.open()
streamSegueErrorDialog.quitAfter = quitAfter
streamSegueErrorDialog.open()
}
else {
// Quit immediately
@@ -87,8 +88,9 @@ Item {
// the Qt UI is visible again to prevent losing
// focus on the dialog which would impact gamepad
// users.
if (errorDialog.text) {
errorDialog.open()
if (streamSegueErrorDialog.text) {
streamSegueErrorDialog.quitAfter = quitAfter
streamSegueErrorDialog.open()
}
}
}
@@ -115,9 +117,22 @@ Item {
session.sessionFinished.connect(sessionFinished)
// Kick off the stream
spinnerTimer.start()
streamLoader.active = true
}
Timer {
id: spinnerTimer
// Display the spinner appearance a bit to allow us to reach
// the code in Session.exec() that pumps the event loop.
// If we display it immediately, it will briefly hang in the
// middle of the animation on Windows, which looks very
// obviously broken.
interval: 100
onTriggered: stageSpinner.running = true
}
Loader {
id: streamLoader
active: false
@@ -148,6 +163,7 @@ Item {
BusyIndicator {
id: stageSpinner
running: false
}
Label {
@@ -171,14 +187,4 @@ Item {
wrapMode: Text.Wrap
}
ErrorMessageDialog {
id: errorDialog
onClosed: {
if (quitAfter) {
Qt.quit()
}
}
}
}
+7
View File
@@ -128,6 +128,13 @@ void ComputerModel::wakeComputer(int computerIndex)
QThreadPool::globalInstance()->start(wakeTask);
}
void ComputerModel::renameComputer(int computerIndex, QString name)
{
Q_ASSERT(computerIndex < m_Computers.count());
m_ComputerManager->renameHost(m_Computers[computerIndex], name);
}
void ComputerModel::pairComputer(int computerIndex, QString pin)
{
Q_ASSERT(computerIndex < m_Computers.count());
+2
View File
@@ -35,6 +35,8 @@ public:
Q_INVOKABLE void wakeComputer(int computerIndex);
Q_INVOKABLE void renameComputer(int computerIndex, QString name);
Q_INVOKABLE Session* createSessionForCurrentGame(int computerIndex);
signals:
+82 -69
View File
@@ -17,11 +17,7 @@ ApplicationWindow {
width: 1280
height: 600
visibility: StreamingPreferences.startWindowed ? "Windowed" : "Maximized"
Component.onCompleted: {
SdlGamepadKeyNavigation.enable()
}
visibility: (SystemProperties.hasWindowManager && StreamingPreferences.startWindowed) ? "Windowed" : "Maximized"
StackView {
id: stackView
@@ -138,14 +134,16 @@ ApplicationWindow {
// onAfterRendering call and potentially reenter this code.
initialized = true;
if (SystemProperties.isRunningWayland) {
waylandDialog.open()
}
else if (SystemProperties.isWow64) {
if (SystemProperties.isWow64) {
wow64Dialog.open()
}
else if (!SystemProperties.hasHardwareAcceleration) {
noHwDecoderDialog.open()
if (SystemProperties.isRunningXWayland) {
xWaylandDialog.open()
}
else {
noHwDecoderDialog.open()
}
}
if (SystemProperties.unmappedGamepads) {
@@ -173,28 +171,32 @@ ApplicationWindow {
header: ToolBar {
id: toolBar
height: 60
anchors.topMargin: 5
anchors.bottomMargin: 5
Label {
id: titleLabel
visible: toolBar.width > 700
anchors.fill: parent
text: stackView.currentItem.objectName
font.pointSize: 20
elide: Label.ElideRight
horizontalAlignment: Qt.AlignHCenter
verticalAlignment: Qt.AlignVCenter
}
RowLayout {
spacing: 20
anchors.leftMargin: 10
anchors.rightMargin: 10
anchors.fill: parent
NavigableToolButton {
// Only make the button visible if the user has navigated somewhere.
visible: stackView.depth > 1
// FIXME: We're using an Image here rather than icon.source because
// icons don't work on Qt 5.9 LTS.
Image {
source: "qrc:/res/arrow_left.svg"
anchors.centerIn: parent
sourceSize {
// The icon should be square so use the height as the width too
width: toolBar.height - toolBar.anchors.bottomMargin - toolBar.anchors.topMargin
height: toolBar.height - toolBar.anchors.bottomMargin - toolBar.anchors.topMargin
}
}
iconSource: "qrc:/res/arrow_left.svg"
onClicked: stackView.pop()
@@ -203,28 +205,36 @@ ApplicationWindow {
}
}
// This label will appear when the window gets too small and
// we need to ensure the toolbar controls don't collide
Label {
id: titleLabel
text: stackView.currentItem.objectName
font.pointSize: 15
id: titleRowLabel
font.pointSize: titleLabel.font.pointSize
elide: Label.ElideRight
horizontalAlignment: Qt.AlignHCenter
verticalAlignment: Qt.AlignVCenter
Layout.fillWidth: true
// We need this label to always be visible so it can occupy
// the remaining space in the RowLayout. To "hide" it, we
// just set the text to empty string.
text: !titleLabel.visible ? stackView.currentItem.objectName : ""
}
Label {
id: versionLabel
visible: stackView.currentItem.objectName === "Settings"
text: "Version " + SystemProperties.versionString
font.pointSize: 12
horizontalAlignment: Qt.AlignRight
verticalAlignment: Qt.AlignVCenter
}
NavigableToolButton {
id: addPcButton
visible: stackView.currentItem.objectName === "Computers"
Image {
source: "qrc:/res/ic_add_to_queue_white_48px.svg"
anchors.centerIn: parent
sourceSize {
width: toolBar.height - toolBar.anchors.bottomMargin - toolBar.anchors.topMargin
height: toolBar.height - toolBar.anchors.bottomMargin - toolBar.anchors.topMargin
}
}
iconSource: "qrc:/res/ic_add_to_queue_white_48px.svg"
ToolTip.delay: 1000
ToolTip.timeout: 3000
@@ -251,24 +261,21 @@ ApplicationWindow {
id: updateButton
Image {
source: "qrc:/res/update.svg"
anchors.centerIn: parent
sourceSize {
width: toolBar.height - toolBar.anchors.bottomMargin - toolBar.anchors.topMargin
height: toolBar.height - toolBar.anchors.bottomMargin - toolBar.anchors.topMargin
}
}
iconSource: "qrc:/res/update.svg"
ToolTip.delay: 1000
ToolTip.timeout: 3000
ToolTip.visible: hovered
ToolTip.visible: hovered || visible
// Invisible until we get a callback notifying us that
// an update is available
visible: false
onClicked: Qt.openUrlExternally(browserUrl);
onClicked: {
if (SystemProperties.hasBrowser) {
Qt.openUrlExternally(browserUrl);
}
}
function updateAvailable(version, url)
{
@@ -291,14 +298,7 @@ ApplicationWindow {
id: helpButton
visible: SystemProperties.hasBrowser
Image {
source: "qrc:/res/question_mark.svg"
anchors.centerIn: parent
sourceSize {
width: toolBar.height - toolBar.anchors.bottomMargin - toolBar.anchors.topMargin
height: toolBar.height - toolBar.anchors.bottomMargin - toolBar.anchors.topMargin
}
}
iconSource: "qrc:/res/question_mark.svg"
ToolTip.delay: 1000
ToolTip.timeout: 3000
@@ -328,14 +328,7 @@ ApplicationWindow {
ToolTip.visible: hovered
ToolTip.text: "Gamepad Mapper"
Image {
source: "qrc:/res/ic_videogame_asset_white_48px.svg"
anchors.centerIn: parent
sourceSize {
width: toolBar.height - toolBar.anchors.bottomMargin - toolBar.anchors.topMargin
height: toolBar.height - toolBar.anchors.bottomMargin - toolBar.anchors.topMargin
}
}
iconSource: "qrc:/res/ic_videogame_asset_white_48px.svg"
onClicked: navigateTo("qrc:/gui/GamepadMapper.qml", "Gamepad Mapping")
@@ -347,14 +340,7 @@ ApplicationWindow {
NavigableToolButton {
id: settingsButton
Image {
source: "qrc:/res/settings.svg"
anchors.centerIn: parent
sourceSize {
width: toolBar.height - toolBar.anchors.bottomMargin - toolBar.anchors.topMargin
height: toolBar.height - toolBar.anchors.bottomMargin - toolBar.anchors.topMargin
}
}
iconSource: "qrc:/res/settings.svg"
onClicked: navigateTo("qrc:/gui/SettingsView.qml", "Settings")
@@ -385,9 +371,9 @@ ApplicationWindow {
}
ErrorMessageDialog {
id: waylandDialog
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."
id: xWaylandDialog
text: "Hardware acceleration doesn't work on XWayland. Continuing on XWayland may result in poor streaming performance. " +
"Try running with QT_QPA_PLATFORM=wayland or switch to X11."
helpText: "Click the Help button for more information."
helpUrl: "https://github.com/moonlight-stream/moonlight-docs/wiki/Fixing-Hardware-Decoding-Problems"
}
@@ -419,6 +405,29 @@ ApplicationWindow {
onAccepted: Qt.quit()
}
// HACK: This belongs in StreamSegue but keeping a dialog around after the parent
// dies can trigger bugs in Qt 5.12 that cause the app to crash. For now, we will
// host this dialog in a QML component that is never destroyed.
//
// To repro: Start a stream, cut the network connection to trigger the "Connection
// terminated" dialog, wait until the app grid times out back to the PC grid, then
// try to dismiss the dialog.
ErrorMessageDialog {
id: streamSegueErrorDialog
property bool quitAfter: false
onClosed: {
if (quitAfter) {
Qt.quit()
}
// StreamSegue assumes its dialog will be re-created each time we
// start streaming, so fake it by wiping out the text each time.
text = ""
}
}
NavigableDialog {
id: addPcDialog
property string label: "Enter the IP address of your GameStream PC:"
@@ -430,6 +439,10 @@ ApplicationWindow {
editText.forceActiveFocus()
}
onClosed: {
editText.clear()
}
onAccepted: {
if (editText.text) {
ComputerManager.addNewHost(editText.text, false)
+48 -35
View File
@@ -10,7 +10,7 @@
SdlGamepadKeyNavigation::SdlGamepadKeyNavigation()
: m_Enabled(false),
m_SettingsMode(false),
m_UiNavMode(false),
m_LastAxisNavigationEventTime(0)
{
m_PollingTimer = new QTimer(this);
@@ -106,7 +106,7 @@ void SdlGamepadKeyNavigation::onPollingTimerFired()
switch (event.cbutton.button) {
case SDL_CONTROLLER_BUTTON_DPAD_UP:
if (m_SettingsMode) {
if (m_UiNavMode) {
// Back-tab
sendKey(type, Qt::Key_Tab, Qt::ShiftModifier);
}
@@ -115,7 +115,7 @@ void SdlGamepadKeyNavigation::onPollingTimerFired()
}
break;
case SDL_CONTROLLER_BUTTON_DPAD_DOWN:
if (m_SettingsMode) {
if (m_UiNavMode) {
sendKey(type, Qt::Key_Tab);
}
else {
@@ -124,24 +124,12 @@ void SdlGamepadKeyNavigation::onPollingTimerFired()
break;
case SDL_CONTROLLER_BUTTON_DPAD_LEFT:
sendKey(type, Qt::Key_Left);
if (m_SettingsMode) {
// Some settings controls respond to left/right (like the slider)
// and others respond to up/down (like combo boxes). They seem to
// be mutually exclusive though so let's just send both.
sendKey(type, Qt::Key_Up);
}
break;
case SDL_CONTROLLER_BUTTON_DPAD_RIGHT:
sendKey(type, Qt::Key_Right);
if (m_SettingsMode) {
// Some settings controls respond to left/right (like the slider)
// and others respond to up/down (like combo boxes). They seem to
// be mutually exclusive though so let's just send both.
sendKey(type, Qt::Key_Down);
}
break;
case SDL_CONTROLLER_BUTTON_A:
if (m_SettingsMode) {
if (m_UiNavMode) {
sendKey(type, Qt::Key_Space);
}
else {
@@ -183,7 +171,7 @@ void SdlGamepadKeyNavigation::onPollingTimerFired()
// Do nothing
}
else if (leftY < -30000) {
if (m_SettingsMode) {
if (m_UiNavMode) {
// Back-tab
sendKey(QEvent::Type::KeyPress, Qt::Key_Tab, Qt::ShiftModifier);
sendKey(QEvent::Type::KeyRelease, Qt::Key_Tab, Qt::ShiftModifier);
@@ -196,7 +184,7 @@ void SdlGamepadKeyNavigation::onPollingTimerFired()
m_LastAxisNavigationEventTime = SDL_GetTicks();
}
else if (leftY > 30000) {
if (m_SettingsMode) {
if (m_UiNavMode) {
sendKey(QEvent::Type::KeyPress, Qt::Key_Tab);
sendKey(QEvent::Type::KeyRelease, Qt::Key_Tab);
}
@@ -210,28 +198,37 @@ void SdlGamepadKeyNavigation::onPollingTimerFired()
else if (leftX < -30000) {
sendKey(QEvent::Type::KeyPress, Qt::Key_Left);
sendKey(QEvent::Type::KeyRelease, Qt::Key_Left);
if (m_SettingsMode) {
// Some settings controls respond to left/right (like the slider)
// and others respond to up/down (like combo boxes). They seem to
// be mutually exclusive though so let's just send both.
sendKey(QEvent::Type::KeyPress, Qt::Key_Up);
sendKey(QEvent::Type::KeyRelease, Qt::Key_Up);
}
m_LastAxisNavigationEventTime = SDL_GetTicks();
}
else if (leftX > 30000) {
sendKey(QEvent::Type::KeyPress, Qt::Key_Right);
sendKey(QEvent::Type::KeyRelease, Qt::Key_Right);
if (m_SettingsMode) {
// Some settings controls respond to left/right (like the slider)
// and others respond to up/down (like combo boxes). They seem to
// be mutually exclusive though so let's just send both.
sendKey(QEvent::Type::KeyPress, Qt::Key_Down);
sendKey(QEvent::Type::KeyRelease, Qt::Key_Down);
m_LastAxisNavigationEventTime = SDL_GetTicks();
}
// In UI navigation mode (settings page), use the right stick to scroll
if (m_UiNavMode) {
short rightX = SDL_GameControllerGetAxis(gc, SDL_CONTROLLER_AXIS_RIGHTX);
short rightY = SDL_GameControllerGetAxis(gc, SDL_CONTROLLER_AXIS_RIGHTY);
QPoint wheelDelta;
if (rightX > 30000) {
wheelDelta.setX(30);
}
else if (rightX < -30000) {
wheelDelta.setX(-30);
}
m_LastAxisNavigationEventTime = SDL_GetTicks();
if (rightY > 30000) {
wheelDelta.setY(-30);
}
else if (rightY < -30000) {
wheelDelta.setY(30);
}
if (!wheelDelta.isNull()) {
sendWheel(wheelDelta);
}
}
}
}
@@ -246,9 +243,25 @@ void SdlGamepadKeyNavigation::sendKey(QEvent::Type type, Qt::Key key, Qt::Keyboa
}
}
void SdlGamepadKeyNavigation::setSettingsMode(bool settingsMode)
void SdlGamepadKeyNavigation::sendWheel(QPoint& angleDelta)
{
m_SettingsMode = settingsMode;
QGuiApplication* app = static_cast<QGuiApplication*>(QGuiApplication::instance());
QWindow* focusWindow = app->focusWindow();
if (focusWindow != nullptr) {
QPoint mousePos(focusWindow->width() / 2, focusWindow->height() / 2);
QPoint globalPos(focusWindow->mapToGlobal(mousePos));
#if QT_VERSION >= QT_VERSION_CHECK(5, 12, 0)
QWheelEvent wheelEvent(mousePos, globalPos, QPoint(), angleDelta, Qt::NoButton, Qt::NoModifier, Qt::NoScrollPhase, false, Qt::MouseEventSynthesizedByApplication);
#else
QWheelEvent wheelEvent(mousePos, globalPos, QPoint(), angleDelta, angleDelta.y(), Qt::Vertical, Qt::NoButton, Qt::NoModifier, Qt::NoScrollPhase, Qt::MouseEventSynthesizedByApplication, false);
#endif
app->sendEvent(focusWindow, &wheelEvent);
}
}
void SdlGamepadKeyNavigation::setUiNavMode(bool uiNavMode)
{
m_UiNavMode = uiNavMode;
}
int SdlGamepadKeyNavigation::getConnectedGamepads()
+3 -2
View File
@@ -18,12 +18,13 @@ public:
Q_INVOKABLE void disable();
Q_INVOKABLE void setSettingsMode(bool settingsMode);
Q_INVOKABLE void setUiNavMode(bool settingsMode);
Q_INVOKABLE int getConnectedGamepads();
private:
void sendKey(QEvent::Type type, Qt::Key key, Qt::KeyboardModifiers modifiers = Qt::NoModifier);
void sendWheel(QPoint& angleDelta);
private slots:
void onPollingTimerFired();
@@ -32,6 +33,6 @@ private:
QTimer* m_PollingTimer;
QList<SDL_GameController*> m_Gamepads;
bool m_Enabled;
bool m_SettingsMode;
bool m_UiNavMode;
Uint32 m_LastAxisNavigationEventTime;
};
+115 -18
View File
@@ -9,6 +9,8 @@
#include <QPalette>
#include <QFont>
#include <QCursor>
#include <QElapsedTimer>
#include <QFile>
// Don't let SDL hook our main function, since Qt is already
// doing the same thing. This needs to be before any headers
@@ -20,14 +22,17 @@
#include "streaming/video/ffmpeg.h"
#endif
#ifdef Q_OS_WIN32
#if defined(Q_OS_WIN32)
#include "antihookingprotection.h"
#elif defined(Q_OS_LINUX)
#include <openssl/ssl.h>
#endif
#include "cli/quitstream.h"
#include "cli/startstream.h"
#include "cli/commandlineparser.h"
#include "path.h"
#include "utils.h"
#include "gui/computermodel.h"
#include "gui/appmodel.h"
#include "backend/autoupdatechecker.h"
@@ -52,7 +57,7 @@
#endif
#ifdef USE_CUSTOM_LOGGER
static QTime s_LoggerTime;
static QElapsedTimer s_LoggerTime;
static QTextStream s_LoggerStream(stdout);
static QMutex s_LoggerLock;
#ifdef LOG_TO_FILE
@@ -71,7 +76,12 @@ void logToLoggerStream(QString& message)
return;
}
else if (s_LogLinesWritten == MAX_LOG_LINES) {
s_LoggerStream << "Log size limit reached!" << endl;
s_LoggerStream << "Log size limit reached!";
#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)
s_LoggerStream << Qt::endl;
#else
s_LoggerStream << endl;
#endif
s_LogLimitReached = true;
return;
}
@@ -278,22 +288,50 @@ int main(int argc, char *argv[])
SetUnhandledExceptionFilter(UnhandledExceptionHandler);
#endif
#ifdef Q_OS_WIN32
#if defined(Q_OS_WIN32)
// Force AntiHooking.dll to be statically imported and loaded
// by ntdll by calling a dummy function.
AntiHookingDummyImport();
#elif defined(Q_OS_LINUX)
// Force libssl.so to be directly linked to our binary, so
// linuxdeployqt can find it and include it in our AppImage.
// QtNetwork will pull it in via dlopen().
SSL_free(nullptr);
#endif
QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
// Avoid using High DPI on EGLFS. It breaks font rendering.
// https://bugreports.qt.io/browse/QTBUG-64377
//
// NB: We can't use QGuiApplication::platformName() here because it is only
// set once the QGuiApplication is created, which is too late to enable High DPI :(
if (WMUtils::isRunningWindowManager()) {
// Enable High DPI support
QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)
// Enable fractional High DPI scaling on Qt 5.14 and later
QGuiApplication::setHighDpiScaleFactorRoundingPolicy(Qt::HighDpiScaleFactorRoundingPolicy::PassThrough);
#endif
}
else {
#ifndef STEAM_LINK
if (!qEnvironmentVariableIsSet("QT_QPA_PLATFORM")) {
qInfo() << "Unable to detect Wayland or X11, so EGLFS will be used by default. Set QT_QPA_PLATFORM to override this.";
qputenv("QT_QPA_PLATFORM", "eglfs");
if (!QFile("/dev/dri").exists()) {
qWarning() << "Unable to find a KMSDRM display device!";
qWarning() << "On the Raspberry Pi, you must enable the 'fake KMS' driver in raspi-config to use Moonlight outside of the GUI environment.";
}
}
#endif
}
// This avoids using the default keychain for SSL, which may cause
// password prompts on macOS.
qputenv("QT_SSL_USE_TEMPORARY_KEYCHAIN", "1");
// The debug build of ANGLE on Qt 5.12.2 triggers all sorts of asserts
// even simply creating an empty Qt Quick window and then trying to close
// the program. Avoid using ANGLE for debug builds until the issue is fixed.
#if defined(Q_OS_WIN32) && !defined(QT_DEBUG)
#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)
@@ -312,22 +350,47 @@ int main(int argc, char *argv[])
// Register custom metatypes for use in signals
qRegisterMetaType<NvApp>("NvApp");
SDL_version compileVersion;
SDL_VERSION(&compileVersion);
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Compiled with SDL %d.%d.%d",
compileVersion.major, compileVersion.minor, compileVersion.patch);
SDL_version runtimeVersion;
SDL_GetVersion(&runtimeVersion);
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Running with SDL %d.%d.%d",
runtimeVersion.major, runtimeVersion.minor, runtimeVersion.patch);
// Allow the display to sleep by default. We will manually use SDL_DisableScreenSaver()
// and SDL_EnableScreenSaver() when appropriate. This hint must be set before
// initializing the SDL video subsystem to have any effect.
SDL_SetHint(SDL_HINT_VIDEO_ALLOW_SCREENSAVER, "1");
// Steam Link requires that we initialize video before creating our
// QGuiApplication in order to configure the framebuffer correctly.
// It's fine to do on other platforms too, and it can save some time
// doing initialization and teardown of the video subsystem after streaming.
if (SDL_InitSubSystem(SDL_INIT_VIDEO | SDL_INIT_TIMER) != 0) {
// For SDL backends that support it, use double buffering instead of triple buffering
// to save a frame of latency. This doesn't matter for MMAL or DRM renderers since they
// are drawing directly to the screen without involving SDL, but it may matter for other
// future KMSDRM platforms that use SDL for rendering.
SDL_SetHint(SDL_HINT_VIDEO_DOUBLE_BUFFER, "1");
if (SDL_InitSubSystem(SDL_INIT_TIMER) != 0) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"SDL_InitSubSystem(SDL_INIT_VIDEO | SDL_INIT_TIMER) failed: %s",
"SDL_InitSubSystem(SDL_INIT_TIMER) failed: %s",
SDL_GetError());
return -1;
}
#ifdef STEAM_LINK
// Steam Link requires that we initialize video before creating our
// QGuiApplication in order to configure the framebuffer correctly.
if (SDL_InitSubSystem(SDL_INIT_VIDEO) != 0) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"SDL_InitSubSystem(SDL_INIT_VIDEO) failed: %s",
SDL_GetError());
return -1;
}
#endif
// Use atexit() to ensure SDL_Quit() is called. This avoids
// racing with object destruction where SDL may be used.
atexit(SDL_Quit);
@@ -340,8 +403,31 @@ int main(int argc, char *argv[])
// Disable minimize on focus loss by default. Users seem to want this off by default.
SDL_SetHint(SDL_HINT_VIDEO_MINIMIZE_ON_FOCUS_LOSS, "0");
// SDL 2.0.12 changes the default behavior to use the button label rather than the button
// position as most other software does. Set this back to 0 to stay consistent with prior
// releases of Moonlight.
SDL_SetHint("SDL_GAMECONTROLLER_USE_BUTTON_LABELS", "0");
#ifdef QT_DEBUG
// Allow thread naming using exceptions on debug builds. SDL doesn't use SEH
// when throwing the exceptions, so we don't enable it for release builds out
// of caution.
SDL_SetHint(SDL_HINT_WINDOWS_DISABLE_THREAD_NAMING, "0");
#endif
QGuiApplication app(argc, argv);
// After the QGuiApplication is created, the platform stuff will be initialized
// and we can set the SDL video driver to match Qt.
if (WMUtils::isRunningWayland() && QGuiApplication::platformName() == "xcb") {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"Detected XWayland. This will probably break hardware decoding! Try running with QT_QPA_PLATFORM=wayland or switch to X11.");
}
else if (QGuiApplication::platformName().startsWith("wayland")) {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Detected Wayland");
qputenv("SDL_VIDEODRIVER", "wayland");
}
#ifdef STEAM_LINK
// Qt 5.9 from the Steam Link SDK is not able to load any fonts
// since the Steam Link doesn't include any of the ones it looks
@@ -356,6 +442,17 @@ int main(int argc, char *argv[])
// Move the mouse to the bottom right so it's invisible when using
// gamepad-only navigation.
QCursor().setPos(0xFFFF, 0xFFFF);
#elif !SDL_VERSION_ATLEAST(2, 0, 11) && defined(Q_OS_LINUX) && (defined(__arm__) || defined(__aarch64__))
if (qgetenv("SDL_VIDEO_GL_DRIVER").isEmpty() && QGuiApplication::platformName() == "eglfs") {
// Look for Raspberry Pi GLES libraries. SDL 2.0.10 and earlier needs some help finding
// the correct libraries for the KMSDRM backend if not compiled with the RPI backend enabled.
if (SDL_LoadObject("libbrcmGLESv2.so") != nullptr) {
qputenv("SDL_VIDEO_GL_DRIVER", "libbrcmGLESv2.so");
}
else if (SDL_LoadObject("/opt/vc/lib/libbrcmGLESv2.so") != nullptr) {
qputenv("SDL_VIDEO_GL_DRIVER", "/opt/vc/lib/libbrcmGLESv2.so");
}
}
#endif
app.setWindowIcon(QIcon(":/res/moonlight.svg"));
@@ -390,6 +487,9 @@ int main(int argc, char *argv[])
return new StreamingPreferences();
});
// Create the identity manager on the main thread
IdentityManager::get();
#ifndef Q_OS_WINRT
// Use the dense material dark theme by default
if (!qEnvironmentVariableIsSet("QT_QUICK_CONTROLS_STYLE")) {
@@ -413,9 +513,6 @@ int main(int argc, char *argv[])
if (!qEnvironmentVariableIsSet("QT_QUICK_CONTROLS_UNIVERSAL_THEME")) {
qputenv("QT_QUICK_CONTROLS_UNIVERSAL_THEME", "Dark");
}
if (!qEnvironmentVariableIsSet("QT_QUICK_CONTROLS_1_STYLE")) {
qputenv("QT_QUICK_CONTROLS_1_STYLE", "Base");
}
QQmlApplicationEngine engine;
QString initialView;
+4
View File
@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="#FFFFFF" width="48" height="48" viewBox="0 0 24 24">
<path fill="none" d="M0 0h24v24H0V0z"/>
<path d="M21 2H3c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h7v2H8v2h8v-2h-2v-2h7c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2zm0 14H3V4h18v12z"/>
</svg>

After

Width:  |  Height:  |  Size: 276 B

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

Before

Width:  |  Height:  |  Size: 303 B

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

Before

Width:  |  Height:  |  Size: 467 B

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

Before

Width:  |  Height:  |  Size: 273 B

+3 -3
View File
@@ -1,9 +1,7 @@
<RCC>
<qresource prefix="/">
<file>res/ic_remove_circle_outline_white_48px.svg</file>
<file>res/ic_remove_from_queue_white_48px.svg</file>
<file>res/ic_videogame_asset_white_48px.svg</file>
<file>res/ic_tv_white_48px.svg</file>
<file>res/desktop_windows-48px.svg</file>
<file>res/ic_add_to_queue_white_48px.svg</file>
<file>res/baseline-lock-24px.svg</file>
<file>res/baseline-play_circle_filled_white-48px.svg</file>
@@ -21,5 +19,7 @@
<qresource prefix="/data">
<file alias="gamecontrollerdb.txt">SDL_GameControllerDB/gamecontrollerdb.txt</file>
<file alias="ModeSeven.ttf">ModeSeven.ttf</file>
<file alias="egl.frag">shaders/egl.frag</file>
<file alias="egl.vert">shaders/egl.vert</file>
</qresource>
</RCC>
+7 -1
View File
@@ -26,7 +26,13 @@ MappingManager::MappingManager()
settings.endArray();
// Finally load mappings from SDL_HINT_GAMECONTROLLERCONFIG
QStringList sdlMappings = QString::fromLocal8Bit(SDL_GetHint(SDL_HINT_GAMECONTROLLERCONFIG)).split('\n', QString::SkipEmptyParts);
QStringList sdlMappings =
QString::fromLocal8Bit(SDL_GetHint(SDL_HINT_GAMECONTROLLERCONFIG))
#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)
.split('\n', Qt::SkipEmptyParts);
#else
.split('\n', QString::SkipEmptyParts);
#endif
for (QString sdlMapping : sdlMappings) {
SdlGamepadMapping mapping(sdlMapping);
addMapping(mapping);
+39 -5
View File
@@ -19,10 +19,17 @@
#define SER_UNSUPPORTEDFPS "unsupportedfps"
#define SER_MDNS "mdns"
#define SER_QUITAPPAFTER "quitAppAfter"
#define SER_MOUSEACCELERATION "mouseacceleration"
#define SER_ABSMOUSEMODE "mouseacceleration"
#define SER_ABSTOUCHMODE "abstouchmode"
#define SER_STARTWINDOWED "startwindowed"
#define SER_FRAMEPACING "framepacing"
#define SER_CONNWARNINGS "connwarnings"
#define SER_RICHPRESENCE "richpresence"
#define SER_GAMEPADMOUSE "gamepadmouse"
#define SER_DEFAULTVER "defaultver"
#define SER_PACKETSIZE "packetsize"
#define CURRENT_DEFAULT_VER 1
StreamingPreferences::StreamingPreferences(QObject *parent)
: QObject(parent)
@@ -34,6 +41,14 @@ void StreamingPreferences::reload()
{
QSettings settings;
int defaultVer = settings.value(SER_DEFAULTVER, 0).toInt();
#ifdef Q_OS_DARWIN
recommendedFullScreenMode = WindowMode::WM_FULLSCREEN_DESKTOP;
#else
recommendedFullScreenMode = WindowMode::WM_FULLSCREEN;
#endif
width = settings.value(SER_WIDTH, 1280).toInt();
height = settings.value(SER_HEIGHT, 720).toInt();
fps = settings.value(SER_FPS, 60).toInt();
@@ -45,10 +60,14 @@ void StreamingPreferences::reload()
unsupportedFps = settings.value(SER_UNSUPPORTEDFPS, false).toBool();
enableMdns = settings.value(SER_MDNS, true).toBool();
quitAppAfter = settings.value(SER_QUITAPPAFTER, false).toBool();
mouseAcceleration = settings.value(SER_MOUSEACCELERATION, false).toBool();
startWindowed = settings.value(SER_STARTWINDOWED, false).toBool();
absoluteMouseMode = settings.value(SER_ABSMOUSEMODE, false).toBool();
absoluteTouchMode = settings.value(SER_ABSTOUCHMODE, true).toBool();
startWindowed = settings.value(SER_STARTWINDOWED, true).toBool();
framePacing = settings.value(SER_FRAMEPACING, false).toBool();
connectionWarnings = settings.value(SER_CONNWARNINGS, true).toBool();
richPresence = settings.value(SER_RICHPRESENCE, true).toBool();
gamepadMouse = settings.value(SER_GAMEPADMOUSE, true).toBool();
packetSize = settings.value(SER_PACKETSIZE, 0).toInt();
audioConfig = static_cast<AudioConfig>(settings.value(SER_AUDIOCFG,
static_cast<int>(AudioConfig::AC_STEREO)).toInt());
videoCodecConfig = static_cast<VideoCodecConfig>(settings.value(SER_VIDEOCFG,
@@ -58,7 +77,17 @@ void StreamingPreferences::reload()
windowMode = static_cast<WindowMode>(settings.value(SER_WINDOWMODE,
// Try to load from the old preference value too
static_cast<int>(settings.value(SER_FULLSCREEN, true).toBool() ?
WindowMode::WM_FULLSCREEN : WindowMode::WM_WINDOWED)).toInt());
recommendedFullScreenMode : WindowMode::WM_WINDOWED)).toInt());
// Perform default settings updates as required based on last default version
if (defaultVer == 0) {
#ifdef Q_OS_DARWIN
// Update window mode setting on macOS from full-screen (old default) to borderless windowed (new default)
if (windowMode == WindowMode::WM_FULLSCREEN) {
windowMode = WindowMode::WM_FULLSCREEN_DESKTOP;
}
#endif
}
}
void StreamingPreferences::save()
@@ -76,14 +105,19 @@ void StreamingPreferences::save()
settings.setValue(SER_UNSUPPORTEDFPS, unsupportedFps);
settings.setValue(SER_MDNS, enableMdns);
settings.setValue(SER_QUITAPPAFTER, quitAppAfter);
settings.setValue(SER_MOUSEACCELERATION, mouseAcceleration);
settings.setValue(SER_ABSMOUSEMODE, absoluteMouseMode);
settings.setValue(SER_ABSTOUCHMODE, absoluteTouchMode);
settings.setValue(SER_STARTWINDOWED, startWindowed);
settings.setValue(SER_FRAMEPACING, framePacing);
settings.setValue(SER_CONNWARNINGS, connectionWarnings);
settings.setValue(SER_RICHPRESENCE, richPresence);
settings.setValue(SER_GAMEPADMOUSE, gamepadMouse);
settings.setValue(SER_PACKETSIZE, packetSize);
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));
settings.setValue(SER_DEFAULTVER, CURRENT_DEFAULT_VER);
}
int StreamingPreferences::getDefaultBitrate(int width, int height, int fps)
+17 -4
View File
@@ -20,7 +20,8 @@ public:
enum AudioConfig
{
AC_STEREO,
AC_51_SURROUND
AC_51_SURROUND,
AC_71_SURROUND
};
Q_ENUM(AudioConfig)
@@ -60,14 +61,18 @@ public:
Q_PROPERTY(bool unsupportedFps MEMBER unsupportedFps NOTIFY unsupportedFpsChanged)
Q_PROPERTY(bool enableMdns MEMBER enableMdns NOTIFY enableMdnsChanged)
Q_PROPERTY(bool quitAppAfter MEMBER quitAppAfter NOTIFY quitAppAfterChanged)
Q_PROPERTY(bool mouseAcceleration MEMBER mouseAcceleration NOTIFY mouseAccelerationChanged)
Q_PROPERTY(bool absoluteMouseMode MEMBER absoluteMouseMode NOTIFY absoluteMouseModeChanged)
Q_PROPERTY(bool absoluteTouchMode MEMBER absoluteTouchMode NOTIFY absoluteTouchModeChanged)
Q_PROPERTY(bool startWindowed MEMBER startWindowed NOTIFY startWindowedChanged)
Q_PROPERTY(bool framePacing MEMBER framePacing NOTIFY framePacingChanged)
Q_PROPERTY(bool connectionWarnings MEMBER connectionWarnings NOTIFY connectionWarningsChanged)
Q_PROPERTY(bool richPresence MEMBER richPresence NOTIFY richPresenceChanged)
Q_PROPERTY(bool gamepadMouse MEMBER gamepadMouse NOTIFY gamepadMouseChanged)
Q_PROPERTY(AudioConfig audioConfig MEMBER audioConfig NOTIFY audioConfigChanged)
Q_PROPERTY(VideoCodecConfig videoCodecConfig MEMBER videoCodecConfig NOTIFY videoCodecConfigChanged)
Q_PROPERTY(VideoDecoderSelection videoDecoderSelection MEMBER videoDecoderSelection NOTIFY videoDecoderSelectionChanged)
Q_PROPERTY(WindowMode windowMode MEMBER windowMode NOTIFY windowModeChanged)
Q_PROPERTY(WindowMode recommendedFullScreenMode MEMBER recommendedFullScreenMode CONSTANT)
// Directly accessible members for preferences
int width;
@@ -81,14 +86,19 @@ public:
bool unsupportedFps;
bool enableMdns;
bool quitAppAfter;
bool mouseAcceleration;
bool absoluteMouseMode;
bool absoluteTouchMode;
bool startWindowed;
bool framePacing;
bool connectionWarnings;
bool richPresence;
bool gamepadMouse;
int packetSize;
AudioConfig audioConfig;
VideoCodecConfig videoCodecConfig;
VideoDecoderSelection videoDecoderSelection;
WindowMode windowMode;
WindowMode recommendedFullScreenMode;
signals:
void displayModeChanged();
@@ -100,7 +110,8 @@ signals:
void unsupportedFpsChanged();
void enableMdnsChanged();
void quitAppAfterChanged();
void mouseAccelerationChanged();
void absoluteMouseModeChanged();
void absoluteTouchModeChanged();
void audioConfigChanged();
void videoCodecConfigChanged();
void videoDecoderSelectionChanged();
@@ -108,5 +119,7 @@ signals:
void startWindowedChanged();
void framePacingChanged();
void connectionWarningsChanged();
void richPresenceChanged();
void gamepadMouseChanged();
};
+21
View File
@@ -0,0 +1,21 @@
#version 300 es
#extension GL_OES_EGL_image_external : require
precision mediump float;
out vec4 FragColor;
in vec2 vTextCoord;
uniform mat3 yuvmat;
uniform vec3 offset;
uniform samplerExternalOES plane1;
uniform samplerExternalOES plane2;
void main() {
vec3 YCbCr = vec3(
texture2D(plane1, vTextCoord)[0],
texture2D(plane2, vTextCoord).xy
);
YCbCr -= offset;
FragColor = vec4(clamp(yuvmat * YCbCr, 0.0, 1.0), 1.0f);
}
+10
View File
@@ -0,0 +1,10 @@
#version 300 es
layout (location = 0) in vec2 aPosition; // 2D: X,Y
layout (location = 1) in vec2 aTexCoord;
out vec2 vTextCoord;
void main() {
vTextCoord = aTexCoord;
gl_Position = vec4(aPosition, 0, 1);
}
+128 -64
View File
@@ -13,53 +13,104 @@
#include <Limelight.h>
IAudioRenderer* Session::createAudioRenderer()
{
#if defined(HAVE_SOUNDIO)
if (qgetenv("ML_AUDIO") == "SDL") {
return new SdlAudioRenderer();
}
return new SoundIoAudioRenderer();
#elif defined(HAVE_SLAUDIO)
return new SLAudioRenderer();
#else
return new SdlAudioRenderer();
#endif
#define TRY_INIT_RENDERER(renderer, opusConfig) \
{ \
IAudioRenderer* __renderer = new renderer(); \
if (__renderer->prepareForPlayback(opusConfig)) \
return __renderer; \
delete __renderer; \
}
bool Session::testAudio(int audioConfiguration)
IAudioRenderer* Session::createAudioRenderer(const POPUS_MULTISTREAM_CONFIGURATION opusConfig)
{
IAudioRenderer* audioRenderer;
audioRenderer = createAudioRenderer();
if (audioRenderer == nullptr) {
return false;
// Handle explicit ML_AUDIO setting and fail if the requested backend fails
QString mlAudio = qgetenv("ML_AUDIO").toLower();
if (mlAudio == "sdl") {
TRY_INIT_RENDERER(SdlAudioRenderer, opusConfig)
return nullptr;
}
#ifdef HAVE_SOUNDIO
else if (mlAudio == "libsoundio") {
TRY_INIT_RENDERER(SoundIoAudioRenderer, opusConfig)
return nullptr;
}
#endif
#if defined(HAVE_SLAUDIO)
else if (mlAudio == "slaudio") {
TRY_INIT_RENDERER(SLAudioRenderer, opusConfig)
return nullptr;
}
#endif
else if (!mlAudio.isEmpty()) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Unknown audio backend: %s",
SDL_getenv("ML_AUDIO"));
return nullptr;
}
// -------------- Automatic backend selection below this line ---------------
#if defined(HAVE_SLAUDIO)
// Steam Link should always have SLAudio
TRY_INIT_RENDERER(SLAudioRenderer, opusConfig)
#endif
#if !defined(Q_OS_WIN32) && !defined(Q_OS_DARWIN)
// Linux defaults to SDL due to persistent glitching issues under libsoundio.
// Platforms that libsoundio doesn't support also default to SDL.
TRY_INIT_RENDERER(SdlAudioRenderer, opusConfig)
#ifdef HAVE_SOUNDIO
TRY_INIT_RENDERER(SoundIoAudioRenderer, opusConfig)
#endif
#else
// Windows and macOS default to libsoundio and fall back to SDL
#ifdef HAVE_SOUNDIO
TRY_INIT_RENDERER(SoundIoAudioRenderer, opusConfig)
#endif
TRY_INIT_RENDERER(SdlAudioRenderer, opusConfig)
#endif
return nullptr;
}
int Session::getAudioRendererCapabilities(int audioConfiguration)
{
// Build a fake OPUS_MULTISTREAM_CONFIGURATION to give
// the renderer the channel count and sample rate.
OPUS_MULTISTREAM_CONFIGURATION opusConfig = {};
opusConfig.sampleRate = 48000;
opusConfig.samplesPerFrame = 240;
opusConfig.channelCount = CHANNEL_COUNT_FROM_AUDIO_CONFIGURATION(audioConfiguration);
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;
IAudioRenderer* audioRenderer = createAudioRenderer(&opusConfig);
if (audioRenderer == nullptr) {
return 0;
}
bool ret = audioRenderer->prepareForPlayback(&opusConfig);
int caps = audioRenderer->getCapabilities();
delete audioRenderer;
return ret;
return caps;
}
bool Session::testAudio(int audioConfiguration)
{
// Build a fake OPUS_MULTISTREAM_CONFIGURATION to give
// the renderer the channel count and sample rate.
OPUS_MULTISTREAM_CONFIGURATION opusConfig = {};
opusConfig.sampleRate = 48000;
opusConfig.samplesPerFrame = 240;
opusConfig.channelCount = CHANNEL_COUNT_FROM_AUDIO_CONFIGURATION(audioConfiguration);
IAudioRenderer* audioRenderer = createAudioRenderer(&opusConfig);
if (audioRenderer == nullptr) {
return false;
}
delete audioRenderer;
return true;
}
int Session::arInit(int /* audioConfiguration */,
@@ -70,30 +121,34 @@ int Session::arInit(int /* audioConfiguration */,
SDL_memcpy(&s_ActiveSession->m_AudioConfig, opusConfig, sizeof(*opusConfig));
s_ActiveSession->m_AudioRenderer = s_ActiveSession->createAudioRenderer(&s_ActiveSession->m_AudioConfig);
if (s_ActiveSession->m_AudioRenderer == nullptr) {
return -2;
}
// Allow the chosen renderer to remap Opus channels as needed to ensure proper output
s_ActiveSession->m_AudioRenderer->remapChannels(&s_ActiveSession->m_AudioConfig);
// Create the Opus decoder with the renderer's preferred channel mapping
s_ActiveSession->m_OpusDecoder =
opus_multistream_decoder_create(opusConfig->sampleRate,
opusConfig->channelCount,
opusConfig->streams,
opusConfig->coupledStreams,
opusConfig->mapping,
opus_multistream_decoder_create(s_ActiveSession->m_AudioConfig.sampleRate,
s_ActiveSession->m_AudioConfig.channelCount,
s_ActiveSession->m_AudioConfig.streams,
s_ActiveSession->m_AudioConfig.coupledStreams,
s_ActiveSession->m_AudioConfig.mapping,
&error);
if (s_ActiveSession->m_OpusDecoder == NULL) {
delete s_ActiveSession->m_AudioRenderer;
s_ActiveSession->m_AudioRenderer = nullptr;
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Failed to create decoder: %d",
error);
return -1;
}
s_ActiveSession->m_AudioRenderer = s_ActiveSession->createAudioRenderer();
if (!s_ActiveSession->m_AudioRenderer->prepareForPlayback(opusConfig)) {
delete s_ActiveSession->m_AudioRenderer;
opus_multistream_decoder_destroy(s_ActiveSession->m_OpusDecoder);
return -2;
}
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Audio stream has %d channels",
opusConfig->channelCount);
s_ActiveSession->m_AudioConfig.channelCount);
return 0;
}
@@ -111,8 +166,11 @@ void Session::arDecodeAndPlaySample(char* sampleData, int sampleLength)
{
int samplesDecoded;
// Set this thread to high priority to reduce
// the chance of missing our sample delivery time
#ifndef STEAM_LINK
// Set this thread to high priority to reduce the chance of missing
// our sample delivery time. On Steam Link, this causes starvation
// of other threads due to severely restricted CPU time available,
// so we will skip it on that platform.
if (s_ActiveSession->m_AudioSampleCount == 0) {
if (SDL_SetThreadPriority(SDL_THREAD_PRIORITY_HIGH) < 0) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
@@ -120,6 +178,7 @@ void Session::arDecodeAndPlaySample(char* sampleData, int sampleLength)
SDL_GetError());
}
}
#endif
// See if we need to drop this sample
if (s_ActiveSession->m_DropAudioEndTime != 0) {
@@ -139,25 +198,34 @@ void Session::arDecodeAndPlaySample(char* sampleData, int sampleLength)
s_ActiveSession->m_AudioSampleCount++;
if (s_ActiveSession->m_AudioRenderer != nullptr) {
int desiredSize = sizeof(short) * s_ActiveSession->m_AudioConfig.samplesPerFrame * s_ActiveSession->m_AudioConfig.channelCount;
void* buffer = s_ActiveSession->m_AudioRenderer->getAudioBuffer(&desiredSize);
if (buffer == nullptr) {
return;
}
samplesDecoded = opus_multistream_decode(s_ActiveSession->m_OpusDecoder,
(unsigned char*)sampleData,
sampleLength,
s_ActiveSession->m_OpusDecodeBuffer,
SAMPLES_PER_FRAME,
(short*)buffer,
desiredSize / sizeof(short) / s_ActiveSession->m_AudioConfig.channelCount,
0);
// Update desiredSize with the number of bytes actually populated by the decoding operation
if (samplesDecoded > 0) {
if (!s_ActiveSession->m_AudioRenderer->submitAudio(s_ActiveSession->m_OpusDecodeBuffer,
static_cast<int>(
sizeof(short) *
samplesDecoded *
s_ActiveSession->m_AudioConfig.channelCount))) {
SDL_assert(desiredSize >= (int)(sizeof(short) * samplesDecoded * s_ActiveSession->m_AudioConfig.channelCount));
desiredSize = sizeof(short) * samplesDecoded * s_ActiveSession->m_AudioConfig.channelCount;
}
else {
desiredSize = 0;
}
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"Reinitializing audio renderer after failure");
if (!s_ActiveSession->m_AudioRenderer->submitAudio(desiredSize)) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"Reinitializing audio renderer after failure");
delete s_ActiveSession->m_AudioRenderer;
s_ActiveSession->m_AudioRenderer = nullptr;
}
delete s_ActiveSession->m_AudioRenderer;
s_ActiveSession->m_AudioRenderer = nullptr;
}
}
@@ -171,11 +239,7 @@ void Session::arDecodeAndPlaySample(char* sampleData, int sampleLength)
// so we return to real-time playback and don't accumulate latency.
Uint32 audioReinitStartTime = SDL_GetTicks();
s_ActiveSession->m_AudioRenderer = s_ActiveSession->createAudioRenderer();
if (!s_ActiveSession->m_AudioRenderer->prepareForPlayback(&s_ActiveSession->m_AudioConfig)) {
delete s_ActiveSession->m_AudioRenderer;
s_ActiveSession->m_AudioRenderer = nullptr;
}
s_ActiveSession->m_AudioRenderer = s_ActiveSession->createAudioRenderer(&s_ActiveSession->m_AudioConfig);
Uint32 audioReinitStopTime = SDL_GetTicks();
+15 -4
View File
@@ -2,9 +2,6 @@
#include <Limelight.h>
#define MAX_CHANNELS 6
#define SAMPLES_PER_FRAME 240
class IAudioRenderer
{
public:
@@ -12,6 +9,20 @@ public:
virtual bool prepareForPlayback(const OPUS_MULTISTREAM_CONFIGURATION* opusConfig) = 0;
virtual void* getAudioBuffer(int* size) = 0;
// Return false if an unrecoverable error has occurred and the renderer must be reinitialized
virtual bool submitAudio(short* audioBuffer, int audioSize) = 0;
virtual bool submitAudio(int bytesWritten) = 0;
virtual int getCapabilities() = 0;
virtual void remapChannels(POPUS_MULTISTREAM_CONFIGURATION) {
// Use default channel mapping:
// 0 - Front Left
// 1 - Front Right
// 2 - Center
// 3 - LFE
// 4 - Surround Left
// 5 - Surround Right
}
};
+7 -1
View File
@@ -12,8 +12,14 @@ public:
virtual bool prepareForPlayback(const OPUS_MULTISTREAM_CONFIGURATION* opusConfig);
virtual bool submitAudio(short* audioBuffer, int audioSize);
virtual void* getAudioBuffer(int* size);
virtual bool submitAudio(int bytesWritten);
virtual int getCapabilities();
private:
SDL_AudioDeviceID m_AudioDevice;
void* m_AudioBuffer;
int m_FrameSize;
};
+78 -7
View File
@@ -4,11 +4,42 @@
#include <SDL.h>
#include <QtGlobal>
#include <QFile>
#include <QTextStream>
SdlAudioRenderer::SdlAudioRenderer()
: m_AudioDevice(0)
: m_AudioDevice(0),
m_AudioBuffer(nullptr)
{
SDL_assert(!SDL_WasInit(SDL_INIT_AUDIO));
#ifdef HAVE_MMAL
// HACK: PulseAudio on Raspberry Pi suffers from horrible underruns,
// so switch to ALSA if we detect that we're on a Pi. We need to
// actually set a bogus PULSE_SERVER so it doesn't try to get smart on us
// and find PA anyway. SDL_AUDIODRIVER=pulseaudio can override this logic.
if (qgetenv("SDL_AUDIODRIVER").toLower() != "pulseaudio") {
QFile file("/proc/cpuinfo");
if (file.open(QIODevice::ReadOnly | QFile::Text)) {
QTextStream textStream(&file);
if (textStream.readAll().contains("Raspberry Pi")) {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Avoiding PulseAudio on Raspberry Pi");
qputenv("PULSE_SERVER", "");
if (SDL_InitSubSystem(SDL_INIT_AUDIO) != 0) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"ALSA failed; falling back to default");
qunsetenv("PULSE_SERVER");
}
else {
SDL_QuitSubSystem(SDL_INIT_AUDIO);
}
}
}
}
#endif
if (SDL_InitSubSystem(SDL_INIT_AUDIO) != 0) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"SDL_InitSubSystem(SDL_INIT_AUDIO) failed: %s",
@@ -30,21 +61,29 @@ bool SdlAudioRenderer::prepareForPlayback(const OPUS_MULTISTREAM_CONFIGURATION*
// frames contain a non-power of 2 number of samples,
// so the slop would require buffering another full frame.
// Specifying non-Po2 seems to work for our supported platforms.
want.samples = SAMPLES_PER_FRAME;
want.samples = opusConfig->samplesPerFrame;
m_FrameSize = opusConfig->samplesPerFrame * sizeof(short) * opusConfig->channelCount;
m_AudioDevice = SDL_OpenAudioDevice(NULL, 0, &want, &have, 0);
if (m_AudioDevice == 0) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Failed to open audio device: %s",
SDL_GetError());
SDL_QuitSubSystem(SDL_INIT_AUDIO);
return false;
}
m_AudioBuffer = malloc(m_FrameSize);
if (m_AudioBuffer == nullptr) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Failed to allocate audio buffer");
return false;
}
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Desired audio buffer: %u samples (%lu bytes)",
"Desired audio buffer: %u samples (%u bytes)",
want.samples,
want.samples * sizeof(short) * want.channels);
want.samples * (Uint32)sizeof(short) * want.channels);
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Obtained audio buffer: %u samples (%u bytes)",
@@ -65,13 +104,39 @@ SdlAudioRenderer::~SdlAudioRenderer()
SDL_CloseAudioDevice(m_AudioDevice);
}
if (m_AudioBuffer != nullptr) {
free(m_AudioBuffer);
}
SDL_QuitSubSystem(SDL_INIT_AUDIO);
SDL_assert(!SDL_WasInit(SDL_INIT_AUDIO));
}
bool SdlAudioRenderer::submitAudio(short* audioBuffer, int audioSize)
void* SdlAudioRenderer::getAudioBuffer(int*)
{
if (SDL_QueueAudio(m_AudioDevice, audioBuffer, audioSize) < 0) {
return m_AudioBuffer;
}
bool SdlAudioRenderer::submitAudio(int bytesWritten)
{
if (bytesWritten == 0) {
// Nothing to do
return true;
}
// Don't queue if there's already more than 30 ms of audio data waiting
// in Moonlight's audio queue.
if (LiGetPendingAudioDuration() > 30) {
return true;
}
// Provide backpressure on the queue to ensure too many frames don't build up
// in SDL's audio queue.
while (SDL_GetQueuedAudioSize(m_AudioDevice) / m_FrameSize > 10) {
SDL_Delay(1);
}
if (SDL_QueueAudio(m_AudioDevice, m_AudioBuffer, bytesWritten) < 0) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Failed to queue audio sample: %s",
SDL_GetError());
@@ -79,3 +144,9 @@ bool SdlAudioRenderer::submitAudio(short* audioBuffer, int audioSize)
return true;
}
int SdlAudioRenderer::getCapabilities()
{
// Direct submit can't be used because we use LiGetPendingAudioDuration()
return CAPABILITY_SUPPORTS_ARBITRARY_AUDIO_DURATION;
}
+94 -7
View File
@@ -4,8 +4,10 @@
SLAudioRenderer::SLAudioRenderer()
: m_AudioContext(nullptr),
m_AudioStream(nullptr)
m_AudioStream(nullptr),
m_AudioBuffer(nullptr)
{
SLAudio_SetLogFunction(SLAudioRenderer::slLogCallback, nullptr);
}
bool SLAudioRenderer::prepareForPlayback(const OPUS_MULTISTREAM_CONFIGURATION* opusConfig)
@@ -17,10 +19,15 @@ bool SLAudioRenderer::prepareForPlayback(const OPUS_MULTISTREAM_CONFIGURATION* o
return false;
}
// This number is pretty conservative (especially for surround), but
// it's hard to avoid since we get crushed by CPU limitations.
m_MaxQueuedAudioMs = 40 * opusConfig->channelCount / 2;
m_AudioBufferSize = opusConfig->samplesPerFrame * sizeof(short) * opusConfig->channelCount;
m_AudioStream = SLAudio_CreateStream(m_AudioContext,
opusConfig->sampleRate,
opusConfig->channelCount,
SAMPLES_PER_FRAME * sizeof(short) * opusConfig->channelCount,
m_AudioBufferSize,
1);
if (m_AudioStream == nullptr) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
@@ -29,13 +36,53 @@ bool SLAudioRenderer::prepareForPlayback(const OPUS_MULTISTREAM_CONFIGURATION* o
}
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Using SLAudio renderer");
"Using SLAudio renderer with %d samples per frame",
opusConfig->samplesPerFrame);
return true;
}
void SLAudioRenderer::remapChannels(POPUS_MULTISTREAM_CONFIGURATION opusConfig) {
OPUS_MULTISTREAM_CONFIGURATION originalConfig = *opusConfig;
// The Moonlight's default channel order is FL,FR,C,LFE,RL,RR,SL,SR
// SLAudio expects FL,C,FR,RL,RR,(SL,SR),LFE for 5.1/7.1 so we swap the channels around to match
if (opusConfig->channelCount == 3 || opusConfig->channelCount >= 6) {
// Swap FR and C
opusConfig->mapping[1] = originalConfig.mapping[2];
opusConfig->mapping[2] = originalConfig.mapping[1];
}
if (opusConfig->channelCount >= 6) {
// SLAudio expects the LFE channel at the end
opusConfig->mapping[opusConfig->channelCount - 1] = originalConfig.mapping[3];
// Slide the other channels down
memcpy(&opusConfig->mapping[3],
&originalConfig.mapping[4],
opusConfig->channelCount - 4);
}
}
void* SLAudioRenderer::getAudioBuffer(int* size)
{
SDL_assert(*size == m_AudioBufferSize);
if (m_AudioBuffer == nullptr) {
m_AudioBuffer = SLAudio_BeginFrame(m_AudioStream);
}
return m_AudioBuffer;
}
SLAudioRenderer::~SLAudioRenderer()
{
if (m_AudioBuffer != nullptr) {
memset(m_AudioBuffer, 0, m_AudioBufferSize);
SLAudio_SubmitFrame(m_AudioStream);
}
if (m_AudioStream != nullptr) {
SLAudio_FreeStream(m_AudioStream);
}
@@ -45,14 +92,54 @@ SLAudioRenderer::~SLAudioRenderer()
}
}
bool SLAudioRenderer::submitAudio(short* audioBuffer, int audioSize)
bool SLAudioRenderer::submitAudio(int bytesWritten)
{
void* outputBuffer = SLAudio_BeginFrame(m_AudioStream);
if (bytesWritten == 0) {
// This buffer will be reused next time
return true;
}
if (outputBuffer != nullptr) {
memcpy(outputBuffer, audioBuffer, audioSize);
if (LiGetPendingAudioDuration() < m_MaxQueuedAudioMs) {
SLAudio_SubmitFrame(m_AudioStream);
m_AudioBuffer = nullptr;
}
else {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Too many queued audio frames: %d",
LiGetPendingAudioFrames());
}
return true;
}
int SLAudioRenderer::getCapabilities()
{
return CAPABILITY_SLOW_OPUS_DECODER | CAPABILITY_SUPPORTS_ARBITRARY_AUDIO_DURATION;
}
void SLAudioRenderer::slLogCallback(void*, ESLAudioLog logLevel, const char *message)
{
SDL_LogPriority priority;
switch (logLevel)
{
case k_ESLAudioLogError:
priority = SDL_LOG_PRIORITY_ERROR;
break;
case k_ESLAudioLogWarning:
priority = SDL_LOG_PRIORITY_WARN;
break;
case k_ESLAudioLogInfo:
priority = SDL_LOG_PRIORITY_INFO;
break;
default:
case k_ESLAudioLogDebug:
priority = SDL_LOG_PRIORITY_DEBUG;
break;
}
SDL_LogMessage(SDL_LOG_CATEGORY_APPLICATION,
priority,
"SLAudio: %s",
message);
}
+13 -1
View File
@@ -12,9 +12,21 @@ public:
virtual bool prepareForPlayback(const OPUS_MULTISTREAM_CONFIGURATION* opusConfig);
virtual bool submitAudio(short* audioBuffer, int audioSize);
virtual void* getAudioBuffer(int* size);
virtual bool submitAudio(int bytesWritten);
virtual int getCapabilities();
virtual void remapChannels(POPUS_MULTISTREAM_CONFIGURATION opusConfig);
private:
static void slLogCallback(void* context, ESLAudioLog logLevel, const char* message);
CSLAudioContext* m_AudioContext;
CSLAudioStream* m_AudioStream;
void* m_AudioBuffer;
int m_AudioBufferSize;
int m_MaxQueuedAudioMs;
};
@@ -4,29 +4,13 @@
#include <QtGlobal>
// GFE sends us packets in 5 ms chunks
const double SoundIoAudioRenderer::k_RawSampleLengthSec = 0.005;
#ifdef Q_OS_LINUX
// PulseAudio and ALSA require more than just 5 ms samples
// for some reason, so write a minimum of 25 ms each time to
// prevent underruns on Bluetooth.
// https://github.com/moonlight-stream/moonlight-qt/issues/147
// https://github.com/moonlight-stream/moonlight-qt/issues/157
const double SoundIoAudioRenderer::k_MinSampleLengthSec = 0.025;
#else
// This determines the size of the buffers we'll
// get from CoreAudio. It is also the minimum
// size that we will write when called to fill a buffer.
const double SoundIoAudioRenderer::k_MinSampleLengthSec = k_RawSampleLengthSec;
#endif
SoundIoAudioRenderer::SoundIoAudioRenderer()
: m_OpusChannelCount(0),
m_SoundIo(nullptr),
m_Device(nullptr),
m_OutputStream(nullptr),
m_RingBuffer(nullptr),
m_AudioPacketDuration(0),
m_Latency(0),
m_Errored(false)
{
@@ -88,11 +72,17 @@ int SoundIoAudioRenderer::scoreChannelLayout(const struct SoundIoChannelLayout*
score++;
break;
// Score layouts using the back L/R as higher
// value than those using side L/R.
case SoundIoChannelIdBackLeft:
case SoundIoChannelIdBackRight:
score += 2;
if (opusConfig->channelCount == 6) {
// Score back channels higher in 5.1 mode to
// discourage selection of side channel layouts.
score += 2;
}
else {
// Score back channels normally for 7.1 mode
score++;
}
break;
default:
@@ -172,9 +162,11 @@ bool SoundIoAudioRenderer::prepareForPlayback(const OPUS_MULTISTREAM_CONFIGURATI
return false;
}
m_AudioPacketDuration = (opusConfig->samplesPerFrame / (opusConfig->sampleRate / 1000)) / 1000.0;
m_OutputStream->format = SoundIoFormatS16NE;
m_OutputStream->sample_rate = opusConfig->sampleRate;
m_OutputStream->software_latency = k_MinSampleLengthSec;
m_OutputStream->software_latency = m_AudioPacketDuration;
m_OutputStream->name = "Moonlight";
m_OutputStream->userdata = this;
m_OutputStream->error_callback = sioErrorCallback;
@@ -225,42 +217,50 @@ bool SoundIoAudioRenderer::prepareForPlayback(const OPUS_MULTISTREAM_CONFIGURATI
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 (opusConfig->channelCount == 6) {
// For 5.1, replace side L/R with 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;
}
}
if (m_EffectiveLayout.channels[i] == SoundIoChannelIdSideRight) {
m_EffectiveLayout.channels[i] = SoundIoChannelIdBackRight;
else if (opusConfig->channelCount == 8) {
// For 5.1, replace side L/R with LOC/ROC so our channel position
// logic in sioWriteCallback() works.
if (m_EffectiveLayout.channels[i] == SoundIoChannelIdSideLeft) {
m_EffectiveLayout.channels[i] = SoundIoChannelIdFrontLeftCenter;
}
if (m_EffectiveLayout.channels[i] == SoundIoChannelIdSideRight) {
m_EffectiveLayout.channels[i] = SoundIoChannelIdFrontRightCenter;
}
}
}
int packetsToBuffer;
#ifdef Q_OS_LINUX
// PulseAudio and ALSA need the large buffer (see comment on k_MinSampleLengthSec),
// so we need a buffer at least double that size to allow packets to arrive
// while we're writing to the sink.
packetsToBuffer = (int)(k_MinSampleLengthSec / k_RawSampleLengthSec) * 2;
#else
if (m_SoundIo->current_backend == SoundIoBackendWasapi) {
// 15 ms buffer seems to be fine for WASAPI
packetsToBuffer = 3;
packetsToBuffer = (int)ceil(0.015 / m_AudioPacketDuration);
}
else {
// 30 ms buffer on CoreAudio to avoid glitching on macOS
packetsToBuffer = 6;
packetsToBuffer = (int)ceil(0.030 / m_AudioPacketDuration);
}
#endif
// Always buffer at least 2 packets
packetsToBuffer = qMax(2, packetsToBuffer);
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Audio buffer size: %d packets",
packetsToBuffer);
"Audio buffer size: %f seconds",
packetsToBuffer * m_AudioPacketDuration);
m_RingBuffer = soundio_ring_buffer_create(m_SoundIo,
m_OutputStream->bytes_per_sample *
m_OpusChannelCount *
SAMPLES_PER_FRAME *
opusConfig->samplesPerFrame *
packetsToBuffer);
if (m_RingBuffer == nullptr) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
@@ -284,15 +284,8 @@ bool SoundIoAudioRenderer::prepareForPlayback(const OPUS_MULTISTREAM_CONFIGURATI
return true;
}
bool SoundIoAudioRenderer::submitAudio(short* audioBuffer, int audioSize)
void* SoundIoAudioRenderer::getAudioBuffer(int* size)
{
if (m_Errored) {
return false;
}
// Flush events to update with new device arrivals
soundio_flush_events(m_SoundIo);
// We must always write a full frame of audio. If we don't,
// the reader will get out of sync with the writer and our
// channels will get all mixed up. To ensure this is always
@@ -300,13 +293,36 @@ bool SoundIoAudioRenderer::submitAudio(short* audioBuffer, int audioSize)
// of our frame size.
int bytesFree = soundio_ring_buffer_free_count(m_RingBuffer);
int bytesPerFrame = m_OpusChannelCount * m_OutputStream->bytes_per_sample;
int bytesToWrite = qMin(audioSize, (bytesFree / bytesPerFrame) * bytesPerFrame);
memcpy(soundio_ring_buffer_write_ptr(m_RingBuffer), audioBuffer, bytesToWrite);
soundio_ring_buffer_advance_write_ptr(m_RingBuffer, bytesToWrite);
*size = qMin(*size, (bytesFree / bytesPerFrame) * bytesPerFrame);
return soundio_ring_buffer_write_ptr(m_RingBuffer);
}
bool SoundIoAudioRenderer::submitAudio(int bytesWritten)
{
if (m_Errored) {
return false;
}
if (bytesWritten == 0) {
// Nothing to do
return true;
}
// Flush events to update with new device arrivals
soundio_flush_events(m_SoundIo);
// Advance the write pointer
soundio_ring_buffer_advance_write_ptr(m_RingBuffer, bytesWritten);
return true;
}
int SoundIoAudioRenderer::getCapabilities()
{
// TODO: Tweak buffer sizes then re-enable arbitrary audio duration
return CAPABILITY_DIRECT_SUBMIT /* | CAPABILITY_SUPPORTS_ARBITRARY_AUDIO_DURATION */;
}
void SoundIoAudioRenderer::sioErrorCallback(SoundIoOutStream* stream, int err)
{
auto me = reinterpret_cast<SoundIoAudioRenderer*>(stream->userdata);
@@ -371,15 +387,19 @@ void SoundIoAudioRenderer::sioWriteCallback(SoundIoOutStream* stream, int frameC
(me->m_OpusChannelCount * stream->bytes_per_sample);
int bytesRead = 0;
// Clamp framesLeft to frameCountMax
framesLeft = qMin(framesLeft, frameCountMax);
// Ensure we always write at least a buffer, even if it's silence, to avoid
// busy looping when no audio data is available while libsoundio tries to keep
// us from starving the output device.
frameCountMin = qMax(frameCountMin, (int)(stream->sample_rate * k_MinSampleLengthSec));
frameCountMin = qMax(frameCountMin, (int)(stream->sample_rate * me->m_AudioPacketDuration));
// Clamp frameCountMax to at least 2 packets or 20 ms to stop our latency from growing if audio packets lag.
// This makes sure that we never increase our latency far beyond what the sink is consuming.
frameCountMax = qMin(frameCountMax, (int)(stream->sample_rate * qMax(me->m_AudioPacketDuration * 2, 0.020)));
frameCountMin = qMin(frameCountMin, frameCountMax);
// Clamp framesLeft to frameCountMax
framesLeft = qMin(framesLeft, frameCountMax);
// Track latency on queueing-based backends
if (me->m_SoundIo->current_backend != SoundIoBackendCoreAudio && me->m_SoundIo->current_backend != SoundIoBackendJack) {
soundio_outstream_get_latency(stream, &me->m_Latency);
@@ -409,9 +429,8 @@ void SoundIoAudioRenderer::sioWriteCallback(SoundIoOutStream* stream, int frameC
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.
// SoundIoChannelId - 1 happens to match Moonlight's channel layout
// after we've applied our fixups to m_EffectiveLayout for 5.1 and 7.1.
int readPtrChannel = me->m_EffectiveLayout.channels[ch] - 1;
if (frame >= framesLeft || readPtrChannel >= me->m_OpusChannelCount) {
@@ -13,7 +13,11 @@ public:
virtual bool prepareForPlayback(const OPUS_MULTISTREAM_CONFIGURATION* opusConfig);
virtual bool submitAudio(short* audioBuffer, int audioSize);
virtual void* getAudioBuffer(int* size);
virtual bool submitAudio(int bytesWritten);
virtual int getCapabilities();
private:
int scoreChannelLayout(const struct SoundIoChannelLayout* layout, const OPUS_MULTISTREAM_CONFIGURATION* opusConfig);
@@ -32,9 +36,7 @@ private:
struct SoundIoOutStream* m_OutputStream;
struct SoundIoRingBuffer* m_RingBuffer;
struct SoundIoChannelLayout m_EffectiveLayout;
double m_AudioPacketDuration;
double m_Latency;
bool m_Errored;
static const double k_RawSampleLengthSec;
static const double k_MinSampleLengthSec;
};
File diff suppressed because it is too large Load Diff
+107
View File
@@ -0,0 +1,107 @@
#include "input.h"
#include <Limelight.h>
#include <SDL.h>
#include "streaming/streamutils.h"
#include <QtMath>
// How long the fingers must be stationary to start a right click
#define LONG_PRESS_ACTIVATION_DELAY 650
// How far the finger can move before it cancels a right click
#define LONG_PRESS_ACTIVATION_DELTA 0.01f
// How long the double tap deadzone stays in effect between touch up and touch down
#define DOUBLE_TAP_DEAD_ZONE_DELAY 250
// How far the finger can move before it can override the double tap deadzone
#define DOUBLE_TAP_DEAD_ZONE_DELTA 0.025f
Uint32 SdlInputHandler::longPressTimerCallback(Uint32, void*)
{
// Raise the left click and start a right click
LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_LEFT);
LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, BUTTON_RIGHT);
return 0;
}
void SdlInputHandler::handleAbsoluteFingerEvent(SDL_TouchFingerEvent* event)
{
// 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.
// Ignore touch down events with more than one finger
if (event->type == SDL_FINGERDOWN && SDL_GetNumTouchFingers(event->touchId) > 1) {
return;
}
// Ignore touch move and touch up events from the non-primary finger
if (event->type != SDL_FINGERDOWN && event->fingerId != m_LastTouchDownEvent.fingerId) {
return;
}
SDL_Rect src, dst;
int windowWidth, windowHeight;
SDL_GetWindowSize(m_Window, &windowWidth, &windowHeight);
src.x = src.y = 0;
src.w = m_StreamWidth;
src.h = m_StreamHeight;
dst.x = dst.y = 0;
dst.w = windowWidth;
dst.h = windowHeight;
// Use the stream and window sizes to determine the video region
StreamUtils::scaleSourceToDestinationSurface(&src, &dst);
if (qSqrt(qPow(event->x - m_LastTouchDownEvent.x, 2) + qPow(event->y - m_LastTouchDownEvent.y, 2)) > LONG_PRESS_ACTIVATION_DELTA) {
// Moved too far since touch down. Cancel the long press timer.
SDL_RemoveTimer(m_LongPressTimer);
m_LongPressTimer = 0;
}
// Don't reposition for finger down events within the deadzone. This makes double-clicking easier.
if (event->type != SDL_FINGERDOWN ||
event->timestamp - m_LastTouchUpEvent.timestamp > DOUBLE_TAP_DEAD_ZONE_DELAY ||
qSqrt(qPow(event->x - m_LastTouchUpEvent.x, 2) + qPow(event->y - m_LastTouchUpEvent.y, 2)) > DOUBLE_TAP_DEAD_ZONE_DELTA) {
// Scale window-relative events to be video-relative and clamp to video region
short x = qMin(qMax((int)(event->x * windowWidth), dst.x), dst.x + dst.w);
short y = qMin(qMax((int)(event->y * windowHeight), dst.y), dst.y + dst.h);
// Update the cursor position relative to the video region
LiSendMousePositionEvent(x - dst.x, y - dst.y, dst.w, dst.h);
}
if (event->type == SDL_FINGERDOWN) {
m_LastTouchDownEvent = *event;
// Start/restart the long press timer
SDL_RemoveTimer(m_LongPressTimer);
m_LongPressTimer = SDL_AddTimer(LONG_PRESS_ACTIVATION_DELAY,
longPressTimerCallback,
nullptr);
// Left button down on finger down
LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, BUTTON_LEFT);
}
else if (event->type == SDL_FINGERUP) {
m_LastTouchUpEvent = *event;
// Cancel the long press timer
SDL_RemoveTimer(m_LongPressTimer);
m_LongPressTimer = 0;
// Left button up on finger up
LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_LEFT);
// Raise right button too in case we triggered a long press gesture
LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_RIGHT);
}
}
+595
View File
@@ -0,0 +1,595 @@
#include "streaming/session.h"
#include <Limelight.h>
#include <SDL.h>
#include "settings/mappingmanager.h"
#include <QtMath>
// How long the Start button must be pressed to toggle mouse emulation
#define MOUSE_EMULATION_LONG_PRESS_TIME 750
// How long between polling the gamepad to send virtual mouse input
#define MOUSE_EMULATION_POLLING_INTERVAL 50
// Determines how fast the mouse will move each interval
#define MOUSE_EMULATION_MOTION_MULTIPLIER 4
// Determines the maximum motion amount before allowing movement
#define MOUSE_EMULATION_DEADZONE 2
// Haptic capabilities (in addition to those from SDL_HapticQuery())
#define ML_HAPTIC_GC_RUMBLE (1U << 16)
#define ML_HAPTIC_SIMPLE_RUMBLE (1U << 17)
const int SdlInputHandler::k_ButtonMap[] = {
A_FLAG, B_FLAG, X_FLAG, Y_FLAG,
BACK_FLAG, SPECIAL_FLAG, PLAY_FLAG,
LS_CLK_FLAG, RS_CLK_FLAG,
LB_FLAG, RB_FLAG,
UP_FLAG, DOWN_FLAG, LEFT_FLAG, RIGHT_FLAG
};
GamepadState*
SdlInputHandler::findStateForGamepad(SDL_JoystickID id)
{
int i;
for (i = 0; i < MAX_GAMEPADS; i++) {
if (m_GamepadState[i].jsId == id) {
SDL_assert(!m_MultiController || m_GamepadState[i].index == i);
return &m_GamepadState[i];
}
}
// This should only happen with > 4 gamepads
SDL_assert(SDL_NumJoysticks() > 4);
return nullptr;
}
void SdlInputHandler::sendGamepadState(GamepadState* state)
{
SDL_assert(m_GamepadMask == 0x1 || m_MultiController);
LiSendMultiControllerEvent(state->index,
m_GamepadMask,
state->buttons,
state->lt,
state->rt,
state->lsX,
state->lsY,
state->rsX,
state->rsY);
}
Uint32 SdlInputHandler::mouseEmulationTimerCallback(Uint32 interval, void *param)
{
auto gamepad = reinterpret_cast<GamepadState*>(param);
short rawX;
short rawY;
// Determine which analog stick is currently receiving the strongest input
if ((uint32_t)qAbs(gamepad->lsX) + qAbs(gamepad->lsY) > (uint32_t)qAbs(gamepad->rsX) + qAbs(gamepad->rsY)) {
rawX = gamepad->lsX;
rawY = -gamepad->lsY;
}
else {
rawX = gamepad->rsX;
rawY = -gamepad->rsY;
}
float deltaX;
float deltaY;
// Produce a base vector for mouse movement with increased speed as we deviate further from center
deltaX = qPow(rawX / 32766.0f * MOUSE_EMULATION_MOTION_MULTIPLIER, 3);
deltaY = qPow(rawY / 32766.0f * MOUSE_EMULATION_MOTION_MULTIPLIER, 3);
// Enforce deadzones
deltaX = qAbs(deltaX) > MOUSE_EMULATION_DEADZONE ? deltaX - MOUSE_EMULATION_DEADZONE : 0;
deltaY = qAbs(deltaY) > MOUSE_EMULATION_DEADZONE ? deltaY - MOUSE_EMULATION_DEADZONE : 0;
if (deltaX != 0 || deltaY != 0) {
LiSendMouseMoveEvent((short)deltaX, (short)deltaY);
}
return interval;
}
void SdlInputHandler::handleControllerAxisEvent(SDL_ControllerAxisEvent* event)
{
SDL_JoystickID gameControllerId = event->which;
GamepadState* state = findStateForGamepad(gameControllerId);
if (state == NULL) {
return;
}
// Batch all pending axis motion events for this gamepad to save CPU time
SDL_Event nextEvent;
for (;;) {
switch (event->axis)
{
case SDL_CONTROLLER_AXIS_LEFTX:
state->lsX = event->value;
break;
case SDL_CONTROLLER_AXIS_LEFTY:
// Signed values have one more negative value than
// positive value, so inverting the sign on -32768
// could actually cause the value to overflow and
// wrap around to be negative again. Avoid that by
// capping the value at 32767.
state->lsY = -qMax(event->value, (short)-32767);
break;
case SDL_CONTROLLER_AXIS_RIGHTX:
state->rsX = event->value;
break;
case SDL_CONTROLLER_AXIS_RIGHTY:
state->rsY = -qMax(event->value, (short)-32767);
break;
case SDL_CONTROLLER_AXIS_TRIGGERLEFT:
state->lt = (unsigned char)(event->value * 255UL / 32767);
break;
case SDL_CONTROLLER_AXIS_TRIGGERRIGHT:
state->rt = (unsigned char)(event->value * 255UL / 32767);
break;
default:
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Unhandled controller axis: %d",
event->axis);
return;
}
// Check for another event to batch with
if (SDL_PeepEvents(&nextEvent, 1, SDL_PEEKEVENT, SDL_CONTROLLERAXISMOTION, SDL_CONTROLLERAXISMOTION) <= 0) {
break;
}
event = &nextEvent.caxis;
if (event->which != gameControllerId) {
// Stop batching if a different gamepad interrupts us
break;
}
// Remove the next event to batch
SDL_PeepEvents(&nextEvent, 1, SDL_GETEVENT, SDL_CONTROLLERAXISMOTION, SDL_CONTROLLERAXISMOTION);
}
// Only send the gamepad state to the host if it's not in mouse emulation mode
if (state->mouseEmulationTimer == 0) {
sendGamepadState(state);
}
}
void SdlInputHandler::handleControllerButtonEvent(SDL_ControllerButtonEvent* event)
{
GamepadState* state = findStateForGamepad(event->which);
if (state == NULL) {
return;
}
if (event->state == SDL_PRESSED) {
state->buttons |= k_ButtonMap[event->button];
if (event->button == SDL_CONTROLLER_BUTTON_START) {
state->lastStartDownTime = SDL_GetTicks();
}
else if (state->mouseEmulationTimer != 0) {
if (event->button == SDL_CONTROLLER_BUTTON_A) {
LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, BUTTON_LEFT);
}
else if (event->button == SDL_CONTROLLER_BUTTON_B) {
LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, BUTTON_RIGHT);
}
else if (event->button == SDL_CONTROLLER_BUTTON_X) {
LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, BUTTON_MIDDLE);
}
else if (event->button == SDL_CONTROLLER_BUTTON_LEFTSHOULDER) {
LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, BUTTON_X1);
}
else if (event->button == SDL_CONTROLLER_BUTTON_RIGHTSHOULDER) {
LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, BUTTON_X2);
}
else if (event->button == SDL_CONTROLLER_BUTTON_DPAD_UP) {
LiSendScrollEvent(1);
}
else if (event->button == SDL_CONTROLLER_BUTTON_DPAD_DOWN) {
LiSendScrollEvent(-1);
}
}
}
else {
state->buttons &= ~k_ButtonMap[event->button];
if (event->button == SDL_CONTROLLER_BUTTON_START) {
if (SDL_GetTicks() - state->lastStartDownTime > MOUSE_EMULATION_LONG_PRESS_TIME) {
if (state->mouseEmulationTimer != 0) {
SDL_RemoveTimer(state->mouseEmulationTimer);
state->mouseEmulationTimer = 0;
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Mouse emulation deactivated");
Session::get()->notifyMouseEmulationMode(false);
}
else if (m_GamepadMouse) {
// Send the start button up event to the host, since we won't do it below
sendGamepadState(state);
state->mouseEmulationTimer = SDL_AddTimer(MOUSE_EMULATION_POLLING_INTERVAL, SdlInputHandler::mouseEmulationTimerCallback, state);
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Mouse emulation active");
Session::get()->notifyMouseEmulationMode(true);
}
}
}
else if (state->mouseEmulationTimer != 0) {
if (event->button == SDL_CONTROLLER_BUTTON_A) {
LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_LEFT);
}
else if (event->button == SDL_CONTROLLER_BUTTON_B) {
LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_RIGHT);
}
else if (event->button == SDL_CONTROLLER_BUTTON_X) {
LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_MIDDLE);
}
else if (event->button == SDL_CONTROLLER_BUTTON_LEFTSHOULDER) {
LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_X1);
}
else if (event->button == SDL_CONTROLLER_BUTTON_RIGHTSHOULDER) {
LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_X2);
}
}
}
// Handle Start+Select+L1+R1 as a gamepad quit combo
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;
}
// Only send the gamepad state to the host if it's not in mouse emulation mode
if (state->mouseEmulationTimer == 0) {
sendGamepadState(state);
}
}
void SdlInputHandler::handleControllerDeviceEvent(SDL_ControllerDeviceEvent* event)
{
GamepadState* state;
if (event->type == SDL_CONTROLLERDEVICEADDED) {
int i;
const char* name;
SDL_GameController* controller;
const char* mapping;
char guidStr[33];
uint32_t hapticCaps;
controller = SDL_GameControllerOpen(event->which);
if (controller == NULL) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Failed to open gamepad: %s",
SDL_GetError());
return;
}
// We used to use SDL_GameControllerGetPlayerIndex() here but that
// can lead to strange issues due to bugs in Windows where an Xbox
// controller will join as player 2, even though no player 1 controller
// is connected at all. This pretty much screws any attempt to use
// the gamepad in single player games, so just assign them in order from 0.
i = 0;
for (; i < MAX_GAMEPADS; i++) {
SDL_assert(m_GamepadState[i].controller != controller);
if (m_GamepadState[i].controller == NULL) {
// Found an empty slot
break;
}
}
if (i == MAX_GAMEPADS) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"No open gamepad slots found!");
SDL_GameControllerClose(controller);
return;
}
state = &m_GamepadState[i];
if (m_MultiController) {
state->index = i;
#if SDL_VERSION_ATLEAST(2, 0, 12)
// This will change indicators on the controller to show the assigned
// player index. For Xbox 360 controllers, that means updating the LED
// ring to light up the corresponding quadrant for this player.
SDL_GameControllerSetPlayerIndex(controller, state->index);
#endif
}
else {
// Always player 1 in single controller mode
state->index = 0;
}
state->controller = controller;
state->jsId = SDL_JoystickInstanceID(SDL_GameControllerGetJoystick(state->controller));
#if SDL_VERSION_ATLEAST(2, 0, 9)
// Perform a tiny rumble to see if haptics are supported.
// NB: We cannot use zeros for rumble intensity or SDL will not actually call the JS driver
// and we'll get a (potentially false) success value returned.
hapticCaps = SDL_GameControllerRumble(controller, 1, 1, 1) == 0 ?
ML_HAPTIC_GC_RUMBLE : 0;
#else
state->haptic = SDL_HapticOpenFromJoystick(SDL_GameControllerGetJoystick(state->controller));
state->hapticEffectId = -1;
state->hapticMethod = GAMEPAD_HAPTIC_METHOD_NONE;
if (state->haptic != nullptr) {
// Query for supported haptic effects
hapticCaps = SDL_HapticQuery(state->haptic);
hapticCaps |= SDL_HapticRumbleSupported(state->haptic) ?
ML_HAPTIC_SIMPLE_RUMBLE : 0;
if ((SDL_HapticQuery(state->haptic) & SDL_HAPTIC_LEFTRIGHT) == 0) {
if (SDL_HapticRumbleSupported(state->haptic)) {
if (SDL_HapticRumbleInit(state->haptic) == 0) {
state->hapticMethod = GAMEPAD_HAPTIC_METHOD_SIMPLERUMBLE;
}
}
if (state->hapticMethod == GAMEPAD_HAPTIC_METHOD_NONE) {
SDL_HapticClose(state->haptic);
state->haptic = nullptr;
}
} else {
state->hapticMethod = GAMEPAD_HAPTIC_METHOD_LEFTRIGHT;
}
}
else {
hapticCaps = 0;
}
#endif
SDL_JoystickGetGUIDString(SDL_JoystickGetGUID(SDL_GameControllerGetJoystick(state->controller)),
guidStr, sizeof(guidStr));
mapping = SDL_GameControllerMapping(state->controller);
name = SDL_GameControllerName(state->controller);
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Gamepad %d (player %d) is: %s (haptic capabilities: 0x%x) (mapping: %s -> %s)",
i,
state->index,
name != nullptr ? name : "<null>",
hapticCaps,
guidStr,
mapping != nullptr ? mapping : "<null>");
if (mapping != nullptr) {
SDL_free((void*)mapping);
}
// Add this gamepad to the gamepad mask
if (m_MultiController) {
// NB: Don't assert that it's unset here because we will already
// have the mask set for initially attached gamepads to avoid confusing
// apps running on the host.
m_GamepadMask |= (1 << state->index);
}
else {
SDL_assert(m_GamepadMask == 0x1);
}
// Send an empty event to tell the PC we've arrived
sendGamepadState(state);
}
else if (event->type == SDL_CONTROLLERDEVICEREMOVED) {
state = findStateForGamepad(event->which);
if (state != NULL) {
if (state->mouseEmulationTimer != 0) {
Session::get()->notifyMouseEmulationMode(false);
SDL_RemoveTimer(state->mouseEmulationTimer);
}
SDL_GameControllerClose(state->controller);
#if !SDL_VERSION_ATLEAST(2, 0, 9)
if (state->haptic != nullptr) {
SDL_HapticClose(state->haptic);
}
#endif
// Remove this from the gamepad mask in MC-mode
if (m_MultiController) {
SDL_assert(m_GamepadMask & (1 << state->index));
m_GamepadMask &= ~(1 << state->index);
}
else {
SDL_assert(m_GamepadMask == 0x1);
}
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Gamepad %d is gone",
state->index);
// Send a final event to let the PC know this gamepad is gone
LiSendMultiControllerEvent(state->index, m_GamepadMask,
0, 0, 0, 0, 0, 0, 0);
// Clear all remaining state from this slot
SDL_memset(state, 0, sizeof(*state));
}
}
}
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::rumble(unsigned short controllerNumber, unsigned short lowFreqMotor, unsigned short highFreqMotor)
{
// Make sure the controller number is within our supported count
if (controllerNumber >= MAX_GAMEPADS) {
return;
}
#if SDL_VERSION_ATLEAST(2, 0, 9)
if (m_GamepadState[controllerNumber].controller != nullptr) {
SDL_GameControllerRumble(m_GamepadState[controllerNumber].controller, lowFreqMotor, highFreqMotor, 30000);
}
#else
// Check if the controller supports haptics (and if the controller exists at all)
SDL_Haptic* haptic = m_GamepadState[controllerNumber].haptic;
if (haptic == nullptr) {
return;
}
// Stop the last effect we played
if (m_GamepadState[controllerNumber].hapticMethod == GAMEPAD_HAPTIC_METHOD_LEFTRIGHT) {
if (m_GamepadState[controllerNumber].hapticEffectId >= 0) {
SDL_HapticDestroyEffect(haptic, m_GamepadState[controllerNumber].hapticEffectId);
}
} else if (m_GamepadState[controllerNumber].hapticMethod == GAMEPAD_HAPTIC_METHOD_SIMPLERUMBLE) {
SDL_HapticRumbleStop(haptic);
}
// If this callback is telling us to stop both motors, don't bother queuing a new effect
if (lowFreqMotor == 0 && highFreqMotor == 0) {
return;
}
if (m_GamepadState[controllerNumber].hapticMethod == GAMEPAD_HAPTIC_METHOD_LEFTRIGHT) {
SDL_HapticEffect effect;
SDL_memset(&effect, 0, sizeof(effect));
effect.type = SDL_HAPTIC_LEFTRIGHT;
// The effect should last until we are instructed to stop or change it
effect.leftright.length = SDL_HAPTIC_INFINITY;
// SDL haptics range from 0-32767 but XInput uses 0-65535, so divide by 2 to correct for SDL's scaling
effect.leftright.large_magnitude = lowFreqMotor / 2;
effect.leftright.small_magnitude = highFreqMotor / 2;
// Play the new effect
m_GamepadState[controllerNumber].hapticEffectId = SDL_HapticNewEffect(haptic, &effect);
if (m_GamepadState[controllerNumber].hapticEffectId >= 0) {
SDL_HapticRunEffect(haptic, m_GamepadState[controllerNumber].hapticEffectId, 1);
}
} else if (m_GamepadState[controllerNumber].hapticMethod == GAMEPAD_HAPTIC_METHOD_SIMPLERUMBLE) {
SDL_HapticRumblePlay(haptic,
std::min(1.0, (GAMEPAD_HAPTIC_SIMPLE_HIFREQ_MOTOR_WEIGHT*highFreqMotor +
GAMEPAD_HAPTIC_SIMPLE_LOWFREQ_MOTOR_WEIGHT*lowFreqMotor) / 65535.0),
SDL_HAPTIC_INFINITY);
}
#endif
}
QString SdlInputHandler::getUnmappedGamepads()
{
QString ret;
if (SDL_InitSubSystem(SDL_INIT_GAMECONTROLLER) != 0) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"SDL_InitSubSystem(SDL_INIT_GAMECONTROLLER) failed: %s",
SDL_GetError());
}
MappingManager mappingManager;
mappingManager.applyMappings();
for (int i = 0; i < SDL_NumJoysticks(); i++) {
if (!SDL_IsGameController(i)) {
char guidStr[33];
SDL_JoystickGetGUIDString(SDL_JoystickGetDeviceGUID(i),
guidStr, sizeof(guidStr));
const char* name = SDL_JoystickNameForIndex(i);
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Unmapped joystick: %s %s",
name ? name : "<UNKNOWN>",
guidStr);
SDL_Joystick* joy = SDL_JoystickOpen(i);
if (joy != nullptr) {
int numButtons = SDL_JoystickNumButtons(joy);
int numHats = SDL_JoystickNumHats(joy);
int numAxes = SDL_JoystickNumAxes(joy);
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Number of axes: %d | Number of buttons: %d | Number of hats: %d",
numAxes, numButtons, numHats);
if ((numAxes >= 4 && numAxes <= 8) && numButtons >= 8 && numHats <= 1) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"Joystick likely to be an unmapped game controller");
if (!ret.isEmpty()) {
ret += ", ";
}
ret += name;
}
SDL_JoystickClose(joy);
}
else {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"Unable to open joystick for query: %s",
SDL_GetError());
}
}
}
SDL_QuitSubSystem(SDL_INIT_GAMECONTROLLER);
return ret;
}
int SdlInputHandler::getAttachedGamepadMask()
{
int count;
int mask;
if (!m_MultiController) {
// Player 1 is always present in non-MC mode
return 0x1;
}
count = mask = 0;
for (int i = 0; i < SDL_NumJoysticks(); i++) {
if (SDL_IsGameController(i)) {
mask |= (1 << count++);
}
}
return mask;
}
+300
View File
@@ -0,0 +1,300 @@
#include <Limelight.h>
#include <SDL.h>
#include "streaming/session.h"
#include "settings/mappingmanager.h"
#include "path.h"
#include <QtGlobal>
#include <QDir>
#define MOUSE_POLLING_INTERVAL 5
SdlInputHandler::SdlInputHandler(StreamingPreferences& prefs, NvComputer*, int streamWidth, int streamHeight)
: m_MultiController(prefs.multiController),
m_GamepadMouse(prefs.gamepadMouse),
m_MouseMoveTimer(0),
m_FakeCaptureActive(false),
m_LongPressTimer(0),
m_StreamWidth(streamWidth),
m_StreamHeight(streamHeight),
m_AbsoluteMouseMode(prefs.absoluteMouseMode),
m_AbsoluteTouchMode(prefs.absoluteTouchMode),
m_PendingMouseLeaveButtonUp(0),
m_LeftButtonReleaseTimer(0),
m_RightButtonReleaseTimer(0),
m_DragTimer(0),
m_DragButton(0),
m_NumFingersDown(0)
{
// Allow gamepad input when the app doesn't have focus
SDL_SetHint(SDL_HINT_JOYSTICK_ALLOW_BACKGROUND_EVENTS, "1");
// If absolute mouse mode 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_SetHintWithPriority(SDL_HINT_MOUSE_RELATIVE_MODE_WARP,
prefs.absoluteMouseMode ? "1" : "0",
SDL_HINT_OVERRIDE);
// Allow clicks to pass through to us when focusing the window. If we're in
// absolute mouse mode, this will avoid the user having to click twice to
// trigger a click on the host if the Moonlight window is not focused. In
// relative mode, the click event will trigger the mouse to be recaptured.
SDL_SetHint(SDL_HINT_MOUSE_FOCUS_CLICKTHROUGH, "1");
#if defined(Q_OS_DARWIN) && !SDL_VERSION_ATLEAST(2, 0, 10)
// SDL 2.0.9 on macOS has a broken HIDAPI mapping for the older Xbox One S
// firmware, so we have to disable HIDAPI for Xbox gamepads on macOS until
// SDL 2.0.10 where the bug is fixed.
// https://github.com/moonlight-stream/moonlight-qt/issues/133
// https://bugzilla.libsdl.org/show_bug.cgi?id=4395
SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI_XBOX, "0");
#endif
#if SDL_VERSION_ATLEAST(2, 0, 9)
// Enabling extended input reports allows rumble to function on Bluetooth PS4
// controllers, but breaks DirectInput applications. We will enable it because
// it's likely that working rumble is what the user is expecting. If they don't
// want this behavior, they can override it with the environment variable.
SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI_PS4_RUMBLE, "1");
#endif
// We must initialize joystick explicitly before gamecontroller in order
// to ensure we receive gamecontroller attach events for gamepads where
// SDL doesn't have a built-in mapping. By starting joystick first, we
// can allow mapping manager to update the mappings before GC attach
// events are generated.
SDL_assert(!SDL_WasInit(SDL_INIT_JOYSTICK));
if (SDL_InitSubSystem(SDL_INIT_JOYSTICK) != 0) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"SDL_InitSubSystem(SDL_INIT_JOYSTICK) failed: %s",
SDL_GetError());
}
MappingManager mappingManager;
mappingManager.applyMappings();
// Flush gamepad arrival and departure events which may be queued before
// starting the gamecontroller subsystem again. This prevents us from
// receiving duplicate arrival and departure events for the same gamepad.
SDL_FlushEvent(SDL_CONTROLLERDEVICEADDED);
SDL_FlushEvent(SDL_CONTROLLERDEVICEREMOVED);
// We need to reinit this each time, since you only get
// an initial set of gamepad arrival events once per init.
SDL_assert(!SDL_WasInit(SDL_INIT_GAMECONTROLLER));
if (SDL_InitSubSystem(SDL_INIT_GAMECONTROLLER) != 0) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"SDL_InitSubSystem(SDL_INIT_GAMECONTROLLER) failed: %s",
SDL_GetError());
}
#if !SDL_VERSION_ATLEAST(2, 0, 9)
SDL_assert(!SDL_WasInit(SDL_INIT_HAPTIC));
if (SDL_InitSubSystem(SDL_INIT_HAPTIC) != 0) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"SDL_InitSubSystem(SDL_INIT_HAPTIC) failed: %s",
SDL_GetError());
}
#endif
// Initialize the gamepad mask with currently attached gamepads to avoid
// causing gamepads to unexpectedly disappear and reappear on the host
// during stream startup as we detect currently attached gamepads one at a time.
m_GamepadMask = getAttachedGamepadMask();
SDL_zero(m_GamepadState);
SDL_zero(m_LastTouchDownEvent);
SDL_zero(m_LastTouchUpEvent);
SDL_zero(m_TouchDownEvent);
SDL_AtomicSet(&m_MouseDeltaX, 0);
SDL_AtomicSet(&m_MouseDeltaY, 0);
Uint32 pollingInterval = QString(qgetenv("MOUSE_POLLING_INTERVAL")).toUInt();
if (pollingInterval == 0) {
pollingInterval = MOUSE_POLLING_INTERVAL;
}
else {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"Using custom mouse polling interval: %u ms",
pollingInterval);
}
m_MouseMoveTimer = SDL_AddTimer(pollingInterval, SdlInputHandler::mouseMoveTimerCallback, this);
}
SdlInputHandler::~SdlInputHandler()
{
for (int i = 0; i < MAX_GAMEPADS; i++) {
if (m_GamepadState[i].mouseEmulationTimer != 0) {
Session::get()->notifyMouseEmulationMode(false);
SDL_RemoveTimer(m_GamepadState[i].mouseEmulationTimer);
}
#if !SDL_VERSION_ATLEAST(2, 0, 9)
if (m_GamepadState[i].haptic != nullptr) {
SDL_HapticClose(m_GamepadState[i].haptic);
}
#endif
if (m_GamepadState[i].controller != nullptr) {
SDL_GameControllerClose(m_GamepadState[i].controller);
}
}
SDL_RemoveTimer(m_MouseMoveTimer);
SDL_RemoveTimer(m_LongPressTimer);
SDL_RemoveTimer(m_LeftButtonReleaseTimer);
SDL_RemoveTimer(m_RightButtonReleaseTimer);
SDL_RemoveTimer(m_DragTimer);
#if !SDL_VERSION_ATLEAST(2, 0, 9)
SDL_QuitSubSystem(SDL_INIT_HAPTIC);
SDL_assert(!SDL_WasInit(SDL_INIT_HAPTIC));
#endif
SDL_QuitSubSystem(SDL_INIT_GAMECONTROLLER);
SDL_assert(!SDL_WasInit(SDL_INIT_GAMECONTROLLER));
SDL_QuitSubSystem(SDL_INIT_JOYSTICK);
SDL_assert(!SDL_WasInit(SDL_INIT_JOYSTICK));
// Return background event handling to off
SDL_SetHint(SDL_HINT_JOYSTICK_ALLOW_BACKGROUND_EVENTS, "0");
#ifdef STEAM_LINK
// Hide SDL's cursor on Steam Link after quitting the stream.
// FIXME: We should also do this for other situations where SDL
// and Qt will draw their own mouse cursors like KMSDRM or RPi
// video backends.
SDL_ShowCursor(SDL_DISABLE);
#endif
}
void SdlInputHandler::setWindow(SDL_Window *window)
{
m_Window = window;
}
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();
}
void SdlInputHandler::notifyMouseLeave()
{
#ifdef Q_OS_WIN32
// SDL on Windows doesn't send the mouse button up until the mouse re-enters the window
// after leaving it. This breaks some of the Aero snap gestures, so we'll fake it here.
if (m_AbsoluteMouseMode && isCaptureActive()) {
// NB: Not using SDL_GetGlobalMouseState() because we want our state not the system's
Uint32 mouseState = SDL_GetMouseState(nullptr, nullptr);
for (Uint32 button = SDL_BUTTON_LEFT; button <= SDL_BUTTON_X2; button++) {
if (mouseState & SDL_BUTTON(button)) {
m_PendingMouseLeaveButtonUp = button;
break;
}
}
}
#endif
}
void SdlInputHandler::notifyFocusLost()
{
// 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) && !m_AbsoluteMouseMode) {
setCaptureActive(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.
raiseAllKeys();
}
bool SdlInputHandler::isCaptureActive()
{
if (SDL_GetRelativeMouseMode()) {
return true;
}
// Some platforms don't support SDL_SetRelativeMouseMode
return m_FakeCaptureActive;
}
void SdlInputHandler::setCaptureActive(bool active)
{
if (active) {
// If we're in full-screen exclusive mode, grab the cursor so it can't accidentally leave our window.
if ((SDL_GetWindowFlags(m_Window) & SDL_WINDOW_FULLSCREEN_DESKTOP) == SDL_WINDOW_FULLSCREEN) {
SDL_SetWindowGrab(m_Window, SDL_TRUE);
}
if (!m_AbsoluteMouseMode) {
// If our window is occluded when mouse is captured, the mouse may
// get stuck on top of the occluding window and not be properly
// captured. We can avoid this by raising our window before we
// capture the mouse.
SDL_RaiseWindow(m_Window);
}
// If we're in relative mode, try to activate SDL's relative mouse mode
if (m_AbsoluteMouseMode || SDL_SetRelativeMouseMode(SDL_TRUE) < 0) {
// Relative mouse mode didn't work or was disabled, so we'll just hide the cursor
SDL_ShowCursor(SDL_DISABLE);
m_FakeCaptureActive = true;
}
}
else {
if (m_FakeCaptureActive) {
// Display the cursor again
SDL_ShowCursor(SDL_ENABLE);
m_FakeCaptureActive = false;
}
else {
SDL_SetRelativeMouseMode(SDL_FALSE);
}
// Allow the cursor to leave the bounds of our window again.
SDL_SetWindowGrab(m_Window, SDL_FALSE);
}
}
void SdlInputHandler::handleTouchFingerEvent(SDL_TouchFingerEvent* event)
{
#if SDL_VERSION_ATLEAST(2, 0, 10)
if (SDL_GetTouchDeviceType(event->touchId) != SDL_TOUCH_DEVICE_DIRECT) {
// Ignore anything that isn't a touchscreen. We may get callbacks
// for trackpads, but we want to handle those in the mouse path.
return;
}
#elif defined(Q_OS_DARWIN)
// SDL2 sends touch events from trackpads by default on
// macOS. This totally screws our actual mouse handling,
// so we must explicitly ignore touch events on macOS
// until SDL 2.0.10 where we have SDL_GetTouchDeviceType()
// to tell them apart.
return;
#endif
if (m_AbsoluteTouchMode) {
handleAbsoluteFingerEvent(event);
}
else {
handleRelativeFingerEvent(event);
}
}
@@ -8,10 +8,16 @@
struct GamepadState {
SDL_GameController* controller;
SDL_JoystickID jsId;
short index;
#if !SDL_VERSION_ATLEAST(2, 0, 9)
SDL_Haptic* haptic;
int hapticMethod;
int hapticEffectId;
short index;
#endif
SDL_TimerID mouseEmulationTimer;
uint32_t lastStartDownTime;
short buttons;
short lsX, lsY;
@@ -37,6 +43,8 @@ public:
~SdlInputHandler();
void setWindow(SDL_Window* window);
void handleKeyEvent(SDL_KeyboardEvent* event);
void handleMouseButtonEvent(SDL_MouseButtonEvent* event);
@@ -61,6 +69,14 @@ public:
void raiseAllKeys();
void notifyMouseLeave();
void notifyFocusLost();
bool isCaptureActive();
void setCaptureActive(bool active);
static
QString getUnmappedGamepads();
@@ -70,6 +86,19 @@ private:
void sendGamepadState(GamepadState* state);
void handleAbsoluteFingerEvent(SDL_TouchFingerEvent* event);
void handleRelativeFingerEvent(SDL_TouchFingerEvent* event);
static
Uint32 longPressTimerCallback(Uint32 interval, void* param);
static
Uint32 mouseMoveTimerCallback(Uint32 interval, void* param);
static
Uint32 mouseEmulationTimerCallback(Uint32 interval, void* param);
static
Uint32 releaseLeftButtonTimerCallback(Uint32 interval, void* param);
@@ -79,26 +108,32 @@ private:
static
Uint32 dragTimerCallback(Uint32 interval, void* param);
static
Uint32 mouseMoveTimerCallback(Uint32 interval, void* param);
SDL_Window* m_Window;
bool m_MultiController;
bool m_GamepadMouse;
SDL_TimerID m_MouseMoveTimer;
SDL_atomic_t m_MouseDeltaX;
SDL_atomic_t m_MouseDeltaY;
int m_GamepadMask;
GamepadState m_GamepadState[MAX_GAMEPADS];
QSet<short> m_KeysDown;
bool m_FakeCaptureActive;
SDL_TouchFingerEvent m_LastTouchDownEvent;
SDL_TouchFingerEvent m_LastTouchUpEvent;
SDL_TimerID m_LongPressTimer;
int m_StreamWidth;
int m_StreamHeight;
bool m_AbsoluteMouseMode;
bool m_AbsoluteTouchMode;
Uint32 m_PendingMouseLeaveButtonUp;
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[];
};
+420
View File
@@ -0,0 +1,420 @@
#include "streaming/session.h"
#include <Limelight.h>
#include <SDL.h>
#include <QGuiApplication>
// Until we can fully capture these on all platforms (without conflicting with
// OS-provided shortcuts), we should avoid passing them through to the host.
//#define ENABLE_META
#define VK_0 0x30
#define VK_A 0x41
// These are real Windows VK_* codes
#ifndef VK_F1
#define VK_F1 0x70
#define VK_F13 0x7C
#define VK_NUMPAD0 0x60
#endif
void SdlInputHandler::handleKeyEvent(SDL_KeyboardEvent* event)
{
short keyCode;
char modifiers;
// Check for our special key combos
if ((event->state == SDL_PRESSED) &&
(event->keysym.mod & KMOD_CTRL) &&
(event->keysym.mod & KMOD_ALT) &&
(event->keysym.mod & KMOD_SHIFT)) {
// First we test the SDLK combos for matches,
// that way we ensure that latin keyboard users
// can match to the key they see on their keyboards.
// If nothing matches that, we'll then go on to
// checking scancodes so non-latin keyboard users
// can have working hotkeys (though possibly in
// odd positions). We must do all SDLK tests before
// any scancode tests to avoid issues in cases
// where the SDLK for one shortcut collides with
// the scancode of another.
// Check for quit combo (Ctrl+Alt+Shift+Q)
if (event->keysym.sym == SDLK_q) {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Detected quit key combo (SDLK)");
// Push a quit event to the main loop
SDL_Event event;
event.type = SDL_QUIT;
event.quit.timestamp = SDL_GetTicks();
SDL_PushEvent(&event);
return;
}
// Check for the unbind combo (Ctrl+Alt+Shift+Z) unless on EGLFS which has no window manager
else if (event->keysym.sym == SDLK_z && QGuiApplication::platformName() != "eglfs") {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Detected mouse capture toggle combo (SDLK)");
// Stop handling future input
setCaptureActive(!isCaptureActive());
// Force raise all keys to ensure they aren't stuck,
// since we won't get their key up events.
raiseAllKeys();
return;
}
// Check for the mouse mode combo (Ctrl+Alt+Shift+M) unless on EGLFS which has no window manager
else if (event->keysym.sym == SDLK_m && QGuiApplication::platformName() != "eglfs") {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Detected mouse mode toggle combo (SDLK)");
// Uncapture input
setCaptureActive(false);
// Toggle mouse mode
m_AbsoluteMouseMode = !m_AbsoluteMouseMode;
// Recapture input
setCaptureActive(true);
return;
}
else if (event->keysym.sym == SDLK_x && QGuiApplication::platformName() != "eglfs") {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Detected full-screen toggle combo (SDLK)");
Session::s_ActiveSession->toggleFullscreen();
// Force raise all keys just be safe across this full-screen/windowed
// transition just in case key events get lost.
raiseAllKeys();
return;
}
// Check for overlay combo (Ctrl+Alt+Shift+S)
else if (event->keysym.sym == SDLK_s) {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Detected stats toggle combo (SDLK)");
// Toggle the stats overlay
Session::get()->getOverlayManager().setOverlayState(Overlay::OverlayDebug,
!Session::get()->getOverlayManager().isOverlayEnabled(Overlay::OverlayDebug));
// Force raise all keys just be safe across this full-screen/windowed
// transition just in case key events get lost.
raiseAllKeys();
return;
}
// Check for quit combo (Ctrl+Alt+Shift+Q)
else if (event->keysym.scancode == SDL_SCANCODE_Q) {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Detected quit key combo (scancode)");
// Push a quit event to the main loop
SDL_Event event;
event.type = SDL_QUIT;
event.quit.timestamp = SDL_GetTicks();
SDL_PushEvent(&event);
return;
}
// Check for the unbind combo (Ctrl+Alt+Shift+Z)
else if (event->keysym.scancode == SDL_SCANCODE_Z && QGuiApplication::platformName() != "eglfs") {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Detected mouse capture toggle combo (scancode)");
// Stop handling future input
setCaptureActive(!isCaptureActive());
// Force raise all keys to ensure they aren't stuck,
// since we won't get their key up events.
raiseAllKeys();
return;
}
// Check for the full-screen combo (Ctrl+Alt+Shift+X) unless on EGLFS which has no window manager
else if (event->keysym.scancode == SDL_SCANCODE_X && QGuiApplication::platformName() != "eglfs") {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Detected full-screen toggle combo (scancode)");
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;
}
// Check for the mouse mode toggle combo (Ctrl+Alt+Shift+M) unless on EGLFS which has no window manager
else if (event->keysym.scancode == SDL_SCANCODE_M && QGuiApplication::platformName() != "eglfs") {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Detected mouse mode toggle combo (scancode)");
// Uncapture input
setCaptureActive(false);
// Toggle mouse mode
m_AbsoluteMouseMode = !m_AbsoluteMouseMode;
// Recapture input
setCaptureActive(true);
return;
}
else if (event->keysym.scancode == SDL_SCANCODE_S) {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Detected stats toggle combo (scancode)");
// Toggle the stats overlay
Session::get()->getOverlayManager().setOverlayState(Overlay::OverlayDebug,
!Session::get()->getOverlayManager().isOverlayEnabled(Overlay::OverlayDebug));
// Force raise all keys just be safe across this full-screen/windowed
// transition just in case key events get lost.
raiseAllKeys();
return;
}
}
if (event->repeat) {
// Ignore repeat key down events
SDL_assert(event->state == SDL_PRESSED);
return;
}
// Set modifier flags
modifiers = 0;
if (event->keysym.mod & KMOD_CTRL) {
modifiers |= MODIFIER_CTRL;
}
if (event->keysym.mod & KMOD_ALT) {
modifiers |= MODIFIER_ALT;
}
if (event->keysym.mod & KMOD_SHIFT) {
modifiers |= MODIFIER_SHIFT;
}
#ifdef ENABLE_META
if (event->keysym.mod & KMOD_GUI) {
modifiers |= MODIFIER_META;
}
#endif
// Set keycode. We explicitly use scancode here because GFE will try to correct
// for AZERTY layouts on the host but it depends on receiving VK_ values matching
// a QWERTY layout to work.
if (event->keysym.scancode >= SDL_SCANCODE_1 && event->keysym.scancode <= SDL_SCANCODE_9) {
// SDL defines SDL_SCANCODE_0 > SDL_SCANCODE_9, so we need to handle that manually
keyCode = (event->keysym.scancode - SDL_SCANCODE_1) + VK_0 + 1;
}
else if (event->keysym.scancode >= SDL_SCANCODE_A && event->keysym.scancode <= SDL_SCANCODE_Z) {
keyCode = (event->keysym.scancode - SDL_SCANCODE_A) + VK_A;
}
else if (event->keysym.scancode >= SDL_SCANCODE_F1 && event->keysym.scancode <= SDL_SCANCODE_F12) {
keyCode = (event->keysym.scancode - SDL_SCANCODE_F1) + VK_F1;
}
else if (event->keysym.scancode >= SDL_SCANCODE_F13 && event->keysym.scancode <= SDL_SCANCODE_F24) {
keyCode = (event->keysym.scancode - SDL_SCANCODE_F13) + VK_F13;
}
else if (event->keysym.scancode >= SDL_SCANCODE_KP_1 && event->keysym.scancode <= SDL_SCANCODE_KP_9) {
// SDL defines SDL_SCANCODE_KP_0 > SDL_SCANCODE_KP_9, so we need to handle that manually
keyCode = (event->keysym.scancode - SDL_SCANCODE_KP_1) + VK_NUMPAD0 + 1;
}
else {
switch (event->keysym.scancode) {
case SDL_SCANCODE_BACKSPACE:
keyCode = 0x08;
break;
case SDL_SCANCODE_TAB:
keyCode = 0x09;
break;
case SDL_SCANCODE_CLEAR:
keyCode = 0x0C;
break;
case SDL_SCANCODE_KP_ENTER: // FIXME: Is this correct?
case SDL_SCANCODE_RETURN:
keyCode = 0x0D;
break;
case SDL_SCANCODE_PAUSE:
keyCode = 0x13;
break;
case SDL_SCANCODE_CAPSLOCK:
keyCode = 0x14;
break;
case SDL_SCANCODE_ESCAPE:
keyCode = 0x1B;
break;
case SDL_SCANCODE_SPACE:
keyCode = 0x20;
break;
case SDL_SCANCODE_PAGEUP:
keyCode = 0x21;
break;
case SDL_SCANCODE_PAGEDOWN:
keyCode = 0x22;
break;
case SDL_SCANCODE_END:
keyCode = 0x23;
break;
case SDL_SCANCODE_HOME:
keyCode = 0x24;
break;
case SDL_SCANCODE_LEFT:
keyCode = 0x25;
break;
case SDL_SCANCODE_UP:
keyCode = 0x26;
break;
case SDL_SCANCODE_RIGHT:
keyCode = 0x27;
break;
case SDL_SCANCODE_DOWN:
keyCode = 0x28;
break;
case SDL_SCANCODE_SELECT:
keyCode = 0x29;
break;
case SDL_SCANCODE_EXECUTE:
keyCode = 0x2B;
break;
case SDL_SCANCODE_PRINTSCREEN:
keyCode = 0x2C;
break;
case SDL_SCANCODE_INSERT:
keyCode = 0x2D;
break;
case SDL_SCANCODE_DELETE:
keyCode = 0x2E;
break;
case SDL_SCANCODE_HELP:
keyCode = 0x2F;
break;
case SDL_SCANCODE_KP_0:
// See comment above about why we only handle SDL_SCANCODE_KP_0 here
keyCode = VK_NUMPAD0;
break;
case SDL_SCANCODE_0:
// See comment above about why we only handle SDL_SCANCODE_0 here
keyCode = VK_0;
break;
case SDL_SCANCODE_KP_MULTIPLY:
keyCode = 0x6A;
break;
case SDL_SCANCODE_KP_PLUS:
keyCode = 0x6B;
break;
case SDL_SCANCODE_KP_COMMA:
keyCode = 0x6C;
break;
case SDL_SCANCODE_KP_MINUS:
keyCode = 0x6D;
break;
case SDL_SCANCODE_KP_PERIOD:
keyCode = 0x6E;
break;
case SDL_SCANCODE_KP_DIVIDE:
keyCode = 0x6F;
break;
case SDL_SCANCODE_NUMLOCKCLEAR:
keyCode = 0x90;
break;
case SDL_SCANCODE_SCROLLLOCK:
keyCode = 0x91;
break;
case SDL_SCANCODE_LSHIFT:
keyCode = 0xA0;
break;
case SDL_SCANCODE_RSHIFT:
keyCode = 0xA1;
break;
case SDL_SCANCODE_LCTRL:
keyCode = 0xA2;
break;
case SDL_SCANCODE_RCTRL:
keyCode = 0xA3;
break;
case SDL_SCANCODE_LALT:
keyCode = 0xA4;
break;
case SDL_SCANCODE_RALT:
keyCode = 0xA5;
break;
#ifdef ENABLE_META
case SDL_SCANCODE_LGUI:
keyCode = 0x5B;
break;
case SDL_SCANCODE_RGUI:
keyCode = 0x5C;
break;
#endif
case SDL_SCANCODE_AC_BACK:
keyCode = 0xA6;
break;
case SDL_SCANCODE_AC_FORWARD:
keyCode = 0xA7;
break;
case SDL_SCANCODE_AC_REFRESH:
keyCode = 0xA8;
break;
case SDL_SCANCODE_AC_STOP:
keyCode = 0xA9;
break;
case SDL_SCANCODE_AC_SEARCH:
keyCode = 0xAA;
break;
case SDL_SCANCODE_AC_BOOKMARKS:
keyCode = 0xAB;
break;
case SDL_SCANCODE_AC_HOME:
keyCode = 0xAC;
break;
case SDL_SCANCODE_SEMICOLON:
keyCode = 0xBA;
break;
case SDL_SCANCODE_EQUALS:
keyCode = 0xBB;
break;
case SDL_SCANCODE_COMMA:
keyCode = 0xBC;
break;
case SDL_SCANCODE_MINUS:
keyCode = 0xBD;
break;
case SDL_SCANCODE_PERIOD:
keyCode = 0xBE;
break;
case SDL_SCANCODE_SLASH:
keyCode = 0xBF;
break;
case SDL_SCANCODE_GRAVE:
keyCode = 0xC0;
break;
case SDL_SCANCODE_LEFTBRACKET:
keyCode = 0xDB;
break;
case SDL_SCANCODE_BACKSLASH:
keyCode = 0xDC;
break;
case SDL_SCANCODE_RIGHTBRACKET:
keyCode = 0xDD;
break;
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",
event->keysym.scancode);
return;
}
}
// 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,
modifiers);
}
+154
View File
@@ -0,0 +1,154 @@
#include "input.h"
#include <Limelight.h>
#include <SDL.h>
#include "streaming/streamutils.h"
void SdlInputHandler::handleMouseButtonEvent(SDL_MouseButtonEvent* event)
{
int button;
if (event->which == SDL_TOUCH_MOUSEID) {
// Ignore synthetic mouse events
return;
}
else if (!isCaptureActive()) {
if (event->button == SDL_BUTTON_LEFT && event->state == SDL_RELEASED) {
// 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).
setCaptureActive(true);
}
// Not capturing
return;
}
switch (event->button)
{
case SDL_BUTTON_LEFT:
button = BUTTON_LEFT;
break;
case SDL_BUTTON_MIDDLE:
button = BUTTON_MIDDLE;
break;
case SDL_BUTTON_RIGHT:
button = BUTTON_RIGHT;
break;
case SDL_BUTTON_X1:
button = BUTTON_X1;
break;
case SDL_BUTTON_X2:
button = BUTTON_X2;
break;
default:
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Unhandled button event: %d",
event->button);
return;
}
LiSendMouseButtonEvent(event->state == SDL_PRESSED ?
BUTTON_ACTION_PRESS :
BUTTON_ACTION_RELEASE,
button);
}
void SdlInputHandler::handleMouseMotionEvent(SDL_MouseMotionEvent* event)
{
if (!isCaptureActive()) {
// Not capturing
return;
}
else if (event->which == SDL_TOUCH_MOUSEID) {
// Ignore synthetic mouse events
return;
}
if (m_AbsoluteMouseMode) {
SDL_Rect src, dst;
src.x = src.y = 0;
src.w = m_StreamWidth;
src.h = m_StreamHeight;
dst.x = dst.y = 0;
SDL_GetWindowSize(m_Window, &dst.w, &dst.h);
// Use the stream and window sizes to determine the video region
StreamUtils::scaleSourceToDestinationSurface(&src, &dst);
// Clamp motion to the video region
short x = qMin(qMax(event->x - dst.x, 0), dst.w);
short y = qMin(qMax(event->y - dst.y, 0), dst.h);
// Send the mouse position update
LiSendMousePositionEvent(x, y, dst.w, dst.h);
}
else {
// Batch until the next mouse polling window or we'll get awful
// input lag everything except GFE 3.14 and 3.15.
SDL_AtomicAdd(&m_MouseDeltaX, event->xrel);
SDL_AtomicAdd(&m_MouseDeltaY, event->yrel);
}
}
void SdlInputHandler::handleMouseWheelEvent(SDL_MouseWheelEvent* event)
{
if (!isCaptureActive()) {
// Not capturing
return;
}
else if (event->which == SDL_TOUCH_MOUSEID) {
// Ignore synthetic mouse events
return;
}
if (event->y != 0) {
LiSendScrollEvent((signed char)event->y);
}
}
Uint32 SdlInputHandler::mouseMoveTimerCallback(Uint32 interval, void *param)
{
auto me = reinterpret_cast<SdlInputHandler*>(param);
short deltaX = (short)SDL_AtomicSet(&me->m_MouseDeltaX, 0);
short deltaY = (short)SDL_AtomicSet(&me->m_MouseDeltaY, 0);
if (deltaX != 0 || deltaY != 0) {
LiSendMouseMoveEvent(deltaX, deltaY);
}
#ifdef Q_OS_WIN32
// See comment in SdlInputHandler::notifyMouseLeave()
if (me->m_AbsoluteMouseMode && me->m_PendingMouseLeaveButtonUp != 0 && me->isCaptureActive()) {
int mouseX, mouseY;
int windowX, windowY;
Uint32 mouseState = SDL_GetGlobalMouseState(&mouseX, &mouseY);
SDL_GetWindowPosition(me->m_Window, &windowX, &windowY);
// If the button is now up, send the synthetic mouse up event
if ((mouseState & SDL_BUTTON(me->m_PendingMouseLeaveButtonUp)) == 0) {
SDL_Event event;
event.button.type = SDL_MOUSEBUTTONUP;
event.button.timestamp = SDL_GetTicks();
event.button.windowID = SDL_GetWindowID(me->m_Window);
event.button.which = 0;
event.button.button = me->m_PendingMouseLeaveButtonUp;
event.button.state = SDL_RELEASED;
event.button.clicks = 1;
event.button.x = mouseX - windowX;
event.button.y = mouseY - windowY;
SDL_PushEvent(&event);
me->m_PendingMouseLeaveButtonUp = 0;
}
}
#endif
return interval;
}
+175
View File
@@ -0,0 +1,175 @@
#include "input.h"
#include <Limelight.h>
#include <SDL.h>
#include <QtMath>
// 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.01f
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::handleRelativeFingerEvent(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) {
// If it's outside the deadzone delta, cancel drags and taps
if (qSqrt(qPow(event->x - m_TouchDownEvent[fingerIndex].x, 2) +
qPow(event->y - m_TouchDownEvent[fingerIndex].y, 2)) > 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;
}
else if (event->type == SDL_FINGERUP) {
m_TouchDownEvent[fingerIndex] = {};
}
}
+283 -125
View File
@@ -1,6 +1,7 @@
#include "session.h"
#include "settings/streamingpreferences.h"
#include "streaming/streamutils.h"
#include "backend/richpresencemanager.h"
#include <Limelight.h>
#include <SDL.h>
@@ -29,6 +30,8 @@
#include <QSvgRenderer>
#include <QPainter>
#include <QImage>
#include <QGuiApplication>
#include <QCursor>
CONNECTION_LISTENER_CALLBACKS Session::k_ConnCallbacks = {
Session::clStageStarting,
@@ -36,22 +39,11 @@ CONNECTION_LISTENER_CALLBACKS Session::k_ConnCallbacks = {
Session::clStageFailed,
nullptr,
Session::clConnectionTerminated,
nullptr,
nullptr,
Session::clLogMessage,
Session::clRumble,
Session::clConnectionStatusUpdate
};
AUDIO_RENDERER_CALLBACKS Session::k_AudioCallbacks = {
Session::arInit,
nullptr,
nullptr,
Session::arCleanup,
Session::arDecodeAndPlaySample,
CAPABILITY_DIRECT_SUBMIT
};
Session* Session::s_ActiveSession;
QSemaphore Session::s_ActiveSessionSemaphore(1);
@@ -64,7 +56,7 @@ void Session::clStageStarting(int stage)
QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents);
}
void Session::clStageFailed(int stage, long errorCode)
void Session::clStageFailed(int stage, int errorCode)
{
// We know this is called on the same thread as LiStartConnection()
// which happens to be the main thread, so it's cool to interact
@@ -73,16 +65,26 @@ void Session::clStageFailed(int stage, long errorCode)
QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents);
}
void Session::clConnectionTerminated(long errorCode)
void Session::clConnectionTerminated(int errorCode)
{
// Display the termination dialog if this was not intended
if (errorCode != 0) {
switch (errorCode) {
case ML_ERROR_GRACEFUL_TERMINATION:
break;
case ML_ERROR_NO_VIDEO_TRAFFIC:
s_ActiveSession->m_UnexpectedTermination = true;
emit s_ActiveSession->displayLaunchError("No video received from host. Check the host PC's firewall and port forwarding rules.");
break;
default:
s_ActiveSession->m_UnexpectedTermination = true;
emit s_ActiveSession->displayLaunchError("Connection terminated");
break;
}
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Connection terminated: %ld",
"Connection terminated: %d",
errorCode);
// Push a quit event to the main loop
@@ -125,6 +127,11 @@ void Session::clConnectionStatusUpdate(int connectionStatus)
return;
}
if (s_ActiveSession->m_MouseEmulationRefCount > 0) {
// Don't display the overlay if mouse emulation is already using it
return;
}
switch (connectionStatus)
{
case CONN_STATUS_POOR:
@@ -143,19 +150,28 @@ void Session::clConnectionStatusUpdate(int connectionStatus)
}
}
#define CALL_INITIALIZE(dec) (dec)->initialize(vds, window, videoFormat, width, height, frameRate, enableVsync, enableFramePacing)
bool Session::chooseDecoder(StreamingPreferences::VideoDecoderSelection vds,
SDL_Window* window, int videoFormat, int width, int height,
int frameRate, bool enableVsync, bool enableFramePacing, bool testOnly, IVideoDecoder*& chosenDecoder)
{
DECODER_PARAMETERS params;
params.width = width;
params.height = height;
params.frameRate = frameRate;
params.videoFormat = videoFormat;
params.window = window;
params.enableVsync = enableVsync;
params.enableFramePacing = enableFramePacing;
params.vds = vds;
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"V-sync %s",
enableVsync ? "enabled" : "disabled");
#ifdef HAVE_SLVIDEO
chosenDecoder = new SLVideoDecoder(testOnly);
if (CALL_INITIALIZE(chosenDecoder)) {
if (chosenDecoder->initialize(&params)) {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"SLVideo video decoder chosen");
return true;
@@ -170,7 +186,7 @@ bool Session::chooseDecoder(StreamingPreferences::VideoDecoderSelection vds,
#ifdef HAVE_FFMPEG
chosenDecoder = new FFmpegVideoDecoder(testOnly);
if (CALL_INITIALIZE(chosenDecoder)) {
if (chosenDecoder->initialize(&params)) {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"FFmpeg-based video decoder chosen");
return true;
@@ -244,6 +260,25 @@ int Session::drSubmitDecodeUnit(PDECODE_UNIT du)
}
}
void Session::getDecoderInfo(SDL_Window* window,
bool& isHardwareAccelerated, bool& isFullScreenOnly, QSize& maxResolution)
{
IVideoDecoder* decoder;
if (!chooseDecoder(StreamingPreferences::VDS_AUTO,
window, VIDEO_FORMAT_H264, 1920, 1080, 60,
true, false, true, decoder)) {
isHardwareAccelerated = isFullScreenOnly = false;
return;
}
isHardwareAccelerated = decoder->isHardwareAccelerated();
isFullScreenOnly = decoder->isAlwaysFullScreen();
maxResolution = decoder->getDecoderMaxResolution();
delete decoder;
}
bool Session::isHardwareDecodeAvailable(SDL_Window* window,
StreamingPreferences::VideoDecoderSelection vds,
int videoFormat, int width, int height, int frameRate)
@@ -261,21 +296,28 @@ bool Session::isHardwareDecodeAvailable(SDL_Window* window,
return ret;
}
int Session::getDecoderCapabilities(SDL_Window* window,
StreamingPreferences::VideoDecoderSelection vds,
int videoFormat, int width, int height, int frameRate)
bool Session::populateDecoderProperties(SDL_Window* window)
{
IVideoDecoder* decoder;
if (!chooseDecoder(vds, window, videoFormat, width, height, frameRate, true, false, true, decoder)) {
if (!chooseDecoder(m_Preferences->videoDecoderSelection,
window,
m_StreamConfig.enableHdr ? VIDEO_FORMAT_H265_MAIN10 :
(m_StreamConfig.supportsHevc ? VIDEO_FORMAT_H265 : VIDEO_FORMAT_H264),
m_StreamConfig.width,
m_StreamConfig.height,
m_StreamConfig.fps,
true, false, true, decoder)) {
return false;
}
int caps = decoder->getDecoderCapabilities();
m_VideoCallbacks.capabilities = decoder->getDecoderCapabilities();
m_StreamConfig.colorSpace = decoder->getDecoderColorspace();
delete decoder;
return caps;
return true;
}
Session::Session(NvComputer* computer, NvApp& app, StreamingPreferences *preferences)
@@ -293,6 +335,7 @@ Session::Session(NvComputer* computer, NvApp& app, StreamingPreferences *prefere
m_UnexpectedTermination(true), // Failure prior to streaming is unexpected
m_InputHandler(nullptr),
m_InputHandlerLock(0),
m_MouseEmulationRefCount(0),
m_OpusDecoder(nullptr),
m_AudioRenderer(nullptr),
m_AudioSampleCount(0),
@@ -312,12 +355,20 @@ Session::~Session()
bool Session::initialize()
{
if (SDL_InitSubSystem(SDL_INIT_VIDEO) != 0) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"SDL_InitSubSystem(SDL_INIT_VIDEO) failed: %s",
SDL_GetError());
return false;
}
// Create a hidden window to use for decoder initialization tests
SDL_Window* testWindow = SDL_CreateWindow("", 0, 0, 1280, 720, SDL_WINDOW_HIDDEN);
if (!testWindow) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Failed to create window for hardware decode test: %s",
SDL_GetError());
SDL_QuitSubSystem(SDL_INIT_VIDEO);
return false;
}
@@ -328,21 +379,12 @@ bool Session::initialize()
m_VideoCallbacks.setup = drSetup;
m_VideoCallbacks.submitDecodeUnit = drSubmitDecodeUnit;
// Slice up to 4 times for parallel decode, once slice per core
int slices = qMin(MAX_SLICES, SDL_GetCPUCount());
m_VideoCallbacks.capabilities |= CAPABILITY_SLICES_PER_FRAME(slices);
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Encoder configured for %d slices per frame",
slices);
LiInitializeStreamConfiguration(&m_StreamConfig);
m_StreamConfig.width = m_Preferences->width;
m_StreamConfig.height = m_Preferences->height;
m_StreamConfig.fps = m_Preferences->fps;
m_StreamConfig.bitrate = m_Preferences->bitrateKbps;
m_StreamConfig.hevcBitratePercentageMultiplier = 75;
m_StreamConfig.streamingRemotely = STREAM_CFG_AUTO;
m_StreamConfig.packetSize = 1392;
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Video bitrate: %d kbps",
@@ -362,11 +404,23 @@ bool Session::initialize()
case StreamingPreferences::AC_51_SURROUND:
m_StreamConfig.audioConfiguration = AUDIO_CONFIGURATION_51_SURROUND;
break;
case StreamingPreferences::AC_71_SURROUND:
m_StreamConfig.audioConfiguration = AUDIO_CONFIGURATION_71_SURROUND;
break;
}
LiInitializeAudioCallbacks(&m_AudioCallbacks);
m_AudioCallbacks.init = arInit;
m_AudioCallbacks.cleanup = arCleanup;
m_AudioCallbacks.decodeAndPlaySample = arDecodeAndPlaySample;
m_AudioCallbacks.capabilities = getAudioRendererCapabilities(m_StreamConfig.audioConfiguration);
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Audio configuration: %d",
m_StreamConfig.audioConfiguration);
"Audio channel count: %d",
CHANNEL_COUNT_FROM_AUDIO_CONFIGURATION(m_StreamConfig.audioConfiguration));
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Audio channel mask: %X",
CHANNEL_MASK_FROM_AUDIO_CONFIGURATION(m_StreamConfig.audioConfiguration));
switch (m_Preferences->videoCodecConfig)
{
@@ -411,33 +465,46 @@ bool Session::initialize()
break;
}
// Add the capability flags from the chosen decoder/renderer
// Requires m_StreamConfig.supportsHevc to be initialized
m_VideoCallbacks.capabilities |= getDecoderCapabilities(testWindow,
m_Preferences->videoDecoderSelection,
m_StreamConfig.supportsHevc ? VIDEO_FORMAT_H265 : VIDEO_FORMAT_H264,
m_StreamConfig.width,
m_StreamConfig.height,
m_StreamConfig.fps);
switch (m_Preferences->windowMode)
{
default:
case StreamingPreferences::WM_FULLSCREEN_DESKTOP:
m_FullScreenFlag = SDL_WINDOW_FULLSCREEN_DESKTOP;
break;
case StreamingPreferences::WM_FULLSCREEN:
default:
m_FullScreenFlag = SDL_WINDOW_FULLSCREEN;
break;
}
#if !SDL_VERSION_ATLEAST(2, 0, 11)
// HACK: Using a full-screen window breaks mouse capture on the Pi's LXDE
// GUI environment. Force the session to use windowed mode (which won't
// really matter anyway because the MMAL renderer always draws full-screen).
if (qgetenv("DESKTOP_SESSION") == "LXDE-pi") {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Forcing windowed mode on LXDE-Pi");
m_FullScreenFlag = 0;
}
#endif
// Check for validation errors/warnings and emit
// signals for them, if appropriate
bool ret = validateLaunch(testWindow);
if (ret) {
// Populate decoder-dependent properties.
// Must be done after validateLaunch() since m_StreamConfig is finalized.
ret = populateDecoderProperties(testWindow);
}
SDL_DestroyWindow(testWindow);
return ret;
if (!ret) {
SDL_QuitSubSystem(SDL_INIT_VIDEO);
return false;
}
return true;
}
void Session::emitLaunchWarning(QString text)
@@ -460,8 +527,18 @@ bool Session::validateLaunch(SDL_Window* testWindow)
{
QStringList warningList;
if (m_Preferences->absoluteMouseMode && !m_App.isAppCollectorGame) {
emitLaunchWarning("Your selection to enable remote desktop mouse mode may cause problems in games.");
}
if (m_Preferences->videoDecoderSelection == StreamingPreferences::VDS_FORCE_SOFTWARE) {
emitLaunchWarning("Your settings selection to force software decoding may cause poor streaming performance.");
if (m_Preferences->videoCodecConfig == StreamingPreferences::VCC_FORCE_HEVC_HDR) {
emitLaunchWarning("HDR is not supported with software decoding.");
m_StreamConfig.enableHdr = false;
}
else {
emitLaunchWarning("Your settings selection to force software decoding may cause poor streaming performance.");
}
}
if (m_Preferences->unsupportedFps && m_StreamConfig.fps > 60) {
@@ -476,13 +553,14 @@ bool Session::validateLaunch(SDL_Window* testWindow)
bool hevcForced = m_Preferences->videoCodecConfig == StreamingPreferences::VCC_FORCE_HEVC ||
m_Preferences->videoCodecConfig == StreamingPreferences::VCC_FORCE_HEVC_HDR;
if (!isHardwareDecodeAvailable(testWindow,
m_Preferences->videoDecoderSelection,
VIDEO_FORMAT_H265,
m_StreamConfig.width,
m_StreamConfig.height,
m_StreamConfig.fps) &&
m_Preferences->videoDecoderSelection == StreamingPreferences::VDS_AUTO) {
if (m_Preferences->videoDecoderSelection == StreamingPreferences::VDS_AUTO && // Force hardware decoding checked below
m_Preferences->videoCodecConfig != StreamingPreferences::VCC_AUTO && // Already checked in initialize()
!isHardwareDecodeAvailable(testWindow,
m_Preferences->videoDecoderSelection,
VIDEO_FORMAT_H265,
m_StreamConfig.width,
m_StreamConfig.height,
m_StreamConfig.fps)) {
if (hevcForced) {
emitLaunchWarning("Using software decoding due to your selection to force HEVC without GPU support. This may cause poor streaming performance.");
}
@@ -547,12 +625,12 @@ bool Session::validateLaunch(SDL_Window* testWindow)
// 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) {
// Gracefully degrade to stereo if surround sound doesn't work
if (!audioTestPassed && CHANNEL_COUNT_FROM_AUDIO_CONFIGURATION(m_StreamConfig.audioConfiguration) > 2) {
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.");
emitLaunchWarning("Your selected surround sound setting is not supported by the current audio device.");
}
}
@@ -567,7 +645,20 @@ bool Session::validateLaunch(SDL_Window* testWindow)
emitLaunchWarning("An attached gamepad has no mapping and won't be usable. Visit the Moonlight help to resolve this.");
}
// NVENC will fail to initialize when using dimensions over 4096 and H.264.
if (m_StreamConfig.width > 4096 || m_StreamConfig.height > 4096) {
if (m_Computer->maxLumaPixelsHEVC == 0) {
emit displayLaunchError("Your host PC's GPU doesn't support streaming video resolutions over 4K.");
return false;
}
else if (!m_StreamConfig.supportsHevc) {
emit displayLaunchError("Video resolutions over 4K are only supported by the HEVC codec.");
return false;
}
}
if (m_Preferences->videoDecoderSelection == StreamingPreferences::VDS_FORCE_HARDWARE &&
!m_StreamConfig.enableHdr && // HEVC Main10 was already checked for hardware decode support above
!isHardwareDecodeAvailable(testWindow,
m_Preferences->videoDecoderSelection,
m_StreamConfig.supportsHevc ? VIDEO_FORMAT_H265 : VIDEO_FORMAT_H264,
@@ -786,10 +877,18 @@ void Session::toggleFullscreen()
bool fullScreen = !(SDL_GetWindowFlags(m_Window) & m_FullScreenFlag);
if (fullScreen) {
if (m_FullScreenFlag == SDL_WINDOW_FULLSCREEN && m_InputHandler->isCaptureActive()) {
// Confine the cursor to the window if we're capturing input
SDL_SetWindowGrab(m_Window, SDL_TRUE);
}
SDL_SetWindowResizable(m_Window, SDL_FALSE);
SDL_SetWindowFullscreen(m_Window, m_FullScreenFlag);
}
else {
// Unconfine the cursor
SDL_SetWindowGrab(m_Window, SDL_FALSE);
SDL_SetWindowFullscreen(m_Window, 0);
SDL_SetWindowResizable(m_Window, SDL_TRUE);
@@ -798,6 +897,22 @@ void Session::toggleFullscreen()
}
}
void Session::notifyMouseEmulationMode(bool enabled)
{
m_MouseEmulationRefCount += enabled ? 1 : -1;
SDL_assert(m_MouseEmulationRefCount >= 0);
// We re-use the status update overlay for mouse mode notification
if (m_MouseEmulationRefCount > 0) {
strcpy(m_OverlayManager.getOverlayText(Overlay::OverlayStatusUpdate), "Gamepad mouse mode active\nLong press Start to deactivate");
m_OverlayManager.setOverlayTextUpdated(Overlay::OverlayStatusUpdate);
m_OverlayManager.setOverlayState(Overlay::OverlayStatusUpdate, true);
}
else {
m_OverlayManager.setOverlayState(Overlay::OverlayStatusUpdate, false);
}
}
void Session::exec(int displayOriginX, int displayOriginY)
{
m_DisplayOriginX = displayOriginX;
@@ -852,6 +967,18 @@ void Session::exec(int displayOriginX, int displayOriginY)
}
}
if (prefs.fps > 60) {
// Using SOPS with FPS values over 60 causes GFE to fall back
// to 720p60. On previous GFE versions, we could avoid this by
// forcing the FPS value to 60 when launching the stream, but
// now on GFE 3.20.3 that seems to trigger some sort of
// frame rate limiter that locks the game to 60 FPS.
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Disabling SOPS for %d FPS stream",
prefs.fps);
enableGameOptimizations = false;
}
try {
NvHTTP http(m_Computer->activeAddress, m_Computer->serverCert);
if (m_Computer->currentGameId != 0) {
@@ -866,12 +993,14 @@ void Session::exec(int displayOriginX, int displayOriginY)
} catch (const GfeHttpResponseException& e) {
delete m_InputHandler;
m_InputHandler = nullptr;
SDL_QuitSubSystem(SDL_INIT_VIDEO);
emit displayLaunchError("GeForce Experience returned error: " + e.toQString());
QThreadPool::globalInstance()->start(new DeferredSessionCleanupTask(this));
return;
} catch (const QtNetworkReplyException& e) {
delete m_InputHandler;
m_InputHandler = nullptr;
SDL_QuitSubSystem(SDL_INIT_VIDEO);
emit displayLaunchError(e.toQString());
QThreadPool::globalInstance()->start(new DeferredSessionCleanupTask(this));
return;
@@ -893,15 +1022,40 @@ void Session::exec(int displayOriginX, int displayOriginY)
hostInfo.serverInfoGfeVersion = siGfeVersion.data();
}
if (m_Preferences->packetSize != 0) {
// Override default packet size and remote streaming detection
// NB: Using STREAM_CFG_AUTO will cap our packet size at 1024 for remote hosts.
m_StreamConfig.streamingRemotely = STREAM_CFG_LOCAL;
m_StreamConfig.packetSize = m_Preferences->packetSize;
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"Using custom packet size: %d bytes",
m_Preferences->packetSize);
}
else {
// isReachableOverVpn() does network I/O, so we only attempt to check
// VPN reachability if we've already contacted the PC successfully
if (m_Computer->isReachableOverVpn()) {
// It looks like our route to this PC is over a VPN.
// Treat it as remote even if the target address is in RFC 1918 address space.
m_StreamConfig.streamingRemotely = STREAM_CFG_REMOTE;
m_StreamConfig.packetSize = 1024;
}
else {
m_StreamConfig.streamingRemotely = STREAM_CFG_AUTO;
m_StreamConfig.packetSize = 1392;
}
}
int err = LiStartConnection(&hostInfo, &m_StreamConfig, &k_ConnCallbacks,
&m_VideoCallbacks,
m_AudioDisabled ? nullptr : &k_AudioCallbacks,
m_AudioDisabled ? nullptr : &m_AudioCallbacks,
NULL, 0, NULL, 0);
if (err != 0) {
// We already displayed an error dialog in the stage failure
// listener.
delete m_InputHandler;
m_InputHandler = nullptr;
SDL_QuitSubSystem(SDL_INIT_VIDEO);
QThreadPool::globalInstance()->start(new DeferredSessionCleanupTask(this));
return;
}
@@ -913,6 +1067,13 @@ void Session::exec(int displayOriginX, int displayOriginY)
int x, y, width, height;
getWindowDimensions(x, y, width, height);
#ifdef STEAM_LINK
// We need a little delay before creating the window or we will trigger some kind
// of graphics driver bug on Steam Link that causes a jagged overlay to appear in
// the top right corner randomly.
SDL_Delay(500);
#endif
m_Window = SDL_CreateWindow("Moonlight",
x,
y,
@@ -925,10 +1086,13 @@ void Session::exec(int displayOriginX, int displayOriginY)
SDL_GetError());
delete m_InputHandler;
m_InputHandler = nullptr;
SDL_QuitSubSystem(SDL_INIT_VIDEO);
QThreadPool::globalInstance()->start(new DeferredSessionCleanupTask(this));
return;
}
m_InputHandler->setWindow(m_Window);
QSvgRenderer svgIconRenderer(QString(":/res/moonlight.svg"));
QImage svgImage(ICON_SIZE, ICON_SIZE, QImage::Format_RGBA8888);
svgImage.fill(0);
@@ -973,14 +1137,21 @@ void Session::exec(int displayOriginX, int displayOriginY)
SDL_SetWindowFullscreen(m_Window, m_FullScreenFlag);
}
#ifndef QT_DEBUG
// Capture the mouse by default on release builds only.
// This prevents the mouse from becoming trapped inside
// Moonlight when it's halted at a debug break.
if (m_Preferences->windowMode != StreamingPreferences::WM_WINDOWED) {
SDL_SetRelativeMouseMode(SDL_TRUE);
bool needsFirstEnterCapture = false;
bool needsPostDecoderCreationCapture = false;
// HACK: For Wayland, we wait until we get the first SDL_WINDOWEVENT_ENTER
// event where it seems to work consistently on GNOME. For other platforms,
// especially where SDL may call SDL_RecreateWindow(), we must only capture
// after the decoder is created.
if (strcmp(SDL_GetCurrentVideoDriver(), "wayland") == 0) {
// Native Wayland: Capture on SDL_WINDOWEVENT_ENTER
needsFirstEnterCapture = true;
}
else {
// X11/XWayland: Capture after decoder creation
needsPostDecoderCreationCapture = true;
}
#endif
// Stop text input. SDL enables it by default
// when we initialize the video subsystem, but this
@@ -991,6 +1162,11 @@ void Session::exec(int displayOriginX, int displayOriginY)
// Disable the screen saver
SDL_DisableScreenSaver();
// Hide Qt's fake mouse cursor on EGLFS systems
if (QGuiApplication::platformName() == "eglfs") {
QGuiApplication::setOverrideCursor(QCursor(Qt::BlankCursor));
}
// Set timer resolution to 1 ms on Windows for greater
// sleep precision and more accurate callback timing.
SDL_SetHint(SDL_HINT_TIMER_RESOLUTION, "1");
@@ -1002,6 +1178,9 @@ void Session::exec(int displayOriginX, int displayOriginY)
// (m_UnexpectedTermination is set back to true).
m_UnexpectedTermination = false;
// Start rich presence to indicate we're in game
RichPresenceManager presence(prefs, m_App.name);
// Hijack this thread to be the SDL main thread. We have to do this
// because we want to suspend all Qt processing until the stream is over.
SDL_Event event;
@@ -1011,7 +1190,14 @@ void Session::exec(int displayOriginX, int displayOriginY)
// blocks this thread too long for high polling rate mice and high
// refresh rate displays.
if (!SDL_PollEvent(&event)) {
#ifndef STEAM_LINK
SDL_Delay(1);
#else
// Waking every 1 ms to process input is too much for the low performance
// ARM core in the Steam Link, so we will wait 10 ms instead.
SDL_Delay(10);
#endif
presence.runCallbacks();
continue;
}
switch (event.type) {
@@ -1019,65 +1205,24 @@ void Session::exec(int displayOriginX, int displayOriginY)
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Quit event received");
goto DispatchDeferredCleanup;
case SDL_USEREVENT: {
SDL_Event nextEvent;
case SDL_USEREVENT:
SDL_assert(event.user.code == SDL_CODE_FRAME_READY);
// Drop any earlier frames
while (SDL_PeepEvents(&nextEvent,
1,
SDL_GETEVENT,
SDL_USEREVENT,
SDL_USEREVENT) == 1) {
m_VideoDecoder->dropFrame(&event.user);
event = nextEvent;
}
// Render the last frame
m_VideoDecoder->renderFrame(&event.user);
m_VideoDecoder->renderFrameOnMainThread();
break;
}
case SDL_WINDOWEVENT:
// Capture mouse cursor when user actives the window by clicking on
// 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);
}
m_InputHandler->notifyFocusLost();
}
else if (event.window.event == SDL_WINDOWEVENT_LEAVE) {
m_InputHandler->notifyMouseLeave();
}
// 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.
m_InputHandler->raiseAllKeys();
// Capture the mouse on SDL_WINDOWEVENT_ENTER if needed
if (needsFirstEnterCapture && event.window.event == SDL_WINDOWEVENT_ENTER) {
m_InputHandler->setCaptureActive(true);
needsFirstEnterCapture = false;
}
// We want to recreate the decoder for resizes (full-screen toggles) and the initial shown event.
@@ -1106,6 +1251,12 @@ void Session::exec(int displayOriginX, int displayOriginY)
SDL_SetWindowPosition(m_Window, x, y);
}
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Recreating renderer for window event: %d (%d %d)",
event.window.event,
event.window.data1,
event.window.data2);
// Fall through
case SDL_RENDER_DEVICE_RESET:
case SDL_RENDER_TARGETS_RESET:
@@ -1158,6 +1309,14 @@ void Session::exec(int displayOriginX, int displayOriginY)
emit displayLaunchError("Unable to initialize video decoder. Please check your streaming settings and try again.");
goto DispatchDeferredCleanup;
}
// As of SDL 2.0.12, SDL_RecreateWindow() doesn't carry over mouse capture
// or mouse hiding state to the new window. By capturing after the decoder
// is set up, this ensures the window re-creation is already done.
if (needsPostDecoderCreationCapture) {
m_InputHandler->setCaptureActive(true);
needsFirstEnterCapture = false;
}
}
// Request an IDR frame to complete the reset
@@ -1194,26 +1353,23 @@ void Session::exec(int displayOriginX, int displayOriginY)
case SDL_JOYDEVICEADDED:
m_InputHandler->handleJoystickArrivalEvent(&event.jdevice);
break;
// SDL2 sends touch events from trackpads by default on
// macOS. This totally screws our actual mouse handling,
// so we must explicitly ignore touch events on macOS.
#ifndef Q_OS_DARWIN
case SDL_FINGERDOWN:
case SDL_FINGERMOTION:
case SDL_FINGERUP:
m_InputHandler->handleTouchFingerEvent(&event.tfinger);
break;
#endif
}
}
DispatchDeferredCleanup:
// Uncapture the mouse and hide the window immediately,
// so we can return to the Qt GUI ASAP.
SDL_SetRelativeMouseMode(SDL_FALSE);
m_InputHandler->setCaptureActive(false);
SDL_EnableScreenSaver();
SDL_SetHint(SDL_HINT_TIMER_RESOLUTION, "0");
if (QGuiApplication::platformName() == "eglfs") {
QGuiApplication::restoreOverrideCursor();
}
// Raise any keys that are still down
m_InputHandler->raiseAllKeys();
@@ -1241,6 +1397,8 @@ DispatchDeferredCleanup:
SDL_FreeSurface(iconSurface);
}
SDL_QuitSubSystem(SDL_INIT_VIDEO);
// Cleanup can take a while, so dispatch it to a worker thread.
// When it is complete, it will release our s_ActiveSessionSemaphore
// reference.
+19 -16
View File
@@ -5,7 +5,7 @@
#include <Limelight.h>
#include <opus_multistream.h>
#include "settings/streamingpreferences.h"
#include "input.h"
#include "input/input.h"
#include "video/decoder.h"
#include "audio/renderers/renderer.h"
#include "video/overlaymanager.h"
@@ -25,9 +25,8 @@ public:
Q_INVOKABLE void exec(int displayOriginX, int displayOriginY);
static
bool isHardwareDecodeAvailable(SDL_Window* window,
StreamingPreferences::VideoDecoderSelection vds,
int videoFormat, int width, int height, int frameRate);
void getDecoderInfo(SDL_Window* window,
bool& isHardwareAccelerated, bool& isFullScreenOnly, QSize& maxResolution);
static Session* get()
{
@@ -42,7 +41,7 @@ public:
signals:
void stageStarting(QString stage);
void stageFailed(QString stage, long errorCode);
void stageFailed(QString stage, int errorCode);
void connectionStarted();
@@ -61,24 +60,28 @@ private:
void emitLaunchWarning(QString text);
static
int getDecoderCapabilities(SDL_Window* window,
StreamingPreferences::VideoDecoderSelection vds,
int videoFormat, int width, int height, int frameRate);
bool populateDecoderProperties(SDL_Window* window);
IAudioRenderer* createAudioRenderer();
int detectAudioConfiguration();
IAudioRenderer* createAudioRenderer(const POPUS_MULTISTREAM_CONFIGURATION opusConfig);
bool testAudio(int audioConfiguration);
int getAudioRendererCapabilities(int audioConfiguration);
void getWindowDimensions(int& x, int& y,
int& width, int& height);
void toggleFullscreen();
void notifyMouseEmulationMode(bool enabled);
void updateOptimalWindowDisplayMode();
static
bool isHardwareDecodeAvailable(SDL_Window* window,
StreamingPreferences::VideoDecoderSelection vds,
int videoFormat, int width, int height, int frameRate);
static
bool chooseDecoder(StreamingPreferences::VideoDecoderSelection vds,
SDL_Window* window, int videoFormat, int width, int height,
@@ -90,10 +93,10 @@ private:
void clStageStarting(int stage);
static
void clStageFailed(int stage, long errorCode);
void clStageFailed(int stage, int errorCode);
static
void clConnectionTerminated(long errorCode);
void clConnectionTerminated(int errorCode);
static
void clLogMessage(const char* format, ...);
@@ -127,6 +130,7 @@ private:
StreamingPreferences* m_Preferences;
STREAM_CONFIGURATION m_StreamConfig;
DECODER_RENDERER_CALLBACKS m_VideoCallbacks;
AUDIO_RENDERER_CALLBACKS m_AudioCallbacks;
NvComputer* m_Computer;
NvApp m_App;
SDL_Window* m_Window;
@@ -141,6 +145,7 @@ private:
bool m_UnexpectedTermination;
SdlInputHandler* m_InputHandler;
SDL_SpinLock m_InputHandlerLock;
int m_MouseEmulationRefCount;
int m_ActiveVideoFormat;
int m_ActiveVideoWidth;
@@ -148,7 +153,6 @@ private:
int m_ActiveVideoFrameRate;
OpusMSDecoder* m_OpusDecoder;
short m_OpusDecodeBuffer[MAX_CHANNELS * SAMPLES_PER_FRAME];
IAudioRenderer* m_AudioRenderer;
OPUS_MULTISTREAM_CONFIGURATION m_AudioConfig;
int m_AudioSampleCount;
@@ -156,7 +160,6 @@ private:
Overlay::OverlayManager m_OverlayManager;
static AUDIO_RENDERER_CALLBACKS k_AudioCallbacks;
static CONNECTION_LISTENER_CALLBACKS k_ConnCallbacks;
static Session* s_ActiveSession;
static QSemaphore s_ActiveSessionSemaphore;
+2 -4
View File
@@ -12,14 +12,12 @@ void StreamUtils::scaleSourceToDestinationSurface(SDL_Rect* src, SDL_Rect* dst)
int dstW = dst->h * src->w / src->h;
if (dstH > dst->h) {
dst->y = 0;
dst->x = (dst->w - dstW) / 2;
dst->x += (dst->w - dstW) / 2;
dst->w = dstW;
SDL_assert(dst->w * src->h / src->w <= dst->h);
}
else {
dst->x = 0;
dst->y = (dst->h - dstH) / 2;
dst->y += (dst->h - dstH) / 2;
dst->h = dstH;
SDL_assert(dst->h * src->w / src->h <= dst->w);
}
+17 -23
View File
@@ -26,33 +26,27 @@ typedef struct _VIDEO_STATS {
uint32_t measurementStartTimestamp;
} VIDEO_STATS, *PVIDEO_STATS;
typedef struct _DECODER_PARAMETERS {
SDL_Window* window;
StreamingPreferences::VideoDecoderSelection vds;
int videoFormat;
int width;
int height;
int frameRate;
bool enableVsync;
bool enableFramePacing;
} DECODER_PARAMETERS, *PDECODER_PARAMETERS;
class IVideoDecoder {
public:
virtual ~IVideoDecoder() {}
virtual bool initialize(StreamingPreferences::VideoDecoderSelection vds,
SDL_Window* window,
int videoFormat,
int width,
int height,
int frameRate,
bool enableVsync,
bool enableFramePacing) = 0;
virtual bool initialize(PDECODER_PARAMETERS params) = 0;
virtual bool isHardwareAccelerated() = 0;
virtual bool isAlwaysFullScreen() = 0;
virtual int getDecoderCapabilities() = 0;
virtual int getDecoderColorspace() = 0;
virtual QSize getDecoderMaxResolution() = 0;
virtual int submitDecodeUnit(PDECODE_UNIT du) = 0;
virtual void renderFrame(SDL_UserEvent* event) = 0;
virtual void dropFrame(SDL_UserEvent* event) = 0;
virtual void queueFrame(void* data1 = nullptr,
void* data2 = nullptr)
{
SDL_Event event;
event.type = SDL_USEREVENT;
event.user.code = SDL_CODE_FRAME_READY;
event.user.data1 = data1;
event.user.data2 = data2;
SDL_PushEvent(&event);
}
virtual void renderFrameOnMainThread() = 0;
};
@@ -0,0 +1,57 @@
#include "cuda.h"
CUDARenderer::CUDARenderer()
: m_HwContext(nullptr)
{
}
CUDARenderer::~CUDARenderer()
{
if (m_HwContext != nullptr) {
av_buffer_unref(&m_HwContext);
}
}
bool CUDARenderer::initialize(PDECODER_PARAMETERS)
{
int err;
err = av_hwdevice_ctx_create(&m_HwContext, AV_HWDEVICE_TYPE_CUDA, nullptr, nullptr, 0);
if (err != 0) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"av_hwdevice_ctx_create(CUDA) failed: %d",
err);
return false;
}
return true;
}
bool CUDARenderer::prepareDecoderContext(AVCodecContext* context, AVDictionary**)
{
context->hw_device_ctx = av_buffer_ref(m_HwContext);
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Using CUDA accelerated decoder");
return true;
}
void CUDARenderer::renderFrame(AVFrame*)
{
// We only support indirect rendering
SDL_assert(false);
}
bool CUDARenderer::needsTestFrame()
{
return true;
}
bool CUDARenderer::isDirectRenderingSupported()
{
// We only support rendering via SDL read-back
return false;
}
@@ -0,0 +1,18 @@
#pragma once
#include "renderer.h"
class CUDARenderer : public IFFmpegRenderer {
public:
CUDARenderer();
virtual ~CUDARenderer() override;
virtual bool initialize(PDECODER_PARAMETERS) override;
virtual bool prepareDecoderContext(AVCodecContext* context, AVDictionary** options) override;
virtual void renderFrame(AVFrame* frame) override;
virtual bool needsTestFrame() override;
virtual bool isDirectRenderingSupported() override;
private:
AVBufferRef* m_HwContext;
};
@@ -0,0 +1,279 @@
#include "drm.h"
extern "C" {
#include <libavutil/hwcontext_drm.h>
}
#include <libdrm/drm_fourcc.h>
#include <unistd.h>
#include <fcntl.h>
#include "streaming/streamutils.h"
#include "streaming/session.h"
#include <Limelight.h>
DrmRenderer::DrmRenderer()
: m_DrmFd(-1),
m_CrtcId(0),
m_PlaneId(0),
m_CurrentFbId(0)
{
}
DrmRenderer::~DrmRenderer()
{
if (m_CurrentFbId != 0) {
drmModeRmFB(m_DrmFd, m_CurrentFbId);
}
if (m_DrmFd != -1) {
close(m_DrmFd);
}
}
bool DrmRenderer::prepareDecoderContext(AVCodecContext*, AVDictionary**)
{
/* Nothing to do */
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Using DRM renderer");
return true;
}
bool DrmRenderer::initialize(PDECODER_PARAMETERS)
{
const char* device = SDL_getenv("DRM_DEV");
int i;
if (device == nullptr) {
device = "/dev/dri/card0";
}
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Opening DRM device: %s",
device);
m_DrmFd = open(device, O_RDWR | O_CLOEXEC);
if (m_DrmFd < 0) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Failed to open DRM device: %d",
errno);
return false;
}
drmModeRes* resources = drmModeGetResources(m_DrmFd);
if (resources == nullptr) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"drmModeGetResources() failed: %d",
errno);
return false;
}
// Look for a connected connector and get the associated encoder
uint32_t encoderId = 0;
for (i = 0; i < resources->count_connectors && encoderId == 0; i++) {
drmModeConnector* connector = drmModeGetConnector(m_DrmFd, resources->connectors[i]);
if (connector != nullptr) {
if (connector->connection == DRM_MODE_CONNECTED && connector->count_modes > 0) {
encoderId = connector->encoder_id;
}
drmModeFreeConnector(connector);
}
}
if (encoderId == 0) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"No connected displays found!");
drmModeFreeResources(resources);
return false;
}
// Now find the CRTC from the encoder
m_CrtcId = 0;
for (i = 0; i < resources->count_encoders && m_CrtcId == 0; i++) {
drmModeEncoder* encoder = drmModeGetEncoder(m_DrmFd, resources->encoders[i]);
if (encoder != nullptr) {
if (encoder->encoder_id == encoderId) {
m_CrtcId = encoder->crtc_id;
}
drmModeFreeEncoder(encoder);
}
}
if (m_CrtcId == 0) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"DRM encoder not found!");
drmModeFreeResources(resources);
return false;
}
int crtcIndex = -1;
for (int i = 0; i < resources->count_crtcs; i++) {
if (resources->crtcs[i] == m_CrtcId) {
drmModeCrtc* crtc = drmModeGetCrtc(m_DrmFd, resources->crtcs[i]);
crtcIndex = i;
m_OutputRect.x = m_OutputRect.y = 0;
m_OutputRect.w = crtc->width;
m_OutputRect.h = crtc->height;
drmModeFreeCrtc(crtc);
break;
}
}
drmModeFreeResources(resources);
if (crtcIndex == -1) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Failed to get CRTC!");
return false;
}
drmSetClientCap(m_DrmFd, DRM_CLIENT_CAP_UNIVERSAL_PLANES, 1);
drmModePlaneRes* planeRes = drmModeGetPlaneResources(m_DrmFd);
if (planeRes == nullptr) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"drmGetPlaneResources() failed: %d",
errno);
return false;
}
// Find an NV12 overlay plane to render on
m_PlaneId = 0;
for (uint32_t i = 0; i < planeRes->count_planes && m_PlaneId == 0; i++) {
drmModePlane* plane = drmModeGetPlane(m_DrmFd, planeRes->planes[i]);
if (plane != nullptr) {
bool matchingFormat = false;
for (uint32_t j = 0; j < plane->count_formats; j++) {
if (plane->formats[j] == DRM_FORMAT_NV12) {
matchingFormat = true;
break;
}
}
if (matchingFormat == false) {
drmModeFreePlane(plane);
continue;
}
if ((plane->possible_crtcs & (1 << crtcIndex)) && plane->crtc_id == 0) {
drmModeObjectPropertiesPtr props = drmModeObjectGetProperties(m_DrmFd, planeRes->planes[i], DRM_MODE_OBJECT_PLANE);
if (props != nullptr) {
for (uint32_t j = 0; j < props->count_props && m_PlaneId == 0; j++) {
drmModePropertyPtr prop = drmModeGetProperty(m_DrmFd, props->props[j]);
if (prop != nullptr) {
if (!strcmp(prop->name, "type") && props->prop_values[j] == DRM_PLANE_TYPE_OVERLAY) {
m_PlaneId = plane->plane_id;
}
drmModeFreeProperty(prop);
}
}
drmModeFreeObjectProperties(props);
}
}
drmModeFreePlane(plane);
}
}
drmModeFreePlaneResources(planeRes);
return true;
}
enum AVPixelFormat DrmRenderer::getPreferredPixelFormat(int)
{
// DRM PRIME buffers
return AV_PIX_FMT_DRM_PRIME;
}
int DrmRenderer::getRendererAttributes()
{
// This renderer can only draw in full-screen
return RENDERER_ATTRIBUTE_FULLSCREEN_ONLY;
}
void DrmRenderer::renderFrame(AVFrame* frame)
{
if (frame == nullptr) {
// End of stream - nothing to do for us
return;
}
AVDRMFrameDescriptor* drmFrame = (AVDRMFrameDescriptor*)frame->data[0];
int err;
uint32_t primeHandle;
uint32_t handles[4] = {};
uint32_t pitches[4] = {};
uint32_t offsets[4] = {};
SDL_Rect src, dst;
src.x = src.y = 0;
src.w = frame->width;
src.h = frame->height;
dst = m_OutputRect;
StreamUtils::scaleSourceToDestinationSurface(&src, &dst);
// Convert the FD in the AVDRMFrameDescriptor to a PRIME handle
// that can be used in drmModeAddFB2()
SDL_assert(drmFrame->nb_objects == 1);
err = drmPrimeFDToHandle(m_DrmFd, drmFrame->objects[0].fd, &primeHandle);
if (err < 0) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"drmPrimeFDToHandle() failed: %d",
errno);
return;
}
SDL_assert(drmFrame->nb_layers == 1);
SDL_assert(drmFrame->layers[0].nb_planes == 2);
for (int i = 0; i < drmFrame->layers[0].nb_planes; i++) {
handles[i] = primeHandle;
pitches[i] = drmFrame->layers[0].planes[i].pitch;
offsets[i] = drmFrame->layers[0].planes[i].offset;
}
// Remember the last FB object we created so we can free it
// when we are finished rendering this one (if successful).
uint32_t lastFbId = m_CurrentFbId;
// Create a frame buffer object from the PRIME buffer
err = drmModeAddFB2(m_DrmFd, frame->width, frame->height,
drmFrame->layers[0].format,
handles, pitches, offsets, &m_CurrentFbId, 0);
if (err < 0) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"drmModeAddFB2() failed: %d",
errno);
m_CurrentFbId = lastFbId;
return;
}
// Update the overlay
err = drmModeSetPlane(m_DrmFd, m_PlaneId, m_CrtcId, m_CurrentFbId, 0,
dst.x, dst.y,
dst.w, dst.h,
0, 0,
frame->width << 16,
frame->height << 16);
if (err < 0) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"drmModeSetPlane() failed: %d",
errno);
drmModeRmFB(m_DrmFd, m_CurrentFbId);
m_CurrentFbId = lastFbId;
return;
}
// Free the previous FB object which has now been superseded
drmModeRmFB(m_DrmFd, lastFbId);
}
@@ -0,0 +1,25 @@
#pragma once
#include "renderer.h"
#include <xf86drm.h>
#include <xf86drmMode.h>
class DrmRenderer : public IFFmpegRenderer {
public:
DrmRenderer();
virtual ~DrmRenderer() override;
virtual bool initialize(PDECODER_PARAMETERS params) override;
virtual bool prepareDecoderContext(AVCodecContext* context, AVDictionary** options) override;
virtual void renderFrame(AVFrame* frame) override;
virtual enum AVPixelFormat getPreferredPixelFormat(int videoFormat) override;
virtual int getRendererAttributes() override;
private:
int m_DrmFd;
uint32_t m_CrtcId;
uint32_t m_PlaneId;
uint32_t m_CurrentFbId;
SDL_Rect m_OutputRect;
};
+65 -29
View File
@@ -80,7 +80,7 @@ AVBufferRef* DXVA2Renderer::ffPoolAlloc(void* opaque, int)
return NULL;
}
bool DXVA2Renderer::prepareDecoderContext(AVCodecContext* context)
bool DXVA2Renderer::prepareDecoderContext(AVCodecContext* context, AVDictionary**)
{
// m_DXVAContext.workaround and report_id already initialized elsewhere
m_DXVAContext.decoder = m_Decoder;
@@ -107,7 +107,7 @@ bool DXVA2Renderer::prepareDecoderContext(AVCodecContext* context)
int DXVA2Renderer::ffGetBuffer2(AVCodecContext* context, AVFrame* frame, int)
{
DXVA2Renderer* me = (DXVA2Renderer*)((FFmpegVideoDecoder*)context->opaque)->getRenderer();
DXVA2Renderer* me = (DXVA2Renderer*)((FFmpegVideoDecoder*)context->opaque)->getBackendRenderer();
frame->buf[0] = av_buffer_pool_get(me->m_Pool);
if (!frame->buf[0]) {
@@ -382,9 +382,6 @@ bool DXVA2Renderer::isDecoderBlacklisted()
HRESULT hr;
bool result = false;
// TODO: Update for HEVC Main10
SDL_assert(m_VideoFormat != VIDEO_FORMAT_H265_MAIN10);
if (qgetenv("DXVA2_DISABLE_DECODER_BLACKLIST") == "1") {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"DXVA2 decoder blacklist is disabled");
@@ -432,6 +429,15 @@ bool DXVA2Renderer::isDecoderBlacklisted()
"GPU blacklisted for HEVC due to hybrid decode");
result = (m_VideoFormat & VIDEO_FORMAT_MASK_H265) != 0;
break;
case 0x1900: // Skylake
// Blacklist these for HEVC Main10 to avoid hybrid decode.
// Regular HEVC Main is fine though.
if (m_VideoFormat == VIDEO_FORMAT_H265_MAIN10) {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"GPU blacklisted for HEVC Main10 due to hybrid decode");
result = false;
break;
}
default:
// Intel drivers from before late-2017 had a bug that caused some strange artifacts
// when decoding HEVC. Avoid HEVC on drivers prior to build 4836 which I confirmed
@@ -540,12 +546,34 @@ bool DXVA2Renderer::initializeDevice(SDL_Window* window, bool enableVsync)
d3dpp.hDeviceWindow = info.info.win.window;
d3dpp.Flags = D3DPRESENTFLAG_VIDEO;
if (m_VideoFormat == VIDEO_FORMAT_H265_MAIN10) {
// Verify 10-bit A2R10G10B10 color support. This is only available
// as a display format in full-screen exclusive mode on DX9.
hr = d3d9ex->CheckDeviceType(adapterIndex,
D3DDEVTYPE_HAL,
D3DFMT_A2R10G10B10,
D3DFMT_A2R10G10B10,
FALSE);
if (FAILED(hr)) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"GPU/driver doesn't support A2R10G10B10");
d3d9ex->Release();
return false;
}
}
if ((windowFlags & SDL_WINDOW_FULLSCREEN_DESKTOP) == SDL_WINDOW_FULLSCREEN) {
d3dpp.Windowed = false;
d3dpp.BackBufferWidth = currentMode.Width;
d3dpp.BackBufferHeight = currentMode.Height;
d3dpp.FullScreen_RefreshRateInHz = currentMode.RefreshRate;
d3dpp.BackBufferFormat = currentMode.Format;
if (m_VideoFormat == VIDEO_FORMAT_H265_MAIN10) {
d3dpp.BackBufferFormat = currentMode.Format = D3DFMT_A2R10G10B10;
}
else {
d3dpp.BackBufferFormat = currentMode.Format;
}
}
else {
d3dpp.Windowed = true;
@@ -649,11 +677,11 @@ bool DXVA2Renderer::initializeDevice(SDL_Window* window, bool enableVsync)
return true;
}
bool DXVA2Renderer::initialize(SDL_Window* window, int videoFormat, int width, int height, int, bool enableVsync)
bool DXVA2Renderer::initialize(PDECODER_PARAMETERS params)
{
m_VideoFormat = videoFormat;
m_VideoWidth = width;
m_VideoHeight = height;
m_VideoFormat = params->videoFormat;
m_VideoWidth = params->width;
m_VideoHeight = params->height;
RtlZeroMemory(&m_Desc, sizeof(m_Desc));
@@ -680,9 +708,15 @@ bool DXVA2Renderer::initialize(SDL_Window* window, int videoFormat, int width, i
m_Desc.SampleFormat.VideoPrimaries = DXVA2_VideoPrimaries_Unknown;
m_Desc.SampleFormat.VideoTransferFunction = DXVA2_VideoTransFunc_Unknown;
m_Desc.SampleFormat.SampleFormat = DXVA2_SampleProgressiveFrame;
m_Desc.Format = (D3DFORMAT)MAKEFOURCC('N','V','1','2');
if (!initializeDevice(window, enableVsync)) {
if (m_VideoFormat == VIDEO_FORMAT_H265_MAIN10) {
m_Desc.Format = (D3DFORMAT)MAKEFOURCC('P','0','1','0');
}
else {
m_Desc.Format = (D3DFORMAT)MAKEFOURCC('N','V','1','2');
}
if (!initializeDevice(params->window, params->enableVsync)) {
return false;
}
@@ -704,23 +738,6 @@ bool DXVA2Renderer::initialize(SDL_Window* window, int videoFormat, int width, i
return true;
}
bool DXVA2Renderer::needsTestFrame()
{
// We validate the DXVA2 profiles are supported
// in initialize() so no test frame is required
return false;
}
int DXVA2Renderer::getDecoderCapabilities()
{
return 0;
}
IFFmpegRenderer::FramePacingConstraint DXVA2Renderer::getFramePacingConstraint()
{
return PACING_ANY;
}
void DXVA2Renderer::notifyOverlayUpdated(Overlay::OverlayType type)
{
HRESULT hr;
@@ -779,8 +796,27 @@ void DXVA2Renderer::notifyOverlayUpdated(Overlay::OverlayType type)
}
}
int DXVA2Renderer::getDecoderColorspace()
{
if (isDXVideoProcessorAPIBlacklisted()) {
// StretchRect() assumes Rec 601 on Intel GPUs
return COLORSPACE_REC_601;
}
else {
// VideoProcessBlt() *should* properly handle whatever, since
// we provide colorspace information. However, AMD GPUs seem to
// always assume Rec 709, so we'll use that as our default.
return COLORSPACE_REC_709;
}
}
void DXVA2Renderer::renderFrame(AVFrame *frame)
{
if (frame == nullptr) {
// End of stream - nothing to do for us
return;
}
IDirect3DSurface9* surface = reinterpret_cast<IDirect3DSurface9*>(frame->data[3]);
HRESULT hr;
+4 -11
View File
@@ -15,19 +15,12 @@ class DXVA2Renderer : public IFFmpegRenderer
{
public:
DXVA2Renderer();
virtual ~DXVA2Renderer();
virtual bool initialize(SDL_Window* window,
int videoFormat,
int width,
int height,
int maxFps,
bool enableVsync) override;
virtual bool prepareDecoderContext(AVCodecContext* context) override;
virtual ~DXVA2Renderer() override;
virtual bool initialize(PDECODER_PARAMETERS params) override;
virtual bool prepareDecoderContext(AVCodecContext* context, AVDictionary** options) override;
virtual void renderFrame(AVFrame* frame) override;
virtual bool needsTestFrame() override;
virtual int getDecoderCapabilities() override;
virtual FramePacingConstraint getFramePacingConstraint() override;
virtual void notifyOverlayUpdated(Overlay::OverlayType) override;
virtual int getDecoderColorspace() override;
private:
bool initializeDecoder();
@@ -0,0 +1,21 @@
// vim: noai:ts=4:sw=4:softtabstop=4:expandtab
#include "renderer.h"
#include <SDL_egl.h>
static QStringList egl_get_extensions(EGLDisplay dpy) {
const auto EGLExtensionsStr = eglQueryString(dpy, EGL_EXTENSIONS);
if (!EGLExtensionsStr) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, "Unable to get EGL extensions");
return QStringList();
}
return QString(EGLExtensionsStr).split(" ");
}
EGLExtensions::EGLExtensions(EGLDisplay dpy) :
m_Extensions(egl_get_extensions(dpy))
{}
bool EGLExtensions::isSupported(const QString &extension) const {
return m_Extensions.contains(extension);
}
@@ -0,0 +1,627 @@
// vim: noai:ts=4:sw=4:softtabstop=4:expandtab
#include "eglvid.h"
#include "path.h"
#include "streaming/session.h"
#include "streaming/streamutils.h"
#include <QDir>
#include <Limelight.h>
#include <unistd.h>
#include <SDL_egl.h>
#include <SDL_opengles2.h>
#include <SDL_render.h>
#include <SDL_syswm.h>
#ifndef EGL_VERSION_1_5
typedef intptr_t EGLAttrib;
#endif
#if !defined(EGL_VERSION_1_5) || !defined(EGL_EGL_PROTOTYPES)
typedef EGLDisplay (EGLAPIENTRYP PFNEGLGETPLATFORMDISPLAYPROC) (EGLenum platform, void *native_display, const EGLAttrib *attrib_list);
#endif
#ifndef EGL_EXT_platform_base
typedef EGLDisplay (EGLAPIENTRYP PFNEGLGETPLATFORMDISPLAYEXTPROC) (EGLenum platform, void *native_display, const EGLint *attrib_list);
#endif
// These are EGL extensions, so some platform headers may not provide them
#ifndef EGL_PLATFORM_WAYLAND_KHR
#define EGL_PLATFORM_WAYLAND_KHR 0x31D8
#endif
#ifndef EGL_PLATFORM_X11_KHR
#define EGL_PLATFORM_X11_KHR 0x31D5
#endif
/* TODO:
* - handle more pixel formats
* - handle software decoding
*/
/* DOC/misc:
* - https://kernel-recipes.org/en/2016/talks/video-and-colorspaces/
* - http://www.brucelindbloom.com/
* - https://learnopengl.com/Getting-started/Shaders
* - https://github.com/stunpix/yuvit
* - https://en.wikipedia.org/wiki/YCbCr#ITU-R_BT.601_conversion
* - https://www.renesas.com/eu/en/www/doc/application-note/an9717.pdf
* - https://www.xilinx.com/support/documentation/application_notes/xapp283.pdf
* - https://www.itu.int/dms_pubrec/itu-r/rec/bt/R-REC-BT.709-6-201506-I!!PDF-E.pdf
* - https://www.khronos.org/registry/OpenGL/extensions/OES/OES_EGL_image_external.txt
* - https://gist.github.com/rexguo/6696123
* - https://wiki.libsdl.org/CategoryVideo
*/
#define EGL_LOG(Category, ...) SDL_Log ## Category(\
SDL_LOG_CATEGORY_APPLICATION, \
"EGLRenderer: " __VA_ARGS__)
EGLRenderer::EGLRenderer(IFFmpegRenderer *backendRenderer)
:
m_SwPixelFormat(AV_PIX_FMT_NONE),
m_EGLDisplay(EGL_NO_DISPLAY),
m_Textures{0},
m_ShaderProgram(0),
m_Context(0),
m_Window(nullptr),
m_Backend(backendRenderer),
m_VAO(0),
m_ColorSpace(AVCOL_SPC_NB),
m_ColorFull(false),
m_BlockingSwapBuffers(false),
m_glEGLImageTargetTexture2DOES(nullptr),
m_glGenVertexArraysOES(nullptr),
m_glBindVertexArrayOES(nullptr),
m_glDeleteVertexArraysOES(nullptr),
m_DummyRenderer(nullptr)
{
SDL_assert(backendRenderer);
SDL_assert(backendRenderer->canExportEGL());
// Save these global parameters so we can restore them in our destructor
SDL_GL_GetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, &m_OldContextProfileMask);
SDL_GL_GetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, &m_OldContextMajorVersion);
SDL_GL_GetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, &m_OldContextMinorVersion);
}
EGLRenderer::~EGLRenderer()
{
if (m_Context) {
// Reattach the GL context to the main thread for destruction
SDL_GL_MakeCurrent(m_Window, m_Context);
if (m_ShaderProgram) {
glDeleteProgram(m_ShaderProgram);
}
if (m_VAO) {
SDL_assert(m_glDeleteVertexArraysOES != nullptr);
m_glDeleteVertexArraysOES(1, &m_VAO);
}
if (m_DummyRenderer) {
SDL_DestroyRenderer(m_DummyRenderer);
}
SDL_GL_DeleteContext(m_Context);
}
// Reset the global properties back to what they were before
SDL_SetHint(SDL_HINT_OPENGL_ES_DRIVER, "0");
SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, m_OldContextProfileMask);
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, m_OldContextMajorVersion);
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, m_OldContextMinorVersion);
}
bool EGLRenderer::prepareDecoderContext(AVCodecContext*, AVDictionary**)
{
/* Nothing to do */
EGL_LOG(Info, "Using EGL renderer");
return true;
}
void EGLRenderer::notifyOverlayUpdated(Overlay::OverlayType)
{
// TODO: FIXME
}
bool EGLRenderer::isPixelFormatSupported(int, AVPixelFormat pixelFormat)
{
// Remember to keep this in sync with EGLRenderer::renderFrame()!
switch (pixelFormat)
{
case AV_PIX_FMT_NV12:
return true;
default:
return false;
}
}
int EGLRenderer::loadAndBuildShader(int shaderType,
const char *file) {
GLuint shader = glCreateShader(shaderType);
if (!shader || shader == GL_INVALID_ENUM) {
EGL_LOG(Error, "Can't create shader: %d", glGetError());
return 0;
}
auto sourceData = Path::readDataFile(file);
GLint len = sourceData.size();
const char *buf = sourceData.data();
glShaderSource(shader, 1, &buf, &len);
glCompileShader(shader);
GLint status;
glGetShaderiv(shader, GL_COMPILE_STATUS, &status);
if (!status) {
char shaderLog[512];
glGetShaderInfoLog(shader, sizeof (shaderLog), nullptr, shaderLog);
EGL_LOG(Error, "Cannot load shader \"%s\": %s", file, shaderLog);
return 0;
}
return shader;
}
bool EGLRenderer::openDisplay(unsigned int platform, void* nativeDisplay)
{
PFNEGLGETPLATFORMDISPLAYPROC eglGetPlatformDisplayProc;
PFNEGLGETPLATFORMDISPLAYEXTPROC eglGetPlatformDisplayEXTProc;
m_EGLDisplay = EGL_NO_DISPLAY;
// NB: eglGetPlatformDisplay() and eglGetPlatformDisplayEXT() have slightly different definitions
eglGetPlatformDisplayProc = (typeof(eglGetPlatformDisplayProc))eglGetProcAddress("eglGetPlatformDisplay");
eglGetPlatformDisplayEXTProc = (typeof(eglGetPlatformDisplayEXTProc))eglGetProcAddress("eglGetPlatformDisplayEXT");
if (m_EGLDisplay == EGL_NO_DISPLAY) {
// eglGetPlatformDisplay() is part of the EGL 1.5 core specification
if (eglGetPlatformDisplayProc != nullptr) {
m_EGLDisplay = eglGetPlatformDisplayProc(platform, nativeDisplay, nullptr);
if (m_EGLDisplay == EGL_NO_DISPLAY) {
EGL_LOG(Warn, "eglGetPlatformDisplay() failed: %d", eglGetError());
}
}
}
if (m_EGLDisplay == EGL_NO_DISPLAY) {
// eglGetPlatformDisplayEXT() is an extension for EGL 1.4
const EGLExtensions eglExtensions(EGL_NO_DISPLAY);
if (eglExtensions.isSupported("EGL_EXT_platform_base")) {
if (eglGetPlatformDisplayEXTProc != nullptr) {
m_EGLDisplay = eglGetPlatformDisplayEXTProc(platform, nativeDisplay, nullptr);
if (m_EGLDisplay == EGL_NO_DISPLAY) {
EGL_LOG(Warn, "eglGetPlatformDisplayEXT() failed: %d", eglGetError());
}
}
else {
EGL_LOG(Warn, "EGL_EXT_platform_base supported but no eglGetPlatformDisplayEXT() export!");
}
}
}
if (m_EGLDisplay == EGL_NO_DISPLAY) {
// Finally, if all else fails, use eglGetDisplay()
m_EGLDisplay = eglGetDisplay((EGLNativeDisplayType)nativeDisplay);
if (m_EGLDisplay == EGL_NO_DISPLAY) {
EGL_LOG(Error, "eglGetDisplay() failed: %d", eglGetError());
}
}
return m_EGLDisplay != EGL_NO_DISPLAY;
}
bool EGLRenderer::compileShader() {
SDL_assert(!m_ShaderProgram);
SDL_assert(m_SwPixelFormat != AV_PIX_FMT_NONE);
// XXX: TODO: other formats
SDL_assert(m_SwPixelFormat == AV_PIX_FMT_NV12);
bool ret = false;
GLuint vertexShader = loadAndBuildShader(GL_VERTEX_SHADER, "egl.vert");
if (!vertexShader)
return false;
GLuint fragmentShader = loadAndBuildShader(GL_FRAGMENT_SHADER, "egl.frag");
if (!fragmentShader)
goto fragError;
m_ShaderProgram = glCreateProgram();
if (!m_ShaderProgram) {
EGL_LOG(Error, "Cannot create shader program");
goto progFailCreate;
}
glAttachShader(m_ShaderProgram, vertexShader);
glAttachShader(m_ShaderProgram, fragmentShader);
glLinkProgram(m_ShaderProgram);
int status;
glGetProgramiv(m_ShaderProgram, GL_LINK_STATUS, &status);
if (status) {
ret = true;
} else {
char shader_log[512];
glGetProgramInfoLog(m_ShaderProgram, sizeof (shader_log), nullptr, shader_log);
EGL_LOG(Error, "Cannot link shader program: %s", shader_log);
glDeleteProgram(m_ShaderProgram);
m_ShaderProgram = 0;
}
progFailCreate:
glDeleteShader(fragmentShader);
fragError:
glDeleteShader(vertexShader);
return ret;
}
bool EGLRenderer::initialize(PDECODER_PARAMETERS params)
{
m_Window = params->window;
if (params->videoFormat == VIDEO_FORMAT_H265_MAIN10) {
// EGL doesn't support rendering YUV 10-bit textures yet
return false;
}
// It's not safe to attempt to opportunistically create a GLES2
// renderer prior to 2.0.10. If GLES2 isn't available, SDL will
// attempt to dereference a null pointer and crash Moonlight.
// https://bugzilla.libsdl.org/show_bug.cgi?id=4350
// https://hg.libsdl.org/SDL/rev/84618d571795
if (!SDL_VERSION_ATLEAST(2, 0, 10)) {
EGL_LOG(Error, "Not supported until SDL 2.0.10");
return false;
}
/*
* Create a dummy renderer in order to craft an accelerated SDL Window.
* Request opengl ES 3.0 context, otherwise it will SIGSEGV
* https://gitlab.freedesktop.org/mesa/mesa/issues/1011
*/
SDL_SetHint(SDL_HINT_OPENGL_ES_DRIVER, "1");
SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_ES);
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 3);
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 0);
int renderIndex;
int maxRenderers = SDL_GetNumRenderDrivers();
SDL_assert(maxRenderers >= 0);
SDL_RendererInfo renderInfo;
for (renderIndex = 0; renderIndex < maxRenderers; ++renderIndex) {
if (SDL_GetRenderDriverInfo(renderIndex, &renderInfo))
continue;
if (!strcmp(renderInfo.name, "opengles2")) {
SDL_assert(renderInfo.flags & SDL_RENDERER_ACCELERATED);
break;
}
}
if (renderIndex == maxRenderers) {
EGL_LOG(Error, "Could not find a suitable SDL_Renderer");
return false;
}
if (!(m_DummyRenderer = SDL_CreateRenderer(m_Window, renderIndex, SDL_RENDERER_ACCELERATED))) {
EGL_LOG(Error, "SDL_CreateRenderer() failed: %s", SDL_GetError());
return false;
}
SDL_SysWMinfo info;
SDL_VERSION(&info.version);
if (!SDL_GetWindowWMInfo(params->window, &info)) {
EGL_LOG(Error, "SDL_GetWindowWMInfo() failed: %s", SDL_GetError());
return false;
}
switch (info.subsystem) {
#ifdef SDL_VIDEO_DRIVER_WAYLAND
case SDL_SYSWM_WAYLAND:
if (!openDisplay(EGL_PLATFORM_WAYLAND_KHR, info.info.wl.display)) {
return false;
}
break;
#endif
#ifdef SDL_VIDEO_DRIVER_X11
case SDL_SYSWM_X11:
if (!openDisplay(EGL_PLATFORM_X11_KHR, info.info.x11.display)) {
return false;
}
break;
#endif
default:
EGL_LOG(Error, "not compatible with SYSWM");
return false;
}
if (m_EGLDisplay == EGL_NO_DISPLAY) {
EGL_LOG(Error, "Cannot get EGL display: %d", eglGetError());
return false;
}
if (!(m_Context = SDL_GL_CreateContext(params->window))) {
EGL_LOG(Error, "Cannot create OpenGL context: %s", SDL_GetError());
return false;
}
if (SDL_GL_MakeCurrent(params->window, m_Context)) {
EGL_LOG(Error, "Cannot use created EGL context: %s", SDL_GetError());
return false;
}
const EGLExtensions eglExtensions(m_EGLDisplay);
if (!eglExtensions.isSupported("EGL_KHR_image_base") &&
!eglExtensions.isSupported("EGL_KHR_image")) {
EGL_LOG(Error, "EGL_KHR_image unsupported");
return false;
}
else if (!SDL_GL_ExtensionSupported("GL_OES_EGL_image")) {
EGL_LOG(Error, "GL_OES_EGL_image unsupported");
return false;
}
if (!m_Backend->initializeEGL(m_EGLDisplay, eglExtensions))
return false;
if (!(m_glEGLImageTargetTexture2DOES = (typeof(m_glEGLImageTargetTexture2DOES))eglGetProcAddress("glEGLImageTargetTexture2DOES"))) {
EGL_LOG(Error,
"EGL: cannot retrieve `glEGLImageTargetTexture2DOES` address");
return false;
}
// Vertex arrays are an extension on OpenGL ES 2.0
if (SDL_GL_ExtensionSupported("GL_OES_vertex_array_object")) {
m_glGenVertexArraysOES = (typeof(m_glGenVertexArraysOES))eglGetProcAddress("glGenVertexArraysOES");
m_glBindVertexArrayOES = (typeof(m_glBindVertexArrayOES))eglGetProcAddress("glBindVertexArrayOES");
m_glDeleteVertexArraysOES = (typeof(m_glDeleteVertexArraysOES))eglGetProcAddress("glDeleteVertexArraysOES");
}
else {
// They are included in OpenGL ES 3.0 as part of the standard
m_glGenVertexArraysOES = (typeof(m_glGenVertexArraysOES))eglGetProcAddress("glGenVertexArrays");
m_glBindVertexArrayOES = (typeof(m_glBindVertexArrayOES))eglGetProcAddress("glBindVertexArray");
m_glDeleteVertexArraysOES = (typeof(m_glDeleteVertexArraysOES))eglGetProcAddress("glDeleteVertexArrays");
}
if (!m_glGenVertexArraysOES || !m_glBindVertexArrayOES || !m_glDeleteVertexArraysOES) {
EGL_LOG(Error, "Failed to find VAO functions");
return false;
}
/* Compute the video region size in order to keep the aspect ratio of the
* video stream.
*/
SDL_Rect src, dst;
src.x = src.y = dst.x = dst.y = 0;
src.w = params->width;
src.h = params->height;
SDL_GetWindowSize(m_Window, &dst.w, &dst.h);
StreamUtils::scaleSourceToDestinationSurface(&src, &dst);
glViewport(dst.x, dst.y, dst.w, dst.h);
glClearColor(0, 0, 0, 1);
glClear(GL_COLOR_BUFFER_BIT);
if (params->enableVsync) {
// Try to use adaptive VSYNC unless we're using frame pacing.
// Frame pacing relies on us blocking in renderFrame() to
// match the display refresh rate.
if (params->enableFramePacing || SDL_GL_SetSwapInterval(-1)) {
SDL_GL_SetSwapInterval(1);
m_BlockingSwapBuffers = true;
}
} else {
SDL_GL_SetSwapInterval(0);
}
SDL_GL_SwapWindow(params->window);
glGenTextures(EGL_MAX_PLANES, m_Textures);
for (size_t i = 0; i < EGL_MAX_PLANES; ++i) {
glBindTexture(GL_TEXTURE_EXTERNAL_OES, m_Textures[i]);
glTexParameteri(GL_TEXTURE_EXTERNAL_OES, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_EXTERNAL_OES, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_EXTERNAL_OES, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_EXTERNAL_OES, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
}
GLenum err = glGetError();
if (err != GL_NO_ERROR)
EGL_LOG(Error, "OpenGL error: %d", err);
// Detach the context from this thread, so the render thread can attach it
SDL_GL_MakeCurrent(m_Window, nullptr);
return err == GL_NO_ERROR;
}
const float *EGLRenderer::getColorMatrix() {
/* The conversion matrices are shamelessly stolen from linux:
* drivers/media/platform/imx-pxp.c:pxp_setup_csc
*/
static const float bt601Lim[] = {
1.1644f, 1.1644f, 1.1644f,
0.0f, -0.3917f, 2.0172f,
1.5960f, -0.8129f, 0.0f
};
static const float bt601Full[] = {
1.0f, 1.0f, 1.0f,
0.0f, -0.3441f, 1.7720f,
1.4020f, -0.7141f, 0.0f
};
static const float bt709Lim[] = {
1.1644f, 1.1644f, 1.1644f,
0.0f, -0.2132f, 2.1124f,
1.7927f, -0.5329f, 0.0f
};
static const float bt709Full[] = {
1.0f, 1.0f, 1.0f,
0.0f, -0.1873f, 1.8556f,
1.5748f, -0.4681f, 0.0f
};
static const float bt2020Lim[] = {
1.1644f, 1.1644f, 1.1644f,
0.0f, -0.1874f, 2.1418f,
1.6781f, -0.6505f, 0.0f
};
static const float bt2020Full[] = {
1.0f, 1.0f, 1.0f,
0.0f, -0.1646f, 1.8814f,
1.4746f, -0.5714f, 0.0f
};
switch (m_ColorSpace) {
case AVCOL_SPC_SMPTE170M:
case AVCOL_SPC_BT470BG:
EGL_LOG(Info, "BT-601 pixels");
return m_ColorFull ? bt601Full : bt601Lim;
case AVCOL_SPC_BT709:
EGL_LOG(Info, "BT-709 pixels");
return m_ColorFull ? bt709Full : bt709Lim;
case AVCOL_SPC_BT2020_NCL:
case AVCOL_SPC_BT2020_CL:
EGL_LOG(Info, "BT-2020 pixels");
return m_ColorFull ? bt2020Full : bt2020Lim;
};
EGL_LOG(Warn, "unknown color space: %d, falling back to BT-601",
m_ColorSpace);
return bt601Lim;
}
bool EGLRenderer::specialize() {
SDL_assert(!m_VAO);
// Attach our GL context to the render thread
SDL_GL_MakeCurrent(m_Window, m_Context);
if (!compileShader())
return false;
// The viewport should have the aspect ratio of the video stream
static const float vertices[] = {
// pos .... // tex coords
1.0f, 1.0f, 1.0f, 0.0f,
1.0f, -1.0f, 1.0f, 1.0f,
-1.0f, -1.0f, 0.0f, 1.0f,
-1.0f, 1.0f, 0.0f, 0.0f,
};
static const unsigned int indices[] = {
0, 1, 3,
1, 2, 3,
};
glUseProgram(m_ShaderProgram);
unsigned int VBO, EBO;
m_glGenVertexArraysOES(1, &m_VAO);
glGenBuffers(1, &VBO);
glGenBuffers(1, &EBO);
m_glBindVertexArrayOES(m_VAO);
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof (vertices), vertices, GL_STATIC_DRAW);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof (indices), indices, GL_STATIC_DRAW);
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 4 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 4 * sizeof(float), (void*)(2 * sizeof (float)));
glEnableVertexAttribArray(1);
glBindBuffer(GL_ARRAY_BUFFER, 0);
m_glBindVertexArrayOES(0);
int yuvmatLocation = glGetUniformLocation(m_ShaderProgram, "yuvmat");
glUniformMatrix3fv(yuvmatLocation, 1, GL_FALSE, getColorMatrix());
static const float limitedOffsets[] = { 16.0f / 255.0f, 128.0f / 255.0f, 128.0f / 255.0f };
static const float fullOffsets[] = { 0.0f, 128.0f / 255.0f, 128.0f / 255.0f };
int offLocation = glGetUniformLocation(m_ShaderProgram, "offset");
glUniform3fv(offLocation, 1, m_ColorFull ? fullOffsets : limitedOffsets);
int colorPlane = glGetUniformLocation(m_ShaderProgram, "plane1");
glUniform1i(colorPlane, 0);
colorPlane = glGetUniformLocation(m_ShaderProgram, "plane2");
glUniform1i(colorPlane, 1);
glDeleteBuffers(1, &VBO);
glDeleteBuffers(1, &EBO);
GLenum err = glGetError();
if (err != GL_NO_ERROR) {
EGL_LOG(Error, "OpenGL error: %d", err);
}
return err == GL_NO_ERROR;
}
void EGLRenderer::renderFrame(AVFrame* frame)
{
EGLImage imgs[EGL_MAX_PLANES];
if (frame == nullptr) {
// End of stream - unbind the GL context
SDL_GL_MakeCurrent(m_Window, nullptr);
return;
}
if (frame->hw_frames_ctx != nullptr) {
// Find the native read-back format and load the shader
if (m_SwPixelFormat == AV_PIX_FMT_NONE) {
auto hwFrameCtx = (AVHWFramesContext*)frame->hw_frames_ctx->data;
m_SwPixelFormat = hwFrameCtx->sw_format;
SDL_assert(m_SwPixelFormat != AV_PIX_FMT_NONE);
EGL_LOG(Info, "Selected read-back format: %d", m_SwPixelFormat);
// XXX: TODO: Handle other pixel formats
SDL_assert(m_SwPixelFormat == AV_PIX_FMT_NV12);
m_ColorSpace = frame->colorspace;
m_ColorFull = frame->color_range == AVCOL_RANGE_JPEG;
if (!specialize()) {
m_SwPixelFormat = AV_PIX_FMT_NONE;
return;
}
}
ssize_t plane_count = m_Backend->exportEGLImages(frame, m_EGLDisplay, imgs);
if (plane_count < 0)
return;
for (ssize_t i = 0; i < plane_count; ++i) {
glActiveTexture(GL_TEXTURE0 + i);
glBindTexture(GL_TEXTURE_EXTERNAL_OES, m_Textures[i]);
m_glEGLImageTargetTexture2DOES(GL_TEXTURE_EXTERNAL_OES, imgs[i]);
}
} else {
// TODO: load texture for SW decoding ?
EGL_LOG(Error, "EGL rendering only supports hw frames");
return;
}
glClear(GL_COLOR_BUFFER_BIT);
glUseProgram(m_ShaderProgram);
m_glBindVertexArrayOES(m_VAO);
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
SDL_GL_SwapWindow(m_Window);
if (m_BlockingSwapBuffers) {
// This glClear() forces us to block until the buffer swap is
// complete to continue rendering. Mesa won't actually wait
// for the swap with just glFinish() alone. Waiting here keeps us
// in lock step with the display refresh rate. If we don't wait
// here, we'll stall on the first GL call next frame. Doing the
// wait here instead allows more time for a newer frame to arrive
// for next renderFrame() call.
glClear(GL_COLOR_BUFFER_BIT);
glFinish();
}
if (frame->hw_frames_ctx != nullptr)
m_Backend->freeEGLImages(m_EGLDisplay, imgs);
}
@@ -0,0 +1,47 @@
#pragma once
#include "renderer.h"
#include <SDL_opengles2.h>
#include <SDL_opengles2_gl2ext.h>
class EGLRenderer : public IFFmpegRenderer {
public:
EGLRenderer(IFFmpegRenderer *backendRenderer);
virtual ~EGLRenderer() override;
virtual bool initialize(PDECODER_PARAMETERS params) override;
virtual bool prepareDecoderContext(AVCodecContext* context, AVDictionary** options) override;
virtual void renderFrame(AVFrame* frame) override;
virtual void notifyOverlayUpdated(Overlay::OverlayType) override;
virtual bool isPixelFormatSupported(int videoFormat, enum AVPixelFormat pixelFormat) override;
private:
bool compileShader();
bool specialize();
const float *getColorMatrix();
static int loadAndBuildShader(int shaderType, const char *filename);
bool openDisplay(unsigned int platform, void* nativeDisplay);
int m_SwPixelFormat;
void *m_EGLDisplay;
unsigned m_Textures[EGL_MAX_PLANES];
unsigned m_ShaderProgram;
SDL_GLContext m_Context;
SDL_Window *m_Window;
IFFmpegRenderer *m_Backend;
unsigned int m_VAO;
int m_ColorSpace;
bool m_ColorFull;
bool m_BlockingSwapBuffers;
PFNGLEGLIMAGETARGETTEXTURE2DOESPROC m_glEGLImageTargetTexture2DOES;
PFNGLGENVERTEXARRAYSOESPROC m_glGenVertexArraysOES;
PFNGLBINDVERTEXARRAYOESPROC m_glBindVertexArrayOES;
PFNGLDELETEVERTEXARRAYSOESPROC m_glDeleteVertexArraysOES;
int m_OldContextProfileMask;
int m_OldContextMajorVersion;
int m_OldContextMinorVersion;
SDL_Renderer *m_DummyRenderer;
};
@@ -0,0 +1,170 @@
#include "mmal.h"
#include "streaming/streamutils.h"
#include "streaming/session.h"
#include <Limelight.h>
MmalRenderer::MmalRenderer()
: m_Renderer(nullptr),
m_InputPort(nullptr)
{
}
MmalRenderer::~MmalRenderer()
{
if (m_InputPort != nullptr) {
mmal_port_disable(m_InputPort);
}
if (m_Renderer != nullptr) {
mmal_component_destroy(m_Renderer);
}
}
bool MmalRenderer::prepareDecoderContext(AVCodecContext* context, AVDictionary** options)
{
// FFmpeg defaults this to 10 which is too large to fit in the default 64 MB VRAM split.
// Reducing to 2 seems to work fine for our bitstreams (max of 1 buffered frame needed).
av_dict_set_int(options, "extra_buffers", 2, 0);
// MMAL seems to dislike certain initial width and height values, but it seems okay
// with getting zero for the width and height. We'll zero them all the time to be safe.
context->width = 0;
context->height = 0;
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Using MMAL renderer");
return true;
}
bool MmalRenderer::initialize(PDECODER_PARAMETERS params)
{
MMAL_STATUS_T status;
status = mmal_component_create(MMAL_COMPONENT_DEFAULT_VIDEO_RENDERER, &m_Renderer);
if (status != MMAL_SUCCESS) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"mmal_component_create() failed: %x (%s)",
status, mmal_status_to_string(status));
return false;
}
m_InputPort = m_Renderer->input[0];
m_InputPort->format->encoding = MMAL_ENCODING_OPAQUE;
m_InputPort->format->es->video.width = params->width;
m_InputPort->format->es->video.height = params->height;
m_InputPort->format->es->video.crop.x = 0;
m_InputPort->format->es->video.crop.y = 0;
m_InputPort->format->es->video.crop.width = params->width;
m_InputPort->format->es->video.crop.height = params->height;
status = mmal_port_format_commit(m_InputPort);
if (status != MMAL_SUCCESS) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"mmal_port_format_commit() failed: %x (%s)",
status, mmal_status_to_string(status));
return false;
}
status = mmal_component_enable(m_Renderer);
if (status != MMAL_SUCCESS) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"mmal_component_enable() failed: %x (%s)",
status, mmal_status_to_string(status));
return false;
}
{
MMAL_DISPLAYREGION_T dr;
dr.hdr.id = MMAL_PARAMETER_DISPLAYREGION;
dr.hdr.size = sizeof(MMAL_DISPLAYREGION_T);
dr.set = MMAL_DISPLAY_SET_LAYER;
dr.layer = 128;
dr.set |= MMAL_DISPLAY_SET_ALPHA;
dr.alpha = 255;
dr.set |= MMAL_DISPLAY_SET_FULLSCREEN;
dr.fullscreen = true;
{
SDL_Rect src, dst;
src.x = src.y = 0;
src.w = params->width;
src.h = params->height;
dst.x = dst.y = 0;
SDL_GetWindowSize(params->window, &dst.w, &dst.h);
StreamUtils::scaleSourceToDestinationSurface(&src, &dst);
dr.set |= MMAL_DISPLAY_SET_DEST_RECT;
dr.dest_rect.x = dst.x;
dr.dest_rect.y = dst.y;
dr.dest_rect.width = dst.w;
dr.dest_rect.height = dst.h;
}
}
status = mmal_port_enable(m_InputPort, InputPortCallback);
if (status != MMAL_SUCCESS) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"mmal_port_enable() failed: %x (%s)",
status, mmal_status_to_string(status));
return false;
}
return true;
}
void MmalRenderer::InputPortCallback(MMAL_PORT_T*, MMAL_BUFFER_HEADER_T* buffer)
{
mmal_buffer_header_release(buffer);
}
enum AVPixelFormat MmalRenderer::getPreferredPixelFormat(int videoFormat)
{
// Opaque MMAL buffers
SDL_assert(videoFormat == VIDEO_FORMAT_H264);
return AV_PIX_FMT_MMAL;
}
int MmalRenderer::getRendererAttributes()
{
// This renderer can only draw in full-screen and maxes out at 1080p
return RENDERER_ATTRIBUTE_FULLSCREEN_ONLY | RENDERER_ATTRIBUTE_1080P_MAX;
}
bool MmalRenderer::needsTestFrame()
{
// We won't be able to decode if the GPU memory is 64 MB or lower,
// so we must test before allowing the decoder to be used.
return true;
}
void MmalRenderer::renderFrame(AVFrame* frame)
{
if (frame == nullptr) {
// End of stream - nothing to do for us
return;
}
MMAL_BUFFER_HEADER_T* buffer = (MMAL_BUFFER_HEADER_T*)frame->data[3];
MMAL_STATUS_T status;
status = mmal_port_send_buffer(m_InputPort, buffer);
if (status != MMAL_SUCCESS) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"mmal_port_send_buffer() failed: %x (%s)",
status, mmal_status_to_string(status));
}
else {
// Prevent the buffer from being freed during av_frame_free()
// until rendering is complete. The reference is dropped in
// InputPortCallback().
mmal_buffer_header_acquire(buffer);
}
}
@@ -0,0 +1,27 @@
#pragma once
#include "renderer.h"
#include <interface/mmal/mmal.h>
#include <interface/mmal/util/mmal_util.h>
#include <interface/mmal/util/mmal_util_params.h>
#include <interface/mmal/util/mmal_default_components.h>
class MmalRenderer : public IFFmpegRenderer {
public:
MmalRenderer();
virtual ~MmalRenderer() override;
virtual bool initialize(PDECODER_PARAMETERS params) override;
virtual bool prepareDecoderContext(AVCodecContext* context, AVDictionary** options) override;
virtual void renderFrame(AVFrame* frame) override;
virtual enum AVPixelFormat getPreferredPixelFormat(int videoFormat) override;
virtual bool needsTestFrame() override;
virtual int getRendererAttributes() override;
private:
static void InputPortCallback(MMAL_PORT_T* port, MMAL_BUFFER_HEADER_T* buffer);
MMAL_COMPONENT_T* m_Renderer;
MMAL_PORT_T* m_InputPort;
};
@@ -1,11 +0,0 @@
#pragma once
#include "pacer.h"
class DisplayLinkVsyncSourceFactory
{
public:
static
IVsyncSource* createVsyncSource(Pacer* pacer);
};
@@ -1,132 +0,0 @@
#include "displaylinkvsyncsource.h"
#include <SDL_syswm.h>
#include <CoreVideo/CoreVideo.h>
#import <Cocoa/Cocoa.h>
class DisplayLinkVsyncSource : public IVsyncSource
{
public:
DisplayLinkVsyncSource(Pacer* pacer)
: m_Pacer(pacer),
m_DisplayLink(nullptr)
{
}
virtual ~DisplayLinkVsyncSource() override
{
if (m_DisplayLink != nullptr) {
CVDisplayLinkStop(m_DisplayLink);
CVDisplayLinkRelease(m_DisplayLink);
}
}
static
CGDirectDisplayID
getDisplayID(NSScreen* screen)
{
NSNumber* screenNumber = [screen deviceDescription][@"NSScreenNumber"];
return [screenNumber unsignedIntValue];
}
static
CVReturn
displayLinkOutputCallback(
CVDisplayLinkRef displayLink,
const CVTimeStamp* /* now */,
const CVTimeStamp* /* vsyncTime */,
CVOptionFlags,
CVOptionFlags*,
void *displayLinkContext)
{
auto me = reinterpret_cast<DisplayLinkVsyncSource*>(displayLinkContext);
SDL_assert(displayLink == me->m_DisplayLink);
// In my testing on macOS 10.13, this callback is invoked about 24 ms
// prior to the specified v-sync time (now - vsyncTime). Since this is
// greater than the standard v-sync interval (16 ms = 60 FPS), we will
// draw using the current host time, rather than the actual v-sync target
// time. Because the CVDisplayLink is in sync with the actual v-sync
// interval, even if many ms prior, we can safely use the current host time
// and get a consistent callback for each v-sync. This reduces video latency
// by at least 1 frame vs. rendering with the actual vsyncTime.
me->m_Pacer->vsyncCallback(500 / me->m_DisplayFps);
return kCVReturnSuccess;
}
virtual bool initialize(SDL_Window* window, int displayFps) override
{
SDL_SysWMinfo info;
SDL_VERSION(&info.version);
if (!SDL_GetWindowWMInfo(window, &info)) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"SDL_GetWindowWMInfo() failed: %s",
SDL_GetError());
return false;
}
SDL_assert(info.subsystem == SDL_SYSWM_COCOA);
m_DisplayFps = displayFps;
NSScreen* screen = [info.info.cocoa.window screen];
CVReturn status;
if (screen == nullptr) {
// Window not visible on any display, so use a
// CVDisplayLink that can work with all active displays.
// When we become visible, we'll recreate ourselves
// and associate with the new screen.
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"NSWindow is not visible on any display");
status = CVDisplayLinkCreateWithActiveCGDisplays(&m_DisplayLink);
}
else {
CGDirectDisplayID displayId;
displayId = getDisplayID(screen);
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"NSWindow on display: %x",
displayId);
status = CVDisplayLinkCreateWithCGDisplay(displayId, &m_DisplayLink);
}
if (status != kCVReturnSuccess) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Failed to create CVDisplayLink: %d",
status);
return false;
}
status = CVDisplayLinkSetOutputCallback(m_DisplayLink, displayLinkOutputCallback, this);
if (status != kCVReturnSuccess) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"CVDisplayLinkSetOutputCallback() failed: %d",
status);
return false;
}
status = CVDisplayLinkStart(m_DisplayLink);
if (status != kCVReturnSuccess) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"CVDisplayLinkStart() failed: %d",
status);
return false;
}
return true;
}
private:
Pacer* m_Pacer;
CVDisplayLinkRef m_DisplayLink;
int m_DisplayFps;
};
IVsyncSource* DisplayLinkVsyncSourceFactory::createVsyncSource(Pacer* pacer) {
return new DisplayLinkVsyncSource(pacer);
}
@@ -61,7 +61,7 @@ bool DxVsyncSource::initialize(SDL_Window* window, int displayFps)
m_Window = info.info.win.window;
m_Thread = SDL_CreateThread(vsyncThread, "DX Vsync Thread", this);
m_Thread = SDL_CreateThread(vsyncThread, "DXVsync", this);
if (m_Thread == nullptr) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Unable to create DX V-sync thread: %s",
@@ -18,7 +18,7 @@ NullThreadedVsyncSource::~NullThreadedVsyncSource()
bool NullThreadedVsyncSource::initialize(SDL_Window*, int displayFps)
{
m_DisplayFps = displayFps;
m_Thread = SDL_CreateThread(vsyncThread, "Null Vsync Thread", this);
m_Thread = SDL_CreateThread(vsyncThread, "NullVsync", this);
if (m_Thread == nullptr) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Unable to create DX V-sync thread: %s",
@@ -3,10 +3,6 @@
#include "nullthreadedvsyncsource.h"
#ifdef Q_OS_DARWIN
#include "displaylinkvsyncsource.h"
#endif
#ifdef Q_OS_WIN32
#define WIN32_LEAN_AND_MEAN
#include <Windows.h>
@@ -49,6 +45,10 @@ Pacer::~Pacer()
m_RenderQueueNotEmpty.wakeAll();
SDL_WaitThread(m_RenderThread, nullptr);
}
else {
// Send a null AVFrame to indicate end of stream on the main thread
m_VsyncRenderer->renderFrame(nullptr);
}
// Delete any remaining unconsumed frames
while (!m_RenderQueue.isEmpty()) {
@@ -61,6 +61,24 @@ Pacer::~Pacer()
}
}
void Pacer::renderOnMainThread()
{
// Ignore this call for renderers that work on a dedicated render thread
if (m_RenderThread != nullptr) {
return;
}
m_FrameQueueLock.lock();
if (!m_RenderQueue.isEmpty()) {
// Releases m_FrameQueueLock
renderLastFrameAndUnlock();
}
else {
m_FrameQueueLock.unlock();
}
}
int Pacer::renderThread(void* context)
{
Pacer* me = reinterpret_cast<Pacer*>(context);
@@ -87,34 +105,64 @@ int Pacer::renderThread(void* context)
break;
}
// Dequeue the most recent frame for rendering and free the others.
// Render the latest frame and discard the others
// NB: m_FrameQueueLock still held here!
AVFrame* lastFrame = nullptr;
while (!me->m_RenderQueue.isEmpty()) {
if (lastFrame != nullptr) {
// Don't hold the frame queue lock across av_frame_free(),
// since it could need to talk to the GPU driver. This is safe
// because we're guaranteed that the queue will not shrink during
// this time (and so dequeue() below will always get something).
me->m_FrameQueueLock.unlock();
av_frame_free(&lastFrame);
me->m_VideoStats->pacerDroppedFrames++;
me->m_FrameQueueLock.lock();
}
lastFrame = me->m_RenderQueue.dequeue();
}
// Release the frame queue lock before rendering
me->m_FrameQueueLock.unlock();
// Render and free the mot current frame
me->renderFrame(lastFrame);
me->renderLastFrameAndUnlock();
}
// Send a null AVFrame to indicate end of stream on the render thread
me->m_VsyncRenderer->renderFrame(nullptr);
return 0;
}
void Pacer::enqueueFrameForRenderingAndUnlock(AVFrame *frame)
{
dropFrameForEnqueue(m_RenderQueue);
m_RenderQueue.enqueue(frame);
m_FrameQueueLock.unlock();
if (m_RenderThread != nullptr) {
m_RenderQueueNotEmpty.wakeOne();
}
else {
SDL_Event event;
// For main thread rendering, we'll push an event to trigger a callback
event.type = SDL_USEREVENT;
event.user.code = SDL_CODE_FRAME_READY;
SDL_PushEvent(&event);
}
}
// Caller must hold m_FrameQueueLock
void Pacer::renderLastFrameAndUnlock()
{
// Dequeue the most recent frame for rendering and free the others.
AVFrame* lastFrame = nullptr;
while (!m_RenderQueue.isEmpty()) {
if (lastFrame != nullptr) {
// Don't hold the frame queue lock across av_frame_free(),
// since it could need to talk to the GPU driver. This is safe
// because we're guaranteed that the queue will not shrink during
// this time (and so dequeue() below will always get something).
m_FrameQueueLock.unlock();
av_frame_free(&lastFrame);
m_VideoStats->pacerDroppedFrames++;
m_FrameQueueLock.lock();
}
lastFrame = m_RenderQueue.dequeue();
}
// Release the frame queue lock before rendering
m_FrameQueueLock.unlock();
// Render and free the mot current frame
renderFrame(lastFrame);
}
// Called in an arbitrary thread by the IVsyncSource on V-sync
// or an event synchronized with V-sync
void Pacer::vsyncCallback(int timeUntilNextVsyncMillis)
@@ -172,10 +220,7 @@ void Pacer::vsyncCallback(int timeUntilNextVsyncMillis)
}
// Place the first frame on the render queue
dropFrameForEnqueue(m_RenderQueue);
m_RenderQueue.enqueue(m_PacingQueue.dequeue());
m_FrameQueueLock.unlock();
m_RenderQueueNotEmpty.wakeOne();
enqueueFrameForRenderingAndUnlock(m_PacingQueue.dequeue());
}
bool Pacer::initialize(SDL_Window* window, int maxVideoFps, bool enablePacing)
@@ -188,9 +233,7 @@ bool Pacer::initialize(SDL_Window* window, int maxVideoFps, bool enablePacing)
"Frame pacing active: target %d Hz with %d FPS stream",
m_DisplayFps, m_MaxVideoFps);
#if defined(Q_OS_DARWIN)
m_VsyncSource = DisplayLinkVsyncSourceFactory::createVsyncSource(this);
#elif defined(Q_OS_WIN32)
#if defined(Q_OS_WIN32)
// Don't use D3DKMTWaitForVerticalBlankEvent() on Windows 7, because
// it blocks during other concurrent DX operations (like actually rendering).
if (IsWindows8OrGreater()) {
@@ -211,7 +254,9 @@ bool Pacer::initialize(SDL_Window* window, int maxVideoFps, bool enablePacing)
m_DisplayFps, m_MaxVideoFps);
}
m_RenderThread = SDL_CreateThread(Pacer::renderThread, "Pacer Render Thread", this);
if (m_VsyncRenderer->isRenderThreadSupported()) {
m_RenderThread = SDL_CreateThread(Pacer::renderThread, "PacerRender", this);
}
return true;
}
@@ -220,7 +265,7 @@ void Pacer::renderFrame(AVFrame* frame)
{
// Count time spent in Pacer's queues
Uint32 beforeRender = SDL_GetTicks();
m_VideoStats->totalPacerTime += beforeRender - frame->pts;
m_VideoStats->totalPacerTime += beforeRender - frame->pkt_dts;
// Render it
m_VsyncRenderer->renderFrame(frame);
@@ -283,16 +328,10 @@ void Pacer::submitFrame(AVFrame* frame)
if (m_VsyncSource != nullptr) {
dropFrameForEnqueue(m_PacingQueue);
m_PacingQueue.enqueue(frame);
}
else {
dropFrameForEnqueue(m_RenderQueue);
m_RenderQueue.enqueue(frame);
}
m_FrameQueueLock.unlock();
if (m_VsyncSource != nullptr) {
m_FrameQueueLock.unlock();
m_PacingQueueNotEmpty.wakeOne();
}
else {
m_RenderQueueNotEmpty.wakeOne();
enqueueFrameForRenderingAndUnlock(frame);
}
}
@@ -26,9 +26,15 @@ public:
void vsyncCallback(int timeUntilNextVsyncMillis);
void renderOnMainThread();
private:
static int renderThread(void* context);
void enqueueFrameForRenderingAndUnlock(AVFrame* frame);
void renderLastFrameAndUnlock();
void renderFrame(AVFrame* frame);
void dropFrameForEnqueue(QQueue<AVFrame*>& queue);
+90 -16
View File
@@ -2,34 +2,108 @@
#include <SDL.h>
#include "streaming/video/decoder.h"
#include "streaming/video/overlaymanager.h"
extern "C" {
#include <libavcodec/avcodec.h>
}
#ifdef HAVE_EGL
// SDL_egl.h have too many conflicts, we will do without
typedef void *EGLDisplay;
typedef void *EGLImage;
#define EGL_MAX_PLANES 4
class EGLExtensions {
public:
EGLExtensions(EGLDisplay dpy);
~EGLExtensions() {}
bool isSupported(const QString &extension) const;
private:
const QStringList m_Extensions;
};
#endif
#define RENDERER_ATTRIBUTE_FULLSCREEN_ONLY 0x01
#define RENDERER_ATTRIBUTE_1080P_MAX 0x02
class IFFmpegRenderer : public Overlay::IOverlayRenderer {
public:
enum FramePacingConstraint {
PACING_FORCE_OFF,
PACING_FORCE_ON,
PACING_ANY
};
virtual bool initialize(SDL_Window* window,
int videoFormat,
int width,
int height,
int maxFps,
bool enableVsync) = 0;
virtual bool prepareDecoderContext(AVCodecContext* context) = 0;
virtual bool initialize(PDECODER_PARAMETERS params) = 0;
virtual bool prepareDecoderContext(AVCodecContext* context, AVDictionary** options) = 0;
virtual void renderFrame(AVFrame* frame) = 0;
virtual bool needsTestFrame() = 0;
virtual int getDecoderCapabilities() = 0;
virtual FramePacingConstraint getFramePacingConstraint() = 0;
virtual bool needsTestFrame() {
// No test frame required by default
return false;
}
virtual int getDecoderCapabilities() {
// No special capabilities by default
return 0;
}
virtual int getRendererAttributes() {
// No special attributes by default
return 0;
}
virtual int getDecoderColorspace() {
// Rec 601 is default
return COLORSPACE_REC_601;
}
virtual bool isRenderThreadSupported() {
// Render thread is supported by default
return true;
}
virtual bool isDirectRenderingSupported() {
// The renderer can render directly to the display
return true;
}
virtual enum AVPixelFormat getPreferredPixelFormat(int videoFormat) {
if (videoFormat == VIDEO_FORMAT_H265_MAIN10) {
// 10-bit YUV 4:2:0
return AV_PIX_FMT_P010;
}
else {
// Planar YUV 4:2:0
return AV_PIX_FMT_YUV420P;
}
}
virtual bool isPixelFormatSupported(int videoFormat, enum AVPixelFormat pixelFormat) {
// By default, we only support the preferred pixel format
return getPreferredPixelFormat(videoFormat) == pixelFormat;
}
// IOverlayRenderer
virtual void notifyOverlayUpdated(Overlay::OverlayType) override {
// Nothing
}
#ifdef HAVE_EGL
// By default we can't do EGL
virtual bool canExportEGL() {
return false;
}
virtual bool initializeEGL(EGLDisplay,
const EGLExtensions &) {
return false;
}
virtual ssize_t exportEGLImages(AVFrame *,
EGLDisplay,
EGLImage[EGL_MAX_PLANES]) {
return -1;
}
// Free the ressources allocated during the last `exportEGLImages` call
virtual void freeEGLImages(EGLDisplay, EGLImage[EGL_MAX_PLANES]) {}
#endif
};
+205 -59
View File
@@ -1,6 +1,7 @@
#include "sdlvid.h"
#include "streaming/session.h"
#include "streaming/streamutils.h"
#include "path.h"
#include <QDir>
@@ -9,7 +10,9 @@
SdlRenderer::SdlRenderer()
: m_Renderer(nullptr),
m_Texture(nullptr)
m_Texture(nullptr),
m_SwPixelFormat(AV_PIX_FMT_NONE),
m_FontData(Path::readDataFile("ModeSeven.ttf"))
{
SDL_assert(TTF_WasInit() == 0);
if (TTF_Init() != 0) {
@@ -56,46 +59,28 @@ SdlRenderer::~SdlRenderer()
}
}
bool SdlRenderer::prepareDecoderContext(AVCodecContext*)
bool SdlRenderer::prepareDecoderContext(AVCodecContext*, AVDictionary**)
{
/* Nothing to do */
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Using SDL software renderer");
"Using SDL renderer");
return true;
}
bool SdlRenderer::needsTestFrame()
{
// This renderer should always work
return false;
}
int SdlRenderer::getDecoderCapabilities()
{
// The FFmpeg CPU decoder can handle reference frame invalidation,
// but only for H.264.
return CAPABILITY_REFERENCE_FRAME_INVALIDATION_AVC;
}
IFFmpegRenderer::FramePacingConstraint SdlRenderer::getFramePacingConstraint()
{
return PACING_ANY;
}
void SdlRenderer::notifyOverlayUpdated(Overlay::OverlayType type)
{
// Construct the required font to render the overlay
if (m_OverlayFonts[type] == nullptr) {
QByteArray fontData = Path::readDataFile("ModeSeven.ttf");
if (fontData.isEmpty()) {
if (m_FontData.isEmpty()) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Unable to load SDL overlay font");
"SDL overlay font failed to load");
return;
}
m_OverlayFonts[type] = TTF_OpenFontRW(SDL_RWFromConstMem(fontData.constData(), fontData.size()),
// m_FontData must stay around until the font is closed
m_OverlayFonts[type] = TTF_OpenFontRW(SDL_RWFromConstMem(m_FontData.constData(), m_FontData.size()),
1,
Session::get()->getOverlayManager().getOverlayFontSize(type));
if (m_OverlayFonts[type] == nullptr) {
@@ -125,20 +110,53 @@ void SdlRenderer::notifyOverlayUpdated(Overlay::OverlayType type)
}
}
bool SdlRenderer::initialize(SDL_Window* window,
int,
int width,
int height,
int,
bool enableVsync)
bool SdlRenderer::isRenderThreadSupported()
{
SDL_RendererInfo info;
SDL_GetRendererInfo(m_Renderer, &info);
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"SDL renderer backend: %s",
info.name);
if (info.name != QString("direct3d")) {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"SDL renderer backend requires main thread rendering");
return false;
}
return true;
}
bool SdlRenderer::isPixelFormatSupported(int, AVPixelFormat pixelFormat)
{
// Remember to keep this in sync with SdlRenderer::renderFrame()!
switch (pixelFormat)
{
case AV_PIX_FMT_YUV420P:
case AV_PIX_FMT_NV12:
case AV_PIX_FMT_NV21:
return true;
default:
return false;
}
}
bool SdlRenderer::initialize(PDECODER_PARAMETERS params)
{
Uint32 rendererFlags = SDL_RENDERER_ACCELERATED;
if ((SDL_GetWindowFlags(window) & SDL_WINDOW_FULLSCREEN_DESKTOP) == SDL_WINDOW_FULLSCREEN) {
if (params->videoFormat == VIDEO_FORMAT_H265_MAIN10) {
// SDL doesn't support rendering YUV 10-bit textures yet
return false;
}
if ((SDL_GetWindowFlags(params->window) & SDL_WINDOW_FULLSCREEN_DESKTOP) == SDL_WINDOW_FULLSCREEN) {
// In full-screen exclusive mode, we enable V-sync if requested. For other modes, Windows and Mac
// have compositors that make rendering tear-free. Linux compositor varies by distro and user
// configuration but doesn't seem feasible to detect here.
if (enableVsync) {
if (params->enableVsync) {
rendererFlags |= SDL_RENDERER_PRESENTVSYNC;
}
}
@@ -151,7 +169,7 @@ bool SdlRenderer::initialize(SDL_Window* window,
SDL_SetHintWithPriority(SDL_HINT_RENDER_DIRECT3D_THREADSAFE, "1", SDL_HINT_OVERRIDE);
#endif
m_Renderer = SDL_CreateRenderer(window, -1, rendererFlags);
m_Renderer = SDL_CreateRenderer(params->window, -1, rendererFlags);
if (!m_Renderer) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"SDL_CreateRenderer() failed: %s",
@@ -159,27 +177,32 @@ bool SdlRenderer::initialize(SDL_Window* window,
return false;
}
// The window may be smaller than the stream size, so ensure our
// logical rendering surface size is equal to the stream size
SDL_RenderSetLogicalSize(m_Renderer, width, height);
// SDL_CreateRenderer() can end up having to recreate our window (SDL_RecreateWindow())
// to ensure it's compatible with the renderer's OpenGL context. If that happens, we
// can get spurious SDL_WINDOWEVENT events that will cause us to (again) recreate our
// renderer. This can lead to an infinite to renderer recreation, so discard all
// SDL_WINDOWEVENT events after SDL_CreateRenderer().
SDL_PumpEvents();
SDL_FlushEvent(SDL_WINDOWEVENT);
// Calculate the video region size, scaling to fill the output size while
// preserving the aspect ratio of the video stream.
SDL_Rect src, dst;
src.x = src.y = 0;
src.w = params->width;
src.h = params->height;
dst.x = dst.y = 0;
SDL_GetRendererOutputSize(m_Renderer, &dst.w, &dst.h);
StreamUtils::scaleSourceToDestinationSurface(&src, &dst);
// Ensure the viewport is set to the desired video region
SDL_RenderSetViewport(m_Renderer, &dst);
// Draw a black frame until the video stream starts rendering
SDL_SetRenderDrawColor(m_Renderer, 0, 0, 0, SDL_ALPHA_OPAQUE);
SDL_RenderClear(m_Renderer);
SDL_RenderPresent(m_Renderer);
m_Texture = SDL_CreateTexture(m_Renderer,
SDL_PIXELFORMAT_YV12,
SDL_TEXTUREACCESS_STREAMING,
width,
height);
if (!m_Texture) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"SDL_CreateRenderer() failed: %s",
SDL_GetError());
return false;
}
#ifdef Q_OS_WIN32
// For some reason, using Direct3D9Ex breaks this with multi-monitor setups.
// When focus is lost, the window is minimized then immediately restored without
@@ -206,10 +229,10 @@ void SdlRenderer::renderOverlay(Overlay::OverlayType type)
if (type == Overlay::OverlayStatusUpdate) {
// Bottom Left
int unused;
SDL_RenderGetLogicalSize(m_Renderer, &unused, &m_OverlayRects[type].y);
SDL_Rect viewportRect;
SDL_RenderGetViewport(m_Renderer, &viewportRect);
m_OverlayRects[type].x = 0;
m_OverlayRects[type].y -= surface->h;
m_OverlayRects[type].y = viewportRect.h - surface->h;
}
else if (type == Overlay::OverlayDebug) {
// Top left
@@ -233,13 +256,131 @@ void SdlRenderer::renderOverlay(Overlay::OverlayType type)
void SdlRenderer::renderFrame(AVFrame* frame)
{
SDL_UpdateYUVTexture(m_Texture, nullptr,
frame->data[0],
frame->linesize[0],
frame->data[1],
frame->linesize[1],
frame->data[2],
frame->linesize[2]);
int err;
AVFrame* swFrame = nullptr;
if (frame == nullptr) {
// End of stream - nothing to do for us
return;
}
if (frame->hw_frames_ctx != nullptr) {
// If we are acting as the frontend for a hardware
// accelerated decoder, we'll need to read the frame
// back to render it.
// Find the native read-back format
if (m_SwPixelFormat == AV_PIX_FMT_NONE) {
auto hwFrameCtx = (AVHWFramesContext*)frame->hw_frames_ctx->data;
m_SwPixelFormat = hwFrameCtx->sw_format;
SDL_assert(m_SwPixelFormat != AV_PIX_FMT_NONE);
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Selected read-back format: %d",
m_SwPixelFormat);
}
swFrame = av_frame_alloc();
if (swFrame == nullptr) {
return;
}
swFrame->width = frame->width;
swFrame->height = frame->height;
swFrame->format = m_SwPixelFormat;
err = av_hwframe_transfer_data(swFrame, frame, 0);
if (err != 0) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"av_hwframe_transfer_data() failed: %d",
err);
goto Exit;
}
// av_hwframe_transfer_data() can nuke frame metadata,
// so anything other than width, height, and format must
// be set *after* calling av_hwframe_transfer_data().
swFrame->colorspace = frame->colorspace;
frame = swFrame;
}
if (m_Texture == nullptr) {
Uint32 sdlFormat;
// Remember to keep this in sync with SdlRenderer::isPixelFormatSupported()!
switch (frame->format)
{
case AV_PIX_FMT_YUV420P:
sdlFormat = SDL_PIXELFORMAT_YV12;
break;
case AV_PIX_FMT_NV12:
sdlFormat = SDL_PIXELFORMAT_NV12;
break;
case AV_PIX_FMT_NV21:
sdlFormat = SDL_PIXELFORMAT_NV21;
break;
default:
SDL_assert(false);
goto Exit;
}
switch (frame->colorspace)
{
case AVCOL_SPC_BT709:
SDL_SetYUVConversionMode(SDL_YUV_CONVERSION_BT709);
break;
case AVCOL_SPC_BT470BG:
case AVCOL_SPC_SMPTE170M:
default:
SDL_SetYUVConversionMode(SDL_YUV_CONVERSION_BT601);
break;
}
m_Texture = SDL_CreateTexture(m_Renderer,
sdlFormat,
SDL_TEXTUREACCESS_STREAMING,
frame->width,
frame->height);
if (!m_Texture) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"SDL_CreateTexture() failed: %s",
SDL_GetError());
goto Exit;
}
}
if (frame->format == AV_PIX_FMT_YUV420P) {
SDL_UpdateYUVTexture(m_Texture, nullptr,
frame->data[0],
frame->linesize[0],
frame->data[1],
frame->linesize[1],
frame->data[2],
frame->linesize[2]);
}
else {
char* pixels;
int pitch;
err = SDL_LockTexture(m_Texture, nullptr, (void**)&pixels, &pitch);
if (err < 0) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"SDL_LockTexture() failed: %s",
SDL_GetError());
goto Exit;
}
memcpy(pixels,
frame->data[0],
frame->linesize[0] * frame->height);
memcpy(pixels + (frame->linesize[0] * frame->height),
frame->data[1],
frame->linesize[1] * frame->height / 2);
SDL_UnlockTexture(m_Texture);
}
SDL_RenderClear(m_Renderer);
@@ -252,4 +393,9 @@ void SdlRenderer::renderFrame(AVFrame* frame)
}
SDL_RenderPresent(m_Renderer);
Exit:
if (swFrame != nullptr) {
av_frame_free(&swFrame);
}
}
+6 -10
View File
@@ -8,24 +8,20 @@ class SdlRenderer : public IFFmpegRenderer {
public:
SdlRenderer();
virtual ~SdlRenderer() override;
virtual bool initialize(SDL_Window* window,
int videoFormat,
int width,
int height,
int maxFps,
bool enableVsync) override;
virtual bool prepareDecoderContext(AVCodecContext* context) override;
virtual bool initialize(PDECODER_PARAMETERS params) override;
virtual bool prepareDecoderContext(AVCodecContext* context, AVDictionary** options) override;
virtual void renderFrame(AVFrame* frame) override;
virtual bool needsTestFrame() override;
virtual int getDecoderCapabilities() override;
virtual FramePacingConstraint getFramePacingConstraint() override;
virtual void notifyOverlayUpdated(Overlay::OverlayType) override;
virtual bool isRenderThreadSupported() override;
virtual bool isPixelFormatSupported(int videoFormat, enum AVPixelFormat pixelFormat) override;
private:
void renderOverlay(Overlay::OverlayType type);
SDL_Renderer* m_Renderer;
SDL_Texture* m_Texture;
int m_SwPixelFormat;
QByteArray m_FontData;
TTF_Font* m_OverlayFonts[Overlay::OverlayMax];
SDL_Surface* m_OverlaySurfaces[Overlay::OverlayMax];
SDL_Texture* m_OverlayTextures[Overlay::OverlayMax];
+443 -83
View File
@@ -1,14 +1,27 @@
#include <QString>
#include "vaapi.h"
#include "utils.h"
#include <streaming/streamutils.h>
#include <SDL_syswm.h>
#ifdef HAVE_EGL
#include <SDL_egl.h>
#endif
#include <unistd.h>
#include <fcntl.h>
VAAPIRenderer::VAAPIRenderer()
: m_HwContext(nullptr)
: m_HwContext(nullptr),
m_DrmFd(-1),
m_BlacklistedForDirectRendering(false)
{
#ifdef HAVE_EGL
m_PrimeDescriptor.num_layers = 0;
m_PrimeDescriptor.num_objects = 0;
m_EGLExtDmaBuf = false;
#endif
}
VAAPIRenderer::~VAAPIRenderer()
@@ -26,18 +39,17 @@ VAAPIRenderer::~VAAPIRenderer()
vaTerminate(display);
}
}
if (m_DrmFd != -1) {
close(m_DrmFd);
}
}
bool
VAAPIRenderer::initialize(SDL_Window* window, int, int width, int height, int, bool)
VADisplay
VAAPIRenderer::openDisplay(SDL_Window* window)
{
int err;
SDL_SysWMinfo info;
m_VideoWidth = width;
m_VideoHeight = height;
SDL_GetWindowSize(window, &m_DisplayWidth, &m_DisplayHeight);
VADisplay display;
SDL_VERSION(&info.version);
@@ -45,9 +57,94 @@ VAAPIRenderer::initialize(SDL_Window* window, int, int width, int height, int, b
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"SDL_GetWindowWMInfo() failed: %s",
SDL_GetError());
return false;
return nullptr;
}
m_WindowSystem = info.subsystem;
if (info.subsystem == SDL_SYSWM_X11) {
m_XWindow = info.info.x11.window;
#ifdef HAVE_LIBVA_X11
display = vaGetDisplay(info.info.x11.display);
if (display == nullptr) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Unable to open X11 display for VAAPI");
return nullptr;
}
#else
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Moonlight not compiled with VAAPI X11 support!");
return nullptr;
#endif
}
else if (info.subsystem == SDL_SYSWM_WAYLAND) {
#ifdef HAVE_LIBVA_WAYLAND
display = vaGetDisplayWl(info.info.wl.display);
if (display == nullptr) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Unable to open Wayland display for VAAPI");
return nullptr;
}
#else
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Moonlight not compiled with VAAPI Wayland support!");
return nullptr;
#endif
}
// TODO: Upstream a better solution for SDL_GetWindowWMInfo on KMSDRM
else if (strcmp(SDL_GetCurrentVideoDriver(), "KMSDRM") == 0) {
#ifdef HAVE_LIBVA_DRM
if (m_DrmFd < 0) {
const char* device = SDL_getenv("DRM_DEV");
if (device == nullptr) {
device = "/dev/dri/card0";
}
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Opening DRM device: %s",
device);
m_DrmFd = open(device, O_RDWR | O_CLOEXEC);
if (m_DrmFd < 0) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Failed to open DRM device: %d",
errno);
return nullptr;
}
}
display = vaGetDisplayDRM(m_DrmFd);
if (display == nullptr) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Unable to open DRM display for VAAPI");
return nullptr;
}
#else
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Moonlight not compiled with VAAPI DRM support!");
return nullptr;
#endif
}
else {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Unsupported VAAPI rendering subsystem: %d",
info.subsystem);
return nullptr;
}
return display;
}
bool
VAAPIRenderer::initialize(PDECODER_PARAMETERS params)
{
int err;
m_VideoWidth = params->width;
m_VideoHeight = params->height;
SDL_GetWindowSize(params->window, &m_DisplayWidth, &m_DisplayHeight);
m_HwContext = av_hwdevice_ctx_alloc(AV_HWDEVICE_TYPE_VAAPI);
if (!m_HwContext) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
@@ -58,48 +155,93 @@ VAAPIRenderer::initialize(SDL_Window* window, int, int width, int height, int, b
AVHWDeviceContext* deviceContext = (AVHWDeviceContext*)m_HwContext->data;
AVVAAPIDeviceContext* vaDeviceContext = (AVVAAPIDeviceContext*)deviceContext->hwctx;
m_WindowSystem = info.subsystem;
if (info.subsystem == SDL_SYSWM_X11) {
#ifdef HAVE_LIBVA_X11
m_XWindow = info.info.x11.window;
vaDeviceContext->display = vaGetDisplay(info.info.x11.display);
if (!vaDeviceContext->display) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Unable to open X11 display for VAAPI");
return false;
}
#else
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Moonlight not compiled with VAAPI X11 support!");
return false;
#endif
}
else if (info.subsystem == SDL_SYSWM_WAYLAND) {
#ifdef HAVE_LIBVA_WAYLAND
m_WaylandSurface = info.info.wl.surface;
m_WaylandDisplay = info.info.wl.display;
vaDeviceContext->display = vaGetDisplayWl(info.info.wl.display);
if (!vaDeviceContext->display) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Unable to open Wayland display for VAAPI");
return false;
}
#else
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Moonlight not compiled with VAAPI Wayland support!");
return false;
#endif
}
else {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Unsupported VAAPI rendering subsystem: %d",
info.subsystem);
vaDeviceContext->display = openDisplay(params->window);
if (vaDeviceContext->display == nullptr) {
// openDisplay() logs the error
return false;
}
int major, minor;
VAStatus status;
status = vaInitialize(vaDeviceContext->display, &major, &minor);
bool setPathVar = false;
for (;;) {
status = vaInitialize(vaDeviceContext->display, &major, &minor);
if (status != VA_STATUS_SUCCESS && qEnvironmentVariableIsEmpty("LIBVA_DRIVER_NAME")) {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Trying fallback VAAPI driver names");
// It would be nice to use vaSetDriverName() here, but there's no way to unset
// it and get back to the default driver selection logic once we've overridden
// the driver name using that API. As a result, we must use LIBVA_DRIVER_NAME.
if (status != VA_STATUS_SUCCESS) {
// The iHD driver supports newer hardware like Ice Lake and Comet Lake.
// It should be picked by default on those platforms, but that doesn't
// always seem to be the case for some reason.
qputenv("LIBVA_DRIVER_NAME", "iHD");
status = vaInitialize(vaDeviceContext->display, &major, &minor);
}
if (status != VA_STATUS_SUCCESS) {
// The Iris driver in Mesa 20.0 returns a bogus VA driver (iris_drv_video.so)
// even though the correct driver is still i965. If we hit this path, we'll
// explicitly try i965 to handle this case.
qputenv("LIBVA_DRIVER_NAME", "i965");
status = vaInitialize(vaDeviceContext->display, &major, &minor);
}
if (status != VA_STATUS_SUCCESS) {
// The RadeonSI driver is compatible with XWayland but can't be detected by libva
// so try it too if all else fails.
qputenv("LIBVA_DRIVER_NAME", "radeonsi");
status = vaInitialize(vaDeviceContext->display, &major, &minor);
}
if (status != VA_STATUS_SUCCESS) {
// Unset LIBVA_DRIVER_NAME if none of the drivers we tried worked. This ensures
// we will get a fresh start using the default driver selection behavior after
// setting LIBVA_DRIVERS_PATH in the code below.
qunsetenv("LIBVA_DRIVER_NAME");
}
}
if (status == VA_STATUS_SUCCESS) {
// Success!
break;
}
if (qEnvironmentVariableIsEmpty("LIBVA_DRIVERS_PATH")) {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Trying fallback VAAPI driver paths");
qputenv("LIBVA_DRIVERS_PATH",
#if Q_PROCESSOR_WORDSIZE == 8
"/usr/lib64/dri:" // Fedora x86_64
"/usr/lib64/va/drivers:" // Gentoo x86_64
#endif
"/usr/lib/dri:" // Arch i386 & x86_64, Fedora i386
"/usr/lib/va/drivers:" // Gentoo i386
#if defined(Q_PROCESSOR_X86_64)
"/usr/lib/x86_64-linux-gnu/dri:" // Ubuntu/Debian x86_64
#elif defined(Q_PROCESSOR_X86_32)
"/usr/lib/i386-linux-gnu/dri:" // Ubuntu/Debian i386
#endif
);
setPathVar = true;
}
else {
if (setPathVar) {
// Unset LIBVA_DRIVERS_PATH if we set it ourselves
// and we didn't find any working VAAPI drivers.
qunsetenv("LIBVA_DRIVERS_PATH");
}
// Give up
break;
}
}
if (status != VA_STATUS_SUCCESS) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Failed to initialize VAAPI: %d",
@@ -112,16 +254,17 @@ VAAPIRenderer::initialize(SDL_Window* window, int, int width, int height, int, b
major, minor);
const char* vendorString = vaQueryVendorString(vaDeviceContext->display);
QString vendorStr(vendorString);
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Driver: %s",
vendorString ? vendorString : "<unknown>");
// AMD's Gallium VAAPI driver has a nasty memory leak
// that causes memory to be leaked for each submitted frame.
// The Flatpak runtime has a VDPAU driver in place that works
// well, so use that instead on AMD systems.
if (vendorString && qgetenv("FORCE_VAAPI") != "1") {
QString vendorStr(vendorString);
// AMD's Gallium VAAPI driver has a nasty memory leak that causes memory
// to be leaked for each submitted frame. The Flatpak runtime has a VDPAU
// driver in place that works well, so use that instead on AMD systems. If
// we're using Wayland, we have no choice but to use VAAPI because VDPAU
// is only supported under X11 or XWayland.
if (qgetenv("FORCE_VAAPI") != "1" && !WMUtils::isRunningWayland()) {
if (vendorStr.contains("AMD", Qt::CaseInsensitive) ||
vendorStr.contains("Radeon", Qt::CaseInsensitive)) {
// Fail and let VDPAU pick this up
@@ -131,6 +274,12 @@ VAAPIRenderer::initialize(SDL_Window* window, int, int width, int height, int, b
}
}
if (WMUtils::isRunningWayland()) {
// The iHD VAAPI driver can initialize on XWayland but it crashes in
// vaPutSurface() so we must also not directly render on XWayland.
m_BlacklistedForDirectRendering = vendorStr.contains("iHD");
}
// This will populate the driver_quirks
err = av_hwdevice_ctx_init(m_HwContext);
if (err < 0) {
@@ -152,13 +301,13 @@ VAAPIRenderer::initialize(SDL_Window* window, int, int width, int height, int, b
}
bool
VAAPIRenderer::prepareDecoderContext(AVCodecContext* context)
VAAPIRenderer::prepareDecoderContext(AVCodecContext* context, AVDictionary**)
{
context->hw_device_ctx = av_buffer_ref(m_HwContext);
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Using VAAPI accelerated renderer on %s",
m_WindowSystem == SDL_SYSWM_X11 ? "X11" : "Wayland");
SDL_GetCurrentVideoDriver());
return true;
}
@@ -171,20 +320,63 @@ VAAPIRenderer::needsTestFrame()
return true;
}
int
VAAPIRenderer::getDecoderCapabilities()
bool
VAAPIRenderer::isDirectRenderingSupported()
{
return 0;
if (qgetenv("VAAPI_FORCE_DIRECT") == "1") {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Using direct rendering due to environment variable");
return true;
}
else if (qgetenv("VAAPI_FORCE_INDIRECT") == "1") {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Using indirect rendering due to environment variable");
return false;
}
// We only support direct rendering on X11 with VAEntrypointVideoProc support
if (m_WindowSystem != SDL_SYSWM_X11 || m_BlacklistedForDirectRendering) {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Using indirect rendering due to WM or blacklist");
return false;
}
AVHWDeviceContext* deviceContext = (AVHWDeviceContext*)m_HwContext->data;
AVVAAPIDeviceContext* vaDeviceContext = (AVVAAPIDeviceContext*)deviceContext->hwctx;
VAEntrypoint entrypoints[vaMaxNumEntrypoints(vaDeviceContext->display)];
int entrypointCount;
VAStatus status = vaQueryConfigEntrypoints(vaDeviceContext->display, VAProfileNone, entrypoints, &entrypointCount);
if (status == VA_STATUS_SUCCESS) {
for (int i = 0; i < entrypointCount; i++) {
// Without VAEntrypointVideoProc support, the driver will crash inside vaPutSurface()
if (entrypoints[i] == VAEntrypointVideoProc) {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Using direct rendering with VAEntrypointVideoProc");
return true;
}
}
}
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Using indirect rendering due to lack of VAEntrypointVideoProc");
return false;
}
IFFmpegRenderer::FramePacingConstraint VAAPIRenderer::getFramePacingConstraint()
int VAAPIRenderer::getDecoderColorspace()
{
return PACING_ANY;
// Gallium drivers don't support Rec 709 yet - https://gitlab.freedesktop.org/mesa/mesa/issues/1915
// Intel-vaapi-driver defaults to Rec 601 - https://github.com/intel/intel-vaapi-driver/blob/021bcb79d1bd873bbd9fbca55f40320344bab866/src/i965_output_dri.c#L186
return COLORSPACE_REC_601;
}
void
VAAPIRenderer::renderFrame(AVFrame* frame)
{
if (frame == nullptr) {
// End of stream - nothing to do for us
return;
}
VASurfaceID surface = (VASurfaceID)(uintptr_t)frame->data[3];
AVHWDeviceContext* deviceContext = (AVHWDeviceContext*)m_HwContext->data;
AVVAAPIDeviceContext* vaDeviceContext = (AVVAAPIDeviceContext*)deviceContext->hwctx;
@@ -201,6 +393,27 @@ VAAPIRenderer::renderFrame(AVFrame* frame)
if (m_WindowSystem == SDL_SYSWM_X11) {
#ifdef HAVE_LIBVA_X11
unsigned int flags = 0;
// NB: Not all VAAPI drivers respect these flags. Many drivers
// just ignore them and do the color conversion as Rec 601.
switch (frame->colorspace) {
case AVCOL_SPC_BT709:
flags |= VA_SRC_BT709;
break;
case AVCOL_SPC_BT470BG:
case AVCOL_SPC_SMPTE170M:
flags |= VA_SRC_BT601;
break;
case AVCOL_SPC_SMPTE240M:
flags |= VA_SRC_SMPTE_240;
break;
default:
// Unknown colorspace
SDL_assert(false);
break;
}
vaPutSurface(vaDeviceContext->display,
surface,
m_XWindow,
@@ -208,34 +421,181 @@ VAAPIRenderer::renderFrame(AVFrame* frame)
m_VideoWidth, m_VideoHeight,
dst.x, dst.y,
dst.w, dst.h,
NULL, 0, 0);
NULL, 0, flags);
#endif
}
else if (m_WindowSystem == SDL_SYSWM_WAYLAND) {
#ifdef HAVE_LIBVA_WAYLAND
struct wl_buffer* buffer;
VAStatus status;
status = vaGetSurfaceBufferWl(vaDeviceContext->display,
surface,
VA_FRAME_PICTURE,
&buffer);
if (status == VA_STATUS_SUCCESS) {
wl_surface_attach(m_WaylandSurface, buffer, 0, 0);
wl_surface_damage(m_WaylandSurface, dst.x, dst.y, dst.w, dst.h);
wl_display_flush(m_WaylandDisplay);
wl_surface_commit(m_WaylandSurface);
}
else {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"vaGetSurfaceBufferWl failed(): %d",
status);
}
#endif
// We don't support direct rendering on Wayland, so we should
// never get called there. Many common Wayland compositors don't
// support YUV surfaces, so direct rendering would fail.
SDL_assert(false);
}
else {
// We don't accept anything else in initialize().
SDL_assert(false);
}
}
#ifdef HAVE_EGL
// Ensure that vaExportSurfaceHandle() is supported by the VA-API driver
bool
VAAPIRenderer::canExportEGL() {
AVHWDeviceContext* deviceContext = (AVHWDeviceContext*)m_HwContext->data;
AVVAAPIDeviceContext* vaDeviceContext = (AVVAAPIDeviceContext*)deviceContext->hwctx;
VASurfaceID surfaceId;
VAStatus st;
VADRMPRIMESurfaceDescriptor descriptor;
VASurfaceAttrib attrs[2];
int attributeCount = 0;
// FFmpeg handles setting these quirk flags for us
if (!(vaDeviceContext->driver_quirks & AV_VAAPI_DRIVER_QUIRK_ATTRIB_MEMTYPE)) {
attrs[attributeCount].type = VASurfaceAttribMemoryType;
attrs[attributeCount].flags = VA_SURFACE_ATTRIB_SETTABLE;
attrs[attributeCount].value.type = VAGenericValueTypeInteger;
attrs[attributeCount].value.value.i = VA_SURFACE_ATTRIB_MEM_TYPE_VA;
attributeCount++;
}
// These attributes are required for i965 to create a surface that can
// be successfully exported via vaExportSurfaceHandle(). iHD doesn't
// need these, but it doesn't seem to hurt either.
attrs[attributeCount].type = VASurfaceAttribPixelFormat;
attrs[attributeCount].flags = VA_SURFACE_ATTRIB_SETTABLE;
attrs[attributeCount].value.type = VAGenericValueTypeInteger;
attrs[attributeCount].value.value.i = VA_FOURCC_NV12;
attributeCount++;
st = vaCreateSurfaces(vaDeviceContext->display,
VA_RT_FORMAT_YUV420,
1280,
720,
&surfaceId,
1,
attrs,
attributeCount);
if (st != VA_STATUS_SUCCESS) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"vaCreateSurfaces() failed: %d", st);
return false;
}
st = vaExportSurfaceHandle(vaDeviceContext->display,
surfaceId,
VA_SURFACE_ATTRIB_MEM_TYPE_DRM_PRIME_2,
VA_EXPORT_SURFACE_READ_ONLY | VA_EXPORT_SURFACE_SEPARATE_LAYERS,
&descriptor);
vaDestroySurfaces(vaDeviceContext->display, &surfaceId, 1);
if (st != VA_STATUS_SUCCESS) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"vaExportSurfaceHandle() failed: %d", st);
return false;
}
for (size_t i = 0; i < descriptor.num_objects; ++i) {
close(descriptor.objects[i].fd);
}
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"VAAPI driver supports exporting DRM PRIME surface handles");
return true;
}
bool
VAAPIRenderer::initializeEGL(EGLDisplay,
const EGLExtensions &ext) {
if (!ext.isSupported("EGL_EXT_image_dma_buf_import")) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"VAAPI-EGL: DMABUF unsupported");
return false;
}
m_EGLExtDmaBuf = ext.isSupported("EGL_EXT_image_dma_buf_import_modifiers");
return true;
}
ssize_t
VAAPIRenderer::exportEGLImages(AVFrame *frame, EGLDisplay dpy,
EGLImage images[EGL_MAX_PLANES]) {
ssize_t count = 0;
auto hwFrameCtx = (AVHWFramesContext*)frame->hw_frames_ctx->data;
AVVAAPIDeviceContext* vaDeviceContext = (AVVAAPIDeviceContext*)hwFrameCtx->device_ctx->hwctx;
VASurfaceID surface_id = (VASurfaceID)(uintptr_t)frame->data[3];
VAStatus st = vaExportSurfaceHandle(vaDeviceContext->display,
surface_id,
VA_SURFACE_ATTRIB_MEM_TYPE_DRM_PRIME_2,
VA_EXPORT_SURFACE_READ_ONLY | VA_EXPORT_SURFACE_SEPARATE_LAYERS,
&m_PrimeDescriptor);
if (st != VA_STATUS_SUCCESS) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"vaExportSurfaceHandle failed: %d", st);
return -1;
}
SDL_assert(m_PrimeDescriptor.num_layers <= EGL_MAX_PLANES);
st = vaSyncSurface(vaDeviceContext->display, surface_id);
if (st != VA_STATUS_SUCCESS) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"vaSyncSurface failed: %d", st);
goto sync_fail;
}
for (size_t i = 0; i < m_PrimeDescriptor.num_layers; ++i) {
const auto &layer = m_PrimeDescriptor.layers[i];
const auto &object = m_PrimeDescriptor.objects[layer.object_index[0]];
EGLAttrib attribs[17] = {
EGL_LINUX_DRM_FOURCC_EXT, (EGLint)layer.drm_format,
EGL_WIDTH, i == 0 ? frame->width : frame->width / 2,
EGL_HEIGHT, i == 0 ? frame->height : frame->height / 2,
EGL_DMA_BUF_PLANE0_FD_EXT, object.fd,
EGL_DMA_BUF_PLANE0_OFFSET_EXT, (EGLint)layer.offset[0],
EGL_DMA_BUF_PLANE0_PITCH_EXT, (EGLint)layer.pitch[0],
EGL_NONE,
};
if (m_EGLExtDmaBuf) {
const EGLAttrib extra[] = {
EGL_DMA_BUF_PLANE0_MODIFIER_LO_EXT,
(EGLint)object.drm_format_modifier,
EGL_DMA_BUF_PLANE0_MODIFIER_HI_EXT,
(EGLint)(object.drm_format_modifier >> 32),
EGL_NONE,
};
memcpy((void *)(&attribs[12]), (void *)extra, sizeof (extra));
}
images[i] = eglCreateImage(dpy, EGL_NO_CONTEXT,
EGL_LINUX_DMA_BUF_EXT,
nullptr, attribs);
if (!images[i]) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"eglCreateImage() Failed: %d", eglGetError());
goto create_image_fail;
}
++count;
}
return count;
create_image_fail:
m_PrimeDescriptor.num_layers = count;
sync_fail:
freeEGLImages(dpy, images);
return -1;
}
void
VAAPIRenderer::freeEGLImages(EGLDisplay dpy, EGLImage images[EGL_MAX_PLANES]) {
for (size_t i = 0; i < m_PrimeDescriptor.num_layers; ++i) {
eglDestroyImage(dpy, images[i]);
}
for (size_t i = 0; i < m_PrimeDescriptor.num_objects; ++i) {
close(m_PrimeDescriptor.objects[i].fd);
}
m_PrimeDescriptor.num_layers = 0;
m_PrimeDescriptor.num_objects = 0;
}
#endif
+28 -17
View File
@@ -22,41 +22,52 @@ extern "C" {
#ifdef HAVE_LIBVA_WAYLAND
#include <va/va_wayland.h>
#endif
#ifdef HAVE_LIBVA_DRM
#include <va/va_drm.h>
#endif
#include <libavutil/hwcontext_vaapi.h>
#ifdef HAVE_EGL
#include <va/va_drmcommon.h>
#endif
}
class VAAPIRenderer : public IFFmpegRenderer
{
public:
VAAPIRenderer();
virtual ~VAAPIRenderer();
virtual bool initialize(SDL_Window* window,
int videoFormat,
int width,
int height,
int maxFps,
bool enableVsync);
virtual bool prepareDecoderContext(AVCodecContext* context);
virtual void renderFrame(AVFrame* frame);
virtual bool needsTestFrame();
virtual int getDecoderCapabilities();
virtual FramePacingConstraint getFramePacingConstraint();
virtual ~VAAPIRenderer() override;
virtual bool initialize(PDECODER_PARAMETERS params) override;
virtual bool prepareDecoderContext(AVCodecContext* context, AVDictionary** options) override;
virtual void renderFrame(AVFrame* frame) override;
virtual bool needsTestFrame() override;
virtual bool isDirectRenderingSupported() override;
virtual int getDecoderColorspace() override;
#ifdef HAVE_EGL
virtual bool canExportEGL() override;
virtual bool initializeEGL(EGLDisplay dpy, const EGLExtensions &ext) override;
virtual ssize_t exportEGLImages(AVFrame *frame, EGLDisplay dpy, EGLImage images[EGL_MAX_PLANES]) override;
virtual void freeEGLImages(EGLDisplay dpy, EGLImage[EGL_MAX_PLANES]) override;
#endif
private:
VADisplay openDisplay(SDL_Window* window);
int m_WindowSystem;
AVBufferRef* m_HwContext;
int m_DrmFd;
bool m_BlacklistedForDirectRendering;
#ifdef HAVE_LIBVA_X11
Window m_XWindow;
#endif
#ifdef HAVE_LIBVA_WAYLAND
struct wl_surface* m_WaylandSurface;
struct wl_display* m_WaylandDisplay;
#endif
int m_VideoWidth;
int m_VideoHeight;
int m_DisplayWidth;
int m_DisplayHeight;
#ifdef HAVE_EGL
VADRMPRIMESurfaceDescriptor m_PrimeDescriptor;
bool m_EGLExtDmaBuf;
#endif
};
+70 -16
View File
@@ -12,11 +12,16 @@
#define GET_PROC_ADDRESS(id, func) status = vdpauCtx->get_proc_address(m_Device, id, (void**)func); \
BAIL_ON_FAIL(status, id)
const VdpRGBAFormat VDPAURenderer::k_OutputFormats[] = {
const VdpRGBAFormat VDPAURenderer::k_OutputFormats8Bit[] = {
VDP_RGBA_FORMAT_B8G8R8A8,
VDP_RGBA_FORMAT_R8G8B8A8
};
const VdpRGBAFormat VDPAURenderer::k_OutputFormats10Bit[] = {
VDP_RGBA_FORMAT_B10G10R10A2,
VDP_RGBA_FORMAT_R10G10B10A2
};
VDPAURenderer::VDPAURenderer()
: m_HwContext(nullptr),
m_PresentationQueueTarget(0),
@@ -54,18 +59,59 @@ VDPAURenderer::~VDPAURenderer()
}
}
bool VDPAURenderer::initialize(SDL_Window* window, int, int width, int height, int, bool)
bool VDPAURenderer::initialize(PDECODER_PARAMETERS params)
{
int err;
VdpStatus status;
SDL_SysWMinfo info;
static const char* driverPathsToTry[] = {
#if Q_PROCESSOR_WORDSIZE == 8
"/usr/lib64",
"/usr/lib64/vdpau", // Fedora x86_64
#endif
"/usr/lib",
"/usr/lib/vdpau", // Fedora i386
#if defined(Q_PROCESSOR_X86_64)
"/usr/lib/x86_64-linux-gnu",
"/usr/lib/x86_64-linux-gnu/vdpau", // Ubuntu/Debian x86_64
#elif defined(Q_PROCESSOR_X86_32)
"/usr/lib/i386-linux-gnu",
"/usr/lib/i386-linux-gnu/vdpau", // Ubuntu/Debian i386
#endif
};
m_VideoWidth = width;
m_VideoHeight = height;
m_VideoWidth = params->width;
m_VideoHeight = params->height;
err = av_hwdevice_ctx_create(&m_HwContext,
AV_HWDEVICE_TYPE_VDPAU,
nullptr, nullptr, 0);
if (err < 0 && qEnvironmentVariableIsEmpty("VDPAU_DRIVER_PATH")) {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Trying fallback VDPAU driver paths");
// Unlike libva, libvdpau doesn't support multiple paths in
// their VDPAU_DRIVER_PATH variable, so we must try them all
// one at a time.
for (auto& driverPath : driverPathsToTry) {
qputenv("VDPAU_DRIVER_PATH", driverPath);
err = av_hwdevice_ctx_create(&m_HwContext,
AV_HWDEVICE_TYPE_VDPAU,
nullptr, nullptr, 0);
if (err == 0) {
// Success!
break;
}
}
if (err < 0) {
// Unset VDPAU_DRIVER_PATH if we set it ourselves
// and we didn't find any working VDPAU drivers.
qunsetenv("VDPAU_DRIVER_PATH");
}
}
if (err < 0) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Failed to create VDPAU context: %d",
@@ -93,11 +139,11 @@ bool VDPAURenderer::initialize(SDL_Window* window, int, int width, int height, i
GET_PROC_ADDRESS(VDP_FUNC_ID_VIDEO_SURFACE_GET_PARAMETERS, &m_VdpVideoSurfaceGetParameters);
GET_PROC_ADDRESS(VDP_FUNC_ID_GET_INFORMATION_STRING, &m_VdpGetInformationString);
SDL_GetWindowSize(window, (int*)&m_DisplayWidth, (int*)&m_DisplayHeight);
SDL_GetWindowSize(params->window, (int*)&m_DisplayWidth, (int*)&m_DisplayHeight);
SDL_VERSION(&info.version);
if (!SDL_GetWindowWMInfo(window, &info)) {
if (!SDL_GetWindowWMInfo(params->window, &info)) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"SDL_GetWindowWMInfo() failed: %s",
SDL_GetError());
@@ -143,7 +189,11 @@ bool VDPAURenderer::initialize(SDL_Window* window, int, int width, int height, i
for (int i = 0; i < OUTPUT_SURFACE_FORMAT_COUNT; i++) {
VdpBool supported;
uint32_t maxWidth, maxHeight;
status = m_VdpOutputSurfaceQueryCapabilities(m_Device, k_OutputFormats[i],
VdpRGBAFormat candidateFormat =
params->videoFormat == VIDEO_FORMAT_H265_MAIN10 ?
k_OutputFormats10Bit[i] : k_OutputFormats8Bit[i];
status = m_VdpOutputSurfaceQueryCapabilities(m_Device, candidateFormat,
&supported, &maxWidth, &maxHeight);
if (status != VDP_STATUS_OK) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
@@ -154,7 +204,7 @@ bool VDPAURenderer::initialize(SDL_Window* window, int, int width, int height, i
if (supported) {
if (m_DisplayWidth <= maxWidth && m_DisplayHeight <= maxHeight) {
m_OutputSurfaceFormat = k_OutputFormats[i];
m_OutputSurfaceFormat = candidateFormat;
foundFormat = true;
break;
}
@@ -216,7 +266,7 @@ bool VDPAURenderer::initialize(SDL_Window* window, int, int width, int height, i
return true;
}
bool VDPAURenderer::prepareDecoderContext(AVCodecContext* context)
bool VDPAURenderer::prepareDecoderContext(AVCodecContext* context, AVDictionary**)
{
context->hw_device_ctx = av_buffer_ref(m_HwContext);
@@ -241,18 +291,22 @@ bool VDPAURenderer::needsTestFrame()
return true;
}
int VDPAURenderer::getDecoderCapabilities()
int VDPAURenderer::getDecoderColorspace()
{
return 0;
}
IFFmpegRenderer::FramePacingConstraint VDPAURenderer::getFramePacingConstraint()
{
return PACING_ANY;
// VDPAU defaults to Rec 601.
// https://http.download.nvidia.com/XFree86/vdpau/doxygen/html/group___vdp_video_mixer.html#ga65580813e9045d94b739ed2bb8b62b46
//
// AMD and Nvidia GPUs both correctly process Rec 601, so let's not try our luck using a non-default colorspace.
return COLORSPACE_REC_601;
}
void VDPAURenderer::renderFrame(AVFrame* frame)
{
if (frame == nullptr) {
// End of stream - nothing to do for us
return;
}
VdpStatus status;
VdpVideoSurface videoSurface = (VdpVideoSurface)(uintptr_t)frame->data[3];
+8 -13
View File
@@ -12,18 +12,12 @@ class VDPAURenderer : public IFFmpegRenderer
{
public:
VDPAURenderer();
virtual ~VDPAURenderer();
virtual bool initialize(SDL_Window* window,
int videoFormat,
int width,
int height,
int maxFps,
bool enableVsync);
virtual bool prepareDecoderContext(AVCodecContext* context);
virtual void renderFrame(AVFrame* frame);
virtual bool needsTestFrame();
virtual int getDecoderCapabilities();
virtual FramePacingConstraint getFramePacingConstraint();
virtual ~VDPAURenderer() override;
virtual bool initialize(PDECODER_PARAMETERS params) override;
virtual bool prepareDecoderContext(AVCodecContext* context, AVDictionary** options) override;
virtual void renderFrame(AVFrame* frame) override;
virtual bool needsTestFrame() override;
virtual int getDecoderColorspace() override;
private:
uint32_t m_VideoWidth, m_VideoHeight;
@@ -40,7 +34,8 @@ private:
int m_NextSurfaceIndex;
#define OUTPUT_SURFACE_FORMAT_COUNT 2
static const VdpRGBAFormat k_OutputFormats[OUTPUT_SURFACE_FORMAT_COUNT];
static const VdpRGBAFormat k_OutputFormats8Bit[OUTPUT_SURFACE_FORMAT_COUNT];
static const VdpRGBAFormat k_OutputFormats10Bit[OUTPUT_SURFACE_FORMAT_COUNT];
VdpGetErrorString* m_VdpGetErrorString;
VdpPresentationQueueTargetDestroy* m_VdpPresentationQueueTargetDestroy;
+275 -24
View File
@@ -14,6 +14,20 @@
#import <VideoToolbox/VideoToolbox.h>
#import <AVFoundation/AVFoundation.h>
#import <dispatch/dispatch.h>
#import <Metal/Metal.h>
@interface VTView : NSView
- (NSView *)hitTest:(NSPoint)point;
@end
@implementation VTView
- (NSView *)hitTest:(NSPoint)point {
Q_UNUSED(point);
return nil;
}
@end
class VTRenderer : public IFFmpegRenderer
{
@@ -22,13 +36,30 @@ public:
: m_HwContext(nullptr),
m_DisplayLayer(nullptr),
m_FormatDesc(nullptr),
m_StreamView(nullptr)
m_StreamView(nullptr),
m_DisplayLink(nullptr),
m_ColorSpace(nullptr),
m_VsyncMutex(nullptr),
m_VsyncPassed(nullptr)
{
SDL_zero(m_OverlayTextFields);
}
virtual ~VTRenderer() override
{
if (m_DisplayLink != nullptr) {
CVDisplayLinkStop(m_DisplayLink);
CVDisplayLinkRelease(m_DisplayLink);
}
if (m_VsyncPassed != nullptr) {
SDL_DestroyCond(m_VsyncPassed);
}
if (m_VsyncMutex != nullptr) {
SDL_DestroyMutex(m_VsyncMutex);
}
if (m_HwContext != nullptr) {
av_buffer_unref(&m_HwContext);
}
@@ -37,6 +68,10 @@ public:
CFRelease(m_FormatDesc);
}
if (m_ColorSpace != nullptr) {
CGColorSpaceRelease(m_ColorSpace);
}
for (int i = 0; i < Overlay::OverlayMax; i++) {
if (m_OverlayTextFields[i] != nullptr) {
[m_OverlayTextFields[i] removeFromSuperview];
@@ -48,9 +83,86 @@ public:
}
}
static
CVReturn
displayLinkOutputCallback(
CVDisplayLinkRef displayLink,
const CVTimeStamp* /* now */,
const CVTimeStamp* /* vsyncTime */,
CVOptionFlags,
CVOptionFlags*,
void *displayLinkContext)
{
auto me = reinterpret_cast<VTRenderer*>(displayLinkContext);
SDL_assert(displayLink == me->m_DisplayLink);
SDL_LockMutex(me->m_VsyncMutex);
SDL_CondSignal(me->m_VsyncPassed);
SDL_UnlockMutex(me->m_VsyncMutex);
return kCVReturnSuccess;
}
bool initializeVsyncCallback(SDL_SysWMinfo* info)
{
NSScreen* screen = [info->info.cocoa.window screen];
CVReturn status;
if (screen == nullptr) {
// Window not visible on any display, so use a
// CVDisplayLink that can work with all active displays.
// When we become visible, we'll recreate ourselves
// and associate with the new screen.
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"NSWindow is not visible on any display");
status = CVDisplayLinkCreateWithActiveCGDisplays(&m_DisplayLink);
}
else {
CGDirectDisplayID displayId = [[screen deviceDescription][@"NSScreenNumber"] unsignedIntValue];
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"NSWindow on display: %x",
displayId);
status = CVDisplayLinkCreateWithCGDisplay(displayId, &m_DisplayLink);
}
if (status != kCVReturnSuccess) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Failed to create CVDisplayLink: %d",
status);
return false;
}
status = CVDisplayLinkSetOutputCallback(m_DisplayLink, displayLinkOutputCallback, this);
if (status != kCVReturnSuccess) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"CVDisplayLinkSetOutputCallback() failed: %d",
status);
return false;
}
// The CVDisplayLink callback uses these, so we must initialize them before
// starting the callbacks.
m_VsyncMutex = SDL_CreateMutex();
m_VsyncPassed = SDL_CreateCond();
status = CVDisplayLinkStart(m_DisplayLink);
if (status != kCVReturnSuccess) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"CVDisplayLinkStart() failed: %d",
status);
return false;
}
return true;
}
// Caller frees frame after we return
virtual void renderFrame(AVFrame* frame) override
{
if (frame == nullptr) {
// End of stream - nothing to do for us
return;
}
OSStatus status;
CVPixelBufferRef pixBuf = reinterpret_cast<CVPixelBufferRef>(frame->data[3]);
@@ -65,6 +177,91 @@ public:
return;
}
// Strip the attachments added by VT. They are likely wrong.
CVBufferRemoveAllAttachments(pixBuf);
CVBufferSetAttachment(pixBuf,
kCVImageBufferChromaSubsamplingKey,
kCVImageBufferChromaSubsampling_420,
kCVAttachmentMode_ShouldPropagate);
switch (frame->color_primaries) {
case AVCOL_PRI_BT709:
CVBufferSetAttachment(pixBuf,
kCVImageBufferColorPrimariesKey,
kCVImageBufferColorPrimaries_ITU_R_709_2,
kCVAttachmentMode_ShouldPropagate);
break;
case AVCOL_PRI_SMPTE170M:
CVBufferSetAttachment(pixBuf,
kCVImageBufferColorPrimariesKey,
kCVImageBufferColorPrimaries_SMPTE_C,
kCVAttachmentMode_ShouldPropagate);
break;
case AVCOL_PRI_BT2020:
CVBufferSetAttachment(pixBuf,
kCVImageBufferColorPrimariesKey,
kCVImageBufferColorPrimaries_ITU_R_2020,
kCVAttachmentMode_ShouldPropagate);
break;
default:
break;
}
switch (frame->color_trc) {
case AVCOL_TRC_BT709:
case AVCOL_TRC_SMPTE170M:
CVBufferSetAttachment(pixBuf,
kCVImageBufferTransferFunctionKey,
kCVImageBufferTransferFunction_ITU_R_709_2,
kCVAttachmentMode_ShouldPropagate);
break;
case AVCOL_TRC_BT2020_10:
CVBufferSetAttachment(pixBuf,
kCVImageBufferTransferFunctionKey,
kCVImageBufferTransferFunction_ITU_R_2020,
kCVAttachmentMode_ShouldPropagate);
break;
default:
break;
}
switch (frame->colorspace) {
case AVCOL_SPC_BT709:
if (m_ColorSpace == nullptr) {
m_ColorSpace = CGColorSpaceCreateWithName(kCGColorSpaceITUR_709);
}
CVBufferSetAttachment(pixBuf,
kCVImageBufferYCbCrMatrixKey,
kCVImageBufferYCbCrMatrix_ITU_R_709_2,
kCVAttachmentMode_ShouldPropagate);
break;
case AVCOL_SPC_BT2020_NCL:
if (m_ColorSpace == nullptr) {
m_ColorSpace = CGColorSpaceCreateWithName(kCGColorSpaceITUR_2020);
}
CVBufferSetAttachment(pixBuf,
kCVImageBufferYCbCrMatrixKey,
kCVImageBufferYCbCrMatrix_ITU_R_2020,
kCVAttachmentMode_ShouldPropagate);
break;
case AVCOL_SPC_SMPTE170M:
if (m_ColorSpace == nullptr) {
m_ColorSpace = CGColorSpaceCreateWithName(kCGColorSpaceSRGB);
}
CVBufferSetAttachment(pixBuf,
kCVImageBufferYCbCrMatrixKey,
kCVImageBufferYCbCrMatrix_ITU_R_601_4,
kCVAttachmentMode_ShouldPropagate);
break;
default:
break;
}
if (m_ColorSpace != nullptr) {
CVBufferSetAttachment(pixBuf, kCVImageBufferCGColorSpaceKey, m_ColorSpace, kCVAttachmentMode_ShouldPropagate);
}
// If the format has changed or doesn't exist yet, construct it with the
// pixel buffer data
if (!m_FormatDesc || !CMVideoFormatDescriptionMatchesImageBuffer(m_FormatDesc, pixBuf)) {
@@ -104,18 +301,23 @@ public:
[m_DisplayLayer enqueueSampleBuffer:sampleBuffer];
CFRelease(sampleBuffer);
if (m_DisplayLink != nullptr) {
// Vsync is enabled, so wait for a swap before returning
SDL_LockMutex(m_VsyncMutex);
if (SDL_CondWaitTimeout(m_VsyncPassed, m_VsyncMutex, 100) == SDL_MUTEX_TIMEDOUT) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"V-sync wait timed out after 100 ms");
}
SDL_UnlockMutex(m_VsyncMutex);
}
}
virtual bool initialize(SDL_Window* window,
int videoFormat,
int,
int,
int,
bool) override
virtual bool initialize(PDECODER_PARAMETERS params) override
{
int err;
if (videoFormat & VIDEO_FORMAT_MASK_H264) {
if (params->videoFormat & VIDEO_FORMAT_MASK_H264) {
// Prior to 10.13, we'll just assume everything has
// H.264 support and fail open to allow VT decode.
#if __MAC_OS_X_VERSION_MAX_ALLOWED >= 101300
@@ -133,7 +335,7 @@ public:
"Assuming H.264 HW decode on < macOS 10.13");
}
}
else if (videoFormat & VIDEO_FORMAT_MASK_H265) {
else if (params->videoFormat & VIDEO_FORMAT_MASK_H265) {
#if __MAC_OS_X_VERSION_MAX_ALLOWED >= 101300
if (__builtin_available(macOS 10.13, *)) {
if (!VTIsHardwareDecodeSupported(kCMVideoCodecType_HEVC)) {
@@ -141,6 +343,52 @@ public:
"No HW accelerated HEVC decode via VT");
return false;
}
// HEVC Main10 requires more extensive checks because there's no
// simple API to check for Main10 hardware decoding, and if we don't
// have it, we'll silently get software decoding with horrible performance.
if (params->videoFormat == VIDEO_FORMAT_H265_MAIN10) {
#if __MAC_OS_X_VERSION_MAX_ALLOWED >= 101400
if (__builtin_available(macOS 10.14, *)) {
id<MTLDevice> device = MTLCreateSystemDefaultDevice();
if (device == nullptr) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"Unable to get default Metal device");
return false;
}
// Exclude all GPUs earlier than macOSGPUFamily2
// https://developer.apple.com/documentation/metal/mtlfeatureset/mtlfeatureset_macos_gpufamily2_v1
if ([device supportsFeatureSet:MTLFeatureSet_macOS_GPUFamily2_v1]) {
if ([device.name containsString:@"Intel"]) {
// 500-series Intel GPUs are Skylake and don't support Main10 hardware decoding
if ([device.name containsString:@" 5"]) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"No HEVC Main10 support on Skylake iGPU");
return false;
}
}
else if ([device.name containsString:@"AMD"]) {
// FirePro D, M200, and M300 series GPUs don't support Main10 hardware decoding
if ([device.name containsString:@"FirePro D"] ||
[device.name containsString:@" M2"] ||
[device.name containsString:@" M3"]) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"No HEVC Main10 support on AMD GPUs until Polaris");
return false;
}
}
}
}
else
#endif
{
// Fail closed for HEVC Main10 if we're not on 10.14+
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"No HEVC Main10 support on < macOS 10.14");
return false;
}
}
}
else
#endif
@@ -156,7 +404,7 @@ public:
SDL_VERSION(&info.version);
if (!SDL_GetWindowWMInfo(window, &info)) {
if (!SDL_GetWindowWMInfo(params->window, &info)) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"SDL_GetWindowWMInfo() failed: %s",
SDL_GetError());
@@ -168,7 +416,7 @@ public:
// SDL adds its own content view to listen for events.
// We need to add a subview for our display layer.
NSView* contentView = info.info.cocoa.window.contentView;
m_StreamView = [[NSView alloc] initWithFrame:contentView.bounds];
m_StreamView = [[VTView alloc] initWithFrame:contentView.bounds];
m_DisplayLayer = [[AVSampleBufferDisplayLayer alloc] init];
m_DisplayLayer.bounds = m_StreamView.bounds;
@@ -196,6 +444,12 @@ public:
return false;
}
if (params->enableVsync) {
if (!initializeVsyncCallback(&info)) {
return false;
}
}
return true;
}
@@ -211,10 +465,10 @@ public:
switch (type) {
case Overlay::OverlayDebug:
[m_OverlayTextFields[type] setAlignment:NSLeftTextAlignment];
[m_OverlayTextFields[type] setAlignment:NSTextAlignmentLeft];
break;
case Overlay::OverlayStatusUpdate:
[m_OverlayTextFields[type] setAlignment:NSRightTextAlignment];
[m_OverlayTextFields[type] setAlignment:NSTextAlignmentRight];
break;
default:
break;
@@ -264,7 +518,7 @@ public:
}
}
virtual bool prepareDecoderContext(AVCodecContext* context) override
virtual bool prepareDecoderContext(AVCodecContext* context, AVDictionary**) override
{
context->hw_device_ctx = av_buffer_ref(m_HwContext);
@@ -283,17 +537,10 @@ public:
return true;
}
virtual int getDecoderCapabilities() override
int getDecoderColorspace() override
{
return 0;
}
virtual IFFmpegRenderer::FramePacingConstraint getFramePacingConstraint() override
{
// This renderer is inherently tied to V-sync due how we're
// rendering with AVSampleBufferDisplay layer. Running without
// the V-Sync source leads to massive stuttering.
return PACING_FORCE_ON;
// macOS seems to handle Rec 601 best
return COLORSPACE_REC_601;
}
private:
@@ -302,6 +549,10 @@ private:
CMVideoFormatDescriptionRef m_FormatDesc;
NSView* m_StreamView;
NSTextField* m_OverlayTextFields[Overlay::OverlayMax];
CVDisplayLinkRef m_DisplayLink;
CGColorSpaceRef m_ColorSpace;
SDL_mutex* m_VsyncMutex;
SDL_cond* m_VsyncPassed;
};
IFFmpegRenderer* VTRendererFactory::createRenderer() {
+431 -125
View File
@@ -6,6 +6,7 @@
#include <h264_stream.h>
#include "ffmpeg-renderers/sdlvid.h"
#include "ffmpeg-renderers/cuda.h"
#ifdef Q_OS_WIN32
#include "ffmpeg-renderers/dxva2.h"
@@ -23,6 +24,18 @@
#include "ffmpeg-renderers/vdpau.h"
#endif
#ifdef HAVE_MMAL
#include "ffmpeg-renderers/mmal.h"
#endif
#ifdef HAVE_DRM
#include "ffmpeg-renderers/drm.h"
#endif
#ifdef HAVE_EGL
#include "ffmpeg-renderers/eglvid.h"
#endif
// This is gross but it allows us to use sizeof()
#include "ffmpeg_videosamples.cpp"
@@ -32,12 +45,45 @@
bool FFmpegVideoDecoder::isHardwareAccelerated()
{
return m_HwDecodeCfg != nullptr;
return m_HwDecodeCfg != nullptr ||
(m_VideoDecoderCtx->codec->capabilities & AV_CODEC_CAP_HARDWARE) != 0;
}
bool FFmpegVideoDecoder::isAlwaysFullScreen()
{
return m_BackendRenderer->getRendererAttributes() & RENDERER_ATTRIBUTE_FULLSCREEN_ONLY;
}
int FFmpegVideoDecoder::getDecoderCapabilities()
{
return m_Renderer->getDecoderCapabilities();
int capabilities = m_BackendRenderer->getDecoderCapabilities();
if (!isHardwareAccelerated()) {
// Slice up to 4 times for parallel CPU decoding, once slice per core
int slices = qMin(MAX_SLICES, SDL_GetCPUCount());
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Encoder configured for %d slices per frame",
slices);
capabilities |= CAPABILITY_SLICES_PER_FRAME(slices);
}
return capabilities;
}
int FFmpegVideoDecoder::getDecoderColorspace()
{
return m_FrontendRenderer->getDecoderColorspace();
}
QSize FFmpegVideoDecoder::getDecoderMaxResolution()
{
if (m_BackendRenderer->getRendererAttributes() & RENDERER_ATTRIBUTE_1080P_MAX) {
return QSize(1920, 1080);
}
else {
// No known maximum
return QSize(0, 0);
}
}
enum AVPixelFormat FFmpegVideoDecoder::ffGetFormat(AVCodecContext* context,
@@ -47,7 +93,7 @@ enum AVPixelFormat FFmpegVideoDecoder::ffGetFormat(AVCodecContext* context,
const enum AVPixelFormat *p;
for (p = pixFmts; *p != -1; p++) {
// Only match our hardware decoding codec or SW pixel
// Only match our hardware decoding codec or preferred SW pixel
// format (if not using hardware decoding). It's crucial
// to override the default get_format() which will try
// to gracefully fall back to software decode and break us.
@@ -58,6 +104,15 @@ enum AVPixelFormat FFmpegVideoDecoder::ffGetFormat(AVCodecContext* context,
}
}
// Failed to match the preferred pixel formats. Try non-preferred options for non-hwaccel decoders.
if (decoder->m_HwDecodeCfg == nullptr) {
for (p = pixFmts; *p != -1; p++) {
if (decoder->m_FrontendRenderer->isPixelFormatSupported(decoder->m_VideoFormat, *p)) {
return *p;
}
}
}
return AV_PIX_FMT_NONE;
}
@@ -65,11 +120,15 @@ FFmpegVideoDecoder::FFmpegVideoDecoder(bool testOnly)
: m_VideoDecoderCtx(nullptr),
m_DecodeBuffer(1024 * 1024, 0),
m_HwDecodeCfg(nullptr),
m_Renderer(nullptr),
m_BackendRenderer(nullptr),
m_FrontendRenderer(nullptr),
m_ConsecutiveFailedDecodes(0),
m_Pacer(nullptr),
m_FramesIn(0),
m_FramesOut(0),
m_LastFrameNumber(0),
m_StreamFps(0),
m_VideoFormat(0),
m_NeedsSpsFixup(false),
m_TestOnly(testOnly)
{
@@ -94,9 +153,9 @@ FFmpegVideoDecoder::~FFmpegVideoDecoder()
av_log_set_level(AV_LOG_INFO);
}
IFFmpegRenderer* FFmpegVideoDecoder::getRenderer()
IFFmpegRenderer* FFmpegVideoDecoder::getBackendRenderer()
{
return m_Renderer;
return m_BackendRenderer;
}
void FFmpegVideoDecoder::reset()
@@ -115,8 +174,14 @@ void FFmpegVideoDecoder::reset()
Session::get()->getOverlayManager().setOverlayRenderer(nullptr);
}
delete m_Renderer;
m_Renderer = nullptr;
// If we have a separate frontend renderer, free that first
if (m_FrontendRenderer != m_BackendRenderer) {
delete m_FrontendRenderer;
}
delete m_BackendRenderer;
m_FrontendRenderer = m_BackendRenderer = nullptr;
if (!m_TestOnly) {
logVideoStats(m_GlobalVideoStats, "Global video stats");
@@ -127,35 +192,50 @@ void FFmpegVideoDecoder::reset()
}
}
bool FFmpegVideoDecoder::completeInitialization(AVCodec* decoder, SDL_Window* window,
int videoFormat, int width, int height,
int maxFps, bool enableFramePacing, bool testFrame)
bool FFmpegVideoDecoder::createFrontendRenderer(PDECODER_PARAMETERS params)
{
if (m_BackendRenderer->isDirectRenderingSupported()) {
// The backend renderer can render to the display
m_FrontendRenderer = m_BackendRenderer;
}
else {
#ifdef HAVE_EGL
if (m_BackendRenderer->canExportEGL()) {
m_FrontendRenderer = new EGLRenderer(m_BackendRenderer);
if (m_FrontendRenderer->initialize(params)) {
return true;
}
delete m_FrontendRenderer;
}
#endif
// The backend renderer cannot directly render to the display, so
// we will create an SDL renderer to draw the frames.
m_FrontendRenderer = new SdlRenderer();
if (!m_FrontendRenderer->initialize(params)) {
return false;
}
}
return true;
}
bool FFmpegVideoDecoder::completeInitialization(AVCodec* decoder, PDECODER_PARAMETERS params, bool testFrame)
{
// In test-only mode, we should only see test frames
SDL_assert(!m_TestOnly || testFrame);
auto vsyncConstraint = m_Renderer->getFramePacingConstraint();
if (vsyncConstraint == IFFmpegRenderer::PACING_FORCE_OFF && enableFramePacing) {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Frame pacing is forcefully disabled by the active renderer");
enableFramePacing = false;
}
else if (vsyncConstraint == IFFmpegRenderer::PACING_FORCE_ON && !enableFramePacing) {
// FIXME: This duplicates logic in Session.cpp
int displayHz = StreamUtils::getDisplayRefreshRate(window);
if (displayHz + 5 >= maxFps) {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Frame pacing is forcefully enabled by the active renderer");
enableFramePacing = true;
}
// Create the frontend renderer based on the capabilities of the backend renderer
if (!createFrontendRenderer(params)) {
return false;
}
m_StreamFps = maxFps;
m_StreamFps = params->frameRate;
m_VideoFormat = params->videoFormat;
// Don't bother initializing Pacer if we're not actually going to render
if (!testFrame) {
m_Pacer = new Pacer(m_Renderer, &m_ActiveWndVideoStats);
if (!m_Pacer->initialize(window, maxFps, enableFramePacing)) {
m_Pacer = new Pacer(m_FrontendRenderer, &m_ActiveWndVideoStats);
if (!m_Pacer->initialize(params->window, params->frameRate, params->enableFramePacing)) {
return false;
}
}
@@ -174,8 +254,17 @@ bool FFmpegVideoDecoder::completeInitialization(AVCodec* decoder, SDL_Window* wi
m_VideoDecoderCtx->flags |= AV_CODEC_FLAG_OUTPUT_CORRUPT;
m_VideoDecoderCtx->flags2 |= AV_CODEC_FLAG2_SHOW_ALL;
// Report decoding errors to allow us to request a key frame
//
// With HEVC streams, FFmpeg can drop a frame (hwaccel->start_frame() fails)
// without telling us. Since we have an infinite GOP length, this causes artifacts
// on screen that persist for a long time. It's easy to cause this condition
// by using NVDEC and delaying 100 ms randomly in the render path so the decoder
// runs out of output buffers.
m_VideoDecoderCtx->err_recognition = AV_EF_EXPLODE;
// Enable slice multi-threading for software decoding
if (!m_HwDecodeCfg) {
if (!isHardwareAccelerated()) {
m_VideoDecoderCtx->thread_type = FF_THREAD_SLICE;
m_VideoDecoderCtx->thread_count = qMin(MAX_SLICES, SDL_GetCPUCount());
}
@@ -185,13 +274,15 @@ bool FFmpegVideoDecoder::completeInitialization(AVCodec* decoder, SDL_Window* wi
}
// Setup decoding parameters
m_VideoDecoderCtx->width = width;
m_VideoDecoderCtx->height = height;
m_VideoDecoderCtx->pix_fmt = AV_PIX_FMT_YUV420P; // FIXME: HDR
m_VideoDecoderCtx->width = params->width;
m_VideoDecoderCtx->height = params->height;
m_VideoDecoderCtx->pix_fmt = m_FrontendRenderer->getPreferredPixelFormat(params->videoFormat);
m_VideoDecoderCtx->get_format = ffGetFormat;
// Allow the renderer to attach data to this decoder
if (!m_Renderer->prepareDecoderContext(m_VideoDecoderCtx)) {
AVDictionary* options = nullptr;
// Allow the backend renderer to attach data to this decoder
if (!m_BackendRenderer->prepareDecoderContext(m_VideoDecoderCtx, &options)) {
return false;
}
@@ -202,11 +293,12 @@ bool FFmpegVideoDecoder::completeInitialization(AVCodec* decoder, SDL_Window* wi
SDL_assert(m_VideoDecoderCtx->opaque == nullptr);
m_VideoDecoderCtx->opaque = this;
int err = avcodec_open2(m_VideoDecoderCtx, decoder, nullptr);
int err = avcodec_open2(m_VideoDecoderCtx, decoder, &options);
av_dict_free(&options);
if (err < 0) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Unable to open decoder for format: %x",
videoFormat);
params->videoFormat);
return false;
}
@@ -216,16 +308,62 @@ bool FFmpegVideoDecoder::completeInitialization(AVCodec* decoder, SDL_Window* wi
// now to see if things will actually work when the video stream
// comes in.
if (testFrame) {
if (videoFormat & VIDEO_FORMAT_MASK_H264) {
switch (params->videoFormat) {
case VIDEO_FORMAT_H264:
m_Pkt.data = (uint8_t*)k_H264TestFrame;
m_Pkt.size = sizeof(k_H264TestFrame);
}
else {
m_Pkt.data = (uint8_t*)k_HEVCTestFrame;
m_Pkt.size = sizeof(k_HEVCTestFrame);
break;
case VIDEO_FORMAT_H265:
m_Pkt.data = (uint8_t*)k_HEVCMainTestFrame;
m_Pkt.size = sizeof(k_HEVCMainTestFrame);
break;
case VIDEO_FORMAT_H265_MAIN10:
m_Pkt.data = (uint8_t*)k_HEVCMain10TestFrame;
m_Pkt.size = sizeof(k_HEVCMain10TestFrame);
break;
default:
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"No test frame for format: %x",
params->videoFormat);
return false;
}
err = avcodec_send_packet(m_VideoDecoderCtx, &m_Pkt);
AVFrame* frame = av_frame_alloc();
if (!frame) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Failed to allocate frame");
return false;
}
// Some decoders won't output on the first frame, so we'll submit
// a few test frames if we get an EAGAIN error.
for (int retries = 0; retries < 5; retries++) {
// Most FFmpeg decoders process input using a "push" model.
// We'll see those fail here if the format is not supported.
err = avcodec_send_packet(m_VideoDecoderCtx, &m_Pkt);
if (err < 0) {
av_frame_free(&frame);
char errorstring[512];
av_strerror(err, errorstring, sizeof(errorstring));
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"Test decode failed: %s", errorstring);
return false;
}
// A few FFmpeg decoders (h264_mmal) process here using a "pull" model.
// Those decoders will fail here if the format is not supported.
err = avcodec_receive_frame(m_VideoDecoderCtx, frame);
if (err == AVERROR(EAGAIN)) {
// Wait a little while to let the hardware work
SDL_Delay(100);
}
else {
// Done!
break;
}
}
av_frame_free(&frame);
if (err < 0) {
char errorstring[512];
av_strerror(err, errorstring, sizeof(errorstring));
@@ -235,8 +373,8 @@ bool FFmpegVideoDecoder::completeInitialization(AVCodec* decoder, SDL_Window* wi
}
}
else {
if ((videoFormat & VIDEO_FORMAT_MASK_H264) &&
!(m_Renderer->getDecoderCapabilities() & CAPABILITY_REFERENCE_FRAME_INVALIDATION_AVC)) {
if ((params->videoFormat & VIDEO_FORMAT_MASK_H264) &&
!(m_BackendRenderer->getDecoderCapabilities() & CAPABILITY_REFERENCE_FRAME_INVALIDATION_AVC)) {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Using H.264 SPS fixup");
m_NeedsSpsFixup = true;
@@ -245,8 +383,8 @@ bool FFmpegVideoDecoder::completeInitialization(AVCodec* decoder, SDL_Window* wi
m_NeedsSpsFixup = false;
}
// Tell overlay manager to use this renderer
Session::get()->getOverlayManager().setOverlayRenderer(m_Renderer);
// Tell overlay manager to use this frontend renderer
Session::get()->getOverlayManager().setOverlayRenderer(m_FrontendRenderer);
}
return true;
@@ -331,54 +469,158 @@ void FFmpegVideoDecoder::logVideoStats(VIDEO_STATS& stats, const char* title)
}
}
IFFmpegRenderer* FFmpegVideoDecoder::createAcceleratedRenderer(const AVCodecHWConfig* hwDecodeCfg)
IFFmpegRenderer* FFmpegVideoDecoder::createHwAccelRenderer(const AVCodecHWConfig* hwDecodeCfg, int pass)
{
if (!(hwDecodeCfg->methods & AV_CODEC_HW_CONFIG_METHOD_HW_DEVICE_CTX)) {
return nullptr;
}
// Look for acceleration types we support
switch (hwDecodeCfg->device_type) {
// First pass using our top-tier hwaccel implementations
if (pass == 0) {
switch (hwDecodeCfg->device_type) {
#ifdef Q_OS_WIN32
case AV_HWDEVICE_TYPE_DXVA2:
return new DXVA2Renderer();
case AV_HWDEVICE_TYPE_DXVA2:
return new DXVA2Renderer();
#endif
#ifdef Q_OS_DARWIN
case AV_HWDEVICE_TYPE_VIDEOTOOLBOX:
return VTRendererFactory::createRenderer();
case AV_HWDEVICE_TYPE_VIDEOTOOLBOX:
return VTRendererFactory::createRenderer();
#endif
#ifdef HAVE_LIBVA
case AV_HWDEVICE_TYPE_VAAPI:
return new VAAPIRenderer();
case AV_HWDEVICE_TYPE_VAAPI:
return new VAAPIRenderer();
#endif
#ifdef HAVE_LIBVDPAU
case AV_HWDEVICE_TYPE_VDPAU:
return new VDPAURenderer();
case AV_HWDEVICE_TYPE_VDPAU:
return new VDPAURenderer();
#endif
default:
default:
return nullptr;
}
}
// Second pass for our second-tier hwaccel implementations
else if (pass == 1) {
switch (hwDecodeCfg->device_type) {
case AV_HWDEVICE_TYPE_CUDA:
// CUDA should only be used if all other options fail, since it requires
// read-back of frames. This should only be used for the NVIDIA+Wayland case
// with VDPAU covering the NVIDIA+X11 scenario.
return new CUDARenderer();
default:
return nullptr;
}
}
else {
SDL_assert(false);
return nullptr;
}
}
bool FFmpegVideoDecoder::initialize(
StreamingPreferences::VideoDecoderSelection vds,
SDL_Window* window,
int videoFormat,
int width,
int height,
int maxFps,
bool enableVsync,
bool enableFramePacing)
bool FFmpegVideoDecoder::tryInitializeRenderer(AVCodec* decoder,
PDECODER_PARAMETERS params,
const AVCodecHWConfig* hwConfig,
std::function<IFFmpegRenderer*()> createRendererFunc)
{
AVCodec* decoder;
m_BackendRenderer = createRendererFunc();
m_HwDecodeCfg = hwConfig;
if (m_BackendRenderer != nullptr &&
m_BackendRenderer->initialize(params) &&
completeInitialization(decoder, params, m_TestOnly || m_BackendRenderer->needsTestFrame())) {
if (m_TestOnly) {
// This decoder is only for testing capabilities, so don't bother
// creating a usable renderer
return true;
}
if (m_BackendRenderer->needsTestFrame()) {
// The test worked, so now let's initialize it for real
reset();
if ((m_BackendRenderer = createRendererFunc()) != nullptr &&
m_BackendRenderer->initialize(params) &&
completeInitialization(decoder, params, false)) {
return true;
}
else {
SDL_LogCritical(SDL_LOG_CATEGORY_APPLICATION,
"Decoder failed to initialize after successful test");
reset();
}
}
else {
// No test required. Good to go now.
return true;
}
}
else {
// Failed to initialize or test frame failed, so keep looking
reset();
}
return false;
}
bool FFmpegVideoDecoder::initialize(PDECODER_PARAMETERS params)
{
// Increase log level until the first frame is decoded
av_log_set_level(AV_LOG_DEBUG);
if (videoFormat & VIDEO_FORMAT_MASK_H264) {
// First try decoders that the user has manually specified via environment variables.
// These must output surfaces in one of the formats that the SDL renderer supports,
// which is currently:
// - AV_PIX_FMT_YUV420P (preferred)
// - AV_PIX_FMT_NV12
// - AV_PIX_FMT_NV21
// These formats should cover most/all decoders that output in a standard YUV format.
{
QString h264DecoderHint = qgetenv("H264_DECODER_HINT");
if (!h264DecoderHint.isEmpty() && (params->videoFormat & VIDEO_FORMAT_MASK_H264)) {
QByteArray decoderString = h264DecoderHint.toLocal8Bit();
AVCodec* customAvcDecoder = avcodec_find_decoder_by_name(decoderString.constData());
if (customAvcDecoder != nullptr &&
tryInitializeRenderer(customAvcDecoder, params, nullptr,
[]() -> IFFmpegRenderer* { return new SdlRenderer(); })) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"Using custom H.264 decoder (H264_DECODER_HINT): %s",
decoderString.constData());
return true;
}
else {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Custom H.264 decoder (H264_DECODER_HINT) failed to load: %s",
decoderString.constData());
}
}
}
{
QString hevcDecoderHint = qgetenv("HEVC_DECODER_HINT");
if (!hevcDecoderHint.isEmpty() && (params->videoFormat & VIDEO_FORMAT_MASK_H265)) {
QByteArray decoderString = hevcDecoderHint.toLocal8Bit();
AVCodec* customHevcDecoder = avcodec_find_decoder_by_name(decoderString.constData());
if (customHevcDecoder != nullptr &&
tryInitializeRenderer(customHevcDecoder, params, nullptr,
[]() -> IFFmpegRenderer* { return new SdlRenderer(); })) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"Using custom HEVC decoder (HEVC_DECODER_HINT): %s",
decoderString.constData());
return true;
}
else {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Custom HEVC decoder (HEVC_DECODER_HINT) failed to load: %s",
decoderString.constData());
}
}
}
AVCodec* decoder;
if (params->videoFormat & VIDEO_FORMAT_MASK_H264) {
decoder = avcodec_find_decoder(AV_CODEC_ID_H264);
}
else if (videoFormat & VIDEO_FORMAT_MASK_H265) {
else if (params->videoFormat & VIDEO_FORMAT_MASK_H265) {
decoder = avcodec_find_decoder(AV_CODEC_ID_HEVC);
}
else {
@@ -389,67 +631,125 @@ bool FFmpegVideoDecoder::initialize(
if (!decoder) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Unable to find decoder for format: %x",
videoFormat);
params->videoFormat);
return false;
}
for (int i = 0;; i++) {
const AVCodecHWConfig *config = avcodec_get_hw_config(decoder, i);
if (!config || vds == StreamingPreferences::VDS_FORCE_SOFTWARE) {
// No matching hardware acceleration support.
// This is not an error.
m_HwDecodeCfg = nullptr;
m_Renderer = new SdlRenderer();
if (vds != StreamingPreferences::VDS_FORCE_HARDWARE &&
m_Renderer->initialize(window, videoFormat, width, height, maxFps, enableVsync) &&
completeInitialization(decoder, window, videoFormat, width, height, maxFps, enableFramePacing, m_TestOnly)) {
// Look for a hardware decoder first unless software-only
if (params->vds != StreamingPreferences::VDS_FORCE_SOFTWARE) {
// Look for the first matching hwaccel hardware decoder (pass 0)
for (int i = 0;; i++) {
const AVCodecHWConfig *config = avcodec_get_hw_config(decoder, i);
if (!config) {
// No remaing hwaccel options
break;
}
// Initialize the hardware codec and submit a test frame if the renderer needs it
if (tryInitializeRenderer(decoder, params, config,
[config]() -> IFFmpegRenderer* { return createHwAccelRenderer(config, 0); })) {
return true;
}
}
// Continue with special non-hwaccel hardware decoders
#ifdef HAVE_MMAL
// MMAL is the decoder for the Raspberry Pi
if (params->videoFormat & VIDEO_FORMAT_MASK_H264) {
AVCodec* mmalDecoder = avcodec_find_decoder_by_name("h264_mmal");
if (mmalDecoder != nullptr &&
tryInitializeRenderer(mmalDecoder, params, nullptr,
[]() -> IFFmpegRenderer* { return new MmalRenderer(); })) {
return true;
}
}
#endif
#ifdef HAVE_DRM
{
// RKMPP is a hardware accelerated decoder that outputs DRI PRIME buffers
AVCodec* rkmppDecoder;
if (params->videoFormat & VIDEO_FORMAT_MASK_H264) {
rkmppDecoder = avcodec_find_decoder_by_name("h264_rkmpp");
}
else {
reset();
return false;
rkmppDecoder = avcodec_find_decoder_by_name("hevc_rkmpp");
}
}
m_Renderer = createAcceleratedRenderer(config);
if (!m_Renderer) {
continue;
}
m_HwDecodeCfg = config;
// Initialize the hardware codec and submit a test frame if the renderer needs it
if (m_Renderer->initialize(window, videoFormat, width, height, maxFps, enableVsync) &&
completeInitialization(decoder, window, videoFormat, width, height, maxFps, enableFramePacing, m_TestOnly || m_Renderer->needsTestFrame())) {
if (m_TestOnly) {
// This decoder is only for testing capabilities, so don't bother
// creating a usable renderer
if (rkmppDecoder != nullptr &&
tryInitializeRenderer(rkmppDecoder, params, nullptr,
[]() -> IFFmpegRenderer* { return new DrmRenderer(); })) {
return true;
}
}
#endif
if (m_Renderer->needsTestFrame()) {
// The test worked, so now let's initialize it for real
reset();
if ((m_Renderer = createAcceleratedRenderer(config)) != nullptr &&
m_Renderer->initialize(window, videoFormat, width, height, maxFps, enableVsync) &&
completeInitialization(decoder, window, videoFormat, width, height, maxFps, enableFramePacing, false)) {
return true;
}
else {
SDL_LogCritical(SDL_LOG_CATEGORY_APPLICATION,
"Decoder failed to initialize after successful test");
reset();
}
#ifdef Q_OS_LINUX
{
AVCodec* nvmpiDecoder;
if (params->videoFormat & VIDEO_FORMAT_MASK_H264) {
nvmpiDecoder = avcodec_find_decoder_by_name("h264_nvmpi");
}
else {
// No test required. Good to go now.
nvmpiDecoder = avcodec_find_decoder_by_name("hevc_nvmpi");
}
if (nvmpiDecoder != nullptr &&
tryInitializeRenderer(nvmpiDecoder, params, nullptr,
[]() -> IFFmpegRenderer* { return new SdlRenderer(); })) {
return true;
}
}
else {
// Failed to initialize or test frame failed, so keep looking
reset();
{
AVCodec* v4l2Decoder;
if (params->videoFormat & VIDEO_FORMAT_MASK_H264) {
v4l2Decoder = avcodec_find_decoder_by_name("h264_v4l2m2m");
}
else {
v4l2Decoder = avcodec_find_decoder_by_name("hevc_v4l2m2m");
}
if (v4l2Decoder != nullptr &&
tryInitializeRenderer(v4l2Decoder, params, nullptr,
[]() -> IFFmpegRenderer* { return new SdlRenderer(); })) {
return true;
}
}
#endif
// Look for the first matching hwaccel hardware decoder (pass 1)
// This picks up "second-tier" hwaccels like CUDA.
for (int i = 0;; i++) {
const AVCodecHWConfig *config = avcodec_get_hw_config(decoder, i);
if (!config) {
// No remaing hwaccel options
break;
}
// Initialize the hardware codec and submit a test frame if the renderer needs it
if (tryInitializeRenderer(decoder, params, config,
[config]() -> IFFmpegRenderer* { return createHwAccelRenderer(config, 1); })) {
return true;
}
}
}
// Fallback to software if no matching hardware decoder was found
// and if software fallback is allowed
if (params->vds != StreamingPreferences::VDS_FORCE_HARDWARE) {
if (tryInitializeRenderer(decoder, params, nullptr,
[]() -> IFFmpegRenderer* { return new SdlRenderer(); })) {
return true;
}
}
// No decoder worked
return false;
}
void FFmpegVideoDecoder::writeBuffer(PLENTRY entry, int& offset)
@@ -542,9 +842,9 @@ int FFmpegVideoDecoder::submitDecodeUnit(PDECODE_UNIT du)
// Add some extra space in case we need to do an SPS fixup
requiredBufferSize += MAX_SPS_EXTRA_SIZE;
}
if (requiredBufferSize + AV_INPUT_BUFFER_PADDING_SIZE > m_DecodeBuffer.length()) {
m_DecodeBuffer = QByteArray(requiredBufferSize + AV_INPUT_BUFFER_PADDING_SIZE, 0);
}
// Ensure the decoder buffer is large enough
m_DecodeBuffer.reserve(requiredBufferSize + AV_INPUT_BUFFER_PADDING_SIZE);
int offset = 0;
while (entry != nullptr) {
@@ -581,6 +881,8 @@ int FFmpegVideoDecoder::submitDecodeUnit(PDECODE_UNIT du)
return DR_NEED_IDR;
}
m_FramesIn++;
AVFrame* frame = av_frame_alloc();
if (!frame) {
// Failed to allocate a frame but we did submit,
@@ -592,18 +894,28 @@ int FFmpegVideoDecoder::submitDecodeUnit(PDECODE_UNIT du)
err = avcodec_receive_frame(m_VideoDecoderCtx, frame);
if (err == 0) {
m_FramesOut++;
// Reset failed decodes count if we reached this far
m_ConsecutiveFailedDecodes = 0;
// Restore default log level after a successful decode
av_log_set_level(AV_LOG_INFO);
// Store the presentation time
frame->pts = du->presentationTimeMs;
// Capture a frame timestamp to measuring pacing delay
frame->pts = SDL_GetTicks();
frame->pkt_dts = SDL_GetTicks();
// Count time in avcodec_send_packet() and avcodec_receive_frame()
// as time spent decoding
m_ActiveWndVideoStats.totalDecodeTime += SDL_GetTicks() - beforeDecode;
// Also count the frame-to-frame delay if the decoder is delaying frames
// until a subsequent frame is submitted.
m_ActiveWndVideoStats.totalDecodeTime += (m_FramesIn - m_FramesOut) * (1000 / m_StreamFps);
m_ActiveWndVideoStats.decodedFrames++;
// Queue the frame for rendering (or render now if pacer is disabled)
@@ -630,14 +942,8 @@ int FFmpegVideoDecoder::submitDecodeUnit(PDECODE_UNIT du)
return DR_OK;
}
// Called on main thread
void FFmpegVideoDecoder::renderFrame(SDL_UserEvent*)
void FFmpegVideoDecoder::renderFrameOnMainThread()
{
SDL_assert(false);
m_Pacer->renderOnMainThread();
}
// Called on main thread
void FFmpegVideoDecoder::dropFrame(SDL_UserEvent*)
{
SDL_assert(false);
}
+25 -17
View File
@@ -1,5 +1,7 @@
#pragma once
#include <functional>
#include "decoder.h"
#include "ffmpeg-renderers/renderer.h"
#include "ffmpeg-renderers/pacer/pacer.h"
@@ -12,26 +14,19 @@ class FFmpegVideoDecoder : public IVideoDecoder {
public:
FFmpegVideoDecoder(bool testOnly);
virtual ~FFmpegVideoDecoder() override;
virtual bool initialize(StreamingPreferences::VideoDecoderSelection vds,
SDL_Window* window,
int videoFormat,
int width,
int height,
int maxFps,
bool enableVsync,
bool enableFramePacing) override;
virtual bool initialize(PDECODER_PARAMETERS params) override;
virtual bool isHardwareAccelerated() override;
virtual bool isAlwaysFullScreen() override;
virtual int getDecoderCapabilities() override;
virtual int getDecoderColorspace() override;
virtual QSize getDecoderMaxResolution() override;
virtual int submitDecodeUnit(PDECODE_UNIT du) override;
virtual void renderFrame(SDL_UserEvent* event) override;
virtual void dropFrame(SDL_UserEvent* event) override;
virtual void renderFrameOnMainThread() override;
virtual IFFmpegRenderer* getRenderer();
virtual IFFmpegRenderer* getBackendRenderer();
private:
bool completeInitialization(AVCodec* decoder, SDL_Window* window,
int videoFormat, int width, int height,
int maxFps, bool enableFramePacing, bool testFrame);
bool completeInitialization(AVCodec* decoder, PDECODER_PARAMETERS params, bool testFrame);
void stringifyVideoStats(VIDEO_STATS& stats, char* output);
@@ -39,7 +34,14 @@ private:
void addVideoStats(VIDEO_STATS& src, VIDEO_STATS& dst);
IFFmpegRenderer* createAcceleratedRenderer(const AVCodecHWConfig* hwDecodeCfg);
bool createFrontendRenderer(PDECODER_PARAMETERS params);
bool tryInitializeRenderer(AVCodec* decoder,
PDECODER_PARAMETERS params,
const AVCodecHWConfig* hwConfig,
std::function<IFFmpegRenderer*()> createRendererFunc);
static IFFmpegRenderer* createHwAccelRenderer(const AVCodecHWConfig* hwDecodeCfg, int pass);
void reset();
@@ -53,18 +55,24 @@ private:
AVCodecContext* m_VideoDecoderCtx;
QByteArray m_DecodeBuffer;
const AVCodecHWConfig* m_HwDecodeCfg;
IFFmpegRenderer* m_Renderer;
IFFmpegRenderer* m_BackendRenderer;
IFFmpegRenderer* m_FrontendRenderer;
int m_ConsecutiveFailedDecodes;
Pacer* m_Pacer;
VIDEO_STATS m_ActiveWndVideoStats;
VIDEO_STATS m_LastWndVideoStats;
VIDEO_STATS m_GlobalVideoStats;
int m_FramesIn;
int m_FramesOut;
int m_LastFrameNumber;
int m_StreamFps;
int m_VideoFormat;
bool m_NeedsSpsFixup;
bool m_TestOnly;
static const uint8_t k_H264TestFrame[];
static const uint8_t k_HEVCTestFrame[];
static const uint8_t k_HEVCMainTestFrame[];
static const uint8_t k_HEVCMain10TestFrame[];
};

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