Compare commits

..
206 Commits
Author SHA1 Message Date
Cameron Gutman 6c523570e5 Prepare for v3.1.4 2021-06-25 20:39:30 -05:00
Cameron Gutman 047e5bdd81 Update AppImage to libva 2.12 2021-06-24 21:47:01 -05:00
Cameron Gutman 6b1559d397 Update moonlight-common-c to fix audio problems with old GFE and Sunshine versions 2021-06-21 19:26:08 -05:00
Cameron Gutman cbeb23f16a Adjust Steam Link icon size to 116x116 2021-06-13 16:53:03 -05:00
Cameron Gutman 1af2742013 Update moonlight-common-c to fix spurious exit of fast audio recovery mode on startup 2021-06-13 11:20:28 -05:00
Cameron Gutman 8048938158 Fix Linux build 2021-06-12 16:12:46 -05:00
Cameron Gutman 3689c7f982 Don't enable input capture if the window can't be foregrounded 2021-06-12 13:59:13 -05:00
Cameron Gutman 96c7057fee Update moonlight-common-c to avoid qwave.dll dependency 2021-06-12 12:21:55 -05:00
Cameron Gutman df7c4c82a9 Update moonlight-common-c with performance optimizations 2021-06-09 23:23:47 -05:00
Cameron Gutman 18d8db338b Update moonlight-common-c 2021-06-08 20:19:39 -05:00
Cameron Gutman 9637639e31 Update moonlight-common-c with audio FEC support 2021-06-04 18:46:36 -05:00
Cameron Gutman badaaece04 Update moonlight-common-c 2021-05-25 20:39:53 -05:00
Cameron Gutman 56119eebae Adapt to constification of AVCodec in FFmpeg master 2021-05-24 23:27:31 -05:00
Cameron Gutman c17c8e2a2c Remove receive time from performance overlay
It is largely superseded by network latency
2021-05-22 13:59:14 -05:00
Cameron Gutman e45a60f2ed Save RTT in VIDEO_STATS so it can be logged on disconnect 2021-05-22 13:57:12 -05:00
Cameron Gutman ebe9356937 Integrate Thai and Korean translations 2021-05-22 13:44:45 -05:00
Cameron Gutman 0094305299 Merge remote-tracking branch 'origin/weblate' 2021-05-22 13:33:20 -05:00
NicolasS b27050d3bf Translated using Weblate (French)
Currently translated at 100.0% (186 of 186 strings)

Translation: Moonlight Game Streaming/moonlight-qt
Translate-URL: https://hosted.weblate.org/projects/moonlight/moonlight-qt/fr/
2021-05-22 18:32:19 +02:00
Jorys Paulin d941e596ad Translated using Weblate (French)
Currently translated at 100.0% (186 of 186 strings)

Translation: Moonlight Game Streaming/moonlight-qt
Translate-URL: https://hosted.weblate.org/projects/moonlight/moonlight-qt/fr/
2021-05-22 18:32:19 +02:00
sanhoe ddaec92da5 Translated using Weblate (Korean)
Currently translated at 100.0% (186 of 186 strings)

Translation: Moonlight Game Streaming/moonlight-qt
Translate-URL: https://hosted.weblate.org/projects/moonlight/moonlight-qt/ko/
2021-05-21 16:14:30 +02:00
sanhoe 1c0a4c92a8 Translated using Weblate (Korean)
Currently translated at 100.0% (186 of 186 strings)

Translation: Moonlight Game Streaming/moonlight-qt
Translate-URL: https://hosted.weblate.org/projects/moonlight/moonlight-qt/ko/
2021-05-20 21:45:31 +02:00
sanhoe 383c00afbd Added translation using Weblate (Korean) 2021-05-20 16:32:05 +02:00
Krit789 d3b2bff8ad Translated using Weblate (Thai)
Currently translated at 41.9% (78 of 186 strings)

Translation: Moonlight Game Streaming/moonlight-qt
Translate-URL: https://hosted.weblate.org/projects/moonlight/moonlight-qt/th/
2021-05-18 00:32:49 +02:00
Cameron Gutman 77aaa6141d Update moonlight-common-c to avoid excessive ENet retransmissions when RTT variance is 0 2021-05-16 15:40:38 -05:00
Krit789 2bb1af61d0 Added translation using Weblate (Thai) 2021-05-16 21:58:44 +02:00
Cameron Gutman 40b29472b3 Update moonlight-common-c with more accurate RTTs, minRequiredFecPackets, and input queue draining 2021-05-16 14:46:57 -05:00
Cameron Gutman 5d5029de59 Increase limit for the --fps option 2021-05-16 14:45:53 -05:00
Cameron Gutman cc96e6a222 Rebase AppImage on Ubuntu Bionic and Qt 5.15.2 2021-05-16 12:45:34 -05:00
Cameron Gutman 581c9438ca Add network latency to stats overlay 2021-05-15 14:40:31 -05:00
Cameron Gutman 54bd427f26 Integrate Japanese and Vietnamese translations 2021-05-13 00:24:00 -05:00
Cameron Gutman c55d184195 Rerun lupdate-pro and lrelease-pro 2021-05-13 00:13:58 -05:00
Cameron Gutman b279f94789 Merge remote-tracking branch 'origin/weblate' 2021-05-13 00:06:47 -05:00
bruh f3a897e99a Translated using Weblate (Vietnamese)
Currently translated at 100.0% (186 of 186 strings)

Translation: Moonlight Game Streaming/moonlight-qt
Translate-URL: https://hosted.weblate.org/projects/moonlight/moonlight-qt/vi/
2021-05-13 07:04:50 +02:00
bruh 392b1b5904 Added translation using Weblate (Vietnamese) 2021-05-12 07:36:31 +02:00
grtw2116 4daa8066a6 Translated using Weblate (Japanese)
Currently translated at 100.0% (186 of 186 strings)

Translation: Moonlight Game Streaming/moonlight-qt
Translate-URL: https://hosted.weblate.org/projects/moonlight/moonlight-qt/ja/
2021-05-10 07:33:45 +02:00
NicolasS 8a89951286 Translated using Weblate (French)
Currently translated at 100.0% (186 of 186 strings)

Translation: Moonlight Game Streaming/moonlight-qt
Translate-URL: https://hosted.weblate.org/projects/moonlight/moonlight-qt/fr/
2021-05-08 07:28:33 +02:00
grtw2116 c41ed55291 Added translation using Weblate (Japanese) 2021-05-08 07:28:33 +02:00
Cameron Gutman caba0499d7 Prepare for v3.1.3 2021-05-07 19:07:52 -05:00
Cameron Gutman 13deacfc3b Fix Spanish translation args 2021-05-07 18:56:08 -05:00
Cameron Gutman 7231085456 Run lupdate-pro and lrelease-pro 2021-05-06 17:26:26 -05:00
Cameron Gutman 663bb4aa10 Merge remote-tracking branch 'origin/weblate' 2021-05-06 17:24:39 -05:00
Cameron Gutman 18721b4542 Improve sanity checking and logging for unsupported GFE versions 2021-05-06 17:18:47 -05:00
NicolasS 19232722e2 Translated using Weblate (French)
Currently translated at 100.0% (186 of 186 strings)

Translation: Moonlight Game Streaming/moonlight-qt
Translate-URL: https://hosted.weblate.org/projects/moonlight/moonlight-qt/fr/
2021-05-05 22:32:11 +02:00
Cameron Gutman d2eca68484 Update moonlight-common-c to attempt to fix audio latency issues 2021-05-04 10:22:29 -05:00
Héctor 091167478e Translated using Weblate (Spanish)
Currently translated at 23.6% (44 of 186 strings)

Translation: Moonlight Game Streaming/moonlight-qt
Translate-URL: https://hosted.weblate.org/projects/moonlight/moonlight-qt/es/
2021-05-04 10:32:11 +02:00
Cameron Gutman 547b8abc56 Prepare for v3.1.2 release 2021-05-03 17:38:14 -05:00
Héctor dbfac45d59 Translated using Weblate (Spanish)
Currently translated at 17.7% (33 of 186 strings)

Translation: Moonlight Game Streaming/moonlight-qt
Translate-URL: https://hosted.weblate.org/projects/moonlight/moonlight-qt/es/
2021-05-03 09:32:23 +02:00
Cameron Gutman 086f6de867 Work around broken package repositories in AppVeyor 2021-05-02 14:49:07 -05:00
Cameron Gutman 0f93091848 Update SDL_GameControllerDB 2021-05-02 10:56:50 -05:00
Cameron Gutman ac19f62ed9 Never display placeholder text for an app collector game
Fixes #567
2021-05-02 10:39:25 -05:00
Cameron Gutman 8fcef63890 Don't waste time performing extra decoder initializations for EGL when it's not supported 2021-04-30 20:52:09 -05:00
Cameron Gutman 34cd2c1afa Run lupdate-pro and lrelease-pro 2021-04-30 20:09:15 -05:00
Cameron Gutman 5d1a19d49c Merge remote-tracking branch 'origin/weblate' 2021-04-30 20:07:59 -05:00
Cameron Gutman 2fbb320539 Refuse to stream if Moonlight is known incompatible with the host GFE version 2021-04-30 20:05:49 -05:00
Cameron Gutman b50e5ed7e6 Fix keyboard grab on X11
Fixes #584
2021-04-30 18:17:15 -05:00
Cameron Gutman 26c82ca72c Don't enable vsync on test decoders 2021-04-30 18:12:56 -05:00
Cameron Gutman f8a8a431a8 Revert warn_off removal
There are still a bunch of unused parameter warnings with GCC/Clang
2021-04-29 21:22:59 -05:00
Cameron Gutman fd27867195 Update moonlight-common-c and remove warn_off config option 2021-04-29 18:05:40 -05:00
Cameron Gutman 5e2a627795 Update moonlight-common-c with RTSP handshake retry logic 2021-04-28 17:38:59 -05:00
Cameron Gutman e081401c2d Update moonlight-common-c for multi-FEC support and audio latency fix 2021-04-26 22:51:46 -05:00
Cameron Gutman e62a268aac Avoid audio encryption on Steam Link for performance reasons 2021-04-23 19:49:03 -05:00
Cameron Gutman 140a9e5191 Enable audio stream encryption 2021-04-23 18:55:10 -05:00
Cameron Gutman 51b5f8046f Disable HTTP/2 on host PC connections for GFE 3.22 2021-04-19 18:41:01 -05:00
Cameron Gutman 91b965d1aa Update moonlight-common-c 2021-04-18 19:34:12 -05:00
Cameron Gutman f8faccee59 Remove qt5-devel which doesn't exist in Fedora 34 2021-04-17 11:11:37 -05:00
shower 6ac46a36a7 Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (184 of 184 strings)

Translation: Moonlight Game Streaming/moonlight-qt
Translate-URL: https://hosted.weblate.org/projects/moonlight/moonlight-qt/zh_Hans/
2021-04-17 13:27:02 +02:00
Héctor 27572ba4bc Translated using Weblate (Spanish)
Currently translated at 16.3% (30 of 184 strings)

Translation: Moonlight Game Streaming/moonlight-qt
Translate-URL: https://hosted.weblate.org/projects/moonlight/moonlight-qt/es/
2021-04-15 22:27:01 +02:00
Cameron Gutman 21be832161 Prepare for v3.1.1 release 2021-04-09 21:07:05 -05:00
Cameron Gutman 5769632793 Update moonlight-common-c for initial GFE 3.22 compatibility 2021-04-09 20:18:47 -05:00
Cameron Gutman 9efd959ed8 Merge branch 'weblate' 2021-04-09 20:17:39 -05:00
Cameron Gutman 5c72afe17e Run lupdate-pro and lrelease-pro 2021-04-04 20:45:57 -05:00
a dc24319333 Translated using Weblate (Russian)
Currently translated at 100.0% (184 of 184 strings)

Translation: Moonlight Game Streaming/moonlight-qt
Translate-URL: https://hosted.weblate.org/projects/moonlight/moonlight-qt/ru/
2021-04-05 03:42:34 +02:00
Jorys Paulin 1acbdf94aa Translated using Weblate (French)
Currently translated at 100.0% (184 of 184 strings)

Translation: Moonlight Game Streaming/moonlight-qt
Translate-URL: https://hosted.weblate.org/projects/moonlight/moonlight-qt/fr/
2021-04-05 03:42:33 +02:00
Cameron Gutman 44229dfe51 Update translations
- Add RU and ES to resources.qrc and app.pro
- Rerun lupdate-pro and lrelease-pro
2021-04-04 10:25:06 -05:00
a 13206227d9 Translated using Weblate (Russian)
Currently translated at 100.0% (184 of 184 strings)

Translation: Moonlight Game Streaming/moonlight-qt
Translate-URL: https://hosted.weblate.org/projects/moonlight/moonlight-qt/ru/
2021-04-04 17:03:35 +02:00
Cameron Gutman e3e8a57df6 Update to OpenSSL 1.1.1k and FFmpeg 4.3.2 2021-03-31 20:27:33 -05:00
Cameron Gutman 7afff80984 Update moonlight-common-c to pick up AMF HEVC parsing fix 2021-03-31 20:07:45 -05:00
Cameron Gutman 500522418e Fix AppImage build 2021-03-28 16:07:20 -05:00
a 5ccac5a5c3 Translated using Weblate (Russian)
Currently translated at 45.1% (83 of 184 strings)

Translation: Moonlight Game Streaming/moonlight-qt
Translate-URL: https://hosted.weblate.org/projects/moonlight/moonlight-qt/ru/
2021-03-28 14:29:40 +02:00
Héctor 69822f4401 Translated using Weblate (Spanish)
Currently translated at 11.4% (21 of 184 strings)

Translation: Moonlight Game Streaming/moonlight-qt
Translate-URL: https://hosted.weblate.org/projects/moonlight/moonlight-qt/es/
2021-03-28 14:29:39 +02:00
reloxx13 f290cfbd35 Translated using Weblate (German)
Currently translated at 100.0% (184 of 184 strings)

Translation: Moonlight Game Streaming/moonlight-qt
Translate-URL: https://hosted.weblate.org/projects/moonlight/moonlight-qt/de/
2021-03-28 14:29:39 +02:00
Cameron Gutman fae30f59ce Add attribution for Cloudsmith 2021-03-27 12:40:51 -05:00
a a15f303c0a Added translation using Weblate (Russian) 2021-03-26 05:05:09 +01:00
Cameron Gutman 5e85038d78 Update AppImage to libva 2.11 2021-03-25 22:17:51 -05:00
Héctor 4e7b35c54e Added translation using Weblate (Spanish) 2021-03-25 09:04:18 +01:00
Cameron Gutman 48dde310ae Add Norwegian Bokmål to the build 2021-03-24 20:40:22 -05:00
Cameron Gutman 6b041e7657 Run lrelease-pro 2021-03-24 20:39:52 -05:00
Cameron Gutman 69576ee1e1 Run lupdate-pro 2021-03-24 20:39:39 -05:00
Cameron Gutman bd3105f62e Merge remote-tracking branch 'origin/weblate' 2021-03-24 20:28:24 -05:00
Cameron Gutman 02e3f0e71b Add missing declaration 2021-03-22 23:32:00 -05:00
Cameron Gutman 74bf981f3d Ensure hwframe read-back works in SDL renderer 2021-03-22 23:15:14 -05:00
Cameron Gutman 32f0678692 Only pass modifiers array if DRM_MODE_FB_MODIFIERS is set 2021-03-22 22:52:06 -05:00
Cameron Gutman e74753bec1 Allow fallback from EGL to direct on EGLImage export failure 2021-03-22 22:51:29 -05:00
Cameron Gutman 4a8c9ad17f Add support for 4-plane frames 2021-03-22 00:46:04 -05:00
Cameron Gutman 0c6c882f50 Fix attribute buffer overflow for fully planar YUV formats 2021-03-22 00:14:06 -05:00
Cameron Gutman 579f25c1ba Add missing EGL_DMA_BUF_PLANE2 definitions 2021-03-22 00:02:03 -05:00
Cameron Gutman d5eb5c8816 Fix build with EGL 1.4 headers 2021-03-21 23:59:35 -05:00
Cameron Gutman 26d04b5f9c Render DRM-PRIME frames as opaque images
We can't always assume they are NV12. Even if they _are_ NV12,
they may have DRM format modifiers that are incompatible with
simply assuming linear Y and UV buffers (such as tiling).
2021-03-21 23:23:12 -05:00
Cameron Gutman a0b9684504 Continue refactoring for non-NV12 support in EGLRenderer 2021-03-21 21:43:57 -05:00
Cameron Gutman 402b6d1f0c Begin refactoring for non-NV12 support in EGLRenderer 2021-03-21 19:20:52 -05:00
Cameron Gutman e87a5fa354 Pass DRM format modifiers to FB creation 2021-03-21 17:54:10 -05:00
Allan Nordhøy 63574f643e Translated using Weblate (Norwegian Bokmål)
Currently translated at 70.1% (129 of 184 strings)

Translation: Moonlight Game Streaming/moonlight-qt
Translate-URL: https://hosted.weblate.org/projects/moonlight/moonlight-qt/nb_NO/
2021-03-21 10:36:54 +01:00
Cameron Gutman 1d6b1020c3 Translated using Weblate (French)
Currently translated at 73.9% (136 of 184 strings)

Translation: Moonlight Game Streaming/moonlight-qt
Translate-URL: https://hosted.weblate.org/projects/moonlight/moonlight-qt/fr/
2021-03-21 06:29:36 +01:00
Allan Nordhøy 96337a3230 Translated using Weblate (Norwegian Bokmål)
Currently translated at 16.3% (30 of 184 strings)

Translation: moonlight/moonlight-qt
Translate-URL: https://hosted.weblate.org/projects/moonlight/moonlight-qt/nb_NO/
2021-03-21 02:33:56 +01:00
reloxx13 950c59d8b4 Translated using Weblate (German)
Currently translated at 100.0% (184 of 184 strings)

Translation: moonlight/moonlight-qt
Translate-URL: https://hosted.weblate.org/projects/moonlight/moonlight-qt/de/
2021-03-21 02:33:56 +01:00
Allan Nordhøy 9cfd095281 Added translation using Weblate (Norwegian Bokmål) 2021-03-21 02:33:56 +01:00
Anonymous 996e049863 Translated using Weblate (Chinese (Simplified))
Currently translated at 96.1% (177 of 184 strings)

Translation: moonlight/moonlight-qt
Translate-URL: https://hosted.weblate.org/projects/moonlight/moonlight-qt/zh_Hans/
2021-03-21 02:33:56 +01:00
Anonymous 6e42817c3e Translated using Weblate (French)
Currently translated at 74.4% (137 of 184 strings)

Translation: moonlight/moonlight-qt
Translate-URL: https://hosted.weblate.org/projects/moonlight/moonlight-qt/fr/
2021-03-21 02:33:56 +01:00
Cameron Gutman 7758344a57 Fix Weblate badge 2021-03-20 18:12:04 -05:00
Cameron Gutman b5df566b6b Add Weblate links to readme
Closes #539
2021-03-20 18:11:15 -05:00
Cameron Gutman e21fd19a70 Prepare for v3.1.0 release 2021-03-14 11:30:21 -05:00
Cameron Gutman 98672c8e44 Allow remote desktop mouse mode in embedded mode
It can be easier to control the mouse on the desktop that way
2021-03-13 16:58:58 -06:00
Cameron Gutman 2e92f0640b Update SDL_GameControllerDB 2021-03-13 15:50:01 -06:00
Cameron Gutman d6b314f573 Add option to build for embedded environments 2021-03-13 15:20:58 -06:00
Cameron Gutman d7a150ffa8 Only enable capture mode list if capture toggle is enabled 2021-03-13 14:59:32 -06:00
Cameron Gutman 955da6be30 Enable Gallium VAAPI driver on VAAPI 1.x 2021-03-13 14:37:13 -06:00
Cameron Gutman b5f8e6738a Move LANG_DE to the end of the Language enum 2021-03-09 20:13:55 -06:00
reloxx13 d7fa8c9795 Add german translation (#536)
* add german translation

* add german to language list

* donttranslate language names. always use the native name.

* add compiled german translation file

* german translation handling

* add german to getSuffixFromLanguage

* update german string and translation

* update all language files with new strings

* typo

* compile

* more typos and fixes for german
2021-03-09 20:10:36 -06:00
Cameron Gutman acb7a5b3c8 Fix XCB out-of-sequence abort after switching to non-threaded Qt renderer 2021-03-07 16:48:10 -06:00
Cameron Gutman 783a57ef89 Use separate thread for streaming on X11 and Wayland 2021-03-07 12:15:42 -06:00
Cameron Gutman d33ccce683 Hold a reference to the underlying DMA-BUFs until the next frame 2021-03-07 12:01:25 -06:00
Cameron Gutman d722bf10b9 Don't use glFinish() with KMSDRM backend 2021-03-07 11:23:31 -06:00
Cameron Gutman 41337475a2 Update SDL_GameControllerDB 2021-03-06 14:29:55 -06:00
Cameron Gutman f504a069d7 Update VCRedist version for VC 16.9 2021-03-05 17:51:26 -06:00
Cameron Gutman 96f79688dd Use av_packet_alloc() instead of av_init_packet()
sizeof(AVPacket) may be deprecated from ABI in the near future
2021-03-05 17:47:04 -06:00
Cameron Gutman c330898aeb Add workaround for AppView breaking after changing language at runtime 2021-03-05 17:44:31 -06:00
Cameron Gutman 94952cf0a4 Update issue template 2021-03-03 01:39:55 -06:00
Cameron Gutman a3c249abe9 Don't load the app label asynchronously 2021-03-02 19:03:09 -06:00
Cameron Gutman c26562fab8 Fix Steam Link build 2021-03-02 18:57:16 -06:00
Cameron Gutman b7cbd99212 Fix tooltip display in app grid 2021-03-02 18:35:50 -06:00
Cameron Gutman b53ef35a85 Improve loading speed of PC and app grids 2021-03-02 18:32:56 -06:00
Cameron Gutman 1ebb5fefb1 Address Clazy warnings 2021-03-02 18:14:15 -06:00
Cameron Gutman 164b3edd41 Fix viewport size on High DPI Wayland systems
Fixes #533
2021-03-01 22:47:14 -06:00
Cameron Gutman 819ea36cca Hack to fix build on AppVeyor 2021-02-28 16:47:39 -06:00
Cameron Gutman 088c9b9e4e Stop single-arch binary build before extracting dSYM 2021-02-28 16:02:50 -06:00
Cameron Gutman ab25a9b8ee Add Universal binary support to build script 2021-02-28 15:49:59 -06:00
Cameron Gutman b2aea9f8f0 Revert change to only fire special combos on key up 2021-02-28 15:06:46 -06:00
Cameron Gutman 5399b95936 Fix update checker on macOS with Qt 6.0 builds 2021-02-28 14:33:20 -06:00
Cameron Gutman 1d5c120748 Add support for specifying minimum kernel versions in the update manifest 2021-02-28 14:27:29 -06:00
jmt-gh 6be7301e69 Stats overlay controller combo (#495)
* Update gamepad.cpp

add gamepad combo to toggle stats overlay

* typo fixes
2021-02-27 17:03:52 -06:00
Maximiliano Bertacchini fff9f2b17d Add numpad Enter handlers (Keys.onEnterPressed) (#516) 2021-02-27 17:01:22 -06:00
Cameron Gutman 87a7d2e45c Add setting to only capture system keys in full-screen 2021-02-27 16:47:38 -06:00
Cameron Gutman 58803ef40d Update SDL to e561ce3 and OpenSSL to 1.1.1j 2021-02-27 13:08:21 -06:00
Cameron Gutman b27ca993aa Add support for system key capture in windowed mode on macOS 2021-02-26 21:43:47 -06:00
Cameron Gutman 28ecc6bcbf Remove old Qt Multimedia audio renderer 2021-02-25 23:23:08 -06:00
Cameron Gutman 30c5e3f237 Fix window icons on Wayland 2021-02-25 23:20:21 -06:00
Cameron Gutman 6a25a3a3d0 Don't require DRM master for SDL KMSDRM backend 2021-02-25 22:27:20 -06:00
Cameron Gutman 9a84c0ae30 Update AppImage to official SDL Git repo 2021-02-25 22:00:35 -06:00
Cameron Gutman 133cc5457d Use SDL_CaptureMouse() on macOS and Linux too 2021-02-08 18:30:22 -06:00
Cameron Gutman 05e4c5b507 Move clipboard processing to a separate thread to avoid blocking the main thread during pastes 2021-02-07 17:18:35 -06:00
Cameron Gutman 8372717a09 Improve Unicode support for some common characters 2021-02-07 16:00:00 -06:00
Cameron Gutman 5f3f2a96fd Improve detection of having no clipboard text 2021-02-07 13:53:03 -06:00
Cameron Gutman b83c6f0c28 Add support for pasting clipboard text 2021-02-07 13:46:38 -06:00
Cameron Gutman 36dc0f3e3c Use mouse capture for Win32 instead of our global mouse state hack 2021-02-07 12:38:57 -06:00
Cameron Gutman b4edde6f90 Disable Qt 6 threaded renderer on Windows
They switched from non-threaded to threaded by default in Qt 6
2021-02-07 11:21:47 -06:00
Cameron Gutman 395eb5581c Cache results of WMUtils::isRunningX11() and WMUtils::isRunningWayland() 2021-02-07 11:03:53 -06:00
Cameron Gutman 6dab251055 Don't overwrite window mode settings when switching between WM and non-WM environments 2021-02-07 10:49:29 -06:00
Cameron Gutman 3bd6a6a614 Fullscreen is no longer required for passing system keyboard shortcuts 2021-02-06 20:15:58 -06:00
Cameron Gutman 93d21a0d1d Fix EGLImage leak when not using a hwaccel backend 2021-02-06 17:59:05 -06:00
Cameron Gutman 2c2a73e320 Fix build and require SDL 2.0.15 to hook 2021-02-06 17:07:21 -06:00
Cameron Gutman a88a3c9ea3 Allow SDL to render with KMSDRM backend while Qt is also using DRM 2021-02-06 16:32:54 -06:00
Cameron Gutman e4df70cd56 Use the async connect thread on all platforms
It turned out that the cause of the random crashes was the threaded renderer
that we now turn off.
2021-02-06 15:14:15 -06:00
Cameron Gutman 90f7d35150 Always use a non-threaded renderer
This ensures we can block Qt's rendering as needed to synchronize with SDL
2021-02-06 14:58:30 -06:00
Cameron Gutman 75b19a60c1 Fix build on macOS and keyboard grab bugs 2021-02-06 11:29:18 -06:00
Cameron Gutman 9fc921f49f Use SDL's keyboard grab support now that it's been upstreamed 2021-02-05 00:13:20 -06:00
Cameron Gutman 4b9e90ac72 Update SDL for Windows and macOS to Hg 60b9ce4ee73e
https://github.com/cgutman/moonlight-deps/commit/ad955430988be24fd379a6152642c7ce4053bc7f
2021-02-04 20:51:33 -06:00
Cameron Gutman 36dd6c83e0 Fix assert failure when using v4l2m2m with EGL 2021-02-04 20:38:18 -06:00
Cameron Gutman d790313221 Use the existing install of Qt on macOS in CI 2021-02-04 19:39:58 -06:00
Cameron Gutman c3895f06c0 Fix handling of preferred/compatible pixel formats with EGLRenderer and VAAPI/DRM backends 2021-02-04 19:39:18 -06:00
Cameron Gutman 76e81fa651 Add windowed support for MMAL 2021-02-03 20:57:39 -06:00
Cameron Gutman d63a1b0eb0 Handle custom hwaccel decoders 2021-02-02 19:45:22 -06:00
Cameron Gutman 4dffac3c78 Ask for NV12 buffers for DRMRenderer 2021-02-02 19:40:06 -06:00
Cameron Gutman 88391b0274 Use the decoder's supported pix_fmts to select a suitable renderer 2021-02-02 19:05:27 -06:00
Cameron Gutman c3cea0238e Fix build with old EGL 1.5 headers without EGL_EGL_PROTOTYPES 2021-01-31 21:42:56 -06:00
Cameron Gutman 5bf4266973 Fix build with old Mesa EGL headers 2021-01-31 18:41:53 -06:00
Cameron Gutman ed57c54d06 Move EGL definitions into renderer.h to try to fix build on Armbian 2021-01-31 17:01:11 -06:00
Cameron Gutman 01936e000e Fix Steam Link bundle build 2021-01-31 16:27:38 -06:00
Cameron Gutman 80137848dc Add DMA-BUF export compatibility with EGL 1.4 2021-01-31 16:24:31 -06:00
Cameron Gutman 149575e0c4 Don't retranslate if the language didn't change 2021-01-31 15:28:00 -06:00
Cameron Gutman 6a66b462ce Allow DRMRenderer to be used for indirect rendering 2021-01-31 15:19:19 -06:00
Cameron Gutman 1e31f6fe77 Fix initialization of DRM backend with X running 2021-01-31 15:03:40 -06:00
Cameron Gutman f173b3ff09 Attempt to fix AppImage and Steam Link build errors 2021-01-31 14:48:42 -06:00
Cameron Gutman baffab62ce Fix uninitialized variable warning 2021-01-31 12:58:19 -06:00
Cameron Gutman d141cec2a8 Fix mangled macOS libraries after transition to submodule 2021-01-31 12:23:52 -06:00
Cameron Gutman df87ac3810 Update SDL in AppImage to Hg d75deb75464a 2021-01-31 12:13:20 -06:00
Cameron Gutman 46960ce0a4 Remove side-by-side support for Qt 5.9 on Steam Link 2021-01-31 12:08:15 -06:00
Cameron Gutman 522206b0bc Use shallow-cloned submodule for prebuilts 2021-01-31 11:50:41 -06:00
Cameron Gutman 24d60dd3ab Remove prebuilt binaries 2021-01-31 11:40:41 -06:00
Cameron Gutman dbfdc2fd14 Share DRM FD and GBM device with SDL
Depends on https://hg.libsdl.org/SDL/rev/d75deb75464a
2021-01-30 21:11:52 -06:00
Cameron Gutman a97912e297 Print the error when querying EGL_EXTENSIONS fails 2021-01-30 20:13:20 -06:00
Cameron Gutman cedba501ee Query the frontend renderer to determine fullscreen-only limitations 2021-01-30 17:57:34 -06:00
Cameron Gutman ac947d3bda Add EGLImage export support to DRMRenderer 2021-01-30 17:52:23 -06:00
Cameron Gutman 243ba371b3 Initialize a DRM hardware context for hwaccel usage 2021-01-30 16:33:58 -06:00
Cameron Gutman 3af24eafbb Build fixes 2021-01-30 12:53:57 -06:00
Cameron Gutman 69b6022afe Add language selection option in settings
Fixes #501
2021-01-30 12:14:31 -06:00
Cameron Gutman 1cd66419d4 Remember desired cursor hide state
Fixes #511
2021-01-30 11:08:01 -06:00
WLongandWLong 5309a02f6d Add simplified Chinese language (#506)
* 添加qml_zh_cn.ts

* 更新部分翻译

* 优化部分文字描述

* 优化部分文字描述

* 恢复README.md

Co-authored-by: WLong <a101084@126.com>
2021-01-30 10:10:36 -06:00
Cameron Gutman 541784382a Add support for AV_HWDEVICE_TYPE_DRM hwaccels 2021-01-30 10:00:01 -06:00
Cameron Gutman 2a7fae2c82 Revert "Add logic to try LibreELEC's out-of-tree stateless V4L2 decoders"
v4l2request is a hwaccel not a decoder

This reverts commit ed3a5448b1.
2021-01-29 20:30:01 -06:00
Cameron Gutman ed3a5448b1 Add logic to try LibreELEC's out-of-tree stateless V4L2 decoders 2021-01-29 20:19:27 -06:00
Cameron Gutman 2eb4769bbc Only trigger special key combos when all keys are up 2021-01-24 13:02:42 -06:00
Cameron Gutman 02019000df Ctrl+Alt+Shift+Down Arrow is a reserved key combo on GNOME, so switch to Ctrl+Alt+Shift+D 2021-01-21 23:05:48 -06:00
Cameron Gutman 6262f51c96 Key capture is supported on Wayland with latest SDL Hg 2021-01-21 21:07:54 -06:00
Cameron Gutman d38ff5759a Add Ctrl+Alt+Shift+Down Arrow shortcut to minimize 2021-01-21 19:39:17 -06:00
Cameron Gutman f6de2e4dd3 Revert "Disable the system keyboard capture option on Wayland"
SDL now supports keyboard grab with https://hg.libsdl.org/SDL/rev/b0e13627c656

This reverts commit bc10fd0aba.
2021-01-20 21:43:16 -06:00
Cameron Gutman 3959f447d4 Fix detection of x64 architecture on some machines 2021-01-17 12:58:08 -06:00
811 changed files with 12721 additions and 197760 deletions

No files matched your search

+11 -2
View File
@@ -1,10 +1,12 @@
--- ---
name: Bug report name: Bug report
about: Ask for help on Discord before reporting a bug about: Follow the troubleshooting guide before reporting a bug
--- ---
**READ ME FIRST!** **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 If you're here because something basic is not working (like gamepad input, video, or similar), it's probably something specific to your setup, so make sure you've gone through the Troubleshooting Guide first: https://github.com/moonlight-stream/moonlight-docs/wiki/Troubleshooting
If you still have trouble with basic functionality after following the guide, join our Discord server where there are many other volunteers 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** **Describe the bug**
A clear and concise description of what the bug is. A clear and concise description of what the bug is.
@@ -26,14 +28,21 @@ List the games you've tried that exhibit the issue. To see if the issue is game-
- If so, which settings have been changed? - If so, which settings have been changed?
- Does the problem still occur after reverting settings back to default? - Does the problem still occur after reverting settings back to default?
**Gamepad-related issues (please complete if problem is gamepad-related)**
- Do you have any gamepads connected to your host PC directly?
- Does the problem still remain if you stream the desktop and use https://html5gamepad.com to test your gamepad?
- Instructions for streaming the desktop can be found here: https://github.com/moonlight-stream/moonlight-docs/wiki/Setup-Guide
**Client PC details (please complete the following information)** **Client PC details (please complete the following information)**
- OS: [e.g. Windows 10 1809] - OS: [e.g. Windows 10 1809]
- Moonlight Version: [e.g. v0.9.0] - Moonlight Version: [e.g. v0.9.0]
- GPU: [e.g. Intel HD Graphics 520]
**Server PC details (please complete the following information)** **Server PC details (please complete the following information)**
- OS: [e.g. Windows 10 1809] - OS: [e.g. Windows 10 1809]
- GeForce Experience version: [e.g. 3.16.0.140] - GeForce Experience version: [e.g. 3.16.0.140]
- Nvidia GPU driver: [e.g. 417.35] - Nvidia GPU driver: [e.g. 417.35]
- Antivirus and firewall software: [e.g. Windows Defender and Windows Firewall]
**Moonlight Logs (please attach)** **Moonlight Logs (please attach)**
- On Windows, `Moonlight-###.log` files can be found in `%TEMP%`. Simply type that into the File Explorer path field to navigate there. - On Windows, `Moonlight-###.log` files can be found in `%TEMP%`. Simply type that into the File Explorer path field to navigate there.
+4
View File
@@ -13,3 +13,7 @@
[submodule "h264bitstream/h264bitstream"] [submodule "h264bitstream/h264bitstream"]
path = h264bitstream/h264bitstream path = h264bitstream/h264bitstream
url = https://github.com/aizvorski/h264bitstream.git url = https://github.com/aizvorski/h264bitstream.git
[submodule "libs"]
path = libs
url = https://github.com/cgutman/moonlight-qt-prebuilts.git
shallow = true
+12 -3
View File
@@ -4,10 +4,11 @@
Moonlight also has mobile versions for [Android](https://github.com/moonlight-stream/moonlight-android) and [iOS](https://github.com/moonlight-stream/moonlight-ios). Moonlight also has mobile versions for [Android](https://github.com/moonlight-stream/moonlight-android) and [iOS](https://github.com/moonlight-stream/moonlight-ios).
You can follow development on our [Discord server](https://moonlight-stream.org/discord). You can follow development on our [Discord server](https://moonlight-stream.org/discord) and help translate Moonlight into your language on [Weblate](https://hosted.weblate.org/projects/moonlight/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) [![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) [![Downloads](https://img.shields.io/github/downloads/moonlight-stream/moonlight-qt/total)](https://github.com/moonlight-stream/moonlight-qt/releases)
[![Translation Status](https://hosted.weblate.org/widgets/moonlight/-/moonlight-qt/svg-badge.svg)](https://hosted.weblate.org/projects/moonlight/moonlight-qt/)
## Features ## Features
- Hardware accelerated video decoding on Windows, Mac, and Linux - Hardware accelerated video decoding on Windows, Mac, and Linux
@@ -15,7 +16,7 @@ You can follow development on our [Discord server](https://moonlight-stream.org/
- Supports streaming at 720p, 1080p, 1440p, 4K, and the client PC's native screen resolution - Supports streaming at 720p, 1080p, 1440p, 4K, and the client PC's native screen resolution
- 7.1 surround sound audio support - 7.1 surround sound audio support
- Support for both pointer capture (for games) and direct mouse control (for remote desktop) - Support for both pointer capture (for games) and direct mouse control (for remote desktop)
- Support for passing system-wide keyboard shortcuts like Alt+Tab to the host in fullscreen mode - Support for passing system-wide keyboard shortcuts like Alt+Tab to the host
- Direct touchscreen mouse control - Direct touchscreen mouse control
- HEVC support for better image quality at reduced bandwidth - HEVC support for better image quality at reduced bandwidth
- Gamepad support with force feedback for up to 4 players - Gamepad support with force feedback for up to 4 players
@@ -28,6 +29,12 @@ You can follow development on our [Discord server](https://moonlight-stream.org/
- [Raspberry Pi 4](https://github.com/moonlight-stream/moonlight-docs/wiki/Installing-Moonlight-Qt-on-Raspberry-Pi-4) - [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) - [NVIDIA Jetson and Nintendo Switch (Ubuntu L4T)](https://github.com/moonlight-stream/moonlight-docs/wiki/Installing-Moonlight-Qt-on-Linux4Tegra-(L4T)-Ubuntu)
#### Special Thanks
[![Hosted By: Cloudsmith](https://img.shields.io/badge/OSS%20hosting%20by-cloudsmith-blue?logo=cloudsmith&style=flat-square)](https://cloudsmith.com)
Hosting for Moonlight's Raspberry Pi and L4T package repositories is graciously provided for free by [Cloudsmith](https://cloudsmith.com).
## Building ## Building
### Windows Build Requirements ### Windows Build Requirements
@@ -49,7 +56,7 @@ You can follow development on our [Discord server](https://moonlight-stream.org/
* GCC or Clang * GCC or Clang
* Install the required packages: * Install the required packages:
* Debian/Ubuntu: `libegl1-mesa-dev libgl1-mesa-dev libopus-dev libqt5svg5-dev libsdl2-dev libsdl2-ttf-dev libssl-dev libavcodec-dev libva-dev libvdpau-dev libxkbcommon-dev qt5-default qt5-qmake qtbase5-dev qtdeclarative5-dev qtquickcontrols2-5-dev wayland-protocols qml-module-qtquick-controls2 qml-module-qtquick-layouts qml-module-qtquick-window2 qml-module-qtquick2` * Debian/Ubuntu: `libegl1-mesa-dev libgl1-mesa-dev libopus-dev libqt5svg5-dev libsdl2-dev libsdl2-ttf-dev libssl-dev libavcodec-dev libva-dev libvdpau-dev libxkbcommon-dev qt5-default qt5-qmake qtbase5-dev qtdeclarative5-dev qtquickcontrols2-5-dev wayland-protocols qml-module-qtquick-controls2 qml-module-qtquick-layouts qml-module-qtquick-window2 qml-module-qtquick2`
* RedHat/Fedora: `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` * RedHat/Fedora: `openssl-devel SDL2-devel SDL2_ttf-devel ffmpeg-devel qt5-qtsvg-devel qt5-qtquickcontrols2-devel libva-devel libvdpau-devel opus-devel pulseaudio-libs-devel alsa-lib-devel`
* FFmpeg 4.0+ is required to build. If your distro doesn't package FFmpeg 4.0 or later, you can build and install it from source on https://ffmpeg.org/ * FFmpeg 4.0+ is required to build. If your distro doesn't package FFmpeg 4.0 or later, you can build and install it from source on https://ffmpeg.org/
### Steam Link Build Requirements ### Steam Link Build Requirements
@@ -68,6 +75,8 @@ You can follow development on our [Discord server](https://moonlight-stream.org/
* For macOS builds, use `scripts/generate-dmg.sh`. Execute this script from the root of the repository and ensure Qt's `bin` folder is in your `$PATH`. * For macOS builds, use `scripts/generate-dmg.sh`. Execute this script from the root of the repository and ensure Qt's `bin` folder is in your `$PATH`.
* For Steam Link builds, run `scripts/build-steamlink-app.sh` from the root of the repository. * For Steam Link builds, run `scripts/build-steamlink-app.sh` from the root of the repository.
* To build from the command line for development use, run `qmake moonlight-qt.pro` then `make debug` or `make release` * To build from the command line for development use, run `qmake moonlight-qt.pro` then `make debug` or `make release`
* To create an embedded build for a single-purpose device, use `qmake "CONFIG+=embedded" moonlight-qt.pro` and build normally.
* This build will lack windowed mode, Discord/Help links, and other features that don't make sense on an embedded device.
## Contribute ## Contribute
1. Fork us 1. Fork us
+26 -3
View File
@@ -45,7 +45,7 @@ win32 {
LIBS += -L$$PWD/../libs/windows/lib/arm64 LIBS += -L$$PWD/../libs/windows/lib/arm64
} }
LIBS += ws2_32.lib winmm.lib dxva2.lib ole32.lib gdi32.lib user32.lib d3d9.lib dwmapi.lib dbghelp.lib qwave.lib LIBS += ws2_32.lib winmm.lib dxva2.lib ole32.lib gdi32.lib user32.lib d3d9.lib dwmapi.lib dbghelp.lib
} }
macx { macx {
INCLUDEPATH += $$PWD/../libs/mac/include INCLUDEPATH += $$PWD/../libs/mac/include
@@ -131,9 +131,11 @@ SOURCES += \
cli/commandlineparser.cpp \ cli/commandlineparser.cpp \
cli/quitstream.cpp \ cli/quitstream.cpp \
cli/startstream.cpp \ cli/startstream.cpp \
settings/compatfetcher.cpp \
settings/mappingfetcher.cpp \ settings/mappingfetcher.cpp \
settings/streamingpreferences.cpp \ settings/streamingpreferences.cpp \
streaming/input/abstouch.cpp \ streaming/input/abstouch.cpp \
streaming/input/clipboard.cpp \
streaming/input/gamepad.cpp \ streaming/input/gamepad.cpp \
streaming/input/input.cpp \ streaming/input/input.cpp \
streaming/input/keyboard.cpp \ streaming/input/keyboard.cpp \
@@ -155,6 +157,7 @@ SOURCES += \
HEADERS += \ HEADERS += \
backend/nvapp.h \ backend/nvapp.h \
settings/compatfetcher.h \
settings/mappingfetcher.h \ settings/mappingfetcher.h \
utils.h \ utils.h \
backend/computerseeker.h \ backend/computerseeker.h \
@@ -250,6 +253,12 @@ libdrm {
DEFINES += HAVE_DRM DEFINES += HAVE_DRM
SOURCES += streaming/video/ffmpeg-renderers/drm.cpp SOURCES += streaming/video/ffmpeg-renderers/drm.cpp
HEADERS += streaming/video/ffmpeg-renderers/drm.h HEADERS += streaming/video/ffmpeg-renderers/drm.h
linux {
message(Master hooks enabled)
SOURCES += masterhook.c
LIBS += -ldl
}
} }
config_EGL { config_EGL {
message(EGL renderer selected) message(EGL renderer selected)
@@ -264,7 +273,7 @@ config_EGL {
config_SL { config_SL {
message(Steam Link build configuration selected) message(Steam Link build configuration selected)
DEFINES += STEAM_LINK HAVE_SLVIDEO HAVE_SLAUDIO DEFINES += EMBEDDED_BUILD STEAM_LINK HAVE_SLVIDEO HAVE_SLAUDIO
LIBS += -lSLVideo -lSLAudio LIBS += -lSLVideo -lSLAudio
SOURCES += \ SOURCES += \
@@ -307,13 +316,27 @@ discord-rpc {
LIBS += -ldiscord-rpc LIBS += -ldiscord-rpc
DEFINES += HAVE_DISCORD DEFINES += HAVE_DISCORD
} }
embedded {
message(Embedded build)
DEFINES += EMBEDDED_BUILD
}
RESOURCES += \ RESOURCES += \
resources.qrc \ resources.qrc \
qml.qrc qml.qrc
TRANSLATIONS += \ TRANSLATIONS += \
languages/qml_fr.ts languages/qml_zh_cn.ts \
languages/qml_de.ts \
languages/qml_fr.ts \
languages/qml_nb_NO.ts \
languages/qml_ru.ts \
languages/qml_es.ts \
languages/qml_ja.ts \
languages/qml_vi.ts \
languages/qml_th.ts \
languages/qml_ko.ts
# Additional import path used to resolve QML modules in Qt Creator's code model # Additional import path used to resolve QML modules in Qt Creator's code model
QML_IMPORT_PATH = QML_IMPORT_PATH =
+66 -35
View File
@@ -15,8 +15,8 @@ AutoUpdateChecker::AutoUpdateChecker(QObject *parent) :
// Allow HTTP redirects // Allow HTTP redirects
m_Nam.setRedirectPolicy(QNetworkRequest::NoLessSafeRedirectPolicy); m_Nam.setRedirectPolicy(QNetworkRequest::NoLessSafeRedirectPolicy);
connect(&m_Nam, SIGNAL(finished(QNetworkReply*)), connect(&m_Nam, &QNetworkAccessManager::finished,
this, SLOT(handleUpdateCheckRequestFinished(QNetworkReply*))); this, &AutoUpdateChecker::handleUpdateCheckRequestFinished);
QString currentVersion(VERSION_STR); QString currentVersion(VERSION_STR);
qDebug() << "Current Moonlight version:" << currentVersion; qDebug() << "Current Moonlight version:" << currentVersion;
@@ -63,11 +63,41 @@ QString AutoUpdateChecker::getPlatform()
return QStringLiteral("steamlink"); return QStringLiteral("steamlink");
#elif defined(APP_IMAGE) #elif defined(APP_IMAGE)
return QStringLiteral("appimage"); return QStringLiteral("appimage");
#elif defined(Q_OS_DARWIN) && QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
// Qt 6 changed this from 'osx' to 'macos'. Use the old one
// to be consistent (and not require another entry in the manifest).
return QStringLiteral("osx");
#else #else
return QSysInfo::productType(); return QSysInfo::productType();
#endif #endif
} }
int AutoUpdateChecker::compareVersion(QVector<int>& version1, QVector<int>& version2) {
for (int i = 0;; i++) {
int v1Val = 0;
int v2Val = 0;
// Treat missing decimal places as 0
if (i < version1.count()) {
v1Val = version1[i];
}
if (i < version2.count()) {
v2Val = version2[i];
}
if (i >= version1.count() && i >= version2.count()) {
// Equal versions
return 0;
}
if (v1Val < v2Val) {
return -1;
}
else if (v1Val > v2Val) {
return 1;
}
}
}
void AutoUpdateChecker::handleUpdateCheckRequestFinished(QNetworkReply* reply) void AutoUpdateChecker::handleUpdateCheckRequestFinished(QNetworkReply* reply)
{ {
Q_ASSERT(reply->isFinished()); Q_ASSERT(reply->isFinished());
@@ -119,46 +149,47 @@ void AutoUpdateChecker::handleUpdateCheckRequestFinished(QNetworkReply* reply)
if (updateObj["arch"] == QSysInfo::buildCpuArchitecture() && if (updateObj["arch"] == QSysInfo::buildCpuArchitecture() &&
updateObj["platform"] == getPlatform()) { updateObj["platform"] == getPlatform()) {
// Check the kernel version minimum if one exists
if (updateObj.contains("kernel_version_at_least") && updateObj["kernel_version_at_least"].isString()) {
QVector<int> requiredVersionQuad;
QVector<int> actualVersionQuad;
QString requiredVersion = updateObj["kernel_version_at_least"].toString();
QString actualVersion = QSysInfo::kernelVersion();
parseStringToVersionQuad(requiredVersion, requiredVersionQuad);
parseStringToVersionQuad(actualVersion, actualVersionQuad);
if (compareVersion(actualVersionQuad, requiredVersionQuad) < 0) {
qDebug() << "Skipping manifest entry due to kernel version (" << actualVersion << "<" << requiredVersion << ")";
continue;
}
}
qDebug() << "Found update manifest match for current platform"; qDebug() << "Found update manifest match for current platform";
QString latestVersion = updateObj["version"].toString(); QString latestVersion = updateObj["version"].toString();
qDebug() << "Latest version of Moonlight for this platform is:" << latestVersion; qDebug() << "Latest version of Moonlight for this platform is:" << latestVersion;
QVector<int> latestVersionQuad; QVector<int> latestVersionQuad;
parseStringToVersionQuad(latestVersion, latestVersionQuad); parseStringToVersionQuad(latestVersion, latestVersionQuad);
for (int i = 0;; i++) { int res = compareVersion(m_CurrentVersionQuad, latestVersionQuad);
int latestVer = 0; if (res < 0) {
int currentVer = 0; // m_CurrentVersionQuad < latestVersionQuad
qDebug() << "Update available";
// Treat missing decimal places as 0 emit onUpdateAvailable(updateObj["version"].toString(),
if (i < latestVersionQuad.count()) { updateObj["browser_url"].toString());
latestVer = latestVersionQuad[i]; return;
} }
if (i < m_CurrentVersionQuad.count()) { else if (res > 0) {
currentVer = m_CurrentVersionQuad[i]; qDebug() << "Update manifest version lower than current version";
} return;
if (i >= latestVersionQuad.count() && i >= m_CurrentVersionQuad.count()) { }
break; else {
} qDebug() << "Update manifest version equal to current version";
return;
if (currentVer < latestVer) {
qDebug() << "Update available";
emit onUpdateAvailable(updateObj["version"].toString(),
updateObj["browser_url"].toString());
return;
}
else if (currentVer > latestVer) {
qDebug() << "Update manifest version lower than current version";
return;
}
} }
qDebug() << "Update manifest version equal to current version";
return;
} }
} }
else { else {
@@ -166,8 +197,8 @@ void AutoUpdateChecker::handleUpdateCheckRequestFinished(QNetworkReply* reply)
} }
} }
qWarning() << "No entry in update manifest found for current platform: " qWarning() << "No entry in update manifest found for current platform:"
<< QSysInfo::buildCpuArchitecture() << getPlatform(); << QSysInfo::buildCpuArchitecture() << getPlatform() << QSysInfo::kernelVersion();
} }
else { else {
qWarning() << "Update checking failed with error: " << reply->error(); qWarning() << "Update checking failed with error: " << reply->error();
+2
View File
@@ -20,6 +20,8 @@ private slots:
private: private:
void parseStringToVersionQuad(QString& string, QVector<int>& version); void parseStringToVersionQuad(QString& string, QVector<int>& version);
int compareVersion(QVector<int>& version1, QVector<int>& version2);
QString getPlatform(); QString getPlatform();
QVector<int> m_CurrentVersionQuad; QVector<int> m_CurrentVersionQuad;
+2 -2
View File
@@ -46,8 +46,8 @@ public:
m_Computer(computer), m_Computer(computer),
m_App(app) m_App(app)
{ {
connect(this, SIGNAL(boxArtFetchCompleted(NvComputer*,NvApp,QUrl)), connect(this, &NetworkBoxArtLoadTask::boxArtFetchCompleted,
boxArtManager, SLOT(handleBoxArtLoadComplete(NvComputer*,NvApp,QUrl))); boxArtManager, &BoxArtManager::handleBoxArtLoadComplete);
} }
signals: signals:
+9 -6
View File
@@ -146,7 +146,8 @@ private:
ComputerManager::ComputerManager(QObject *parent) ComputerManager::ComputerManager(QObject *parent)
: QObject(parent), : QObject(parent),
m_PollingRef(0), m_PollingRef(0),
m_MdnsBrowser(nullptr) m_MdnsBrowser(nullptr),
m_CompatFetcher(nullptr)
{ {
QSettings settings; QSettings settings;
@@ -159,12 +160,15 @@ ComputerManager::ComputerManager(QObject *parent)
} }
settings.endArray(); settings.endArray();
// Fetch latest compatibility data asynchronously
m_CompatFetcher.start();
// To quit in a timely manner, we must block additional requests // To quit in a timely manner, we must block additional requests
// after we receive the aboutToQuit() signal. This is neccessary // after we receive the aboutToQuit() signal. This is neccessary
// because NvHTTP uses aboutToQuit() to abort requests in progres // because NvHTTP uses aboutToQuit() to abort requests in progres
// while quitting, however this is a one time signal - additional // while quitting, however this is a one time signal - additional
// requests would not be aborted and block termination. // requests would not be aborted and block termination.
connect(QCoreApplication::instance(), SIGNAL(aboutToQuit()), this, SLOT(handleAboutToQuit())); connect(QCoreApplication::instance(), &QCoreApplication::aboutToQuit, this, &ComputerManager::handleAboutToQuit);
} }
ComputerManager::~ComputerManager() ComputerManager::~ComputerManager()
@@ -302,8 +306,8 @@ void ComputerManager::startPollingComputer(NvComputer* computer)
if (!pollingEntry->isActive()) { if (!pollingEntry->isActive()) {
PcMonitorThread* thread = new PcMonitorThread(computer); PcMonitorThread* thread = new PcMonitorThread(computer);
connect(thread, SIGNAL(computerStateChanged(NvComputer*)), connect(thread, &PcMonitorThread::computerStateChanged,
this, SLOT(handleComputerStateChanged(NvComputer*))); this, &ComputerManager::handleComputerStateChanged);
pollingEntry->setActiveThread(thread); pollingEntry->setActiveThread(thread);
thread->start(); thread->start();
} }
@@ -337,7 +341,6 @@ void ComputerManager::handleMdnsServiceResolved(MdnsPendingComputer* computer,
address.isInSubnet(QHostAddress("fec0::"), 10) || address.isInSubnet(QHostAddress("fec0::"), 10) ||
address.isInSubnet(QHostAddress("fc00::"), 7)) { address.isInSubnet(QHostAddress("fc00::"), 7)) {
addNewHost(address.toString(), true, v6Global); addNewHost(address.toString(), true, v6Global);
added = true;
break; break;
} }
} }
@@ -418,7 +421,7 @@ void ComputerManager::deleteHost(NvComputer* computer)
void ComputerManager::renameHost(NvComputer* computer, QString name) void ComputerManager::renameHost(NvComputer* computer, QString name)
{ {
{ {
QWriteLocker(&computer->lock); QWriteLocker lock(&computer->lock);
computer->name = name; computer->name = name;
computer->hasCustomName = true; computer->hasCustomName = true;
+3 -1
View File
@@ -2,6 +2,7 @@
#include "nvcomputer.h" #include "nvcomputer.h"
#include "nvpairingmanager.h" #include "nvpairingmanager.h"
#include "settings/compatfetcher.h"
#include <qmdnsengine/server.h> #include <qmdnsengine/server.h>
#include <qmdnsengine/cache.h> #include <qmdnsengine/cache.h>
@@ -70,7 +71,7 @@ private:
m_Resolver = new QMdnsEngine::Resolver(m_Server, m_Hostname); m_Resolver = new QMdnsEngine::Resolver(m_Server, m_Hostname);
connect(m_Resolver, &QMdnsEngine::Resolver::resolved, connect(m_Resolver, &QMdnsEngine::Resolver::resolved,
this, &MdnsPendingComputer::handleResolvedAddress); this, &MdnsPendingComputer::handleResolvedAddress);
QTimer::singleShot(2000, this, SLOT(handleResolvedTimeout())); QTimer::singleShot(2000, this, &MdnsPendingComputer::handleResolvedTimeout);
} }
QByteArray m_Hostname; QByteArray m_Hostname;
@@ -214,4 +215,5 @@ private:
QMdnsEngine::Browser* m_MdnsBrowser; QMdnsEngine::Browser* m_MdnsBrowser;
QMdnsEngine::Cache m_MdnsCache; QMdnsEngine::Cache m_MdnsCache;
QVector<MdnsPendingComputer*> m_PendingResolution; QVector<MdnsPendingComputer*> m_PendingResolution;
CompatFetcher m_CompatFetcher;
}; };
+4 -1
View File
@@ -143,7 +143,10 @@ QSslKey
IdentityManager::getSslKey() IdentityManager::getSslKey()
{ {
if (m_CachedSslKey.isNull()) { if (m_CachedSslKey.isNull()) {
BIO* bio = BIO_new_mem_buf(m_CachedPrivateKey.data(), -1); // This seemingly useless const_cast is required for old OpenSSL headers
// where BIO_new_mem_buf's parameter is not declared const like those on
// the Steam Link hardware.
BIO* bio = BIO_new_mem_buf(const_cast<char*>(m_CachedPrivateKey.constData()), -1);
THROW_BAD_ALLOC_IF_NULL(bio); THROW_BAD_ALLOC_IF_NULL(bio);
EVP_PKEY* pk = PEM_read_bio_PrivateKey(bio, nullptr, nullptr, nullptr); EVP_PKEY* pk = PEM_read_bio_PrivateKey(bio, nullptr, nullptr, nullptr);
+6 -1
View File
@@ -1,5 +1,6 @@
#include "nvcomputer.h" #include "nvcomputer.h"
#include "nvapp.h" #include "nvapp.h"
#include "settings/compatfetcher.h"
#include <QUdpSocket> #include <QUdpSocket>
#include <QHostInfo> #include <QHostInfo>
@@ -30,6 +31,7 @@ NvComputer::NvComputer(QSettings& settings)
this->serverCert = QSslCertificate(settings.value(SER_SRVCERT).toByteArray()); this->serverCert = QSslCertificate(settings.value(SER_SRVCERT).toByteArray());
int appCount = settings.beginReadArray(SER_APPLIST); int appCount = settings.beginReadArray(SER_APPLIST);
this->appList.reserve(appCount);
for (int i = 0; i < appCount; i++) { for (int i = 0; i < appCount; i++) {
settings.setArrayIndex(i); settings.setArrayIndex(i);
@@ -49,6 +51,7 @@ NvComputer::NvComputer(QSettings& settings)
this->serverCodecModeSupport = 0; this->serverCodecModeSupport = 0;
this->pendingQuit = false; this->pendingQuit = false;
this->gpuModel = nullptr; this->gpuModel = nullptr;
this->isSupportedServerVersion = true;
} }
void NvComputer::serialize(QSettings& settings) const void NvComputer::serialize(QSettings& settings) const
@@ -98,7 +101,7 @@ NvComputer::NvComputer(QString address, QString serverInfo, QSslCertificate serv
QString newMacString = NvHTTP::getXmlString(serverInfo, "mac"); QString newMacString = NvHTTP::getXmlString(serverInfo, "mac");
if (newMacString != "00:00:00:00:00:00") { if (newMacString != "00:00:00:00:00:00") {
QStringList macOctets = newMacString.split(':'); QStringList macOctets = newMacString.split(':');
for (QString macOctet : macOctets) { for (const QString& macOctet : macOctets) {
this->macAddress.append((char) macOctet.toInt(nullptr, 16)); this->macAddress.append((char) macOctet.toInt(nullptr, 16));
} }
} }
@@ -142,6 +145,7 @@ NvComputer::NvComputer(QString address, QString serverInfo, QSslCertificate serv
this->activeAddress = address; this->activeAddress = address;
this->state = NvComputer::CS_ONLINE; this->state = NvComputer::CS_ONLINE;
this->pendingQuit = false; this->pendingQuit = false;
this->isSupportedServerVersion = CompatFetcher::isGfeVersionSupported(this->gfeVersion);
} }
bool NvComputer::wake() bool NvComputer::wake()
@@ -407,6 +411,7 @@ bool NvComputer::update(NvComputer& that)
ASSIGN_IF_CHANGED(state); ASSIGN_IF_CHANGED(state);
ASSIGN_IF_CHANGED(gfeVersion); ASSIGN_IF_CHANGED(gfeVersion);
ASSIGN_IF_CHANGED(appVersion); ASSIGN_IF_CHANGED(appVersion);
ASSIGN_IF_CHANGED(isSupportedServerVersion);
ASSIGN_IF_CHANGED(maxLumaPixelsHEVC); ASSIGN_IF_CHANGED(maxLumaPixelsHEVC);
ASSIGN_IF_CHANGED(gpuModel); ASSIGN_IF_CHANGED(gpuModel);
ASSIGN_IF_CHANGED_AND_NONNULL(serverCert); ASSIGN_IF_CHANGED_AND_NONNULL(serverCert);
+1
View File
@@ -65,6 +65,7 @@ public:
int maxLumaPixelsHEVC; int maxLumaPixelsHEVC;
int serverCodecModeSupport; int serverCodecModeSupport;
QString gpuModel; QString gpuModel;
bool isSupportedServerVersion;
// Persisted traits // Persisted traits
QString localAddress; QString localAddress;
+11 -5
View File
@@ -67,7 +67,8 @@ NvHTTP::parseQuad(QString quad)
} }
QStringList parts = quad.split("."); QStringList parts = quad.split(".");
for (int i = 0; i < 4; i++) ret.reserve(parts.length());
for (int i = 0; i < parts.length(); i++)
{ {
ret.append(parts.at(i).toInt()); ret.append(parts.at(i).toInt());
} }
@@ -391,7 +392,7 @@ void NvHTTP::handleSslErrors(QNetworkReply* reply, const QList<QSslError>& error
return; return;
} }
for (auto error : errors) { for (const QSslError& error : errors) {
if (m_ServerCert != error.certificate()) { if (m_ServerCert != error.certificate()) {
ignoreErrors = false; ignoreErrors = false;
break; break;
@@ -450,6 +451,11 @@ NvHTTP::openConnection(QUrl baseUrl,
// Add our client certificate // Add our client certificate
request.setSslConfiguration(IdentityManager::get()->getSslConfig()); request.setSslConfiguration(IdentityManager::get()->getSslConfig());
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
// Disable HTTP/2 (GFE 3.22 doesn't like it) and Qt 6 enables it by default
request.setAttribute(QNetworkRequest::Http2AllowedAttribute, false);
#endif
#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0) && QT_VERSION < QT_VERSION_CHECK(5, 15, 1) && !defined(QT_NO_BEARERMANAGEMENT) #if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0) && QT_VERSION < QT_VERSION_CHECK(5, 15, 1) && !defined(QT_NO_BEARERMANAGEMENT)
// HACK: Set network accessibility to work around QTBUG-80947 (introduced in Qt 5.14.0 and fixed in Qt 5.15.1) // HACK: Set network accessibility to work around QTBUG-80947 (introduced in Qt 5.14.0 and fixed in Qt 5.15.1)
QT_WARNING_PUSH QT_WARNING_PUSH
@@ -462,10 +468,10 @@ NvHTTP::openConnection(QUrl baseUrl,
// Run the request with a timeout if requested // Run the request with a timeout if requested
QEventLoop loop; QEventLoop loop;
connect(reply, SIGNAL(finished()), &loop, SLOT(quit())); connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit);
connect(QCoreApplication::instance(), SIGNAL(aboutToQuit()), &loop, SLOT(quit())); connect(QCoreApplication::instance(), &QCoreApplication::aboutToQuit, &loop, &QEventLoop::quit);
if (timeoutMs) { if (timeoutMs) {
QTimer::singleShot(timeoutMs, &loop, SLOT(quit())); QTimer::singleShot(timeoutMs, &loop, &QEventLoop::quit);
} }
if (logLevel >= NvLogLevel::NVLL_VERBOSE) { if (logLevel >= NvLogLevel::NVLL_VERBOSE) {
qInfo() << "Executing request:" << url.toString(); qInfo() << "Executing request:" << url.toString();
+1
View File
@@ -23,6 +23,7 @@ public:
int height; int height;
int refreshRate; int refreshRate;
}; };
Q_DECLARE_TYPEINFO(NvDisplayMode, Q_PRIMITIVE_TYPE);
class GfeHttpResponseException : public std::exception class GfeHttpResponseException : public std::exception
{ {
+1 -1
View File
@@ -215,7 +215,7 @@ NvPairingManager::pair(QString appVersion, QString pin, QSslCertificate& serverC
QByteArray salt = generateRandomBytes(16); QByteArray salt = generateRandomBytes(16);
QByteArray saltedPin = saltPin(salt, pin); QByteArray saltedPin = saltPin(salt, pin);
QByteArray aesKey = QCryptographicHash::hash(saltedPin, hashAlgo).data(); QByteArray aesKey = QCryptographicHash::hash(saltedPin, hashAlgo).constData();
aesKey.truncate(16); aesKey.truncate(16);
QString getCert = m_Http.openConnectionToString(m_Http.m_BaseUrlHttp, QString getCert = m_Http.openConnectionToString(m_Http.m_BaseUrlHttp,
+62 -7
View File
@@ -14,7 +14,7 @@
SystemProperties::SystemProperties() SystemProperties::SystemProperties()
{ {
versionString = QString(VERSION_STR); versionString = QString(VERSION_STR);
hasWindowManager = WMUtils::isRunningWindowManager(); hasDesktopEnvironment = WMUtils::isRunningDesktopEnvironment();
isRunningWayland = WMUtils::isRunningWayland(); isRunningWayland = WMUtils::isRunningWayland();
isRunningXWayland = isRunningWayland && QGuiApplication::platformName() == "xcb"; isRunningXWayland = isRunningWayland && QGuiApplication::platformName() == "xcb";
QString nativeArch = QSysInfo::currentCpuArchitecture(); QString nativeArch = QSysInfo::currentCpuArchitecture();
@@ -55,12 +55,8 @@ SystemProperties::SystemProperties()
friendlyNativeArchName = nativeArch.toUpper(); friendlyNativeArchName = nativeArch.toUpper();
} }
#ifndef STEAM_LINK
// Assume we can probably launch a browser if we're in a GUI environment // Assume we can probably launch a browser if we're in a GUI environment
hasBrowser = hasWindowManager; hasBrowser = hasDesktopEnvironment;
#else
hasBrowser = false;
#endif
#ifdef HAVE_DISCORD #ifdef HAVE_DISCORD
hasDiscordIntegration = true; hasDiscordIntegration = true;
@@ -91,7 +87,36 @@ QRect SystemProperties::getNativeResolution(int displayIndex)
return monitorNativeResolutions.value(displayIndex); return monitorNativeResolutions.value(displayIndex);
} }
class QuerySdlVideoThread : public QThread
{
public:
QuerySdlVideoThread(SystemProperties* me) :
QThread(nullptr),
m_Me(me) {}
void run() override
{
m_Me->querySdlVideoInfoInternal();
}
SystemProperties* m_Me;
};
void SystemProperties::querySdlVideoInfo() void SystemProperties::querySdlVideoInfo()
{
if (WMUtils::isRunningX11() || WMUtils::isRunningWayland()) {
// Use a separate thread to temporarily initialize SDL
// video to avoid stomping on Qt's X11 and OGL state.
QuerySdlVideoThread thread(this);
thread.start();
thread.wait();
}
else {
querySdlVideoInfoInternal();
}
}
void SystemProperties::querySdlVideoInfoInternal()
{ {
hasHardwareAcceleration = false; hasHardwareAcceleration = false;
@@ -103,7 +128,8 @@ void SystemProperties::querySdlVideoInfo()
} }
// Update display related attributes (max FPS, native resolution, etc). // Update display related attributes (max FPS, native resolution, etc).
refreshDisplays(); // We call the internal variant because we're already in a safe thread context.
refreshDisplaysInternal();
SDL_Window* testWindow = SDL_CreateWindow("", 0, 0, 1280, 720, SDL_Window* testWindow = SDL_CreateWindow("", 0, 0, 1280, 720,
SDL_WINDOW_HIDDEN | StreamUtils::getPlatformWindowFlags()); SDL_WINDOW_HIDDEN | StreamUtils::getPlatformWindowFlags());
@@ -129,7 +155,36 @@ void SystemProperties::querySdlVideoInfo()
SDL_QuitSubSystem(SDL_INIT_VIDEO); SDL_QuitSubSystem(SDL_INIT_VIDEO);
} }
class RefreshDisplaysThread : public QThread
{
public:
RefreshDisplaysThread(SystemProperties* me) :
QThread(nullptr),
m_Me(me) {}
void run() override
{
m_Me->refreshDisplaysInternal();
}
SystemProperties* m_Me;
};
void SystemProperties::refreshDisplays() void SystemProperties::refreshDisplays()
{
if (WMUtils::isRunningX11() || WMUtils::isRunningWayland()) {
// Use a separate thread to temporarily initialize SDL
// video to avoid stomping on Qt's X11 and OGL state.
RefreshDisplaysThread thread(this);
thread.start();
thread.wait();
}
else {
refreshDisplaysInternal();
}
}
void SystemProperties::refreshDisplaysInternal()
{ {
if (SDL_InitSubSystem(SDL_INIT_VIDEO) != 0) { if (SDL_InitSubSystem(SDL_INIT_VIDEO) != 0) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
+7 -2
View File
@@ -7,6 +7,9 @@ class SystemProperties : public QObject
{ {
Q_OBJECT Q_OBJECT
friend class QuerySdlVideoThread;
friend class RefreshDisplaysThread;
public: public:
SystemProperties(); SystemProperties();
@@ -16,7 +19,7 @@ public:
Q_PROPERTY(bool isRunningXWayland MEMBER isRunningXWayland CONSTANT) Q_PROPERTY(bool isRunningXWayland MEMBER isRunningXWayland CONSTANT)
Q_PROPERTY(bool isWow64 MEMBER isWow64 CONSTANT) Q_PROPERTY(bool isWow64 MEMBER isWow64 CONSTANT)
Q_PROPERTY(QString friendlyNativeArchName MEMBER friendlyNativeArchName CONSTANT) Q_PROPERTY(QString friendlyNativeArchName MEMBER friendlyNativeArchName CONSTANT)
Q_PROPERTY(bool hasWindowManager MEMBER hasWindowManager CONSTANT) Q_PROPERTY(bool hasDesktopEnvironment MEMBER hasDesktopEnvironment CONSTANT)
Q_PROPERTY(bool hasBrowser MEMBER hasBrowser CONSTANT) Q_PROPERTY(bool hasBrowser MEMBER hasBrowser CONSTANT)
Q_PROPERTY(bool hasDiscordIntegration MEMBER hasDiscordIntegration CONSTANT) Q_PROPERTY(bool hasDiscordIntegration MEMBER hasDiscordIntegration CONSTANT)
Q_PROPERTY(QString unmappedGamepads MEMBER unmappedGamepads NOTIFY unmappedGamepadsChanged) Q_PROPERTY(QString unmappedGamepads MEMBER unmappedGamepads NOTIFY unmappedGamepadsChanged)
@@ -33,6 +36,8 @@ signals:
private: private:
void querySdlVideoInfo(); void querySdlVideoInfo();
void querySdlVideoInfoInternal();
void refreshDisplaysInternal();
bool hasHardwareAcceleration; bool hasHardwareAcceleration;
bool rendererAlwaysFullScreen; bool rendererAlwaysFullScreen;
@@ -40,7 +45,7 @@ private:
bool isRunningXWayland; bool isRunningXWayland;
bool isWow64; bool isWow64;
QString friendlyNativeArchName; QString friendlyNativeArchName;
bool hasWindowManager; bool hasDesktopEnvironment;
bool hasBrowser; bool hasBrowser;
bool hasDiscordIntegration; bool hasDiscordIntegration;
QString unmappedGamepads; QString unmappedGamepads;
+14 -11
View File
@@ -52,7 +52,7 @@ public:
void handleUnknownOptions() void handleUnknownOptions()
{ {
if (unknownOptionNames().length()) { if (!unknownOptionNames().isEmpty()) {
showError(QString("Unknown options: %1").arg(unknownOptionNames().join(", "))); showError(QString("Unknown options: %1").arg(unknownOptionNames().join(", ")));
} }
} }
@@ -264,6 +264,11 @@ StreamCommandLineParser::StreamCommandLineParser()
{"software", StreamingPreferences::VDS_FORCE_SOFTWARE}, {"software", StreamingPreferences::VDS_FORCE_SOFTWARE},
{"hardware", StreamingPreferences::VDS_FORCE_HARDWARE}, {"hardware", StreamingPreferences::VDS_FORCE_HARDWARE},
}; };
m_CaptureSysKeysModeMap = {
{"never", StreamingPreferences::CSK_OFF},
{"fullscreen", StreamingPreferences::CSK_FULLSCREEN},
{"always", StreamingPreferences::CSK_ALWAYS},
};
} }
StreamCommandLineParser::~StreamCommandLineParser() StreamCommandLineParser::~StreamCommandLineParser()
@@ -307,7 +312,7 @@ void StreamCommandLineParser::parse(const QStringList &args, StreamingPreference
parser.addToggleOption("background-gamepad", "background gamepad input"); parser.addToggleOption("background-gamepad", "background gamepad input");
parser.addToggleOption("reverse-scroll-direction", "inverted scroll direction"); parser.addToggleOption("reverse-scroll-direction", "inverted scroll direction");
parser.addToggleOption("swap-gamepad-buttons", "swap A/B and X/Y gamepad buttons (Nintendo-style)"); parser.addToggleOption("swap-gamepad-buttons", "swap A/B and X/Y gamepad buttons (Nintendo-style)");
parser.addToggleOption("capture-system-keys", "capture system key combos in fullscreen mode"); parser.addChoiceOption("capture-system-keys", "capture system key combos", m_CaptureSysKeysModeMap.keys());
parser.addChoiceOption("video-codec", "video codec", m_VideoCodecMap.keys()); parser.addChoiceOption("video-codec", "video codec", m_VideoCodecMap.keys());
parser.addChoiceOption("video-decoder", "video decoder", m_VideoDecoderMap.keys()); parser.addChoiceOption("video-decoder", "video decoder", m_VideoDecoderMap.keys());
@@ -320,25 +325,21 @@ void StreamCommandLineParser::parse(const QStringList &args, StreamingPreference
// Resolve display's width and height // Resolve display's width and height
QRegularExpression resolutionRexExp("^(720|1080|1440|4K|resolution)$"); QRegularExpression resolutionRexExp("^(720|1080|1440|4K|resolution)$");
QStringList resoOptions = parser.optionNames().filter(resolutionRexExp); QStringList resoOptions = parser.optionNames().filter(resolutionRexExp);
bool displaySet = resoOptions.length(); bool displaySet = !resoOptions.isEmpty();
if (displaySet) { if (displaySet) {
QString name = resoOptions.last(); QString name = resoOptions.last();
if (name == "720") { if (name == "720") {
preferences->width = 1280; preferences->width = 1280;
preferences->height = 720; preferences->height = 720;
displaySet = true;
} else if (name == "1080") { } else if (name == "1080") {
preferences->width = 1920; preferences->width = 1920;
preferences->height = 1080; preferences->height = 1080;
displaySet = true;
} else if (name == "1440") { } else if (name == "1440") {
preferences->width = 2560; preferences->width = 2560;
preferences->height = 1440; preferences->height = 1440;
displaySet = true;
} else if (name == "4K") { } else if (name == "4K") {
preferences->width = 3840; preferences->width = 3840;
preferences->height = 2160; preferences->height = 2160;
displaySet = true;
} else if (name == "resolution") { } else if (name == "resolution") {
auto resolution = parser.getResolutionOptionValue(name); auto resolution = parser.getResolutionOptionValue(name);
preferences->width = resolution.first; preferences->width = resolution.first;
@@ -349,8 +350,8 @@ void StreamCommandLineParser::parse(const QStringList &args, StreamingPreference
// Resolve --fps option // Resolve --fps option
if (parser.isSet("fps")) { if (parser.isSet("fps")) {
preferences->fps = parser.getIntOption("fps"); preferences->fps = parser.getIntOption("fps");
if (!inRange(preferences->fps, 30, 120)) { if (!inRange(preferences->fps, 30, 240)) {
parser.showError("FPS must be in range: 30 - 120"); parser.showError("FPS must be in range: 30 - 240");
} }
} }
@@ -422,8 +423,10 @@ void StreamCommandLineParser::parse(const QStringList &args, StreamingPreference
// Resolve --swap-gamepad-buttons and --no-swap-gamepad-buttons options // Resolve --swap-gamepad-buttons and --no-swap-gamepad-buttons options
preferences->swapFaceButtons = parser.getToggleOptionValue("swap-gamepad-buttons", preferences->swapFaceButtons); preferences->swapFaceButtons = parser.getToggleOptionValue("swap-gamepad-buttons", preferences->swapFaceButtons);
// Resolve --capture-system-keys and --no-capture-system-keys options // Resolve --capture-system-keys option
preferences->captureSysKeys = parser.getToggleOptionValue("capture-system-keys", preferences->captureSysKeys); if (parser.isSet("capture-system-keys")) {
preferences->captureSysKeysMode = mapValue(m_CaptureSysKeysModeMap, parser.getChoiceOptionValue("capture-system-keys"));
}
// Resolve --video-codec option // Resolve --video-codec option
if (parser.isSet("video-codec")) { if (parser.isSet("video-codec")) {
+1
View File
@@ -53,4 +53,5 @@ private:
QMap<QString, StreamingPreferences::AudioConfig> m_AudioConfigMap; QMap<QString, StreamingPreferences::AudioConfig> m_AudioConfigMap;
QMap<QString, StreamingPreferences::VideoCodecConfig> m_VideoCodecMap; QMap<QString, StreamingPreferences::VideoCodecConfig> m_VideoCodecMap;
QMap<QString, StreamingPreferences::VideoDecoderSelection> m_VideoDecoderMap; QMap<QString, StreamingPreferences::VideoDecoderSelection> m_VideoDecoderMap;
QMap<QString, StreamingPreferences::CaptureSysKeysMode> m_CaptureSysKeysModeMap;
}; };
@@ -15,7 +15,7 @@
<li>Hardware accelerated video decoding with VAAPI, VDPAU, and NVDEC support</li> <li>Hardware accelerated video decoding with VAAPI, VDPAU, and NVDEC support</li>
<li>7.1 surround sound audio 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>Support for both pointer capture (for games) and direct mouse control (for remote desktop)</li>
<li>Support for passing system-wide keyboard shortcuts like Alt+Tab to the host in fullscreen mode on X11</li> <li>Support for passing system-wide keyboard shortcuts like Alt+Tab to the host</li>
<li>Direct touchscreen mouse control</li> <li>Direct touchscreen mouse control</li>
<li>HEVC support for better video compression efficiency</li> <li>HEVC support for better video compression efficiency</li>
<li>Gamepad support with force feedback for up to 4 players</li> <li>Gamepad support with force feedback for up to 4 players</li>
@@ -36,6 +36,71 @@
</screenshots> </screenshots>
<releases> <releases>
<release version="3.1.4" date="2021-06-25">
<description>
<p>Changes:</p>
<ul>
<li>Added network latency statistics to the performance overlay</li>
<li>Improved audio quality on unreliable connections (requires GeForce Experience 3.19 or later)</li>
<li>Reduced video frame drops on unreliable connections (requires GeForce Experience 3.22 or later)</li>
<li>Updated community-contributed translations from Weblate</li>
</ul>
</description>
</release>
<release version="3.1.3" date="2021-05-07">
<description>
<p>Changes:</p>
<ul>
<li>Fixed an issue that could cause excessive audio latency on some connections</li>
<li>Updated community-contributed translations from Weblate</li>
</ul>
</description>
</release>
<release version="3.1.2" date="2021-05-03">
<description>
<p>Changes:</p>
<ul>
<li>Improved connection reliability and performance with GeForce Experience 3.22</li>
<li>Added support for audio encryption with GeForce Experience 3.22</li>
<li>Added compatibility update checks for future versions of GeForce Experience</li>
<li>Fixed keyboard capture on X11</li>
<li>Updated community-contributed translations from Weblate</li>
</ul>
</description>
</release>
<release version="3.1.1" date="2021-04-09">
<description>
<p>Changes:</p>
<ul>
<li>Added support for streaming from GeForce Experience 3.22</li>
<li>Added support for AMF-encoded HEVC streams from servers like Sunshine and OpenStream</li>
<li>Added new community-contributed translations from Weblate</li>
<li>Fixed KMSDRM rendering with buffer modifiers on embedded devices</li>
</ul>
</description>
</release>
<release version="3.1.0" date="2021-03-14">
<description>
<p>New features:</p>
<ul>
<li>System keyboard shortcuts (Alt+Tab, Super key, Ctrl+Shift+Esc, etc.) can now be (optionally) captured in windowed mode</li>
<li>System keyboard shortcuts can now be captured under Wayland</li>
<li>Added support for typing the client's clipboard text on the host by pressing Ctrl+Alt+Shift+V</li>
<li>Added Ctrl+Alt+Shift+D key combo to minimize the streaming window</li>
<li>Added Ctrl+Alt+Shift+C key combo to show the local cursor while streaming in remote desktop mouse mode</li>
<li>Added Select+L1+R1+X gamepad button combo to toggle performance overlay</li>
<li>Added translations for Simplified Chinese and German</li>
<li>Added a language selector on the settings page</li>
</ul>
<p>Bugfixes:</p>
<ul>
<li>Improved performance of loading the app grid</li>
<li>Fixed video scaling in High DPI mode on Wayland</li>
<li>Fixed window icons on Wayland</li>
<li>Fixed number pad enter key functionality in the UI</li>
</ul>
</description>
</release>
<release version="3.0.0" date="2021-01-17"> <release version="3.0.0" date="2021-01-17">
<description> <description>
<p>New features:</p> <p>New features:</p>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 13 KiB

+1 -11
View File
@@ -5,19 +5,9 @@
# pairing data will be lost each reboot. # pairing data will be lost each reboot.
HOME=/usr/local/moonlight 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 PE_Single_CPU which seems to host A/V stuff
renice -10 -p $(pidof PE_Single_CPU) renice -10 -p $(pidof PE_Single_CPU)
# Renice Moonlight itself to avoid preemption by background tasks # Renice Moonlight itself to avoid preemption by background tasks
# Write output to a logfile in /tmp # Write output to a logfile in /tmp
echo Launching $BIN exec nice -n -10 ./bin/moonlight > /tmp/moonlight.log
exec nice -n -10 ./bin/$BIN > /tmp/moonlight.log
+136 -100
View File
@@ -73,6 +73,9 @@ CenteredGridView {
width: 220; height: 287; width: 220; height: 287;
grid: appGrid grid: appGrid
property alias appContextMenu: appContextMenuLoader.item
property alias appNameText: appNameTextLoader.item
// Dim the app if it's hidden // Dim the app if it's hidden
opacity: model.hidden ? 0.4 : 1.0 opacity: model.hidden ? 0.4 : 1.0
@@ -85,9 +88,14 @@ CenteredGridView {
source: model.boxart source: model.boxart
onSourceSizeChanged: { onSourceSizeChanged: {
if ((sourceSize.width == 130 && sourceSize.height == 180) || // GFE 2.0 placeholder image // Nearly all of Nvidia's official box art does not match the dimensions of placeholder
(sourceSize.width == 628 && sourceSize.height == 888) || // GFE 3.0 placeholder image // images, however the one known exeception is Overcooked. Therefore, we only execute
(sourceSize.width == 200 && sourceSize.height == 266)) // Our no_app_image.png // the image size checks if this is not an app collector game. We know the officially
// supported games all have box art, so this check is not required.
if (!model.isAppCollectorGame &&
((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 isPlaceholder = true
} }
@@ -104,81 +112,94 @@ CenteredGridView {
ToolTip.text: model.name ToolTip.text: model.name
ToolTip.delay: 1000 ToolTip.delay: 1000
ToolTip.timeout: 5000 ToolTip.timeout: 5000
ToolTip.visible: (parent.hovered || parent.highlighted) && (!appNameText.visible || appNameText.truncated) ToolTip.visible: (parent.hovered || parent.highlighted) && (!appNameText || appNameText.truncated)
} }
ToolButton { Loader {
id: resumeButton active: model.running
anchors.horizontalCenterOffset: appIcon.isPlaceholder ? -47 : 0 asynchronous: true
anchors.verticalCenterOffset: appIcon.isPlaceholder ? -75 : -60 anchors.fill: appIcon
anchors.centerIn: appIcon
visible: model.running
implicitWidth: 125
implicitHeight: 125
Image { sourceComponent: Item {
source: "qrc:/res/baseline-play_circle_filled_white-48px.svg" ToolButton {
anchors.centerIn: parent anchors.horizontalCenterOffset: appIcon.isPlaceholder ? -47 : 0
sourceSize { anchors.verticalCenterOffset: appIcon.isPlaceholder ? -75 : -60
width: 75 anchors.centerIn: parent
height: 75 implicitWidth: 125
implicitHeight: 125
Image {
source: "qrc:/res/baseline-play_circle_filled_white-48px.svg"
anchors.centerIn: parent
sourceSize {
width: 75
height: 75
}
}
onClicked: {
launchOrResumeSelectedApp(true)
}
ToolTip.text: qsTr("Resume Game")
ToolTip.delay: 1000
ToolTip.timeout: 3000
ToolTip.visible: hovered
}
ToolButton {
anchors.horizontalCenterOffset: appIcon.isPlaceholder ? 47 : 0
anchors.verticalCenterOffset: appIcon.isPlaceholder ? -75 : 60
anchors.centerIn: parent
implicitWidth: 125
implicitHeight: 125
Image {
source: "qrc:/res/baseline-cancel-24px.svg"
anchors.centerIn: parent
sourceSize {
width: 75
height: 75
}
}
onClicked: {
doQuitGame()
}
ToolTip.text: qsTr("Quit Game")
ToolTip.delay: 1000
ToolTip.timeout: 3000
ToolTip.visible: hovered
} }
} }
onClicked: {
launchOrResumeSelectedApp(true)
}
ToolTip.text: qsTr("Resume Game")
ToolTip.delay: 1000
ToolTip.timeout: 3000
ToolTip.visible: hovered
} }
ToolButton { Loader {
id: quitButton id: appNameTextLoader
anchors.horizontalCenterOffset: appIcon.isPlaceholder ? 47 : 0 active: appIcon.isPlaceholder
anchors.verticalCenterOffset: appIcon.isPlaceholder ? -75 : 60
anchors.centerIn: appIcon
visible: model.running
implicitWidth: 125
implicitHeight: 125
Image { // This loader is not asynchronous to avoid noticeable differences
source: "qrc:/res/baseline-cancel-24px.svg" // in the time in which the text loads for each game.
anchors.centerIn: parent
sourceSize {
width: 75
height: 75
}
}
onClicked: {
doQuitGame()
}
ToolTip.text: qsTr("Quit Game")
ToolTip.delay: 1000
ToolTip.timeout: 3000
ToolTip.visible: hovered
}
Label {
id: appNameText
visible: appIcon.isPlaceholder
text: model.name
width: appIcon.width width: appIcon.width
height: model.running ? 175 : appIcon.height height: model.running ? 175 : appIcon.height
leftPadding: 20
rightPadding: 20
anchors.left: appIcon.left anchors.left: appIcon.left
anchors.right: appIcon.right anchors.right: appIcon.right
anchors.bottom: appIcon.bottom anchors.bottom: appIcon.bottom
font.pointSize: 22
verticalAlignment: Text.AlignVCenter sourceComponent: Label {
horizontalAlignment: Text.AlignHCenter id: appNameText
wrapMode: Text.Wrap text: model.name
elide: Text.ElideRight font.pointSize: 22
leftPadding: 20
rightPadding: 20
verticalAlignment: Text.AlignVCenter
horizontalAlignment: Text.AlignHCenter
wrapMode: Text.Wrap
elide: Text.ElideRight
}
} }
function launchOrResumeSelectedApp(quitExistingApp) function launchOrResumeSelectedApp(quitExistingApp)
@@ -245,6 +266,17 @@ CenteredGridView {
} }
} }
Keys.onEnterPressed: {
// Open the app context menu if activated via the gamepad or keyboard
// for running games. If the game isn't running, the above onClicked
// method will handle the launch.
if (model.running) {
// This will be keyboard/gamepad driven so use
// open() instead of popup()
appContextMenu.open()
}
}
Keys.onMenuPressed: { Keys.onMenuPressed: {
// This will be keyboard/gamepad driven so use open() instead of popup() // This will be keyboard/gamepad driven so use open() instead of popup()
appContextMenu.open() appContextMenu.open()
@@ -256,44 +288,48 @@ CenteredGridView {
quitAppDialog.open() quitAppDialog.open()
} }
NavigableMenu { Loader {
id: appContextMenu id: appContextMenuLoader
NavigableMenuItem { asynchronous: true
parentMenu: appContextMenu sourceComponent: NavigableMenu {
text: model.running ? qsTr("Resume Game") : qsTr("Launch Game") id: appContextMenu
onTriggered: launchOrResumeSelectedApp(true) NavigableMenuItem {
} parentMenu: appContextMenu
NavigableMenuItem { text: model.running ? qsTr("Resume Game") : qsTr("Launch Game")
parentMenu: appContextMenu onTriggered: launchOrResumeSelectedApp(true)
text: qsTr("Quit Game") }
onTriggered: doQuitGame() NavigableMenuItem {
visible: model.running parentMenu: appContextMenu
} text: qsTr("Quit Game")
NavigableMenuItem { onTriggered: doQuitGame()
parentMenu: appContextMenu visible: model.running
checkable: true }
checked: model.directLaunch NavigableMenuItem {
text: qsTr("Direct Launch") parentMenu: appContextMenu
onTriggered: appModel.setAppDirectLaunch(model.index, !model.directLaunch) checkable: true
enabled: !model.hidden checked: model.directLaunch
text: qsTr("Direct Launch")
onTriggered: appModel.setAppDirectLaunch(model.index, !model.directLaunch)
enabled: !model.hidden
ToolTip.text: qsTr("Launch this app immediately when the host is selected, bypassing the app selection grid.") ToolTip.text: qsTr("Launch this app immediately when the host is selected, bypassing the app selection grid.")
ToolTip.delay: 1000 ToolTip.delay: 1000
ToolTip.timeout: 3000 ToolTip.timeout: 3000
ToolTip.visible: hovered ToolTip.visible: hovered
} }
NavigableMenuItem { NavigableMenuItem {
parentMenu: appContextMenu parentMenu: appContextMenu
checkable: true checkable: true
checked: model.hidden checked: model.hidden
text: qsTr("Hide Game") text: qsTr("Hide Game")
onTriggered: appModel.setAppHidden(model.index, !model.hidden) onTriggered: appModel.setAppHidden(model.index, !model.hidden)
enabled: model.hidden || (!model.running && !model.directLaunch) enabled: model.hidden || (!model.running && !model.directLaunch)
ToolTip.text: qsTr("Hide this game from the app grid. To access hidden games, right-click on the host and choose %1.").arg(qsTr("View All Apps")) ToolTip.text: qsTr("Hide this game from the app grid. To access hidden games, right-click on the host and choose %1.").arg(qsTr("View All Apps"))
ToolTip.delay: 1000 ToolTip.delay: 1000
ToolTip.timeout: 5000 ToolTip.timeout: 5000
ToolTip.visible: hovered ToolTip.visible: hovered
}
} }
} }
} }
+3
View File
@@ -21,4 +21,7 @@ ItemDelegate {
Keys.onReturnPressed: { Keys.onReturnPressed: {
clicked() clicked()
} }
Keys.onEnterPressed: {
clicked()
}
} }
+4
View File
@@ -22,6 +22,10 @@ MenuItem {
triggered() triggered()
} }
Keys.onEnterPressed: {
triggered()
}
Keys.onEscapePressed: { Keys.onEscapePressed: {
parentMenu.close() parentMenu.close()
} }
+4
View File
@@ -56,6 +56,10 @@ NavigableDialog {
accept() accept()
} }
Keys.onEnterPressed: {
accept()
}
Keys.onEscapePressed: { Keys.onEscapePressed: {
reject() reject()
} }
+4
View File
@@ -27,6 +27,10 @@ ToolButton {
clicked() clicked()
} }
Keys.onEnterPressed: {
clicked()
}
Keys.onRightPressed: { Keys.onRightPressed: {
nextItemInFocusChain(true).forceActiveFocus(Qt.TabFocus) nextItemInFocusChain(true).forceActiveFocus(Qt.TabFocus)
} }
+60 -45
View File
@@ -113,6 +113,8 @@ CenteredGridView {
width: 300; height: 320; width: 300; height: 320;
grid: pcGrid grid: pcGrid
property alias pcContextMenu : pcContextMenuLoader.item
Image { Image {
id: pcIcon id: pcIcon
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
@@ -160,61 +162,70 @@ CenteredGridView {
elide: Text.ElideRight elide: Text.ElideRight
} }
NavigableMenu { Loader {
id: pcContextMenu id: pcContextMenuLoader
MenuItem { asynchronous: true
text: qsTr("PC Status: %1").arg(model.online ? qsTr("Online") : qsTr("Offline")) sourceComponent: NavigableMenu {
font.bold: true id: pcContextMenu
enabled: false MenuItem {
} text: qsTr("PC Status: %1").arg(model.online ? qsTr("Online") : qsTr("Offline"))
NavigableMenuItem { font.bold: true
parentMenu: pcContextMenu enabled: false
text: qsTr("View All Apps")
onTriggered: {
var component = Qt.createComponent("AppView.qml")
var appView = component.createObject(stackView, {"computerIndex": index, "objectName": model.name, "showHiddenGames": true})
stackView.push(appView)
} }
visible: model.online && model.paired NavigableMenuItem {
} parentMenu: pcContextMenu
NavigableMenuItem { text: qsTr("View All Apps")
parentMenu: pcContextMenu onTriggered: {
text: qsTr("Wake PC") var component = Qt.createComponent("AppView.qml")
onTriggered: computerModel.wakeComputer(index) var appView = component.createObject(stackView, {"computerIndex": index, "objectName": model.name, "showHiddenGames": true})
visible: !model.online && model.wakeable stackView.push(appView)
} }
NavigableMenuItem { visible: model.online && model.paired
parentMenu: pcContextMenu }
text: qsTr("Test Network") NavigableMenuItem {
onTriggered: { parentMenu: pcContextMenu
computerModel.testConnectionForComputer(index) text: qsTr("Wake PC")
testConnectionDialog.open() onTriggered: computerModel.wakeComputer(index)
visible: !model.online && model.wakeable
}
NavigableMenuItem {
parentMenu: pcContextMenu
text: qsTr("Test Network")
onTriggered: {
computerModel.testConnectionForComputer(index)
testConnectionDialog.open()
}
} }
}
NavigableMenuItem { NavigableMenuItem {
parentMenu: pcContextMenu parentMenu: pcContextMenu
text: qsTr("Rename PC") text: qsTr("Rename PC")
onTriggered: { onTriggered: {
renamePcDialog.pcIndex = index renamePcDialog.pcIndex = index
renamePcDialog.originalName = model.name renamePcDialog.originalName = model.name
renamePcDialog.open() renamePcDialog.open()
}
} }
} NavigableMenuItem {
NavigableMenuItem { parentMenu: pcContextMenu
parentMenu: pcContextMenu text: qsTr("Delete PC")
text: qsTr("Delete PC") onTriggered: {
onTriggered: { deletePcDialog.pcIndex = index
deletePcDialog.pcIndex = index // get confirmation first, actual closing is called from the dialog
// get confirmation first, actual closing is called from the dialog deletePcDialog.open()
deletePcDialog.open() }
} }
} }
} }
onClicked: { onClicked: {
if (model.online) { if (model.online) {
if (model.paired) { if (!model.serverSupported) {
errorDialog.text = qsTr("The version of GeForce Experience on %1 is not supported by this build of Moonlight. You must update Moonlight to stream from %1.").arg(model.name)
errorDialog.helpText = ""
errorDialog.open()
}
else if (model.paired) {
// go to game view // go to game view
var component = Qt.createComponent("AppView.qml") var component = Qt.createComponent("AppView.qml")
var appView = component.createObject(stackView, {"computerIndex": index, "objectName": model.name}) var appView = component.createObject(stackView, {"computerIndex": index, "objectName": model.name})
@@ -379,6 +390,10 @@ CenteredGridView {
Keys.onReturnPressed: { Keys.onReturnPressed: {
renamePcDialog.accept() renamePcDialog.accept()
} }
Keys.onEnterPressed: {
renamePcDialog.accept()
}
} }
} }
} }
+205 -46
View File
@@ -358,6 +358,10 @@ Flickable {
Keys.onReturnPressed: { Keys.onReturnPressed: {
customResolutionDialog.accept() customResolutionDialog.accept()
} }
Keys.onEnterPressed: {
customResolutionDialog.accept()
}
} }
Label { Label {
@@ -382,6 +386,10 @@ Flickable {
Keys.onReturnPressed: { Keys.onReturnPressed: {
customResolutionDialog.accept() customResolutionDialog.accept()
} }
Keys.onEnterPressed: {
customResolutionDialog.accept()
}
} }
} }
} }
@@ -519,12 +527,17 @@ Flickable {
text: qsTr("Display mode") text: qsTr("Display mode")
font.pointSize: 12 font.pointSize: 12
wrapMode: Text.Wrap wrapMode: Text.Wrap
visible: SystemProperties.hasWindowManager visible: SystemProperties.hasDesktopEnvironment
} }
AutoResizingComboBox { AutoResizingComboBox {
// ignore setting the index at first, and actually set it when the component is loaded // ignore setting the index at first, and actually set it when the component is loaded
Component.onCompleted: { Component.onCompleted: {
if (!visible) {
// Do nothing if the control won't even be visible
return
}
// Set the recommended option based on the OS // Set the recommended option based on the OS
for (var i = 0; i < windowModeListModel.count; i++) { for (var i = 0; i < windowModeListModel.count; i++) {
var thisWm = windowModeListModel.get(i).val; var thisWm = windowModeListModel.get(i).val;
@@ -537,22 +550,20 @@ Flickable {
currentIndex = 0 currentIndex = 0
if (SystemProperties.hasWindowManager && !SystemProperties.rendererAlwaysFullScreen) { var savedWm = StreamingPreferences.windowMode
var savedWm = StreamingPreferences.windowMode for (var i = 0; i < windowModeListModel.count; i++) {
for (var i = 0; i < windowModeListModel.count; i++) { var thisWm = windowModeListModel.get(i).val;
var thisWm = windowModeListModel.get(i).val; if (savedWm === thisWm) {
if (savedWm === thisWm) { currentIndex = i
currentIndex = i break
break }
}
}
} }
activated(currentIndex) activated(currentIndex)
} }
id: windowModeComboBox id: windowModeComboBox
visible: SystemProperties.hasWindowManager visible: SystemProperties.hasDesktopEnvironment
enabled: !SystemProperties.rendererAlwaysFullScreen enabled: !SystemProperties.rendererAlwaysFullScreen
hoverEnabled: true hoverEnabled: true
textRole: "text" textRole: "text"
@@ -697,7 +708,7 @@ Flickable {
width: parent.width width: parent.width
text: qsTr("Mute audio stream when Moonlight is not the active window") text: qsTr("Mute audio stream when Moonlight is not the active window")
font.pointSize: 12 font.pointSize: 12
visible: SystemProperties.hasWindowManager visible: SystemProperties.hasDesktopEnvironment
checked: StreamingPreferences.muteOnFocusLoss checked: StreamingPreferences.muteOnFocusLoss
onCheckedChanged: { onCheckedChanged: {
StreamingPreferences.muteOnFocusLoss = checked StreamingPreferences.muteOnFocusLoss = checked
@@ -724,37 +735,132 @@ Flickable {
Label { Label {
width: parent.width width: parent.width
id: uiDisplayModeTitle id: languageTitle
text: qsTr("GUI display mode") text: qsTr("Language")
font.pointSize: 12 font.pointSize: 12
wrapMode: Text.Wrap wrapMode: Text.Wrap
visible: SystemProperties.hasWindowManager
} }
AutoResizingComboBox { AutoResizingComboBox {
// ignore setting the index at first, and actually set it when the component is loaded // ignore setting the index at first, and actually set it when the component is loaded
Component.onCompleted: { Component.onCompleted: {
if (SystemProperties.hasWindowManager) { var saved_language = StreamingPreferences.language
var saved_uidisplaymode = StreamingPreferences.uiDisplayMode currentIndex = 0
currentIndex = 0 for (var i = 0; i < languageListModel.count; i++) {
for (var i = 0; i < uiDisplayModeListModel.count; i++) { var el_language = languageListModel.get(i).val;
var el_uidisplaymode = uiDisplayModeListModel.get(i).val; if (saved_language === el_language) {
if (saved_uidisplaymode === el_uidisplaymode) { currentIndex = i
currentIndex = i break
break
}
} }
} }
else {
// Full-screen is always selected when there is no window manager activated(currentIndex)
currentIndex = 2 }
id: languageComboBox
textRole: "text"
model: ListModel {
id: languageListModel
ListElement {
text: qsTr("Automatic")
val: StreamingPreferences.LANG_AUTO
}
ListElement {
text: "Deutsch" //German
val: StreamingPreferences.LANG_DE
}
ListElement {
text: "English"
val: StreamingPreferences.LANG_EN
}
ListElement {
text: "Français" //French
val: StreamingPreferences.LANG_FR
}
ListElement {
text: "简体中文" //Simplified Chinese
val: StreamingPreferences.LANG_ZH_CN
}
ListElement {
text: "Norwegian Bokmål"
val: StreamingPreferences.LANG_NB_NO
}
ListElement {
text: "русский" // Russian
val: StreamingPreferences.LANG_RU
}
// Don't list Spanish until it is more complete
/*ListElement {
text: "Español" // Spanish
val: StreamingPreferences.LANG_ES
}*/
ListElement {
text: "日本語" // Japanese
val: StreamingPreferences.LANG_JA
}
ListElement {
text: "Tiếng Việt" // Vietnamese
val: StreamingPreferences.LANG_VI
}
ListElement {
text: "ภาษาไทย" // Thai
val: StreamingPreferences.LANG_TH
}
ListElement {
text: "한국어" // Korean
val: StreamingPreferences.LANG_KO
}
}
// ::onActivated must be used, as it only listens for when the index is changed by a human
onActivated : {
// Retranslating is expensive, so only do it if the language actually changed
var new_language = languageListModel.get(currentIndex).val
if (StreamingPreferences.language !== new_language) {
StreamingPreferences.language = languageListModel.get(currentIndex).val
if (!StreamingPreferences.retranslate()) {
ToolTip.show(qsTr("You must restart Moonlight for this change to take effect"), 5000)
}
else {
// Force the back operation to pop any AppView pages that exist.
// The AppView stops working after retranslate() for some reason.
window.clearOnBack = true
}
}
}
}
Label {
width: parent.width
id: uiDisplayModeTitle
text: qsTr("GUI display mode")
font.pointSize: 12
wrapMode: Text.Wrap
visible: SystemProperties.hasDesktopEnvironment
}
AutoResizingComboBox {
// ignore setting the index at first, and actually set it when the component is loaded
Component.onCompleted: {
if (!visible) {
// Do nothing if the control won't even be visible
return
}
var saved_uidisplaymode = StreamingPreferences.uiDisplayMode
currentIndex = 0
for (var i = 0; i < uiDisplayModeListModel.count; i++) {
var el_uidisplaymode = uiDisplayModeListModel.get(i).val;
if (saved_uidisplaymode === el_uidisplaymode) {
currentIndex = i
break
}
} }
activated(currentIndex) activated(currentIndex)
} }
id: uiDisplayModeComboBox id: uiDisplayModeComboBox
visible: SystemProperties.hasWindowManager visible: SystemProperties.hasDesktopEnvironment
textRole: "text" textRole: "text"
model: ListModel { model: ListModel {
id: uiDisplayModeListModel id: uiDisplayModeListModel
@@ -833,7 +939,6 @@ Flickable {
width: parent.width width: parent.width
text: qsTr("Optimize mouse for remote desktop instead of games") text: qsTr("Optimize mouse for remote desktop instead of games")
font.pointSize: 12 font.pointSize: 12
visible: SystemProperties.hasWindowManager
checked: StreamingPreferences.absoluteMouseMode checked: StreamingPreferences.absoluteMouseMode
onCheckedChanged: { onCheckedChanged: {
StreamingPreferences.absoluteMouseMode = checked StreamingPreferences.absoluteMouseMode = checked
@@ -847,25 +952,79 @@ Flickable {
qsTr("NOTE: Due to a bug in GeForce Experience, this option may not work properly if your host PC has multiple monitors.") qsTr("NOTE: Due to a bug in GeForce Experience, this option may not work properly if your host PC has multiple monitors.")
} }
CheckBox { Row {
id: captureSysKeysCheck spacing: 5
hoverEnabled: true
width: parent.width width: parent.width
text: qsTr("Capture system keyboard shortcuts ") +
(!SystemProperties.isRunningWayland ? qsTr("while streaming in fullscreen") : CheckBox {
qsTr("(unsupported on Wayland)")) id: captureSysKeysCheck
font.pointSize: 12 hoverEnabled: true
enabled: SystemProperties.hasWindowManager && !SystemProperties.isRunningWayland text: qsTr("Capture system keyboard shortcuts")
checked: (StreamingPreferences.captureSysKeys || !SystemProperties.hasWindowManager) && !SystemProperties.isRunningWayland font.pointSize: 12
onCheckedChanged: { enabled: SystemProperties.hasDesktopEnvironment
StreamingPreferences.captureSysKeys = checked checked: StreamingPreferences.captureSysKeysMode !== StreamingPreferences.CSK_OFF || !SystemProperties.hasDesktopEnvironment
ToolTip.delay: 1000
ToolTip.timeout: 10000
ToolTip.visible: hovered
ToolTip.text: qsTr("This enables the capture of system-wide keyboard shortcuts like Alt+Tab that would normally be handled by the client OS while streaming.") + "\n\n" +
qsTr("NOTE: Certain keyboard shortcuts like Ctrl+Alt+Del on Windows cannot be intercepted by any application, including Moonlight.")
} }
ToolTip.delay: 1000 AutoResizingComboBox {
ToolTip.timeout: 10000 // ignore setting the index at first, and actually set it when the component is loaded
ToolTip.visible: hovered Component.onCompleted: {
ToolTip.text: qsTr("This enables the capture of system-wide keyboard shortcuts like Alt+Tab that would normally be handled by the client OS while streaming in fullscreen.") + "\n\n" + if (!visible) {
qsTr("NOTE: Certain keyboard shortcuts like Ctrl+Alt+Del on Windows cannot be intercepted by any application, including Moonlight.") // Do nothing if the control won't even be visible
return
}
var saved_syskeysmode = StreamingPreferences.captureSysKeysMode
currentIndex = 0
for (var i = 0; i < captureSysKeysModeListModel.count; i++) {
var el_syskeysmode = captureSysKeysModeListModel.get(i).val;
if (saved_syskeysmode === el_syskeysmode) {
currentIndex = i
break
}
}
activated(currentIndex)
}
enabled: captureSysKeysCheck.checked && captureSysKeysCheck.enabled
textRole: "text"
model: ListModel {
id: captureSysKeysModeListModel
ListElement {
text: qsTr("in fullscreen")
val: StreamingPreferences.CSK_FULLSCREEN
}
ListElement {
text: qsTr("always")
val: StreamingPreferences.CSK_ALWAYS
}
}
function updatePref() {
if (!enabled) {
StreamingPreferences.captureSysKeysMode = StreamingPreferences.CSK_OFF
}
else {
StreamingPreferences.captureSysKeysMode = captureSysKeysModeListModel.get(currentIndex).val
}
}
// ::onActivated must be used, as it only listens for when the index is changed by a human
onActivated: {
updatePref()
}
// This handles transition of the checkbox state
onEnabledChanged: {
updatePref()
}
}
} }
CheckBox { CheckBox {
@@ -980,7 +1139,7 @@ Flickable {
width: parent.width width: parent.width
text: qsTr("Process gamepad input when Moonlight is in the background") text: qsTr("Process gamepad input when Moonlight is in the background")
font.pointSize: 12 font.pointSize: 12
visible: SystemProperties.hasWindowManager visible: SystemProperties.hasDesktopEnvironment
checked: StreamingPreferences.backgroundGamepad checked: StreamingPreferences.backgroundGamepad
onCheckedChanged: { onCheckedChanged: {
StreamingPreferences.backgroundGamepad = checked StreamingPreferences.backgroundGamepad = checked
+3
View File
@@ -91,6 +91,8 @@ QVariant AppModel::data(const QModelIndex &index, int role) const
return app.id; return app.id;
case DirectLaunchRole: case DirectLaunchRole:
return app.directLaunch; return app.directLaunch;
case AppCollectorGameRole:
return app.isAppCollectorGame;
default: default:
return QVariant(); return QVariant();
} }
@@ -106,6 +108,7 @@ QHash<int, QByteArray> AppModel::roleNames() const
names[HiddenRole] = "hidden"; names[HiddenRole] = "hidden";
names[AppIdRole] = "appid"; names[AppIdRole] = "appid";
names[DirectLaunchRole] = "directLaunch"; names[DirectLaunchRole] = "directLaunch";
names[AppCollectorGameRole] = "appCollectorGame";
return names; return names;
} }
+2 -1
View File
@@ -17,7 +17,8 @@ class AppModel : public QAbstractListModel
BoxArtRole, BoxArtRole,
HiddenRole, HiddenRole,
AppIdRole, AppIdRole,
DirectLaunchRole DirectLaunchRole,
AppCollectorGameRole,
}; };
public: public:
+3
View File
@@ -42,6 +42,8 @@ QVariant ComputerModel::data(const QModelIndex& index, int role) const
return !computer->macAddress.isEmpty(); return !computer->macAddress.isEmpty();
case StatusUnknownRole: case StatusUnknownRole:
return computer->state == NvComputer::CS_UNKNOWN; return computer->state == NvComputer::CS_UNKNOWN;
case ServerSupportedRole:
return computer->isSupportedServerVersion;
default: default:
return QVariant(); return QVariant();
} }
@@ -68,6 +70,7 @@ QHash<int, QByteArray> ComputerModel::roleNames() const
names[BusyRole] = "busy"; names[BusyRole] = "busy";
names[WakeableRole] = "wakeable"; names[WakeableRole] = "wakeable";
names[StatusUnknownRole] = "statusUnknown"; names[StatusUnknownRole] = "statusUnknown";
names[ServerSupportedRole] = "serverSupported";
return names; return names;
} }
+2 -1
View File
@@ -14,7 +14,8 @@ class ComputerModel : public QAbstractListModel
PairedRole, PairedRole,
BusyRole, BusyRole,
WakeableRole, WakeableRole,
StatusUnknownRole StatusUnknownRole,
ServerSupportedRole
}; };
public: public:
+24 -4
View File
@@ -12,13 +12,18 @@ import SdlGamepadKeyNavigation 1.0
ApplicationWindow { ApplicationWindow {
property bool pollingActive: false property bool pollingActive: false
// Set by SettingsView to force the back operation to pop all
// pages except the initial view. This is required when doing
// a retranslate() because AppView breaks for some reason.
property bool clearOnBack: false
id: window id: window
visible: true visible: true
width: 1280 width: 1280
height: 600 height: 600
visibility: { visibility: {
if (SystemProperties.hasWindowManager) { if (SystemProperties.hasDesktopEnvironment) {
if (StreamingPreferences.uiDisplayMode == StreamingPreferences.UI_WINDOWED) return "Windowed" if (StreamingPreferences.uiDisplayMode == StreamingPreferences.UI_WINDOWED) return "Windowed"
else if (StreamingPreferences.uiDisplayMode == StreamingPreferences.UI_MAXIMIZED) return "Maximized" else if (StreamingPreferences.uiDisplayMode == StreamingPreferences.UI_MAXIMIZED) return "Maximized"
else if (StreamingPreferences.uiDisplayMode == StreamingPreferences.UI_FULLSCREEN) return "FullScreen" else if (StreamingPreferences.uiDisplayMode == StreamingPreferences.UI_FULLSCREEN) return "FullScreen"
@@ -31,6 +36,17 @@ ApplicationWindow {
// it will never insert a line break and just extend on forever. // it will never insert a line break and just extend on forever.
ToolTip.toolTip.contentWidth: ToolTip.toolTip.implicitContentWidth < 400 ? ToolTip.toolTip.implicitContentWidth : 400 ToolTip.toolTip.contentWidth: ToolTip.toolTip.implicitContentWidth < 400 ? ToolTip.toolTip.implicitContentWidth : 400
function goBack() {
if (clearOnBack) {
// Pop all items except the first one
stackView.pop(null)
clearOnBack = false
}
else {
stackView.pop()
}
}
StackView { StackView {
id: stackView id: stackView
initialItem: initialView initialItem: initialView
@@ -46,7 +62,7 @@ ApplicationWindow {
Keys.onEscapePressed: { Keys.onEscapePressed: {
if (depth > 1) { if (depth > 1) {
stackView.pop() goBack()
} }
else { else {
quitConfirmationDialog.open() quitConfirmationDialog.open()
@@ -55,7 +71,7 @@ ApplicationWindow {
Keys.onBackPressed: { Keys.onBackPressed: {
if (depth > 1) { if (depth > 1) {
stackView.pop() goBack()
} }
else { else {
quitConfirmationDialog.open() quitConfirmationDialog.open()
@@ -210,7 +226,7 @@ ApplicationWindow {
iconSource: "qrc:/res/arrow_left.svg" iconSource: "qrc:/res/arrow_left.svg"
onClicked: stackView.pop() onClicked: goBack()
Keys.onDownPressed: { Keys.onDownPressed: {
stackView.currentItem.forceActiveFocus(Qt.TabFocus) stackView.currentItem.forceActiveFocus(Qt.TabFocus)
@@ -495,6 +511,10 @@ ApplicationWindow {
Keys.onReturnPressed: { Keys.onReturnPressed: {
addPcDialog.accept() addPcDialog.accept()
} }
Keys.onEnterPressed: {
addPcDialog.accept()
}
} }
} }
} }
+1 -1
View File
@@ -15,7 +15,7 @@ SdlGamepadKeyNavigation::SdlGamepadKeyNavigation()
m_LastAxisNavigationEventTime(0) m_LastAxisNavigationEventTime(0)
{ {
m_PollingTimer = new QTimer(this); m_PollingTimer = new QTimer(this);
connect(m_PollingTimer, SIGNAL(timeout()), this, SLOT(onPollingTimerFired())); connect(m_PollingTimer, &QTimer::timeout, this, &SdlGamepadKeyNavigation::onPollingTimerFired);
} }
SdlGamepadKeyNavigation::~SdlGamepadKeyNavigation() SdlGamepadKeyNavigation::~SdlGamepadKeyNavigation()
Binary file not shown.
File diff suppressed because it is too large. Load diff
Binary file not shown.
File diff suppressed because it is too large. Load diff
Binary file not shown.
+439 -207
View File
File diff suppressed because it is too large. Load diff
Binary file not shown.
File diff suppressed because it is too large. Load diff
Binary file not shown.
File diff suppressed because it is too large. Load diff
Binary file not shown.
File diff suppressed because it is too large. Load diff
Binary file not shown.
File diff suppressed because it is too large. Load diff
Binary file not shown.
File diff suppressed because it is too large. Load diff
Binary file not shown.
File diff suppressed because it is too large. Load diff
Binary file not shown.
+1000
View File
@@ -0,0 +1,1000 @@
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE TS>
<TS version="2.1" language="zh_CN">
<context>
<name>AppView</name>
<message>
<location filename="../gui/AppView.qml" line="144"/>
<location filename="../gui/AppView.qml" line="298"/>
<source>Resume Game</source>
<translation>继续游戏</translation>
</message>
<message>
<location filename="../gui/AppView.qml" line="170"/>
<location filename="../gui/AppView.qml" line="303"/>
<source>Quit Game</source>
<translation>退出游戏</translation>
</message>
<message>
<location filename="../gui/AppView.qml" line="298"/>
<source>Launch Game</source>
<translation>启动游戏</translation>
</message>
<message>
<location filename="../gui/AppView.qml" line="311"/>
<source>Direct Launch</source>
<translation>直接启动</translation>
</message>
<message>
<location filename="../gui/AppView.qml" line="315"/>
<source>Launch this app immediately when the host is selected, bypassing the app selection grid.</source>
<translation>选择目标计算机后立即启动此应用程序,绕过应用程序选择网格。</translation>
</message>
<message>
<location filename="../gui/AppView.qml" line="324"/>
<source>Hide Game</source>
<translation>隐藏游戏</translation>
</message>
<message>
<location filename="../gui/AppView.qml" line="328"/>
<source>Hide this game from the app grid. To access hidden games, right-click on the host and choose %1.</source>
<translation>从应用程序网格中隐藏此游戏。要访问隐藏的游戏,请右键单击目标计算机并选择 %1。</translation>
</message>
<message>
<location filename="../gui/AppView.qml" line="328"/>
<source>View All Apps</source>
<translation>查看所有应用程序</translation>
</message>
<message>
<location filename="../gui/AppView.qml" line="343"/>
<source>Are you sure you want to quit %1? Any unsaved progress will be lost.</source>
<translation>你确定要退出 %1 吗?所有未保存的游戏进度都将丢失。</translation>
</message>
</context>
<context>
<name>CliQuitStreamSegue</name>
<message>
<location filename="../gui/CliQuitStreamSegue.qml" line="9"/>
<source>Establishing connection to PC...</source>
<translation>正在建立与目标计算机的连接...</translation>
</message>
<message>
<location filename="../gui/CliQuitStreamSegue.qml" line="13"/>
<source>Quitting app...</source>
<translation>应用程序退出中...</translation>
</message>
</context>
<context>
<name>CliStartStreamSegue</name>
<message>
<location filename="../gui/CliStartStreamSegue.qml" line="9"/>
<source>Establishing connection to PC...</source>
<translation>正在建立与目标计算机的连接...</translation>
</message>
<message>
<location filename="../gui/CliStartStreamSegue.qml" line="13"/>
<source>Loading app list...</source>
<translation>应用程序列表加载中...</translation>
</message>
<message>
<location filename="../gui/CliStartStreamSegue.qml" line="81"/>
<source>Are you sure you want to quit %1? Any unsaved progress will be lost.</source>
<translation>你确定要退出 %1 吗?任何未保存的进度都将丢失。</translation>
</message>
</context>
<context>
<name>GamepadMapper</name>
<message>
<location filename="../gui/GamepadMapper.qml" line="4"/>
<source>Gamepad Mapping</source>
<translation>手柄按键映射</translation>
</message>
</context>
<context>
<name>NvHTTP</name>
<message>
<location filename="../backend/nvhttp.cpp" line="327"/>
<source>Missing audio capture device. Reinstalling GeForce Experience should resolve this error.</source>
<translation>缺少音频捕获设备。重新安装 GeForce Experience 可以解决此错误。</translation>
</message>
</context>
<context>
<name>PcView</name>
<message>
<location filename="../gui/PcView.qml" line="20"/>
<source>Computers</source>
<translation>计算机列表</translation>
</message>
<message>
<location filename="../gui/PcView.qml" line="67"/>
<source>Unable to connect to the specified PC.</source>
<translation>无法连接到目标计算机。</translation>
</message>
<message>
<location filename="../gui/PcView.qml" line="70"/>
<source>This PC&apos;s Internet connection is blocking Moonlight. Streaming over the Internet may not work while connected to this network.</source>
<translation>此计算机的互联网连接被阻塞。当连接到此网络时,互联网上的流式传输可能无法工作。</translation>
</message>
<message>
<location filename="../gui/PcView.qml" line="73"/>
<source>Click the Help button for possible solutions.</source>
<translation>单击帮助按钮以获取可能的解决方案。</translation>
</message>
<message>
<location filename="../gui/PcView.qml" line="102"/>
<source>Searching for PCs on your local network with NVIDIA GameStream enabled...</source>
<translation>正在本地网络上搜索启用 NVIDIA GameStrea 的计算机...</translation>
</message>
<message>
<location filename="../gui/PcView.qml" line="103"/>
<source>Automatic PC discovery is disabled. Add your PC manually.</source>
<translation>已禁用计算机自动发现。需要手动添加计算机。</translation>
</message>
<message>
<location filename="../gui/PcView.qml" line="171"/>
<source>PC Status: %1</source>
<translation>计算机状态: %1</translation>
</message>
<message>
<location filename="../gui/PcView.qml" line="171"/>
<source>Online</source>
<translation>在线</translation>
</message>
<message>
<location filename="../gui/PcView.qml" line="171"/>
<source>Offline</source>
<translation>离线</translation>
</message>
<message>
<location filename="../gui/PcView.qml" line="177"/>
<source>View All Apps</source>
<translation>查看所有应用程序</translation>
</message>
<message>
<location filename="../gui/PcView.qml" line="187"/>
<source>Wake PC</source>
<translation>唤醒目标计算机</translation>
</message>
<message>
<location filename="../gui/PcView.qml" line="193"/>
<source>Test Network</source>
<translation>网络测试</translation>
</message>
<message>
<location filename="../gui/PcView.qml" line="202"/>
<source>Rename PC</source>
<translation>重命名计算机</translation>
</message>
<message>
<location filename="../gui/PcView.qml" line="211"/>
<source>Delete PC</source>
<translation>删除计算机</translation>
</message>
<message>
<location filename="../gui/PcView.qml" line="224"/>
<source>The version of GeForce Experience on %1 is not supported by this build of Moonlight. You must update Moonlight to stream from %1.</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../gui/PcView.qml" line="247"/>
<source>You cannot pair while a previous session is still running on the host PC. Quit any running games or reboot the host PC, then try pairing again.</source>
<translation>当上一个会话仍在运行时,无法进行配对。请退出所有正在运行的游戏或重新启动目标计算机,然后重试配对。</translation>
</message>
<message>
<location filename="../gui/PcView.qml" line="302"/>
<source>Please enter %1 on your GameStream PC. This dialog will close when pairing is completed.</source>
<translation>请在启用 GameStream 的计算机上输入 %1。配对完成后,此对话框将关闭。</translation>
</message>
<message>
<location filename="../gui/PcView.qml" line="313"/>
<source>Are you sure you want to remove this PC?</source>
<translation>确定要移除这个计算机吗?</translation>
</message>
<message>
<location filename="../gui/PcView.qml" line="330"/>
<source>Moonlight is testing your network connection to determine if NVIDIA GameStream is blocked.</source>
<translation>Moonlight 正在测试你的网络连接,以检测 NVIDIA GameStream 是否被阻止。</translation>
</message>
<message>
<location filename="../gui/PcView.qml" line="330"/>
<source>This may take a few seconds…</source>
<translation>这可能需要几秒钟……</translation>
</message>
<message>
<location filename="../gui/PcView.qml" line="337"/>
<source>The network test could not be performed because none of Moonlight&apos;s connection testing servers were reachable from this PC. Check your Internet connection or try again later.</source>
<translation>无法执行网络测试,因为此计算机无法访问 Moonlight 的连接测试服务器。请检查你的互联网连接或稍后再试。</translation>
</message>
<message>
<location filename="../gui/PcView.qml" line="341"/>
<source>This network does not appear to be blocking Moonlight. If you still have trouble connecting, check your PC&apos;s firewall settings.</source>
<translation>此网络似乎没有阻止 Moonlight。如果仍然无法连接,请检查计算机的防火墙设置。</translation>
</message>
<message>
<location filename="../gui/PcView.qml" line="341"/>
<source>If you are trying to stream over the Internet, install the Moonlight Internet Hosting Tool on your gaming PC and run the included Internet Streaming Tester to check your gaming PC&apos;s Internet connection.</source>
<translation>如果你想通过互联网进行流式传输,请在你的目标计算机上安装 Moonlight Internet Hosting 工具,并运行包含的互联网流式传输测试程序来检查你目标计算机的互联网连接。</translation>
</message>
<message>
<location filename="../gui/PcView.qml" line="345"/>
<source>Your PC&apos;s current network connection seems to be blocking Moonlight. Streaming over the Internet may not work while connected to this network.</source>
<translation>此计算机当前的网络连接似乎被阻塞了。当连接到此网络时,互联网上的流式传输可能无法工作。</translation>
</message>
<message>
<location filename="../gui/PcView.qml" line="345"/>
<source>The following network ports were blocked:</source>
<translation>下列网络端口被阻止:</translation>
</message>
<message>
<location filename="../gui/PcView.qml" line="357"/>
<source>Enter the new name for this PC:</source>
<translation>输入这台计算机的新名称:</translation>
</message>
</context>
<context>
<name>PendingQuitTask</name>
<message>
<location filename="../backend/computermanager.cpp" line="542"/>
<source>The running game wasn&apos;t started by this PC. You must quit the game on the host PC manually or use the device that originally started the game.</source>
<translation>正在运行的游戏不是由这台计算机启动的。你必须在目标计算机上手动退出游戏或使用最初启动游戏的设备。</translation>
</message>
</context>
<context>
<name>QObject</name>
<message>
<location filename="../cli/quitstream.cpp" line="76"/>
<location filename="../cli/startstream.cpp" line="133"/>
<source>Failed to connect to %1</source>
<translation>无法连接到 %1</translation>
</message>
<message>
<location filename="../cli/quitstream.cpp" line="88"/>
<location filename="../cli/startstream.cpp" line="89"/>
<source>Computer %1 has not been paired. Please open Moonlight to pair before streaming.</source>
<translation>计算机 %1 尚未配对。</translation>
</message>
<message>
<location filename="../cli/quitstream.cpp" line="102"/>
<location filename="../cli/startstream.cpp" line="126"/>
<source>Quitting app failed, reason: %1</source>
<translation>退出应用程序失败,原因: %1</translation>
</message>
<message>
<location filename="../cli/startstream.cpp" line="137"/>
<source>Failed to find application %1</source>
<translation>找不到应用程序 %1</translation>
</message>
</context>
<context>
<name>QuitSegue</name>
<message>
<location filename="../gui/QuitSegue.qml" line="12"/>
<source>Quitting %1...</source>
<translation>正在退出 %1...</translation>
</message>
</context>
<context>
<name>Session</name>
<message>
<location filename="../streaming/session.cpp" line="89"/>
<source>No video received from host.</source>
<translation>没有从目标计算机接收到视频画面。</translation>
</message>
<message>
<location filename="../streaming/session.cpp" line="90"/>
<source>Check your firewall and port forwarding rules for port(s): %1</source>
<translation>检查防火墙和端口转发规则: %1</translation>
</message>
<message>
<location filename="../streaming/session.cpp" line="95"/>
<source>Your network connection isn&apos;t performing well. Reduce your video bitrate setting or try a faster connection.</source>
<translation>网络连接性能不佳。请降低视频比特率或尝试更快的网络连接。</translation>
</message>
<message>
<location filename="../streaming/session.cpp" line="100"/>
<source>Something went wrong on your host PC when starting the stream.</source>
<translation>目标计算机启动流式传输时出现问题。</translation>
</message>
<message>
<location filename="../streaming/session.cpp" line="101"/>
<source>Make sure you don&apos;t have any DRM-protected content open on your host PC. You can also try restarting your host PC.</source>
<translation>确保目标计算机上没有打开任何受 DRM 保护的内容。你也可以尝试重新启动目标计算机。</translation>
</message>
<message>
<location filename="../streaming/session.cpp" line="102"/>
<source>If the issue persists, try reinstalling your GPU drivers and GeForce Experience.</source>
<translation>如果问题仍然存在,请尝试重新安装显卡驱动程序和 GeForce Experience。</translation>
</message>
<message>
<location filename="../streaming/session.cpp" line="107"/>
<source>Connection terminated</source>
<translation>会话已终止</translation>
</message>
<message>
<location filename="../streaming/session.cpp" line="592"/>
<source>The version of GeForce Experience on %1 is not supported by this build of Moonlight. You must update Moonlight to stream from %1.</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../streaming/session.cpp" line="597"/>
<source>Your selection to enable remote desktop mouse mode may cause problems in games.</source>
<translation>启用远程桌面鼠标模式可能会导致游戏出现问题。</translation>
</message>
<message>
<location filename="../streaming/session.cpp" line="602"/>
<source>HDR is not supported with software decoding.</source>
<translation>软件解码不支持 HDR。</translation>
</message>
<message>
<location filename="../streaming/session.cpp" line="606"/>
<source>Your settings selection to force software decoding may cause poor streaming performance.</source>
<translation>你选择的用于强制软件解码的设置可能会导致流式传输性能不佳。</translation>
</message>
<message>
<location filename="../streaming/session.cpp" line="611"/>
<source>Using unsupported FPS options may cause stuttering or lag.</source>
<translation>使用不受支持的 FPS 选项可能会导致卡顿或延迟。</translation>
</message>
<message>
<location filename="../streaming/session.cpp" line="614"/>
<source>V-sync will be disabled when streaming at a higher frame rate than the display.</source>
<translation>当流式传输的帧率高于显示时,垂直同步将被禁用。</translation>
</message>
<message>
<location filename="../streaming/session.cpp" line="631"/>
<source>Using software decoding due to your selection to force HEVC without GPU support. This may cause poor streaming performance.</source>
<translation>根据你的选择使用软件解码,以在没有显卡支持的情况下强制 HEVC。这可能会导致流式传输性能不佳。</translation>
</message>
<message>
<location filename="../streaming/session.cpp" line="634"/>
<source>This PC&apos;s GPU doesn&apos;t support HEVC decoding.</source>
<translation>此计算机的显卡不支持 HEVC 解码。</translation>
</message>
<message>
<location filename="../streaming/session.cpp" line="641"/>
<source>Your host PC GPU doesn&apos;t support HEVC. A GeForce GTX 900-series (Maxwell) or later GPU is required for HEVC streaming.</source>
<translation>目标计算机的显卡不支持 HEVC。</translation>
</message>
<message>
<location filename="../streaming/session.cpp" line="658"/>
<source>%1 doesn&apos;t support HDR10.</source>
<translation>%1 不支持 HDR10。</translation>
</message>
<message>
<location filename="../streaming/session.cpp" line="662"/>
<source>Your host PC GPU doesn&apos;t support HDR streaming. A GeForce GTX 1000-series (Pascal) or later GPU is required for HDR streaming.</source>
<translation>目标计算机的显卡不支持 HDR 流。</translation>
</message>
<message>
<location filename="../streaming/session.cpp" line="671"/>
<source>This PC&apos;s GPU doesn&apos;t support HEVC Main10 decoding for HDR streaming.</source>
<translation>此计算机的显卡不支持 HEVC Main10 解码 HDR 流。</translation>
</message>
<message>
<location filename="../streaming/session.cpp" line="684"/>
<source>GeForce Experience 3.0 or higher is required for 4K streaming.</source>
<translation>4K 流式传输需要 GeForce Experience 3.0 或更高版本。</translation>
</message>
<message>
<location filename="../streaming/session.cpp" line="699"/>
<source>Your selected surround sound setting is not supported by the current audio device.</source>
<translation>当前音频设备不支持你选择的环绕声设置。</translation>
</message>
<message>
<location filename="../streaming/session.cpp" line="706"/>
<source>Failed to open audio device. Audio will be unavailable during this session.</source>
<translation>无法打开音频设备。音频在此会话期间将不可用。</translation>
</message>
<message>
<location filename="../streaming/session.cpp" line="711"/>
<source>An attached gamepad has no mapping and won&apos;t be usable. Visit the Moonlight help to resolve this.</source>
<translation>附加的手柄没有映射,因此无法使用。请访问 Moonlight 帮助解决此问题。</translation>
</message>
<message>
<location filename="../streaming/session.cpp" line="719"/>
<source>Your host PC&apos;s GPU doesn&apos;t support streaming video resolutions over 4K.</source>
<translation>目标计算机的显卡不支持 4K 以上的流式传输视频分辨率。</translation>
</message>
<message>
<location filename="../streaming/session.cpp" line="723"/>
<source>Video resolutions over 4K are only supported by the HEVC codec.</source>
<translation>只有 HEVC 编解码器才支持 4K 以上的视频分辨率。</translation>
</message>
<message>
<location filename="../streaming/session.cpp" line="737"/>
<source>Your selection to force hardware decoding cannot be satisfied due to missing hardware decoding support on this PC&apos;s GPU.</source>
<translation>由于此计算机的显卡上缺少硬件解码支持,无法满足强制硬件解码的选择。</translation>
</message>
<message>
<location filename="../streaming/session.cpp" line="740"/>
<source>Your codec selection and force hardware decoding setting are not compatible. This PC&apos;s GPU lacks support for decoding your chosen codec.</source>
<translation>你选择的编解码器和强制硬件解码设置不兼容。此计算机的显卡不支持解码你选择的编解码器。</translation>
</message>
<message>
<location filename="../streaming/session.cpp" line="1041"/>
<source>GeForce Experience returned error: %1</source>
<translation>GeForce Experience 返回错误: %1</translation>
</message>
<message>
<location filename="../streaming/session.cpp" line="1519"/>
<source>Unable to initialize video decoder. Please check your streaming settings and try again.</source>
<translation>无法初始化视频解码器。请检查你的流式传输设置,然后重试。</translation>
</message>
</context>
<context>
<name>SettingsView</name>
<message>
<location filename="../gui/SettingsView.qml" line="12"/>
<source>Settings</source>
<translation>设置</translation>
</message>
<message>
<location filename="../gui/SettingsView.qml" line="60"/>
<source>Basic Settings</source>
<translation>基本设置</translation>
</message>
<message>
<location filename="../gui/SettingsView.qml" line="70"/>
<source>Resolution and FPS</source>
<translation>分辨率和 FPS</translation>
</message>
<message>
<location filename="../gui/SettingsView.qml" line="78"/>
<source>Setting values too high for your PC or network connection may cause lag, stuttering, or errors.</source>
<translation>如果你的计算机或网络连接设置的值过高,可能会导致延迟,卡顿或错误。</translation>
</message>
<message>
<location filename="../gui/SettingsView.qml" line="209"/>
<source>720p</source>
<translation>720p</translation>
</message>
<message>
<location filename="../gui/SettingsView.qml" line="215"/>
<source>1080p</source>
<translation>1080p</translation>
</message>
<message>
<location filename="../gui/SettingsView.qml" line="221"/>
<source>1440p</source>
<translation>1440p</translation>
</message>
<message>
<location filename="../gui/SettingsView.qml" line="227"/>
<source>4K</source>
<translation>4K</translation>
</message>
<message>
<location filename="../gui/SettingsView.qml" line="331"/>
<source>Custom resolutions are not officially supported by GeForce Experience, so it will not set your host display resolution. You will need to set it manually while in game.</source>
<translation>自定义分辨率不受 GeForce Experience 的正式支持,因此不会设置目标计算机的分辨率。你需要在游戏中手动设置。</translation>
</message>
<message>
<location filename="../gui/SettingsView.qml" line="332"/>
<source>Resolutions that are not supported by your client or host PC may cause streaming errors.</source>
<translation>使用当前计算机或目标计算机不支持的分辨率可能会导致流式传输错误。</translation>
</message>
<message>
<location filename="../gui/SettingsView.qml" line="338"/>
<source>Enter a custom resolution:</source>
<translation>输入自定义分辨率:</translation>
</message>
<message>
<location filename="../gui/SettingsView.qml" line="406"/>
<location filename="../gui/SettingsView.qml" line="407"/>
<location filename="../gui/SettingsView.qml" line="427"/>
<location filename="../gui/SettingsView.qml" line="430"/>
<source>%1 FPS</source>
<translation>%1 FPS</translation>
</message>
<message>
<location filename="../gui/SettingsView.qml" line="412"/>
<location filename="../gui/SettingsView.qml" line="415"/>
<location filename="../gui/SettingsView.qml" line="424"/>
<location filename="../gui/SettingsView.qml" line="437"/>
<location filename="../gui/SettingsView.qml" line="440"/>
<source>%1 FPS (Unsupported)</source>
<translation>%1 FPS (不支持)</translation>
</message>
<message>
<location filename="../gui/SettingsView.qml" line="493"/>
<source>Video bitrate:</source>
<translation>视频比特率:</translation>
</message>
<message>
<location filename="../gui/SettingsView.qml" line="501"/>
<source>Lower the bitrate on slower connections. Raise the bitrate to increase image quality.</source>
<translation>在较慢的连接上降低比特率。提高比特率以提高图像质量。</translation>
</message>
<message>
<location filename="../gui/SettingsView.qml" line="519"/>
<source>Video bitrate: %1 Mbps</source>
<translation>视频比特率: %1 Mbps</translation>
</message>
<message>
<location filename="../gui/SettingsView.qml" line="527"/>
<source>Display mode</source>
<translation>显示模式</translation>
</message>
<message>
<location filename="../gui/SettingsView.qml" line="545"/>
<source> (Recommended)</source>
<translation> (推荐)</translation>
</message>
<message>
<location filename="../gui/SettingsView.qml" line="573"/>
<location filename="../gui/SettingsView.qml" line="868"/>
<source>Fullscreen</source>
<translation>全屏</translation>
</message>
<message>
<location filename="../gui/SettingsView.qml" line="577"/>
<source>Borderless windowed</source>
<translation>无边框窗口</translation>
</message>
<message>
<location filename="../gui/SettingsView.qml" line="581"/>
<location filename="../gui/SettingsView.qml" line="860"/>
<source>Windowed</source>
<translation>窗口化</translation>
</message>
<message>
<location filename="../gui/SettingsView.qml" line="592"/>
<source>Fullscreen generally provides the best performance, but borderless windowed may work better with features like macOS Spaces, Alt+Tab, screenshot tools, on-screen overlays, etc.</source>
<translation>全屏通常提供最好的性能,但是无边框窗口可以更好地与 macOS Spaces、Alt+Tab、屏幕截图工具、屏幕覆盖等功能配合使用。</translation>
</message>
<message>
<location filename="../gui/SettingsView.qml" line="599"/>
<source>V-Sync</source>
<translation>垂直同步</translation>
</message>
<message>
<location filename="../gui/SettingsView.qml" line="609"/>
<source>Disabling V-Sync allows sub-frame rendering latency, but it can display visible tearing</source>
<translation>禁用垂直同步可允许子帧渲染延迟,但会显示可见的撕裂</translation>
</message>
<message>
<location filename="../gui/SettingsView.qml" line="616"/>
<source>Frame pacing</source>
<translation>帧速调节</translation>
</message>
<message>
<location filename="../gui/SettingsView.qml" line="626"/>
<source>Frame pacing reduces micro-stutter by delaying frames that come in too early</source>
<translation>通过延迟过早进入的帧来减少卡顿</translation>
</message>
<message>
<location filename="../gui/SettingsView.qml" line="636"/>
<source>Audio Settings</source>
<translation>音频设置</translation>
</message>
<message>
<location filename="../gui/SettingsView.qml" line="646"/>
<source>Audio configuration</source>
<translation>音频配置</translation>
</message>
<message>
<location filename="../gui/SettingsView.qml" line="671"/>
<source>Stereo</source>
<translation>立体声</translation>
</message>
<message>
<location filename="../gui/SettingsView.qml" line="675"/>
<source>5.1 surround sound</source>
<translation>5.1 环绕声</translation>
</message>
<message>
<location filename="../gui/SettingsView.qml" line="679"/>
<source>7.1 surround sound</source>
<translation>7.1 环绕声</translation>
</message>
<message>
<location filename="../gui/SettingsView.qml" line="693"/>
<source>Mute host PC speakers while streaming</source>
<translation>流式传输启动时将目标计算机的扬声器静音</translation>
</message>
<message>
<location filename="../gui/SettingsView.qml" line="703"/>
<source>You must restart any game currently in progress for this setting to take effect</source>
<translation>要使此设置生效,必须重新启动当前正在进行的所有游戏</translation>
</message>
<message>
<location filename="../gui/SettingsView.qml" line="709"/>
<source>Mute audio stream when Moonlight is not the active window</source>
<translation>当 Moonlight 失去焦点时静音</translation>
</message>
<message>
<location filename="../gui/SettingsView.qml" line="720"/>
<source>Mutes Moonlight&apos;s audio when you Alt+Tab out of the stream or click on a different window.</source>
<translation>当你用 Alt+Tab 键离开流式传输或者点击其他窗口时,Moonlight 将静音。</translation>
</message>
<message>
<location filename="../gui/SettingsView.qml" line="729"/>
<source>UI Settings</source>
<translation>界面设置</translation>
</message>
<message>
<location filename="../gui/SettingsView.qml" line="739"/>
<source>Language</source>
<translation>语言</translation>
</message>
<message>
<location filename="../gui/SettingsView.qml" line="765"/>
<source>Automatic</source>
<translation>自动化</translation>
</message>
<message>
<location filename="../gui/SettingsView.qml" line="813"/>
<source>You must restart Moonlight for this change to take effect</source>
<translation>你必须重新启动 Moonlight 才能使更改生效</translation>
</message>
<message>
<location filename="../gui/SettingsView.qml" line="827"/>
<source>GUI display mode</source>
<translation>窗口显示模式</translation>
</message>
<message>
<location filename="../gui/SettingsView.qml" line="864"/>
<source>Maximized</source>
<translation>最大化</translation>
</message>
<message>
<location filename="../gui/SettingsView.qml" line="881"/>
<source>Show connection quality warnings</source>
<translation>显示连接质量警告</translation>
</message>
<message>
<location filename="../gui/SettingsView.qml" line="893"/>
<source>Discord Rich Presence integration</source>
<translation>Discord Rich Presence 集成</translation>
</message>
<message>
<location filename="../gui/SettingsView.qml" line="903"/>
<source>Updates your Discord status to display the name of the game you&apos;re streaming.</source>
<translation>更新你的 Discord 状态,以显示正在运行的游戏名称。</translation>
</message>
<message>
<location filename="../gui/SettingsView.qml" line="921"/>
<source>Input Settings</source>
<translation>输入设置</translation>
</message>
<message>
<location filename="../gui/SettingsView.qml" line="932"/>
<source>Optimize mouse for remote desktop instead of games</source>
<translation>为远程桌面而不是游戏优化鼠标</translation>
</message>
<message>
<location filename="../gui/SettingsView.qml" line="942"/>
<source>This enables seamless mouse control without capturing the client&apos;s mouse cursor. It is ideal for remote desktop usage but will not work in most games.</source>
<translation>这可以实现无缝鼠标控制,而无需捕获客户端的鼠标光标。这是理想的远程桌面使用方案,但不会在大多数游戏中工作。</translation>
</message>
<message>
<location filename="../gui/SettingsView.qml" line="943"/>
<source>You can toggle this while streaming using Ctrl+Alt+Shift+M.</source>
<translation>你可以使用 Ctrl+Alt+Shift+M 切换鼠标控制模式。</translation>
</message>
<message>
<location filename="../gui/SettingsView.qml" line="944"/>
<source>NOTE: Due to a bug in GeForce Experience, this option may not work properly if your host PC has multiple monitors.</source>
<translation>注意:由于 GeForce Experience 的 Bug,如果你的目标计算机有多个显示器,则此选项可能无法正常工作。</translation>
</message>
<message>
<location filename="../gui/SettingsView.qml" line="954"/>
<source>Capture system keyboard shortcuts</source>
<translation>捕获系统快捷键</translation>
</message>
<message>
<location filename="../gui/SettingsView.qml" line="962"/>
<source>This enables the capture of system-wide keyboard shortcuts like Alt+Tab that would normally be handled by the client OS while streaming.</source>
<translation>这样可以捕获系统范围的键盘快捷键,例如 Alt + Tab,这些快捷键通常在流式传输时由客户端系统处理。</translation>
</message>
<message>
<source>Capture system keyboard shortcuts while streaming in fullscreen</source>
<translation type="vanished">在全屏流式传输时捕获系统键盘快捷键</translation>
</message>
<message>
<source>This enables the capture of system-wide keyboard shortcuts like Alt+Tab that would normally be handled by the client OS while streaming in fullscreen.</source>
<translation type="vanished">这允许捕获系统范围内的键盘快捷键(如Alt+Tab),这些快捷键通常在全屏流式传输时由客户端操作系统处理。</translation>
</message>
<message>
<location filename="../gui/SettingsView.qml" line="963"/>
<source>NOTE: Certain keyboard shortcuts like Ctrl+Alt+Del on Windows cannot be intercepted by any application, including Moonlight.</source>
<translation>注意:Windows 上的某些键盘快捷键(如 Ctrl+Alt+Del)不能被任何应用程序截获,包括 Moonlight。</translation>
</message>
<message>
<location filename="../gui/SettingsView.qml" line="992"/>
<source>in fullscreen</source>
<translation>全屏</translation>
</message>
<message>
<location filename="../gui/SettingsView.qml" line="996"/>
<source>always</source>
<translation>总是</translation>
</message>
<message>
<location filename="../gui/SettingsView.qml" line="1026"/>
<source>Use touchscreen as a virtual trackpad</source>
<translation>将触摸屏用作虚拟触控板</translation>
</message>
<message>
<location filename="../gui/SettingsView.qml" line="1036"/>
<source>When checked, the touchscreen acts like a trackpad. When unchecked, the touchscreen will directly control the mouse pointer.</source>
<translation>选中时,触摸屏就像一个触控板。取消选中时,触摸屏将直接控制鼠标指针。</translation>
</message>
<message>
<location filename="../gui/SettingsView.qml" line="1043"/>
<source>Swap left and right mouse buttons</source>
<translation>交换鼠标左键和右键</translation>
</message>
<message>
<location filename="../gui/SettingsView.qml" line="1055"/>
<source>Reverse mouse scrolling direction</source>
<translation>反转鼠标滚动方向</translation>
</message>
<message>
<location filename="../gui/SettingsView.qml" line="1069"/>
<source>Gamepad Settings</source>
<translation>手柄设置</translation>
</message>
<message>
<location filename="../gui/SettingsView.qml" line="1079"/>
<source>Swap A/B and X/Y gamepad buttons</source>
<translation>交换手柄的 A/B 和 X/Y 按钮</translation>
</message>
<message>
<location filename="../gui/SettingsView.qml" line="1097"/>
<source>This switches gamepads into a Nintendo-style button layout</source>
<translation>这会将手柄切换为任天堂风格的按钮布局</translation>
</message>
<message>
<location filename="../gui/SettingsView.qml" line="1103"/>
<source>Force gamepad #1 always connected</source>
<translation>强制手柄 #1 始终连接</translation>
</message>
<message>
<location filename="../gui/SettingsView.qml" line="1113"/>
<source>Forces a single gamepad to always stay connected to the host, even if no gamepads are actually connected to this PC.</source>
<translation>强制单个手柄始终与目标计算机保持连接,即使没有任何手柄实际连接到此计算机。</translation>
</message>
<message>
<location filename="../gui/SettingsView.qml" line="1114"/>
<source>Only enable this option when streaming a game that doesn&apos;t support gamepads being connected after startup.</source>
<translation>仅在游戏启动后不支持手柄连接时才启用此选项。</translation>
</message>
<message>
<location filename="../gui/SettingsView.qml" line="1121"/>
<source>Enable mouse control with gamepads by holding the &apos;Start&apos; button</source>
<translation>按住手柄的“Start”按钮启用鼠标控制</translation>
</message>
<message>
<location filename="../gui/SettingsView.qml" line="1132"/>
<source>Process gamepad input when Moonlight is in the background</source>
<translation>当 Moonlight 在后台时处理手柄输入</translation>
</message>
<message>
<location filename="../gui/SettingsView.qml" line="1143"/>
<source>Allows Moonlight to capture gamepad inputs even if it&apos;s not the current window in focus</source>
<translation>即使不是焦点窗口,也允许 Moonlight 捕获手柄的输入</translation>
</message>
<message>
<location filename="../gui/SettingsView.qml" line="1152"/>
<source>Host Settings</source>
<translation>目标计算机设置</translation>
</message>
<message>
<location filename="../gui/SettingsView.qml" line="1162"/>
<source>Optimize game settings for streaming</source>
<translation>优化游戏设置以进行流式传输</translation>
</message>
<message>
<location filename="../gui/SettingsView.qml" line="1173"/>
<source>Quit app on host PC after ending stream</source>
<translation>流式传输结束后退出目标计算机上的应用程序</translation>
</message>
<message>
<location filename="../gui/SettingsView.qml" line="1183"/>
<source>This will close the app or game you are streaming when you end your stream. You will lose any unsaved progress!</source>
<translation>当你结束流式传输时,这将关闭正在运行的应用程序或游戏。这会导致你失去所有未保存的游戏进度!</translation>
</message>
<message>
<location filename="../gui/SettingsView.qml" line="1192"/>
<source>Advanced Settings</source>
<translation>高级设置</translation>
</message>
<message>
<location filename="../gui/SettingsView.qml" line="1202"/>
<source>Video decoder</source>
<translation>视频解码器</translation>
</message>
<message>
<location filename="../gui/SettingsView.qml" line="1227"/>
<location filename="../gui/SettingsView.qml" line="1273"/>
<source>Automatic (Recommended)</source>
<translation>自动 (推荐)</translation>
</message>
<message>
<location filename="../gui/SettingsView.qml" line="1231"/>
<source>Force software decoding</source>
<translation>强制软件解码</translation>
</message>
<message>
<location filename="../gui/SettingsView.qml" line="1235"/>
<source>Force hardware decoding</source>
<translation>强制硬件解码</translation>
</message>
<message>
<location filename="../gui/SettingsView.qml" line="1248"/>
<source>Video codec</source>
<translation>视频编解码器</translation>
</message>
<message>
<location filename="../gui/SettingsView.qml" line="1277"/>
<source>H.264</source>
<translation>H.264</translation>
</message>
<message>
<location filename="../gui/SettingsView.qml" line="1281"/>
<source>HEVC (H.265)</source>
<translation>HEVC (H.265)</translation>
</message>
<message>
<location filename="../gui/SettingsView.qml" line="1298"/>
<source>Unlock unsupported FPS options</source>
<translation>解锁不受支持的 FPS 选项</translation>
</message>
<message>
<location filename="../gui/SettingsView.qml" line="1317"/>
<source>Automatically find PCs on the local network (Recommended)</source>
<translation>自动在本地网络上查找计算机 (推荐)</translation>
</message>
<message>
<location filename="../gui/SettingsView.qml" line="1342"/>
<source>Automatically detect blocked connections (Recommended)</source>
<translation>自动检测被阻止的连接 (推荐)</translation>
</message>
</context>
<context>
<name>StreamSegue</name>
<message>
<location filename="../gui/StreamSegue.qml" line="11"/>
<source>Resuming %1...</source>
<translation>正在恢复 %1...</translation>
</message>
<message>
<location filename="../gui/StreamSegue.qml" line="12"/>
<location filename="../gui/StreamSegue.qml" line="19"/>
<source>Starting %1...</source>
<translation>正在启动 %1...</translation>
</message>
<message>
<location filename="../gui/StreamSegue.qml" line="25"/>
<source>Starting %1 failed: Error %2</source>
<translation>启动 %1 失败: 错误 %2</translation>
</message>
<message>
<location filename="../gui/StreamSegue.qml" line="28"/>
<source>Check your firewall and port forwarding rules for port(s): %1</source>
<translation>检查防火墙和端口转发规则: %1</translation>
</message>
<message>
<location filename="../gui/StreamSegue.qml" line="74"/>
<source>This PC&apos;s Internet connection is blocking Moonlight. Streaming over the Internet may not work while connected to this network.</source>
<translation>此计算机的互联网连接被阻塞。当连接到此网络时,互联网上的流式传输可能无法工作。</translation>
</message>
<message>
<location filename="../gui/StreamSegue.qml" line="161"/>
<source>Tip:</source>
<translation>提示:</translation>
</message>
<message>
<location filename="../gui/StreamSegue.qml" line="161"/>
<source>Press %1 to disconnect your session</source>
<translation>按 %1 来断开会话</translation>
</message>
<message>
<location filename="../gui/StreamSegue.qml" line="162"/>
<source>Start+Select+L1+R1</source>
<translation>Start+Select+L1+R1</translation>
</message>
<message>
<location filename="../gui/StreamSegue.qml" line="162"/>
<source>Ctrl+Alt+Shift+Q</source>
<translation>Ctrl+Alt+Shift+Q</translation>
</message>
</context>
<context>
<name>main</name>
<message>
<location filename="../gui/main.qml" line="254"/>
<location filename="../gui/main.qml" line="264"/>
<location filename="../gui/main.qml" line="393"/>
<location filename="../gui/main.qml" line="408"/>
<source>Settings</source>
<translation>设置</translation>
</message>
<message>
<location filename="../gui/main.qml" line="255"/>
<source>Version %1</source>
<translation>版本 %1</translation>
</message>
<message>
<location filename="../gui/main.qml" line="271"/>
<source>Join our community on Discord</source>
<translation>加入我们的社区</translation>
</message>
<message>
<location filename="../gui/main.qml" line="283"/>
<source>Computers</source>
<translation>计算机列表</translation>
</message>
<message>
<location filename="../gui/main.qml" line="290"/>
<source>Add PC manually</source>
<translation>手动添加计算机</translation>
</message>
<message>
<location filename="../gui/main.qml" line="330"/>
<source>Update available for Moonlight: Version %1</source>
<translation>Moonlight 可用更新:版本 %1</translation>
</message>
<message>
<location filename="../gui/main.qml" line="354"/>
<source>Help</source>
<translation>帮助</translation>
</message>
<message>
<location filename="../gui/main.qml" line="377"/>
<source>Gamepad Mapper</source>
<translation>手柄按键映射</translation>
</message>
<message>
<location filename="../gui/main.qml" line="381"/>
<source>Gamepad Mapping</source>
<translation>手柄按键映射</translation>
</message>
<message>
<location filename="../gui/main.qml" line="415"/>
<source>No functioning hardware accelerated H.264 video decoder was detected by Moonlight. Your streaming performance may be severely degraded in this configuration.</source>
<translation>Moonlight 未检测到功能正常的硬件加速 H.264 视频解码器。</translation>
</message>
<message>
<location filename="../gui/main.qml" line="417"/>
<source>Click the Help button for more information on solving this problem.</source>
<translation>单击帮助按钮以获取有关解决此问题的更多信息。</translation>
</message>
<message>
<location filename="../gui/main.qml" line="423"/>
<source>Hardware acceleration doesn&apos;t work on XWayland. Continuing on XWayland may result in poor streaming performance. Try running with QT_QPA_PLATFORM=wayland or switch to X11.</source>
<translation>硬件加速在 XWayland 上不起作用。在 XWayland 上继续操作可能会导致流式传输性能下降。</translation>
</message>
<message>
<location filename="../gui/main.qml" line="425"/>
<source>Click the Help button for more information.</source>
<translation>单击帮助按钮以获取更多信息。</translation>
</message>
<message>
<location filename="../gui/main.qml" line="432"/>
<source>This version of Moonlight isn&apos;t optimized for your PC. Please download the &apos;%1&apos; version of Moonlight for the best streaming performance.</source>
<translation>此版本的 Moonlight 尚未针对你的计算机优化。请下载“%1”版本的 Moonlight,以获得最佳的流式传输性能。</translation>
</message>
<message>
<location filename="../gui/main.qml" line="441"/>
<source>Moonlight detected gamepads without a mapping:</source>
<translation>Moonlight 检测到没有映射的手柄:</translation>
</message>
<message>
<location filename="../gui/main.qml" line="443"/>
<source>Click the Help button for information on how to map your gamepads.</source>
<translation>单击帮助按钮以获取有关如何映射手柄的信息。</translation>
</message>
<message>
<location filename="../gui/main.qml" line="451"/>
<source>Are you sure you want to quit?</source>
<translation>确定要退出吗?</translation>
</message>
<message>
<location filename="../gui/main.qml" line="481"/>
<source>Enter the IP address of your GameStream PC:</source>
<translation>输入目标计算机的 IP 地址:</translation>
</message>
</context>
</TS>
+23 -14
View File
@@ -11,7 +11,6 @@
#include <QCursor> #include <QCursor>
#include <QElapsedTimer> #include <QElapsedTimer>
#include <QFile> #include <QFile>
#include<QTranslator>
// Don't let SDL hook our main function, since Qt is already // Don't let SDL hook our main function, since Qt is already
// doing the same thing. This needs to be before any headers // doing the same thing. This needs to be before any headers
@@ -351,14 +350,16 @@ int main(int argc, char *argv[])
} }
#endif #endif
#if defined(Q_OS_DARWIN) && QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) #if !defined(Q_OS_WIN32) || QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
if (!qEnvironmentVariableIsSet("QSG_RHI_BACKEND")) { // Moonlight requires the non-threaded renderer because we depend
// The Metal backend in Qt 6.0.0 causes really strange issues transitioning to // on being able to control the render thread by blocking in the
// full-screen in our SDL window (hangs in SDL's Cocoa_SetWindowFullscreenSpace()) // main thread (and pumping events from the main thread when needed).
// and breaks drawing our status updates in the StreamSegue, so use OpenGL like // That doesn't work with the threaded renderer which causes all
// Qt 5 does until we figure out the cause of these issues. // sorts of odd behavior depending on the platform.
qputenv("QSG_RHI_BACKEND", "opengl"); //
} // NB: Windows defaults to the "windows" non-threaded render loop on
// Qt 5 and the threaded render loop on Qt 6.
qputenv("QSG_RENDER_LOOP", "basic");
#endif #endif
// We don't want system proxies to apply to us // We don't want system proxies to apply to us
@@ -394,6 +395,9 @@ int main(int argc, char *argv[])
// future KMSDRM platforms that use SDL for rendering. // future KMSDRM platforms that use SDL for rendering.
SDL_SetHint(SDL_HINT_VIDEO_DOUBLE_BUFFER, "1"); SDL_SetHint(SDL_HINT_VIDEO_DOUBLE_BUFFER, "1");
// We use MMAL to render on Raspberry Pi, so we do not require DRM master.
SDL_SetHint("SDL_KMSDRM_REQUIRE_DRM_MASTER", "0");
// Use Direct3D 9Ex to avoid a deadlock caused by the D3D device being reset when // Use Direct3D 9Ex to avoid a deadlock caused by the D3D device being reset when
// the user triggers a UAC prompt. This option controls the software/SDL renderer. // the user triggers a UAC prompt. This option controls the software/SDL renderer.
// The DXVA2 renderer uses Direct3D 9Ex itself directly. // The DXVA2 renderer uses Direct3D 9Ex itself directly.
@@ -451,9 +455,9 @@ int main(int argc, char *argv[])
QGuiApplication app(argc, argv); QGuiApplication app(argc, argv);
QTranslator translator; // Apply the initial translation based on user preference
qDebug() << "Translation loaded:" << translator.load(QString(":/languages/qml_") + QLocale::system().name()); StreamingPreferences prefs;
app.installTranslator(&translator); prefs.retranslate();
// After the QGuiApplication is created, the platform stuff will be initialized // After the QGuiApplication is created, the platform stuff will be initialized
// and we can set the SDL video driver to match Qt. // and we can set the SDL video driver to match Qt.
@@ -499,6 +503,11 @@ int main(int argc, char *argv[])
app.setWindowIcon(QIcon(":/res/moonlight.svg")); app.setWindowIcon(QIcon(":/res/moonlight.svg"));
#endif #endif
// This is necessary to show our icon correctly on Wayland
app.setDesktopFileName("com.moonlight_stream.Moonlight.desktop");
qputenv("SDL_VIDEO_WAYLAND_WMCLASS", "com.moonlight_stream.Moonlight");
qputenv("SDL_VIDEO_X11_WMCLASS", "com.moonlight_stream.Moonlight");
// Register our C++ types for QML // Register our C++ types for QML
qmlRegisterType<ComputerModel>("ComputerModel", 1, 0, "ComputerModel"); qmlRegisterType<ComputerModel>("ComputerModel", 1, 0, "ComputerModel");
qmlRegisterType<AppModel>("AppModel", 1, 0, "AppModel"); qmlRegisterType<AppModel>("AppModel", 1, 0, "AppModel");
@@ -525,8 +534,8 @@ int main(int argc, char *argv[])
}); });
qmlRegisterSingletonType<StreamingPreferences>("StreamingPreferences", 1, 0, qmlRegisterSingletonType<StreamingPreferences>("StreamingPreferences", 1, 0,
"StreamingPreferences", "StreamingPreferences",
[](QQmlEngine*, QJSEngine*) -> QObject* { [](QQmlEngine* qmlEngine, QJSEngine*) -> QObject* {
return new StreamingPreferences(); return new StreamingPreferences(qmlEngine);
}); });
// Create the identity manager on the main thread // Create the identity manager on the main thread
+146
View File
@@ -0,0 +1,146 @@
// This file contains hooks for several functions that allow
// Qt and SDL to more or less share DRM master ownership.
//
// This technique requires Linux v5.8 or later, or for Moonlight
// to run as root (with CAP_SYS_ADMIN). Prior to Linux v5.8,
// DRM_IOCTL_DROP_MASTER required CAP_SYS_ADMIN, which prevents
// our trick from working (without root, that is).
//
// The specific kernel change required to run without root is:
// https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=45bc3d26c95a8fc63a7d8668ca9e57ef0883351c
#define _GNU_SOURCE
#include <SDL.h>
#include <dlfcn.h>
#include <unistd.h>
#include <errno.h>
#include <sys/stat.h>
#include <xf86drm.h>
#include <xf86drmMode.h>
// We require SDL 2.0.15+ to hook because it supports sharing
// the DRM FD with our code. This avoids having multiple DRM FDs
// in flight at the same time which would significantly complicate
// the logic here because we'd need to figure out exactly which FD
// should be the master at any given time. With the position of our
// hooks, that is definitely not trivial.
#if SDL_VERSION_ATLEAST(2, 0, 15)
// Qt's DRM master FD grabbed by our hook
int g_QtDrmMasterFd = -1;
struct stat64 g_DrmMasterStat;
// The DRM master FD created for SDL
int g_SdlDrmMasterFd = -1;
// This hook will handle legacy DRM rendering
int drmModeSetCrtc(int fd, uint32_t crtcId, uint32_t bufferId,
uint32_t x, uint32_t y, uint32_t *connectors, int count,
drmModeModeInfoPtr mode)
{
// Grab the first DRM FD that makes it in here. This will be the Qt
// EGLFS backend's DRM FD, which we will need later.
if (g_QtDrmMasterFd == -1) {
g_QtDrmMasterFd = fd;
fstat64(g_QtDrmMasterFd, &g_DrmMasterStat);
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Captured Qt EGLFS DRM master fd (legacy): %d",
g_QtDrmMasterFd);
}
// Call into the real thing
return ((typeof(drmModeSetCrtc)*)dlsym(RTLD_NEXT, "drmModeSetCrtc"))(fd, crtcId, bufferId, x, y, connectors, count, mode);
}
// This hook will handle atomic DRM rendering
int drmModeAtomicCommit(int fd, drmModeAtomicReqPtr req,
uint32_t flags, void *user_data)
{
// Grab the first DRM FD that makes it in here. This will be the Qt
// EGLFS backend's DRM FD, which we will need later.
if (g_QtDrmMasterFd == -1) {
g_QtDrmMasterFd = fd;
fstat64(g_QtDrmMasterFd, &g_DrmMasterStat);
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Captured Qt EGLFS DRM master fd (atomic): %d",
g_QtDrmMasterFd);
}
// Call into the real thing
return ((typeof(drmModeAtomicCommit)*)dlsym(RTLD_NEXT, "drmModeAtomicCommit"))(fd, req, flags, user_data);
}
// This hook will handle SDL's open() on the DRM device. We just need to
// hook this variant of open(), since that's what SDL uses. When we see
// the open a FD for the same card as the Qt DRM master FD, we'll drop
// master on the Qt FD to allow the new FD to have master.
int open(const char *pathname, int flags)
{
// Call the real thing to do the open operation
int fd = ((typeof(open)*)dlsym(RTLD_NEXT, "open"))(pathname, flags);
// If the file was successfully opened and we have a DRM master FD,
// check if the FD we just opened is a DRM device.
if (fd >= 0 && g_QtDrmMasterFd != -1) {
if (strncmp(pathname, "/dev/dri/card", 13) == 0) {
// It's a DRM device, but is it _our_ DRM device?
struct stat64 fdstat;
fstat64(fd, &fdstat);
if (g_DrmMasterStat.st_dev == fdstat.st_dev &&
g_DrmMasterStat.st_ino == fdstat.st_ino) {
// It is our device. Time to do the magic!
// This code assumes SDL only ever opens a single FD
// for a given DRM device.
SDL_assert(g_SdlDrmMasterFd == -1);
// Drop master on Qt's FD so we can pick it up for SDL.
if (drmDropMaster(g_QtDrmMasterFd) < 0) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Failed to drop master on Qt DRM FD: %d",
errno);
// Hope for the best
return fd;
}
// We are not allowed to call drmSetMaster() without CAP_SYS_ADMIN,
// but since we just dropped the master, we can become master by
// simply creating a new FD. Let's do it.
close(fd);
fd = ((typeof(open)*)dlsym(RTLD_NEXT, "open"))(pathname, flags);
g_SdlDrmMasterFd = fd;
}
}
}
return fd;
}
// Our close() hook handles restoring DRM master to the Qt FD
// after SDL closes its DRM FD.
int close(int fd)
{
// Call the real thing
int ret = ((typeof(close)*)dlsym(RTLD_NEXT, "close"))(fd);
if (ret == 0) {
// If we just closed the SDL DRM master FD, restore master
// to the Qt DRM FD. This works because the Qt DRM master FD
// was master once before, so we can set it as master again
// using drmSetMaster() without CAP_SYS_ADMIN.
if (g_SdlDrmMasterFd != -1 && fd == g_SdlDrmMasterFd) {
if (drmSetMaster(g_QtDrmMasterFd) < 0) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Failed to restore master to Qt DRM FD: %d",
errno);
}
g_SdlDrmMasterFd = -1;
}
}
return ret;
}
#endif
+22 -2
View File
@@ -17,14 +17,34 @@
<file>res/baseline-error_outline-24px.svg</file> <file>res/baseline-error_outline-24px.svg</file>
<file>res/baseline-check_circle_outline-24px.svg</file> <file>res/baseline-check_circle_outline-24px.svg</file>
<file>res/Discord-Logo-White.svg</file> <file>res/Discord-Logo-White.svg</file>
<file>languages/qml_de.ts</file>
<file>languages/qml_de.qm</file>
<file>languages/qml_fr.ts</file> <file>languages/qml_fr.ts</file>
<file>languages/qml_fr.qm</file> <file>languages/qml_fr.qm</file>
<file>languages/qml_zh_cn.ts</file>
<file>languages/qml_zh_cn.qm</file>
<file>languages/qml_nb_NO.ts</file>
<file>languages/qml_nb_NO.qm</file>
<file>languages/qml_ru.ts</file>
<file>languages/qml_ru.qm</file>
<file>languages/qml_es.ts</file>
<file>languages/qml_es.qm</file>
<file>languages/qml_ja.ts</file>
<file>languages/qml_ja.qm</file>
<file>languages/qml_vi.ts</file>
<file>languages/qml_vi.qm</file>
<file>languages/qml_th.ts</file>
<file>languages/qml_th.qm</file>
<file>languages/qml_ko.ts</file>
<file>languages/qml_ko.qm</file>
</qresource> </qresource>
<qresource prefix="/data"> <qresource prefix="/data">
<file alias="gamecontrollerdb.txt">SDL_GameControllerDB/gamecontrollerdb.txt</file> <file alias="gamecontrollerdb.txt">SDL_GameControllerDB/gamecontrollerdb.txt</file>
<file alias="ModeSeven.ttf">ModeSeven.ttf</file> <file alias="ModeSeven.ttf">ModeSeven.ttf</file>
<file alias="egl.frag">shaders/egl.frag</file> <file alias="egl_nv12.frag">shaders/egl_nv12.frag</file>
<file alias="egl.vert">shaders/egl.vert</file> <file alias="egl_nv12.vert">shaders/egl_nv12.vert</file>
<file alias="egl_opaque.frag">shaders/egl_opaque.frag</file>
<file alias="egl_opaque.vert">shaders/egl_opaque.vert</file>
<file alias="egl_overlay.frag">shaders/egl_overlay.frag</file> <file alias="egl_overlay.frag">shaders/egl_overlay.frag</file>
<file alias="egl_overlay.vert">shaders/egl_overlay.vert</file> <file alias="egl_overlay.vert">shaders/egl_overlay.vert</file>
</qresource> </qresource>
+137
View File
@@ -0,0 +1,137 @@
#include "compatfetcher.h"
#include "path.h"
#include <QNetworkReply>
#include <QSettings>
#define COMPAT_VERSION "v1"
#define COMPAT_KEY "latestsupportedversion-"
CompatFetcher::CompatFetcher(QObject *parent) :
QObject(parent),
m_Nam(this)
{
// Never communicate over HTTP
m_Nam.setStrictTransportSecurityEnabled(true);
// Allow HTTP redirects
m_Nam.setRedirectPolicy(QNetworkRequest::NoLessSafeRedirectPolicy);
connect(&m_Nam, &QNetworkAccessManager::finished,
this, &CompatFetcher::handleCompatInfoFetched);
}
void CompatFetcher::start()
{
#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0) && QT_VERSION < QT_VERSION_CHECK(5, 15, 1) && !defined(QT_NO_BEARERMANAGEMENT)
// HACK: Set network accessibility to work around QTBUG-80947 (introduced in Qt 5.14.0 and fixed in Qt 5.15.1)
QT_WARNING_PUSH
QT_WARNING_DISABLE_DEPRECATED
m_Nam.setNetworkAccessible(QNetworkAccessManager::Accessible);
QT_WARNING_POP
#endif
QUrl url("https://moonlight-stream.org/compatibility/" COMPAT_VERSION);
QNetworkRequest request(url);
#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)
request.setAttribute(QNetworkRequest::Http2AllowedAttribute, true);
#else
request.setAttribute(QNetworkRequest::HTTP2AllowedAttribute, true);
#endif
// We'll get a callback when this is finished
m_Nam.get(request);
}
bool CompatFetcher::isGfeVersionSupported(QString gfeVersion)
{
QSettings settings;
if (gfeVersion.trimmed().isEmpty()) {
// If we don't have a GFE version, just allow it
return true;
}
QString latestSupportedVersion = settings.value(COMPAT_KEY COMPAT_VERSION).toString();
if (latestSupportedVersion.isEmpty()) {
// We don't have compat data yet, so just assume it's supported
return true;
}
QStringList latestSupportedVersionQuad = latestSupportedVersion.split('.');
QStringList gfeVersionQuad = gfeVersion.split('.');
if (gfeVersionQuad.count() <= 1) {
qWarning() << "Failed to parse GFE version:" << gfeVersion;
return true;
}
if (latestSupportedVersionQuad.count() <= 1) {
qWarning() << "Failed to parse latest supported version:" << latestSupportedVersion;
return true;
}
for (int i = 0;; i++) {
int actualVerVal = 0;
int latestSupportedVal = 0;
// Treat missing decimal places as 0
if (i < gfeVersionQuad.count()) {
bool ok;
actualVerVal = gfeVersionQuad[i].toInt(&ok);
if (!ok || actualVerVal < 0) {
// Return true to be safe
qWarning() << "Failed to parse GFE version:" << gfeVersion;
return true;
}
}
if (i < latestSupportedVersionQuad.count()) {
bool ok;
latestSupportedVal = latestSupportedVersionQuad[i].toInt(&ok);
if (!ok || latestSupportedVal < 0) {
// Return true to be safe
qWarning() << "Failed to parse latest supported version:" << latestSupportedVersion;
return true;
}
}
if (i >= gfeVersionQuad.count() && i >= latestSupportedVersionQuad.count()) {
// Equal versions - this is fine
return true;
}
if (actualVerVal < latestSupportedVal) {
// Actual version is lower than latest supported - this is fine
return true;
}
else if (actualVerVal > latestSupportedVal) {
// Actual version is greater than latest supported - this is bad
qWarning() << "GFE version" << gfeVersion << "is not supported by this version of Moonlight";
return false;
}
}
}
void CompatFetcher::handleCompatInfoFetched(QNetworkReply* reply)
{
Q_ASSERT(reply->isFinished());
if (reply->error() == QNetworkReply::NoError) {
// Queue the reply for deletion
reply->deleteLater();
QString version = QString(reply->readAll()).trimmed();
QSettings settings;
settings.setValue(COMPAT_KEY COMPAT_VERSION, version);
qInfo() << "Latest supported GFE server:" << version;
}
else {
qWarning() << "Failed to download latest compatibility data:" << reply->error();
reply->deleteLater();
}
}
+22
View File
@@ -0,0 +1,22 @@
#pragma once
#include <QObject>
#include <QNetworkAccessManager>
class CompatFetcher : public QObject
{
Q_OBJECT
public:
explicit CompatFetcher(QObject *parent = nullptr);
void start();
static bool isGfeVersionSupported(QString gfeVersion);
private slots:
void handleCompatInfoFetched(QNetworkReply* reply);
private:
QNetworkAccessManager m_Nam;
};
+2 -2
View File
@@ -13,8 +13,8 @@ MappingFetcher::MappingFetcher(QObject *parent) :
// Allow HTTP redirects // Allow HTTP redirects
m_Nam.setRedirectPolicy(QNetworkRequest::NoLessSafeRedirectPolicy); m_Nam.setRedirectPolicy(QNetworkRequest::NoLessSafeRedirectPolicy);
connect(&m_Nam, SIGNAL(finished(QNetworkReply*)), connect(&m_Nam, &QNetworkAccessManager::finished,
this, SLOT(handleMappingListFetched(QNetworkReply*))); this, &MappingFetcher::handleMappingListFetched);
} }
void MappingFetcher::start() void MappingFetcher::start()
+1 -1
View File
@@ -41,7 +41,7 @@ MappingManager::MappingManager()
#else #else
.split('\n', QString::SkipEmptyParts); .split('\n', QString::SkipEmptyParts);
#endif #endif
for (QString sdlMapping : sdlMappings) { for (const QString& sdlMapping : sdlMappings) {
SdlGamepadMapping mapping(sdlMapping); SdlGamepadMapping mapping(sdlMapping);
addMapping(mapping); addMapping(mapping);
} }
+105 -3
View File
@@ -1,6 +1,11 @@
#include "streamingpreferences.h" #include "streamingpreferences.h"
#include <QSettings> #include <QSettings>
#include <QTranslator>
#include <QCoreApplication>
#include <QLocale>
#include <QtDebug>
#define SER_STREAMSETTINGS "streamsettings" #define SER_STREAMSETTINGS "streamsettings"
#define SER_WIDTH "width" #define SER_WIDTH "width"
@@ -36,11 +41,20 @@
#define SER_REVERSESCROLL "reversescroll" #define SER_REVERSESCROLL "reversescroll"
#define SER_SWAPFACEBUTTONS "swapfacebuttons" #define SER_SWAPFACEBUTTONS "swapfacebuttons"
#define SER_CAPTURESYSKEYS "capturesyskeys" #define SER_CAPTURESYSKEYS "capturesyskeys"
#define SER_LANGUAGE "language"
#define CURRENT_DEFAULT_VER 1 #define CURRENT_DEFAULT_VER 1
StreamingPreferences::StreamingPreferences(QObject *parent) StreamingPreferences::StreamingPreferences(QObject *parent)
: QObject(parent) : QObject(parent),
m_QmlEngine(nullptr)
{
reload();
}
StreamingPreferences::StreamingPreferences(QQmlEngine *qmlEngine, QObject *parent)
: QObject(parent),
m_QmlEngine(qmlEngine)
{ {
reload(); reload();
} }
@@ -81,7 +95,8 @@ void StreamingPreferences::reload()
backgroundGamepad = settings.value(SER_BACKGROUNDGAMEPAD, false).toBool(); backgroundGamepad = settings.value(SER_BACKGROUNDGAMEPAD, false).toBool();
reverseScrollDirection = settings.value(SER_REVERSESCROLL, false).toBool(); reverseScrollDirection = settings.value(SER_REVERSESCROLL, false).toBool();
swapFaceButtons = settings.value(SER_SWAPFACEBUTTONS, false).toBool(); swapFaceButtons = settings.value(SER_SWAPFACEBUTTONS, false).toBool();
captureSysKeys = settings.value(SER_CAPTURESYSKEYS, false).toBool(); captureSysKeysMode = static_cast<CaptureSysKeysMode>(settings.value(SER_CAPTURESYSKEYS,
static_cast<int>(CaptureSysKeysMode::CSK_OFF)).toInt());
audioConfig = static_cast<AudioConfig>(settings.value(SER_AUDIOCFG, audioConfig = static_cast<AudioConfig>(settings.value(SER_AUDIOCFG,
static_cast<int>(AudioConfig::AC_STEREO)).toInt()); static_cast<int>(AudioConfig::AC_STEREO)).toInt());
videoCodecConfig = static_cast<VideoCodecConfig>(settings.value(SER_VIDEOCFG, videoCodecConfig = static_cast<VideoCodecConfig>(settings.value(SER_VIDEOCFG,
@@ -95,6 +110,8 @@ void StreamingPreferences::reload()
uiDisplayMode = static_cast<UIDisplayMode>(settings.value(SER_UIDISPLAYMODE, uiDisplayMode = static_cast<UIDisplayMode>(settings.value(SER_UIDISPLAYMODE,
static_cast<int>(settings.value(SER_STARTWINDOWED, true).toBool() ? UIDisplayMode::UI_WINDOWED static_cast<int>(settings.value(SER_STARTWINDOWED, true).toBool() ? UIDisplayMode::UI_WINDOWED
: UIDisplayMode::UI_MAXIMIZED)).toInt()); : UIDisplayMode::UI_MAXIMIZED)).toInt());
language = static_cast<Language>(settings.value(SER_LANGUAGE,
static_cast<int>(Language::LANG_AUTO)).toInt());
// Perform default settings updates as required based on last default version // Perform default settings updates as required based on last default version
@@ -108,6 +125,90 @@ void StreamingPreferences::reload()
} }
} }
bool StreamingPreferences::retranslate()
{
static QTranslator* translator = nullptr;
#if QT_VERSION < QT_VERSION_CHECK(5, 10, 0)
if (m_QmlEngine != nullptr) {
// Dynamic retranslation is not supported until Qt 5.10
return false;
}
#endif
QTranslator* newTranslator = new QTranslator();
QString languageSuffix = getSuffixFromLanguage(language);
// Remove the old translator, even if we can't load a new one.
// Otherwise we'll be stuck with the old translated values instead
// of defaulting to English.
if (translator != nullptr) {
QCoreApplication::removeTranslator(translator);
delete translator;
translator = nullptr;
}
if (newTranslator->load(QString(":/languages/qml_") + languageSuffix)) {
qInfo() << "Successfully loaded translation for " << languageSuffix;
translator = newTranslator;
QCoreApplication::installTranslator(translator);
}
else {
qInfo() << "No translation available for " << languageSuffix;
delete newTranslator;
}
if (m_QmlEngine != nullptr) {
#if QT_VERSION >= QT_VERSION_CHECK(5, 10, 0)
// This is a dynamic retranslation from the settings page.
// We have to kick the QML engine into reloading our text.
m_QmlEngine->retranslate();
#else
// Unreachable below Qt 5.10 due to the check above
Q_ASSERT(false);
#endif
}
else {
// This is a translation from a non-QML context, which means
// it is probably app startup. There's nothing to refresh.
}
return true;
}
QString StreamingPreferences::getSuffixFromLanguage(StreamingPreferences::Language lang)
{
switch (lang)
{
case LANG_DE:
return "de";
case LANG_EN:
return "en";
case LANG_FR:
return "fr";
case LANG_ZH_CN:
return "zh_cn";
case LANG_NB_NO:
return "nb_NO";
case LANG_RU:
return "ru";
case LANG_ES:
return "es";
case LANG_JA:
return "ja";
case LANG_VI:
return "vi";
case LANG_TH:
return "th";
case LANG_KO:
return "ko";
case LANG_AUTO:
default:
return QLocale::system().name();
}
}
void StreamingPreferences::save() void StreamingPreferences::save()
{ {
QSettings settings; QSettings settings;
@@ -136,13 +237,14 @@ void StreamingPreferences::save()
settings.setValue(SER_VIDEODEC, static_cast<int>(videoDecoderSelection)); settings.setValue(SER_VIDEODEC, static_cast<int>(videoDecoderSelection));
settings.setValue(SER_WINDOWMODE, static_cast<int>(windowMode)); settings.setValue(SER_WINDOWMODE, static_cast<int>(windowMode));
settings.setValue(SER_UIDISPLAYMODE, static_cast<int>(uiDisplayMode)); settings.setValue(SER_UIDISPLAYMODE, static_cast<int>(uiDisplayMode));
settings.setValue(SER_LANGUAGE, static_cast<int>(language));
settings.setValue(SER_DEFAULTVER, CURRENT_DEFAULT_VER); settings.setValue(SER_DEFAULTVER, CURRENT_DEFAULT_VER);
settings.setValue(SER_SWAPMOUSEBUTTONS, swapMouseButtons); settings.setValue(SER_SWAPMOUSEBUTTONS, swapMouseButtons);
settings.setValue(SER_MUTEONFOCUSLOSS, muteOnFocusLoss); settings.setValue(SER_MUTEONFOCUSLOSS, muteOnFocusLoss);
settings.setValue(SER_BACKGROUNDGAMEPAD, backgroundGamepad); settings.setValue(SER_BACKGROUNDGAMEPAD, backgroundGamepad);
settings.setValue(SER_REVERSESCROLL, reverseScrollDirection); settings.setValue(SER_REVERSESCROLL, reverseScrollDirection);
settings.setValue(SER_SWAPFACEBUTTONS, swapFaceButtons); settings.setValue(SER_SWAPFACEBUTTONS, swapFaceButtons);
settings.setValue(SER_CAPTURESYSKEYS, captureSysKeys); settings.setValue(SER_CAPTURESYSKEYS, captureSysKeysMode);
} }
int StreamingPreferences::getDefaultBitrate(int width, int height, int fps) int StreamingPreferences::getDefaultBitrate(int width, int height, int fps)
+43 -3
View File
@@ -2,6 +2,7 @@
#include <QObject> #include <QObject>
#include <QRect> #include <QRect>
#include <QQmlEngine>
class StreamingPreferences : public QObject class StreamingPreferences : public QObject
{ {
@@ -9,6 +10,7 @@ class StreamingPreferences : public QObject
public: public:
StreamingPreferences(QObject *parent = nullptr); StreamingPreferences(QObject *parent = nullptr);
StreamingPreferences(QQmlEngine *qmlEngine, QObject *parent = nullptr);
Q_INVOKABLE static int Q_INVOKABLE static int
getDefaultBitrate(int width, int height, int fps); getDefaultBitrate(int width, int height, int fps);
@@ -58,6 +60,34 @@ public:
}; };
Q_ENUM(UIDisplayMode) Q_ENUM(UIDisplayMode)
// New entries must go at the end of the enum
// to avoid renumbering existing entries (which
// would affect existing user preferences).
enum Language
{
LANG_AUTO,
LANG_EN,
LANG_FR,
LANG_ZH_CN,
LANG_DE,
LANG_NB_NO,
LANG_RU,
LANG_ES,
LANG_JA,
LANG_VI,
LANG_TH,
LANG_KO
};
Q_ENUM(Language);
enum CaptureSysKeysMode
{
CSK_OFF,
CSK_FULLSCREEN,
CSK_ALWAYS,
};
Q_ENUM(CaptureSysKeysMode);
Q_PROPERTY(int width MEMBER width NOTIFY displayModeChanged) Q_PROPERTY(int width MEMBER width NOTIFY displayModeChanged)
Q_PROPERTY(int height MEMBER height NOTIFY displayModeChanged) Q_PROPERTY(int height MEMBER height NOTIFY displayModeChanged)
Q_PROPERTY(int fps MEMBER fps NOTIFY displayModeChanged) Q_PROPERTY(int fps MEMBER fps NOTIFY displayModeChanged)
@@ -87,7 +117,10 @@ public:
Q_PROPERTY(bool backgroundGamepad MEMBER backgroundGamepad NOTIFY backgroundGamepadChanged) Q_PROPERTY(bool backgroundGamepad MEMBER backgroundGamepad NOTIFY backgroundGamepadChanged)
Q_PROPERTY(bool reverseScrollDirection MEMBER reverseScrollDirection NOTIFY reverseScrollDirectionChanged) Q_PROPERTY(bool reverseScrollDirection MEMBER reverseScrollDirection NOTIFY reverseScrollDirectionChanged)
Q_PROPERTY(bool swapFaceButtons MEMBER swapFaceButtons NOTIFY swapFaceButtonsChanged) Q_PROPERTY(bool swapFaceButtons MEMBER swapFaceButtons NOTIFY swapFaceButtonsChanged)
Q_PROPERTY(bool captureSysKeys MEMBER captureSysKeys NOTIFY captureSysKeysChanged) Q_PROPERTY(CaptureSysKeysMode captureSysKeysMode MEMBER captureSysKeysMode NOTIFY captureSysKeysModeChanged)
Q_PROPERTY(Language language MEMBER language NOTIFY languageChanged);
Q_INVOKABLE bool retranslate();
// Directly accessible members for preferences // Directly accessible members for preferences
int width; int width;
@@ -113,7 +146,6 @@ public:
bool backgroundGamepad; bool backgroundGamepad;
bool reverseScrollDirection; bool reverseScrollDirection;
bool swapFaceButtons; bool swapFaceButtons;
bool captureSysKeys;
int packetSize; int packetSize;
AudioConfig audioConfig; AudioConfig audioConfig;
VideoCodecConfig videoCodecConfig; VideoCodecConfig videoCodecConfig;
@@ -121,6 +153,8 @@ public:
WindowMode windowMode; WindowMode windowMode;
WindowMode recommendedFullScreenMode; WindowMode recommendedFullScreenMode;
UIDisplayMode uiDisplayMode; UIDisplayMode uiDisplayMode;
Language language;
CaptureSysKeysMode captureSysKeysMode;
signals: signals:
void displayModeChanged(); void displayModeChanged();
@@ -149,6 +183,12 @@ signals:
void backgroundGamepadChanged(); void backgroundGamepadChanged();
void reverseScrollDirectionChanged(); void reverseScrollDirectionChanged();
void swapFaceButtonsChanged(); void swapFaceButtonsChanged();
void captureSysKeysChanged(); void captureSysKeysModeChanged();
void languageChanged();
private:
QString getSuffixFromLanguage(Language lang);
QQmlEngine* m_QmlEngine;
}; };
File renamed without changes.
File renamed without changes.
+12
View File
@@ -0,0 +1,12 @@
#version 300 es
#extension GL_OES_EGL_image_external : require
precision mediump float;
out vec4 FragColor;
in vec2 vTextCoord;
uniform samplerExternalOES uTexture;
void main() {
FragColor = texture2D(uTexture, vTextCoord);
}
+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);
}
-96
View File
@@ -1,96 +0,0 @@
#include "qtaud.h"
#include <QDebug>
QtAudioRenderer::QtAudioRenderer()
: m_AudioOutput(nullptr),
m_OutputDevice(nullptr)
{
}
QtAudioRenderer::~QtAudioRenderer()
{
delete m_AudioOutput;
}
bool QtAudioRenderer::prepareForPlayback(const OPUS_MULTISTREAM_CONFIGURATION* opusConfig)
{
QAudioFormat format;
// testAudio() assumes this sample rate
Q_ASSERT(opusConfig->sampleRate == 48000);
format.setSampleRate(opusConfig->sampleRate);
format.setChannelCount(opusConfig->channelCount);
format.setSampleSize(16);
format.setSampleType(QAudioFormat::SignedInt);
format.setCodec("audio/pcm");
m_AudioOutput = new QAudioOutput(format);
// Buffer 40 ms of audio data
m_AudioOutput->setBufferSize(format.bytesForDuration(40000));
m_OutputDevice = m_AudioOutput->start();
if (m_OutputDevice == nullptr) {
qWarning() << "Audio start failed:" << m_AudioOutput->error();
return false;
}
// Check what buffer size was really used after starting
qInfo() << "Audio stream buffer:" << m_AudioOutput->bufferSize() << "bytes";
return true;
}
void QtAudioRenderer::submitAudio(short* audioBuffer, int audioSize)
{
m_OutputDevice->write((const char *)audioBuffer, audioSize);
}
bool QtAudioRenderer::testAudio(int audioConfiguration)
{
QAudioFormat format;
format.setSampleRate(48000);
format.setSampleSize(16);
format.setSampleType(QAudioFormat::SignedInt);
format.setCodec("audio/pcm");
switch (audioConfiguration) {
case AUDIO_CONFIGURATION_STEREO:
format.setChannelCount(2);
break;
case AUDIO_CONFIGURATION_51_SURROUND:
format.setChannelCount(6);
break;
default:
Q_ASSERT(false);
return false;
}
if (QAudioDeviceInfo::defaultOutputDevice().isFormatSupported(format)) {
qInfo() << "Audio test successful with" << format.channelCount() << "channels";
return true;
}
else {
qWarning() << "Audio test failed with" << format.channelCount() << "channels";
return false;
}
}
int QtAudioRenderer::detectAudioConfiguration()
{
int preferredChannelCount = QAudioDeviceInfo::defaultOutputDevice().preferredFormat().channelCount();
qInfo() << "Audio output device prefers" << preferredChannelCount << "channel configuration";
// We can better downmix 5.1 to quad than we can upmix stereo
if (preferredChannelCount > 2) {
return AUDIO_CONFIGURATION_51_SURROUND;
}
else {
return AUDIO_CONFIGURATION_STEREO;
}
}
-24
View File
@@ -1,24 +0,0 @@
#pragma once
#include "renderer.h"
#include <QAudioOutput>
class QtAudioRenderer : public IAudioRenderer
{
public:
QtAudioRenderer();
virtual ~QtAudioRenderer();
virtual bool prepareForPlayback(const OPUS_MULTISTREAM_CONFIGURATION* opusConfig);
virtual void submitAudio(short* audioBuffer, int audioSize);
virtual bool testAudio(int audioConfiguration);
virtual int detectAudioConfiguration();
private:
QAudioOutput* m_AudioOutput;
QIODevice* m_OutputDevice;
};
+190
View File
@@ -0,0 +1,190 @@
#include "input.h"
#include <QString>
int SdlInputHandler::clipboardThreadProc(void *ptr)
{
auto me = (SdlInputHandler*)ptr;
while (!SDL_AtomicGet(&me->m_ShutdownClipboardThread)) {
QString clipboardData;
SDL_LockMutex(me->m_ClipboardLock);
for (;;) {
clipboardData = me->m_ClipboardData;
me->m_ClipboardData.clear();
if (!clipboardData.isEmpty() || SDL_AtomicGet(&me->m_ShutdownClipboardThread)) {
break;
}
SDL_CondWait(me->m_ClipboardHasData, me->m_ClipboardLock);
}
SDL_UnlockMutex(me->m_ClipboardLock);
// We might get here on shutdown, so don't send text if there's
// nothing to send.
if (!clipboardData.isEmpty()) {
me->sendText(clipboardData);
}
}
return 0;
}
#define MAP_KEY(c, sc) \
case c: \
event.key.keysym.scancode = sc; \
break
#define MAP_KEY_SHIFT(c, sc) \
case c: \
event.key.keysym.scancode = sc; \
event.key.keysym.mod = KMOD_SHIFT; \
break
void SdlInputHandler::sendText(QString& string)
{
for (int i = 0; i < string.size(); i++) {
char16_t c = string[i].unicode();
SDL_Event event = {};
// If we're sending a very long string, we might get a termination request
// while we're in the middle of sending a string. In that case, just bail.
if (SDL_AtomicGet(&m_ShutdownClipboardThread)) {
return;
}
if (c >= 'A' && c <= 'Z') {
event.key.keysym.scancode = (SDL_Scancode)((c - 'A') + SDL_SCANCODE_A);
event.key.keysym.mod = KMOD_SHIFT;
}
else if (c >= 'a' && c <= 'z') {
event.key.keysym.scancode = (SDL_Scancode)((c - 'a') + SDL_SCANCODE_A);
}
else if (c >= '1' && c <= '9') {
event.key.keysym.scancode = (SDL_Scancode)((c - '1') + SDL_SCANCODE_1);
}
else {
// Fixup some Unicode characters to be expressible in ASCII
switch (c) {
case u'\U0000201C': // Left "
case u'\U0000201D': // Right "
c = '"';
break;
case u'\U00002018': // Left '
case u'\U00002019': // Right '
c = '\'';
break;
case u'\U00002013': // Smart -
c = '-';
break;
case u'\U00002026': // Ellipsis (...)
// Convert this character into 3 periods
string.insert(i+1, '.');
string.insert(i+1, '.');
c = '.';
break;
case u'\U000000A0': // Non-breaking space
c = ' ';
break;
default:
// Nothing
break;
}
switch (c) {
// Handle CRLF separately to avoid duplicate newlines
case '\r':
if (string[i + 1] == '\n') {
i++;
}
event.key.keysym.scancode = SDL_SCANCODE_RETURN;
break;
MAP_KEY('\b', SDL_SCANCODE_BACKSPACE);
MAP_KEY('\n', SDL_SCANCODE_RETURN);
MAP_KEY('\t', SDL_SCANCODE_TAB);
MAP_KEY(' ', SDL_SCANCODE_SPACE);
MAP_KEY_SHIFT('!', SDL_SCANCODE_1);
MAP_KEY_SHIFT('"', SDL_SCANCODE_APOSTROPHE);
MAP_KEY_SHIFT('#', SDL_SCANCODE_3);
MAP_KEY_SHIFT('$', SDL_SCANCODE_4);
MAP_KEY_SHIFT('%', SDL_SCANCODE_5);
MAP_KEY_SHIFT('&', SDL_SCANCODE_7);
MAP_KEY('\'', SDL_SCANCODE_APOSTROPHE);
MAP_KEY_SHIFT('(', SDL_SCANCODE_9);
MAP_KEY_SHIFT(')', SDL_SCANCODE_0);
MAP_KEY_SHIFT('*', SDL_SCANCODE_8);
MAP_KEY_SHIFT('+', SDL_SCANCODE_EQUALS);
MAP_KEY(',', SDL_SCANCODE_COMMA);
MAP_KEY('-', SDL_SCANCODE_MINUS);
MAP_KEY('.', SDL_SCANCODE_PERIOD);
MAP_KEY('/', SDL_SCANCODE_SLASH);
MAP_KEY('0', SDL_SCANCODE_0);
MAP_KEY_SHIFT(':', SDL_SCANCODE_SEMICOLON);
MAP_KEY(';', SDL_SCANCODE_SEMICOLON);
MAP_KEY_SHIFT('<', SDL_SCANCODE_COMMA);
MAP_KEY('=', SDL_SCANCODE_EQUALS);
MAP_KEY_SHIFT('>', SDL_SCANCODE_PERIOD);
MAP_KEY_SHIFT('?', SDL_SCANCODE_SLASH);
MAP_KEY_SHIFT('@', SDL_SCANCODE_2);
MAP_KEY('[', SDL_SCANCODE_LEFTBRACKET);
MAP_KEY('\\', SDL_SCANCODE_BACKSLASH);
MAP_KEY(']', SDL_SCANCODE_RIGHTBRACKET);
MAP_KEY_SHIFT('^', SDL_SCANCODE_6);
MAP_KEY_SHIFT('_', SDL_SCANCODE_MINUS);
MAP_KEY('`', SDL_SCANCODE_GRAVE);
MAP_KEY_SHIFT('{', SDL_SCANCODE_LEFTBRACKET);
MAP_KEY_SHIFT('|', SDL_SCANCODE_BACKSLASH);
MAP_KEY_SHIFT('}', SDL_SCANCODE_RIGHTBRACKET);
MAP_KEY_SHIFT('~', SDL_SCANCODE_GRAVE);
default:
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"Pasting text - non-ASCII character value 'U+%x' ignored",
c);
continue;
}
}
if (event.key.keysym.mod & KMOD_SHIFT) {
SDL_Event modifierEvent = {};
modifierEvent.type = SDL_KEYDOWN;
modifierEvent.key.state = SDL_PRESSED;
modifierEvent.key.keysym.scancode = SDL_SCANCODE_LSHIFT;
handleKeyEvent(&modifierEvent.key);
SDL_Delay(10);
}
event.type = SDL_KEYDOWN;
event.key.state = SDL_PRESSED;
handleKeyEvent(&event.key);
SDL_Delay(10);
event.type = SDL_KEYUP;
event.key.state = SDL_RELEASED;
handleKeyEvent(&event.key);
if (event.key.keysym.mod & KMOD_SHIFT) {
SDL_Event modifierEvent = {};
modifierEvent.type = SDL_KEYUP;
modifierEvent.key.state = SDL_RELEASED;
modifierEvent.key.keysym.scancode = SDL_SCANCODE_LSHIFT;
handleKeyEvent(&modifierEvent.key);
SDL_Delay(10);
}
}
}
+16 -1
View File
@@ -269,7 +269,22 @@ void SdlInputHandler::handleControllerButtonEvent(SDL_ControllerButtonEvent* eve
event.quit.timestamp = SDL_GetTicks(); event.quit.timestamp = SDL_GetTicks();
SDL_PushEvent(&event); SDL_PushEvent(&event);
// Clear buttons down on this gameapd // Clear buttons down on this gamepad
LiSendMultiControllerEvent(state->index, m_GamepadMask,
0, 0, 0, 0, 0, 0, 0);
return;
}
// Handle Select+L1+R1+X as a gamepad overlay combo
if (state->buttons == (BACK_FLAG | LB_FLAG | RB_FLAG | X_FLAG)) {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Detected stats toggle gamepad combo");
// Toggle the stats overlay
Session::get()->getOverlayManager().setOverlayState(Overlay::OverlayDebug,
!Session::get()->getOverlayManager().isOverlayEnabled(Overlay::OverlayDebug));
// Clear buttons down on this gamepad
LiSendMultiControllerEvent(state->index, m_GamepadMask, LiSendMultiControllerEvent(state->index, m_GamepadMask,
0, 0, 0, 0, 0, 0, 0); 0, 0, 0, 0, 0, 0, 0);
return; return;
+227 -208
View File
@@ -5,15 +5,16 @@
#include "path.h" #include "path.h"
#include "utils.h" #include "utils.h"
#ifdef Q_OS_WIN32
#include <SDL_syswm.h>
#endif
#include <QtGlobal> #include <QtGlobal>
#include <QDir> #include <QDir>
#include <QGuiApplication>
#define MOUSE_POLLING_INTERVAL 5 #define MOUSE_POLLING_INTERVAL 5
#ifdef Q_OS_WIN32
HHOOK g_KeyboardHook;
#endif
SdlInputHandler::SdlInputHandler(StreamingPreferences& prefs, NvComputer*, int streamWidth, int streamHeight) SdlInputHandler::SdlInputHandler(StreamingPreferences& prefs, NvComputer*, int streamWidth, int streamHeight)
: m_MultiController(prefs.multiController), : m_MultiController(prefs.multiController),
m_GamepadMouse(prefs.gamepadMouse), m_GamepadMouse(prefs.gamepadMouse),
@@ -25,19 +26,25 @@ SdlInputHandler::SdlInputHandler(StreamingPreferences& prefs, NvComputer*, int s
m_MouseWasInVideoRegion(false), m_MouseWasInVideoRegion(false),
m_PendingMouseButtonsAllUpOnVideoRegionLeave(false), m_PendingMouseButtonsAllUpOnVideoRegionLeave(false),
m_FakeCaptureActive(false), m_FakeCaptureActive(false),
m_CaptureSystemKeysEnabled(prefs.captureSysKeys || !WMUtils::isRunningWindowManager()), m_CaptureSystemKeysMode(prefs.captureSysKeysMode),
m_MouseCursorCapturedVisibilityState(SDL_DISABLE),
m_LongPressTimer(0), m_LongPressTimer(0),
m_StreamWidth(streamWidth), m_StreamWidth(streamWidth),
m_StreamHeight(streamHeight), m_StreamHeight(streamHeight),
m_AbsoluteMouseMode(prefs.absoluteMouseMode), m_AbsoluteMouseMode(prefs.absoluteMouseMode),
m_AbsoluteTouchMode(prefs.absoluteTouchMode), m_AbsoluteTouchMode(prefs.absoluteTouchMode),
m_PendingMouseLeaveButtonUp(0),
m_LeftButtonReleaseTimer(0), m_LeftButtonReleaseTimer(0),
m_RightButtonReleaseTimer(0), m_RightButtonReleaseTimer(0),
m_DragTimer(0), m_DragTimer(0),
m_DragButton(0), m_DragButton(0),
m_NumFingersDown(0) m_NumFingersDown(0),
m_ClipboardData()
{ {
// System keys are always captured when running without a DE
if (!WMUtils::isRunningDesktopEnvironment()) {
m_CaptureSystemKeysMode = StreamingPreferences::CSK_ALWAYS;
}
// Allow gamepad input when the app doesn't have focus if requested // Allow gamepad input when the app doesn't have focus if requested
SDL_SetHint(SDL_HINT_JOYSTICK_ALLOW_BACKGROUND_EVENTS, prefs.backgroundGamepad ? "1" : "0"); SDL_SetHint(SDL_HINT_JOYSTICK_ALLOW_BACKGROUND_EVENTS, prefs.backgroundGamepad ? "1" : "0");
@@ -49,10 +56,15 @@ SdlInputHandler::SdlInputHandler(StreamingPreferences& prefs, NvComputer*, int s
prefs.absoluteMouseMode ? "1" : "0", prefs.absoluteMouseMode ? "1" : "0",
SDL_HINT_OVERRIDE); SDL_HINT_OVERRIDE);
// If we're grabbing system keys, enable the X11 keyboard grab in SDL #if !SDL_VERSION_ATLEAST(2, 0, 15)
// For older versions of SDL (2.0.14 and earlier), use SDL_HINT_GRAB_KEYBOARD
SDL_SetHintWithPriority(SDL_HINT_GRAB_KEYBOARD, SDL_SetHintWithPriority(SDL_HINT_GRAB_KEYBOARD,
m_CaptureSystemKeysEnabled ? "1" : "0", m_CaptureSystemKeysMode != StreamingPreferences::CSK_OFF ? "1" : "0",
SDL_HINT_OVERRIDE); SDL_HINT_OVERRIDE);
#endif
// Opt-out of SDL's built-in Alt+Tab handling while keyboard grab is enabled
SDL_SetHint("SDL_ALLOW_ALT_TAB_WHILE_GRABBED", "0");
// Allow clicks to pass through to us when focusing the window. If we're in // 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 // absolute mouse mode, this will avoid the user having to click twice to
@@ -67,6 +79,47 @@ SdlInputHandler::SdlInputHandler(StreamingPreferences& prefs, NvComputer*, int s
SDL_SetHint("SDL_JOYSTICK_HIDAPI_PS4_RUMBLE", "1"); SDL_SetHint("SDL_JOYSTICK_HIDAPI_PS4_RUMBLE", "1");
SDL_SetHint("SDL_JOYSTICK_HIDAPI_PS5_RUMBLE", "1"); SDL_SetHint("SDL_JOYSTICK_HIDAPI_PS5_RUMBLE", "1");
// Populate special key combo configuration
m_SpecialKeyCombos[KeyComboQuit].keyCombo = KeyComboQuit;
m_SpecialKeyCombos[KeyComboQuit].keyCode = SDLK_q;
m_SpecialKeyCombos[KeyComboQuit].scanCode = SDL_SCANCODE_Q;
m_SpecialKeyCombos[KeyComboQuit].enabled = true;
m_SpecialKeyCombos[KeyComboUngrabInput].keyCombo = KeyComboUngrabInput;
m_SpecialKeyCombos[KeyComboUngrabInput].keyCode = SDLK_z;
m_SpecialKeyCombos[KeyComboUngrabInput].scanCode = SDL_SCANCODE_Z;
m_SpecialKeyCombos[KeyComboUngrabInput].enabled = QGuiApplication::platformName() != "eglfs";
m_SpecialKeyCombos[KeyComboToggleFullScreen].keyCombo = KeyComboToggleFullScreen;
m_SpecialKeyCombos[KeyComboToggleFullScreen].keyCode = SDLK_x;
m_SpecialKeyCombos[KeyComboToggleFullScreen].scanCode = SDL_SCANCODE_X;
m_SpecialKeyCombos[KeyComboToggleFullScreen].enabled = QGuiApplication::platformName() != "eglfs";
m_SpecialKeyCombos[KeyComboToggleStatsOverlay].keyCombo = KeyComboToggleStatsOverlay;
m_SpecialKeyCombos[KeyComboToggleStatsOverlay].keyCode = SDLK_s;
m_SpecialKeyCombos[KeyComboToggleStatsOverlay].scanCode = SDL_SCANCODE_S;
m_SpecialKeyCombos[KeyComboToggleStatsOverlay].enabled = true;
m_SpecialKeyCombos[KeyComboToggleMouseMode].keyCombo = KeyComboToggleMouseMode;
m_SpecialKeyCombos[KeyComboToggleMouseMode].keyCode = SDLK_m;
m_SpecialKeyCombos[KeyComboToggleMouseMode].scanCode = SDL_SCANCODE_M;
m_SpecialKeyCombos[KeyComboToggleMouseMode].enabled = true;
m_SpecialKeyCombos[KeyComboToggleCursorHide].keyCombo = KeyComboToggleCursorHide;
m_SpecialKeyCombos[KeyComboToggleCursorHide].keyCode = SDLK_c;
m_SpecialKeyCombos[KeyComboToggleCursorHide].scanCode = SDL_SCANCODE_C;
m_SpecialKeyCombos[KeyComboToggleCursorHide].enabled = true;
m_SpecialKeyCombos[KeyComboToggleMinimize].keyCombo = KeyComboToggleMinimize;
m_SpecialKeyCombos[KeyComboToggleMinimize].keyCode = SDLK_d;
m_SpecialKeyCombos[KeyComboToggleMinimize].scanCode = SDL_SCANCODE_D;
m_SpecialKeyCombos[KeyComboToggleMinimize].enabled = QGuiApplication::platformName() != "eglfs";
m_SpecialKeyCombos[KeyComboPasteText].keyCombo = KeyComboPasteText;
m_SpecialKeyCombos[KeyComboPasteText].keyCode = SDLK_v;
m_SpecialKeyCombos[KeyComboPasteText].scanCode = SDL_SCANCODE_V;
m_SpecialKeyCombos[KeyComboPasteText].enabled = true;
m_OldIgnoreDevices = SDL_GetHint(SDL_HINT_GAMECONTROLLER_IGNORE_DEVICES); m_OldIgnoreDevices = SDL_GetHint(SDL_HINT_GAMECONTROLLER_IGNORE_DEVICES);
m_OldIgnoreDevicesExcept = SDL_GetHint(SDL_HINT_GAMECONTROLLER_IGNORE_DEVICES_EXCEPT); m_OldIgnoreDevicesExcept = SDL_GetHint(SDL_HINT_GAMECONTROLLER_IGNORE_DEVICES_EXCEPT);
@@ -123,6 +176,10 @@ SdlInputHandler::SdlInputHandler(StreamingPreferences& prefs, NvComputer*, int s
} }
#endif #endif
#ifdef Q_OS_DARWIN
CGSGetGlobalHotKeyOperatingMode(_CGSDefaultConnection(), &m_OldHotKeyMode);
#endif
// Initialize the gamepad mask with currently attached gamepads to avoid // Initialize the gamepad mask with currently attached gamepads to avoid
// causing gamepads to unexpectedly disappear and reappear on the host // causing gamepads to unexpectedly disappear and reappear on the host
// during stream startup as we detect currently attached gamepads one at a time. // during stream startup as we detect currently attached gamepads one at a time.
@@ -150,37 +207,16 @@ SdlInputHandler::SdlInputHandler(StreamingPreferences& prefs, NvComputer*, int s
m_MouseMoveTimer = SDL_AddTimer(pollingInterval, SdlInputHandler::mouseMoveTimerCallback, this); m_MouseMoveTimer = SDL_AddTimer(pollingInterval, SdlInputHandler::mouseMoveTimerCallback, this);
#ifdef Q_OS_WIN32 // Initialize state used by the clipboard thread before we start it
if (m_CaptureSystemKeysEnabled) { SDL_AtomicSet(&m_ShutdownClipboardThread, 0);
// If system key capture is enabled, install the window hook required to intercept m_ClipboardHasData = SDL_CreateCond();
// these key presses and block them from the OS itself. m_ClipboardLock = SDL_CreateMutex();
g_KeyboardHook = SetWindowsHookEx(WH_KEYBOARD_LL, SdlInputHandler::keyboardHookProc, GetModuleHandle(NULL), 0);
} m_ClipboardThread = SDL_CreateThread(SdlInputHandler::clipboardThreadProc, "Clipboard Sender", this);
#endif
} }
SdlInputHandler::~SdlInputHandler() SdlInputHandler::~SdlInputHandler()
{ {
#ifdef Q_OS_WIN32
if (g_KeyboardHook != nullptr) {
UnhookWindowsHookEx(g_KeyboardHook);
g_KeyboardHook = nullptr;
// If we're terminating because of the user pressing the quit combo,
// we won't have a chance to inform SDL as the modifier keys raise.
// This will leave stale modifier flags in the SDL_Keyboard global
// inside SDL which will show up on our next key event if the user
// streams again. Avoid this by explicitly zeroing mod state when
// ending the stream.
//
// This is only needed for the case where we're hooking the keyboard
// because we're generating synthetic SDL_KEYDOWN/SDL_KEYUP events
// which don't properly maintain SDL's internal keyboard state,
// so we're forced to invoke SDL_SetModState() ourselves to set mods.
SDL_SetModState(KMOD_NONE);
}
#endif
for (int i = 0; i < MAX_GAMEPADS; i++) { for (int i = 0; i < MAX_GAMEPADS; i++) {
if (m_GamepadState[i].mouseEmulationTimer != 0) { if (m_GamepadState[i].mouseEmulationTimer != 0) {
Session::get()->notifyMouseEmulationMode(false); Session::get()->notifyMouseEmulationMode(false);
@@ -202,6 +238,21 @@ SdlInputHandler::~SdlInputHandler()
SDL_RemoveTimer(m_RightButtonReleaseTimer); SDL_RemoveTimer(m_RightButtonReleaseTimer);
SDL_RemoveTimer(m_DragTimer); SDL_RemoveTimer(m_DragTimer);
// Wake up the clipboard thread to terminate it
SDL_AtomicSet(&m_ShutdownClipboardThread, 1);
SDL_CondBroadcast(m_ClipboardHasData);
// Wait for it to terminate
SDL_WaitThread(m_ClipboardThread, nullptr);
// Now we can safely clean up its resources
SDL_DestroyCond(m_ClipboardHasData);
SDL_DestroyMutex(m_ClipboardLock);
#ifdef Q_OS_DARWIN
CGSSetGlobalHotKeyOperatingMode(_CGSDefaultConnection(), m_OldHotKeyMode);
#endif
#if !SDL_VERSION_ATLEAST(2, 0, 9) #if !SDL_VERSION_ATLEAST(2, 0, 9)
SDL_QuitSubSystem(SDL_INIT_HAPTIC); SDL_QuitSubSystem(SDL_INIT_HAPTIC);
SDL_assert(!SDL_WasInit(SDL_INIT_HAPTIC)); SDL_assert(!SDL_WasInit(SDL_INIT_HAPTIC));
@@ -234,157 +285,6 @@ void SdlInputHandler::setWindow(SDL_Window *window)
m_Window = window; m_Window = window;
} }
#ifdef Q_OS_WIN32
LRESULT CALLBACK SdlInputHandler::keyboardHookProc(int nCode, WPARAM wParam, LPARAM lParam)
{
if (nCode < 0 || nCode != HC_ACTION) {
return CallNextHookEx(g_KeyboardHook, nCode, wParam, lParam);
}
bool keyCaptureActive;
SDL_Event event = {};
KBDLLHOOKSTRUCT* hookData = (KBDLLHOOKSTRUCT*)lParam;
switch (hookData->vkCode) {
case VK_LWIN:
event.key.keysym.sym = SDLK_LGUI;
event.key.keysym.scancode = SDL_SCANCODE_LGUI;
break;
case VK_RWIN:
event.key.keysym.sym = SDLK_RGUI;
event.key.keysym.scancode = SDL_SCANCODE_RGUI;
break;
case VK_LMENU:
event.key.keysym.sym = SDLK_LALT;
event.key.keysym.scancode = SDL_SCANCODE_LALT;
break;
case VK_RMENU:
event.key.keysym.sym = SDLK_RALT;
event.key.keysym.scancode = SDL_SCANCODE_RALT;
break;
case VK_LCONTROL:
event.key.keysym.sym = SDLK_LCTRL;
event.key.keysym.scancode = SDL_SCANCODE_LCTRL;
break;
case VK_RCONTROL:
event.key.keysym.sym = SDLK_RCTRL;
event.key.keysym.scancode = SDL_SCANCODE_RCTRL;
break;
default:
// Bail quickly if it's not a key we care about
goto NextHook;
}
// Make sure we're in a state where we actually want to steal this event (and it is safe to do so)
Session* session = Session::get();
if (session == nullptr || session->m_InputHandler == nullptr) {
goto NextHook;
}
keyCaptureActive = session->m_InputHandler->isSystemKeyCaptureActive();
// If this is a key we're going to intercept, create the synthetic SDL event.
// This is necessary because we are also hiding this key event from ourselves too.
if (wParam == WM_KEYDOWN || wParam == WM_SYSKEYDOWN) {
event.type = SDL_KEYDOWN;
event.key.state = SDL_PRESSED;
}
else {
event.type = SDL_KEYUP;
event.key.state = SDL_RELEASED;
}
// Drop the event if it's a key down and capture is not active.
// For key up, we'll need to do a little more work to determine if we need to send this
// event to SDL in order to keep its internal keyboard modifier state consistent.
if (event.type == SDL_KEYDOWN && !keyCaptureActive) {
goto NextHook;
}
event.key.keysym.mod = SDL_GetModState();
switch (hookData->vkCode) {
case VK_LWIN:
if (event.key.state == SDL_PRESSED) {
event.key.keysym.mod |= KMOD_LGUI;
}
else {
event.key.keysym.mod &= ~KMOD_LGUI;
}
break;
case VK_RWIN:
if (event.key.state == SDL_PRESSED) {
event.key.keysym.mod |= KMOD_RGUI;
}
else {
event.key.keysym.mod &= ~KMOD_RGUI;
}
break;
case VK_LMENU:
if (event.key.state == SDL_PRESSED) {
event.key.keysym.mod |= KMOD_LALT;
}
else {
event.key.keysym.mod &= ~KMOD_LALT;
}
break;
case VK_RMENU:
if (event.key.state == SDL_PRESSED) {
event.key.keysym.mod |= KMOD_RALT;
}
else {
event.key.keysym.mod &= ~KMOD_RALT;
}
break;
case VK_LCONTROL:
if (event.key.state == SDL_PRESSED) {
event.key.keysym.mod |= KMOD_LCTRL;
}
else {
event.key.keysym.mod &= ~KMOD_LCTRL;
}
break;
case VK_RCONTROL:
if (event.key.state == SDL_PRESSED) {
event.key.keysym.mod |= KMOD_RCTRL;
}
else {
event.key.keysym.mod &= ~KMOD_RCTRL;
}
break;
}
// If the modifier state is unchanged and we're not capturing system keys,
// drop the event. If the modifier state is changed, we need to send the
// event to update SDL's state even if system key capture is now inactive
// (due to focus loss, mouse capture toggled off, etc.) otherwise SDL won't
// know that the modifier key has been lifted.
if (event.key.keysym.mod == SDL_GetModState() && !keyCaptureActive) {
goto NextHook;
}
event.key.timestamp = SDL_GetTicks();
event.key.windowID = SDL_GetWindowID(session->m_Window);
// NOTE: event.key.repeat is not populated in this path!
SDL_PushEvent(&event);
// Synchronize SDL's modifier state with the current state.
// SDL won't do this on its own because it will never see the
// WM_KEYUP/WM_KEYDOWN events.
SDL_SetModState((SDL_Keymod)event.key.keysym.mod);
// Eat the event only if key capture is active.
// If capture is not active and we're just resyncing SDL's modifier state,
// we need to ensure the key event is still delivered normally to the
// window in focus.
if (keyCaptureActive) {
return 1;
}
NextHook:
return CallNextHookEx(g_KeyboardHook, nCode, wParam, lParam);
}
#endif
void SdlInputHandler::raiseAllKeys() void SdlInputHandler::raiseAllKeys()
{ {
if (m_KeysDown.isEmpty()) { if (m_KeysDown.isEmpty()) {
@@ -404,20 +304,22 @@ void SdlInputHandler::raiseAllKeys()
void SdlInputHandler::notifyMouseLeave() void SdlInputHandler::notifyMouseLeave()
{ {
#ifdef Q_OS_WIN32
// SDL on Windows doesn't send the mouse button up until the mouse re-enters the window // 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. // after leaving it. This breaks some of the Aero snap gestures, so we'll capture it to
// allow us to receive the mouse button up events later.
//
// On macOS and X11, capturing the mouse allows us to receive mouse motion outside the
// window (button up already worked without capture).
if (m_AbsoluteMouseMode && isCaptureActive()) { if (m_AbsoluteMouseMode && isCaptureActive()) {
// NB: Not using SDL_GetGlobalMouseState() because we want our state not the system's // NB: Not using SDL_GetGlobalMouseState() because we want our state not the system's
Uint32 mouseState = SDL_GetMouseState(nullptr, nullptr); Uint32 mouseState = SDL_GetMouseState(nullptr, nullptr);
for (Uint32 button = SDL_BUTTON_LEFT; button <= SDL_BUTTON_X2; button++) { for (Uint32 button = SDL_BUTTON_LEFT; button <= SDL_BUTTON_X2; button++) {
if (mouseState & SDL_BUTTON(button)) { if (mouseState & SDL_BUTTON(button)) {
m_PendingMouseLeaveButtonUp = button; SDL_CaptureMouse(SDL_TRUE);
break; break;
} }
} }
} }
#endif
} }
void SdlInputHandler::notifyFocusLost() void SdlInputHandler::notifyFocusLost()
@@ -430,11 +332,27 @@ void SdlInputHandler::notifyFocusLost()
setCaptureActive(false); setCaptureActive(false);
} }
#ifdef Q_OS_DARWIN
// Ungrab the keyboard
updateKeyboardGrabState();
#endif
// Raise all keys that are currently pressed. If we don't do this, certain keys // Raise all keys that are currently pressed. If we don't do this, certain keys
// used in shortcuts that cause focus loss (such as Alt+Tab) may get stuck down. // used in shortcuts that cause focus loss (such as Alt+Tab) may get stuck down.
raiseAllKeys(); raiseAllKeys();
} }
void SdlInputHandler::notifyFocusGained()
{
#ifdef Q_OS_DARWIN
// Re-grab the keyboard if it was grabbed before focus loss
// FIXME: We only do this on macOS because we get a spurious
// focus gain when in SDL_WINDOW_FULLSCREEN_DESKTOP on Windows
// immediately after losing focus by clicking on another window.
updateKeyboardGrabState();
#endif
}
bool SdlInputHandler::isCaptureActive() bool SdlInputHandler::isCaptureActive()
{ {
if (SDL_GetRelativeMouseMode()) { if (SDL_GetRelativeMouseMode()) {
@@ -445,9 +363,69 @@ bool SdlInputHandler::isCaptureActive()
return m_FakeCaptureActive; return m_FakeCaptureActive;
} }
void SdlInputHandler::updateKeyboardGrabState()
{
if (m_CaptureSystemKeysMode == StreamingPreferences::CSK_OFF) {
return;
}
bool shouldGrab = isCaptureActive();
Uint32 windowFlags = SDL_GetWindowFlags(m_Window);
if (m_CaptureSystemKeysMode == StreamingPreferences::CSK_FULLSCREEN &&
!(windowFlags & SDL_WINDOW_FULLSCREEN)) {
// Ungrab if it's fullscreen only and we left fullscreen
shouldGrab = false;
}
#ifdef Q_OS_DARWIN
else if (!(windowFlags & SDL_WINDOW_INPUT_FOCUS)) {
// Ungrab if we lose input focus on macOS. SDL will handle
// this internally for platforms where we use the SDL
// SDL_SetWindowKeyboardGrab() API exclusively.
//
// NB: On X11, we may not have input focus at the time of
// the initial keyboard grab update, so we must not enable
// this codepath on Linux.
shouldGrab = false;
}
#endif
// Don't close the window on Alt+F4 when keyboard grab is enabled
SDL_SetHint(SDL_HINT_WINDOWS_NO_CLOSE_ON_ALT_F4, shouldGrab ? "1" : "0");
if (shouldGrab) {
#if SDL_VERSION_ATLEAST(2, 0, 15)
// On SDL 2.0.15, we can get keyboard-only grab on Win32, X11, and Wayland.
// This does nothing on macOS but it sets the SDL_WINDOW_KEYBOARD_GRABBED flag
// that we look for to see if keyboard capture is enabled. We'll handle macOS
// ourselves below using the private CGSSetGlobalHotKeyOperatingMode() API.
SDL_SetWindowKeyboardGrab(m_Window, SDL_TRUE);
#else
// If we're in full-screen desktop mode and SDL doesn't have keyboard grab yet,
// grab the cursor (will grab the keyboard too on X11).
if (SDL_GetWindowFlags(m_Window) & SDL_WINDOW_FULLSCREEN) {
SDL_SetWindowGrab(m_Window, SDL_TRUE);
}
#endif
#ifdef Q_OS_DARWIN
// SDL doesn't support this private macOS API
CGSSetGlobalHotKeyOperatingMode(_CGSDefaultConnection(), CGSGlobalHotKeyDisable);
#endif
}
else {
#if SDL_VERSION_ATLEAST(2, 0, 15)
// Allow the keyboard to leave the window
SDL_SetWindowKeyboardGrab(m_Window, SDL_FALSE);
#endif
#ifdef Q_OS_DARWIN
// SDL doesn't support this private macOS API
CGSSetGlobalHotKeyOperatingMode(_CGSDefaultConnection(), m_OldHotKeyMode);
#endif
}
}
bool SdlInputHandler::isSystemKeyCaptureActive() bool SdlInputHandler::isSystemKeyCaptureActive()
{ {
if (!m_CaptureSystemKeysEnabled) { if (m_CaptureSystemKeysMode == StreamingPreferences::CSK_OFF) {
return false; return false;
} }
@@ -456,33 +434,66 @@ bool SdlInputHandler::isSystemKeyCaptureActive()
} }
Uint32 windowFlags = SDL_GetWindowFlags(m_Window); Uint32 windowFlags = SDL_GetWindowFlags(m_Window);
return (windowFlags & SDL_WINDOW_INPUT_FOCUS) && if (!(windowFlags & SDL_WINDOW_INPUT_FOCUS)
(windowFlags & SDL_WINDOW_INPUT_GRABBED) && #if SDL_VERSION_ATLEAST(2, 0, 15)
(windowFlags & SDL_WINDOW_FULLSCREEN_DESKTOP); || !(windowFlags & SDL_WINDOW_KEYBOARD_GRABBED)
#else
|| !(windowFlags & SDL_WINDOW_INPUT_GRABBED)
#endif
)
{
return false;
}
if (m_CaptureSystemKeysMode == StreamingPreferences::CSK_FULLSCREEN &&
!(windowFlags & SDL_WINDOW_FULLSCREEN)) {
return false;
}
return true;
} }
void SdlInputHandler::setCaptureActive(bool active) void SdlInputHandler::setCaptureActive(bool active)
{ {
if (active) { if (active) {
// If we're in full-screen exclusive mode, grab the cursor so it can't accidentally leave our window. #if defined(Q_OS_WIN32)
// If we're in full-screen desktop mode but system key capture is enabled, also grab the cursor (will grab the keyboard too on X11). // If our window is occluded when mouse is captured, the mouse may
if (((SDL_GetWindowFlags(m_Window) & SDL_WINDOW_FULLSCREEN_DESKTOP) != 0 && m_CaptureSystemKeysEnabled) || // get stuck on top of the occluding window and not be properly
(SDL_GetWindowFlags(m_Window) & SDL_WINDOW_FULLSCREEN_DESKTOP) == SDL_WINDOW_FULLSCREEN) { // captured. We can avoid this by raising our window before we
SDL_SetWindowGrab(m_Window, SDL_TRUE); // capture the mouse. This can also cause problems when keyboard
} // capture is enabled (modifiers will be eaten by our background window).
// We can't use SDL_RaiseWindow() because it doesn't let us know if
// SetForegroundWindow() failed. In the failure case, we must not
// enable capture to avoid trapping the mouse and/or keyboard.
{
SDL_SysWMinfo info;
if (!m_AbsoluteMouseMode) { SDL_VERSION(&info.version);
// If our window is occluded when mouse is captured, the mouse may SDL_GetWindowWMInfo(m_Window, &info);
// get stuck on top of the occluding window and not be properly
// captured. We can avoid this by raising our window before we SDL_assert(info.subsystem == SDL_SYSWM_WINDOWS);
// capture the mouse.
SDL_RaiseWindow(m_Window); if (!SetForegroundWindow(info.info.win.window)) {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Not enabling input capture - window is not foreground");
return;
}
}
#endif
// 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) {
#if SDL_VERSION_ATLEAST(2, 0, 15)
SDL_SetWindowMouseGrab(m_Window, SDL_TRUE);
#else
SDL_SetWindowGrab(m_Window, SDL_TRUE);
#endif
} }
// If we're in relative mode, try to activate SDL's relative mouse mode // If we're in relative mode, try to activate SDL's relative mouse mode
if (m_AbsoluteMouseMode || SDL_SetRelativeMouseMode(SDL_TRUE) < 0) { if (m_AbsoluteMouseMode || SDL_SetRelativeMouseMode(SDL_TRUE) < 0) {
// Relative mouse mode didn't work or was disabled, so we'll just hide the cursor // Relative mouse mode didn't work or was disabled, so we'll just hide the cursor
SDL_ShowCursor(SDL_DISABLE); SDL_ShowCursor(m_MouseCursorCapturedVisibilityState);
m_FakeCaptureActive = true; m_FakeCaptureActive = true;
} }
@@ -515,9 +526,17 @@ void SdlInputHandler::setCaptureActive(bool active)
SDL_SetRelativeMouseMode(SDL_FALSE); SDL_SetRelativeMouseMode(SDL_FALSE);
} }
#if SDL_VERSION_ATLEAST(2, 0, 15)
// Allow the cursor to leave the bounds of our window again.
SDL_SetWindowMouseGrab(m_Window, SDL_FALSE);
#else
// Allow the cursor to leave the bounds of our window again. // Allow the cursor to leave the bounds of our window again.
SDL_SetWindowGrab(m_Window, SDL_FALSE); SDL_SetWindowGrab(m_Window, SDL_FALSE);
#endif
} }
// Now update the keyboard grab
updateKeyboardGrabState();
} }
void SdlInputHandler::handleTouchFingerEvent(SDL_TouchFingerEvent* event) void SdlInputHandler::handleTouchFingerEvent(SDL_TouchFingerEvent* event)
+60 -10
View File
@@ -5,13 +5,9 @@
#include <SDL.h> #include <SDL.h>
#ifdef Q_OS_WIN32
#define WIN32_LEAN_AND_MEAN
#include <Windows.h>
#endif
#define SDL_CODE_HIDE_CURSOR 1 #define SDL_CODE_HIDE_CURSOR 1
#define SDL_CODE_SHOW_CURSOR 2 #define SDL_CODE_SHOW_CURSOR 2
#define SDL_CODE_UNCAPTURE_MOUSE 3
struct GamepadState { struct GamepadState {
SDL_GameController* controller; SDL_GameController* controller;
@@ -33,6 +29,25 @@ struct GamepadState {
unsigned char lt, rt; unsigned char lt, rt;
}; };
#ifdef Q_OS_DARWIN
#include <CoreGraphics/CGError.h>
extern "C" {
typedef int CGSConnection;
typedef enum {
CGSGlobalHotKeyEnable = 0,
CGSGlobalHotKeyDisable = 1,
} CGSGlobalHotKeyOperatingMode;
extern CGSConnection _CGSDefaultConnection(void);
extern CGError CGSGetGlobalHotKeyOperatingMode(CGSConnection connection,
CGSGlobalHotKeyOperatingMode* mode);
extern CGError CGSSetGlobalHotKeyOperatingMode(CGSConnection connection,
CGSGlobalHotKeyOperatingMode mode);
}
#endif
#define MAX_GAMEPADS 4 #define MAX_GAMEPADS 4
#define MAX_FINGERS 2 #define MAX_FINGERS 2
@@ -69,6 +84,8 @@ public:
void handleJoystickArrivalEvent(SDL_JoyDeviceEvent* event); void handleJoystickArrivalEvent(SDL_JoyDeviceEvent* event);
void sendText(QString& string);
void rumble(unsigned short controllerNumber, unsigned short lowFreqMotor, unsigned short highFreqMotor); void rumble(unsigned short controllerNumber, unsigned short lowFreqMotor, unsigned short highFreqMotor);
void handleTouchFingerEvent(SDL_TouchFingerEvent* event); void handleTouchFingerEvent(SDL_TouchFingerEvent* event);
@@ -81,6 +98,8 @@ public:
void notifyFocusLost(); void notifyFocusLost();
void notifyFocusGained();
bool isCaptureActive(); bool isCaptureActive();
bool isSystemKeyCaptureActive(); bool isSystemKeyCaptureActive();
@@ -93,10 +112,24 @@ public:
void flushMousePositionUpdate(); void flushMousePositionUpdate();
void updateKeyboardGrabState();
static static
QString getUnmappedGamepads(); QString getUnmappedGamepads();
private: private:
enum KeyCombo {
KeyComboQuit,
KeyComboUngrabInput,
KeyComboToggleFullScreen,
KeyComboToggleStatsOverlay,
KeyComboToggleMouseMode,
KeyComboToggleCursorHide,
KeyComboToggleMinimize,
KeyComboPasteText,
KeyComboMax
};
GamepadState* GamepadState*
findStateForGamepad(SDL_JoystickID id); findStateForGamepad(SDL_JoystickID id);
@@ -106,6 +139,8 @@ private:
void handleRelativeFingerEvent(SDL_TouchFingerEvent* event); void handleRelativeFingerEvent(SDL_TouchFingerEvent* event);
void performSpecialKeyCombo(KeyCombo combo);
static static
Uint32 longPressTimerCallback(Uint32 interval, void* param); Uint32 longPressTimerCallback(Uint32 interval, void* param);
@@ -124,10 +159,8 @@ private:
static static
Uint32 dragTimerCallback(Uint32 interval, void* param); Uint32 dragTimerCallback(Uint32 interval, void* param);
#ifdef Q_OS_WIN32
static static
LRESULT CALLBACK keyboardHookProc(int nCode, WPARAM wParam, LPARAM lParam); int clipboardThreadProc(void *ptr);
#endif
SDL_Window* m_Window; SDL_Window* m_Window;
bool m_MultiController; bool m_MultiController;
@@ -154,7 +187,19 @@ private:
bool m_FakeCaptureActive; bool m_FakeCaptureActive;
QString m_OldIgnoreDevices; QString m_OldIgnoreDevices;
QString m_OldIgnoreDevicesExcept; QString m_OldIgnoreDevicesExcept;
bool m_CaptureSystemKeysEnabled; StreamingPreferences::CaptureSysKeysMode m_CaptureSystemKeysMode;
int m_MouseCursorCapturedVisibilityState;
#ifdef Q_OS_DARWIN
CGSGlobalHotKeyOperatingMode m_OldHotKeyMode;
#endif
struct {
KeyCombo keyCombo;
SDL_Keycode keyCode;
SDL_Scancode scanCode;
bool enabled;
} m_SpecialKeyCombos[KeyComboMax];
SDL_TouchFingerEvent m_LastTouchDownEvent; SDL_TouchFingerEvent m_LastTouchDownEvent;
SDL_TouchFingerEvent m_LastTouchUpEvent; SDL_TouchFingerEvent m_LastTouchUpEvent;
@@ -163,7 +208,6 @@ private:
int m_StreamHeight; int m_StreamHeight;
bool m_AbsoluteMouseMode; bool m_AbsoluteMouseMode;
bool m_AbsoluteTouchMode; bool m_AbsoluteTouchMode;
Uint32 m_PendingMouseLeaveButtonUp;
SDL_TouchFingerEvent m_TouchDownEvent[MAX_FINGERS]; SDL_TouchFingerEvent m_TouchDownEvent[MAX_FINGERS];
SDL_TimerID m_LeftButtonReleaseTimer; SDL_TimerID m_LeftButtonReleaseTimer;
@@ -172,5 +216,11 @@ private:
char m_DragButton; char m_DragButton;
int m_NumFingersDown; int m_NumFingersDown;
SDL_Thread* m_ClipboardThread;
SDL_atomic_t m_ShutdownClipboardThread;
QString m_ClipboardData;
SDL_cond* m_ClipboardHasData;
SDL_mutex* m_ClipboardLock;
static const int k_ButtonMap[]; static const int k_ButtonMap[];
}; };
+142 -168
View File
@@ -3,8 +3,6 @@
#include <Limelight.h> #include <Limelight.h>
#include <SDL.h> #include <SDL.h>
#include <QGuiApplication>
#define VK_0 0x30 #define VK_0 0x30
#define VK_A 0x41 #define VK_A 0x41
@@ -15,13 +13,134 @@
#define VK_NUMPAD0 0x60 #define VK_NUMPAD0 0x60
#endif #endif
void SdlInputHandler::performSpecialKeyCombo(KeyCombo combo)
{
switch (combo) {
case KeyComboQuit:
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Detected quit key combo");
// Push a quit event to the main loop
SDL_Event event;
event.type = SDL_QUIT;
event.quit.timestamp = SDL_GetTicks();
SDL_PushEvent(&event);
break;
case KeyComboUngrabInput:
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Detected mouse capture toggle combo");
// 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();
break;
case KeyComboToggleFullScreen:
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Detected full-screen toggle combo");
Session::s_ActiveSession->toggleFullscreen();
// Force raise all keys just be safe across this full-screen/windowed
// transition just in case key events get lost.
raiseAllKeys();
break;
case KeyComboToggleStatsOverlay:
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Detected stats toggle combo");
// Toggle the stats overlay
Session::get()->getOverlayManager().setOverlayState(Overlay::OverlayDebug,
!Session::get()->getOverlayManager().isOverlayEnabled(Overlay::OverlayDebug));
break;
case KeyComboToggleMouseMode:
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Detected mouse mode toggle combo");
// Uncapture input
setCaptureActive(false);
// Toggle mouse mode
m_AbsoluteMouseMode = !m_AbsoluteMouseMode;
// Recapture input
setCaptureActive(true);
break;
case KeyComboToggleCursorHide:
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Detected show mouse combo");
if (!SDL_GetRelativeMouseMode()) {
m_MouseCursorCapturedVisibilityState = !m_MouseCursorCapturedVisibilityState;
SDL_ShowCursor(m_MouseCursorCapturedVisibilityState);
}
else {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"Cursor can only be shown in remote desktop mouse mode");
}
break;
case KeyComboToggleMinimize:
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Detected minimize combo");
SDL_MinimizeWindow(m_Window);
break;
case KeyComboPasteText:
{
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Detected type clipboard text combo");
// Force raise all keys to ensure that none of them interfere
// with the text we're going to type.
raiseAllKeys();
const char* text;
if (SDL_HasClipboardText() && (text = SDL_GetClipboardText()) != nullptr) {
// Append this data to the clipboard data string for the thread to process
SDL_LockMutex(m_ClipboardLock);
m_ClipboardData.append(text);
SDL_CondSignal(m_ClipboardHasData);
SDL_UnlockMutex(m_ClipboardLock);
// SDL_GetClipboardText() allocates, so we must free
SDL_free((void*)text);
}
else {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"No text in clipboard to paste!");
}
break;
}
default:
Q_UNREACHABLE();
}
}
#define IS_SYNTHETIC_KEY_EVENT(x) ((x)->timestamp == 0)
void SdlInputHandler::handleKeyEvent(SDL_KeyboardEvent* event) void SdlInputHandler::handleKeyEvent(SDL_KeyboardEvent* event)
{ {
short keyCode; short keyCode;
char modifiers; char modifiers;
if (event->repeat) {
// Ignore repeat key down events
SDL_assert(event->state == SDL_PRESSED);
return;
}
// Check for our special key combos // Check for our special key combos
if ((event->state == SDL_PRESSED) && // Ignore timestamp == 0 which are sent from our keyboard code.
if (!IS_SYNTHETIC_KEY_EVENT(event) &&
(event->state == SDL_PRESSED) &&
(event->keysym.mod & KMOD_CTRL) && (event->keysym.mod & KMOD_CTRL) &&
(event->keysym.mod & KMOD_ALT) && (event->keysym.mod & KMOD_ALT) &&
(event->keysym.mod & KMOD_SHIFT)) { (event->keysym.mod & KMOD_SHIFT)) {
@@ -36,168 +155,19 @@ void SdlInputHandler::handleKeyEvent(SDL_KeyboardEvent* event)
// where the SDLK for one shortcut collides with // where the SDLK for one shortcut collides with
// the scancode of another. // the scancode of another.
// Check for quit combo (Ctrl+Alt+Shift+Q) for (int i = 0; i < KeyComboMax; i++) {
if (event->keysym.sym == SDLK_q) { if (m_SpecialKeyCombos[i].enabled && event->keysym.sym == m_SpecialKeyCombos[i].keyCode) {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, performSpecialKeyCombo(m_SpecialKeyCombos[i].keyCombo);
"Detected quit key combo (SDLK)"); return;
// 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 mouse show combo (Ctrl+Alt+Shift+C)
else if (event->keysym.sym == SDLK_c) {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Detected show mouse combo (SDLK)");
if (!SDL_GetRelativeMouseMode()) {
SDL_ShowCursor(!SDL_ShowCursor(SDL_QUERY));
} }
else { }
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"Cursor can only be shown in remote desktop mouse mode"); for (int i = 0; i < KeyComboMax; i++) {
if (m_SpecialKeyCombos[i].enabled && event->keysym.scancode == m_SpecialKeyCombos[i].scanCode) {
performSpecialKeyCombo(m_SpecialKeyCombos[i].keyCombo);
return;
} }
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;
}
// Check for mouse show combo (Ctrl+Alt+Shift+C)
else if (event->keysym.sym == SDL_SCANCODE_C) {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Detected show mouse combo (scancode)");
if (!SDL_GetRelativeMouseMode()) {
SDL_ShowCursor(!SDL_ShowCursor(SDL_QUERY));
}
else {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"Cursor can only be shown in remote desktop mouse mode");
}
return;
}
}
if (event->repeat) {
// Ignore repeat key down events
SDL_assert(event->state == SDL_PRESSED);
return;
} }
// Set modifier flags // Set modifier flags
@@ -433,12 +403,16 @@ void SdlInputHandler::handleKeyEvent(SDL_KeyboardEvent* event)
} }
} }
// Track the key state so we always know which keys are down // If this is a synthetic keypress from the clipboard code,
if (event->state == SDL_PRESSED) { // this will be on a non-main thread, so don't touch m_KeysDown.
m_KeysDown.insert(keyCode); if (!IS_SYNTHETIC_KEY_EVENT(event)) {
} // Track the key state so we always know which keys are down
else { if (event->state == SDL_PRESSED) {
m_KeysDown.remove(keyCode); m_KeysDown.insert(keyCode);
}
else {
m_KeysDown.remove(keyCode);
}
} }
LiSendKeyboardEvent(0x8000 | keyCode, LiSendKeyboardEvent(0x8000 | keyCode,
+11 -30
View File
@@ -133,7 +133,15 @@ void SdlInputHandler::flushMousePositionUpdate()
// c) a mouse button is still down from before the cursor left the video region (to allow smooth dragging) // c) a mouse button is still down from before the cursor left the video region (to allow smooth dragging)
Uint32 buttonState = SDL_GetMouseState(nullptr, nullptr); Uint32 buttonState = SDL_GetMouseState(nullptr, nullptr);
if (buttonState == 0) { if (buttonState == 0) {
m_PendingMouseButtonsAllUpOnVideoRegionLeave = false; if (m_PendingMouseButtonsAllUpOnVideoRegionLeave) {
// Tell the main thread to stop capturing the mouse now
SDL_Event event;
event.type = SDL_USEREVENT;
event.user.code = SDL_CODE_UNCAPTURE_MOUSE;
SDL_PushEvent(&event);
m_PendingMouseButtonsAllUpOnVideoRegionLeave = false;
}
} }
if (mouseInVideoRegion || m_MouseWasInVideoRegion || m_PendingMouseButtonsAllUpOnVideoRegionLeave) { if (mouseInVideoRegion || m_MouseWasInVideoRegion || m_PendingMouseButtonsAllUpOnVideoRegionLeave) {
LiSendMousePositionEvent(x, y, dst.w, dst.h); LiSendMousePositionEvent(x, y, dst.w, dst.h);
@@ -146,7 +154,8 @@ void SdlInputHandler::flushMousePositionUpdate()
// executes. // executes.
SDL_Event event; SDL_Event event;
event.type = SDL_USEREVENT; event.type = SDL_USEREVENT;
event.user.code = mouseInVideoRegion ? SDL_CODE_HIDE_CURSOR : SDL_CODE_SHOW_CURSOR; event.user.code = (mouseInVideoRegion && m_MouseCursorCapturedVisibilityState == SDL_DISABLE) ?
SDL_CODE_HIDE_CURSOR : SDL_CODE_SHOW_CURSOR;
SDL_PushEvent(&event); SDL_PushEvent(&event);
if (!mouseInVideoRegion && buttonState != 0) { if (!mouseInVideoRegion && buttonState != 0) {
@@ -250,33 +259,5 @@ Uint32 SdlInputHandler::mouseMoveTimerCallback(Uint32 interval, void *param)
// Send mouse position updates if applicable // Send mouse position updates if applicable
me->flushMousePositionUpdate(); me->flushMousePositionUpdate();
#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; return interval;
} }
+109 -62
View File
@@ -37,14 +37,6 @@
#define CONN_TEST_SERVER "qt.conntest.moonlight-stream.org" #define CONN_TEST_SERVER "qt.conntest.moonlight-stream.org"
// Running the connection process asynchronously seems to reliably
// cause a crash in QSGRenderThread on Wayland and strange crashes
// elsewhere. Until these are figured out, avoid the async connect
// thread on Linux and BSDs.
#if !defined(Q_OS_UNIX) || defined(Q_OS_DARWIN)
#define USE_ASYNC_CONNECT_THREAD 1
#endif
CONNECTION_LISTENER_CALLBACKS Session::k_ConnCallbacks = { CONNECTION_LISTENER_CALLBACKS Session::k_ConnCallbacks = {
Session::clStageStarting, Session::clStageStarting,
nullptr, nullptr,
@@ -65,11 +57,6 @@ void Session::clStageStarting(int stage)
// which happens to be the main thread, so it's cool to interact // which happens to be the main thread, so it's cool to interact
// with the GUI in these callbacks. // with the GUI in these callbacks.
emit s_ActiveSession->stageStarting(QString::fromLocal8Bit(LiGetStageName(stage))); emit s_ActiveSession->stageStarting(QString::fromLocal8Bit(LiGetStageName(stage)));
#ifndef USE_ASYNC_CONNECT_THREAD
QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents);
QCoreApplication::sendPostedEvents();
#endif
} }
void Session::clStageFailed(int stage, int errorCode) void Session::clStageFailed(int stage, int errorCode)
@@ -81,11 +68,6 @@ void Session::clStageFailed(int stage, int errorCode)
char failingPorts[128]; char failingPorts[128];
LiStringifyPortFlags(portFlags, ", ", failingPorts, sizeof(failingPorts)); LiStringifyPortFlags(portFlags, ", ", failingPorts, sizeof(failingPorts));
emit s_ActiveSession->stageFailed(QString::fromLocal8Bit(LiGetStageName(stage)), errorCode, QString(failingPorts)); emit s_ActiveSession->stageFailed(QString::fromLocal8Bit(LiGetStageName(stage)), errorCode, QString(failingPorts));
#ifndef USE_ASYNC_CONNECT_THREAD
QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents);
QCoreApplication::sendPostedEvents();
#endif
} }
void Session::clConnectionTerminated(int errorCode) void Session::clConnectionTerminated(int errorCode)
@@ -199,6 +181,11 @@ bool Session::chooseDecoder(StreamingPreferences::VideoDecoderSelection vds,
{ {
DECODER_PARAMETERS params; DECODER_PARAMETERS params;
// We should never have vsync enabled for test-mode.
// It introduces unnecessary delay for renderers that may
// block while waiting for a backbuffer swap.
SDL_assert(!enableVsync || !testOnly);
params.width = width; params.width = width;
params.height = height; params.height = height;
params.frameRate = frameRate; params.frameRate = frameRate;
@@ -310,7 +297,7 @@ void Session::getDecoderInfo(SDL_Window* window,
if (!chooseDecoder(StreamingPreferences::VDS_AUTO, if (!chooseDecoder(StreamingPreferences::VDS_AUTO,
window, VIDEO_FORMAT_H264, 1920, 1080, 60, window, VIDEO_FORMAT_H264, 1920, 1080, 60,
true, false, true, decoder)) { false, false, true, decoder)) {
isHardwareAccelerated = isFullScreenOnly = false; isHardwareAccelerated = isFullScreenOnly = false;
return; return;
} }
@@ -328,7 +315,7 @@ bool Session::isHardwareDecodeAvailable(SDL_Window* window,
{ {
IVideoDecoder* decoder; IVideoDecoder* decoder;
if (!chooseDecoder(vds, window, videoFormat, width, height, frameRate, true, false, true, decoder)) { if (!chooseDecoder(vds, window, videoFormat, width, height, frameRate, false, false, true, decoder)) {
return false; return false;
} }
@@ -350,7 +337,7 @@ bool Session::populateDecoderProperties(SDL_Window* window)
m_StreamConfig.width, m_StreamConfig.width,
m_StreamConfig.height, m_StreamConfig.height,
m_StreamConfig.fps, m_StreamConfig.fps,
true, false, true, decoder)) { false, false, true, decoder)) {
return false; return false;
} }
@@ -358,6 +345,10 @@ bool Session::populateDecoderProperties(SDL_Window* window)
m_StreamConfig.colorSpace = decoder->getDecoderColorspace(); m_StreamConfig.colorSpace = decoder->getDecoderColorspace();
if (decoder->isAlwaysFullScreen()) {
m_IsFullScreen = true;
}
delete decoder; delete decoder;
return true; return true;
@@ -365,6 +356,7 @@ bool Session::populateDecoderProperties(SDL_Window* window)
Session::Session(NvComputer* computer, NvApp& app, StreamingPreferences *preferences) Session::Session(NvComputer* computer, NvApp& app, StreamingPreferences *preferences)
: m_Preferences(preferences ? preferences : new StreamingPreferences(this)), : m_Preferences(preferences ? preferences : new StreamingPreferences(this)),
m_IsFullScreen(m_Preferences->windowMode != StreamingPreferences::WM_WINDOWED || !WMUtils::isRunningDesktopEnvironment()),
m_Computer(computer), m_Computer(computer),
m_App(app), m_App(app),
m_Window(nullptr), m_Window(nullptr),
@@ -441,6 +433,12 @@ bool Session::initialize()
m_StreamConfig.bitrate = m_Preferences->bitrateKbps; m_StreamConfig.bitrate = m_Preferences->bitrateKbps;
m_StreamConfig.hevcBitratePercentageMultiplier = 75; m_StreamConfig.hevcBitratePercentageMultiplier = 75;
#ifndef STEAM_LINK
// Enable audio encryption as long as we're not on Steam Link.
// That hardware can hardly handle Opus decoding at all.
m_StreamConfig.encryptionFlags = ENCFLG_AUDIO;
#endif
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Video bitrate: %d kbps", "Video bitrate: %d kbps",
m_StreamConfig.bitrate); m_StreamConfig.bitrate);
@@ -524,22 +522,17 @@ bool Session::initialize()
{ {
default: default:
case StreamingPreferences::WM_FULLSCREEN_DESKTOP: case StreamingPreferences::WM_FULLSCREEN_DESKTOP:
m_FullScreenFlag = SDL_WINDOW_FULLSCREEN_DESKTOP; // Only use full-screen desktop mode if we're running a desktop environment
break; if (WMUtils::isRunningDesktopEnvironment()) {
m_FullScreenFlag = SDL_WINDOW_FULLSCREEN_DESKTOP;
break;
}
// Fall-through
case StreamingPreferences::WM_FULLSCREEN: case StreamingPreferences::WM_FULLSCREEN:
m_FullScreenFlag = SDL_WINDOW_FULLSCREEN; m_FullScreenFlag = SDL_WINDOW_FULLSCREEN;
break; break;
} }
#ifdef Q_OS_DARWIN
// macOS behaves as if we're capturing system keys when
// we enter "real" full-screen mode, so use standard
// full-screen as our full-screen flag when capturing keys.
if (m_Preferences->captureSysKeys) {
m_FullScreenFlag = SDL_WINDOW_FULLSCREEN;
}
#endif
#if !SDL_VERSION_ATLEAST(2, 0, 11) #if !SDL_VERSION_ATLEAST(2, 0, 11)
// HACK: Using a full-screen window breaks mouse capture on the Pi's LXDE // 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 // GUI environment. Force the session to use windowed mode (which won't
@@ -581,10 +574,13 @@ void Session::emitLaunchWarning(QString text)
// to allow it to transition off the screen before continuing. // to allow it to transition off the screen before continuing.
uint32_t start = SDL_GetTicks(); uint32_t start = SDL_GetTicks();
while (!SDL_TICKS_PASSED(SDL_GetTicks(), start + 3500)) { while (!SDL_TICKS_PASSED(SDL_GetTicks(), start + 3500)) {
// Pump the UI loop while we wait
SDL_Delay(5); SDL_Delay(5);
QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents);
QCoreApplication::sendPostedEvents(); if (!m_ThreadedExec) {
// Pump the UI loop while we wait if we're on the main thread
QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents);
QCoreApplication::sendPostedEvents();
}
} }
} }
@@ -592,6 +588,11 @@ bool Session::validateLaunch(SDL_Window* testWindow)
{ {
QStringList warningList; QStringList warningList;
if (!m_Computer->isSupportedServerVersion) {
emit displayLaunchError(tr("The version of GeForce Experience on %1 is not supported by this build of Moonlight. You must update Moonlight to stream from %1.").arg(m_Computer->name));
return false;
}
if (m_Preferences->absoluteMouseMode && !m_App.isAppCollectorGame) { if (m_Preferences->absoluteMouseMode && !m_App.isAppCollectorGame) {
emitLaunchWarning(tr("Your selection to enable remote desktop mouse mode may cause problems in games.")); emitLaunchWarning(tr("Your selection to enable remote desktop mouse mode may cause problems in games."));
} }
@@ -835,7 +836,7 @@ void Session::getWindowDimensions(int& x, int& y,
} }
} }
fullScreen = (m_Preferences->windowMode != StreamingPreferences::WM_WINDOWED); fullScreen = m_IsFullScreen;
} }
SDL_Rect usableBounds; SDL_Rect usableBounds;
@@ -944,10 +945,8 @@ void Session::toggleFullscreen()
bool fullScreen = !(SDL_GetWindowFlags(m_Window) & m_FullScreenFlag); bool fullScreen = !(SDL_GetWindowFlags(m_Window) & m_FullScreenFlag);
if (fullScreen) { if (fullScreen) {
if ((m_FullScreenFlag == SDL_WINDOW_FULLSCREEN || m_Preferences->captureSysKeys) && m_InputHandler->isCaptureActive()) { if (m_FullScreenFlag == SDL_WINDOW_FULLSCREEN && m_InputHandler->isCaptureActive()) {
// Confine the cursor to the window if we're capturing input while transitioning to full screen. // Confine the cursor to the window if we're capturing input while transitioning to full screen.
// We also need to grab if we're capturing system keys, because SDL requires window grab to
// capture the keyboard on X11.
SDL_SetWindowGrab(m_Window, SDL_TRUE); SDL_SetWindowGrab(m_Window, SDL_TRUE);
} }
@@ -964,6 +963,9 @@ void Session::toggleFullscreen()
// Reposition the window when the resize is complete // Reposition the window when the resize is complete
m_PendingWindowedTransition = true; m_PendingWindowedTransition = true;
} }
// Input handler might need to start/stop keyboard grab after changing modes
m_InputHandler->updateKeyboardGrabState();
} }
void Session::notifyMouseEmulationMode(bool enabled) void Session::notifyMouseEmulationMode(bool enabled)
@@ -1002,17 +1004,7 @@ bool Session::startConnectionAsync()
{ {
// Wait 1.5 seconds before connecting to let the user // Wait 1.5 seconds before connecting to let the user
// have time to read any messages present on the segue // have time to read any messages present on the segue
#ifdef USE_ASYNC_CONNECT_THREAD
SDL_Delay(1500); SDL_Delay(1500);
#else
uint32_t start = SDL_GetTicks();
while (!SDL_TICKS_PASSED(SDL_GetTicks(), start + 1500)) {
// Pump the UI loop while we wait since we're not async
SDL_Delay(5);
QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents);
QCoreApplication::sendPostedEvents();
}
#endif
// The UI should have ensured the old game was already quit // The UI should have ensured the old game was already quit
// if we decide to stream a different game. // if we decide to stream a different game.
@@ -1124,11 +1116,58 @@ void Session::flushWindowEvents()
SDL_PushEvent(&flushEvent); SDL_PushEvent(&flushEvent);
} }
class ExecThread : public QThread
{
public:
ExecThread(Session* session) :
QThread(nullptr),
m_Session(session) {}
void run() override
{
m_Session->execInternal();
}
Session* m_Session;
};
void Session::exec(int displayOriginX, int displayOriginY) void Session::exec(int displayOriginX, int displayOriginY)
{ {
m_DisplayOriginX = displayOriginX; m_DisplayOriginX = displayOriginX;
m_DisplayOriginY = displayOriginY; m_DisplayOriginY = displayOriginY;
// Use a separate thread for the streaming session on X11 or Wayland
// to ensure we don't stomp on Qt's GL context. This breaks when using
// the Qt EGLFS backend, so we will restrict this to X11
m_ThreadedExec = WMUtils::isRunningX11() || WMUtils::isRunningWayland();
if (m_ThreadedExec) {
// Run the streaming session on a separate thread for Linux/BSD
ExecThread execThread(this);
execThread.start();
// Until the SDL streaming window is created, we should continue
// to update the Qt UI to allow warning messages to display and
// make sure that the Qt window can hide itself.
while (!execThread.wait(10) && m_Window == nullptr) {
QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents);
QCoreApplication::sendPostedEvents();
}
QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents);
QCoreApplication::sendPostedEvents();
// SDL is in charge now. Wait until the streaming thread exits
// to further update the Qt window.
execThread.wait();
}
else {
// Run the streaming session on the main thread for Windows and macOS
execInternal();
}
}
void Session::execInternal()
{
// Complete initialization in this deferred context to avoid // Complete initialization in this deferred context to avoid
// calling expensive functions in the constructor (during the // calling expensive functions in the constructor (during the
// process of loading the StreamSegue). // process of loading the StreamSegue).
@@ -1152,22 +1191,26 @@ void Session::exec(int displayOriginX, int displayOriginY)
m_StreamConfig.width, m_StreamConfig.width,
m_StreamConfig.height); m_StreamConfig.height);
// Kick off the async connection thread while we sit here and pump the event loop
AsyncConnectionStartThread asyncConnThread(this); AsyncConnectionStartThread asyncConnThread(this);
#ifdef USE_ASYNC_CONNECT_THREAD if (!m_ThreadedExec) {
asyncConnThread.start(); // Kick off the async connection thread while we sit here and pump the event loop
while (!asyncConnThread.wait(10)) { asyncConnThread.start();
while (!asyncConnThread.wait(10)) {
QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents);
QCoreApplication::sendPostedEvents();
}
// Pump the event loop one last time to ensure we pick up any events from
// the thread that happened while it was in the final successful QThread::wait().
QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents); QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents);
QCoreApplication::sendPostedEvents(); QCoreApplication::sendPostedEvents();
} }
#else else {
asyncConnThread.run(); // We're already in a separate thread so run the connection operations
#endif // synchronously and don't pump the event loop. The main thread is already
// pumping the event loop for us.
// Pump the event loop one last time to ensure we pick up any events from asyncConnThread.run();
// the thread that happened while it was in the final successful QThread::wait(). }
QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents);
QCoreApplication::sendPostedEvents();
// If the connection failed, clean up and abort the connection. // If the connection failed, clean up and abort the connection.
if (!m_AsyncConnectionSuccess) { if (!m_AsyncConnectionSuccess) {
@@ -1244,7 +1287,7 @@ void Session::exec(int displayOriginX, int displayOriginY)
// For non-full screen windows, call getWindowDimensions() // For non-full screen windows, call getWindowDimensions()
// again after creating a window to allow it to account // again after creating a window to allow it to account
// for window chrome size. // for window chrome size.
if (m_Preferences->windowMode == StreamingPreferences::WM_WINDOWED) { if (!m_IsFullScreen) {
getWindowDimensions(x, y, width, height); getWindowDimensions(x, y, width, height);
// We must set the size before the position because centering // We must set the size before the position because centering
@@ -1344,6 +1387,9 @@ void Session::exec(int displayOriginX, int displayOriginY)
case SDL_CODE_SHOW_CURSOR: case SDL_CODE_SHOW_CURSOR:
SDL_ShowCursor(SDL_ENABLE); SDL_ShowCursor(SDL_ENABLE);
break; break;
case SDL_CODE_UNCAPTURE_MOUSE:
SDL_CaptureMouse(SDL_FALSE);
break;
case SDL_CODE_FLUSH_WINDOW_EVENT_BARRIER: case SDL_CODE_FLUSH_WINDOW_EVENT_BARRIER:
m_FlushingWindowEventsRef--; m_FlushingWindowEventsRef--;
break; break;
@@ -1365,6 +1411,7 @@ void Session::exec(int displayOriginX, int displayOriginY)
if (m_Preferences->muteOnFocusLoss) { if (m_Preferences->muteOnFocusLoss) {
m_AudioMuted = false; m_AudioMuted = false;
} }
m_InputHandler->notifyFocusGained();
break; break;
case SDL_WINDOWEVENT_LEAVE: case SDL_WINDOWEVENT_LEAVE:
m_InputHandler->notifyMouseLeave(); m_InputHandler->notifyMouseLeave();
+5
View File
@@ -17,6 +17,7 @@ class Session : public QObject
friend class SdlInputHandler; friend class SdlInputHandler;
friend class DeferredSessionCleanupTask; friend class DeferredSessionCleanupTask;
friend class AsyncConnectionStartThread; friend class AsyncConnectionStartThread;
friend class ExecThread;
public: public:
explicit Session(NvComputer* computer, NvApp& app, StreamingPreferences *preferences = nullptr); explicit Session(NvComputer* computer, NvApp& app, StreamingPreferences *preferences = nullptr);
@@ -57,6 +58,8 @@ signals:
void sessionFinished(int portTestResult); void sessionFinished(int portTestResult);
private: private:
void execInternal();
bool initialize(); bool initialize();
bool startConnectionAsync(); bool startConnectionAsync();
@@ -133,6 +136,7 @@ private:
int drSubmitDecodeUnit(PDECODE_UNIT du); int drSubmitDecodeUnit(PDECODE_UNIT du);
StreamingPreferences* m_Preferences; StreamingPreferences* m_Preferences;
bool m_IsFullScreen;
STREAM_CONFIGURATION m_StreamConfig; STREAM_CONFIGURATION m_StreamConfig;
DECODER_RENDERER_CALLBACKS m_VideoCallbacks; DECODER_RENDERER_CALLBACKS m_VideoCallbacks;
AUDIO_RENDERER_CALLBACKS m_AudioCallbacks; AUDIO_RENDERER_CALLBACKS m_AudioCallbacks;
@@ -147,6 +151,7 @@ private:
Uint32 m_FullScreenFlag; Uint32 m_FullScreenFlag;
int m_DisplayOriginX; int m_DisplayOriginX;
int m_DisplayOriginY; int m_DisplayOriginY;
bool m_ThreadedExec;
bool m_PendingWindowedTransition; bool m_PendingWindowedTransition;
bool m_UnexpectedTermination; bool m_UnexpectedTermination;
SdlInputHandler* m_InputHandler; SdlInputHandler* m_InputHandler;
+2
View File
@@ -19,6 +19,8 @@ typedef struct _VIDEO_STATS {
uint32_t totalDecodeTime; uint32_t totalDecodeTime;
uint32_t totalPacerTime; uint32_t totalPacerTime;
uint32_t totalRenderTime; uint32_t totalRenderTime;
uint32_t lastRtt;
uint32_t lastRttVariance;
float totalFps; float totalFps;
float receivedFps; float receivedFps;
float decodedFps; float decodedFps;
+320 -27
View File
@@ -14,12 +14,24 @@ extern "C" {
#include <Limelight.h> #include <Limelight.h>
#include <SDL_syswm.h>
DrmRenderer::DrmRenderer() DrmRenderer::DrmRenderer()
: m_DrmFd(-1), : m_HwContext(nullptr),
m_DrmFd(-1),
m_SdlOwnsDrmFd(false),
m_SupportsDirectRendering(false),
m_CrtcId(0), m_CrtcId(0),
m_PlaneId(0), m_PlaneId(0),
m_CurrentFbId(0) m_CurrentFbId(0)
{ {
#ifdef HAVE_EGL
m_EGLExtDmaBuf = false;
m_eglCreateImage = nullptr;
m_eglCreateImageKHR = nullptr;
m_eglDestroyImage = nullptr;
m_eglDestroyImageKHR = nullptr;
#endif
} }
DrmRenderer::~DrmRenderer() DrmRenderer::~DrmRenderer()
@@ -28,14 +40,22 @@ DrmRenderer::~DrmRenderer()
drmModeRmFB(m_DrmFd, m_CurrentFbId); drmModeRmFB(m_DrmFd, m_CurrentFbId);
} }
if (m_DrmFd != -1) { if (m_HwContext != nullptr) {
av_buffer_unref(&m_HwContext);
}
if (!m_SdlOwnsDrmFd && m_DrmFd != -1) {
close(m_DrmFd); close(m_DrmFd);
} }
} }
bool DrmRenderer::prepareDecoderContext(AVCodecContext*, AVDictionary**) bool DrmRenderer::prepareDecoderContext(AVCodecContext* context, AVDictionary** options)
{ {
/* Nothing to do */ // The out-of-tree LibreELEC patches use this option to control the type of the V4L2
// buffers that we get back. We only support NV12 buffers now.
av_dict_set_int(options, "pixel_format", AV_PIX_FMT_NV12, 0);
context->hw_device_ctx = av_buffer_ref(m_HwContext);
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Using DRM renderer"); "Using DRM renderer");
@@ -43,33 +63,89 @@ bool DrmRenderer::prepareDecoderContext(AVCodecContext*, AVDictionary**)
return true; return true;
} }
bool DrmRenderer::initialize(PDECODER_PARAMETERS) bool DrmRenderer::initialize(PDECODER_PARAMETERS params)
{ {
const char* device = SDL_getenv("DRM_DEV");
int i; int i;
if (device == nullptr) { #if SDL_VERSION_ATLEAST(2, 0, 15)
device = "/dev/dri/card0"; SDL_SysWMinfo info;
}
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, SDL_VERSION(&info.version);
"Opening DRM device: %s",
device);
m_DrmFd = open(device, O_RDWR | O_CLOEXEC); if (!SDL_GetWindowWMInfo(params->window, &info)) {
if (m_DrmFd < 0) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Failed to open DRM device: %d", "SDL_GetWindowWMInfo() failed: %s",
errno); SDL_GetError());
return false; return false;
} }
if (info.subsystem == SDL_SYSWM_KMSDRM) {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Sharing DRM FD with SDL");
SDL_assert(info.info.kmsdrm.drm_fd >= 0);
m_DrmFd = info.info.kmsdrm.drm_fd;
m_SdlOwnsDrmFd = true;
}
else
#endif
{
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 false;
}
}
// Create the device context first because it is needed whether we can
// actually use direct rendering or not.
m_HwContext = av_hwdevice_ctx_alloc(AV_HWDEVICE_TYPE_DRM);
if (m_HwContext == nullptr) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"av_hwdevice_ctx_alloc(DRM) failed");
return false;
}
AVHWDeviceContext* deviceContext = (AVHWDeviceContext*)m_HwContext->data;
AVDRMDeviceContext* drmDeviceContext = (AVDRMDeviceContext*)deviceContext->hwctx;
drmDeviceContext->fd = m_DrmFd;
int err = av_hwdevice_ctx_init(m_HwContext);
if (err < 0) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"av_hwdevice_ctx_init(DRM) failed: %d",
err);
return false;
}
#ifdef HAVE_EGL
// Still return true if we fail to initialize DRM direct rendering
// stuff, since we have EGL that we can use for indirect rendering.
const bool DIRECT_RENDERING_INIT_FAILED = true;
#else
// Fail if we can't initialize direct rendering and we don't have EGL.
const bool DIRECT_RENDERING_INIT_FAILED = false;
#endif
drmModeRes* resources = drmModeGetResources(m_DrmFd); drmModeRes* resources = drmModeGetResources(m_DrmFd);
if (resources == nullptr) { if (resources == nullptr) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"drmModeGetResources() failed: %d", "drmModeGetResources() failed: %d",
errno); errno);
return false; return DIRECT_RENDERING_INIT_FAILED;
} }
// Look for a connected connector and get the associated encoder // Look for a connected connector and get the associated encoder
@@ -89,7 +165,7 @@ bool DrmRenderer::initialize(PDECODER_PARAMETERS)
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"No connected displays found!"); "No connected displays found!");
drmModeFreeResources(resources); drmModeFreeResources(resources);
return false; return DIRECT_RENDERING_INIT_FAILED;
} }
// Now find the CRTC from the encoder // Now find the CRTC from the encoder
@@ -109,7 +185,7 @@ bool DrmRenderer::initialize(PDECODER_PARAMETERS)
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"DRM encoder not found!"); "DRM encoder not found!");
drmModeFreeResources(resources); drmModeFreeResources(resources);
return false; return DIRECT_RENDERING_INIT_FAILED;
} }
int crtcIndex = -1; int crtcIndex = -1;
@@ -130,7 +206,7 @@ bool DrmRenderer::initialize(PDECODER_PARAMETERS)
if (crtcIndex == -1) { if (crtcIndex == -1) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Failed to get CRTC!"); "Failed to get CRTC!");
return false; return DIRECT_RENDERING_INIT_FAILED;
} }
drmSetClientCap(m_DrmFd, DRM_CLIENT_CAP_UNIVERSAL_PLANES, 1); drmSetClientCap(m_DrmFd, DRM_CLIENT_CAP_UNIVERSAL_PLANES, 1);
@@ -140,7 +216,7 @@ bool DrmRenderer::initialize(PDECODER_PARAMETERS)
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"drmGetPlaneResources() failed: %d", "drmGetPlaneResources() failed: %d",
errno); errno);
return false; return DIRECT_RENDERING_INIT_FAILED;
} }
// Find an NV12 overlay plane to render on // Find an NV12 overlay plane to render on
@@ -185,6 +261,15 @@ bool DrmRenderer::initialize(PDECODER_PARAMETERS)
drmModeFreePlaneResources(planeRes); drmModeFreePlaneResources(planeRes);
if (m_PlaneId == 0) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Failed to find suitable NV12 overlay plane!");
return DIRECT_RENDERING_INIT_FAILED;
}
// If we got this far, we can do direct rendering via the DRM FD.
m_SupportsDirectRendering = true;
return true; return true;
} }
@@ -213,6 +298,8 @@ void DrmRenderer::renderFrame(AVFrame* frame)
uint32_t handles[4] = {}; uint32_t handles[4] = {};
uint32_t pitches[4] = {}; uint32_t pitches[4] = {};
uint32_t offsets[4] = {}; uint32_t offsets[4] = {};
uint64_t modifiers[4] = {};
uint32_t flags = 0;
SDL_Rect src, dst; SDL_Rect src, dst;
@@ -234,12 +321,18 @@ void DrmRenderer::renderFrame(AVFrame* frame)
return; return;
} }
// Pass along the modifiers to DRM if there are some in the descriptor
if (drmFrame->objects[0].format_modifier != DRM_FORMAT_MOD_INVALID) {
flags |= DRM_MODE_FB_MODIFIERS;
}
SDL_assert(drmFrame->nb_layers == 1); SDL_assert(drmFrame->nb_layers == 1);
SDL_assert(drmFrame->layers[0].nb_planes == 2); SDL_assert(drmFrame->layers[0].nb_planes == 2);
for (int i = 0; i < drmFrame->layers[0].nb_planes; i++) { for (int i = 0; i < drmFrame->layers[0].nb_planes; i++) {
handles[i] = primeHandle; handles[i] = primeHandle;
pitches[i] = drmFrame->layers[0].planes[i].pitch; pitches[i] = drmFrame->layers[0].planes[i].pitch;
offsets[i] = drmFrame->layers[0].planes[i].offset; offsets[i] = drmFrame->layers[0].planes[i].offset;
modifiers[i] = drmFrame->objects[0].format_modifier;
} }
// Remember the last FB object we created so we can free it // Remember the last FB object we created so we can free it
@@ -247,12 +340,15 @@ void DrmRenderer::renderFrame(AVFrame* frame)
uint32_t lastFbId = m_CurrentFbId; uint32_t lastFbId = m_CurrentFbId;
// Create a frame buffer object from the PRIME buffer // Create a frame buffer object from the PRIME buffer
err = drmModeAddFB2(m_DrmFd, frame->width, frame->height, // NB: It is an error to pass modifiers without DRM_MODE_FB_MODIFIERS set.
drmFrame->layers[0].format, err = drmModeAddFB2WithModifiers(m_DrmFd, frame->width, frame->height,
handles, pitches, offsets, &m_CurrentFbId, 0); drmFrame->layers[0].format,
handles, pitches, offsets,
(flags & DRM_MODE_FB_MODIFIERS) ? modifiers : NULL,
&m_CurrentFbId, flags);
if (err < 0) { if (err < 0) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"drmModeAddFB2() failed: %d", "drmModeAddFB2WithModifiers() failed: %d",
errno); errno);
m_CurrentFbId = lastFbId; m_CurrentFbId = lastFbId;
return; return;
@@ -277,3 +373,200 @@ void DrmRenderer::renderFrame(AVFrame* frame)
// Free the previous FB object which has now been superseded // Free the previous FB object which has now been superseded
drmModeRmFB(m_DrmFd, lastFbId); drmModeRmFB(m_DrmFd, lastFbId);
} }
bool DrmRenderer::needsTestFrame()
{
return true;
}
bool DrmRenderer::isDirectRenderingSupported()
{
return m_SupportsDirectRendering;
}
#ifdef HAVE_EGL
bool DrmRenderer::canExportEGL() {
if (qgetenv("DRM_FORCE_DIRECT") == "1") {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Using direct rendering due to environment variable");
return false;
}
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"DRM backend supports exporting EGLImage");
return true;
}
AVPixelFormat DrmRenderer::getEGLImagePixelFormat() {
// This tells EGLRenderer to treat the EGLImage as a single opaque texture
return AV_PIX_FMT_DRM_PRIME;
}
bool DrmRenderer::initializeEGL(EGLDisplay,
const EGLExtensions &ext) {
if (!ext.isSupported("EGL_EXT_image_dma_buf_import")) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"DRM-EGL: DMABUF unsupported");
return false;
}
m_EGLExtDmaBuf = ext.isSupported("EGL_EXT_image_dma_buf_import_modifiers");
// NB: eglCreateImage() and eglCreateImageKHR() have slightly different definitions
m_eglCreateImage = (typeof(m_eglCreateImage))eglGetProcAddress("eglCreateImage");
m_eglCreateImageKHR = (typeof(m_eglCreateImageKHR))eglGetProcAddress("eglCreateImageKHR");
m_eglDestroyImage = (typeof(m_eglDestroyImage))eglGetProcAddress("eglDestroyImage");
m_eglDestroyImageKHR = (typeof(m_eglDestroyImageKHR))eglGetProcAddress("eglDestroyImageKHR");
if (!(m_eglCreateImage && m_eglDestroyImage) &&
!(m_eglCreateImageKHR && m_eglDestroyImageKHR)) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Missing eglCreateImage()/eglDestroyImage() in EGL driver");
return false;
}
return true;
}
ssize_t DrmRenderer::exportEGLImages(AVFrame *frame, EGLDisplay dpy,
EGLImage images[EGL_MAX_PLANES]) {
AVDRMFrameDescriptor* drmFrame = (AVDRMFrameDescriptor*)frame->data[0];
memset(images, 0, sizeof(EGLImage) * EGL_MAX_PLANES);
SDL_assert(drmFrame->nb_objects == 1);
SDL_assert(drmFrame->nb_layers == 1);
// Max 30 attributes (1 key + 1 value for each)
const int MAX_ATTRIB_COUNT = 30 * 2;
EGLAttrib attribs[MAX_ATTRIB_COUNT] = {
EGL_LINUX_DRM_FOURCC_EXT, (EGLAttrib)drmFrame->layers[0].format,
EGL_WIDTH, frame->width,
EGL_HEIGHT, frame->height,
};
int attribIndex = 6;
for (int i = 0; i < drmFrame->layers[0].nb_planes; ++i) {
const auto &plane = drmFrame->layers[0].planes[i];
const auto &object = drmFrame->objects[plane.object_index];
switch (i) {
case 0:
attribs[attribIndex++] = EGL_DMA_BUF_PLANE0_FD_EXT;
attribs[attribIndex++] = object.fd;
attribs[attribIndex++] = EGL_DMA_BUF_PLANE0_OFFSET_EXT;
attribs[attribIndex++] = plane.offset;
attribs[attribIndex++] = EGL_DMA_BUF_PLANE0_PITCH_EXT;
attribs[attribIndex++] = plane.pitch;
if (m_EGLExtDmaBuf && object.format_modifier != DRM_FORMAT_MOD_INVALID) {
attribs[attribIndex++] = EGL_DMA_BUF_PLANE0_MODIFIER_LO_EXT;
attribs[attribIndex++] = (EGLint)(object.format_modifier & 0xFFFFFFFF);
attribs[attribIndex++] = EGL_DMA_BUF_PLANE0_MODIFIER_HI_EXT;
attribs[attribIndex++] = (EGLint)(object.format_modifier >> 32);
}
break;
case 1:
attribs[attribIndex++] = EGL_DMA_BUF_PLANE1_FD_EXT;
attribs[attribIndex++] = object.fd;
attribs[attribIndex++] = EGL_DMA_BUF_PLANE1_OFFSET_EXT;
attribs[attribIndex++] = plane.offset;
attribs[attribIndex++] = EGL_DMA_BUF_PLANE1_PITCH_EXT;
attribs[attribIndex++] = plane.pitch;
if (m_EGLExtDmaBuf && object.format_modifier != DRM_FORMAT_MOD_INVALID) {
attribs[attribIndex++] = EGL_DMA_BUF_PLANE1_MODIFIER_LO_EXT;
attribs[attribIndex++] = (EGLint)(object.format_modifier & 0xFFFFFFFF);
attribs[attribIndex++] = EGL_DMA_BUF_PLANE1_MODIFIER_HI_EXT;
attribs[attribIndex++] = (EGLint)(object.format_modifier >> 32);
}
break;
case 2:
attribs[attribIndex++] = EGL_DMA_BUF_PLANE2_FD_EXT;
attribs[attribIndex++] = object.fd;
attribs[attribIndex++] = EGL_DMA_BUF_PLANE2_OFFSET_EXT;
attribs[attribIndex++] = plane.offset;
attribs[attribIndex++] = EGL_DMA_BUF_PLANE2_PITCH_EXT;
attribs[attribIndex++] = plane.pitch;
if (m_EGLExtDmaBuf && object.format_modifier != DRM_FORMAT_MOD_INVALID) {
attribs[attribIndex++] = EGL_DMA_BUF_PLANE2_MODIFIER_LO_EXT;
attribs[attribIndex++] = (EGLint)(object.format_modifier & 0xFFFFFFFF);
attribs[attribIndex++] = EGL_DMA_BUF_PLANE2_MODIFIER_HI_EXT;
attribs[attribIndex++] = (EGLint)(object.format_modifier >> 32);
}
break;
case 3:
attribs[attribIndex++] = EGL_DMA_BUF_PLANE3_FD_EXT;
attribs[attribIndex++] = object.fd;
attribs[attribIndex++] = EGL_DMA_BUF_PLANE3_OFFSET_EXT;
attribs[attribIndex++] = plane.offset;
attribs[attribIndex++] = EGL_DMA_BUF_PLANE3_PITCH_EXT;
attribs[attribIndex++] = plane.pitch;
if (m_EGLExtDmaBuf && object.format_modifier != DRM_FORMAT_MOD_INVALID) {
attribs[attribIndex++] = EGL_DMA_BUF_PLANE3_MODIFIER_LO_EXT;
attribs[attribIndex++] = (EGLint)(object.format_modifier & 0xFFFFFFFF);
attribs[attribIndex++] = EGL_DMA_BUF_PLANE3_MODIFIER_HI_EXT;
attribs[attribIndex++] = (EGLint)(object.format_modifier >> 32);
}
break;
default:
Q_UNREACHABLE();
}
}
// Terminate the attribute list
attribs[attribIndex++] = EGL_NONE;
SDL_assert(attribIndex <= MAX_ATTRIB_COUNT);
// Our EGLImages are non-planar, so we only populate the first entry
if (m_eglCreateImage) {
images[0] = m_eglCreateImage(dpy, EGL_NO_CONTEXT,
EGL_LINUX_DMA_BUF_EXT,
nullptr, attribs);
if (!images[0]) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"eglCreateImage() Failed: %d", eglGetError());
goto fail;
}
}
else {
// Cast the EGLAttrib array elements to EGLint for the KHR extension
EGLint intAttribs[MAX_ATTRIB_COUNT];
for (int i = 0; i < MAX_ATTRIB_COUNT; i++) {
intAttribs[i] = (EGLint)attribs[i];
}
images[0] = m_eglCreateImageKHR(dpy, EGL_NO_CONTEXT,
EGL_LINUX_DMA_BUF_EXT,
nullptr, intAttribs);
if (!images[0]) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"eglCreateImageKHR() Failed: %d", eglGetError());
goto fail;
}
}
return 1;
fail:
freeEGLImages(dpy, images);
return -1;
}
void DrmRenderer::freeEGLImages(EGLDisplay dpy, EGLImage images[EGL_MAX_PLANES]) {
if (m_eglDestroyImage) {
m_eglDestroyImage(dpy, images[0]);
}
else {
m_eglDestroyImageKHR(dpy, images[0]);
}
// Our EGLImages are non-planar
SDL_assert(images[1] == 0);
SDL_assert(images[2] == 0);
}
#endif
@@ -14,12 +14,32 @@ public:
virtual void renderFrame(AVFrame* frame) override; virtual void renderFrame(AVFrame* frame) override;
virtual enum AVPixelFormat getPreferredPixelFormat(int videoFormat) override; virtual enum AVPixelFormat getPreferredPixelFormat(int videoFormat) override;
virtual int getRendererAttributes() override; virtual int getRendererAttributes() override;
virtual bool needsTestFrame() override;
virtual bool isDirectRenderingSupported() override;
#ifdef HAVE_EGL
virtual bool canExportEGL() override;
virtual AVPixelFormat getEGLImagePixelFormat() 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: private:
AVBufferRef* m_HwContext;
int m_DrmFd; int m_DrmFd;
bool m_SdlOwnsDrmFd;
bool m_SupportsDirectRendering;
uint32_t m_CrtcId; uint32_t m_CrtcId;
uint32_t m_PlaneId; uint32_t m_PlaneId;
uint32_t m_CurrentFbId; uint32_t m_CurrentFbId;
SDL_Rect m_OutputRect; SDL_Rect m_OutputRect;
#ifdef HAVE_EGL
bool m_EGLExtDmaBuf;
PFNEGLCREATEIMAGEPROC m_eglCreateImage;
PFNEGLDESTROYIMAGEPROC m_eglDestroyImage;
PFNEGLCREATEIMAGEKHRPROC m_eglCreateImageKHR;
PFNEGLDESTROYIMAGEKHRPROC m_eglDestroyImageKHR;
#endif
}; };
@@ -6,7 +6,7 @@
static QStringList egl_get_extensions(EGLDisplay dpy) { static QStringList egl_get_extensions(EGLDisplay dpy) {
const auto EGLExtensionsStr = eglQueryString(dpy, EGL_EXTENSIONS); const auto EGLExtensionsStr = eglQueryString(dpy, EGL_EXTENSIONS);
if (!EGLExtensionsStr) { if (!EGLExtensionsStr) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, "Unable to get EGL extensions"); SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, "Unable to get EGL extensions: %d", eglGetError());
return QStringList(); return QStringList();
} }
return QString(EGLExtensionsStr).split(" "); return QString(EGLExtensionsStr).split(" ");
+133 -69
View File
@@ -15,18 +15,6 @@
#include <SDL_render.h> #include <SDL_render.h>
#include <SDL_syswm.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 // These are EGL extensions, so some platform headers may not provide them
#ifndef EGL_PLATFORM_WAYLAND_KHR #ifndef EGL_PLATFORM_WAYLAND_KHR
#define EGL_PLATFORM_WAYLAND_KHR 0x31D8 #define EGL_PLATFORM_WAYLAND_KHR 0x31D8
@@ -34,6 +22,9 @@ typedef EGLDisplay (EGLAPIENTRYP PFNEGLGETPLATFORMDISPLAYEXTPROC) (EGLenum platf
#ifndef EGL_PLATFORM_X11_KHR #ifndef EGL_PLATFORM_X11_KHR
#define EGL_PLATFORM_X11_KHR 0x31D5 #define EGL_PLATFORM_X11_KHR 0x31D5
#endif #endif
#ifndef EGL_PLATFORM_GBM_KHR
#define EGL_PLATFORM_GBM_KHR 0x31D7
#endif
typedef struct _OVERLAY_VERTEX typedef struct _OVERLAY_VERTEX
{ {
@@ -68,7 +59,7 @@ SDL_Window* EGLRenderer::s_LastFailedWindow = nullptr;
EGLRenderer::EGLRenderer(IFFmpegRenderer *backendRenderer) EGLRenderer::EGLRenderer(IFFmpegRenderer *backendRenderer)
: :
m_SwPixelFormat(AV_PIX_FMT_NONE), m_EGLImagePixelFormat(AV_PIX_FMT_NONE),
m_EGLDisplay(EGL_NO_DISPLAY), m_EGLDisplay(EGL_NO_DISPLAY),
m_Textures{0}, m_Textures{0},
m_OverlayTextures{0}, m_OverlayTextures{0},
@@ -83,6 +74,7 @@ EGLRenderer::EGLRenderer(IFFmpegRenderer *backendRenderer)
m_ColorSpace(AVCOL_SPC_NB), m_ColorSpace(AVCOL_SPC_NB),
m_ColorFull(false), m_ColorFull(false),
m_BlockingSwapBuffers(false), m_BlockingSwapBuffers(false),
m_LastFrame(av_frame_alloc()),
m_glEGLImageTargetTexture2DOES(nullptr), m_glEGLImageTargetTexture2DOES(nullptr),
m_glGenVertexArraysOES(nullptr), m_glGenVertexArraysOES(nullptr),
m_glBindVertexArrayOES(nullptr), m_glBindVertexArrayOES(nullptr),
@@ -133,6 +125,8 @@ EGLRenderer::~EGLRenderer()
SDL_DestroyRenderer(m_DummyRenderer); SDL_DestroyRenderer(m_DummyRenderer);
} }
av_frame_free(&m_LastFrame);
// Reset the global properties back to what they were before // Reset the global properties back to what they were before
SDL_SetHint(SDL_HINT_OPENGL_ES_DRIVER, "0"); SDL_SetHint(SDL_HINT_OPENGL_ES_DRIVER, "0");
SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, m_OldContextProfileMask); SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, m_OldContextProfileMask);
@@ -162,16 +156,16 @@ void EGLRenderer::notifyOverlayUpdated(Overlay::OverlayType type)
} }
} }
bool EGLRenderer::isPixelFormatSupported(int, AVPixelFormat pixelFormat) bool EGLRenderer::isPixelFormatSupported(int videoFormat, AVPixelFormat pixelFormat)
{ {
// Remember to keep this in sync with EGLRenderer::renderFrame()! // Pixel format support should be determined by the backend renderer
switch (pixelFormat) return m_Backend->isPixelFormatSupported(videoFormat, pixelFormat);
{ }
case AV_PIX_FMT_NV12:
return true; AVPixelFormat EGLRenderer::getPreferredPixelFormat(int videoFormat)
default: {
return false; // Pixel format preference should be determined by the backend renderer
} return m_Backend->getPreferredPixelFormat(videoFormat);
} }
void EGLRenderer::renderOverlay(Overlay::OverlayType type) void EGLRenderer::renderOverlay(Overlay::OverlayType type)
@@ -195,7 +189,7 @@ void EGLRenderer::renderOverlay(Overlay::OverlayType type)
float y; float y;
float w; float w;
float h; float h;
} overlayRect; } overlayRect = {};
// These overlay positions differ from the other renderers because OpenGL // These overlay positions differ from the other renderers because OpenGL
// places the origin in the lower-left corner instead of the upper-left. // places the origin in the lower-left corner instead of the upper-left.
@@ -208,6 +202,8 @@ void EGLRenderer::renderOverlay(Overlay::OverlayType type)
// Top left // Top left
overlayRect.x = 0; overlayRect.x = 0;
overlayRect.y = m_ViewportHeight - newSurface->h; overlayRect.y = m_ViewportHeight - newSurface->h;
} else {
SDL_assert(false);
} }
overlayRect.w = newSurface->w; overlayRect.w = newSurface->w;
@@ -254,7 +250,7 @@ void EGLRenderer::renderOverlay(Overlay::OverlayType type)
glActiveTexture(GL_TEXTURE0); glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, m_OverlayTextures[type]); glBindTexture(GL_TEXTURE_2D, m_OverlayTextures[type]);
glUniform1i(m_OverlayShaderProgramParams[PARAM_TEXTURE], 0); glUniform1i(m_OverlayShaderProgramParams[OVERLAY_PARAM_TEXTURE], 0);
glDrawArrays(GL_TRIANGLES, 0, 6); glDrawArrays(GL_TRIANGLES, 0, 6);
} }
@@ -374,27 +370,42 @@ bool EGLRenderer::compileShaders() {
SDL_assert(!m_ShaderProgram); SDL_assert(!m_ShaderProgram);
SDL_assert(!m_OverlayShaderProgram); SDL_assert(!m_OverlayShaderProgram);
SDL_assert(m_SwPixelFormat != AV_PIX_FMT_NONE); SDL_assert(m_EGLImagePixelFormat != AV_PIX_FMT_NONE);
// XXX: TODO: other formats // XXX: TODO: other formats
SDL_assert(m_SwPixelFormat == AV_PIX_FMT_NV12); if (m_EGLImagePixelFormat == AV_PIX_FMT_NV12) {
m_ShaderProgram = compileShader("egl_nv12.vert", "egl_nv12.frag");
if (!m_ShaderProgram) {
return false;
}
m_ShaderProgram = compileShader("egl.vert", "egl.frag"); m_ShaderProgramParams[NV12_PARAM_YUVMAT] = glGetUniformLocation(m_ShaderProgram, "yuvmat");
if (!m_ShaderProgram) { m_ShaderProgramParams[NV12_PARAM_OFFSET] = glGetUniformLocation(m_ShaderProgram, "offset");
m_ShaderProgramParams[NV12_PARAM_PLANE1] = glGetUniformLocation(m_ShaderProgram, "plane1");
m_ShaderProgramParams[NV12_PARAM_PLANE2] = glGetUniformLocation(m_ShaderProgram, "plane2");
}
else if (m_EGLImagePixelFormat == AV_PIX_FMT_DRM_PRIME) {
m_ShaderProgram = compileShader("egl_opaque.vert", "egl_opaque.frag");
if (!m_ShaderProgram) {
return false;
}
m_ShaderProgramParams[OPAQUE_PARAM_TEXTURE] = glGetUniformLocation(m_ShaderProgram, "uTexture");
}
else {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Unsupported EGL pixel format: %d",
m_EGLImagePixelFormat);
SDL_assert(false);
return false; return false;
} }
m_ShaderProgramParams[PARAM_YUVMAT] = glGetUniformLocation(m_ShaderProgram, "yuvmat");
m_ShaderProgramParams[PARAM_OFFSET] = glGetUniformLocation(m_ShaderProgram, "offset");
m_ShaderProgramParams[PARAM_PLANE1] = glGetUniformLocation(m_ShaderProgram, "plane1");
m_ShaderProgramParams[PARAM_PLANE2] = glGetUniformLocation(m_ShaderProgram, "plane2");
m_OverlayShaderProgram = compileShader("egl_overlay.vert", "egl_overlay.frag"); m_OverlayShaderProgram = compileShader("egl_overlay.vert", "egl_overlay.frag");
if (!m_OverlayShaderProgram) { if (!m_OverlayShaderProgram) {
return false; return false;
} }
m_OverlayShaderProgramParams[PARAM_TEXTURE] = glGetUniformLocation(m_OverlayShaderProgram, "uTexture"); m_OverlayShaderProgramParams[OVERLAY_PARAM_TEXTURE] = glGetUniformLocation(m_OverlayShaderProgram, "uTexture");
return true; return true;
} }
@@ -479,6 +490,13 @@ bool EGLRenderer::initialize(PDECODER_PARAMETERS params)
return false; return false;
} }
break; break;
#endif
#if SDL_VERSION_ATLEAST(2, 0, 15) && defined(SDL_VIDEO_DRIVER_KMSDRM)
case SDL_SYSWM_KMSDRM:
if (!openDisplay(EGL_PLATFORM_GBM_KHR, info.info.kmsdrm.gbm_dev)) {
return false;
}
break;
#endif #endif
default: default:
EGL_LOG(Error, "not compatible with SYSWM"); EGL_LOG(Error, "not compatible with SYSWM");
@@ -544,7 +562,7 @@ bool EGLRenderer::initialize(PDECODER_PARAMETERS params)
src.x = src.y = dst.x = dst.y = 0; src.x = src.y = dst.x = dst.y = 0;
src.w = params->width; src.w = params->width;
src.h = params->height; src.h = params->height;
SDL_GetWindowSize(m_Window, &dst.w, &dst.h); SDL_GL_GetDrawableSize(m_Window, &dst.w, &dst.h);
StreamUtils::scaleSourceToDestinationSurface(&src, &dst); StreamUtils::scaleSourceToDestinationSurface(&src, &dst);
glViewport(dst.x, dst.y, dst.w, dst.h); glViewport(dst.x, dst.y, dst.w, dst.h);
@@ -557,7 +575,17 @@ bool EGLRenderer::initialize(PDECODER_PARAMETERS params)
if (params->enableVsync) { if (params->enableVsync) {
SDL_GL_SetSwapInterval(1); SDL_GL_SetSwapInterval(1);
m_BlockingSwapBuffers = true;
#if SDL_VERSION_ATLEAST(2, 0, 15) && defined(SDL_VIDEO_DRIVER_KMSDRM)
// The SDL KMSDRM backend already enforces double buffering (due to
// SDL_HINT_VIDEO_DOUBLE_BUFFER=1), so calling glFinish() after
// SDL_GL_SwapWindow() will block an extra frame and lock rendering
// at 1/2 the display refresh rate.
if (info.subsystem != SDL_SYSWM_KMSDRM)
#endif
{
m_BlockingSwapBuffers = true;
}
} else { } else {
SDL_GL_SetSwapInterval(0); SDL_GL_SetSwapInterval(0);
} }
@@ -648,7 +676,18 @@ const float *EGLRenderer::getColorMatrix() {
case AVCOL_SPC_BT2020_CL: case AVCOL_SPC_BT2020_CL:
return m_ColorFull ? bt2020Full : bt2020Lim; return m_ColorFull ? bt2020Full : bt2020Lim;
default: default:
SDL_assert(false); // Some backends don't populate this, so we'll assume
// the host gave us what we asked for by default.
switch (getDecoderColorspace()) {
case COLORSPACE_REC_601:
return m_ColorFull ? bt601Full : bt601Lim;
case COLORSPACE_REC_709:
return m_ColorFull ? bt709Full : bt709Lim;
case COLORSPACE_REC_2020:
return m_ColorFull ? bt2020Full : bt2020Lim;
default:
SDL_assert(false);
}
}; };
return bt601Lim; return bt601Lim;
@@ -721,49 +760,49 @@ void EGLRenderer::renderFrame(AVFrame* frame)
return; return;
} }
if (frame->hw_frames_ctx != nullptr) { // Find the native read-back format and load the shaders
// Find the native read-back format and load the shader if (m_EGLImagePixelFormat == AV_PIX_FMT_NONE) {
if (m_SwPixelFormat == AV_PIX_FMT_NONE) { m_EGLImagePixelFormat = m_Backend->getEGLImagePixelFormat();
auto hwFrameCtx = (AVHWFramesContext*)frame->hw_frames_ctx->data; EGL_LOG(Info, "EGLImage pixel format: %d", m_EGLImagePixelFormat);
m_SwPixelFormat = hwFrameCtx->sw_format; SDL_assert(m_EGLImagePixelFormat != AV_PIX_FMT_NONE);
SDL_assert(m_SwPixelFormat != AV_PIX_FMT_NONE);
EGL_LOG(Info, "Selected read-back format: %d", m_SwPixelFormat); m_ColorSpace = frame->colorspace;
// XXX: TODO: Handle other pixel formats // This handles the case where the color range is unknown,
SDL_assert(m_SwPixelFormat == AV_PIX_FMT_NV12); // so that we use Limited color range which is the default
m_ColorSpace = frame->colorspace; // behavior for Moonlight.
m_ColorFull = frame->color_range == AVCOL_RANGE_JPEG; m_ColorFull = frame->color_range == AVCOL_RANGE_JPEG;
if (!specialize()) { if (!specialize()) {
m_SwPixelFormat = AV_PIX_FMT_NONE; m_EGLImagePixelFormat = AV_PIX_FMT_NONE;
return;
}
}
ssize_t plane_count = m_Backend->exportEGLImages(frame, m_EGLDisplay, imgs);
if (plane_count < 0)
return; 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"); ssize_t plane_count = m_Backend->exportEGLImages(frame, m_EGLDisplay, imgs);
if (plane_count < 0)
return; 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]);
} }
glClear(GL_COLOR_BUFFER_BIT); glClear(GL_COLOR_BUFFER_BIT);
glUseProgram(m_ShaderProgram); glUseProgram(m_ShaderProgram);
m_glBindVertexArrayOES(m_VAO); m_glBindVertexArrayOES(m_VAO);
glUniformMatrix3fv(m_ShaderProgramParams[PARAM_YUVMAT], 1, GL_FALSE, getColorMatrix()); // Bind parameters for the shaders
glUniform3fv(m_ShaderProgramParams[PARAM_OFFSET], 1, getColorOffsets()); if (m_EGLImagePixelFormat == AV_PIX_FMT_NV12) {
glUniform1i(m_ShaderProgramParams[PARAM_PLANE1], 0); glUniformMatrix3fv(m_ShaderProgramParams[NV12_PARAM_YUVMAT], 1, GL_FALSE, getColorMatrix());
glUniform1i(m_ShaderProgramParams[PARAM_PLANE2], 1); glUniform3fv(m_ShaderProgramParams[NV12_PARAM_OFFSET], 1, getColorOffsets());
glUniform1i(m_ShaderProgramParams[NV12_PARAM_PLANE1], 0);
glUniform1i(m_ShaderProgramParams[NV12_PARAM_PLANE2], 1);
}
else if (m_EGLImagePixelFormat == AV_PIX_FMT_DRM_PRIME) {
glUniform1i(m_ShaderProgramParams[OPAQUE_PARAM_TEXTURE], 0);
}
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0); glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
@@ -787,6 +826,31 @@ void EGLRenderer::renderFrame(AVFrame* frame)
glFinish(); glFinish();
} }
if (frame->hw_frames_ctx != nullptr) m_Backend->freeEGLImages(m_EGLDisplay, imgs);
m_Backend->freeEGLImages(m_EGLDisplay, imgs);
// Free the DMA-BUF backing the last frame now that it is definitely
// no longer being used anymore. While the PRIME FD stays around until
// EGL is done with it, the memory backing it may be reused by FFmpeg
// before the GPU has read it. This is particularly noticeable on the
// RK3288-based TinkerBoard when V-Sync is disabled.
av_frame_unref(m_LastFrame);
av_frame_move_ref(m_LastFrame, frame);
}
bool EGLRenderer::testRenderFrame(AVFrame* frame)
{
EGLImage imgs[EGL_MAX_PLANES];
// Make sure we can get working EGLImages from the backend renderer.
// Some devices (Raspberry Pi) will happily decode into DRM formats that
// its own GL implementation won't accept in eglCreateImage().
ssize_t plane_count = m_Backend->exportEGLImages(frame, m_EGLDisplay, imgs);
if (plane_count <= 0) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"Backend failed to export EGL image for test frame");
return false;
}
m_Backend->freeEGLImages(m_EGLDisplay, imgs);
return true;
} }
+10 -6
View File
@@ -12,8 +12,10 @@ public:
virtual bool initialize(PDECODER_PARAMETERS params) override; virtual bool initialize(PDECODER_PARAMETERS params) override;
virtual bool prepareDecoderContext(AVCodecContext* context, AVDictionary** options) override; virtual bool prepareDecoderContext(AVCodecContext* context, AVDictionary** options) override;
virtual void renderFrame(AVFrame* frame) override; virtual void renderFrame(AVFrame* frame) override;
virtual bool testRenderFrame(AVFrame* frame) override;
virtual void notifyOverlayUpdated(Overlay::OverlayType) override; virtual void notifyOverlayUpdated(Overlay::OverlayType) override;
virtual bool isPixelFormatSupported(int videoFormat, enum AVPixelFormat pixelFormat) override; virtual bool isPixelFormatSupported(int videoFormat, enum AVPixelFormat pixelFormat) override;
virtual AVPixelFormat getPreferredPixelFormat(int videoFormat) override;
private: private:
@@ -29,7 +31,7 @@ private:
int m_ViewportWidth; int m_ViewportWidth;
int m_ViewportHeight; int m_ViewportHeight;
int m_SwPixelFormat; AVPixelFormat m_EGLImagePixelFormat;
void *m_EGLDisplay; void *m_EGLDisplay;
unsigned m_Textures[EGL_MAX_PLANES]; unsigned m_Textures[EGL_MAX_PLANES];
unsigned m_OverlayTextures[Overlay::OverlayMax]; unsigned m_OverlayTextures[Overlay::OverlayMax];
@@ -44,18 +46,20 @@ private:
int m_ColorSpace; int m_ColorSpace;
bool m_ColorFull; bool m_ColorFull;
bool m_BlockingSwapBuffers; bool m_BlockingSwapBuffers;
AVFrame* m_LastFrame;
PFNGLEGLIMAGETARGETTEXTURE2DOESPROC m_glEGLImageTargetTexture2DOES; PFNGLEGLIMAGETARGETTEXTURE2DOESPROC m_glEGLImageTargetTexture2DOES;
PFNGLGENVERTEXARRAYSOESPROC m_glGenVertexArraysOES; PFNGLGENVERTEXARRAYSOESPROC m_glGenVertexArraysOES;
PFNGLBINDVERTEXARRAYOESPROC m_glBindVertexArrayOES; PFNGLBINDVERTEXARRAYOESPROC m_glBindVertexArrayOES;
PFNGLDELETEVERTEXARRAYSOESPROC m_glDeleteVertexArraysOES; PFNGLDELETEVERTEXARRAYSOESPROC m_glDeleteVertexArraysOES;
#define PARAM_YUVMAT 0 #define NV12_PARAM_YUVMAT 0
#define PARAM_OFFSET 1 #define NV12_PARAM_OFFSET 1
#define PARAM_PLANE1 2 #define NV12_PARAM_PLANE1 2
#define PARAM_PLANE2 3 #define NV12_PARAM_PLANE2 3
#define OPAQUE_PARAM_TEXTURE 0
int m_ShaderProgramParams[4]; int m_ShaderProgramParams[4];
#define PARAM_TEXTURE 0 #define OVERLAY_PARAM_TEXTURE 0
int m_OverlayShaderProgramParams[1]; int m_OverlayShaderProgramParams[1];
int m_OldContextProfileMask; int m_OldContextProfileMask;
+72 -22
View File
@@ -10,7 +10,12 @@
MmalRenderer::MmalRenderer() MmalRenderer::MmalRenderer()
: m_Renderer(nullptr), : m_Renderer(nullptr),
m_InputPort(nullptr), m_InputPort(nullptr),
m_BackgroundRenderer(nullptr) m_BackgroundRenderer(nullptr),
m_Window(nullptr),
m_VideoWidth(0),
m_VideoHeight(0),
m_LastWindowPosX(-1),
m_LastWindowPosY(-1)
{ {
} }
@@ -46,10 +51,67 @@ bool MmalRenderer::prepareDecoderContext(AVCodecContext* context, AVDictionary**
return true; return true;
} }
void MmalRenderer::updateDisplayRegion()
{
MMAL_STATUS_T status;
int currentPosX, currentPosY;
MMAL_DISPLAYREGION_T dr;
dr.hdr.id = MMAL_PARAMETER_DISPLAYREGION;
dr.hdr.size = sizeof(MMAL_DISPLAYREGION_T);
dr.set = MMAL_DISPLAY_SET_DEST_RECT;
SDL_GetWindowPosition(m_Window, &currentPosX, &currentPosY);
if ((SDL_GetWindowFlags(m_Window) & SDL_WINDOW_INPUT_FOCUS) == 0) {
dr.dest_rect.x = 0;
dr.dest_rect.y = 0;
dr.dest_rect.width = 0;
dr.dest_rect.height = 0;
// Force a re-evaluation next time
m_LastWindowPosX = -1;
m_LastWindowPosY = -1;
}
else if (m_LastWindowPosX != currentPosX || m_LastWindowPosY != currentPosY) {
SDL_Rect src, dst;
src.x = src.y = 0;
src.w = m_VideoWidth;
src.h = m_VideoHeight;
dst.x = dst.y = 0;
SDL_GetWindowSize(m_Window, &dst.w, &dst.h);
StreamUtils::scaleSourceToDestinationSurface(&src, &dst);
dr.dest_rect.x = currentPosX + dst.x;
dr.dest_rect.y = currentPosY + dst.y;
dr.dest_rect.width = dst.w;
dr.dest_rect.height = dst.h;
m_LastWindowPosX = currentPosX;
m_LastWindowPosY = currentPosY;
}
else {
// Nothing to do
return;
}
status = mmal_port_parameter_set(m_InputPort, &dr.hdr);
if (status != MMAL_SUCCESS) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"mmal_port_parameter_set() failed: %x (%s)",
status, mmal_status_to_string(status));
}
}
bool MmalRenderer::initialize(PDECODER_PARAMETERS params) bool MmalRenderer::initialize(PDECODER_PARAMETERS params)
{ {
MMAL_STATUS_T status; MMAL_STATUS_T status;
m_Window = params->window;
m_VideoWidth = params->width;
m_VideoHeight = params->height;
// Clear the background if possible // Clear the background if possible
setupBackground(params); setupBackground(params);
@@ -107,23 +169,6 @@ bool MmalRenderer::initialize(PDECODER_PARAMETERS params)
dr.src_rect.width = params->width; dr.src_rect.width = params->width;
dr.src_rect.height = params->height; dr.src_rect.height = params->height;
{
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_parameter_set(m_InputPort, &dr.hdr); status = mmal_port_parameter_set(m_InputPort, &dr.hdr);
if (status != MMAL_SUCCESS) { if (status != MMAL_SUCCESS) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
@@ -131,6 +176,9 @@ bool MmalRenderer::initialize(PDECODER_PARAMETERS params)
status, mmal_status_to_string(status)); status, mmal_status_to_string(status));
return false; return false;
} }
// Set the destination display region
updateDisplayRegion();
} }
status = mmal_port_enable(m_InputPort, InputPortCallback); status = mmal_port_enable(m_InputPort, InputPortCallback);
@@ -179,17 +227,16 @@ void MmalRenderer::InputPortCallback(MMAL_PORT_T*, MMAL_BUFFER_HEADER_T* buffer)
mmal_buffer_header_release(buffer); mmal_buffer_header_release(buffer);
} }
enum AVPixelFormat MmalRenderer::getPreferredPixelFormat(int videoFormat) enum AVPixelFormat MmalRenderer::getPreferredPixelFormat(int)
{ {
// Opaque MMAL buffers // Opaque MMAL buffers
SDL_assert(videoFormat == VIDEO_FORMAT_H264);
return AV_PIX_FMT_MMAL; return AV_PIX_FMT_MMAL;
} }
int MmalRenderer::getRendererAttributes() int MmalRenderer::getRendererAttributes()
{ {
// This renderer can only draw in full-screen and maxes out at 1080p // This renderer maxes out at 1080p
return RENDERER_ATTRIBUTE_FULLSCREEN_ONLY | RENDERER_ATTRIBUTE_1080P_MAX; return RENDERER_ATTRIBUTE_1080P_MAX;
} }
bool MmalRenderer::needsTestFrame() bool MmalRenderer::needsTestFrame()
@@ -209,6 +256,9 @@ void MmalRenderer::renderFrame(AVFrame* frame)
MMAL_BUFFER_HEADER_T* buffer = (MMAL_BUFFER_HEADER_T*)frame->data[3]; MMAL_BUFFER_HEADER_T* buffer = (MMAL_BUFFER_HEADER_T*)frame->data[3];
MMAL_STATUS_T status; MMAL_STATUS_T status;
// Update the destination display region in case the window moved
updateDisplayRegion();
status = mmal_port_send_buffer(m_InputPort, buffer); status = mmal_port_send_buffer(m_InputPort, buffer);
if (status != MMAL_SUCCESS) { if (status != MMAL_SUCCESS) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
@@ -22,10 +22,14 @@ private:
static void InputPortCallback(MMAL_PORT_T* port, MMAL_BUFFER_HEADER_T* buffer); static void InputPortCallback(MMAL_PORT_T* port, MMAL_BUFFER_HEADER_T* buffer);
void setupBackground(PDECODER_PARAMETERS params); void setupBackground(PDECODER_PARAMETERS params);
void updateDisplayRegion();
MMAL_COMPONENT_T* m_Renderer; MMAL_COMPONENT_T* m_Renderer;
MMAL_PORT_T* m_InputPort; MMAL_PORT_T* m_InputPort;
SDL_Renderer* m_BackgroundRenderer; SDL_Renderer* m_BackgroundRenderer;
SDL_Window* m_Window;
int m_VideoWidth, m_VideoHeight;
int m_LastWindowPosX, m_LastWindowPosY;
}; };
@@ -10,9 +10,60 @@ extern "C" {
} }
#ifdef HAVE_EGL #ifdef HAVE_EGL
// SDL_egl.h have too many conflicts, we will do without #define MESA_EGL_NO_X11_HEADERS
typedef void *EGLDisplay; #define EGL_NO_X11
#include <SDL_egl.h>
#ifndef EGL_VERSION_1_5
typedef intptr_t EGLAttrib;
typedef void *EGLImage; typedef void *EGLImage;
#endif
#if !defined(EGL_VERSION_1_5) || !defined(EGL_EGL_PROTOTYPES)
typedef EGLImage (EGLAPIENTRYP PFNEGLCREATEIMAGEPROC) (EGLDisplay dpy, EGLContext ctx, EGLenum target, EGLClientBuffer buffer, const EGLAttrib *attrib_list);
typedef EGLBoolean (EGLAPIENTRYP PFNEGLDESTROYIMAGEPROC) (EGLDisplay dpy, EGLImage image);
typedef EGLDisplay (EGLAPIENTRYP PFNEGLGETPLATFORMDISPLAYPROC) (EGLenum platform, void *native_display, const EGLAttrib *attrib_list);
#endif
#if !defined(EGL_KHR_image) || !defined(EGL_EGLEXT_PROTOTYPES)
// EGL_KHR_image technically uses EGLImageKHR instead of EGLImage, but they're compatible
// so we swap them here to avoid mixing them all over the place
typedef EGLImage (EGLAPIENTRYP PFNEGLCREATEIMAGEKHRPROC) (EGLDisplay dpy, EGLContext ctx, EGLenum target, EGLClientBuffer buffer, const EGLint *attrib_list);
typedef EGLBoolean (EGLAPIENTRYP PFNEGLDESTROYIMAGEKHRPROC) (EGLDisplay dpy, EGLImage image);
#endif
#if !defined(EGL_EXT_platform_base) || !defined(EGL_EGLEXT_PROTOTYPES)
typedef EGLDisplay (EGLAPIENTRYP PFNEGLGETPLATFORMDISPLAYEXTPROC) (EGLenum platform, void *native_display, const EGLint *attrib_list);
#endif
#ifndef EGL_EXT_image_dma_buf_import
#define EGL_LINUX_DMA_BUF_EXT 0x3270
#define EGL_LINUX_DRM_FOURCC_EXT 0x3271
#define EGL_DMA_BUF_PLANE0_FD_EXT 0x3272
#define EGL_DMA_BUF_PLANE0_OFFSET_EXT 0x3273
#define EGL_DMA_BUF_PLANE0_PITCH_EXT 0x3274
#define EGL_DMA_BUF_PLANE1_FD_EXT 0x3275
#define EGL_DMA_BUF_PLANE1_OFFSET_EXT 0x3276
#define EGL_DMA_BUF_PLANE1_PITCH_EXT 0x3277
#define EGL_DMA_BUF_PLANE2_FD_EXT 0x3278
#define EGL_DMA_BUF_PLANE2_OFFSET_EXT 0x3279
#define EGL_DMA_BUF_PLANE2_PITCH_EXT 0x327A
#endif
#ifndef EGL_EXT_image_dma_buf_import_modifiers
#define EGL_DMA_BUF_PLANE3_FD_EXT 0x3440
#define EGL_DMA_BUF_PLANE3_OFFSET_EXT 0x3441
#define EGL_DMA_BUF_PLANE3_PITCH_EXT 0x3442
#define EGL_DMA_BUF_PLANE0_MODIFIER_LO_EXT 0x3443
#define EGL_DMA_BUF_PLANE0_MODIFIER_HI_EXT 0x3444
#define EGL_DMA_BUF_PLANE1_MODIFIER_LO_EXT 0x3445
#define EGL_DMA_BUF_PLANE1_MODIFIER_HI_EXT 0x3446
#define EGL_DMA_BUF_PLANE2_MODIFIER_LO_EXT 0x3447
#define EGL_DMA_BUF_PLANE2_MODIFIER_HI_EXT 0x3448
#define EGL_DMA_BUF_PLANE3_MODIFIER_LO_EXT 0x3449
#define EGL_DMA_BUF_PLANE3_MODIFIER_HI_EXT 0x344A
#endif
#define EGL_MAX_PLANES 4 #define EGL_MAX_PLANES 4
class EGLExtensions { class EGLExtensions {
@@ -35,6 +86,13 @@ public:
virtual bool prepareDecoderContext(AVCodecContext* context, AVDictionary** options) = 0; virtual bool prepareDecoderContext(AVCodecContext* context, AVDictionary** options) = 0;
virtual void renderFrame(AVFrame* frame) = 0; virtual void renderFrame(AVFrame* frame) = 0;
virtual bool testRenderFrame(AVFrame*) {
// If the renderer doesn't provide an explicit test routine,
// we will always assume that any returned AVFrame can be
// rendered successfully.
return true;
}
virtual bool needsTestFrame() { virtual bool needsTestFrame() {
// No test frame required by default // No test frame required by default
return false; return false;
@@ -65,7 +123,7 @@ public:
return true; return true;
} }
virtual enum AVPixelFormat getPreferredPixelFormat(int videoFormat) { virtual AVPixelFormat getPreferredPixelFormat(int videoFormat) {
if (videoFormat == VIDEO_FORMAT_H265_MAIN10) { if (videoFormat == VIDEO_FORMAT_H265_MAIN10) {
// 10-bit YUV 4:2:0 // 10-bit YUV 4:2:0
return AV_PIX_FMT_P010; return AV_PIX_FMT_P010;
@@ -76,7 +134,7 @@ public:
} }
} }
virtual bool isPixelFormatSupported(int videoFormat, enum AVPixelFormat pixelFormat) { virtual bool isPixelFormatSupported(int videoFormat, AVPixelFormat pixelFormat) {
// By default, we only support the preferred pixel format // By default, we only support the preferred pixel format
return getPreferredPixelFormat(videoFormat) == pixelFormat; return getPreferredPixelFormat(videoFormat) == pixelFormat;
} }
@@ -92,6 +150,10 @@ public:
return false; return false;
} }
virtual AVPixelFormat getEGLImagePixelFormat() {
return AV_PIX_FMT_NONE;
}
virtual bool initializeEGL(EGLDisplay, virtual bool initializeEGL(EGLDisplay,
const EGLExtensions &) { const EGLExtensions &) {
return false; return false;
@@ -351,3 +351,36 @@ Exit:
av_frame_free(&swFrame); av_frame_free(&swFrame);
} }
} }
bool SdlRenderer::testRenderFrame(AVFrame* frame)
{
// If we are acting as the frontend for a hardware
// accelerated decoder, we'll need to read the frame
// back to render it. Test that this can be done
// for the given frame successfully.
if (frame->hw_frames_ctx != nullptr) {
auto hwFrameCtx = (AVHWFramesContext*)frame->hw_frames_ctx->data;
AVFrame* swFrame = av_frame_alloc();
if (swFrame == nullptr) {
return false;
}
swFrame->width = frame->width;
swFrame->height = frame->height;
swFrame->format = hwFrameCtx->sw_format;
int err = av_hwframe_transfer_data(swFrame, frame, 0);
av_frame_free(&swFrame);
if (err != 0) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"av_hwframe_transfer_data() failed: %d",
err);
return false;
}
}
return true;
}
@@ -11,6 +11,7 @@ public:
virtual void renderFrame(AVFrame* frame) override; virtual void renderFrame(AVFrame* frame) override;
virtual bool isRenderThreadSupported() override; virtual bool isRenderThreadSupported() override;
virtual bool isPixelFormatSupported(int videoFormat, enum AVPixelFormat pixelFormat) override; virtual bool isPixelFormatSupported(int videoFormat, enum AVPixelFormat pixelFormat) override;
virtual bool testRenderFrame(AVFrame* frame) override;
private: private:
void renderOverlay(Overlay::OverlayType type); void renderOverlay(Overlay::OverlayType type);
+81 -64
View File
@@ -5,22 +5,23 @@
#include <streaming/streamutils.h> #include <streaming/streamutils.h>
#include <SDL_syswm.h> #include <SDL_syswm.h>
#ifdef HAVE_EGL
#include <SDL_egl.h>
#endif
#include <unistd.h> #include <unistd.h>
#include <fcntl.h> #include <fcntl.h>
VAAPIRenderer::VAAPIRenderer() VAAPIRenderer::VAAPIRenderer()
: m_HwContext(nullptr), : m_HwContext(nullptr),
m_DrmFd(-1),
m_BlacklistedForDirectRendering(false) m_BlacklistedForDirectRendering(false)
{ {
#ifdef HAVE_EGL #ifdef HAVE_EGL
m_PrimeDescriptor.num_layers = 0; m_PrimeDescriptor.num_layers = 0;
m_PrimeDescriptor.num_objects = 0; m_PrimeDescriptor.num_objects = 0;
m_EGLExtDmaBuf = false; m_EGLExtDmaBuf = false;
m_eglCreateImage = nullptr;
m_eglCreateImageKHR = nullptr;
m_eglDestroyImage = nullptr;
m_eglDestroyImageKHR = nullptr;
#endif #endif
} }
@@ -39,10 +40,6 @@ VAAPIRenderer::~VAAPIRenderer()
vaTerminate(display); vaTerminate(display);
} }
} }
if (m_DrmFd != -1) {
close(m_DrmFd);
}
} }
VADisplay VADisplay
@@ -90,41 +87,17 @@ VAAPIRenderer::openDisplay(SDL_Window* window)
return nullptr; return nullptr;
#endif #endif
} }
// TODO: Upstream a better solution for SDL_GetWindowWMInfo on KMSDRM #if defined(SDL_VIDEO_DRIVER_KMSDRM) && defined(HAVE_LIBVA_DRM) && SDL_VERSION_ATLEAST(2, 0, 15)
else if (strcmp(SDL_GetCurrentVideoDriver(), "KMSDRM") == 0) { else if (info.subsystem == SDL_SYSWM_KMSDRM) {
#ifdef HAVE_LIBVA_DRM SDL_assert(info.info.kmsdrm.drm_fd >= 0);
if (m_DrmFd < 0) { display = vaGetDisplayDRM(info.info.kmsdrm.drm_fd);
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) { if (display == nullptr) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Unable to open DRM display for VAAPI"); "Unable to open DRM display for VAAPI");
return nullptr; return nullptr;
} }
#else
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Moonlight not compiled with VAAPI DRM support!");
return nullptr;
#endif
} }
#endif
else { else {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Unsupported VAAPI rendering subsystem: %d", "Unsupported VAAPI rendering subsystem: %d",
@@ -259,12 +232,12 @@ VAAPIRenderer::initialize(PDECODER_PARAMETERS params)
"Driver: %s", "Driver: %s",
vendorString ? vendorString : "<unknown>"); vendorString ? vendorString : "<unknown>");
// AMD's Gallium VAAPI driver has a nasty memory leak that causes memory // Older versions of the Gallium VAAPI driver have a nasty memory leak that
// to be leaked for each submitted frame. The Flatpak runtime has a VDPAU // causes memory to be leaked for each submitted frame. I believe this is
// driver in place that works well, so use that instead on AMD systems. If // resolved in the libva2 drivers (VAAPI 1.x).
// we're using Wayland, we have no choice but to use VAAPI because VDPAU // If we're using Wayland, we have no choice but to use VAAPI because VDPAU
// is only supported under X11 or XWayland. // is only supported under X11 or XWayland.
if (qgetenv("FORCE_VAAPI") != "1" && !WMUtils::isRunningWayland()) { if (major == 0 && qgetenv("FORCE_VAAPI") != "1" && !WMUtils::isRunningWayland()) {
if (vendorStr.contains("AMD", Qt::CaseInsensitive) || if (vendorStr.contains("AMD", Qt::CaseInsensitive) ||
vendorStr.contains("Radeon", Qt::CaseInsensitive)) { vendorStr.contains("Radeon", Qt::CaseInsensitive)) {
// Fail and let VDPAU pick this up // Fail and let VDPAU pick this up
@@ -510,6 +483,10 @@ VAAPIRenderer::canExportEGL() {
return true; return true;
} }
AVPixelFormat VAAPIRenderer::getEGLImagePixelFormat() {
return AV_PIX_FMT_NV12;
}
bool bool
VAAPIRenderer::initializeEGL(EGLDisplay, VAAPIRenderer::initializeEGL(EGLDisplay,
const EGLExtensions &ext) { const EGLExtensions &ext) {
@@ -519,6 +496,20 @@ VAAPIRenderer::initializeEGL(EGLDisplay,
return false; return false;
} }
m_EGLExtDmaBuf = ext.isSupported("EGL_EXT_image_dma_buf_import_modifiers"); m_EGLExtDmaBuf = ext.isSupported("EGL_EXT_image_dma_buf_import_modifiers");
// NB: eglCreateImage() and eglCreateImageKHR() have slightly different definitions
m_eglCreateImage = (typeof(m_eglCreateImage))eglGetProcAddress("eglCreateImage");
m_eglCreateImageKHR = (typeof(m_eglCreateImageKHR))eglGetProcAddress("eglCreateImageKHR");
m_eglDestroyImage = (typeof(m_eglDestroyImage))eglGetProcAddress("eglDestroyImage");
m_eglDestroyImageKHR = (typeof(m_eglDestroyImageKHR))eglGetProcAddress("eglDestroyImageKHR");
if (!(m_eglCreateImage && m_eglDestroyImage) &&
!(m_eglCreateImageKHR && m_eglDestroyImageKHR)) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Missing eglCreateImage()/eglDestroyImage() in EGL driver");
return false;
}
return true; return true;
} }
@@ -554,33 +545,54 @@ VAAPIRenderer::exportEGLImages(AVFrame *frame, EGLDisplay dpy,
const auto &layer = m_PrimeDescriptor.layers[i]; const auto &layer = m_PrimeDescriptor.layers[i];
const auto &object = m_PrimeDescriptor.objects[layer.object_index[0]]; const auto &object = m_PrimeDescriptor.objects[layer.object_index[0]];
EGLAttrib attribs[17] = { const int EGL_ATTRIB_COUNT = 17;
EGL_LINUX_DRM_FOURCC_EXT, (EGLint)layer.drm_format, EGLAttrib attribs[EGL_ATTRIB_COUNT] = {
EGL_LINUX_DRM_FOURCC_EXT, layer.drm_format,
EGL_WIDTH, i == 0 ? frame->width : frame->width / 2, EGL_WIDTH, i == 0 ? frame->width : frame->width / 2,
EGL_HEIGHT, i == 0 ? frame->height : frame->height / 2, EGL_HEIGHT, i == 0 ? frame->height : frame->height / 2,
EGL_DMA_BUF_PLANE0_FD_EXT, object.fd, EGL_DMA_BUF_PLANE0_FD_EXT, object.fd,
EGL_DMA_BUF_PLANE0_OFFSET_EXT, (EGLint)layer.offset[0], EGL_DMA_BUF_PLANE0_OFFSET_EXT, layer.offset[0],
EGL_DMA_BUF_PLANE0_PITCH_EXT, (EGLint)layer.pitch[0], EGL_DMA_BUF_PLANE0_PITCH_EXT, layer.pitch[0],
EGL_NONE, 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
}; };
if (m_EGLExtDmaBuf) {
const EGLAttrib extra[] = { // Cut off the attribute array before the modifiers if they aren't supported
EGL_DMA_BUF_PLANE0_MODIFIER_LO_EXT, if (!m_EGLExtDmaBuf) {
(EGLint)object.drm_format_modifier, SDL_assert(attribs[12] == EGL_DMA_BUF_PLANE0_MODIFIER_LO_EXT);
EGL_DMA_BUF_PLANE0_MODIFIER_HI_EXT, SDL_assert(attribs[14] == EGL_DMA_BUF_PLANE0_MODIFIER_HI_EXT);
(EGLint)(object.drm_format_modifier >> 32), SDL_assert(attribs[16] == EGL_NONE);
EGL_NONE, attribs[12] = EGL_NONE;
};
memcpy((void *)(&attribs[12]), (void *)extra, sizeof (extra));
} }
images[i] = eglCreateImage(dpy, EGL_NO_CONTEXT,
EGL_LINUX_DMA_BUF_EXT, if (m_eglCreateImage) {
nullptr, attribs); images[i] = m_eglCreateImage(dpy, EGL_NO_CONTEXT,
if (!images[i]) { EGL_LINUX_DMA_BUF_EXT,
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, nullptr, attribs);
"eglCreateImage() Failed: %d", eglGetError()); if (!images[i]) {
goto create_image_fail; SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"eglCreateImage() Failed: %d", eglGetError());
goto create_image_fail;
}
} }
else {
// Cast the EGLAttrib array elements to EGLint for the KHR extension
EGLint intAttribs[EGL_ATTRIB_COUNT];
for (int i = 0; i < EGL_ATTRIB_COUNT; i++) {
intAttribs[i] = (EGLint)attribs[i];
}
images[i] = m_eglCreateImageKHR(dpy, EGL_NO_CONTEXT,
EGL_LINUX_DMA_BUF_EXT,
nullptr, intAttribs);
if (!images[i]) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"eglCreateImageKHR() Failed: %d", eglGetError());
goto create_image_fail;
}
}
++count; ++count;
} }
return count; return count;
@@ -595,7 +607,12 @@ sync_fail:
void void
VAAPIRenderer::freeEGLImages(EGLDisplay dpy, EGLImage images[EGL_MAX_PLANES]) { VAAPIRenderer::freeEGLImages(EGLDisplay dpy, EGLImage images[EGL_MAX_PLANES]) {
for (size_t i = 0; i < m_PrimeDescriptor.num_layers; ++i) { for (size_t i = 0; i < m_PrimeDescriptor.num_layers; ++i) {
eglDestroyImage(dpy, images[i]); if (m_eglDestroyImage) {
m_eglDestroyImage(dpy, images[i]);
}
else {
m_eglDestroyImageKHR(dpy, images[i]);
}
} }
for (size_t i = 0; i < m_PrimeDescriptor.num_objects; ++i) { for (size_t i = 0; i < m_PrimeDescriptor.num_objects; ++i) {
close(m_PrimeDescriptor.objects[i].fd); close(m_PrimeDescriptor.objects[i].fd);
+5 -1
View File
@@ -44,6 +44,7 @@ public:
virtual int getDecoderColorspace() override; virtual int getDecoderColorspace() override;
#ifdef HAVE_EGL #ifdef HAVE_EGL
virtual bool canExportEGL() override; virtual bool canExportEGL() override;
virtual AVPixelFormat getEGLImagePixelFormat() override;
virtual bool initializeEGL(EGLDisplay dpy, const EGLExtensions &ext) 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 ssize_t exportEGLImages(AVFrame *frame, EGLDisplay dpy, EGLImage images[EGL_MAX_PLANES]) override;
virtual void freeEGLImages(EGLDisplay dpy, EGLImage[EGL_MAX_PLANES]) override; virtual void freeEGLImages(EGLDisplay dpy, EGLImage[EGL_MAX_PLANES]) override;
@@ -54,7 +55,6 @@ private:
int m_WindowSystem; int m_WindowSystem;
AVBufferRef* m_HwContext; AVBufferRef* m_HwContext;
int m_DrmFd;
bool m_BlacklistedForDirectRendering; bool m_BlacklistedForDirectRendering;
#ifdef HAVE_LIBVA_X11 #ifdef HAVE_LIBVA_X11
@@ -69,5 +69,9 @@ private:
#ifdef HAVE_EGL #ifdef HAVE_EGL
VADRMPRIMESurfaceDescriptor m_PrimeDescriptor; VADRMPRIMESurfaceDescriptor m_PrimeDescriptor;
bool m_EGLExtDmaBuf; bool m_EGLExtDmaBuf;
PFNEGLCREATEIMAGEPROC m_eglCreateImage;
PFNEGLDESTROYIMAGEPROC m_eglDestroyImage;
PFNEGLCREATEIMAGEKHRPROC m_eglCreateImageKHR;
PFNEGLDESTROYIMAGEKHRPROC m_eglDestroyImageKHR;
#endif #endif
}; };
+5 -1
View File
@@ -281,8 +281,8 @@ public:
// Queue this sample for the next v-sync // Queue this sample for the next v-sync
CMSampleTimingInfo timingInfo = { CMSampleTimingInfo timingInfo = {
.duration = kCMTimeInvalid, .duration = kCMTimeInvalid,
.presentationTimeStamp = CMClockMakeHostTimeFromSystemUnits(mach_absolute_time()),
.decodeTimeStamp = kCMTimeInvalid, .decodeTimeStamp = kCMTimeInvalid,
.presentationTimeStamp = CMClockMakeHostTimeFromSystemUnits(mach_absolute_time())
}; };
CMSampleBufferRef sampleBuffer; CMSampleBufferRef sampleBuffer;
@@ -365,6 +365,7 @@ public:
if ([device.name containsString:@" 5"]) { if ([device.name containsString:@" 5"]) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"No HEVC Main10 support on Skylake iGPU"); "No HEVC Main10 support on Skylake iGPU");
[device release];
return false; return false;
} }
} }
@@ -375,10 +376,13 @@ public:
[device.name containsString:@" M3"]) { [device.name containsString:@" M3"]) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"No HEVC Main10 support on AMD GPUs until Polaris"); "No HEVC Main10 support on AMD GPUs until Polaris");
[device release];
return false; return false;
} }
} }
} }
[device release];
} }
else else
#endif #endif
+199 -128
View File
@@ -51,7 +51,7 @@ bool FFmpegVideoDecoder::isHardwareAccelerated()
bool FFmpegVideoDecoder::isAlwaysFullScreen() bool FFmpegVideoDecoder::isAlwaysFullScreen()
{ {
return m_BackendRenderer->getRendererAttributes() & RENDERER_ATTRIBUTE_FULLSCREEN_ONLY; return m_FrontendRenderer->getRendererAttributes() & RENDERER_ATTRIBUTE_FULLSCREEN_ONLY;
} }
int FFmpegVideoDecoder::getDecoderCapabilities() int FFmpegVideoDecoder::getDecoderCapabilities()
@@ -117,7 +117,8 @@ enum AVPixelFormat FFmpegVideoDecoder::ffGetFormat(AVCodecContext* context,
} }
FFmpegVideoDecoder::FFmpegVideoDecoder(bool testOnly) FFmpegVideoDecoder::FFmpegVideoDecoder(bool testOnly)
: m_VideoDecoderCtx(nullptr), : m_Pkt(av_packet_alloc()),
m_VideoDecoderCtx(nullptr),
m_DecodeBuffer(1024 * 1024, 0), m_DecodeBuffer(1024 * 1024, 0),
m_HwDecodeCfg(nullptr), m_HwDecodeCfg(nullptr),
m_BackendRenderer(nullptr), m_BackendRenderer(nullptr),
@@ -132,8 +133,6 @@ FFmpegVideoDecoder::FFmpegVideoDecoder(bool testOnly)
m_NeedsSpsFixup(false), m_NeedsSpsFixup(false),
m_TestOnly(testOnly) m_TestOnly(testOnly)
{ {
av_init_packet(&m_Pkt);
SDL_zero(m_ActiveWndVideoStats); SDL_zero(m_ActiveWndVideoStats);
SDL_zero(m_LastWndVideoStats); SDL_zero(m_LastWndVideoStats);
SDL_zero(m_GlobalVideoStats); SDL_zero(m_GlobalVideoStats);
@@ -151,6 +150,8 @@ FFmpegVideoDecoder::~FFmpegVideoDecoder()
// to preserve the log level across reset() during // to preserve the log level across reset() during
// test initialization. // test initialization.
av_log_set_level(AV_LOG_INFO); av_log_set_level(AV_LOG_INFO);
av_packet_free(&m_Pkt);
} }
IFFmpegRenderer* FFmpegVideoDecoder::getBackendRenderer() IFFmpegRenderer* FFmpegVideoDecoder::getBackendRenderer()
@@ -192,17 +193,22 @@ void FFmpegVideoDecoder::reset()
} }
} }
bool FFmpegVideoDecoder::createFrontendRenderer(PDECODER_PARAMETERS params) bool FFmpegVideoDecoder::createFrontendRenderer(PDECODER_PARAMETERS params, bool eglOnly)
{ {
if (eglOnly) {
#ifdef HAVE_EGL #ifdef HAVE_EGL
if (m_BackendRenderer->canExportEGL()) { if (m_BackendRenderer->canExportEGL()) {
m_FrontendRenderer = new EGLRenderer(m_BackendRenderer); m_FrontendRenderer = new EGLRenderer(m_BackendRenderer);
if (m_FrontendRenderer->initialize(params)) { if (m_FrontendRenderer->initialize(params)) {
return true; return true;
}
delete m_FrontendRenderer;
m_FrontendRenderer = nullptr;
} }
delete m_FrontendRenderer;
}
#endif #endif
// If we made it here, we failed to create the EGLRenderer
return false;
}
if (m_BackendRenderer->isDirectRenderingSupported()) { if (m_BackendRenderer->isDirectRenderingSupported()) {
// The backend renderer can render to the display // The backend renderer can render to the display
@@ -220,13 +226,13 @@ bool FFmpegVideoDecoder::createFrontendRenderer(PDECODER_PARAMETERS params)
return true; return true;
} }
bool FFmpegVideoDecoder::completeInitialization(AVCodec* decoder, PDECODER_PARAMETERS params, bool testFrame) bool FFmpegVideoDecoder::completeInitialization(const AVCodec* decoder, PDECODER_PARAMETERS params, bool testFrame, bool eglOnly)
{ {
// In test-only mode, we should only see test frames // In test-only mode, we should only see test frames
SDL_assert(!m_TestOnly || testFrame); SDL_assert(!m_TestOnly || testFrame);
// Create the frontend renderer based on the capabilities of the backend renderer // Create the frontend renderer based on the capabilities of the backend renderer
if (!createFrontendRenderer(params)) { if (!createFrontendRenderer(params, eglOnly)) {
return false; return false;
} }
@@ -311,16 +317,16 @@ bool FFmpegVideoDecoder::completeInitialization(AVCodec* decoder, PDECODER_PARAM
if (testFrame) { if (testFrame) {
switch (params->videoFormat) { switch (params->videoFormat) {
case VIDEO_FORMAT_H264: case VIDEO_FORMAT_H264:
m_Pkt.data = (uint8_t*)k_H264TestFrame; m_Pkt->data = (uint8_t*)k_H264TestFrame;
m_Pkt.size = sizeof(k_H264TestFrame); m_Pkt->size = sizeof(k_H264TestFrame);
break; break;
case VIDEO_FORMAT_H265: case VIDEO_FORMAT_H265:
m_Pkt.data = (uint8_t*)k_HEVCMainTestFrame; m_Pkt->data = (uint8_t*)k_HEVCMainTestFrame;
m_Pkt.size = sizeof(k_HEVCMainTestFrame); m_Pkt->size = sizeof(k_HEVCMainTestFrame);
break; break;
case VIDEO_FORMAT_H265_MAIN10: case VIDEO_FORMAT_H265_MAIN10:
m_Pkt.data = (uint8_t*)k_HEVCMain10TestFrame; m_Pkt->data = (uint8_t*)k_HEVCMain10TestFrame;
m_Pkt.size = sizeof(k_HEVCMain10TestFrame); m_Pkt->size = sizeof(k_HEVCMain10TestFrame);
break; break;
default: default:
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
@@ -341,7 +347,7 @@ bool FFmpegVideoDecoder::completeInitialization(AVCodec* decoder, PDECODER_PARAM
for (int retries = 0; retries < 5; retries++) { for (int retries = 0; retries < 5; retries++) {
// Most FFmpeg decoders process input using a "push" model. // Most FFmpeg decoders process input using a "push" model.
// We'll see those fail here if the format is not supported. // We'll see those fail here if the format is not supported.
err = avcodec_send_packet(m_VideoDecoderCtx, &m_Pkt); err = avcodec_send_packet(m_VideoDecoderCtx, m_Pkt);
if (err < 0) { if (err < 0) {
av_frame_free(&frame); av_frame_free(&frame);
char errorstring[512]; char errorstring[512];
@@ -364,6 +370,14 @@ bool FFmpegVideoDecoder::completeInitialization(AVCodec* decoder, PDECODER_PARAM
} }
} }
// Allow the renderer to do any validation it wants on this frame
if (!m_FrontendRenderer->testRenderFrame(frame)) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"Render test failed");
av_frame_free(&frame);
return false;
}
av_frame_free(&frame); av_frame_free(&frame);
if (err < 0) { if (err < 0) {
char errorstring[512]; char errorstring[512];
@@ -404,6 +418,16 @@ void FFmpegVideoDecoder::addVideoStats(VIDEO_STATS& src, VIDEO_STATS& dst)
dst.totalPacerTime += src.totalPacerTime; dst.totalPacerTime += src.totalPacerTime;
dst.totalRenderTime += src.totalRenderTime; dst.totalRenderTime += src.totalRenderTime;
if (!LiGetEstimatedRttInfo(&dst.lastRtt, &dst.lastRttVariance)) {
dst.lastRtt = 0;
dst.lastRttVariance = 0;
}
else {
// Our logic to determine if RTT is valid depends on us never
// getting an RTT of 0. ENet currently ensures RTTs are >= 1.
SDL_assert(dst.lastRtt > 0);
}
Uint32 now = SDL_GetTicks(); Uint32 now = SDL_GetTicks();
// Initialize the measurement start point if this is the first video stat window // Initialize the measurement start point if this is the first video stat window
@@ -468,16 +492,25 @@ void FFmpegVideoDecoder::stringifyVideoStats(VIDEO_STATS& stats, char* output)
} }
if (stats.renderedFrames != 0) { if (stats.renderedFrames != 0) {
char rttString[32];
if (stats.lastRtt != 0) {
sprintf(rttString, "%u ms (variance: %u ms)", stats.lastRtt, stats.lastRttVariance);
}
else {
sprintf(rttString, "N/A");
}
offset += sprintf(&output[offset], offset += sprintf(&output[offset],
"Frames dropped by your network connection: %.2f%%\n" "Frames dropped by your network connection: %.2f%%\n"
"Frames dropped due to network jitter: %.2f%%\n" "Frames dropped due to network jitter: %.2f%%\n"
"Average receive time: %.2f ms\n" "Average network latency: %s\n"
"Average decoding time: %.2f ms\n" "Average decoding time: %.2f ms\n"
"Average frame queue delay: %.2f ms\n" "Average frame queue delay: %.2f ms\n"
"Average rendering time (including monitor V-sync latency): %.2f ms\n", "Average rendering time (including monitor V-sync latency): %.2f ms\n",
(float)stats.networkDroppedFrames / stats.totalFrames * 100, (float)stats.networkDroppedFrames / stats.totalFrames * 100,
(float)stats.pacerDroppedFrames / stats.decodedFrames * 100, (float)stats.pacerDroppedFrames / stats.decodedFrames * 100,
(float)stats.totalReassemblyTime / stats.receivedFrames, rttString,
(float)stats.totalDecodeTime / stats.decodedFrames, (float)stats.totalDecodeTime / stats.decodedFrames,
(float)stats.totalPacerTime / stats.renderedFrames, (float)stats.totalPacerTime / stats.renderedFrames,
(float)stats.totalRenderTime / stats.renderedFrames); (float)stats.totalRenderTime / stats.renderedFrames);
@@ -522,6 +555,10 @@ IFFmpegRenderer* FFmpegVideoDecoder::createHwAccelRenderer(const AVCodecHWConfig
#ifdef HAVE_LIBVDPAU #ifdef HAVE_LIBVDPAU
case AV_HWDEVICE_TYPE_VDPAU: case AV_HWDEVICE_TYPE_VDPAU:
return new VDPAURenderer(); return new VDPAURenderer();
#endif
#ifdef HAVE_DRM
case AV_HWDEVICE_TYPE_DRM:
return new DrmRenderer();
#endif #endif
default: default:
return nullptr; return nullptr;
@@ -545,47 +582,140 @@ IFFmpegRenderer* FFmpegVideoDecoder::createHwAccelRenderer(const AVCodecHWConfig
} }
} }
bool FFmpegVideoDecoder::tryInitializeRenderer(AVCodec* decoder, bool FFmpegVideoDecoder::tryInitializeRenderer(const AVCodec* decoder,
PDECODER_PARAMETERS params, PDECODER_PARAMETERS params,
const AVCodecHWConfig* hwConfig, const AVCodecHWConfig* hwConfig,
std::function<IFFmpegRenderer*()> createRendererFunc) std::function<IFFmpegRenderer*()> createRendererFunc)
{ {
m_BackendRenderer = createRendererFunc();
m_HwDecodeCfg = hwConfig; m_HwDecodeCfg = hwConfig;
if (m_BackendRenderer != nullptr && // i == 0 - Indirect via EGL frontend with zero-copy DMA-BUF passing
m_BackendRenderer->initialize(params) && // i == 1 - Direct rendering or indirect via SDL read-back
completeInitialization(decoder, params, m_TestOnly || m_BackendRenderer->needsTestFrame())) { #ifdef HAVE_EGL
if (m_TestOnly) { for (int i = 0; i < 2; i++) {
// This decoder is only for testing capabilities, so don't bother #else
// creating a usable renderer for (int i = 1; i < 2; i++) {
return true; #endif
} SDL_assert(m_BackendRenderer == nullptr);
if ((m_BackendRenderer = createRendererFunc()) != nullptr &&
if (m_BackendRenderer->needsTestFrame()) { m_BackendRenderer->initialize(params) &&
// The test worked, so now let's initialize it for real completeInitialization(decoder, params, m_TestOnly || m_BackendRenderer->needsTestFrame(), i == 0 /* EGL */)) {
reset(); if (m_TestOnly) {
if ((m_BackendRenderer = createRendererFunc()) != nullptr && // This decoder is only for testing capabilities, so don't bother
m_BackendRenderer->initialize(params) && // creating a usable renderer
completeInitialization(decoder, params, false)) {
return true; return true;
} }
else {
SDL_LogCritical(SDL_LOG_CATEGORY_APPLICATION, if (m_BackendRenderer->needsTestFrame()) {
"Decoder failed to initialize after successful test"); // The test worked, so now let's initialize it for real
reset(); reset();
if ((m_BackendRenderer = createRendererFunc()) != nullptr &&
m_BackendRenderer->initialize(params) &&
completeInitialization(decoder, params, false, i == 0 /* EGL */)) {
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 { else {
// No test required. Good to go now. // Failed to initialize, so keep looking
reset();
}
}
// reset() must be called before we reach this point!
SDL_assert(m_BackendRenderer == nullptr);
return false;
}
#define TRY_PREFERRED_PIXEL_FORMAT(RENDERER_TYPE) \
{ \
RENDERER_TYPE renderer; \
if (renderer.getPreferredPixelFormat(params->videoFormat) == decoder->pix_fmts[i]) { \
if (tryInitializeRenderer(decoder, params, nullptr, \
[]() -> IFFmpegRenderer* { return new RENDERER_TYPE(); })) { \
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, \
"Chose " #RENDERER_TYPE " for codec %s due to preferred pixel format: 0x%x", \
decoder->name, decoder->pix_fmts[i]); \
return true; \
} \
} \
}
#define TRY_SUPPORTED_PIXEL_FORMAT(RENDERER_TYPE) \
{ \
RENDERER_TYPE renderer; \
if (renderer.isPixelFormatSupported(params->videoFormat, decoder->pix_fmts[i])) { \
if (tryInitializeRenderer(decoder, params, nullptr, \
[]() -> IFFmpegRenderer* { return new RENDERER_TYPE(); })) { \
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, \
"Chose " #RENDERER_TYPE " for codec %s due to compatible pixel format: 0x%x", \
decoder->name, decoder->pix_fmts[i]); \
return true; \
} \
} \
}
bool FFmpegVideoDecoder::tryInitializeRendererForDecoderByName(const char *decoderName,
PDECODER_PARAMETERS params)
{
const AVCodec* decoder = avcodec_find_decoder_by_name(decoderName);
if (decoder == nullptr) {
return false;
}
// This might be a hwaccel decoder, so try any hw configs first
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; return true;
} }
} }
else {
// Failed to initialize or test frame failed, so keep looking if (decoder->pix_fmts == NULL) {
reset(); // Supported output pixel formats are unknown. We'll just try SDL and hope it can cope.
return tryInitializeRenderer(decoder, params, nullptr,
[]() -> IFFmpegRenderer* { return new SdlRenderer(); });
} }
// Check if any of our decoders prefer any of the pixel formats first
for (int i = 0; decoder->pix_fmts[i] != AV_PIX_FMT_NONE; i++) {
#ifdef HAVE_DRM
TRY_PREFERRED_PIXEL_FORMAT(DrmRenderer);
#endif
#ifdef HAVE_MMAL
TRY_PREFERRED_PIXEL_FORMAT(MmalRenderer);
#endif
TRY_PREFERRED_PIXEL_FORMAT(SdlRenderer);
}
// Nothing prefers any of them. Let's see if anyone will tolerate one.
for (int i = 0; decoder->pix_fmts[i] != AV_PIX_FMT_NONE; i++) {
#ifdef HAVE_DRM
TRY_SUPPORTED_PIXEL_FORMAT(DrmRenderer);
#endif
#ifdef HAVE_MMAL
TRY_SUPPORTED_PIXEL_FORMAT(MmalRenderer);
#endif
TRY_SUPPORTED_PIXEL_FORMAT(SdlRenderer);
}
// If we made it here, we couldn't find anything
return false; return false;
} }
@@ -595,21 +725,18 @@ bool FFmpegVideoDecoder::initialize(PDECODER_PARAMETERS params)
av_log_set_level(AV_LOG_DEBUG); av_log_set_level(AV_LOG_DEBUG);
// First try decoders that the user has manually specified via environment variables. // 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, // These must output surfaces in one of the formats that one of our renderers supports,
// which is currently: // which is currently:
// - AV_PIX_FMT_YUV420P (preferred) // - AV_PIX_FMT_DRM_PRIME
// - AV_PIX_FMT_MMAL
// - AV_PIX_FMT_YUV420P
// - AV_PIX_FMT_NV12 // - AV_PIX_FMT_NV12
// - AV_PIX_FMT_NV21 // - AV_PIX_FMT_NV21
// These formats should cover most/all decoders that output in a standard YUV format.
{ {
QString h264DecoderHint = qgetenv("H264_DECODER_HINT"); QString h264DecoderHint = qgetenv("H264_DECODER_HINT");
if (!h264DecoderHint.isEmpty() && (params->videoFormat & VIDEO_FORMAT_MASK_H264)) { if (!h264DecoderHint.isEmpty() && (params->videoFormat & VIDEO_FORMAT_MASK_H264)) {
QByteArray decoderString = h264DecoderHint.toLocal8Bit(); QByteArray decoderString = h264DecoderHint.toLocal8Bit();
AVCodec* customAvcDecoder = avcodec_find_decoder_by_name(decoderString.constData()); if (tryInitializeRendererForDecoderByName(decoderString.constData(), params)) {
if (customAvcDecoder != nullptr &&
tryInitializeRenderer(customAvcDecoder, params, nullptr,
[]() -> IFFmpegRenderer* { return new SdlRenderer(); })) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"Using custom H.264 decoder (H264_DECODER_HINT): %s", "Using custom H.264 decoder (H264_DECODER_HINT): %s",
decoderString.constData()); decoderString.constData());
@@ -626,11 +753,7 @@ bool FFmpegVideoDecoder::initialize(PDECODER_PARAMETERS params)
QString hevcDecoderHint = qgetenv("HEVC_DECODER_HINT"); QString hevcDecoderHint = qgetenv("HEVC_DECODER_HINT");
if (!hevcDecoderHint.isEmpty() && (params->videoFormat & VIDEO_FORMAT_MASK_H265)) { if (!hevcDecoderHint.isEmpty() && (params->videoFormat & VIDEO_FORMAT_MASK_H265)) {
QByteArray decoderString = hevcDecoderHint.toLocal8Bit(); QByteArray decoderString = hevcDecoderHint.toLocal8Bit();
AVCodec* customHevcDecoder = avcodec_find_decoder_by_name(decoderString.constData()); if (tryInitializeRendererForDecoderByName(decoderString.constData(), params)) {
if (customHevcDecoder != nullptr &&
tryInitializeRenderer(customHevcDecoder, params, nullptr,
[]() -> IFFmpegRenderer* { return new SdlRenderer(); })) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"Using custom HEVC decoder (HEVC_DECODER_HINT): %s", "Using custom HEVC decoder (HEVC_DECODER_HINT): %s",
decoderString.constData()); decoderString.constData());
@@ -644,7 +767,7 @@ bool FFmpegVideoDecoder::initialize(PDECODER_PARAMETERS params)
} }
} }
AVCodec* decoder; const AVCodec* decoder;
if (params->videoFormat & VIDEO_FORMAT_MASK_H264) { if (params->videoFormat & VIDEO_FORMAT_MASK_H264) {
decoder = avcodec_find_decoder(AV_CODEC_ID_H264); decoder = avcodec_find_decoder(AV_CODEC_ID_H264);
@@ -682,74 +805,22 @@ bool FFmpegVideoDecoder::initialize(PDECODER_PARAMETERS params)
} }
// Continue with special non-hwaccel hardware decoders // 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) { if (params->videoFormat & VIDEO_FORMAT_MASK_H264) {
AVCodec* mmalDecoder = avcodec_find_decoder_by_name("h264_mmal"); QList<const char *> knownAvcCodecs = { "h264_mmal", "h264_rkmpp", "h264_nvmpi", "h264_v4l2m2m" };
if (mmalDecoder != nullptr && for (const char* codec : knownAvcCodecs) {
tryInitializeRenderer(mmalDecoder, params, nullptr, if (tryInitializeRendererForDecoderByName(codec, params)) {
[]() -> IFFmpegRenderer* { return new MmalRenderer(); })) { return true;
return true; }
} }
} }
#endif else {
QList<const char *> knownHevcCodecs = { "hevc_rkmpp", "hevc_nvmpi", "hevc_v4l2m2m" };
#ifdef HAVE_DRM for (const char* codec : knownHevcCodecs) {
{ if (tryInitializeRendererForDecoderByName(codec, params)) {
// RKMPP is a hardware accelerated decoder that outputs DRI PRIME buffers return true;
AVCodec* rkmppDecoder; }
if (params->videoFormat & VIDEO_FORMAT_MASK_H264) {
rkmppDecoder = avcodec_find_decoder_by_name("h264_rkmpp");
}
else {
rkmppDecoder = avcodec_find_decoder_by_name("hevc_rkmpp");
}
if (rkmppDecoder != nullptr &&
tryInitializeRenderer(rkmppDecoder, params, nullptr,
[]() -> IFFmpegRenderer* { return new DrmRenderer(); })) {
return true;
} }
} }
#endif
#ifdef Q_OS_LINUX
{
AVCodec* nvmpiDecoder;
if (params->videoFormat & VIDEO_FORMAT_MASK_H264) {
nvmpiDecoder = avcodec_find_decoder_by_name("h264_nvmpi");
}
else {
nvmpiDecoder = avcodec_find_decoder_by_name("hevc_nvmpi");
}
if (nvmpiDecoder != nullptr &&
tryInitializeRenderer(nvmpiDecoder, params, nullptr,
[]() -> IFFmpegRenderer* { return new SdlRenderer(); })) {
return true;
}
}
{
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) // Look for the first matching hwaccel hardware decoder (pass 1)
// This picks up "second-tier" hwaccels like CUDA. // This picks up "second-tier" hwaccels like CUDA.
@@ -881,19 +952,19 @@ int FFmpegVideoDecoder::submitDecodeUnit(PDECODE_UNIT du)
entry = entry->next; entry = entry->next;
} }
m_Pkt.data = reinterpret_cast<uint8_t*>(m_DecodeBuffer.data()); m_Pkt->data = reinterpret_cast<uint8_t*>(m_DecodeBuffer.data());
m_Pkt.size = offset; m_Pkt->size = offset;
if (du->frameType == FRAME_TYPE_IDR) { if (du->frameType == FRAME_TYPE_IDR) {
m_Pkt.flags = AV_PKT_FLAG_KEY; m_Pkt->flags = AV_PKT_FLAG_KEY;
} }
else { else {
m_Pkt.flags = 0; m_Pkt->flags = 0;
} }
m_ActiveWndVideoStats.totalReassemblyTime += du->enqueueTimeMs - du->receiveTimeMs; m_ActiveWndVideoStats.totalReassemblyTime += du->enqueueTimeMs - du->receiveTimeMs;
err = avcodec_send_packet(m_VideoDecoderCtx, &m_Pkt); err = avcodec_send_packet(m_VideoDecoderCtx, m_Pkt);
if (err < 0) { if (err < 0) {
char errorstring[512]; char errorstring[512];
av_strerror(err, errorstring, sizeof(errorstring)); av_strerror(err, errorstring, sizeof(errorstring));
+7 -4
View File
@@ -26,7 +26,7 @@ public:
virtual IFFmpegRenderer* getBackendRenderer(); virtual IFFmpegRenderer* getBackendRenderer();
private: private:
bool completeInitialization(AVCodec* decoder, PDECODER_PARAMETERS params, bool testFrame); bool completeInitialization(const AVCodec* decoder, PDECODER_PARAMETERS params, bool testFrame, bool eglOnly);
void stringifyVideoStats(VIDEO_STATS& stats, char* output); void stringifyVideoStats(VIDEO_STATS& stats, char* output);
@@ -34,9 +34,12 @@ private:
void addVideoStats(VIDEO_STATS& src, VIDEO_STATS& dst); void addVideoStats(VIDEO_STATS& src, VIDEO_STATS& dst);
bool createFrontendRenderer(PDECODER_PARAMETERS params); bool createFrontendRenderer(PDECODER_PARAMETERS params, bool eglOnly);
bool tryInitializeRenderer(AVCodec* decoder, bool tryInitializeRendererForDecoderByName(const char* decoderName,
PDECODER_PARAMETERS params);
bool tryInitializeRenderer(const AVCodec* decoder,
PDECODER_PARAMETERS params, PDECODER_PARAMETERS params,
const AVCodecHWConfig* hwConfig, const AVCodecHWConfig* hwConfig,
std::function<IFFmpegRenderer*()> createRendererFunc); std::function<IFFmpegRenderer*()> createRendererFunc);
@@ -51,7 +54,7 @@ private:
enum AVPixelFormat ffGetFormat(AVCodecContext* context, enum AVPixelFormat ffGetFormat(AVCodecContext* context,
const enum AVPixelFormat* pixFmts); const enum AVPixelFormat* pixFmts);
AVPacket m_Pkt; AVPacket* m_Pkt;
AVCodecContext* m_VideoDecoderCtx; AVCodecContext* m_VideoDecoderCtx;
QByteArray m_DecodeBuffer; QByteArray m_DecodeBuffer;
const AVCodecHWConfig* m_HwDecodeCfg; const AVCodecHWConfig* m_HwDecodeCfg;
+1
View File
@@ -7,4 +7,5 @@ namespace WMUtils {
bool isRunningX11(); bool isRunningX11();
bool isRunningWayland(); bool isRunningWayland();
bool isRunningWindowManager(); bool isRunningWindowManager();
bool isRunningDesktopEnvironment();
} }
+1 -1
View File
@@ -1 +1 @@
3.0.0 3.1.4
+58 -8
View File
@@ -2,6 +2,8 @@
#include "utils.h" #include "utils.h"
#include <SDL.h>
#ifdef HAS_X11 #ifdef HAS_X11
#include <X11/Xlib.h> #include <X11/Xlib.h>
#endif #endif
@@ -10,14 +12,30 @@
#include <wayland-client.h> #include <wayland-client.h>
#endif #endif
#define VALUE_SET 0x01
#define VALUE_TRUE 0x02
bool WMUtils::isRunningX11() bool WMUtils::isRunningX11()
{ {
#ifdef HAS_X11 #ifdef HAS_X11
Display* display = XOpenDisplay(nullptr); static SDL_atomic_t isRunningOnX11;
if (display != nullptr) {
XCloseDisplay(display); // If the value is not set yet, populate it now.
return true; int val = SDL_AtomicGet(&isRunningOnX11);
if (!(val & VALUE_SET)) {
Display* display = XOpenDisplay(nullptr);
if (display != nullptr) {
XCloseDisplay(display);
}
// Populate the value to return and have for next time.
// This can race with another thread populating the same data,
// but that's no big deal.
val = VALUE_SET | ((display != nullptr) ? VALUE_TRUE : 0);
SDL_AtomicSet(&isRunningOnX11, val);
} }
return !!(val & VALUE_TRUE);
#endif #endif
return false; return false;
@@ -26,11 +44,24 @@ bool WMUtils::isRunningX11()
bool WMUtils::isRunningWayland() bool WMUtils::isRunningWayland()
{ {
#ifdef HAS_WAYLAND #ifdef HAS_WAYLAND
struct wl_display* display = wl_display_connect(nullptr); static SDL_atomic_t isRunningOnWayland;
if (display != nullptr) {
wl_display_disconnect(display); // If the value is not set yet, populate it now.
return true; int val = SDL_AtomicGet(&isRunningOnWayland);
if (!(val & VALUE_SET)) {
struct wl_display* display = wl_display_connect(nullptr);
if (display != nullptr) {
wl_display_disconnect(display);
}
// Populate the value to return and have for next time.
// This can race with another thread populating the same data,
// but that's no big deal.
val = VALUE_SET | ((display != nullptr) ? VALUE_TRUE : 0);
SDL_AtomicSet(&isRunningOnWayland, val);
} }
return !!(val & VALUE_TRUE);
#endif #endif
return false; return false;
@@ -46,3 +77,22 @@ bool WMUtils::isRunningWindowManager()
return WMUtils::isRunningWayland() || WMUtils::isRunningX11(); return WMUtils::isRunningWayland() || WMUtils::isRunningX11();
#endif #endif
} }
bool WMUtils::isRunningDesktopEnvironment()
{
if (qEnvironmentVariableIsSet("HAS_DESKTOP_ENVIRONMENT")) {
return qEnvironmentVariableIntValue("HAS_DESKTOP_ENVIRONMENT");
}
#if defined(Q_OS_WIN) || defined(Q_OS_DARWIN)
// Windows and macOS are always running a desktop environment
return true;
#elif defined(EMBEDDED_BUILD)
// Embedded systems don't run desktop environments
return false;
#else
// On non-embedded systems, assume we have a desktop environment
// if we have a WM running.
return isRunningWindowManager();
#endif
}
+11 -11
View File
@@ -10,29 +10,29 @@ environment:
BUILD_TARGET: macos BUILD_TARGET: macos
- APPVEYOR_BUILD_WORKER_IMAGE: Ubuntu1604 - APPVEYOR_BUILD_WORKER_IMAGE: Ubuntu1604
BUILD_TARGET: steamlink BUILD_TARGET: steamlink
- APPVEYOR_BUILD_WORKER_IMAGE: Ubuntu1604 - APPVEYOR_BUILD_WORKER_IMAGE: Ubuntu
BUILD_TARGET: linux BUILD_TARGET: linux
FFMPEG_CONFIGURE_ARGS: --enable-pic --disable-static --enable-shared --disable-all --enable-avcodec --enable-decoder=h264 --enable-decoder=hevc --enable-nvdec --enable-hwaccel=h264_nvdec --enable-hwaccel=hevc_nvdec --enable-hwaccel=h264_vaapi --enable-hwaccel=hevc_vaapi --enable-hwaccel=h264_vdpau --enable-hwaccel=hevc_vdpau FFMPEG_CONFIGURE_ARGS: --enable-pic --disable-static --enable-shared --disable-all --enable-avcodec --enable-decoder=h264 --enable-decoder=hevc --enable-nvdec --enable-hwaccel=h264_nvdec --enable-hwaccel=hevc_nvdec --enable-hwaccel=h264_vaapi --enable-hwaccel=hevc_vaapi --enable-hwaccel=h264_vdpau --enable-hwaccel=hevc_vdpau
install: install:
- sh: '[ "$BUILD_TARGET" != macos ] || nvm use node' - sh: '[ "$BUILD_TARGET" != macos ] || nvm use node'
- sh: '[ "$BUILD_TARGET" != macos ] || npm install --global create-dmg' - sh: '[ "$BUILD_TARGET" != macos ] || npm install --global create-dmg'
- sh: '[ "$BUILD_TARGET" != macos ] || brew install qt'
- sh: '[ "$BUILD_TARGET" != macos ] || brew link --force qt'
- sh: '[ "$BUILD_TARGET" != steamlink ] || sudo apt install -y libc6:i386 libstdc++6:i386' - sh: '[ "$BUILD_TARGET" != steamlink ] || sudo apt install -y libc6:i386 libstdc++6:i386'
- sh: '[ "$BUILD_TARGET" != steamlink ] || git clone --depth=1 https://github.com/ValveSoftware/steamlink-sdk.git $HOME/steamlink-sdk' - sh: '[ "$BUILD_TARGET" != steamlink ] || git clone --depth=1 https://github.com/ValveSoftware/steamlink-sdk.git $HOME/steamlink-sdk'
- sh: '[ "$BUILD_TARGET" != linux ] || sudo add-apt-repository ppa:beineri/opt-qt-5.14.2-xenial' - sh: '[ "$BUILD_TARGET" != linux ] || sudo add-apt-repository ppa:beineri/opt-qt-5.15.2-bionic'
- sh: '[ "$BUILD_TARGET" != linux ] || sudo apt update' - sh: '[ "$BUILD_TARGET" != linux ] || sudo apt update || true'
- sh: '[ "$BUILD_TARGET" != linux ] || sudo apt install -y qt514base qt514quickcontrols2 qt514svg qt514wayland nasm libgbm-dev libdrm-dev libfreetype6-dev libasound2-dev libdbus-1-dev libegl1-mesa-dev libgl1-mesa-dev libgles2-mesa-dev libglu1-mesa-dev libibus-1.0-dev libpulse-dev libudev-dev libx11-dev libxcursor-dev libxext-dev libxi-dev libxinerama-dev libxkbcommon-dev libxrandr-dev libxss-dev libxt-dev libxv-dev libxxf86vm-dev wayland-protocols libopus-dev libvdpau-dev' - sh: '[ "$BUILD_TARGET" != linux ] || sudo apt install -y qt515base qt515quickcontrols2 qt515svg qt515wayland nasm libgbm-dev libdrm-dev libfreetype6-dev libasound2-dev libdbus-1-dev libegl1-mesa-dev libgl1-mesa-dev libgles2-mesa-dev libglu1-mesa-dev libibus-1.0-dev libpulse-dev libudev-dev libx11-dev libxcursor-dev libxext-dev libxi-dev libxinerama-dev libxkbcommon-dev libxrandr-dev libxss-dev libxt-dev libxv-dev libxxf86vm-dev wayland-protocols libopus-dev libvdpau-dev'
- sh: 'if [[ "$BUILD_TARGET" = linux ]]; then export SDL2_VER=2.0.14 && 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$(nproc) && sudo make install && cd ..; fi' - sh: 'if [[ "$BUILD_TARGET" = linux ]]; then export SDL2_REV=dfa64eadd38a6a8125290dcf121f1bc285e98dad && git clone https://github.com/libsdl-org/SDL.git SDL2 && cd SDL2 && git checkout $SDL2_REV && ./configure --enable-video-kmsdrm && make -j$(nproc) && sudo make install && cd ..; fi'
- sh: 'if [[ "$BUILD_TARGET" = 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$(nproc) && sudo make install && cd ..; fi' - sh: 'if [[ "$BUILD_TARGET" = 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$(nproc) && sudo make install && cd ..; fi'
- sh: 'if [[ "$BUILD_TARGET" = linux ]]; then export LIBVA_VER=2.10.0 && wget https://github.com/intel/libva/releases/download/$LIBVA_VER/libva-$LIBVA_VER.tar.bz2 && tar -xvf libva-$LIBVA_VER.tar.bz2 && cd libva-$LIBVA_VER && ./configure && make -j$(nproc) && sudo make install && cd ..; fi' - sh: 'if [[ "$BUILD_TARGET" = linux ]]; then export LIBVA_VER=2.12.0 && git clone https://github.com/intel/libva.git && cd libva && git checkout $LIBVA_VER && ./autogen.sh && ./configure && make -j$(nproc) && sudo make install && cd ..; fi'
- sh: 'if [[ "$BUILD_TARGET" = linux ]]; then export NVHDR_VER=8.1.24.12 && wget https://github.com/FFmpeg/nv-codec-headers/releases/download/n$NVHDR_VER/nv-codec-headers-$NVHDR_VER.tar.gz && tar -xvf nv-codec-headers-$NVHDR_VER.tar.gz && cd nv-codec-headers-$NVHDR_VER && sudo make install && cd ..; fi' - sh: 'if [[ "$BUILD_TARGET" = linux ]]; then export NVHDR_VER=8.1.24.12 && wget https://github.com/FFmpeg/nv-codec-headers/releases/download/n$NVHDR_VER/nv-codec-headers-$NVHDR_VER.tar.gz && tar -xvf nv-codec-headers-$NVHDR_VER.tar.gz && cd nv-codec-headers-$NVHDR_VER && sudo make install && cd ..; fi'
- sh: 'if [[ "$BUILD_TARGET" = linux ]]; then export FFMPEG_VER=4.3.1 && wget https://ffmpeg.org/releases/ffmpeg-$FFMPEG_VER.tar.bz2 && tar -xvf ffmpeg-$FFMPEG_VER.tar.bz2 && cd ffmpeg-$FFMPEG_VER && ./configure $FFMPEG_CONFIGURE_ARGS && make -j$(nproc) && sudo make install && sudo ldconfig && cd ..; fi' - sh: 'if [[ "$BUILD_TARGET" = linux ]]; then export FFMPEG_VER=4.3.1 && wget https://ffmpeg.org/releases/ffmpeg-$FFMPEG_VER.tar.bz2 && tar -xvf ffmpeg-$FFMPEG_VER.tar.bz2 && cd ffmpeg-$FFMPEG_VER && ./configure $FFMPEG_CONFIGURE_ARGS && make -j$(nproc) && sudo make install && sudo ldconfig && cd ..; fi'
- sh: 'if [[ "$BUILD_TARGET" = linux ]]; then mkdir $HOME/bin && wget -O $HOME/bin/linuxdeployqt https://github.com/probonopd/linuxdeployqt/releases/download/continuous/linuxdeployqt-continuous-x86_64.AppImage && chmod a+x $HOME/bin/linuxdeployqt; fi' - sh: 'if [[ "$BUILD_TARGET" = linux ]]; then mkdir $HOME/bin && wget -O $HOME/bin/linuxdeployqt https://github.com/probonopd/linuxdeployqt/releases/download/continuous/linuxdeployqt-continuous-x86_64.AppImage && chmod a+x $HOME/bin/linuxdeployqt; fi'
before_build: before_build:
- 'git submodule update --init --recursive' - 'git -c submodule.libs.update=none submodule update --init --recursive'
- cmd: 'git submodule update --init --recursive'
- sh: '[ "$BUILD_TARGET" != macos ] || git submodule update --init --recursive'
- cmd: 'set OLDPATH=%PATH%' - cmd: 'set OLDPATH=%PATH%'
- cmd: 'set /p VERSION=<app\version.txt' - cmd: 'set /p VERSION=<app\version.txt'
- sh: 'export VERSION=`cat app/version.txt`' - sh: 'export VERSION=`cat app/version.txt`'
@@ -43,9 +43,9 @@ build_script:
- cmd: 'set PATH=%OLDPATH%;%QTDIR%\msvc2019\bin;%ProgramFiles(x86)%\WiX Toolset v3.11\bin' - cmd: 'set PATH=%OLDPATH%;%QTDIR%\msvc2019\bin;%ProgramFiles(x86)%\WiX Toolset v3.11\bin'
- cmd: 'scripts\build-arch.bat Release x86' - cmd: 'scripts\build-arch.bat Release x86'
- cmd: 'scripts\generate-bundle.bat Release' - cmd: 'scripts\generate-bundle.bat Release'
- sh: '[ "$BUILD_TARGET" != linux ] || source /opt/qt514/bin/qt514-env.sh' - sh: '[ "$BUILD_TARGET" != linux ] || source /opt/qt515/bin/qt515-env.sh'
- sh: '[ "$BUILD_TARGET" != linux ] || PATH=$PATH:$HOME/bin scripts/build-appimage.sh' - sh: '[ "$BUILD_TARGET" != linux ] || PATH=$PATH:$HOME/bin scripts/build-appimage.sh'
- sh: '[ "$BUILD_TARGET" != macos ] || scripts/generate-dmg.sh Release' - sh: '[ "$BUILD_TARGET" != macos ] || PATH=$PATH:$HOME/Qt/5.15/clang_64/bin scripts/generate-dmg.sh Release'
- sh: '[ "$BUILD_TARGET" != steamlink ] || STEAMLINK_SDK_PATH=$HOME/steamlink-sdk scripts/build-steamlink-app.sh' - sh: '[ "$BUILD_TARGET" != steamlink ] || STEAMLINK_SDK_PATH=$HOME/steamlink-sdk scripts/build-steamlink-app.sh'
after_build: after_build:
Submodule
+1
Submodule libs added at c5149747ff.
Loaded 100 of 811 files, more files were not shown because too many files have changed in this diff. Show more