Compare commits

..

292 Commits

Author SHA1 Message Date
loki
ae04c4afbb Gracefully exit when stopped by systemd or pkill 2021-06-30 17:28:58 +02:00
loki
e8fadd2848 Update version for debian package 2021-06-30 15:51:26 +02:00
loki
beb6bdfadb Allow end user to configure what ports to listen on 2021-06-30 15:25:08 +02:00
loki
8bf4ade9d8 Disable UPnP by default 2021-06-30 12:22:37 +02:00
loki
ea928c53b4 Add support for upnp 2021-06-29 22:42:06 +02:00
loki
c1697c8562 add comma's -_- 2021-06-28 21:56:21 +02:00
loki
57c4986f0e Add necessary dependencies for Debian package 2021-06-28 21:52:24 +02:00
loki
0cc7e35ed9 Automatic service discovery for Windows 2021-06-28 21:05:52 +02:00
loki
d6eceaf0dc Fix incorrect instruction in the README 2021-06-26 16:25:46 +02:00
loki
cf7eb14573 Remove redundent thread creation 2021-06-26 15:48:07 +02:00
loki
ed5de34800 Dynamically load avahi libraries if they are available 2021-06-26 15:36:56 +02:00
loki
27d4f6063f refactored publish.cpp from C code to C++ code 2021-06-26 13:32:14 +02:00
loki
62662edc8d Merge with master 2021-06-26 12:40:06 +02:00
loki
b67600962a Fix incorrect scaling for absolute mouse coordinates 2021-06-24 20:53:19 +02:00
loki
1eda45a81a Allow installation of debian package 2021-06-24 20:23:43 +02:00
loki
926e95f527 Fix absolute mouse coordinates with multiple monitors on Windows 2021-06-23 21:51:15 +02:00
loki
029194cb60 Fix incorrect abs mouse coordinates on Linux when scaling to different aspect ratio 2021-06-23 14:05:09 +02:00
loki
7e3abefc2c pass session event objects through safe::mail_t 2021-06-22 22:26:11 +02:00
loki
cf9eb961fc Pass global event objects through mail_t 2021-06-21 21:21:52 +02:00
loki
0a05c28df8 fixed incorrect colors when scaling with software 2021-06-20 16:13:34 +02:00
loki
0034438c9e fixed incorrect colors when scaling with software 2021-06-20 15:58:13 +02:00
loki
2691489dab Merge branch 'vaapi' 2021-06-20 15:30:16 +02:00
loki
9e7ecf8db2 Allow replacement of hevc headers 2021-06-20 15:29:51 +02:00
loki-47-6F-64
32e6054435 Fix incorrect instruction in the README 2021-06-20 15:16:30 +02:00
loki-47-6F-64
bd2d846557 Merge pull request #100 from TheElixZammuto/master
Fix Config Page AMD RC settings
2021-06-18 22:52:07 +02:00
Elia Zammuto
0932d8bab9 Merge pull request #5 from TheElixZammuto/fix-config-page
Fix amd rc selection on config page
2021-06-18 22:44:25 +02:00
Elia Zammuto
82ac3becd8 Fix amd rc selection on config page 2021-06-18 22:43:55 +02:00
loki
7b86ea9e87 temporary workaround for hanging when interrupting application before http server started 2021-06-18 20:02:57 +02:00
loki
63d15333f2 Allow injecting more than one type of header data into video 2021-06-18 17:27:56 +02:00
loki
ac862f9157 bump up version of debian package 2021-06-18 12:59:39 +02:00
loki
23b09e3d41 fix an issue causing free called on unallocated data 2021-06-17 23:26:25 +02:00
loki-47-6F-64
4024378772 Merge pull request #98 from luk1337/master
include FFMPEG_INCLUDE_DIRS for third-party/cbs module
2021-06-16 19:12:57 +02:00
LuK1337
64e1132579 include FFMPEG_INCLUDE_DIRS for third-party/cbs module 2021-06-16 18:32:11 +02:00
loki-47-6F-64
6749b64ed5 Merge pull request #93 from TheElixZammuto/master
Update README to accomodate Web UI Changes
2021-06-16 16:11:03 +02:00
loki
0c43b83598 make config files writable for Web Manager on Linux when installed web debian package 2021-06-16 16:06:36 +02:00
loki
c47e3cb020 add missing file 2021-06-16 15:04:12 +02:00
loki
f3b07efaf3 add advanced option for credentials file 2021-06-16 14:49:51 +02:00
loki
02360f7aef fix build windows 2021-06-16 14:34:05 +02:00
loki
c21301a423 fix storing log level through web manager 2021-06-16 11:38:26 +02:00
loki
64a6c1419b store state in .config/sunshine on Linux 2021-06-16 11:26:54 +02:00
loki
6f95d360b5 fix web manager when installed through deb package 2021-06-15 21:56:32 +02:00
loki
71cef4b700 remove arch specific code 2021-06-15 18:31:40 +02:00
loki
56bb4ca865 removed redundent build dependency on Windows 2021-06-14 21:09:11 +02:00
loki
fa0ff91d23 some minor refactoring 2021-06-14 20:47:30 +02:00
loki
13caabaec8 fix h264 vui params 2021-06-14 17:43:21 +02:00
Loki
8f2e00f31b minor refactoring 2021-06-14 14:54:09 +02:00
Loki
78244a48d2 remove two redundent tests for encoder validation 2021-06-14 14:44:17 +02:00
Loki
8be7c563a9 Fix windows build 2021-06-14 14:34:38 +02:00
loki
3fe134d8a6 attempt to fix ubuntu build 2021-06-13 21:59:10 +02:00
loki
f4c145136c Attempt to compile on ubuntu 20.04 2021-06-13 21:51:49 +02:00
loki
ee10a795bd Attempt to compile on ubuntu 20.04 2021-06-13 21:48:04 +02:00
loki
8e32c8e6f4 Inject VUI data in SPS header if missing 2021-06-13 21:29:32 +02:00
loki
30f7742f51 Test for presence of sps->vui parameters 2021-06-12 23:08:20 +02:00
loki
55ce46e6ed fix windows build 2021-06-12 19:51:58 +02:00
loki
9e03cdda42 constant bitrate for audio 2021-06-12 18:30:24 +02:00
loki
76f34be395 move third-party source files to folder third-party 2021-06-11 11:59:19 +02:00
loki
ee89b211de include sstream in linux/audio.cpp to prevent compilation errors 2021-06-11 10:31:32 +02:00
loki
0c5daa44a1 Fix not responding to key modifiers on moonlight-embedded 2021-06-10 22:44:56 +02:00
loki
5495c8367a Search for active sink, rather than hardware sink 2021-06-10 18:39:16 +02:00
loki
988bf33b40 Fix bug not accepting paired device after reboot 2021-06-10 17:44:40 +02:00
loki
e53f65c305 Remove use shutdown signal inside init function 2021-06-09 22:01:18 +02:00
loki
1afd1b7c94 generate user credentials based on command line iuput 2021-06-09 21:55:13 +02:00
loki
33fbd5f431 Show/Hide config options based on platform and added vaapi config options to UI 2021-06-09 20:40:17 +02:00
loki
17d3fcf0d0 fix adding and editing apps json 2021-06-09 19:12:00 +02:00
loki
cf77b301bc centralize reading/writing file 2021-06-09 12:04:51 +02:00
loki
9d52174d6b fix selecting incorrect monitor on linux 2021-06-09 11:49:31 +02:00
loki
e2874b40c1 unfinished vaapi encoder should not be default yet 2021-06-08 22:38:48 +02:00
loki
914f329eea removed unnessesary header includes 2021-06-08 22:23:39 +02:00
loki
877c739f1b Merge branch 'master' into vaapi 2021-06-08 22:18:27 +02:00
loki
61b195e9f4 some refactoring 2021-06-08 22:10:42 +02:00
loki
c53564dd31 fixed calling terminate instead of gracefully ending 2021-06-08 21:58:32 +02:00
loki
9eb4eadda6 Fix linux build 2021-06-08 10:11:58 +02:00
Loki
44ebc4846b removed useless if statement 2021-06-07 14:27:42 +02:00
Loki
3a3d4928f9 Fix compilation on Windows 2021-06-07 14:22:23 +02:00
loki
84c55d6efc Fix file descriptor leak 2021-06-06 20:57:42 +02:00
loki
7f636a25a8 Look for VAAPI availabillity during runtime 2021-06-06 18:45:45 +02:00
loki
825a706139 Remove build dependency for gbm 2021-06-06 17:19:30 +02:00
loki
4df57a722e Grab whatever function we can 2021-06-06 17:19:11 +02:00
loki
45f9ce3a20 Allow va to have it's own file descriptor for the VAAPI device 2021-06-05 23:19:58 +02:00
loki
f2863cceb7 Properly scale image on VAAPI 2021-06-05 12:25:19 +02:00
loki
6b9ed7fcb3 Add option to select render device for VAAPI 2021-06-05 10:20:36 +02:00
loki
f33a587218 Fix software encoder 2021-06-04 21:35:00 +02:00
loki
bdb9ed9001 Perform image format conversion for VAAPI on the gpu 2021-06-04 21:12:06 +02:00
Elia Zammuto
41911a26fb Update README to accomodate Web UI Changes 2021-06-03 21:27:11 +02:00
loki-47-6F-64
0942f2ee7f Merge pull request #92 from exalented/exalented-pipewire-docs
Add pipewire documentation
2021-06-02 22:41:40 +02:00
exalented
a05a3b355c Update README.md
Add a bit of pipewire documentation in the README.
2021-06-02 11:55:25 -07:00
loki-47-6F-64
ff1722bbd1 Merge pull request #89 from TheElixZammuto/web-ui
Web UI
2021-05-30 17:36:39 +02:00
Elia Zammuto
dc281119af Fix Header Link 2021-05-30 16:46:05 +02:00
Elia Zammuto
b848db8f2b Config Update and PIN POST Method 2021-05-30 16:42:40 +02:00
Elia Zammuto
bc261fddf2 Updated Config Page 2021-05-30 15:56:13 +02:00
Elia Zammuto
bb251d5046 Code Cleanup 2021-05-30 14:58:38 +02:00
Elia Zammuto
ec9481392a Password Change UI 2021-05-29 23:35:44 +02:00
Elia Zammuto
8961b0462e Fix Incldue Order 2021-05-29 23:05:37 +02:00
Elia Zammuto
ffb80c5fc3 Fix Format 2021-05-29 22:29:10 +02:00
Elia Zammuto
4835366a0c Fixed Build 2021-05-29 22:26:17 +02:00
Elia Zammuto
c09855f703 Merge branch 'loki-47-6F-64-master' into web-ui 2021-05-29 22:07:53 +02:00
Elia Zammuto
5761b05f3b Merge branch 'master' of https://github.com/loki-47-6F-64/sunshine into loki-47-6F-64-master 2021-05-29 22:06:28 +02:00
loki
ff1ea1a63e Use VAAPI for hardware encoding on Linux 2021-05-29 16:25:37 +02:00
Elia Zammuto
0ea6363172 Username/Password Authentication for UI 2021-05-28 22:49:27 +02:00
loki
fe5375f17b update debian package version 2021-05-26 21:40:23 +02:00
loki
fd43ebc2bf add build status 2021-05-26 21:33:28 +02:00
loki
113e7a52d4 advertise display modes 2021-05-26 17:34:25 +02:00
loki
9f6b4ed93b Work around weird mapping for audio channels 2021-05-25 18:55:29 +02:00
loki
7aff15f743 Mainain aspect ratio when using nvenc and amdvce 2021-05-24 20:58:05 +02:00
loki
1c4435312e maintain aspect ratio when using software encoder 2021-05-24 18:08:32 +02:00
loki
dcb32eaaf7 Skip error correction when a video frame is to large, the alternative is skipping the frame itself 2021-05-22 23:09:53 +02:00
loki
ba07fd510e Upmix/Downmix audio 2021-05-22 19:51:01 +02:00
loki
2fb5f8a7d0 Add config option for virtual_sink on windows 2021-05-21 14:28:24 +02:00
loki
b119121e10 Change default audio device on Windows 2021-05-21 13:53:12 +02:00
loki
cb101eca16 Merge branch 'master' into surround 2021-05-20 11:02:26 +02:00
loki-47-6F-64
29674b8ed0 Merge pull request #87 from psyke83/hevc_forced_idr
Force IDR frames for libx265 encoder
2021-05-20 09:27:14 +02:00
Conn O'Griofa
c6a0eeef3f Force IDR frames for libx265 encoder
Parameter is needed to avoid infinite black screen with
"Waiting for IDR frame" in moonlight-qt (3.1.3) on Windows.
2021-05-20 00:22:28 +01:00
loki
825efb512a Based on client request, mute the host on Linux 2021-05-19 21:44:42 +02:00
loki
cd870bdcdd Guard against missing parameters 2021-05-19 20:02:03 +02:00
loki
0868d898f6 Create virtual audio sinks on Linux 2021-05-19 18:11:06 +02:00
loki
66a78f0d40 Fix windows build 2021-05-19 09:42:38 +02:00
loki
2b04e1428c Select audio output on Linux 2021-05-18 13:36:12 +02:00
Elia Zammuto
57f444357d Web UI IP Based Authentication 2021-05-17 21:56:55 +02:00
Elia Zammuto
fe08c241ec Fixes 2021-05-17 21:30:03 +02:00
loki
3d8a99f541 clang-format 2021-05-17 21:21:57 +02:00
Elia Zammuto
03236a50e5 UI for Application Config 2021-05-16 20:06:06 +02:00
loki
a6c1649493 Remove redundent code 2021-05-15 17:01:03 +02:00
loki
33a330fd6c Downmix surround 5.1 to stereo 2021-05-14 21:44:20 +02:00
loki
3901e404a9 Add forgotten requirement for compilation on ubuntu 2021-05-14 21:32:46 +02:00
loki
381e8bcfaa Prepare for release 2021-05-13 15:26:08 +02:00
loki
1050978246 Add warning to ignore errors during encoder validation 2021-05-12 23:22:13 +02:00
loki
9e48e58221 Allow applications started by sunshine to be detached 2021-05-12 23:01:30 +02:00
loki
5d313b509e fix resetting mouse buttons on end stream 2021-05-12 15:48:39 +02:00
loki
022b2202f6 Absolute mouse coordinates on Windows 2021-05-12 14:42:10 +02:00
loki
2e9a1cfbba absolute mouse coordinates regardless of the number of monitors attached on Linux 2021-05-11 23:51:45 +02:00
Elia Zammuto
27a1144217 Moved Common HTTPS Initialization Logic in a common file 2021-05-11 23:38:45 +02:00
loki
1d84c8f9ce Correct dimensions for touchscreen when single monitor attached 2021-05-11 23:30:56 +02:00
Elia Zammuto
04421d84a3 Fix Indentations and Shutdown Handling 2021-05-11 22:19:29 +02:00
loki
92cd8648fa Some refactoring for linux/display.cpp 2021-05-11 20:10:33 +02:00
loki
41cc9a3e80 absolute mouse coordinate support for single monitor on Linux 2021-05-11 18:01:56 +02:00
Loki
b97c902d10 print absolute mouse movement packets 2021-05-10 15:04:41 +02:00
loki
25309f21ee Fix appveyor build 2021-05-10 01:31:08 +02:00
loki
1ba8da9780 Fix error in README file 2021-05-10 01:07:24 +02:00
loki
66bc335d3f Merge branch 'master' of github.com:loki-47-6F-64/sunshine 2021-05-10 01:04:44 +02:00
loki
e834f375fb resolve merge conflict 2021-05-10 01:03:41 +02:00
loki
9c41972b65 minor modification for sunshine.service 2021-05-10 01:02:24 +02:00
loki-47-6F-64
41c30e9cfd Merge pull request #51 from Doomsdayrs/patch-1
Update README.md
2021-05-09 22:32:16 +02:00
Elia Zammuto
fd8cbf0c7d Merge branch 'loki-47-6F-64:master' into web-ui 2021-05-09 18:59:36 +02:00
Elia Zammuto
4fe90dcbd6 Started Work on Web UI 2021-05-09 18:55:34 +02:00
loki
377b086882 Fix amd_rc config options 2021-05-09 16:37:40 +02:00
loki
ade2ef3a15 Fix profile values for amfvce 2021-05-09 16:19:05 +02:00
loki
6f428eb316 Ensure no input remains in the task_pool before resetting 2021-05-09 11:56:53 +02:00
loki
2970ad662c No more dumb pointers for initialization 2021-05-09 11:40:12 +02:00
loki
1dfe49e765 Merge branch 'draw' 2021-05-08 14:16:29 +02:00
loki-47-6F-64
7a93a72710 Merge pull request #83 from TheElixZammuto/fix-stuck-keys
Fix Modifiers stuck on Host On Disconnection
2021-05-08 13:53:35 +02:00
Elia Zammuto
661c8260e5 Fix Position for input::reset 2021-05-08 13:46:20 +02:00
loki
13c2da07e8 Move shaders to seperate folder in assets 2021-05-08 13:22:04 +02:00
Elia Zammuto
71b214ca43 Fix Stuck Modifiers on Disconnection 2021-05-08 13:08:07 +02:00
loki
513942c888 Improve colors for nv12 2021-05-08 12:03:58 +02:00
loki
67df04e0a2 Fix cursor position on lower resolution screens 2021-05-06 16:51:59 +02:00
loki
7b45f0d899 Fix cursor height and width 2021-05-06 13:51:29 +02:00
loki
0f661e467e Blend cursor onto the image 2021-05-06 12:36:26 +02:00
loki
0232d8027c Render cursor on duplicated image 2021-05-06 12:00:39 +02:00
loki
3a0377851d Update gitignore 2021-05-05 16:00:27 +02:00
loki
a93bad4cf3 Fix crash when sending SIGINT before starting the http server 2021-05-05 15:53:22 +02:00
loki
88c3828ad3 Fixed not testing for 10bit pixels support 2021-05-05 12:17:25 +02:00
loki
c19853f03f Add color for BT701 colorspace 2021-05-05 11:28:57 +02:00
loki
1b7e103ef6 Allow resizing the image during conversion 2021-05-04 10:21:56 +02:00
loki
900d59b3ac Dynamically set colors during runtime 2021-05-03 22:06:55 +02:00
loki
37a9256587 Render NV12 color format 2021-05-02 22:35:19 +02:00
loki
127b5501d9 Render luma onto nv12 surface 2021-04-30 20:01:15 +02:00
Elia Zammuto
a081a9f5c4 Merge pull request #3 from loki-47-6F-64/master
Updates
2021-04-28 21:06:52 +02:00
loki
fe8c2ceab9 Warn when VideoProcessorSetStreamAlpha isn't supported 2021-04-28 13:44:42 +02:00
loki
1be5b4787f Moved pairInputDesktop() from desktop.h to input.cpp 2021-04-26 19:27:55 +02:00
loki
bb88d6f828 Third attempt to fix linux build for appveyor 2021-04-26 21:08:20 +02:00
loki
a7030641f3 Second attempt to fix linux build 2021-04-26 20:50:37 +02:00
loki
208de3dae9 Fix Linux build 2021-04-26 20:36:54 +02:00
Loki
e81db118d5 Fix windows build 2021-04-26 14:46:57 +02:00
loki
488d8e5fc2 force merge 2021-04-24 23:46:04 +02:00
loki
0049b36471 Use existing config option for selecting monitor 2021-04-24 23:41:56 +02:00
loki
66b4b70023 merge with notentered 2021-04-24 17:12:10 +02:00
loki
438ae6a761 Don't stop streaming when UAC is running 2021-04-24 15:53:48 +02:00
loki
0cfb440cf6 Added config examples to the config file 2021-04-24 14:23:12 +02:00
loki
a613280bbd Merge branch 'master' into TheElixZammuto 2021-04-24 13:09:14 +02:00
loki
6112c81db7 Fix –undefined reference to __memcpy_chk when building on msys64 2021-04-24 12:07:26 +02:00
arne
3ec4bf52e1 avahi service publishing cleanup & appveyor 2021-04-13 18:36:48 +02:00
arne
ec44a4391a avahi service publishing 2021-04-13 18:15:53 +02:00
Elia Zammuto
3f26a4fd21 Merge pull request #2 from psyke83/realtime_gpu
Use realtime GPU priority to avoid stalls during high GPU usage
2021-04-05 15:35:04 +02:00
Elia Zammuto
bb5b003dd5 Disabled key frames when not needed 2021-04-01 20:27:59 +02:00
Elia Zammuto
99777c8e82 Added more params 2021-04-01 14:25:38 +02:00
Elia Zammuto
4daaa1f089 Added some keyframes 2021-03-31 14:11:21 +02:00
Elia Zammuto
0828cc3f83 Started work on AMD Hardware Decoding 2021-03-29 13:16:56 +02:00
Elia Zammuto
790835f6c6 Merge pull request #1 from theofficialgman/amd_encoder
Add amd decoder support
2021-03-21 18:59:09 +01:00
kiralycraft
fe3784454a Fixed unspecified monitor streaming the whole X 2021-02-28 17:56:38 +02:00
kiralycraft
b336bf2fcb Attempt to update GCC to version 8 for appveyor 2021-02-28 16:10:29 +02:00
kiralycraft
87be37293e Accidentally modified stock config file 2021-02-28 15:56:23 +02:00
kiralycraft
7abcfc0390 Added ability to stream specific monitor on Linux 2021-02-28 15:52:47 +02:00
Garrett
020e2069cb Add amd decoder support 2021-01-28 18:38:11 -05:00
Zlatko Zahariev
83dd07469e Initial elevated windows work 2021-01-15 02:07:49 -05:00
Conn O'Griofa
0a883ab651 Use realtime GPU priority to avoid stalls during high GPU usage
Change derived from OBS project commit: ec769ef008
See related discussion: https://obsproject.com/forum/threads/obs-studio-24-0-3-gpu-priority-fix-testing.111669/
2021-01-11 04:22:18 +00:00
Doomsdayrs
7b39b93bb2 Update README.md
- Cleaned things up a bit more
- Simplified instructions
- make instruction has `-j ${nproc}` to ease up the word count
- Use $USER instead of {username}
2020-11-18 15:31:13 -05:00
Jacek Szafarkiewicz
30496c79ab Make systemd script cleaner 2020-09-04 15:51:20 +02:00
loki
415dec37ad update README 2020-05-04 00:24:11 +02:00
loki
fbbe396416 Install properly on ubuntu20.04 2020-05-03 23:58:32 +02:00
loki
c6da7d31d0 Fix typo 2020-05-03 22:04:07 +02:00
loki
1f9c6945ec support ubuntu 20.04 in favor of 19.10 2020-05-03 21:59:36 +02:00
loki
4094aec2eb Fix bug causing video artifects when FEC is used on Moonlight 2020-05-02 23:38:33 +02:00
loki
ff7a5aa1ea Don't use old images when encoding 2020-05-01 22:54:21 +02:00
loki
bfe59f6cf2 Fix stuttering in Minecraft when moving the mouse 2020-05-01 22:04:18 +02:00
loki
d8b4c66d7e Fix black block in place of cursor in Minecraft 2020-05-01 17:13:30 +02:00
loki
6f92c42ad8 Fix -quitappafter 2020-05-01 13:57:24 +02:00
loki
1b1d514d10 Parse apps after parsing environment in app.json 2020-05-01 13:32:26 +02:00
loki
af1135d455 fix row_pitch 2020-04-29 22:01:43 +02:00
loki
e10c9a1fa1 Exit gracefully if a port is bound by another process 2020-04-26 23:37:47 +02:00
loki
1a233ca4aa Remove incorrect warning 2020-04-26 15:24:36 +02:00
loki
6878dcbf37 Improve compilation instructions for Windows static build 2020-04-26 00:26:47 +02:00
loki
8d735e5611 Support keyboard key repeats 2020-04-26 00:23:34 +02:00
loki
1862662b3a Prevent queue from growing to large, eating up all memory 2020-04-24 22:10:08 +02:00
loki
1102ac9f3b Merge branch 'nvenc' 2020-04-23 16:12:21 +02:00
loki
12115a8a75 Merge branch 'master' of github.com:loki-47-6F-64/sunshine 2020-04-23 16:11:56 +02:00
loki
7ee59669da Removed unnecessary header include 2020-04-23 15:49:47 +02:00
loki
22418cb613 Moved linux specific files to folder platform/linux 2020-04-23 15:48:05 +02:00
loki
4bccec1c39 Refactor platorm Windows 2020-04-23 15:41:40 +02:00
loki
8b1a288ef3 Merge branch 'nvenc' of https://github.com/loki-47-6F-64/sunshine into nvenc 2020-04-23 11:14:35 +02:00
loki
fa489531b0 Don't access video device ctx merely for setting cursor invisible 2020-04-23 00:23:40 +03:00
loki
2e52402e27 Correctly truncate cursor image 2020-04-23 00:09:27 +03:00
loki
519f7a8bf1 convert pointer shape monochrome to color 2020-04-22 22:55:33 +03:00
loki
17e9b803db Display cursor type color with nvenc 2020-04-22 00:07:26 +03:00
loki
48b4dbd81c Merge branch 'master' into nvenc 2020-04-19 21:39:32 +02:00
loki
70bf11ec27 Increase accuracy of fps for nvenc and proper pixel format 2020-04-19 00:10:47 +03:00
loki-47-6F-64
8ea9d34729 Add small description for Sunshine 2020-04-18 12:27:26 +02:00
loki-47-6F-64
8c4519a2d0 Merge pull request #15 from Doomsdayrs/master
README update
2020-04-18 12:25:03 +02:00
Doomsdayrs
ea266b979f Update README.md
- added assets
- made a list
2020-04-17 22:46:07 -04:00
loki
2f978b3159 update pre-compiled 2020-04-17 21:57:27 +02:00
loki
d81ba12aa8 Merge branch 'nvenc' of github.com:loki-47-6F-64/sunshine into nvenc 2020-04-17 19:19:12 +02:00
loki
dd13131fe6 Fix video freezing when resizing display with 2 or more sessions 2020-04-17 19:18:55 +02:00
loki
5a4055f313 Pair and connect with Moonlight-iOS 2020-04-17 18:42:55 +02:00
loki
87f3ab0181 Fix nvenc 2020-04-17 12:28:23 +02:00
loki
c7d6e959e0 Fix stream not closing properly when exiting app 2020-04-16 15:35:12 +02:00
loki
0b1a69a067 Ensure it compiles on Linux again 2020-04-15 21:07:00 +02:00
loki
525e8b3c6d Refactor video.cpp 2020-04-15 19:16:20 +02:00
loki
ad7f93c3cb Switch between nvenc and software encoding 2020-04-14 00:59:43 +03:00
loki
c7a72553c4 Configure settings nvenc 2020-04-14 00:15:24 +03:00
loki
679f74e53c Fix multicasting for nvenc 2020-04-12 02:33:17 +03:00
loki
7edaa0cce0 Encode with nvenc smoothly 2020-04-10 15:39:50 +03:00
loki
c21038af88 Encode video with nvenc 2020-04-08 02:15:08 +03:00
loki
65f44cc885 Fix encoder flags not set properly 2020-04-07 18:57:59 +03:00
loki
ceb784c648 Test capabilities of the encoders 2020-04-07 14:54:56 +03:00
loki
8e3df43caf Pass both nvenc and software in validation 2020-04-07 00:34:52 +03:00
loki
afbca0f6cd initialize nvenc 2020-04-06 23:15:03 +03:00
Doomsdayrs
f4e99a1bd6 Update README.md 2020-04-05 14:46:00 -04:00
Doomsdayrs
ed6b919a4d Update README.md 2020-04-05 14:34:11 -04:00
Doomsdayrs
2d2fd77bc1 Update README.md
- Cleaned up credits
2020-04-05 02:11:26 -04:00
Doomsdayrs
15f347c69c Update README.md
- Super simple table of contents
2020-04-05 02:10:20 -04:00
Doomsdayrs
9fdaf2117f Update README.md
- Added links, Moved some parts around for appeal
2020-04-05 02:08:39 -04:00
Doomsdayrs
631be974ee Update README.md
- Initial rework to be legible in a modern fashion
2020-04-05 02:03:40 -04:00
doomsdayrs
bd3d1d6988 Readme, but now MD 2020-04-05 01:49:08 -04:00
loki
f2636b163e General structure complete 2020-04-02 20:13:44 +02:00
loki
df5daa045a Add a timestamp in front of the log 2020-04-01 18:01:13 +02:00
loki
4de547228c Fix mouse format unsupported and incorrect version string 2020-04-01 14:33:05 +02:00
loki
3d595ce927 Re-add capturing mouse for windows 2020-03-31 23:48:07 +02:00
loki
456d33cf77 Add abillity to supply options for specific encoders 2020-03-31 23:46:41 +02:00
loki
3ceb9b37a0 Reinitialize the video encoder in addition to the capturing device 2020-03-31 21:18:33 +02:00
loki
94181fd047 Prevent unnecessary copies of entire frames on Windows 2020-03-27 21:57:29 +01:00
loki
55705af922 Prepare for hardware encoders 2020-03-25 10:51:32 +01:00
loki
a9423574fe Improve clearity instructions 2020-03-21 17:09:33 +01:00
loki
34b1ef8779 Fix blank screen when starting session 2020-03-20 20:42:11 +01:00
loki
68a7cece49 Fix lack of video when starting a session on Windows without wiggling mouse 2020-03-20 15:53:19 +01:00
loki
ad87463784 Add flags for testing pairing 2020-03-19 19:59:27 +01:00
loki
ad34fef228 Merge branch 'master' of github.com:loki-47-6F-64/sunshine 2020-03-18 22:16:43 +01:00
loki
25de180d6e Fix exception when calling inerrupt handler while a session is running 2020-03-18 21:20:10 +01:00
loki
c0116e0cdd Don't quit application when a session still running 2020-03-18 21:12:05 +01:00
loki
e571b29868 Fix bug incorrectly blending cursor image 2020-03-17 22:17:28 +01:00
loki
5bbca8f517 Don't exit when udp::socket connection refused 2020-03-17 18:01:12 +01:00
loki
5502df5512 Attempt fix receiving PING 2020-03-16 20:01:30 +01:00
loki
87779b0ec8 Additional debug messages 2020-03-16 19:15:50 +01:00
loki
92f51622cc Map session to gamepads 2020-03-15 21:22:42 +01:00
loki
e0721aa104 Use latest captured images periodically 2020-03-14 18:06:11 +01:00
loki
b4f1ef1127 Fixed deadlock on unexpected connection loss 2020-03-14 14:38:09 +01:00
loki
1362abc70d Fix default ping_timeout config value 2020-03-01 14:06:47 +01:00
loki
1d6c6046a2 Fix typo 2020-02-23 20:59:12 +01:00
loki
41f4b71c99 Add debug msg when receiving messages over utp for video and audio 2020-02-23 20:49:53 +01:00
loki
38ec7c22fb Fix error scaling img when getting a timeout instead of an img 2020-02-23 20:48:19 +01:00
loki
02e842b066 Fix failed build on gcc 8.3.0 2020-02-13 22:24:24 +01:00
loki
2ae7d2e363 Fix audio crackling when connected to multiple clients 2020-02-13 21:53:22 +01:00
loki
f170e736c5 Fix Windows not accepting connections on RTSP 2020-02-12 18:05:42 +01:00
loki
da246d6417 Fix stopping all streams when just one should stop 2020-02-12 11:28:27 +01:00
loki
bb95d6ab52 Fix being unable to restart session 2020-02-10 00:33:12 +01:00
loki-47-6F-64
11d447d7eb Create LICENSE 2020-02-09 22:54:06 +01:00
loki
f56e7fc50d Fix incorrect Ping Timeout 2020-02-09 17:30:11 +01:00
loki
5d606bf567 Get video once more 2020-02-09 16:12:36 +01:00
loki
4b216d6676 Fix bad function call 2020-02-08 23:41:27 +01:00
loki
834f7b9063 Fix Empty App in Applist 2020-02-08 18:55:07 +01:00
loki
5cd0fd76bf Compile for Multicasting 2020-02-08 16:26:38 +01:00
loki
753f57c71b Remove dependency on a library for a single function 2020-02-01 10:25:37 +01:00
loki
c71d2739b1 Fixed incorrect logging type called 2020-01-31 21:17:49 +01:00
loki
b10c971374 Dynamically plug and unplug emulated gamepads 2020-01-31 20:57:34 +01:00
150 changed files with 44062 additions and 4295 deletions

67
.clang-format Normal file
View File

@@ -0,0 +1,67 @@
# Generated from CLion C/C++ Code Style settings
BasedOnStyle: LLVM
AccessModifierOffset: -2
AlignAfterOpenBracket: DontAlign
AlignConsecutiveAssignments: true
AlignOperands: Align
AllowAllArgumentsOnNextLine: false
AllowAllConstructorInitializersOnNextLine: false
AllowAllParametersOfDeclarationOnNextLine: false
AllowShortBlocksOnASingleLine: Always
AllowShortCaseLabelsOnASingleLine: false
AllowShortFunctionsOnASingleLine: All
AllowShortIfStatementsOnASingleLine: Always
AllowShortLambdasOnASingleLine: All
AllowShortLoopsOnASingleLine: true
AlwaysBreakAfterReturnType: None
AlwaysBreakTemplateDeclarations: Yes
BreakBeforeBraces: Custom
BraceWrapping:
AfterCaseLabel: false
AfterClass: false
AfterControlStatement: Never
AfterEnum: false
AfterFunction: false
AfterNamespace: false
AfterUnion: false
BeforeCatch: true
BeforeElse: true
IndentBraces: false
SplitEmptyFunction: false
SplitEmptyRecord: true
BreakBeforeBinaryOperators: None
BreakBeforeTernaryOperators: false
BreakConstructorInitializers: BeforeColon
BreakInheritanceList: BeforeColon
ColumnLimit: 0
CompactNamespaces: false
ContinuationIndentWidth: 2
IndentCaseLabels: false
IndentPPDirectives: None
IndentWidth: 2
KeepEmptyLinesAtTheStartOfBlocks: true
MaxEmptyLinesToKeep: 2
NamespaceIndentation: None
ObjCSpaceAfterProperty: false
ObjCSpaceBeforeProtocolList: true
PointerAlignment: Right
ReflowComments: false
SpaceAfterCStyleCast: false
SpaceAfterLogicalNot: false
SpaceAfterTemplateKeyword: false
SpaceBeforeAssignmentOperators: true
SpaceBeforeCpp11BracedList: true
SpaceBeforeCtorInitializerColon: true
SpaceBeforeInheritanceColon: true
SpaceBeforeParens: Never
SpaceBeforeRangeBasedForLoopColon: true
SpaceInEmptyParentheses: false
SpacesBeforeTrailingComments: 1
SpacesInAngles: false
SpacesInCStyleCastParentheses: false
SpacesInContainerLiterals: false
SpacesInParentheses: false
SpacesInSquareBrackets: false
TabWidth: 2
Cpp11BracedListStyle: false
UseTab: Never

8
.gitignore vendored
View File

@@ -1,8 +1,10 @@
build
cmake-build-*
cmake-build*
.DS_Store
.vscode
.vs
*.swp
*.kdev4
.idea
.cache
.idea

12
.gitmodules vendored
View File

@@ -1,12 +1,12 @@
[submodule "moonlight-common-c"]
path = moonlight-common-c
path = third-party/moonlight-common-c
url = https://github.com/moonlight-stream/moonlight-common-c.git
[submodule "Simple-Web-Server"]
path = Simple-Web-Server
path = third-party/Simple-Web-Server
url = https://github.com/loki-47-6F-64/Simple-Web-Server.git
[submodule "ViGEmClient"]
path = ViGEmClient
path = third-party/ViGEmClient
url = https://github.com/ViGEm/ViGEmClient
[submodule "pre-compiled"]
path = pre-compiled
url = https://bitbucket.org/Loki-47-6F-64/pre-compiled.git
[submodule "third-party/miniupnp"]
path = third-party/miniupnp
url = https://github.com/miniupnp/miniupnp

View File

@@ -1,98 +1,138 @@
cmake_minimum_required(VERSION 2.8)
cmake_minimum_required(VERSION 3.0)
project(Sunshine)
set(CMAKE_MODULE_PATH ${CMAKE_CURRENT_SOURCE_DIR})
# On MSYS2, building a stand-alone binary that links with ffmpeg is not possible,
# Therefore, ffmpeg, libx264 and libx265 must be build from source
add_subdirectory(third-party/Simple-Web-Server)
set(UPNPC_BUILD_SHARED OFF CACHE BOOL "no shared libraries")
set(UPNPC_BUILD_TESTS OFF CACHE BOOL "Don't build tests for miniupnpc")
set(UPNPC_BUILD_SAMPLE OFF CACHE BOOL "Don't build samples for miniupnpc")
set(UPNPC_NO_INSTALL ON CACHE BOOL "Don't install any libraries build for miniupnpc")
add_subdirectory(third-party/miniupnp/miniupnpc)
include_directories(third-party/miniupnp)
if(WIN32)
option(SUNSHINE_STANDALONE "Compile stand-alone binary of Sunshine" OFF)
if(SUNSHINE_STANDALONE)
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -static")
if(NOT DEFINED SUNSHINE_PREPARED_BINARIES)
set(SUNSHINE_PREPARED_BINARIES "${CMAKE_CURRENT_SOURCE_DIR}/pre-compiled/windows")
endif()
list(PREPEND PLATFORM_LIBRARIES
C:/msys64/mingw64/lib/gcc/x86_64-w64-mingw32/${CMAKE_CXX_COMPILER_VERSION}/libstdc++.a
C:/msys64/mingw64/x86_64-w64-mingw32/lib/libwinpthread.a
)
set(FFMPEG_INCLUDE_DIRS
${SUNSHINE_PREPARED_BINARIES}/include)
set(FFMPEG_LIBRARIES
${SUNSHINE_PREPARED_BINARIES}/lib/libavcodec.a
${SUNSHINE_PREPARED_BINARIES}/lib/libavdevice.a
${SUNSHINE_PREPARED_BINARIES}/lib/libavfilter.a
${SUNSHINE_PREPARED_BINARIES}/lib/libavformat.a
${SUNSHINE_PREPARED_BINARIES}/lib/libavutil.a
${SUNSHINE_PREPARED_BINARIES}/lib/libpostproc.a
${SUNSHINE_PREPARED_BINARIES}/lib/libswresample.a
${SUNSHINE_PREPARED_BINARIES}/lib/libswscale.a
${SUNSHINE_PREPARED_BINARIES}/lib/libx264.a
${SUNSHINE_PREPARED_BINARIES}/lib/libx265.a
z lzma bcrypt C:/msys64/mingw64/lib/libiconv.a)
endif()
else()
set(SUNSHINE_STANDALONE OFF)
# Ugly hack to compile with #include <qos2.h>
add_compile_definitions(
QOS_FLOWID=UINT32
PQOS_FLOWID=UINT32*
QOS_NON_ADAPTIVE_FLOW=2)
endif()
add_subdirectory(Simple-Web-Server)
add_subdirectory(moonlight-common-c/enet)
add_subdirectory(third-party/moonlight-common-c/enet)
find_package(Threads REQUIRED)
find_package(OpenSSL REQUIRED)
if(NOT SUNSHINE_STANDALONE)
find_package(FFmpeg REQUIRED)
endif()
list(APPEND SUNSHINE_COMPILE_OPTIONS -fPIC -Wall -Wno-missing-braces -Wno-maybe-uninitialized -Wno-sign-compare)
if(WIN32)
file(
DOWNLOAD "https://github.com/TheElixZammuto/sunshine-prebuilt/releases/download/1.0.0/pre-compiled.zip" "${CMAKE_CURRENT_BINARY_DIR}/pre-compiled.zip"
TIMEOUT 60
EXPECTED_HASH SHA256=5d59986bd7f619eaaf82b2dd56b5127b747c9cbe8db61e3b898ff6b485298ed6)
file(ARCHIVE_EXTRACT
INPUT "${CMAKE_CURRENT_BINARY_DIR}/pre-compiled.zip"
DESTINATION ${CMAKE_CURRENT_BINARY_DIR}/pre-compiled)
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -static")
if(NOT DEFINED SUNSHINE_PREPARED_BINARIES)
set(SUNSHINE_PREPARED_BINARIES "${CMAKE_CURRENT_BINARY_DIR}/pre-compiled/windows")
endif()
add_compile_definitions(SUNSHINE_PLATFORM="windows")
add_subdirectory(tools) #This is temporary, only tools for Windows are needed, for now
list(APPEND SUNSHINE_DEFINITIONS APPS_JSON="apps_windows.json")
include_directories(
ViGEmClient/include)
include_directories(third-party/ViGEmClient/include)
set(PLATFORM_TARGET_FILES
sunshine/platform/windows.cpp
sunshine/platform/windows_dxgi.cpp
sunshine/platform/windows_wasapi.cpp
ViGEmClient/src/ViGEmClient.cpp
ViGEmClient/include/ViGEm/Client.h
ViGEmClient/include/ViGEm/Common.h
ViGEmClient/include/ViGEm/Util.h
ViGEmClient/include/ViGEm/km/BusShared.h)
sunshine/platform/windows/publish.cpp
sunshine/platform/windows/misc.cpp
sunshine/platform/windows/input.cpp
sunshine/platform/windows/display.h
sunshine/platform/windows/display_base.cpp
sunshine/platform/windows/display_vram.cpp
sunshine/platform/windows/display_ram.cpp
sunshine/platform/windows/audio.cpp
third-party/ViGEmClient/src/ViGEmClient.cpp
third-party/ViGEmClient/include/ViGEm/Client.h
third-party/ViGEmClient/include/ViGEm/Common.h
third-party/ViGEmClient/include/ViGEm/Util.h
third-party/ViGEmClient/include/ViGEm/km/BusShared.h)
set(OPENSSL_LIBRARIES
libssl.a
libcrypto.a)
set(FFMPEG_INCLUDE_DIRS
${SUNSHINE_PREPARED_BINARIES}/include)
set(FFMPEG_LIBRARIES
${SUNSHINE_PREPARED_BINARIES}/lib/libavcodec.a
${SUNSHINE_PREPARED_BINARIES}/lib/libavdevice.a
${SUNSHINE_PREPARED_BINARIES}/lib/libavfilter.a
${SUNSHINE_PREPARED_BINARIES}/lib/libavformat.a
${SUNSHINE_PREPARED_BINARIES}/lib/libavutil.a
${SUNSHINE_PREPARED_BINARIES}/lib/libpostproc.a
${SUNSHINE_PREPARED_BINARIES}/lib/libswresample.a
${SUNSHINE_PREPARED_BINARIES}/lib/libswscale.a
${SUNSHINE_PREPARED_BINARIES}/lib/libx264.a
${SUNSHINE_PREPARED_BINARIES}/lib/libx265.a
${SUNSHINE_PREPARED_BINARIES}/lib/libhdr10plus.a
z lzma bcrypt libiconv.a)
list(PREPEND PLATFORM_LIBRARIES
libstdc++.a
libwinpthread.a
libssp.a
Qwave
winmm
ksuser
wsock32
ws2_32
iphlpapi
windowsapp
d3d11 dxgi
d3d11 dxgi D3DCompiler
setupapi
dnsapi
)
set_source_files_properties(ViGEmClient/src/ViGEmClient.cpp PROPERTIES COMPILE_DEFINITIONS "UNICODE=1;ERROR_INVALID_DEVICE_OBJECT_PARAMETER=650")
set_source_files_properties(ViGEmClient/src/ViGEmClient.cpp PROPERTIES COMPILE_FLAGS "-Wno-unknown-pragmas -Wno-misleading-indentation -Wno-class-memaccess")
set_source_files_properties(third-party/ViGEmClient/src/ViGEmClient.cpp PROPERTIES COMPILE_DEFINITIONS "UNICODE=1;ERROR_INVALID_DEVICE_OBJECT_PARAMETER=650")
set_source_files_properties(third-party/ViGEmClient/src/ViGEmClient.cpp PROPERTIES COMPILE_FLAGS "-Wno-unknown-pragmas -Wno-misleading-indentation -Wno-class-memaccess")
else()
add_compile_definitions(SUNSHINE_PLATFORM="linux")
list(APPEND SUNSHINE_DEFINITIONS APPS_JSON="apps_linux.json")
find_package(X11 REQUIRED)
find_package(FFmpeg REQUIRED)
set(PLATFORM_TARGET_FILES
sunshine/platform/linux.cpp
sunshine/platform/linux_evdev.cpp)
sunshine/platform/linux/publish.cpp
sunshine/platform/linux/vaapi.h
sunshine/platform/linux/vaapi.cpp
sunshine/platform/linux/misc.h
sunshine/platform/linux/misc.cpp
sunshine/platform/linux/display.cpp
sunshine/platform/linux/audio.cpp
sunshine/platform/linux/input.cpp
third-party/glad/src/egl.c
third-party/glad/src/gl.c
third-party/glad/include/EGL/eglplatform.h
third-party/glad/include/KHR/khrplatform.h
third-party/glad/include/glad/gl.h
third-party/glad/include/glad/egl.h)
set(PLATFORM_LIBRARIES
Xfixes
Xtst
xcb
xcb-shm
xcb-xfixes
Xrandr
${X11_LIBRARIES}
dl
evdev
pulse
pulse-simple
@@ -100,7 +140,8 @@ else()
set(PLATFORM_INCLUDE_DIRS
${X11_INCLUDE_DIR}
/usr/include/libevdev-1.0)
/usr/include/libevdev-1.0
third-party/glad/include)
if(NOT DEFINED SUNSHINE_EXECUTABLE_PATH)
set(SUNSHINE_EXECUTABLE_PATH "${CMAKE_CURRENT_BINARY_DIR}/sunshine")
@@ -109,16 +150,21 @@ else()
configure_file(sunshine.service.in sunshine.service @ONLY)
endif()
add_subdirectory(third-party/cbs)
set(Boost_USE_STATIC_LIBS ON)
find_package(Boost COMPONENTS log filesystem REQUIRED)
set(SUNSHINE_TARGET_FILES
moonlight-common-c/reedsolomon/rs.c
moonlight-common-c/reedsolomon/rs.h
moonlight-common-c/src/Input.h
moonlight-common-c/src/Rtsp.h
moonlight-common-c/src/RtspParser.c
moonlight-common-c/src/Video.h
third-party/moonlight-common-c/reedsolomon/rs.c
third-party/moonlight-common-c/reedsolomon/rs.h
third-party/moonlight-common-c/src/Input.h
third-party/moonlight-common-c/src/Rtsp.h
third-party/moonlight-common-c/src/RtspParser.c
third-party/moonlight-common-c/src/Video.h
sunshine/upnp.cpp
sunshine/upnp.h
sunshine/cbs.cpp
sunshine/utility.h
sunshine/uuid.h
sunshine/config.h
@@ -129,11 +175,16 @@ set(SUNSHINE_TARGET_FILES
sunshine/crypto.h
sunshine/nvhttp.cpp
sunshine/nvhttp.h
sunshine/httpcommon.cpp
sunshine/httpcommon.h
sunshine/confighttp.cpp
sunshine/confighttp.h
sunshine/rtsp.cpp
sunshine/rtsp.h
sunshine/stream.cpp
sunshine/stream.h
sunshine/video.cpp
sunshine/video.h
sunshine/thread_safe.h
sunshine/input.cpp
sunshine/input.h
sunshine/audio.cpp
@@ -146,13 +197,19 @@ set(SUNSHINE_TARGET_FILES
sunshine/move_by_copy.h
sunshine/task_pool.h
sunshine/thread_pool.h
sunshine/thread_safe.h
sunshine/sync.h
sunshine/round_robin.h
${PLATFORM_TARGET_FILES})
set_source_files_properties(sunshine/upnp.cpp PROPERTIES COMPILE_FLAGS -Wno-pedantic)
include_directories(
${CMAKE_CURRENT_SOURCE_DIR}
${CMAKE_CURRENT_SOURCE_DIR}/Simple-Web-Server
${CMAKE_CURRENT_SOURCE_DIR}/moonlight-common-c/enet/include
${CMAKE_CURRENT_SOURCE_DIR}/moonlight-common-c/reedsolomon
${CMAKE_CURRENT_SOURCE_DIR}/third-party
${CMAKE_CURRENT_SOURCE_DIR}/third-party/cbs/include
${CMAKE_CURRENT_SOURCE_DIR}/third-party/moonlight-common-c/enet/include
${CMAKE_CURRENT_SOURCE_DIR}/third-party/moonlight-common-c/reedsolomon
${FFMPEG_INCLUDE_DIRS}
${PLATFORM_INCLUDE_DIRS}
)
@@ -172,13 +229,12 @@ if(NOT SUNSHINE_ASSETS_DIR)
set(SUNSHINE_ASSETS_DIR "${CMAKE_CURRENT_SOURCE_DIR}/assets")
endif()
if(SUNSHINE_STANDALONE)
set(OPENSSL_LIBRARIES
C:/msys64/mingw64/lib/libssl.a
C:/msys64/mingw64/lib/libcrypto.a)
endif()
list(APPEND CBS_EXTERNAL_LIBRARIES
cbs)
list(APPEND SUNSHINE_EXTERNAL_LIBRARIES
libminiupnpc-static
${CBS_EXTERNAL_LIBRARIES}
${CMAKE_THREAD_LIBS_INIT}
stdc++fs
enet

674
LICENSE Normal file
View File

@@ -0,0 +1,674 @@
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The GNU General Public License is a free, copyleft license for
software and other kinds of works.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
the GNU General Public License is intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users. We, the Free Software Foundation, use the
GNU General Public License for most of our software; it applies also to
any other work released this way by its authors. You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
To protect your rights, we need to prevent others from denying you
these rights or asking you to surrender the rights. Therefore, you have
certain responsibilities if you distribute copies of the software, or if
you modify it: responsibilities to respect the freedom of others.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must pass on to the recipients the same
freedoms that you received. You must make sure that they, too, receive
or can get the source code. And you must show them these terms so they
know their rights.
Developers that use the GNU GPL protect your rights with two steps:
(1) assert copyright on the software, and (2) offer you this License
giving you legal permission to copy, distribute and/or modify it.
For the developers' and authors' protection, the GPL clearly explains
that there is no warranty for this free software. For both users' and
authors' sake, the GPL requires that modified versions be marked as
changed, so that their problems will not be attributed erroneously to
authors of previous versions.
Some devices are designed to deny users access to install or run
modified versions of the software inside them, although the manufacturer
can do so. This is fundamentally incompatible with the aim of
protecting users' freedom to change the software. The systematic
pattern of such abuse occurs in the area of products for individuals to
use, which is precisely where it is most unacceptable. Therefore, we
have designed this version of the GPL to prohibit the practice for those
products. If such problems arise substantially in other domains, we
stand ready to extend this provision to those domains in future versions
of the GPL, as needed to protect the freedom of users.
Finally, every program is threatened constantly by software patents.
States should not allow patents to restrict development and use of
software on general-purpose computers, but in those that do, we wish to
avoid the special danger that patents applied to a free program could
make it effectively proprietary. To prevent this, the GPL assures that
patents cannot be used to render the program non-free.
The precise terms and conditions for copying, distribution and
modification follow.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
The Corresponding Source for a work in source code form is that
same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Use with the GNU Affero General Public License.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU Affero General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the special requirements of the GNU Affero General Public License,
section 13, concerning interaction through a network will apply to the
combination as such.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If the program does terminal interaction, make it output a short
notice like this when it starts in an interactive mode:
<program> Copyright (C) <year> <name of author>
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, your program's commands
might be different; for a GUI interface, you would use an "about box".
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU GPL, see
<https://www.gnu.org/licenses/>.
The GNU General Public License does not permit incorporating your program
into proprietary programs. If your program is a subroutine library, you
may consider it more useful to permit linking proprietary applications with
the library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License. But first, please read
<https://www.gnu.org/licenses/why-not-lgpl.html>.

199
README.md Normal file
View File

@@ -0,0 +1,199 @@
# Introduction
Sunshine is a Gamestream host for Moonlight
[![AppVeyor Build Status](https://ci.appveyor.com/api/projects/status/cgrtw2g3fq9b0b70/branch/master?svg=true)](https://ci.appveyor.com/project/loki-47-6F-64/sunshine/branch/master)
[![Downloads](https://img.shields.io/github/downloads/Loki-47-6F-64/sunshine/total)](https://github.com/Loki-47-6F-64/sunshine/releases)
- [Building](README.md#building)
- [Credits](README.md#credits)
# Building
- [Linux](README.md#linux)
- [Windows](README.md#windows-10)
## Linux
### Requirements:
Ubuntu 20.04:
Install the following
```
sudo apt install cmake libssl-dev libavdevice-dev libboost-thread-dev libboost-filesystem-dev libboost-log-dev libpulse-dev libopus-dev libxtst-dev libx11-dev libxrandr-dev libxfixes-dev libevdev-dev libxcb1-dev libxcb-shm0-dev libxcb-xfixes0-dev
```
### Compilation:
- `git clone https://github.com/loki-47-6F-64/sunshine.git --recurse-submodules`
- `cd sunshine && mkdir build && cd build`
- `cmake ..`
- `make -j ${nproc}`
### Setup:
sunshine needs access to uinput to create mouse and gamepad events:
- Add user to group 'input':
`usermod -a -G input $USER`
- Create udev rules:
- Run the following command:
`nano /etc/udev/rules.d/85-sunshine-input.rules`
- Input the following contents:
`KERNEL=="uinput", GROUP="input", MODE="0660"`
- Save the file and exit
1. `CTRL+X` to start exit
2. `Y` to save modifications
- `assets/sunshine.conf` is an example configuration file. Modify it as you see fit, then use it by running:
`sunshine path/to/sunshine.conf`
- Configure autostart service
`path/to/build/dir/sunshine.service` is used to start sunshine in the background. To use it, do the following:
1. Copy it to the users systemd, `cp sunshine.service ~/.config/systemd/user/`
2. Starting
- Onetime:
`systemctl --user start sunshine`
- Always on boot:
`systemctl --user enable sunshine`
- `assets/apps.json` is an [example](README.md#application-list) of a list of applications that are started just before running a stream
### Trouleshooting:
- If you get "Could not create Sunshine Gamepad: Permission Denied", ensure you are part of the group "input":
- `groups $USER`
- If Sunshine sends audio from the microphone instead of the speaker, try the following steps:
1. `$ pacmd list-sources | grep "name:"` or `$ pactl info | grep Source` if running pipewire.
2. Copy the name to the configuration option "audio_sink"
3. restart sunshine
## Windows 10
### Requirements:
mingw-w64-x86_64-openssl mingw-w64-x86_64-cmake mingw-w64-x86_64-toolchain mingw-w64-x86_64-opus mingw-w64-x86_64-x265 mingw-w64-x86_64-boost git mingw-w64-x86_64-make
### Compilation:
- `git clone https://github.com/loki-47-6F-64/sunshine.git --recursive`
- `cd sunshine && mkdir build && cd build`
- `cmake -G"Unix Makefiles" ..`
- `mingw32-make`
### Setup:
- **OPTIONAL** Gamepad support: Download and run 'ViGEmBus_Setup_1.16.116.exe' from [https://github.com/ViGEm/ViGEmBus/releases]
# Common
## Usage:
- run "sunshine path/to/sunshine.conf"
- If running for the first time, make sure to note the username and password Sunshine showed to you, since you **cannot get back later**!
- In Moonlight: Add PC manually
- When Moonlight request you insert the correct pin on sunshine:
- Type in the URL bar of your browser: `https://xxx.xxx.xxx.xxx:47990` where `xxx.xxx.xxx.xxx` is the IP address of your computer
- Ignore any warning given by your browser about "insecure website"
- Type in the username and password shown the first time you run Sunshine
- Go to "PIN" in the Header
- Type in your PIN and press Enter, you should get a Success Message
- Click on one of the Applications listed
- Have fun :)
## Note:
- The Windows key is not passed through by Moonlight, therefore Sunshine maps Right-Alt key to the Windows key
- If you set Video Bitrate to 0.5Mb/s:
- Sunshine will use CRF or QP to controll the quality of the stream. (See example configuration file for more details)
- This is less CPU intensive and it has lower average bandwith requirements compared to manually setting bitrate to acceptable quality
- However, it has higher peak bitrates, forcing Sunshine to drop entire frames when streaming 1080P due to their size.
- When this happens, the video portion of the stream appears to be frozen.
- This is rare enough that using this for the desktop environment is tolerable (in my opinion), however for gaming not so much.
## Credits:
- [Simple-Web-Server](https://gitlab.com/eidheim/Simple-Web-Server)
- [Moonlight](https://github.com/moonlight-stream)
- [Looking-Glass](https://github.com/gnif/LookingGlass) (For showing me how to properly capture frames on Windows, saving me a lot of time :)
- [Eretik](http://eretik.omegahg.com/) (For creating PolicyConfig.h, allowing me to change the default audio device on Windows programmatically)
## Application List:
**Note:** You can change the Application List in the "Apps" section of the User Interface `https://xxx.xxx.xxx.xxx:47990/`
- You can use Environment variables in place of values
- $(HOME) will be replaced by the value of $HOME
- $$ will be replaced by $ --> $$(HOME) will be replaced by $(HOME)
- env: Adds or overwrites Environment variables for the commands/applications run by Sunshine.
- "Variable name":"Variable value"
- apps: The list of applications
- Example:
```json
{
"name":"An App",
"cmd":"command to open app",
"prep-cmd":[
{
"do":"some-command",
"undo":"undo-that-command"
}
],
"detached":[
"some-command",
"another-command"
]
}
```
- name: Self explanatory
- output <optional>: The file where the output of the command is stored
- If it is not specified, the output is ignored
- detached: A list of commands to be run and forgotten about
- prep-cmd: A list of commands to be run before/after the application
- If any of the prep-commands fail, starting the application is aborted
- do: Run before the application
- If it fails, all 'undo' commands of the previously succeeded 'do' commands are run
- undo <optional>: Run after the application has terminated
- This should not fail considering it is supposed to undo the 'do' commands.
- If it fails, Sunshine is terminated
- cmd <optional>: The main application
- If not specified, a processs is started that sleeps indefinitely
1. When an application is started, if there is an application already running, it will be terminated.
2. When the application has been shutdown, the stream shuts down as well.
3. In addition to the apps listed, one app "Desktop" is hardcoded into Sunshine. It does not start an application, instead it simply starts a stream.
Linux
```json
{
"env":{
"DISPLAY":":0",
"DRI_PRIME":"1",
"XAUTHORITY":"$(HOME)/.Xauthority",
"PATH":"$(PATH):$(HOME)/.local/bin"
},
"apps":[
{
"name":"Low Res Desktop",
"prep-cmd":[
{ "do":"xrandr --output HDMI-1 --mode 1920x1080", "undo":"xrandr --output HDMI-1 --mode 1920x1200" }
]
},
{
"name":"Steam BigPicture",
"output":"steam.txt",
"cmd":"steam -bigpicture",
"prep-cmd":[]
}
]
}
```
Windows
```json
{
"env":{
"PATH":"$(PATH);C:\\Program Files (x86)\\Steam"
},
"apps":[
{
"name":"Steam BigPicture",
"output":"steam.txt",
"prep-cmd":[
{"do":"steam \"steam://open/bigpicture\""}
]
}
]
}
```

View File

@@ -1,107 +0,0 @@
######### Linux ##############
Requirements:
Ubuntu 19.10: cmake libssl-dev libavdevice-dev libboost-thread-dev libboost-filesystem-dev libboost-log-dev libpulse-dev libopus-dev libxtst-dev libx11-dev libxfixes-dev libevdev-dev libxcb1-dev libxcb-shm0-dev libxcb-xfixes0-dev
Compilation:
* git clone <repository> --recurse-submodules
* mkdir build && cd build
* cmake ..
* make
Setup:
* sunshine needs access to uinput to create mouse and gamepad events:
* Add user to group 'input': "usermod -a -G input username
* Create a file: "/etc/udev/rules.d/85-sunshine-input.rules"
* The contents of the file is as follows:
KERNEL=="uinput", GROUP="input", mode="0660"
* assets/sunshine.conf is an example configuration file. Modify it as you see fit and use it by running: "sunshine path/to/sunshine.conf"
* path/to/build/dir/sunshine.service is used to start sunshine in the background:
* cp sunshine.service $HOME/.config/systemd/user/
* Modify $HOME/.config/systemd/user/sunshine.conf to point to the sunshine executable
* systemctl --user start sunshine
* assets/apps.json is an example of a list of applications that are started just before running a stream:
* See below for a detailed explanation
Trouleshooting:
* If you get "Could not create Sunshine Gamepad: Permission Denied", ensure you are part of the group "input":
* groups
* If Sunshine sends audio from the microphone instead of the speaker, try the following steps:
* pacmd list-sources | grep "name:"
* Copy the name to the configuration option "audio_sink"
* restart sunshine
######### Windows 10 ############
Requirements:
MSYS2 : mingw-w64-x86_64-openssl mingw-w64-x86_64-cmake mingw-w64-x86_64-toolchain mingw-w64-x86_64-ffmpeg mingw-w64-x86_64-boost
Compilation:
* git clone <repository> --recurse-submodules
* mkdir build && cd build
* cmake -G"Unix Makefiles" ..
* make
Setup:
* <optional> Gamepad support: Download and run 'ViGEmBus_Setup_1.16.116.exe' from [https://github.com/ViGEm/ViGEmBus/releases]
######### Common #############
Usage:
* run "sunshine path/to/sunshine.conf"
* In Moonlight: Add PC manually
* When Moonlight request you insert the correct pin on sunshine:
wget xxx.xxx.xxx.xxx:47989/pin/xxxx -- where the first few x's are substituted by the ip of Sunshine and the final 4 x'es are substituted by the pin
or
Type in the URL bar of your browser: xxx.xxx.xxx.xxx:47989/pin/xxxx -- where the first few x's are substituted by the ip of the final 4 x'es are subsituted by the pin
* Click on one of the Applications listed
* Have fun :)
Note:
* The Windows key is not passed through by Moonlight, therefore Sunshine maps Right-Alt key to the Windows key
* If you set Video Bitrate to 0.5Mb/s:
* Sunshine will use CRF or QP to controll the quality of the stream. (See example configuration file for more details)
* This is less CPU intensive and it has lower average bandwith requirements compared to manually setting bitrate to acceptable quality
* However, it has higher peak bitrates, forcing Sunshine to drop entire frames when streaming 1080P due to their size.
* When this happens, the video portion of the stream appears to be frozen.
* This is rare enough that using this for the desktop environment is tolerable (in my opinion), however for gaming not so much.
Credits:
* Simple-Web-Server [https://gitlab.com/eidheim/Simple-Web-Server]
* Moonlight [https://github.com/moonlight-stream]
* Looking-Glass [https://github.com/gnif/LookingGlass] (For showing me how to properly capture frames on Windows, saving me a lot of time :)
Application List:
* You can use Environment variables in place of values
* $(HOME) will be replaced by the value of $HOME
* $$ will be replaced by $ --> $$(HOME) will be replaced by $(HOME)
* env: Adds or overwrites Environment variables for the commands/applications run by Sunshine.
* "Variable name":"Variable value"
* apps: The list of applications
* name: Self explanatory
* output <optional>: The file where the output of the command is stored
* If it is not specified, the output is ignored
* prep-cmd: A list of commands to be run before/after the application
* If any of the prep-commands fail, starting the application is aborted
* do: Run before the application
* If it fails, all 'undo' commands of the previously succeeded 'do' commands are run
* undo <optional>: Run after the application has terminated
* This should not fail considering it is supposed to undo the 'do' commands.
* If it fails, Sunshine is terminated
* cmd <optional>: The main application
* If not specified, a processs is started that sleeps indefinitely
When an application is started, if there is an application already running, it will be terminated.
When the application has been shutdown, the stream shuts down as well.
In addition to the apps listed, one app "Desktop" is hardcoded into Sunshine. It does not start an application, instead it simply starts a stream.

View File

@@ -1,5 +1,5 @@
image:
- Ubuntu
- Ubuntu2004
- Visual Studio 2019
environment:
@@ -8,18 +8,8 @@ environment:
- BUILD_TYPE: Release
install:
- sh: sudo add-apt-repository ppa:hnakamur/icu
- sh: sudo add-apt-repository ppa:hnakamur/boost
- sh: sudo add-apt-repository ppa:savoury1/build-tools
- sh: sudo add-apt-repository ppa:savoury1/backports
- sh: sudo add-apt-repository ppa:savoury1/graphics
- sh: sudo add-apt-repository ppa:savoury1/multimedia
- sh: sudo add-apt-repository ppa:savoury1/ffmpeg4
- sh: sudo apt update
- sh: sudo apt install -y fakeroot cmake libssl-dev libavdevice-dev libboost-thread1.67-dev libboost-filesystem1.67-dev libboost-log1.67-dev libpulse-dev libopus-dev libxtst-dev libx11-dev libxfixes-dev libevdev-dev libxcb1-dev libxcb-shm0-dev libxcb-xfixes0-dev
- sh: sudo update-alternatives --set gcc /usr/bin/gcc-8
- sh: sudo apt update --ignore-missing
- sh: sudo apt install -y build-essential fakeroot gcc-10 g++-10 cmake libssl-dev libavdevice-dev libboost-thread-dev libboost-filesystem-dev libboost-log-dev libpulse-dev libopus-dev libxtst-dev libx11-dev libxrandr-dev libxfixes-dev libevdev-dev libxcb1-dev libxcb-shm0-dev libxcb-xfixes0-dev
- cmd: C:\msys64\usr\bin\bash -lc "pacman --needed --noconfirm -S mingw-w64-x86_64-openssl mingw-w64-x86_64-cmake mingw-w64-x86_64-toolchain mingw-w64-x86_64-opus mingw-w64-x86_64-x265 mingw-w64-x86_64-boost git yasm nasm diffutils make"
before_build:
@@ -30,8 +20,8 @@ before_build:
build_script:
- cmd: set OLDPATH=%PATH%
- cmd: set PATH=C:\msys64\mingw64\bin
- sh: cmake -DCMAKE_BUILD_TYPE=$BUILD_TYPE -DSUNSHINE_EXECUTABLE_PATH=sunshine -DSUNSHINE_ASSETS_DIR=/etc/sunshine ..
- cmd: cmake -DCMAKE_BUILD_TYPE=$BUILD_TYPE -DOPENSSL_ROOT_DIR=C:\OpenSSL-v111-Win64 -DSUNSHINE_STANDALONE=ON -DSUNSHINE_ASSETS_DIR=assets -G "MinGW Makefiles" ..
- sh: cmake -DCMAKE_C_COMPILER=gcc-10 -DCMAKE_CXX_COMPILER=g++-10 -DCMAKE_BUILD_TYPE=$BUILD_TYPE -DSUNSHINE_EXECUTABLE_PATH=sunshine -DSUNSHINE_ASSETS_DIR=/etc/sunshine ..
- cmd: cmake -DCMAKE_BUILD_TYPE=$BUILD_TYPE -DSUNSHINE_ASSETS_DIR=assets -G "MinGW Makefiles" ..
- sh: make -j$(nproc)
- cmd: mingw32-make -j2
- cmd: set PATH=%OLDPATH%

View File

@@ -1,8 +1,5 @@
{
"env":{
"DISPLAY":":0",
"DRI_PRIME":"1",
"XAUTHORITY":"$(HOME)/.Xauthority",
"PATH":"$(PATH):$(HOME)/.local/bin"
},
"apps":[
@@ -16,8 +13,7 @@
"name":"Steam BigPicture",
"output":"steam.txt",
"cmd":"steam -bigpicture",
"prep-cmd":[]
"detached":["setsid steam steam://open/bigpicture"]
}
]
}

View File

@@ -7,9 +7,7 @@
"name":"Steam BigPicture",
"output":"steam.txt",
"prep-cmd":[
{"do":"steam \"steam://open/bigpicture\""}
]
"detached":["steam steam://open/bigpicture"]
}
]
}

View File

@@ -0,0 +1,33 @@
Texture2D image : register(t0);
SamplerState def_sampler : register(s0);
struct FragTexWide {
float3 uuv : TEXCOORD0;
};
cbuffer ColorMatrix : register(b0) {
float4 color_vec_y;
float4 color_vec_u;
float4 color_vec_v;
float2 range_y;
float2 range_uv;
};
//--------------------------------------------------------------------------------------
// Pixel Shader
//--------------------------------------------------------------------------------------
float2 main_ps(FragTexWide input) : SV_Target
{
float3 rgb_left = image.Sample(def_sampler, input.uuv.xz).rgb;
float3 rgb_right = image.Sample(def_sampler, input.uuv.yz).rgb;
float3 rgb = (rgb_left + rgb_right) * 0.5;
float u = dot(color_vec_u.xyz, rgb) + color_vec_u.w;
float v = dot(color_vec_v.xyz, rgb) + color_vec_v.w;
u = u * range_uv.x + range_uv.y;
v = v * range_uv.x + range_uv.y;
return float2(u, v * 224.0f/256.0f + 0.0625);
}

View File

@@ -0,0 +1,29 @@
struct VertTexPosWide {
float3 uuv : TEXCOORD;
float4 pos : SV_POSITION;
};
cbuffer info : register(b0) {
float width_i;
};
//--------------------------------------------------------------------------------------
// Vertex Shader
//--------------------------------------------------------------------------------------
VertTexPosWide main_vs(uint vI : SV_VERTEXID)
{
float idHigh = float(vI >> 1);
float idLow = float(vI & uint(1));
float x = idHigh * 4.0 - 1.0;
float y = idLow * 4.0 - 1.0;
float u_right = idHigh * 2.0;
float u_left = u_right - width_i;
float v = 1.0 - idLow * 2.0;
VertTexPosWide vert_out;
vert_out.uuv = float3(u_left, u_right, v);
vert_out.pos = float4(x, y, 0.0, 1.0);
return vert_out;
}

View File

@@ -0,0 +1,25 @@
Texture2D image : register(t0);
SamplerState def_sampler : register(s0);
cbuffer ColorMatrix : register(b0) {
float4 color_vec_y;
float4 color_vec_u;
float4 color_vec_v;
float2 range_y;
float2 range_uv;
};
struct PS_INPUT
{
float4 pos : SV_POSITION;
float2 tex : TEXCOORD;
};
float main_ps(PS_INPUT frag_in) : SV_Target
{
float3 rgb = image.Sample(def_sampler, frag_in.tex, 0).rgb;
float y = dot(color_vec_y.xyz, rgb);
return y * range_y.x + range_y.y;
}

View File

@@ -0,0 +1,14 @@
Texture2D image : register(t0);
SamplerState def_sampler : register(s0);
struct PS_INPUT
{
float4 pos : SV_POSITION;
float2 tex : TEXCOORD;
};
float4 main_ps(PS_INPUT frag_in) : SV_Target
{
return image.Sample(def_sampler, frag_in.tex, 0);
}

View File

@@ -0,0 +1,22 @@
struct PS_INPUT
{
float4 pos : SV_POSITION;
float2 tex : TEXCOORD;
};
PS_INPUT main_vs(uint vI : SV_VERTEXID)
{
float idHigh = float(vI >> 1);
float idLow = float(vI & uint(1));
float x = idHigh * 4.0 - 1.0;
float y = idLow * 4.0 - 1.0;
float u = idHigh * 2.0;
float v = 1.0 - idLow * 2.0;
PS_INPUT vert_out;
vert_out.pos = float4(x, y, 0.0, 1.0);
vert_out.tex = float2(u, v);
return vert_out;
}

View File

@@ -0,0 +1,35 @@
#version 300 es
#ifdef GL_ES
precision lowp float;
#endif
uniform sampler2D image;
layout(shared) uniform ColorMatrix {
vec4 color_vec_y;
vec4 color_vec_u;
vec4 color_vec_v;
vec2 range_y;
vec2 range_uv;
};
in vec3 uuv;
layout(location = 0) out vec2 color;
//--------------------------------------------------------------------------------------
// Pixel Shader
//--------------------------------------------------------------------------------------
void main() {
vec3 rgb_left = texture(image, uuv.xz).rgb;
vec3 rgb_right = texture(image, uuv.yz).rgb;
vec3 rgb = (rgb_left + rgb_right) * 0.5;
float u = dot(color_vec_u.xyz, rgb) + color_vec_u.w;
float v = dot(color_vec_v.xyz, rgb) + color_vec_v.w;
u = u * range_uv.x + range_uv.y;
v = v * range_uv.x + range_uv.y;
color = vec2(u, v * 224.0f / 256.0f + 0.0625);
}

View File

@@ -0,0 +1,27 @@
#version 300 es
#ifdef GL_ES
precision mediump float;
#endif
uniform float width_i;
out vec3 uuv;
//--------------------------------------------------------------------------------------
// Vertex Shader
//--------------------------------------------------------------------------------------
void main()
{
float idHigh = float(gl_VertexID >> 1);
float idLow = float(gl_VertexID & int(1));
float x = idHigh * 4.0 - 1.0;
float y = idLow * 4.0 - 1.0;
float u_right = idHigh * 2.0;
float u_left = u_right - width_i;
float v = idLow * 2.0;
uuv = vec3(u_left, u_right, v);
gl_Position = vec4(x, y, 0.0, 1.0);
}

View File

@@ -0,0 +1,26 @@
#version 300 es
#ifdef GL_ES
precision lowp float;
#endif
uniform sampler2D image;
layout(shared) uniform ColorMatrix {
vec4 color_vec_y;
vec4 color_vec_u;
vec4 color_vec_v;
vec2 range_y;
vec2 range_uv;
};
in vec2 tex;
layout(location = 0) out float color;
void main()
{
vec3 rgb = texture(image, tex).rgb;
float y = dot(color_vec_y.xyz, rgb);
color = y * range_y.x + range_y.y;
}

View File

@@ -0,0 +1,14 @@
#version 300 es
#ifdef GL_ES
precision lowp float;
#endif
uniform sampler2D image;
in vec2 tex;
layout(location = 0) out vec4 color;
void main()
{
color = texture(image, tex);
}

View File

@@ -0,0 +1,22 @@
#version 300 es
#ifdef GL_ES
precision mediump float;
#endif
out vec2 tex;
void main()
{
float idHigh = float(gl_VertexID >> 1);
float idLow = float(gl_VertexID & int(1));
float x = idHigh * 4.0 - 1.0;
float y = idLow * 4.0 - 1.0;
float u = idHigh * 2.0;
float v = idLow * 2.0;
gl_Position = vec4(x, y, 0.0, 1.0);
tex = vec2(u, v);
}

View File

@@ -1,6 +1,9 @@
# If no external IP address is given, the local IP address is used
# If no external IP address is given, Sunshine will attempt to automatically detect external ip-address
# external_ip = 123.456.789.12
# Set the familly of ports used by Sunshine
# port = 47989
# The private key must be 2048 bits
# pkey = /dir/pkey.pem
@@ -32,11 +35,48 @@
# lan: Only those in LAN may access /pin
# wan: Anyone may access /pin
#
# origin_pin_allowed = lan
# origin_pin_allowed = pc
# The origin of the remote endpoint address that is not denied for HTTPS Web UI
# Could be any of the following values:
# pc|lan|wan
# pc: Only localhost may access the Web Manager
# lan: Only those in LAN may access the Web Manager
# wan: Anyone may access the Web Manager
#
# origin_web_ui_allowed = lan
# If UPnP is enabled, Sunshine will attempt to open ports for streaming over the internet
# To enable it, uncomment the following line:
# upnp = on
# The file where current state of Sunshine is stored
# file_state = sunshine_state.json
# The file where user credentials for the UI are stored
# By default, credentials are stored in `file_state`
# credentials_file = sunshine_state.json
# The display modes advertised by Sunshine
#
# Some versions of Moonlight, such as Moonlight-nx (Switch),
# rely on this list to ensure that the requested resolutions and fps
# are supported.
#
# fps = [10, 30, 60, 90, 120]
# resolutions = [
# 352x240,
# 480x360,
# 858x480,
# 1280x720,
# 1920x1080,
# 2560x1080,
# 3440x1440,
# 1920x1200,
# 3860x2160,
# 3840x1600,
# ]
# How long to wait in milliseconds for data from moonlight before shutting down the stream
# ping_timeout = 2000
@@ -50,6 +90,14 @@
# The value must be greater than 0 and lower than or equal to 100
# fec_percentage = 10
# When multicasting, it could be usefull to have different configurations for each connected Client.
# For example:
# Clients connected through WAN and LAN have different bitrate contstraints.
# Decoders may require different settings for color
#
# Unlike simply broadcasting to multiple Client, this will generate distinct video streams.
# Note, CPU usage increases for each distinct video stream generated
# channels = 1
# The back/select button on the controller
# On the Shield, the home and powerbutton are not passed to Moonlight
@@ -57,18 +105,31 @@
# If back_button_timeout < 0, then the Home/Guide button will not be emulated
# back_button_timeout = 2000
# !! Windows only !!
# Control how fast keys will repeat themselves
# The initial delay in milliseconds before repeating keys
# key_repeat_delay = 500
#
# How often keys repeat every second
# This configurable option supports decimals
# key_repeat_frequency = 24.9
# The name of the audio sink used for Audio Loopback
# If you do not specify this variable, pulseaudio will select the default monitor device.
#
# You can find the name of the audio sink using the following command:
# !! Linux only !!
# pacmd list-sources | grep "name:"
# audio_sink = alsa_output.pci-0000_09_00.3.analog-stereo.monitor
# pacmd list-sinks | grep "name:" if running vanilla pulseaudio
# pactl info | grep Source` if running pipewire
# audio_sink = alsa_output.pci-0000_09_00.3.analog-stereo
#
# !! Windows only !!
# tools\audio-info.exe
# audio_sink = {0.0.0.00000000}.{FD47D9CC-4218-4135-9CE2-0C195C87405B}
# audio_sink = {0.0.0.00000000}.{FD47D9CC-4218-4135-9CE2-0C195C87405B}
#
# The virtual sink, is the audio device that's virtual (Like Steam Streaming Speakers), it allows Sunshine
# to stream audio, while muting the speakers.
# virtual_sink = {0.0.0.00000000}.{8edba70c-1125-467c-b89c-15da389bc1d4}
# !! Windows only !!
# You can select the video card you want to stream:
@@ -77,6 +138,13 @@
# adapter_name = Radeon RX 580 Series
# output_name = \\.\DISPLAY1
# !! Linux only !!
# Set the display number to stream.
# You can find them by the following command:
# xrandr --listmonitors
# Example output: "0: +HDMI-1 1920/518x1200/324+0+0 HDMI-1"
# ^ <-- You need this.
# output_name = 0
###############################################
# FFmpeg software encoding parameters
@@ -97,15 +165,107 @@
# Increasing the value slightly reduces encoding efficiency, but the tradeoff is usually
# worth it to gain the use of more CPU cores for encoding. The ideal value is the lowest
# value that can reliably encode at your desired streaming settings on your hardware.
# min_threads = 2
# min_threads = 1
# Allows the client to request HEVC Main or HEVC Main10 video streams.
# HEVC is more CPU-intensive to encode, so enabling this may reduce performance.
# If set to 0 (default), Sunshine will not advertise support for HEVC
# If set to 1, Sunshine will advertise support for HEVC Main profile
# If set to 2, Sunshine will advertise support for HEVC Main and Main10 (HDR) profiles
# HEVC is more CPU-intensive to encode, so enabling this may reduce performance when using software encoding.
# If set to 0 (default), Sunshine will specify support for HEVC based on encoder
# If set to 1, Sunshine will not advertise support for HEVC
# If set to 2, Sunshine will advertise support for HEVC Main profile
# If set to 3, Sunshine will advertise support for HEVC Main and Main10 (HDR) profiles
# hevc_mode = 2
# Force a specific encoder, otherwise Sunshine will use the first encoder that is available
# supported encoders:
# nvenc
# amdvce # NOTE: alpha stage. The cursor is not yet displayed
# software
#
# encoder = nvenc
##################################### Software #####################################
# See x264 --fullhelp for the different presets
# preset = superfast
# tune = zerolatency
# sw_preset = superfast
# sw_tune = zerolatency
#
##################################### NVENC #####################################
###### presets ###########
# default
# hp -- high performance
# hq -- high quality
# slow -- hq 2 passes
# medium -- hq 1 pass
# fast -- hp 1 pass
# bd
# ll -- low latency
# llhq
# llhp
# lossless
# losslesshp
##########################
# nv_preset = llhq
#
####### rate control #####
# auto -- let ffmpeg decide rate control
# constqp -- constant QP mode
# vbr -- variable bitrate
# cbr -- constant bitrate
# cbr_hq -- cbr high quality
# cbr_ld_hq -- cbr low delay high quality
# vbr_hq -- vbr high quality
##########################
# nv_rc = auto
###### h264 entropy ######
# auto -- let ffmpeg nvenc decide the entropy encoding
# cabac
# cavlc
##########################
# nv_coder = auto
##################################### AMD #####################################
###### presets ###########
# default
# speed
# balanced
##########################
# amd_preset = balanced
#
####### rate control #####
# auto -- let ffmpeg decide rate control
# constqp -- constant QP mode
# vbr_latency -- Latency Constrained Variable Bitrate
# vbr_peak -- Peak Contrained Variable Bitrate
# cbr -- constant bitrate
##########################
# amd_rc = auto
###### h264 entropy ######
# auto -- let ffmpeg nvenc decide the entropy encoding
# cabac
# cavlc
##########################
# amd_coder = auto
#################################### VAAPI ###################################
####### adapter ##########
# Unlike with `amdvce` and `nvenc`, it doesn't matter if video encoding is done
# on a different GPU.
# Run the following commands:
# 1. ls /dev/dri/renderD*
# to find all devices capable of VAAPI
# 2. vainfo --display drm --device /dev/dri/renderD129 | grep -E "((VAProfileH264High|VAProfileHEVCMain|VAProfileHEVCMain10).*VAEntrypointEncSlice)|Driver version"
# Lists the name and capabilities of the device.
# To be supported by Sunshine, it needs to have at the very minimum:
# VAProfileH264High : VAEntrypointEncSlice
# adapter_name = /dev/dri/renderD128
##############################################
# Some configurable parameters, are merely toggles for specific features
# The first occurrence turns it on, the second occurence turns it off, the third occurence turns it on again, etc, etc
# Here, you change the default state of any flag
#
# To set the initial state of flags -0 and -1 to on, set the following flags:
# flags = 012
#
# See: sunshine --help for all options under the header: flags

162
assets/web/apps.html Normal file
View File

@@ -0,0 +1,162 @@
<div id="app" class="container">
<div class="my-4">
<h1>Applications</h1>
<div>Applications are refreshed only when Client is restarted</div>
</div>
<div class="card p-4">
<table class="table">
<thead>
<tr>
<th scope="col">Name</th>
<th scope="col">Actions</th>
</tr>
</thead>
<tbody>
<tr v-for="(app,i) in apps" :key="i">
<td>{{app.name}}</td>
<td><button class="btn btn-primary" @click="editApp(i)">Edit</button>
<button class="btn btn-danger" @click="showDeleteForm(i)">Delete</button>
</td>
</tr>
</tbody>
</table>
</div>
<div class="edit-form card mt-2" v-if="showEditForm">
<div class="p-4">
<!--name-->
<div class="mb-3">
<label for="appName" class="form-label">Application Name</label>
<input type="text" class="form-control" id="appName" aria-describedby="appNameHelp" v-model="editForm.name">
<div id="appNameHelp" class="form-text">Application Name, as shown on Moonlight</div>
</div>
<!--output-->
<div class="mb-3">
<label for="appOutput" class="form-label">Output</label>
<input type="text" class="form-control monospace" id="appOutput" aria-describedby="appOutputHelp"
v-model="editForm.output">
<div id="appOutputHelp" class="form-text">The file where the output of the command is stored, if it is not
specified, the output is ignored</div>
</div>
<!--prep-cmd-->
<div class="mb-3 d-flex flex-column">
<label for="appName" class="form-label">Command Preparations</label>
<div class="form-text">A list of commands to be run before/after the application. <br> If any of the
prep-commands fail, starting the application is aborted</div>
<table v-if="editForm['prep-cmd'].length > 0">
<thead>
<th class="precmd-head">Do</th>
<th class="precmd-head">Undo</th>
<th style="width: 48px;"></th>
</thead>
<tbody>
<tr v-for="(c,i) in editForm['prep-cmd']">
<td><input type="text" class="form-control monospace" v-model="c.do"></td>
<td><input type="text" class="form-control monospace" v-model="c.undo"></td>
<td><button class="btn btn-danger" @click="editForm['prep-cmd'].splice(i,1)">&times;</button></td>
</tr>
</tbody>
</table>
<button class="mt-2 btn btn-success" style="margin: 0 auto;" @click="addPrepCmd">&plus; Add</button>
</div>
<!--detatched-->
<div class="mb-3">
<label for="appName" class="form-label">Detached Commands</label>
<div v-for="(c,i) in editForm.detached" class="d-flex justify-content-between my-2">
<pre>{{c}}</pre>
<button class="btn btn-danger mx-2" @click="editForm.detached.splice(i,1)">&times;</button>
</div>
<div class="d-flex justify-content-between">
<input type="text" class="form-control monospace" v-model="detachedCmd">
<button class="btn btn-success mx-2" @click="editForm.detached.push(detachedCmd);detachedCmd = '';">+</button>
</div>
<div class="form-text">A list of commands to be run and forgotten about</div>
</div>
<!--command-->
<div class="mb-3">
<label for="appCmd" class="form-label">Command</label>
<input type="text" class="form-control monospace" id="appCmd" aria-describedby="appCmdHelp"
v-model="editForm.cmd">
<div id="appCmdHelp" class="form-text">The main application, if it is not specified, a processs is started that
sleeps indefinitely</div>
</div>
<!--buttons-->
<div class="d-flex">
<button @click="showEditForm = false" class="btn btn-secondary m-2">Cancel</button>
<button class="btn btn-primary m-2" @click="save">Save</button>
</div>
</div>
</div>
<div class="mt-2" v-else>
<button class="btn btn-primary" @click="newApp">+ Add New</button>
</div>
</div>
<script>
new Vue({
el: '#app',
data() {
return {
apps: [],
showEditForm: false,
editForm: null,
detachedCmd: '',
}
},
created() {
fetch("/api/apps").then(r => r.json()).then((r) => {
console.log(r);
this.apps = r.apps;
})
},
methods: {
newApp() {
this.editForm = {
name: '',
output: '',
cmd: [],
index: -1,
"prep-cmd": [],
"detached": []
};
this.editForm.index = -1;
this.showEditForm = true;
},
editApp(id) {
this.editForm = JSON.parse(JSON.stringify(this.apps[id]));
this.$set(this.editForm, "index", id);
if (this.editForm["prep-cmd"] === undefined) this.$set(this.editForm, "prep-cmd", []);
if (this.editForm["detached"] === undefined) this.$set(this.editForm, "detached", []);
this.showEditForm = true;
},
showDeleteForm(id) {
let resp = confirm("Are you sure to delete " + this.apps[id].name + "?");
if (resp) {
fetch("/api/apps/" + id, { method: "DELETE" }).then((r) => {
if (r.status == 200) document.location.reload();
});
}
},
addPrepCmd() {
this.editForm['prep-cmd'].push({
do: '',
undo: '',
});
},
save() {
fetch("/api/apps", { method: "POST", body: JSON.stringify(this.editForm) }).then((r) => {
if (r.status == 200) document.location.reload();
});
}
}
})
</script>
<style>
.precmd-head {
width: 200px;
}
.monospace {
font-family: monospace;
}
</style>

3
assets/web/clients.html Normal file
View File

@@ -0,0 +1,3 @@
<div id="content" class="container">
<h1>Clients</h1>
</div>

592
assets/web/config.html Normal file
View File

@@ -0,0 +1,592 @@
<div id="app" class="container">
<h1 class="my-4">Configuration</h1>
<div class="form" v-if="config">
<!--Header-->
<ul class="nav nav-tabs">
<li class="nav-item" v-for="tab in tabs" :key="tab.id">
<a class="nav-link" :class="{'active': tab.id === currentTab}" href="#"
@click="currentTab = tab.id">{{tab.name}}</a>
</li>
</ul>
<!--General Tab-->
<div v-if="currentTab === 'general'" class="config-page">
<!--Sunshine Name-->
<div class="mb-3">
<label for="sunshine_name" class="form-label">Sunshine Name</label>
<input type="text" class="form-control" id="sunshine_name" placeholder="Sunshine"
v-model="config.sunshine_name">
<div class="form-text">The name displayed by Moonlight. If not specified, the PC's hostname is used
</div>
</div>
<!--Log Level-->
<div class="mb-3">
<label for="min_log_level" class="form-label">Log Level</label>
<select id="min_log_level" class="form-select" v-model="config.min_log_level">
<option :value="0">Verbose</option>
<option :value="1">Debug</option>
<option :value="2">Info</option>
<option :value="3">Warning</option>
<option :value="4">Error</option>
<option :value="5">Fatal</option>
<option :value="6">None</option>
</select>
<div class="form-text">The minimum log level printed to standard out</div>
</div>
<!--Origin Web UI Allowed-->
<div class="mb-3">
<label for="origin_web_ui_allowed" class="form-label">Origin Web UI Allowed</label>
<select id="origin_web_ui_allowed" class="form-select" v-model="config.origin_web_ui_allowed">
<option value="pc">Only localhost may access Web UI</option>
<option value="lan">Only those in LAN may access Web UI</option>
<option value="wan">Anyone may access Web UI</option>
</select>
<div class="form-text">The origin of the remote endpoint address that is not denied access to Web UI
</div>
</div>
<!--UPnP-->
<div class="mb-3">
<label for="upnp" class="form-label">UPnP</label>
<select id="upnp" class="form-select" v-model="config.upnp">
<option value="disabled">Disabled</option>
<option value="enabled">Enabled</option>
</select>
<div class="form-text">Automatically configure port forwarding</div>
</div>
<!--Ping Timeout-->
<div class="mb-3">
<label for="ping_timeout" class="form-label">Ping Timeout</label>
<input type="text" class="form-control" id="ping_timeout" placeholder="2000"
v-model="config.ping_timeout">
<div class="form-text">How long to wait in milliseconds for data from moonlight before shutting down the
stream</div>
</div>
<!--Advertised FPS and Resolutions-->
<div class="mb-3">
<label for="ping_timeout" class="form-label">Advertised Resolutions and FPS</label>
<div class="resolutions-container">
<label>Resolutions</label>
<div class="resolutions d-flex flex-wrap">
<div class="p-2 ms-item m-2 d-flex justify-content-between" v-for="(r,i) in resolutions"
:key="r">
<span class="px-2">{{r}}</span>
<span style="cursor: pointer;" @click="resolutions.splice(i,1)">&times;</span>
</div>
<form @submit.prevent="resolutions.push(resIn);resIn = '';" class="d-flex align-items-center">
<input type="text" v-model="resIn" required pattern="[0-9]+x[0-9]+"
style="border-top-right-radius: 0;border-bottom-right-radius: 0;" class="form-control">
<button style="border-top-left-radius: 0;border-bottom-left-radius: 0;"
class="btn btn-success">+</button>
</form>
</div>
</div>
<div class="fps-container">
<label>FPS</label>
<div class="fps d-flex flex-wrap">
<div class="p-2 ms-item m-2 d-flex justify-content-between" v-for="(f,i) in fps" :key="f">
<span class="px-2">{{f}}</span>
<span style="cursor: pointer;" @click="fps.splice(i,1)">&times;</span>
</div>
<form @submit.prevent="fps.push(fpsIn);fpsIn = '';" class="d-flex align-items-center">
<input type="text" v-model="fpsIn" required pattern="[0-9]+"
style="width: 6ch;border-top-right-radius: 0;border-bottom-right-radius: 0;"
class="form-control">
<button style="border-top-left-radius: 0;border-bottom-left-radius: 0;"
class="btn btn-success">+</button>
</form>
</div>
</div>
<div class="form-text">
The display modes advertised by Sunshine<br>
Some versions of Moonlight, such as Moonlight-nx (Switch),
rely on this list to ensure that the requested resolutions and fps
are supported.
</div>
</div>
</div>
<!--Files Tab-->
<div v-if="currentTab === 'files'" class="config-page">
<!--Private Key-->
<div class="mb-3">
<label for="pkey" class="form-label">Private Key</label>
<input type="text" class="form-control" id="pkey" placeholder="/dir/pkey.pem" v-model="config.pkey">
<div class="form-text">The private key must be 2048 bits</div>
</div>
<!--Cert-->
<div class="mb-3">
<label for="cert" class="form-label">Cert</label>
<input type="text" class="form-control" id="cert" placeholder="/dir/cert.pem" v-model="config.cert">
<div class="form-text">The certificate must be signed with a 2048 bit key</div>
</div>
<!--State File-->
<div class="mb-3">
<label for="file_state" class="form-label">State File</label>
<input type="text" class="form-control" id="file_state" placeholder="sunshine_state.json"
v-model="config.file_state">
<div class="form-text">The file where current state of Sunshine is stored</div>
</div>
<!--Apps File-->
<div class="mb-3">
<label for="file_apps" class="form-label">Apps File</label>
<input type="text" class="form-control" id="file_apps" placeholder="apps.json"
v-model="config.file_apps">
<div class="form-text">The file where current apps of Sunshine are stored</div>
</div>
</div>
<div v-if="currentTab === 'input'" class="config-page">
<!--Back Button Timeout-->
<div class="mb-3">
<label for="back_button_timeout" class="form-label">Back Button Timeout</label>
<input type="text" class="form-control" id="back_button_timeout" placeholder="2000"
v-model="config.back_button_timeout">
<div class="form-text">
The back/select button on the controller.<br>
On the Shield, the home and powerbutton are not passed to Moonlight.<br>
If, after the timeout, the back button is still pressed down, Home/Guide button press is
emulated.<br>
If back_button_timeout &lt; 0, then the Home/Guide button will not be emulated<br>
</div>
</div>
<!-- Key Repeat Delay-->
<div class="mb-3" v-if="platform === 'windows'">
<label for="key_repeat_delay" class="form-label">Key Repeat Delay</label>
<input type="text" class="form-control" id="key_repeat_delay" placeholder="500"
v-model="config.key_repeat_delay">
<div class="form-text">
Control how fast keys will repeat themselves<br>
The initial delay in milliseconds before repeating keys
</div>
</div>
<!-- Key Repeat Frequency-->
<div class="mb-3" v-if="platform === 'windows'">
<label for="key_repeat_frequency" class="form-label">Key Repeat Frequency</label>
<input type="text" class="form-control" id="key_repeat_frequency" placeholder="24.9"
v-model="config.key_repeat_frequency">
<div class="form-text">
How often keys repeat every second<br>
This configurable option supports decimals
</div>
</div>
</div>
<!--Files Tab-->
<div v-if="currentTab === 'av'" class="config-page">
<!--Audio Sink-->
<div class="mb-3" v-if="platform === 'windows'">
<label for="audio_sink" class="form-label">Audio Sink</label>
<input type="text" class="form-control" id="audio_sink"
placeholder="{0.0.0.00000000}.{FD47D9CC-4218-4135-9CE2-0C195C87405B}" v-model="config.audio_sink">
<div class="form-text">
The name of the audio sink used for Audio Loopback<br>
You can find the name of the audio sink using the following command:<br>
<pre>tools\audio-info.exe</pre>
</div>
</div>
<div class="mb-3" v-if="platform === 'linux'">
<label for="audio_sink" class="form-label">Audio Sink</label>
<input type="text" class="form-control" id="audio_sink"
placeholder="alsa_output.pci-0000_09_00.3.analog-stereo" v-model="config.audio_sink">
<div class="form-text">
The name of the audio sink used for Audio Loopback<br>
If you do not specify this variable, pulseaudio will select the default monitor device.<br>
<br>
You can find the name of the audio sink using either command:<br>
<pre>pacmd list-sinks | grep "name:"</pre>
<pre>pactl info | grep Source</pre><br>
</div>
</div>
<!--Virtual Sink-->
<div class="mb-3" v-if="platform === 'windows'">
<label for="virtual_sink" class="form-label">Virtual Sink</label>
<input type="text" class="form-control" id="virtual_sink"
placeholder="{0.0.0.00000000}.{8edba70c-1125-467c-b89c-15da389bc1d4}" v-model="config.virtual_sink">
<div class="form-text">
The virtual sink, is the audio device that's virtual (Like Steam Streaming Speakers), it allows
Sunshine
to stream audio, while muting the speakers.
</div>
</div>
<!--Adapter Name -->
<div class="mb-3" v-if="platform === 'windows'">
<label for="adapter_name" class="form-label">Adapter Name</label>
<input type="text" class="form-control" id="adapter_name" placeholder="Radeon RX 580 Series"
v-model="config.adapter_name">
<div class="form-text" v-if="platform === 'windows'">
You can select the video card you want to stream:<br>
The appropriate values can be found using the following command:<br>
<pre>tools\dxgi-info.exe</pre>
</div>
</div>
<!--Output Name -->
<div class="mb-3" class="config-page" v-if="platform === 'windows'">
<label for="output_name" class="form-label">Output Name</label>
<input type="text" class="form-control" id="output_name" placeholder="\\.\DISPLAY1"
v-model="config.output_name">
<div class="form-text">
You can select the video card you want to stream:<br>
The appropriate values can be found using the following command:<br>
tools\dxgi-info.exe<br><br>
</div>
</div>
<div class="mb-3" class="config-page" v-if="platform === 'linux'">
<label for="output_name" class="form-label">Monitor number</label>
<input type="text" class="form-control" id="output_name" placeholder="0" v-model="config.output_name">
<div class="form-text">
xrandr --listmonitors<br>
Example output:
<pre> 0: +HDMI-1 1920/518x1200/324+0+0 HDMI-1</pre>
</div>
</div>
</div>
<div v-if="currentTab === 'advanced'" class="config-page">
<!--Port familly-->
<div class="mb-3">
<label for="port" class="form-label">Port</label>
<input type="number" min="0" max="65529" class="form-control" id="port" placeholder="47989"
v-model="config.port">
<div class="form-text">
Set the familly of ports used by Sunshine
</div>
</div>
<!--Constant Rate Factor-->
<div class="mb-3">
<label for="crf" class="form-label">Constant Rate Factor</label>
<input type="number" min="0" max="52" class="form-control" id="crf" placeholder="0"
v-model="config.crf">
<div class="form-text">
Constant Rate Factor. Between 1 and 52. It allows QP to go up during motion and down with still
image,
resulting in constant perceived quality<br>
Higher value means more compression, but less quality<br>
If crf == 0, then use QP directly instead
</div>
</div>
<!-- Quantization Parameter -->
<div class="mb-3">
<label for="qp" class="form-label">Quantitization Parameter</label>
<input type="number" class="form-control" id="qp" placeholder="28" v-model="config.qp">
<div class="form-text">
Quantitization Parameter<br>
Higher value means more compression, but less quality<br>
If crf != 0, then this parameter is ignored
</div>
</div>
<!-- Min Threads -->
<div class="mb-3">
<label for="min_threads" class="form-label">Minimum number of threads used by ffmpeg to encode the
video.</label>
<input type="number" min="1" class="form-control" id="min_threads" placeholder="1"
v-model="config.min_threads">
<div class="form-text">
Minimum number of threads used by ffmpeg to encode the video.<br>
Increasing the value slightly reduces encoding efficiency, but the tradeoff is usually<br>
worth it to gain the use of more CPU cores for encoding. The ideal value is the lowest<br>
value that can reliably encode at your desired streaming settings on your hardware.
</div>
</div>
<!--HEVC Suppport -->
<div class="mb-3">
<label for="hevc_mode" class="form-label">HEVC Support</label>
<select id="hevc_mode" class="form-select" v-model="config.hevc_mode">
<option value="0">Sunshine will specify support for HEVC based on encoder</option>
<option value="1">Sunshine will not advertise support for HEVC</option>
<option value="2">Sunshine will advertise support for HEVC Main profile</option>
<option value="3">Sunshine will advertise support for HEVC Main and Main10 (HDR) profiles
</option>
</select>
<div class="form-text">
Allows the client to request HEVC Main or HEVC Main10 video streams.<br>
HEVC is more CPU-intensive to encode, so enabling this may reduce performance when using
software
encoding.
</div>
</div>
<!--Encoder -->
<div class="mb-3">
<label for="encoder" class="form-label">Force a Specific Encoder</label>
<select id="encoder" class="form-select" v-model="config.encoder">
<option :value="''">Autodetect</option>
<option value="nvenc">nVidia NVENC</option>
<option value="amdvce">AMD AMF/VCE</option>
<option value="vaapi">VA-API</option>
<option value="software">Software</option>
</select>
<div class="form-text">
Force a specific encoder, otherwise Sunshine will use the first encoder that is available
</div>
</div>
<!--FEC Percentage-->
<div class="mb-3">
<label for="fec_percentage" class="form-label">FEC Percentage</label>
<input type="text" class="form-control" id="fec_percentage" placeholder="10"
v-model="config.fec_percentage">
<div class="form-text">
How much error correcting packets must be send for every video.<br>
This is just some random number, don't know the optimal value.<br>
The higher fec_percentage, the lower space for the actual data to send per frame there is
</div>
</div>
<!--Channels-->
<div class="mb-3">
<label for="channels" class="form-label">Channels</label>
<input type="text" class="form-control" id="channels" placeholder="1" v-model="config.channels">
<div class="form-text">
When multicasting, it could be useful to have different configurations for each connected
Client.
For example:
<ul>
<li>Clients connected through WAN and LAN have different bitrate contstraints.</li>
<li>Decoders may require different settings for color</li>
</ul>
Unlike simply broadcasting to multiple Client, this will generate distinct video streams.<br>
Note, CPU usage increases for each distinct video stream generated
</div>
</div>
<!--Credentials File-->
<div class="mb-3">
<label for="credentials_file" class="form-label">Web Manager Credentials File</label>
<input type="text" class="form-control" id="credentials_file" placeholder="sunshine_state.json"
v-model="config.credentials_file">
<div class="form-text">
Store Username/Password seperately from Sunshine's state file.
</div>
</div>
<!--Origin PIN Allowed-->
<div class="mb-3">
<label for="origin_pin_allowed" class="form-label">Origin PIN Allowed</label>
<select id="origin_pin_allowed" class="form-select" v-model="config.origin_pin_allowed">
<option value="pc">Only localhost may access /pin</option>
<option value="lan">Only those in LAN may access /pin</option>
<option value="wan">Anyone may access /pin</option>
</select>
<div class="form-text">The origin of the remote endpoint address that is not denied for HTTP method /pin
</div>
</div>
<!--External IP-->
<div class="mb-3">
<label for="external_ip" class="form-label">External IP</label>
<input type="text" class="form-control" id="external_ip" placeholder="123.456.789.12"
v-model="config.external_ip">
<div class="form-text">If no external IP address is given, Sunshine will automatically detect external
IP</div>
</div>
</div>
<!--Software Settings-->
<div v-if="currentTab === 'sw'" class="config-page">
<div class="mb-3">
<label for="sw_preset" class="form-label">SW Presets</label>
<input class="form-control" id="sw_preset" placeholder="superfast" v-model="config.sw_preset">
</div>
<div class="mb-3">
<label for="sw_tune" class="form-label">SW Tune</label>
<input class="form-control" id="sw_tune" placeholder="zerolatency" v-model="config.sw_tune">
</div>
</div>
<!--Nvidia Encoder Settings-->
<div v-if="currentTab === 'nv'" class="config-page">
<!--NVENC SETTINGS-->
<div class="mb-3">
<label for="nv_preset" class="form-label">NVEnc Preset</label>
<select id="nv_preset" class="form-select" v-model="config.nv_preset">
<option value="default">Default</option>
<option value="hp">High Performance</option>
<option value="hq">High Quality</option>
<option value="slow">Slow - hq 2 passes</option>
<option value="medium">medium -- hq 1 pass</option>
<option value="fast">fast -- hp 1 pass</option>
<option value="bd">bd</option>
<option value="ll">ll -- low latency</option>
<option value="llhq">llhq</option>
<option value="llhp">llhp</option>
<option value="lossless">lossless</option>
<option value="losslesshp">losslesshp</option>
</select>
</div>
<div class="mb-3">
<label for="nv_rc" class="form-label">NVEnc Rate Control</label>
<select id="nv_rc" class="form-select" v-model="config.nv_rc">
<option value="auto">auto -- let ffmpeg decide rate control</option>
<option value="constqp">constqp -- constant QP mode</option>
<option value="vbr">vbr -- variable bitrate</option>
<option value="cbr">cbr -- constant bitrate</option>
<option value="cbr_hq">cbr_hq -- cbr high quality</option>
<option value="cbr_ld_hq">cbr_ld_hq -- cbr low delay high quality</option>
<option value="vbr_hq">vbr_hq -- vbr high quality</option>
</select>
</div>
<div class="mb-3">
<label for="nv_coder" class="form-label">NVEnc Coder</label>
<select id="nv_coder" class="form-select" v-model="config.nv_coder">
<option value="auto">auto</option>
<option value="cabac">cabac</option>
<option value="cavlc">cavlc</option>
</select>
</div>
</div>
<!--AMD Encoder Settings-->
<div v-if="currentTab === 'amd'" class="config-page">
<!--Presets-->
<div class="mb-3">
<label for="amd_quality" class="form-label">AMD AMF Quality</label>
<select id="amd_quality" class="form-select" v-model="config.amd_quality">
<option value="default">Default</option>
<option value="speed">Speed</option>
<option value="balanced">Balanced</option>
</select>
</div>
<div class="mb-3">
<label for="amd_rc" class="form-label">AMD AMF Rate Control</label>
<select id="amd_rc" class="form-select" v-model="config.amd_rc">
<option value="auto">auto -- let ffmpeg decide rate control</option>
<option value="constqp">constqp -- constant QP mode</option>
<option value="vbr_latency">vbr_latency -- Latency Constrained Variable Bitrate</option>
<option value="vbr_peak">vbr_peak -- Peak Contrained Variable Bitrate</option>
<option value="cbr">cbr -- constant bitrate</option>
</select>
</div>
<div class="mb-3">
<label for="amd_coder" class="form-label">AMD AMF Rate Control</label>
<select id="amd_coder" class="form-select" v-model="config.amd_coder">
<option value="auto">auto</option>
<option value="cabac">cabac</option>
<option value="cavlc">cavlc</option>
</select>
</div>
</div>
<div v-if="currentTab === 'va-api'" class="config-page">
<input class="form-control" id="adapter_name" placeholder="/dev/dri/renderD128"
v-model="config.adapter_name">
</div>
</div>
<div class="alert alert-success my-4" v-if="success"><b>Success!</b> Restart Sunshine to apply changes</div>
<div class="mb-3 buttons">
<button class="btn btn-primary" @click="save">Save</button>
</div>
</div>
<script>
new Vue({
el: '#app',
data() {
return {
platform: '',
success: false,
config: null,
fps: [],
resolutions: [],
currentTab: 'general',
resIn: '',
fpsIn: '',
tabs: [{
id: 'general',
name: "General"
},
{
id: 'files',
name: "Files"
},
{
id: 'input',
name: "Input"
},
{
id: 'av',
name: "Audio/Video"
},
{
id: 'advanced',
name: "Advanced"
},
{
id: "sw",
name: "Software Encoder"
},
{
id: "nv",
name: "NVENC Encoder"
},
{
id: "amd",
name: "AMF Encoder"
},
{
id: "va-api",
name: "VA-API encoder"
}
]
}
},
created() {
fetch("/api/config").then(r => r.json()).then((r) => {
this.config = r;
this.platform = this.config.platform;
var app = document.getElementById("app");
if (this.platform == "windows") {
this.tabs = this.tabs.filter(el => {
return el.id !== "va-api";
});
}
if (this.platform == "linux") {
this.tabs = this.tabs.filter(el => {
return el.id !== "nv" && el.id !== "amd";
});
}
delete this.config.status;
delete this.config.platform;
//Populate default values if not present in config
this.config.upnp = this.config.upnp || 'disabled';
this.config.min_log_level = this.config.min_log_level || 2;
this.config.origin_pin_allowed = this.config.origin_pin_allowed || "pc";
this.config.origin_web_ui_allowed = this.config.origin_web_manager_allowed || "lan";
this.config.hevc_mode = this.config.hevc_mode || 0;
this.config.encoder = this.config.encoder || '';
this.config.nv_preset = this.config.nv_preset || 'default';
this.config.nv_rc = this.config.nv_rc || 'auto';
this.config.nv_coder = this.config.nv_coder || 'auto';
this.config.amd_quality = this.config.amd_quality || 'default';
this.config.amd_rc = this.config.amd_rc || 'auto';
this.config.fps = this.config.fps || '[10, 30, 60, 90, 120]';
this.config.resolutions = this.config.resolutions || '[352x240,480x360,858x480,1280x720,1920x1080,2560x1080,3440x1440,1920x1200,3860x2160,3840x1600]';
this.fps = JSON.parse(this.config.fps);
//Resolutions should be fixed because are not valid JSON
let res = this.config.resolutions.substring(1, this.config.resolutions.length - 1);
let resolutions = [];
res.split(",").forEach(r => resolutions.push(r.trim()));
this.resolutions = resolutions;
})
},
methods: {
save() {
this.success = false;
let nl = this.config === 'windows' ? "\r\n" : "\n";
this.config.resolutions = "[" + nl + " " + this.resolutions.join("," + nl + " ") + nl + "]";
this.config.fps = JSON.stringify(this.fps);
fetch("/api/config", {
method: "POST",
body: JSON.stringify(this.config)
}).then((r) => {
if (r.status == 200) this.success = true;
});
}
}
})
</script>
<style>
.config-page {
padding: 1em;
border: 1px solid #dee2e6;
border-top: none;
}
.buttons {
padding: 1em 0;
}
.ms-item {
background-color: #CCC;
font-size: 12px;
font-weight: bold;
}
</style>

45
assets/web/header.html Normal file
View File

@@ -0,0 +1,45 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Sunshine</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.0/dist/css/bootstrap.min.css" rel="stylesheet"
integrity="sha384-wEmeIV1mKuiNpC+IOBjI7aAzPcEZeedi5yW5f2yOq55WWLwNGmvvx4Um1vskeMj0" crossorigin="anonymous">
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.0/dist/js/bootstrap.bundle.min.js"
integrity="sha384-p34f1UUtsS3wqzfto5wAAmdvj+osOnFyQFpp4Ua3gs/ZVWx6oOypYoCJhGGScy+8" crossorigin="anonymous">
</script>
<script src="https://cdn.jsdelivr.net/npm/vue@2.6.12/dist/vue.js"></script>
</head>
<body>
<nav class="navbar navbar-expand-lg navbar-light" style="background-color: #ffc400;">
<div class="container-fluid">
<span class="navbar-brand">Sunshine</span>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse"
data-bs-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false"
aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarSupportedContent">
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
<li class="nav-item">
<a class="nav-link" href="/">Home</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/pin">PIN</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/apps">Applications</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/config">Configuration</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/password">Change Password</a>
</li>
</ul>
</div>
</nav>

9
assets/web/index.html Normal file
View File

@@ -0,0 +1,9 @@
<div id="content" class="container">
<div class="row">
<div class="col-md-6 py-4" style="margin: 0 auto;">
<h1>Hello, Sunshine!</h1>
<p>Sunshine is a Gamestream host for Moonlight</p>
<a href="https://github.com/loki-47-6F-64/sunshine">Official GitHub Repository</a>
</div>
</div>
</div>

97
assets/web/password.html Normal file
View File

@@ -0,0 +1,97 @@
<div id="app" class="container">
<h1 class="my-4">Password Change</h1>
<form @submit.prevent="save">
<div class="card d-flex p-4 flex-row">
<div class="col-md-6 px-4">
<h4>Current Credentials</h4>
<div class="mb-3">
<label for="currentUsername" class="form-label">Username</label>
<input required type="text" class="form-control" id="currentUsername" v-model="passwordData.currentUsername">
<div class="form-text">&nbsp;</div>
</div>
<div class="mb-3">
<label for="currentPassword" class="form-label">Password</label>
<input autocomplete="current-password" type="password" class="form-control" id="currentPassword" v-model="passwordData.currentPassword">
</div>
</div>
<div class="col-md-6 px-4">
<h4>New Credentials</h4>
<div class="mb-3">
<label for="newUsername" class="form-label">New Username</label>
<input type="text" class="form-control" id="newUsername" v-model="passwordData.newUsername">
<div class="form-text">If not specified, the username will not change
</div>
</div>
<div class="mb-3">
<label for="newPassword" class="form-label">Password</label>
<input autocomplete="new-password" required type="password" class="form-control" id="newPassword" v-model="passwordData.newPassword">
</div>
<div class="mb-3">
<label for="confirmNewPassword" class="form-label">Confirm Password</label>
<input autocomplete="new-password" required type="password" class="form-control" id="confirmNewPassword" v-model="passwordData.confirmNewPassword">
</div>
</div>
</div>
<div class="alert alert-danger" v-if="error"><b>Error: </b>{{error}}</div>
<div class="alert alert-success" v-if="success"><b>Success! </b>This page will reload soon, your browser will ask you for the new credentials</div>
<div class="mb-3 buttons">
<button class="btn btn-primary">Save</button>
</div>
</form>
</div>
<script>
new Vue({
el: '#app',
data() {
return {
error: null,
success: false,
passwordData: {
currentUsername: '',
currentPassword: '',
newUsername: '',
newPassword: '',
confirmNewPassword: ''
}
}
},
methods: {
save() {
this.error = null;
fetch("/api/password", {
method: "POST",
body: JSON.stringify(this.passwordData)
}).then((r) => {
if (r.status == 200){
r.json().then((rj) => {
if(rj.status.toString() === "true"){
this.success = true;
setTimeout(()=>{
document.location.reload();
},5000);
} else {
this.error = rj.error;
}
})
}
else {
this.error = "Internal Server Error"
}
});
}
}
})
</script>
<style>
.config-page {
padding: 1em;
border: 1px solid #dee2e6;
border-top: none;
}
.buttons {
padding: 1em 0;
}
</style>

31
assets/web/pin.html Normal file
View File

@@ -0,0 +1,31 @@
<div id="content" class="container">
<h1 class="my-4">PIN Pairing</h1>
<form action="" class="form d-flex flex-column align-items-center" id="form">
<div class="card flex-column d-flex p-4 mb-4">
<input type="number" placeholder="PIN" id="pin-input" class="form-control my-4">
<button class="btn btn-primary">Send</button>
</div>
<div class="alert alert-warning">
<b>Warning!</b> Make sure you have access to the client you are pairing with.<br>
This software can give total control to your computer, so be careful!
</div>
<div id="status"></div>
</form>
</div>
<script>
document.querySelector("#form").addEventListener("submit", (e) => {
e.preventDefault();
let pin = document.querySelector("#pin-input").value;
document.querySelector("#status").innerHTML = "";
let b = JSON.stringify({pin: pin});
fetch("/api/pin",{method: "POST",body: b}).then((response) => response.json()).then((response)=>{
if(response.status){
document.querySelector("#status").innerHTML = `<div class="alert alert-success" role="alert">Success! Please check Moonlight to continue</div>`;
} else {
document.querySelector("#status").innerHTML = `<div class="alert alert-danger" role="alert">PIN does not match, please check if it's typed correctly</div>`;
}
})
})
</script>

View File

@@ -35,8 +35,8 @@ Package: sunshine
Architecture: amd64
Maintainer: @loki
Priority: optional
Version: 0.1.1
Depends: libssl | libavdevice | libboost-thread (>= 1.67) | libboost-filesystem (>= 1.67) | libboost-log (>= 1.67) | libpulse | libopus | libxtst | libx11 | libxfixes | libevdev | libxcb1 | libxcb-shm0 | libxcb-xfixes0
Version: 0.8.0
Depends: libssl1.1, libavdevice58, libboost-thread1.67.0 | libboost-thread1.71.0, libboost-filesystem1.67.0 | libboost-filesystem1.71.0, libboost-log1.67.0 | libboost-log1.71.0, libpulse0, libopus0, libxcb-shm0, libxcb-xfixes0, libxtst6, libevdev2
Description: Gamestream host for Moonlight
EOF
@@ -55,22 +55,35 @@ else
echo "Warning: /etc/group not found"
fi
# Prevent necessity of rebooting system
chmod 0660 /dev/uinput
chown root:$GROUP_INPUT /dev/uinput
# Update permissions on config files for Web Manager
if [ -f /etc/sunshine/apps_linux.json ]; then
echo "chmod 666 /etc/sunshine/apps_linux.json"
chmod 666 /etc/sunshine/apps_linux.json
fi
if [ -f /etc/sunshine/sunshine.conf ]; then
echo "chmod 666 /etc/sunshine/sunshine.conf"
chmod 666 /etc/sunshine/sunshine.conf
fi
EOF
cat << 'EOF' > $RULES/85-sunshine-rules.rules
KERNEL=="uinput", GROUP="input", MODE="0660"
EOF
mkdir -p $ASSETS/shaders
cp sunshine $BIN/sunshine
cp @CMAKE_CURRENT_SOURCE_DIR@/assets/apps_linux.json $ASSETS/apps_linux.json
cp @CMAKE_CURRENT_SOURCE_DIR@/assets/sunshine.conf $ASSETS/sunshine.conf
cp -r @CMAKE_CURRENT_SOURCE_DIR@/assets/web $ASSETS/web
cp -r @CMAKE_CURRENT_SOURCE_DIR@/assets/shaders/opengl $ASSETS/shaders/opengl
chmod 755 $DEBIAN/postinst
chmod 755 $BIN/sunshine
chmod 644 $RULES/85-sunshine-rules.rules
chmod 666 $ASSETS/apps_linux.json
chmod 666 $ASSETS/sunshine.conf
cd package-deb
if fakeroot dpkg-deb --build sunshine; then

Submodule pre-compiled deleted from 51f776dbd4

View File

@@ -2,12 +2,7 @@
Description=Sunshine Gamestream Server for Moonlight
[Service]
WorkingDirectory=/home/%u
Environment="DISPLAY=:0"
Type=simple
# wait for Xorg
ExecStartPre=/bin/sh -c 'while ! pgrep Xorg; do sleep 2; done'
ExecStart=@SUNSHINE_EXECUTABLE_PATH@
[Install]
WantedBy=default.target
WantedBy=graphical-session.target

View File

@@ -4,129 +4,242 @@
#include "platform/common.h"
#include "utility.h"
#include "thread_safe.h"
#include "audio.h"
#include "config.h"
#include "main.h"
#include "thread_safe.h"
#include "utility.h"
namespace audio {
using namespace std::literals;
using opus_t = util::safe_ptr<OpusMSEncoder, opus_multistream_encoder_destroy>;
using opus_t = util::safe_ptr<OpusMSEncoder, opus_multistream_encoder_destroy>;
using sample_queue_t = std::shared_ptr<safe::queue_t<std::vector<std::int16_t>>>;
struct opus_stream_config_t {
std::int32_t sampleRate;
int channelCount;
int streams;
int coupledStreams;
const std::uint8_t *mapping;
struct audio_ctx_t {
// We want to change the sink for the first stream only
std::unique_ptr<std::atomic_bool> sink_flag;
std::unique_ptr<platf::audio_control_t> control;
bool restore_sink;
platf::sink_t sink;
};
constexpr std::uint8_t map_stereo[] { 0, 1 };
constexpr std::uint8_t map_surround51[] {0, 4, 1, 5, 2, 3};
constexpr std::uint8_t map_high_surround51[] {0, 1, 2, 3, 4, 5};
static int start_audio_control(audio_ctx_t &ctx);
static void stop_audio_control(audio_ctx_t &);
int map_stream(int channels, bool quality);
constexpr auto SAMPLE_RATE = 48000;
static opus_stream_config_t stereo = {
opus_stream_config_t stream_configs[MAX_STREAM_CONFIG] {
{
SAMPLE_RATE,
2,
1,
1,
map_stereo
};
static opus_stream_config_t Surround51 = {
platf::speaker::map_stereo,
},
{
SAMPLE_RATE,
6,
4,
2,
map_surround51
};
static opus_stream_config_t HighSurround51 = {
platf::speaker::map_surround51,
},
{
SAMPLE_RATE,
6,
6,
0,
map_high_surround51
platf::speaker::map_surround51,
},
{
SAMPLE_RATE,
8,
5,
3,
platf::speaker::map_surround71,
},
{
SAMPLE_RATE,
8,
8,
0,
platf::speaker::map_surround71,
},
};
void encodeThread(std::shared_ptr<safe::queue_t<packet_t>> packets, sample_queue_t samples, config_t config) {
auto control_shared = safe::make_shared<audio_ctx_t>(start_audio_control, stop_audio_control);
void encodeThread(sample_queue_t samples, config_t config, void *channel_data) {
auto packets = mail::man->queue<packet_t>(mail::audio_packets);
//FIXME: Pick correct opus_stream_config_t based on config.channels
auto stream = &stereo;
opus_t opus { opus_multistream_encoder_create(
stream->sampleRate,
stream->channelCount,
stream->streams,
stream->coupledStreams,
stream->mapping,
OPUS_APPLICATION_AUDIO,
nullptr)
};
auto stream = &stream_configs[map_stream(config.channels, config.flags[config_t::HIGH_QUALITY])];
opus_t opus { opus_multistream_encoder_create(
stream->sampleRate,
stream->channelCount,
stream->streams,
stream->coupledStreams,
stream->mapping,
OPUS_APPLICATION_AUDIO,
nullptr) };
opus_multistream_encoder_ctl(opus.get(), OPUS_SET_VBR(0));
auto frame_size = config.packetDuration * stream->sampleRate / 1000;
while(auto sample = samples->pop()) {
packet_t packet { 16*1024 }; // 16KB
buffer_t packet { 1024 }; // 1KB
int bytes = opus_multistream_encode(opus.get(), sample->data(), frame_size, std::begin(packet), packet.size());
if(bytes < 0) {
BOOST_LOG(error) << opus_strerror(bytes);
BOOST_LOG(error) << "Couldn't encode audio: "sv << opus_strerror(bytes);
packets->stop();
return;
}
packet.fake_resize(bytes);
packets->raise(std::move(packet));
packets->raise(channel_data, std::move(packet));
}
}
void capture(std::shared_ptr<safe::queue_t<packet_t>> packets, config_t config) {
auto samples = std::make_shared<sample_queue_t::element_type>();
std::thread thread { encodeThread, packets, samples, config };
auto fg = util::fail_guard([&]() {
packets->stop();
samples->stop();
thread.join();
});
void capture(safe::mail_t mail, config_t config, void *channel_data) {
auto shutdown_event = mail->event<bool>(mail::shutdown);
//FIXME: Pick correct opus_stream_config_t based on config.channels
auto stream = &stereo;
auto stream = &stream_configs[map_stream(config.channels, config.flags[config_t::HIGH_QUALITY])];
auto mic = platf::microphone(stream->sampleRate);
if(!mic) {
BOOST_LOG(error) << "Couldn't create audio input"sv ;
auto ref = control_shared.ref();
if(!ref) {
return;
}
auto &control = ref->control;
if(!control) {
BOOST_LOG(error) << "Couldn't create audio control"sv;
return;
}
auto frame_size = config.packetDuration * stream->sampleRate / 1000;
std::string *sink =
config::audio.sink.empty() ? &ref->sink.host : &config::audio.sink;
if(ref->sink.null) {
auto &null = *ref->sink.null;
switch(stream->channelCount) {
case 2:
sink = &null.stereo;
break;
case 6:
sink = &null.surround51;
break;
case 8:
sink = &null.surround71;
break;
}
}
// Only the first to start a session may change the default sink
if(!ref->sink_flag->exchange(true, std::memory_order_acquire)) {
ref->restore_sink = !config.flags[config_t::HOST_AUDIO];
// If the client requests audio on the host, don't change the default sink
if(!config.flags[config_t::HOST_AUDIO] && control->set_sink(*sink)) {
return;
}
}
auto samples = std::make_shared<sample_queue_t::element_type>(30);
std::thread thread { encodeThread, samples, config, channel_data };
auto fg = util::fail_guard([&]() {
samples->stop();
thread.join();
shutdown_event->view();
});
auto frame_size = config.packetDuration * stream->sampleRate / 1000;
int samples_per_frame = frame_size * stream->channelCount;
while(packets->running()) {
auto mic = control->microphone(stream->mapping, stream->channelCount, stream->sampleRate, frame_size);
if(!mic) {
BOOST_LOG(error) << "Couldn't create audio input"sv;
return;
}
while(!shutdown_event->peek()) {
std::vector<std::int16_t> sample_buffer;
sample_buffer.resize(samples_per_frame);
auto status = mic->sample(sample_buffer);
switch(status) {
case platf::capture_e::ok:
break;
case platf::capture_e::timeout:
continue;
case platf::capture_e::reinit:
mic.reset();
mic = platf::microphone(stream->sampleRate);
if(!mic) {
BOOST_LOG(error) << "Couldn't re-initialize audio input"sv ;
case platf::capture_e::ok:
break;
case platf::capture_e::timeout:
continue;
case platf::capture_e::reinit:
mic.reset();
mic = control->microphone(stream->mapping, stream->channelCount, stream->sampleRate, frame_size);
if(!mic) {
BOOST_LOG(error) << "Couldn't re-initialize audio input"sv;
return;
}
return;
default:
return;
}
return;
default:
return;
}
samples->raise(std::move(sample_buffer));
}
}
int map_stream(int channels, bool quality) {
int shift = quality ? 1 : 0;
switch(channels) {
case 2:
return STEREO;
case 6:
return SURROUND51 + shift;
case 8:
return SURROUND71 + shift;
}
return STEREO;
}
int start_audio_control(audio_ctx_t &ctx) {
ctx.sink_flag = std::make_unique<std::atomic_bool>(false);
if(!(ctx.control = platf::audio_control())) {
return -1;
}
auto sink = ctx.control->sink_info();
if(!sink) {
return -1;
}
// The default sink has not been replaced yet.
ctx.restore_sink = false;
ctx.sink = std::move(*sink);
return 0;
}
void stop_audio_control(audio_ctx_t &ctx) {
// restore audio-sink if applicable
if(!ctx.restore_sink) {
return;
}
const std::string &sink = config::audio.sink.empty() ? ctx.sink.host : config::audio.sink;
if(!sink.empty()) {
// Best effort, it's allowed to fail
ctx.control->set_sink(sink);
}
}
} // namespace audio

View File

@@ -1,18 +1,45 @@
#ifndef SUNSHINE_AUDIO_H
#define SUNSHINE_AUDIO_H
#include "utility.h"
#include "thread_safe.h"
#include "utility.h"
namespace audio {
enum stream_config_e : int {
STEREO,
SURROUND51,
HIGH_SURROUND51,
SURROUND71,
HIGH_SURROUND71,
MAX_STREAM_CONFIG
};
struct opus_stream_config_t {
std::int32_t sampleRate;
int channelCount;
int streams;
int coupledStreams;
const std::uint8_t *mapping;
};
extern opus_stream_config_t stream_configs[MAX_STREAM_CONFIG];
struct config_t {
enum flags_e : int {
HIGH_QUALITY,
HOST_AUDIO,
MAX_FLAGS
};
int packetDuration;
int channels;
int mask;
std::bitset<MAX_FLAGS> flags;
};
using packet_t = util::buffer_t<std::uint8_t>;
using packet_queue_t = std::shared_ptr<safe::queue_t<packet_t>>;
void capture(std::shared_ptr<safe::queue_t<packet_t>> packets, config_t config);
}
using buffer_t = util::buffer_t<std::uint8_t>;
using packet_t = std::pair<void *, buffer_t>;
void capture(safe::mail_t mail, config_t config, void *channel_data);
} // namespace audio
#endif

300
sunshine/cbs.cpp Normal file
View File

@@ -0,0 +1,300 @@
extern "C" {
#include <cbs/cbs_h264.h>
#include <cbs/cbs_h265.h>
#include <cbs/video_levels.h>
#include <libavcodec/avcodec.h>
#include <libavutil/pixdesc.h>
}
#include "cbs.h"
#include "main.h"
#include "utility.h"
using namespace std::literals;
namespace cbs {
void close(CodedBitstreamContext *c) {
ff_cbs_close(&c);
}
using ctx_t = util::safe_ptr<CodedBitstreamContext, close>;
class frag_t : public CodedBitstreamFragment {
public:
frag_t(frag_t &&o) {
std::copy((std::uint8_t *)&o, (std::uint8_t *)(&o + 1), (std::uint8_t *)this);
o.data = nullptr;
o.units = nullptr;
};
frag_t() {
std::fill_n((std::uint8_t *)this, sizeof(*this), 0);
}
frag_t &operator=(frag_t &&o) {
std::copy((std::uint8_t *)&o, (std::uint8_t *)(&o + 1), (std::uint8_t *)this);
o.data = nullptr;
o.units = nullptr;
return *this;
};
~frag_t() {
if(data || units) {
ff_cbs_fragment_free(this);
}
}
};
util::buffer_t<std::uint8_t> write(const cbs::ctx_t &cbs_ctx, std::uint8_t nal, void *uh, AVCodecID codec_id) {
cbs::frag_t frag;
auto err = ff_cbs_insert_unit_content(&frag, -1, nal, uh, nullptr);
if(err < 0) {
char err_str[AV_ERROR_MAX_STRING_SIZE] { 0 };
BOOST_LOG(error) << "Could not insert NAL unit SPS: "sv << av_make_error_string(err_str, AV_ERROR_MAX_STRING_SIZE, err);
return {};
}
err = ff_cbs_write_fragment_data(cbs_ctx.get(), &frag);
if(err < 0) {
char err_str[AV_ERROR_MAX_STRING_SIZE] { 0 };
BOOST_LOG(error) << "Could not write fragment data: "sv << av_make_error_string(err_str, AV_ERROR_MAX_STRING_SIZE, err);
return {};
}
// frag.data_size * 8 - frag.data_bit_padding == bits in fragment
util::buffer_t<std::uint8_t> data { frag.data_size };
std::copy_n(frag.data, frag.data_size, std::begin(data));
return data;
}
util::buffer_t<std::uint8_t> write(std::uint8_t nal, void *uh, AVCodecID codec_id) {
cbs::ctx_t cbs_ctx;
ff_cbs_init(&cbs_ctx, codec_id, nullptr);
return write(cbs_ctx, nal, uh, codec_id);
}
util::buffer_t<std::uint8_t> make_sps_h264(const AVCodecContext *ctx) {
H264RawSPS sps {};
/* b_per_p == ctx->max_b_frames for h264 */
/* desired_b_depth == avoption("b_depth") == 1 */
/* max_b_depth == std::min(av_log2(ctx->b_per_p) + 1, desired_b_depth) ==> 1 */
auto max_b_depth = 1;
auto dpb_frame = ctx->gop_size == 1 ? 0 : 1 + max_b_depth;
auto mb_width = (FFALIGN(ctx->width, 16) / 16) * 16;
auto mb_height = (FFALIGN(ctx->height, 16) / 16) * 16;
sps.nal_unit_header.nal_ref_idc = 3;
sps.nal_unit_header.nal_unit_type = H264_NAL_SPS;
sps.profile_idc = FF_PROFILE_H264_HIGH & 0xFF;
sps.constraint_set1_flag = 1;
if(ctx->level != FF_LEVEL_UNKNOWN) {
sps.level_idc = ctx->level;
}
else {
auto framerate = ctx->framerate;
auto level = ff_h264_guess_level(
sps.profile_idc,
ctx->bit_rate,
framerate.num / framerate.den,
mb_width,
mb_height,
dpb_frame);
if(!level) {
BOOST_LOG(error) << "Could not guess h264 level"sv;
return {};
}
sps.level_idc = level->level_idc;
}
sps.seq_parameter_set_id = 0;
sps.chroma_format_idc = 1;
sps.log2_max_frame_num_minus4 = 3; //4;
sps.pic_order_cnt_type = 0;
sps.log2_max_pic_order_cnt_lsb_minus4 = 0; //4;
sps.max_num_ref_frames = dpb_frame;
sps.pic_width_in_mbs_minus1 = mb_width / 16 - 1;
sps.pic_height_in_map_units_minus1 = mb_height / 16 - 1;
sps.frame_mbs_only_flag = 1;
sps.direct_8x8_inference_flag = 1;
if(ctx->width != mb_width || ctx->height != mb_height) {
sps.frame_cropping_flag = 1;
sps.frame_crop_left_offset = 0;
sps.frame_crop_top_offset = 0;
sps.frame_crop_right_offset = (mb_width - ctx->width) / 2;
sps.frame_crop_bottom_offset = (mb_height - ctx->height) / 2;
}
sps.vui_parameters_present_flag = 1;
auto &vui = sps.vui;
vui.video_format = 5;
vui.colour_description_present_flag = 1;
vui.video_signal_type_present_flag = 1;
vui.video_full_range_flag = ctx->color_range == AVCOL_RANGE_JPEG;
vui.colour_primaries = ctx->color_primaries;
vui.transfer_characteristics = ctx->color_trc;
vui.matrix_coefficients = ctx->colorspace;
vui.low_delay_hrd_flag = 1 - vui.fixed_frame_rate_flag;
vui.bitstream_restriction_flag = 1;
vui.motion_vectors_over_pic_boundaries_flag = 1;
vui.log2_max_mv_length_horizontal = 15;
vui.log2_max_mv_length_vertical = 15;
vui.max_num_reorder_frames = max_b_depth;
vui.max_dec_frame_buffering = max_b_depth + 1;
return write(sps.nal_unit_header.nal_unit_type, (void *)&sps.nal_unit_header, AV_CODEC_ID_H264);
}
hevc_t make_sps_hevc(const AVCodecContext *avctx, const AVPacket *packet) {
cbs::ctx_t ctx;
if(ff_cbs_init(&ctx, AV_CODEC_ID_H265, nullptr)) {
return {};
}
cbs::frag_t frag;
int err = ff_cbs_read_packet(ctx.get(), &frag, packet);
if(err < 0) {
char err_str[AV_ERROR_MAX_STRING_SIZE] { 0 };
BOOST_LOG(error) << "Couldn't read packet: "sv << av_make_error_string(err_str, AV_ERROR_MAX_STRING_SIZE, err);
return {};
}
auto vps_p = ((CodedBitstreamH265Context *)ctx->priv_data)->active_vps;
auto sps_p = ((CodedBitstreamH265Context *)ctx->priv_data)->active_sps;
H265RawSPS sps { *sps_p };
H265RawVPS vps { *vps_p };
vps.profile_tier_level.general_profile_compatibility_flag[4] = 1;
sps.profile_tier_level.general_profile_compatibility_flag[4] = 1;
auto &vui = sps.vui;
std::memset(&vui, 0, sizeof(vui));
sps.vui_parameters_present_flag = 1;
// skip sample aspect ratio
vui.video_format = 5;
vui.colour_description_present_flag = 1;
vui.video_signal_type_present_flag = 1;
vui.video_full_range_flag = avctx->color_range == AVCOL_RANGE_JPEG;
vui.colour_primaries = avctx->color_primaries;
vui.transfer_characteristics = avctx->color_trc;
vui.matrix_coefficients = avctx->colorspace;
vui.vui_timing_info_present_flag = vps.vps_timing_info_present_flag;
vui.vui_num_units_in_tick = vps.vps_num_units_in_tick;
vui.vui_time_scale = vps.vps_time_scale;
vui.vui_poc_proportional_to_timing_flag = vps.vps_poc_proportional_to_timing_flag;
vui.vui_num_ticks_poc_diff_one_minus1 = vps.vps_num_ticks_poc_diff_one_minus1;
vui.vui_hrd_parameters_present_flag = 0;
vui.bitstream_restriction_flag = 1;
vui.motion_vectors_over_pic_boundaries_flag = 1;
vui.restricted_ref_pic_lists_flag = 1;
vui.max_bytes_per_pic_denom = 0;
vui.max_bits_per_min_cu_denom = 0;
vui.log2_max_mv_length_horizontal = 15;
vui.log2_max_mv_length_vertical = 15;
cbs::ctx_t write_ctx;
ff_cbs_init(&write_ctx, AV_CODEC_ID_H265, nullptr);
return hevc_t {
nal_t {
write(write_ctx, vps.nal_unit_header.nal_unit_type, (void *)&vps.nal_unit_header, AV_CODEC_ID_H265),
write(ctx, vps_p->nal_unit_header.nal_unit_type, (void *)&vps_p->nal_unit_header, AV_CODEC_ID_H265),
},
nal_t {
write(write_ctx, sps.nal_unit_header.nal_unit_type, (void *)&sps.nal_unit_header, AV_CODEC_ID_H265),
write(ctx, sps_p->nal_unit_header.nal_unit_type, (void *)&sps_p->nal_unit_header, AV_CODEC_ID_H265),
},
};
}
util::buffer_t<std::uint8_t> read_sps_h264(const AVPacket *packet) {
cbs::ctx_t ctx;
if(ff_cbs_init(&ctx, AV_CODEC_ID_H264, nullptr)) {
return {};
}
cbs::frag_t frag;
int err = ff_cbs_read_packet(ctx.get(), &frag, &*packet);
if(err < 0) {
char err_str[AV_ERROR_MAX_STRING_SIZE] { 0 };
BOOST_LOG(error) << "Couldn't read packet: "sv << av_make_error_string(err_str, AV_ERROR_MAX_STRING_SIZE, err);
return {};
}
auto h264 = (H264RawNALUnitHeader *)((CodedBitstreamH264Context *)ctx->priv_data)->active_sps;
return write(h264->nal_unit_type, (void *)h264, AV_CODEC_ID_H264);
}
h264_t make_sps_h264(const AVCodecContext *ctx, const AVPacket *packet) {
return h264_t {
make_sps_h264(ctx),
read_sps_h264(packet),
};
}
bool validate_sps(const AVPacket *packet, int codec_id) {
cbs::ctx_t ctx;
if(ff_cbs_init(&ctx, (AVCodecID)codec_id, nullptr)) {
return false;
}
cbs::frag_t frag;
int err = ff_cbs_read_packet(ctx.get(), &frag, packet);
if(err < 0) {
char err_str[AV_ERROR_MAX_STRING_SIZE] { 0 };
BOOST_LOG(error) << "Couldn't read packet: "sv << av_make_error_string(err_str, AV_ERROR_MAX_STRING_SIZE, err);
return false;
}
if(codec_id == AV_CODEC_ID_H264) {
auto h264 = (CodedBitstreamH264Context *)ctx->priv_data;
if(!h264->active_sps->vui_parameters_present_flag) {
return false;
}
return true;
}
return ((CodedBitstreamH265Context *)ctx->priv_data)->active_sps->vui_parameters_present_flag;
}
} // namespace cbs

34
sunshine/cbs.h Normal file
View File

@@ -0,0 +1,34 @@
#ifndef SUNSHINE_CBS_H
#define SUNSHINE_CBS_H
#include "utility.h"
struct AVPacket;
struct AVCodecContext;
namespace cbs {
struct nal_t {
util::buffer_t<std::uint8_t> _new;
util::buffer_t<std::uint8_t> old;
};
struct hevc_t {
nal_t vps;
nal_t sps;
};
struct h264_t {
nal_t sps;
};
hevc_t make_sps_hevc(const AVCodecContext *ctx, const AVPacket *packet);
h264_t make_sps_h264(const AVCodecContext *ctx, const AVPacket *packet);
/**
* Check if SPS->VUI is present
*/
bool validate_sps(const AVPacket *packet, int codec_id);
} // namespace cbs
#endif

View File

@@ -1,12 +1,20 @@
#include <algorithm>
#include <filesystem>
#include <fstream>
#include <iostream>
#include <functional>
#include <iostream>
#include <unordered_map>
#include <boost/asio.hpp>
#include "utility.h"
#include "config.h"
#include "main.h"
#include "utility.h"
#include "platform/common.h"
namespace fs = std::filesystem;
using namespace std::literals;
#define CA_DIR "credentials"
#define PRIVATE_KEY_FILE CA_DIR "/cakey.pem"
@@ -14,18 +22,155 @@
#define APPS_JSON_PATH SUNSHINE_ASSETS_DIR "/" APPS_JSON
namespace config {
using namespace std::literals;
namespace nv {
enum preset_e : int {
_default = 0,
slow,
medium,
fast,
hp,
hq,
bd,
ll_default,
llhq,
llhp,
lossless_default, // lossless presets must be the last ones
lossless_hp,
};
enum rc_e : int {
constqp = 0x0, /**< Constant QP mode */
vbr = 0x1, /**< Variable bitrate mode */
cbr = 0x2, /**< Constant bitrate mode */
cbr_ld_hq = 0x8, /**< low-delay CBR, high quality */
cbr_hq = 0x10, /**< CBR, high quality (slower) */
vbr_hq = 0x20 /**< VBR, high quality (slower) */
};
enum coder_e : int {
_auto = 0,
cabac,
cavlc
};
std::optional<preset_e> preset_from_view(const std::string_view &preset) {
#define _CONVERT_(x) \
if(preset == #x##sv) return x
_CONVERT_(slow);
_CONVERT_(medium);
_CONVERT_(fast);
_CONVERT_(hp);
_CONVERT_(bd);
_CONVERT_(ll_default);
_CONVERT_(llhq);
_CONVERT_(llhp);
_CONVERT_(lossless_default);
_CONVERT_(lossless_hp);
if(preset == "default"sv) return _default;
#undef _CONVERT_
return std::nullopt;
}
std::optional<rc_e> rc_from_view(const std::string_view &rc) {
#define _CONVERT_(x) \
if(rc == #x##sv) return x
_CONVERT_(constqp);
_CONVERT_(vbr);
_CONVERT_(cbr);
_CONVERT_(cbr_hq);
_CONVERT_(vbr_hq);
_CONVERT_(cbr_ld_hq);
#undef _CONVERT_
return std::nullopt;
}
int coder_from_view(const std::string_view &coder) {
if(coder == "auto"sv) return _auto;
if(coder == "cabac"sv || coder == "ac"sv) return cabac;
if(coder == "cavlc"sv || coder == "vlc"sv) return cavlc;
return -1;
}
} // namespace nv
namespace amd {
enum quality_e : int {
_default = 0,
speed,
balanced,
//quality2,
};
enum rc_e : int {
constqp, /**< Constant QP mode */
vbr_latency, /**< Latency Constrained Variable Bitrate */
vbr_peak, /**< Peak Contrained Variable Bitrate */
cbr, /**< Constant bitrate mode */
};
enum coder_e : int {
_auto = 0,
cabac,
cavlc
};
std::optional<quality_e> quality_from_view(const std::string_view &quality) {
#define _CONVERT_(x) \
if(quality == #x##sv) return x
_CONVERT_(speed);
_CONVERT_(balanced);
//_CONVERT_(quality2);
if(quality == "default"sv) return _default;
#undef _CONVERT_
return std::nullopt;
}
std::optional<rc_e> rc_from_view(const std::string_view &rc) {
#define _CONVERT_(x) \
if(rc == #x##sv) return x
_CONVERT_(constqp);
_CONVERT_(vbr_latency);
_CONVERT_(vbr_peak);
_CONVERT_(cbr);
#undef _CONVERT_
return std::nullopt;
}
int coder_from_view(const std::string_view &coder) {
if(coder == "auto"sv) return _auto;
if(coder == "cabac"sv || coder == "ac"sv) return cabac;
if(coder == "cavlc"sv || coder == "vlc"sv) return cavlc;
return -1;
}
} // namespace amd
video_t video {
0, // crf
0, // crf
28, // qp
2, // min_threads
0, // hevc_mode
"superfast"s, // preset
"zerolatency"s, // tune
1, // min_threads
{
"superfast"s, // preset
"zerolatency"s, // tune
}, // software
{
nv::llhq,
std::nullopt,
-1 }, // nv
{
amd::balanced,
std::nullopt,
-1 }, // amd
{}, // encoder
{}, // adapter_name
{} // output_name
{}, // output_name
};
audio_t audio {};
@@ -35,61 +180,149 @@ stream_t stream {
APPS_JSON_PATH,
10 // fecPercentage
10, // fecPercentage
1 // channels
};
nvhttp_t nvhttp {
"lan", // origin_pin
"pc", // origin_pin
"lan", // origin web manager
PRIVATE_KEY_FILE,
CERTIFICATE_FILE,
boost::asio::ip::host_name(), // sunshine_name,
"sunshine_state.json"s // file_state
"sunshine_state.json"s, // file_state
{}, // external_ip
{
"352x240"s,
"480x360"s,
"858x480"s,
"1280x720"s,
"1920x1080"s,
"2560x1080"s,
"3440x1440"s
"1920x1200"s,
"3860x2160"s,
"3840x1600"s,
}, // supported resolutions
{ 10, 30, 60, 90, 120 }, // supported fps
};
input_t input {
2s
2s, // back_button_timeout
500ms, // key_repeat_delay
std::chrono::duration<double> { 1 / 24.9 } // key_repeat_period
};
sunshine_t sunshine {
2 // min_log_level
2, // min_log_level
0, // flags
{}, // User file
{}, // Username
{}, // Password
{}, // Password Salt
SUNSHINE_ASSETS_DIR "/sunshine.conf", // config file
{}, // cmd args
47989,
};
bool whitespace(char ch) {
bool endline(char ch) {
return ch == '\r' || ch == '\n';
}
bool space_tab(char ch) {
return ch == ' ' || ch == '\t';
}
std::string to_string(const char *begin, const char *end) {
return { begin, (std::size_t)(end - begin) };
bool whitespace(char ch) {
return space_tab(ch) || endline(ch);
}
std::optional<std::pair<std::string, std::string>> parse_line(std::string_view::const_iterator begin, std::string_view::const_iterator end) {
begin = std::find_if(begin, end, std::not_fn(whitespace));
end = std::find(begin, end, '#');
end = std::find_if(std::make_reverse_iterator(end), std::make_reverse_iterator(begin), std::not_fn(whitespace)).base();
std::string to_string(const char *begin, const char *end) {
std::string result;
auto eq = std::find(begin, end, '=');
if(eq == end || eq == begin) {
return std::nullopt;
KITTY_WHILE_LOOP(auto pos = begin, pos != end, {
auto comment = std::find(pos, end, '#');
auto endl = std::find_if(comment, end, endline);
result.append(pos, comment);
pos = endl;
})
return result;
}
template<class It>
It skip_list(It skipper, It end) {
int stack = 1;
while(skipper != end && stack) {
if(*skipper == '[') {
++stack;
}
if(*skipper == ']') {
--stack;
}
++skipper;
}
auto end_name = std::find_if(std::make_reverse_iterator(eq - 1), std::make_reverse_iterator(begin), std::not_fn(whitespace)).base();
auto begin_val = std::find_if(eq + 1, end, std::not_fn(whitespace));
return std::pair { to_string(begin, end_name), to_string(begin_val, end) };
return skipper;
}
std::unordered_map<std::string, std::string> parse_config(std::string_view file_content) {
std::pair<
std::string_view::const_iterator,
std::optional<std::pair<std::string, std::string>>>
parse_option(std::string_view::const_iterator begin, std::string_view::const_iterator end) {
begin = std::find_if_not(begin, end, whitespace);
auto endl = std::find_if(begin, end, endline);
auto endc = std::find(begin, endl, '#');
endc = std::find_if(std::make_reverse_iterator(endc), std::make_reverse_iterator(begin), std::not_fn(whitespace)).base();
auto eq = std::find(begin, endc, '=');
if(eq == endc || eq == begin) {
return std::make_pair(endl, std::nullopt);
}
auto end_name = std::find_if_not(std::make_reverse_iterator(eq), std::make_reverse_iterator(begin), space_tab).base();
auto begin_val = std::find_if_not(eq + 1, endc, space_tab);
if(begin_val == endl) {
return std::make_pair(endl, std::nullopt);
}
// Lists might contain newlines
if(*begin_val == '[') {
endl = skip_list(begin_val + 1, end);
if(endl == end) {
std::cout << "Warning: Config option ["sv << to_string(begin, end_name) << "] Missing ']'"sv;
return std::make_pair(endl, std::nullopt);
}
}
return std::make_pair(
endl,
std::make_pair(to_string(begin, end_name), to_string(begin_val, endl)));
}
std::unordered_map<std::string, std::string> parse_config(const std::string_view &file_content) {
std::unordered_map<std::string, std::string> vars;
auto pos = std::begin(file_content);
auto end = std::end(file_content);
while(pos < end) {
auto newline = std::find_if(pos, end, [](auto ch) { return ch == '\n' || ch == '\r'; });
auto var = parse_line(pos, newline);
// auto newline = std::find_if(pos, end, [](auto ch) { return ch == '\n' || ch == '\r'; });
TUPLE_2D(endl, var, parse_option(pos, end));
pos = endl;
if(pos != end) {
pos += (*pos == '\r') ? 2 : 1;
}
pos = (*newline == '\r') ? newline + 2 : newline + 1;
if(!var) {
continue;
}
@@ -123,6 +356,38 @@ void string_restricted_f(std::unordered_map<std::string, std::string> &vars, con
}
}
void path_f(std::unordered_map<std::string, std::string> &vars, const std::string &name, fs::path &input) {
// appdata needs to be retrieved once only
static auto appdata = platf::appdata();
std::string temp;
string_f(vars, name, temp);
if(!temp.empty()) {
input = temp;
}
if(input.is_relative()) {
input = appdata / input;
}
auto dir = input;
dir.remove_filename();
// Ensure the directories exists
if(!fs::exists(dir)) {
fs::create_directories(dir);
}
}
void path_f(std::unordered_map<std::string, std::string> &vars, const std::string &name, std::string &input) {
fs::path temp = input;
path_f(vars, name, temp);
input = temp.string();
}
void int_f(std::unordered_map<std::string, std::string> &vars, const std::string &name, int &input) {
auto it = vars.find(name);
@@ -131,11 +396,42 @@ void int_f(std::unordered_map<std::string, std::string> &vars, const std::string
}
auto &val = it->second;
input = util::from_chars(&val[0], &val[0] + val.size());
input = util::from_chars(&val[0], &val[0] + val.size());
vars.erase(it);
}
void int_f(std::unordered_map<std::string, std::string> &vars, const std::string &name, std::optional<int> &input) {
auto it = vars.find(name);
if(it == std::end(vars)) {
return;
}
auto &val = it->second;
input = util::from_chars(&val[0], &val[0] + val.size());
vars.erase(it);
}
template<class F>
void int_f(std::unordered_map<std::string, std::string> &vars, const std::string &name, int &input, F &&f) {
std::string tmp;
string_f(vars, name, tmp);
if(!tmp.empty()) {
input = f(tmp);
}
}
template<class F>
void int_f(std::unordered_map<std::string, std::string> &vars, const std::string &name, std::optional<int> &input, F &&f) {
std::string tmp;
string_f(vars, name, tmp);
if(!tmp.empty()) {
input = f(tmp);
}
}
void int_between_f(std::unordered_map<std::string, std::string> &vars, const std::string &name, int &input, const std::pair<int, int> &range) {
int temp = input;
@@ -147,15 +443,136 @@ void int_between_f(std::unordered_map<std::string, std::string> &vars, const std
}
}
void parse_file(const char *file) {
std::ifstream in(file);
bool to_bool(std::string &boolean) {
std::for_each(std::begin(boolean), std::end(boolean), [](char ch) { return (char)std::tolower(ch); });
auto vars = parse_config(std::string {
// Quick and dirty
std::istreambuf_iterator<char>(in),
std::istreambuf_iterator<char>()
});
return boolean == "true"sv ||
boolean == "yes"sv ||
boolean == "enable"sv ||
boolean == "enabled"sv ||
boolean == "on"sv ||
(std::find(std::begin(boolean), std::end(boolean), '1') != std::end(boolean));
}
void bool_f(std::unordered_map<std::string, std::string> &vars, const std::string &name, bool &input) {
std::string tmp;
string_f(vars, name, tmp);
if(tmp.empty()) {
return;
}
input = to_bool(tmp);
}
void double_f(std::unordered_map<std::string, std::string> &vars, const std::string &name, double &input) {
std::string tmp;
string_f(vars, name, tmp);
if(tmp.empty()) {
return;
}
char *c_str_p;
auto val = std::strtod(tmp.c_str(), &c_str_p);
if(c_str_p == tmp.c_str()) {
return;
}
input = val;
}
void double_between_f(std::unordered_map<std::string, std::string> &vars, const std::string &name, double &input, const std::pair<double, double> &range) {
double temp = input;
double_f(vars, name, temp);
TUPLE_2D_REF(lower, upper, range);
if(temp >= lower && temp <= upper) {
input = temp;
}
}
void list_string_f(std::unordered_map<std::string, std::string> &vars, const std::string &name, std::vector<std::string> &input) {
std::string string;
string_f(vars, name, string);
if(string.empty()) {
return;
}
input.clear();
auto begin = std::cbegin(string);
if(*begin == '[') {
++begin;
}
begin = std::find_if_not(begin, std::cend(string), whitespace);
if(begin == std::cend(string)) {
return;
}
auto pos = begin;
while(pos < std::cend(string)) {
if(*pos == '[') {
pos = skip_list(pos + 1, std::cend(string)) + 1;
}
else if(*pos == ']') {
break;
}
else if(*pos == ',') {
input.emplace_back(begin, pos);
pos = begin = std::find_if_not(pos + 1, std::cend(string), whitespace);
}
else {
++pos;
}
}
if(pos != begin) {
input.emplace_back(begin, pos);
}
}
void list_int_f(std::unordered_map<std::string, std::string> &vars, const std::string &name, std::vector<int> &input) {
std::vector<std::string> list;
list_string_f(vars, name, list);
for(auto &el : list) {
input.emplace_back(util::from_view(el));
}
}
int apply_flags(const char *line) {
int ret = 0;
while(*line != '\0') {
switch(*line) {
case '0':
config::sunshine.flags[config::flag::PIN_STDIN].flip();
break;
case '1':
config::sunshine.flags[config::flag::FRESH_STATE].flip();
break;
case '2':
config::sunshine.flags[config::flag::FORCE_VIDEO_HEADER_REPLACE].flip();
break;
case 'p':
config::sunshine.flags[config::flag::UPNP].flip();
break;
default:
std::cout << "Warning: Unrecognized flag: ["sv << *line << ']' << std::endl;
ret = -1;
}
++line;
}
return ret;
}
void apply_config(std::unordered_map<std::string, std::string> &&vars) {
for(auto &[name, val] : vars) {
std::cout << "["sv << name << "] -- ["sv << val << ']' << std::endl;
}
@@ -163,47 +580,85 @@ void parse_file(const char *file) {
int_f(vars, "crf", video.crf);
int_f(vars, "qp", video.qp);
int_f(vars, "min_threads", video.min_threads);
int_between_f(vars, "hevc_mode", video.hevc_mode, {
0, 2
});
string_f(vars, "preset", video.preset);
string_f(vars, "tune", video.tune);
int_between_f(vars, "hevc_mode", video.hevc_mode, { 0, 3 });
string_f(vars, "sw_preset", video.sw.preset);
string_f(vars, "sw_tune", video.sw.tune);
int_f(vars, "nv_preset", video.nv.preset, nv::preset_from_view);
int_f(vars, "nv_rc", video.nv.rc, nv::rc_from_view);
int_f(vars, "nv_coder", video.nv.coder, nv::coder_from_view);
int_f(vars, "amd_quality", video.amd.quality, amd::quality_from_view);
int_f(vars, "amd_rc", video.amd.rc, amd::rc_from_view);
int_f(vars, "amd_coder", video.amd.coder, amd::coder_from_view);
string_f(vars, "encoder", video.encoder);
string_f(vars, "adapter_name", video.adapter_name);
string_f(vars, "output_name", video.output_name);
string_f(vars, "pkey", nvhttp.pkey);
string_f(vars, "cert", nvhttp.cert);
path_f(vars, "pkey", nvhttp.pkey);
path_f(vars, "cert", nvhttp.cert);
string_f(vars, "sunshine_name", nvhttp.sunshine_name);
string_f(vars, "file_state", nvhttp.file_state);
path_f(vars, "file_state", nvhttp.file_state);
// Must be run after "file_state"
config::sunshine.credentials_file = config::nvhttp.file_state;
path_f(vars, "credentials_file", config::sunshine.credentials_file);
string_f(vars, "external_ip", nvhttp.external_ip);
list_string_f(vars, "resolutions"s, nvhttp.resolutions);
list_int_f(vars, "fps"s, nvhttp.fps);
string_f(vars, "audio_sink", audio.sink);
string_f(vars, "virtual_sink", audio.virtual_sink);
string_restricted_f(vars, "origin_pin_allowed", nvhttp.origin_pin_allowed, {
"pc"sv, "lan"sv, "wan"sv
});
string_restricted_f(vars, "origin_pin_allowed", nvhttp.origin_pin_allowed, { "pc"sv, "lan"sv, "wan"sv });
string_restricted_f(vars, "origin_web_ui_allowed", nvhttp.origin_web_ui_allowed, { "pc"sv, "lan"sv, "wan"sv });
int to = -1;
int_f(vars, "ping_timeout", to);
if(to > 0) {
int_between_f(vars, "ping_timeout", to, { -1, std::numeric_limits<int>::max() });
if(to != -1) {
stream.ping_timeout = std::chrono::milliseconds(to);
}
string_f(vars, "file_apps", stream.file_apps);
int_between_f(vars, "fec_percentage", stream.fec_percentage, {
1, 100
});
int_between_f(vars, "channels", stream.channels, { 1, std::numeric_limits<int>::max() });
path_f(vars, "file_apps", stream.file_apps);
int_between_f(vars, "fec_percentage", stream.fec_percentage, { 1, 100 });
to = std::numeric_limits<int>::min();
int_f(vars, "back_button_timeout", to);
if(to > std::numeric_limits<int>::min()) {
input.back_button_timeout = std::chrono::milliseconds {to };
input.back_button_timeout = std::chrono::milliseconds { to };
}
double repeat_frequency { 0 };
double_between_f(vars, "key_repeat_frequency", repeat_frequency, { 0, std::numeric_limits<double>::max() });
if(repeat_frequency > 0) {
config::input.key_repeat_period = std::chrono::duration<double> { 1 / repeat_frequency };
}
to = -1;
int_f(vars, "key_repeat_delay", to);
if(to >= 0) {
input.key_repeat_delay = std::chrono::milliseconds { to };
}
int port = sunshine.port;
int_f(vars, "port"s, port);
sunshine.port = (std::uint16_t)port;
bool upnp = false;
bool_f(vars, "upnp"s, upnp);
if(upnp) {
config::sunshine.flags[config::flag::UPNP].flip();
}
std::string log_level_string;
string_restricted_f(vars, "min_log_level", log_level_string, {
"verbose"sv, "debug"sv, "info"sv, "warning"sv, "error"sv, "fatal"sv, "none"sv
});
string_restricted_f(vars, "min_log_level", log_level_string, { "verbose"sv, "debug"sv, "info"sv, "warning"sv, "error"sv, "fatal"sv, "none"sv });
if(!log_level_string.empty()) {
if(log_level_string == "verbose"sv) {
@@ -227,14 +682,86 @@ void parse_file(const char *file) {
else if(log_level_string == "none"sv) {
sunshine.min_log_level = 6;
}
else {
// accept digit directly
auto val = log_level_string[0];
if(val >= '0' && val < '7') {
sunshine.min_log_level = val - '0';
}
}
}
auto it = vars.find("flags"s);
if(it != std::end(vars)) {
apply_flags(it->second.c_str());
vars.erase(it);
}
if(sunshine.min_log_level <= 3) {
for(auto &[var,_] : vars) {
for(auto &[var, _] : vars) {
std::cout << "Warning: Unrecognized configurable option ["sv << var << ']' << std::endl;
}
}
}
int parse(int argc, char *argv[]) {
std::unordered_map<std::string, std::string> cmd_vars;
for(auto x = 1; x < argc; ++x) {
auto line = argv[x];
if(line == "--help"sv) {
print_help(*argv);
return 1;
}
else if(*line == '-') {
if(*(line + 1) == '-') {
sunshine.cmd.name = line + 2;
sunshine.cmd.argc = argc - x - 1;
sunshine.cmd.argv = argv + x + 1;
break;
}
if(apply_flags(line + 1)) {
print_help(*argv);
return -1;
}
}
else {
auto line_end = line + strlen(line);
auto pos = std::find(line, line_end, '=');
if(pos == line_end) {
sunshine.config_file = line;
}
else {
TUPLE_EL(var, 1, parse_option(line, line_end));
if(!var) {
print_help(*argv);
return -1;
}
TUPLE_EL_REF(name, 0, *var);
auto it = cmd_vars.find(name);
if(it != std::end(cmd_vars)) {
cmd_vars.erase(it);
}
cmd_vars.emplace(std::move(*var));
}
}
}
auto vars = parse_config(read_file(sunshine.config_file.c_str()));
for(auto &[name, value] : cmd_vars) {
vars.insert_or_assign(std::move(name), std::move(value));
}
apply_config(std::move(vars));
return 0;
}
} // namespace config

View File

@@ -1,27 +1,47 @@
#ifndef SUNSHINE_CONFIG_H
#define SUNSHINE_CONFIG_H
#include <bitset>
#include <chrono>
#include <optional>
#include <string>
#include <unordered_map>
#include <vector>
namespace config {
struct video_t {
// ffmpeg params
int crf; // higher == more compression and less quality
int qp; // higher == more compression and less quality, ignored if crf != 0
int min_threads; // Minimum number of threads/slices for CPU encoding
int qp; // higher == more compression and less quality, ignored if crf != 0
int hevc_mode;
std::string preset;
std::string tune;
int min_threads; // Minimum number of threads/slices for CPU encoding
struct {
std::string preset;
std::string tune;
} sw;
struct {
std::optional<int> preset;
std::optional<int> rc;
int coder;
} nv;
struct {
std::optional<int> quality;
std::optional<int> rc;
int coder;
} amd;
std::string encoder;
std::string adapter_name;
std::string output_name;
};
struct audio_t {
std::string sink;
std::string virtual_sink;
};
struct stream_t {
@@ -30,12 +50,16 @@ struct stream_t {
std::string file_apps;
int fec_percentage;
// max unique instances of video and audio streams
int channels;
};
struct nvhttp_t {
// Could be any of the following values:
// pc|lan|wan
std::string origin_pin_allowed;
std::string origin_web_ui_allowed;
std::string pkey; // must be 2048 bits
std::string cert; // must be signed with a key of 2048 bits
@@ -45,14 +69,45 @@ struct nvhttp_t {
std::string file_state;
std::string external_ip;
std::vector<std::string> resolutions;
std::vector<int> fps;
};
struct input_t {
std::chrono::milliseconds back_button_timeout;
std::chrono::milliseconds key_repeat_delay;
std::chrono::duration<double> key_repeat_period;
};
namespace flag {
enum flag_e : std::size_t {
PIN_STDIN = 0, // Read PIN from stdin instead of http
FRESH_STATE, // Do not load or save state
FORCE_VIDEO_HEADER_REPLACE, // force replacing headers inside video data
UPNP, // Try Universal Plug 'n Play
CONST_PIN, // Use "universal" pin
FLAG_SIZE
};
}
struct sunshine_t {
int min_log_level;
std::bitset<flag::FLAG_SIZE> flags;
std::string credentials_file;
std::string username;
std::string password;
std::string salt;
std::string config_file;
struct cmd_t {
std::string name;
int argc;
char **argv;
} cmd;
std::uint16_t port;
};
extern video_t video;
@@ -62,7 +117,7 @@ extern nvhttp_t nvhttp;
extern input_t input;
extern sunshine_t sunshine;
void parse_file(const char *file);
}
int parse(int argc, char *argv[]);
std::unordered_map<std::string, std::string> parse_config(const std::string_view &file_content);
} // namespace config
#endif

515
sunshine/confighttp.cpp Normal file
View File

@@ -0,0 +1,515 @@
//
// Created by TheElixZammuto on 2021-05-09.
// TODO: Authentication, better handling of routes common to nvhttp, cleanup
#define BOOST_BIND_GLOBAL_PLACEHOLDERS
#include "process.h"
#include <filesystem>
#include <boost/property_tree/json_parser.hpp>
#include <boost/property_tree/ptree.hpp>
#include <boost/property_tree/xml_parser.hpp>
#include <boost/asio/ssl/context.hpp>
#include <Simple-Web-Server/crypto.hpp>
#include <Simple-Web-Server/server_https.hpp>
#include <boost/asio/ssl/context_base.hpp>
#include "config.h"
#include "confighttp.h"
#include "crypto.h"
#include "httpcommon.h"
#include "main.h"
#include "network.h"
#include "nvhttp.h"
#include "platform/common.h"
#include "rtsp.h"
#include "utility.h"
#include "uuid.h"
using namespace std::literals;
namespace confighttp {
namespace fs = std::filesystem;
namespace pt = boost::property_tree;
using https_server_t = SimpleWeb::Server<SimpleWeb::HTTPS>;
using args_t = SimpleWeb::CaseInsensitiveMultimap;
using resp_https_t = std::shared_ptr<typename SimpleWeb::ServerBase<SimpleWeb::HTTPS>::Response>;
using req_https_t = std::shared_ptr<typename SimpleWeb::ServerBase<SimpleWeb::HTTPS>::Request>;
enum class op_e {
ADD,
REMOVE
};
void print_req(const req_https_t &request) {
BOOST_LOG(debug) << "METHOD :: "sv << request->method;
BOOST_LOG(debug) << "DESTINATION :: "sv << request->path;
for(auto &[name, val] : request->header) {
BOOST_LOG(debug) << name << " -- " << val;
}
BOOST_LOG(debug) << " [--] "sv;
for(auto &[name, val] : request->parse_query_string()) {
BOOST_LOG(debug) << name << " -- " << val;
}
BOOST_LOG(debug) << " [--] "sv;
}
void send_unauthorized(resp_https_t response, req_https_t request) {
auto address = request->remote_endpoint_address();
BOOST_LOG(info) << "Web UI: ["sv << address << "] -- not authorized"sv;
const SimpleWeb::CaseInsensitiveMultimap headers {
{ "WWW-Authenticate", R"(Basic realm="Sunshine Gamestream Host", charset="UTF-8")" }
};
response->write(SimpleWeb::StatusCode::client_error_unauthorized, headers);
}
bool authenticate(resp_https_t response, req_https_t request) {
auto address = request->remote_endpoint_address();
auto ip_type = net::from_address(address);
if(ip_type > http::origin_web_ui_allowed) {
BOOST_LOG(info) << "Web UI: ["sv << address << "] -- denied"sv;
response->write(SimpleWeb::StatusCode::client_error_forbidden);
return false;
}
auto fg = util::fail_guard([&]() {
send_unauthorized(response, request);
});
auto auth = request->header.find("authorization");
if(auth == request->header.end()) {
return false;
}
auto &rawAuth = auth->second;
auto authData = SimpleWeb::Crypto::Base64::decode(rawAuth.substr("Basic "sv.length()));
int index = authData.find(':');
if(index >= authData.size() - 1) {
return false;
}
auto username = authData.substr(0, index);
auto password = authData.substr(index + 1);
auto hash = util::hex(crypto::hash(password + config::sunshine.salt)).to_string();
if(username != config::sunshine.username || hash != config::sunshine.password) {
return false;
}
fg.disable();
return true;
}
void not_found(resp_https_t response, req_https_t request) {
pt::ptree tree;
tree.put("root.<xmlattr>.status_code", 404);
std::ostringstream data;
pt::write_xml(data, tree);
response->write(data.str());
*response << "HTTP/1.1 404 NOT FOUND\r\n"
<< data.str();
}
void getIndexPage(resp_https_t response, req_https_t request) {
if(!authenticate(response, request)) return;
print_req(request);
std::string header = read_file(WEB_DIR "header.html");
std::string content = read_file(WEB_DIR "index.html");
response->write(header + content);
}
void getPinPage(resp_https_t response, req_https_t request) {
if(!authenticate(response, request)) return;
print_req(request);
std::string header = read_file(WEB_DIR "header.html");
std::string content = read_file(WEB_DIR "pin.html");
response->write(header + content);
}
void getAppsPage(resp_https_t response, req_https_t request) {
if(!authenticate(response, request)) return;
print_req(request);
std::string header = read_file(WEB_DIR "header.html");
std::string content = read_file(WEB_DIR "apps.html");
response->write(header + content);
}
void getClientsPage(resp_https_t response, req_https_t request) {
if(!authenticate(response, request)) return;
print_req(request);
std::string header = read_file(WEB_DIR "header.html");
std::string content = read_file(WEB_DIR "clients.html");
response->write(header + content);
}
void getConfigPage(resp_https_t response, req_https_t request) {
if(!authenticate(response, request)) return;
print_req(request);
std::string header = read_file(WEB_DIR "header.html");
std::string content = read_file(WEB_DIR "config.html");
response->write(header + content);
}
void getPasswordPage(resp_https_t response, req_https_t request) {
if(!authenticate(response, request)) return;
print_req(request);
std::string header = read_file(WEB_DIR "header.html");
std::string content = read_file(WEB_DIR "password.html");
response->write(header + content);
}
void getApps(resp_https_t response, req_https_t request) {
if(!authenticate(response, request)) return;
print_req(request);
std::string content = read_file(config::stream.file_apps.c_str());
response->write(content);
}
void saveApp(resp_https_t response, req_https_t request) {
if(!authenticate(response, request)) return;
print_req(request);
std::stringstream ss;
ss << request->content.rdbuf();
pt::ptree outputTree;
auto g = util::fail_guard([&]() {
std::ostringstream data;
pt::write_json(data, outputTree);
response->write(data.str());
});
pt::ptree inputTree, fileTree;
BOOST_LOG(fatal) << config::stream.file_apps;
try {
//TODO: Input Validation
pt::read_json(ss, inputTree);
pt::read_json(config::stream.file_apps, fileTree);
if(inputTree.get_child("prep-cmd").empty()) {
inputTree.erase("prep-cmd");
}
if(inputTree.get_child("detached").empty()) {
inputTree.erase("detached");
}
auto &apps_node = fileTree.get_child("apps"s);
int index = inputTree.get<int>("index");
inputTree.erase("index");
if(index == -1) {
apps_node.push_back(std::make_pair("", inputTree));
}
else {
//Unfortuantely Boost PT does not allow to directly edit the array, copy should do the trick
pt::ptree newApps;
int i = 0;
for(const auto &kv : apps_node) {
if(i == index) {
newApps.push_back(std::make_pair("", inputTree));
}
else {
newApps.push_back(std::make_pair("", kv.second));
}
i++;
}
fileTree.erase("apps");
fileTree.push_back(std::make_pair("apps", newApps));
}
pt::write_json(config::stream.file_apps, fileTree);
}
catch(std::exception &e) {
BOOST_LOG(warning) << "SaveApp: "sv << e.what();
outputTree.put("status", "false");
outputTree.put("error", "Invalid Input JSON");
return;
}
outputTree.put("status", "true");
proc::refresh(config::stream.file_apps);
}
void deleteApp(resp_https_t response, req_https_t request) {
if(!authenticate(response, request)) return;
print_req(request);
pt::ptree outputTree;
auto g = util::fail_guard([&]() {
std::ostringstream data;
pt::write_json(data, outputTree);
response->write(data.str());
});
pt::ptree fileTree;
try {
pt::read_json(config::stream.file_apps, fileTree);
auto &apps_node = fileTree.get_child("apps"s);
int index = stoi(request->path_match[1]);
if(index < 0) {
outputTree.put("status", "false");
outputTree.put("error", "Invalid Index");
return;
}
else {
//Unfortuantely Boost PT does not allow to directly edit the array, copy should do the trick
pt::ptree newApps;
int i = 0;
for(const auto &kv : apps_node) {
if(i++ != index) {
newApps.push_back(std::make_pair("", kv.second));
}
}
fileTree.erase("apps");
fileTree.push_back(std::make_pair("apps", newApps));
}
pt::write_json(config::stream.file_apps, fileTree);
}
catch(std::exception &e) {
BOOST_LOG(warning) << "DeleteApp: "sv << e.what();
outputTree.put("status", "false");
outputTree.put("error", "Invalid File JSON");
return;
}
outputTree.put("status", "true");
proc::refresh(config::stream.file_apps);
}
void getConfig(resp_https_t response, req_https_t request) {
if(!authenticate(response, request)) return;
print_req(request);
pt::ptree outputTree;
auto g = util::fail_guard([&]() {
std::ostringstream data;
pt::write_json(data, outputTree);
response->write(data.str());
});
outputTree.put("status", "true");
outputTree.put("platform", SUNSHINE_PLATFORM);
auto vars = config::parse_config(read_file(config::sunshine.config_file.c_str()));
for(auto &[name, value] : vars) {
outputTree.put(std::move(name), std::move(value));
}
}
void saveConfig(resp_https_t response, req_https_t request) {
if(!authenticate(response, request)) return;
print_req(request);
std::stringstream ss;
std::stringstream configStream;
ss << request->content.rdbuf();
pt::ptree outputTree;
auto g = util::fail_guard([&]() {
std::ostringstream data;
pt::write_json(data, outputTree);
response->write(data.str());
});
pt::ptree inputTree;
try {
//TODO: Input Validation
pt::read_json(ss, inputTree);
for(const auto &kv : inputTree) {
std::string value = inputTree.get<std::string>(kv.first);
if(value.length() == 0 || value.compare("null") == 0) continue;
configStream << kv.first << " = " << value << std::endl;
}
write_file(config::sunshine.config_file.c_str(), configStream.str());
}
catch(std::exception &e) {
BOOST_LOG(warning) << "SaveConfig: "sv << e.what();
outputTree.put("status", "false");
outputTree.put("error", e.what());
return;
}
}
void savePassword(resp_https_t response, req_https_t request) {
if(!authenticate(response, request)) return;
print_req(request);
std::stringstream ss;
std::stringstream configStream;
ss << request->content.rdbuf();
pt::ptree inputTree, outputTree;
auto g = util::fail_guard([&]() {
std::ostringstream data;
pt::write_json(data, outputTree);
response->write(data.str());
});
try {
//TODO: Input Validation
pt::read_json(ss, inputTree);
auto username = inputTree.get<std::string>("currentUsername");
auto newUsername = inputTree.get<std::string>("newUsername");
auto password = inputTree.get<std::string>("currentPassword");
auto newPassword = inputTree.get<std::string>("newPassword");
auto confirmPassword = inputTree.get<std::string>("confirmNewPassword");
if(newUsername.length() == 0) newUsername = username;
auto hash = util::hex(crypto::hash(password + config::sunshine.salt)).to_string();
if(username == config::sunshine.username && hash == config::sunshine.password) {
if(newPassword != confirmPassword) {
outputTree.put("status", false);
outputTree.put("error", "Password Mismatch");
}
http::save_user_creds(config::sunshine.credentials_file, newUsername, newPassword);
http::reload_user_creds(config::sunshine.credentials_file);
outputTree.put("status", true);
}
else {
outputTree.put("status", false);
outputTree.put("error", "Invalid Current Credentials");
}
}
catch(std::exception &e) {
BOOST_LOG(warning) << "SavePassword: "sv << e.what();
outputTree.put("status", false);
outputTree.put("error", e.what());
return;
}
}
void savePin(resp_https_t response, req_https_t request) {
if(!authenticate(response, request)) return;
print_req(request);
std::stringstream ss;
ss << request->content.rdbuf();
pt::ptree inputTree, outputTree;
auto g = util::fail_guard([&]() {
std::ostringstream data;
pt::write_json(data, outputTree);
response->write(data.str());
});
try {
//TODO: Input Validation
pt::read_json(ss, inputTree);
std::string pin = inputTree.get<std::string>("pin");
outputTree.put("status", nvhttp::pin(pin));
}
catch(std::exception &e) {
BOOST_LOG(warning) << "SavePin: "sv << e.what();
outputTree.put("status", false);
outputTree.put("error", e.what());
return;
}
}
void start() {
auto shutdown_event = mail::man->event<bool>(mail::shutdown);
auto port_https = map_port(PORT_HTTPS);
auto ctx = std::make_shared<boost::asio::ssl::context>(boost::asio::ssl::context::tls);
ctx->use_certificate_chain_file(config::nvhttp.cert);
ctx->use_private_key_file(config::nvhttp.pkey, boost::asio::ssl::context::pem);
https_server_t server { ctx, 0 };
server.default_resource = not_found;
server.resource["^/$"]["GET"] = getIndexPage;
server.resource["^/pin$"]["GET"] = getPinPage;
server.resource["^/apps$"]["GET"] = getAppsPage;
server.resource["^/clients$"]["GET"] = getClientsPage;
server.resource["^/config$"]["GET"] = getConfigPage;
server.resource["^/password$"]["GET"] = getPasswordPage;
server.resource["^/api/pin"]["POST"] = savePin;
server.resource["^/api/apps$"]["GET"] = getApps;
server.resource["^/api/apps$"]["POST"] = saveApp;
server.resource["^/api/config$"]["GET"] = getConfig;
server.resource["^/api/config$"]["POST"] = saveConfig;
server.resource["^/api/password$"]["POST"] = savePassword;
server.resource["^/api/apps/([0-9]+)$"]["DELETE"] = deleteApp;
server.config.reuse_address = true;
server.config.address = "0.0.0.0"s;
server.config.port = port_https;
try {
server.bind();
BOOST_LOG(info) << "Configuration UI available at [https://localhost:"sv << port_https << "]";
}
catch(boost::system::system_error &err) {
BOOST_LOG(fatal) << "Couldn't bind http server to ports ["sv << port_https << "]: "sv << err.what();
shutdown_event->raise(true);
return;
}
auto accept_and_run = [&](auto *server) {
try {
server->accept_and_run();
}
catch(boost::system::system_error &err) {
// It's possible the exception gets thrown after calling server->stop() from a different thread
if(shutdown_event->peek()) {
return;
}
BOOST_LOG(fatal) << "Couldn't start Configuration HTTPS server to port ["sv << port_https << "]: "sv << err.what();
shutdown_event->raise(true);
return;
}
};
std::thread tcp { accept_and_run, &server };
// Wait for any event
shutdown_event->view();
server.stop();
tcp.join();
}
} // namespace confighttp

21
sunshine/confighttp.h Normal file
View File

@@ -0,0 +1,21 @@
//
// Created by loki on 6/3/19.
//
#ifndef SUNSHINE_CONFIGHTTP_H
#define SUNSHINE_CONFIGHTTP_H
#include <functional>
#include <string>
#include "thread_safe.h"
#define WEB_DIR SUNSHINE_ASSETS_DIR "/web/"
namespace confighttp {
constexpr auto PORT_HTTPS = 1;
void start();
} // namespace confighttp
#endif //SUNSHINE_CONFIGHTTP_H

View File

@@ -2,14 +2,14 @@
// Created by loki on 5/31/19.
//
#include <openssl/pem.h>
#include "crypto.h"
#include <openssl/pem.h>
namespace crypto {
using big_num_t = util::safe_ptr<BIGNUM, BN_free>;
//using rsa_t = util::safe_ptr<RSA, RSA_free>;
using asn1_string_t = util::safe_ptr<ASN1_STRING, ASN1_STRING_free>;
cert_chain_t::cert_chain_t() : _certs {}, _cert_ctx {X509_STORE_CTX_new() } {}
cert_chain_t::cert_chain_t() : _certs {}, _cert_ctx { X509_STORE_CTX_new() } {}
void cert_chain_t::add(x509_t &&cert) {
x509_store_t x509_store { X509_STORE_new() };
@@ -26,7 +26,7 @@ void cert_chain_t::add(x509_t &&cert) {
*/
const char *cert_chain_t::verify(x509_t::element_type *cert) {
int err_code = 0;
for(auto &[_,x509_store] : _certs) {
for(auto &[_, x509_store] : _certs) {
auto fg = util::fail_guard([this]() {
X509_STORE_CTX_cleanup(_cert_ctx.get());
});
@@ -36,7 +36,7 @@ const char *cert_chain_t::verify(x509_t::element_type *cert) {
auto err = X509_verify_cert(_cert_ctx.get());
if (err == 1) {
if(err == 1) {
return nullptr;
}
@@ -46,7 +46,7 @@ const char *cert_chain_t::verify(x509_t::element_type *cert) {
if(err_code == X509_V_ERR_UNABLE_TO_GET_ISSUER_CERT_LOCALLY) {
return nullptr;
}
if (err_code != X509_V_ERR_DEPTH_ZERO_SELF_SIGNED_CERT && err_code != X509_V_ERR_INVALID_CA) {
if(err_code != X509_V_ERR_DEPTH_ZERO_SELF_SIGNED_CERT && err_code != X509_V_ERR_INVALID_CA) {
return X509_verify_cert_error_string(err_code);
}
}
@@ -63,7 +63,7 @@ int cipher_t::decrypt(const std::string_view &cipher, std::vector<std::uint8_t>
});
// Gen 7 servers use 128-bit AES ECB
if (EVP_DecryptInit_ex(ctx.get(), EVP_aes_128_ecb(), nullptr, key.data(), nullptr) != 1) {
if(EVP_DecryptInit_ex(ctx.get(), EVP_aes_128_ecb(), nullptr, key.data(), nullptr) != 1) {
return -1;
}
@@ -72,11 +72,11 @@ int cipher_t::decrypt(const std::string_view &cipher, std::vector<std::uint8_t>
plaintext.resize((cipher.size() + 15) / 16 * 16);
auto size = (int)plaintext.size();
// Encrypt into the caller's buffer, leaving room for the auth tag to be prepended
if (EVP_DecryptUpdate(ctx.get(), plaintext.data(), &size, (const std::uint8_t*)cipher.data(), cipher.size()) != 1) {
if(EVP_DecryptUpdate(ctx.get(), plaintext.data(), &size, (const std::uint8_t *)cipher.data(), cipher.size()) != 1) {
return -1;
}
if (EVP_DecryptFinal_ex(ctx.get(), plaintext.data(), &len) != 1) {
if(EVP_DecryptFinal_ex(ctx.get(), plaintext.data(), &len) != 1) {
return -1;
}
@@ -85,7 +85,7 @@ int cipher_t::decrypt(const std::string_view &cipher, std::vector<std::uint8_t>
}
int cipher_t::decrypt_gcm(aes_t &iv, const std::string_view &tagged_cipher,
std::vector<std::uint8_t> &plaintext) {
std::vector<std::uint8_t> &plaintext) {
auto cipher = tagged_cipher.substr(16);
auto tag = tagged_cipher.substr(0, 16);
@@ -93,15 +93,15 @@ int cipher_t::decrypt_gcm(aes_t &iv, const std::string_view &tagged_cipher,
EVP_CIPHER_CTX_reset(ctx.get());
});
if (EVP_DecryptInit_ex(ctx.get(), EVP_aes_128_gcm(), nullptr, nullptr, nullptr) != 1) {
if(EVP_DecryptInit_ex(ctx.get(), EVP_aes_128_gcm(), nullptr, nullptr, nullptr) != 1) {
return -1;
}
if (EVP_CIPHER_CTX_ctrl(ctx.get(), EVP_CTRL_GCM_SET_IVLEN, iv.size(), nullptr) != 1) {
if(EVP_CIPHER_CTX_ctrl(ctx.get(), EVP_CTRL_GCM_SET_IVLEN, iv.size(), nullptr) != 1) {
return -1;
}
if (EVP_DecryptInit_ex(ctx.get(), nullptr, nullptr, key.data(), iv.data()) != 1) {
if(EVP_DecryptInit_ex(ctx.get(), nullptr, nullptr, key.data(), iv.data()) != 1) {
return -1;
}
@@ -109,16 +109,16 @@ int cipher_t::decrypt_gcm(aes_t &iv, const std::string_view &tagged_cipher,
plaintext.resize((cipher.size() + 15) / 16 * 16);
int size;
if (EVP_DecryptUpdate(ctx.get(), plaintext.data(), &size, (const std::uint8_t*)cipher.data(), cipher.size()) != 1) {
if(EVP_DecryptUpdate(ctx.get(), plaintext.data(), &size, (const std::uint8_t *)cipher.data(), cipher.size()) != 1) {
return -1;
}
if (EVP_CIPHER_CTX_ctrl(ctx.get(), EVP_CTRL_GCM_SET_TAG, tag.size(), const_cast<char*>(tag.data())) != 1) {
if(EVP_CIPHER_CTX_ctrl(ctx.get(), EVP_CTRL_GCM_SET_TAG, tag.size(), const_cast<char *>(tag.data())) != 1) {
return -1;
}
int len = size;
if (EVP_DecryptFinal_ex(ctx.get(), plaintext.data() + size, &len) != 1) {
if(EVP_DecryptFinal_ex(ctx.get(), plaintext.data() + size, &len) != 1) {
return -1;
}
@@ -134,7 +134,7 @@ int cipher_t::encrypt(const std::string_view &plaintext, std::vector<std::uint8_
});
// Gen 7 servers use 128-bit AES ECB
if (EVP_EncryptInit_ex(ctx.get(), EVP_aes_128_ecb(), nullptr, key.data(), nullptr) != 1) {
if(EVP_EncryptInit_ex(ctx.get(), EVP_aes_128_ecb(), nullptr, key.data(), nullptr) != 1) {
return -1;
}
@@ -143,11 +143,11 @@ int cipher_t::encrypt(const std::string_view &plaintext, std::vector<std::uint8_
cipher.resize((plaintext.size() + 15) / 16 * 16);
auto size = (int)cipher.size();
// Encrypt into the caller's buffer
if (EVP_EncryptUpdate(ctx.get(), cipher.data(), &size, (const std::uint8_t*)plaintext.data(), plaintext.size()) != 1) {
if(EVP_EncryptUpdate(ctx.get(), cipher.data(), &size, (const std::uint8_t *)plaintext.data(), plaintext.size()) != 1) {
return -1;
}
if (EVP_EncryptFinal_ex(ctx.get(), cipher.data() + size, &len) != 1) {
if(EVP_EncryptFinal_ex(ctx.get(), cipher.data() + size, &len) != 1) {
return -1;
}
@@ -187,10 +187,10 @@ x509_t x509(const std::string_view &x) {
BIO_write(io.get(), x.data(), x.size());
X509 *p = nullptr;
x509_t p;
PEM_read_bio_X509(io.get(), &p, nullptr, nullptr);
return x509_t { p };
return p;
}
pkey_t pkey(const std::string_view &k) {
@@ -198,10 +198,10 @@ pkey_t pkey(const std::string_view &k) {
BIO_write(io.get(), k.data(), k.size());
EVP_PKEY *p = nullptr;
pkey_t p = nullptr;
PEM_read_bio_PrivateKey(io.get(), &p, nullptr, nullptr);
return pkey_t { p };
return p;
}
std::string pem(x509_t &x509) {
@@ -230,14 +230,14 @@ std::string_view signature(const x509_t &x) {
const ASN1_BIT_STRING *asn1 = nullptr;
X509_get0_signature(&asn1, nullptr, x.get());
return { (const char*)asn1->data, (std::size_t)asn1->length };
return { (const char *)asn1->data, (std::size_t)asn1->length };
}
std::string rand(std::size_t bytes) {
std::string r;
r.resize(bytes);
RAND_bytes((uint8_t*)r.data(), r.size());
RAND_bytes((uint8_t *)r.data(), r.size());
return r;
}
@@ -297,8 +297,8 @@ creds_t gen_creds(const std::string_view &cn, std::uint32_t key_bits) {
X509_set_pubkey(x509.get(), pkey.get());
auto name = X509_get_subject_name(x509.get());
X509_NAME_add_entry_by_txt(name,"CN", MBSTRING_ASC,
(const std::uint8_t*)cn.data(), cn.size(),
X509_NAME_add_entry_by_txt(name, "CN", MBSTRING_ASC,
(const std::uint8_t *)cn.data(), cn.size(),
-1, 0);
X509_set_issuer_name(x509.get(), name);
@@ -324,7 +324,7 @@ bool verify(const x509_t &x509, const std::string_view &data, const std::string_
return false;
}
if(EVP_DigestVerifyFinal(ctx.get(), (const uint8_t*)signature.data(), signature.size()) != 1) {
if(EVP_DigestVerifyFinal(ctx.get(), (const uint8_t *)signature.data(), signature.size()) != 1) {
return false;
}
@@ -338,4 +338,14 @@ bool verify256(const x509_t &x509, const std::string_view &data, const std::stri
void md_ctx_destroy(EVP_MD_CTX *ctx) {
EVP_MD_CTX_destroy(ctx);
}
std::string rand_alphabet(std::size_t bytes, const std::string_view &alphabet) {
auto value = rand(bytes);
for(std::size_t i = 0; i != value.size(); ++i) {
value[i] = alphabet[value[i] % alphabet.length()];
}
return value;
}
} // namespace crypto

View File

@@ -5,12 +5,11 @@
#ifndef SUNSHINE_CRYPTO_H
#define SUNSHINE_CRYPTO_H
#include <cassert>
#include <array>
#include <openssl/evp.h>
#include <openssl/rand.h>
#include <openssl/sha.h>
#include <openssl/x509.h>
#include <openssl/rand.h>
#include "utility.h"
@@ -25,16 +24,17 @@ void md_ctx_destroy(EVP_MD_CTX *);
using sha256_t = std::array<std::uint8_t, SHA256_DIGEST_LENGTH>;
using aes_t = std::array<std::uint8_t, 16>;
using x509_t = util::safe_ptr<X509, X509_free>;
using x509_store_t = util::safe_ptr<X509_STORE, X509_STORE_free>;
using aes_t = std::array<std::uint8_t, 16>;
using x509_t = util::safe_ptr<X509, X509_free>;
using x509_store_t = util::safe_ptr<X509_STORE, X509_STORE_free>;
using x509_store_ctx_t = util::safe_ptr<X509_STORE_CTX, X509_STORE_CTX_free>;
using cipher_ctx_t = util::safe_ptr<EVP_CIPHER_CTX, EVP_CIPHER_CTX_free>;
using md_ctx_t = util::safe_ptr<EVP_MD_CTX, md_ctx_destroy>;
using bio_t = util::safe_ptr<BIO, BIO_free_all>;
using pkey_t = util::safe_ptr<EVP_PKEY, EVP_PKEY_free>;
using cipher_ctx_t = util::safe_ptr<EVP_CIPHER_CTX, EVP_CIPHER_CTX_free>;
using md_ctx_t = util::safe_ptr<EVP_MD_CTX, md_ctx_destroy>;
using bio_t = util::safe_ptr<BIO, BIO_free_all>;
using pkey_t = util::safe_ptr<EVP_PKEY, EVP_PKEY_free>;
sha256_t hash(const std::string_view &plaintext);
aes_t gen_aes_key(const std::array<uint8_t, 16> &salt, const std::string_view &pin);
x509_t x509(const std::string_view &x);
@@ -50,6 +50,8 @@ creds_t gen_creds(const std::string_view &cn, std::uint32_t key_bits);
std::string_view signature(const x509_t &x);
std::string rand(std::size_t bytes);
std::string rand_alphabet(std::size_t bytes,
const std::string_view &alphabet = std::string_view { "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!%&()=-" });
class cert_chain_t {
public:
@@ -58,6 +60,7 @@ public:
void add(x509_t &&cert);
const char *verify(x509_t::element_type *cert);
private:
std::vector<std::pair<x509_t, x509_store_t>> _certs;
x509_store_ctx_t _cert_ctx;
@@ -66,13 +69,14 @@ private:
class cipher_t {
public:
cipher_t(const aes_t &key);
cipher_t(cipher_t&&) noexcept = default;
cipher_t &operator=(cipher_t&&) noexcept = default;
cipher_t(cipher_t &&) noexcept = default;
cipher_t &operator=(cipher_t &&) noexcept = default;
int encrypt(const std::string_view &plaintext, std::vector<std::uint8_t> &cipher);
int decrypt_gcm(aes_t &iv, const std::string_view &cipher, std::vector<std::uint8_t> &plaintext);
int decrypt(const std::string_view &cipher, std::vector<std::uint8_t> &plaintext);
private:
cipher_ctx_t ctx;
aes_t key;
@@ -80,6 +84,6 @@ private:
public:
bool padding;
};
}
} // namespace crypto
#endif //SUNSHINE_CRYPTO_H

192
sunshine/httpcommon.cpp Normal file
View File

@@ -0,0 +1,192 @@
#define BOOST_BIND_GLOBAL_PLACEHOLDERS
#include "process.h"
#include <filesystem>
#include <boost/property_tree/json_parser.hpp>
#include <boost/property_tree/ptree.hpp>
#include <boost/property_tree/xml_parser.hpp>
#include <boost/asio/ssl/context.hpp>
#include <Simple-Web-Server/server_http.hpp>
#include <Simple-Web-Server/server_https.hpp>
#include <boost/asio/ssl/context_base.hpp>
#include "config.h"
#include "crypto.h"
#include "httpcommon.h"
#include "main.h"
#include "network.h"
#include "nvhttp.h"
#include "platform/common.h"
#include "rtsp.h"
#include "utility.h"
#include "uuid.h"
namespace http {
using namespace std::literals;
namespace fs = std::filesystem;
namespace pt = boost::property_tree;
int reload_user_creds(const std::string &file);
bool user_creds_exist(const std::string &file);
std::string unique_id;
net::net_e origin_pin_allowed;
net::net_e origin_web_ui_allowed;
int init() {
bool clean_slate = config::sunshine.flags[config::flag::FRESH_STATE];
origin_pin_allowed = net::from_enum_string(config::nvhttp.origin_pin_allowed);
origin_web_ui_allowed = net::from_enum_string(config::nvhttp.origin_web_ui_allowed);
if(clean_slate) {
unique_id = util::uuid_t::generate().string();
auto dir = std::filesystem::temp_directory_path() / "Sushine"sv;
config::nvhttp.cert = (dir / ("cert-"s + unique_id)).string();
config::nvhttp.pkey = (dir / ("pkey-"s + unique_id)).string();
}
if(!fs::exists(config::nvhttp.pkey) || !fs::exists(config::nvhttp.cert)) {
if(create_creds(config::nvhttp.pkey, config::nvhttp.cert)) {
return -1;
}
}
if(!user_creds_exist(config::sunshine.credentials_file)) {
if(save_user_creds(config::sunshine.credentials_file, "sunshine"s, crypto::rand_alphabet(16), true)) {
return -1;
}
}
if(reload_user_creds(config::sunshine.credentials_file)) {
return -1;
}
return 0;
}
int save_user_creds(const std::string &file, const std::string &username, const std::string &password, bool run_our_mouth) {
pt::ptree outputTree;
if(fs::exists(file)) {
try {
pt::read_json(file, outputTree);
}
catch(std::exception &e) {
BOOST_LOG(error) << "Couldn't read user credentials: "sv << e.what();
return -1;
}
}
auto salt = crypto::rand_alphabet(16);
outputTree.put("username", username);
outputTree.put("salt", salt);
outputTree.put("password", util::hex(crypto::hash(password + salt)).to_string());
try {
pt::write_json(file, outputTree);
}
catch(std::exception &e) {
BOOST_LOG(error) << "generating user credentials: "sv << e.what();
return -1;
}
BOOST_LOG(info) << "New credentials have been created"sv;
if(run_our_mouth) {
BOOST_LOG(info) << "Username: "sv << username;
BOOST_LOG(info) << "Password: "sv << password;
}
return 0;
}
bool user_creds_exist(const std::string &file) {
if(!fs::exists(file)) {
return false;
}
pt::ptree inputTree;
try {
pt::read_json(file, inputTree);
return inputTree.find("username") != inputTree.not_found() &&
inputTree.find("password") != inputTree.not_found() &&
inputTree.find("salt") != inputTree.not_found();
}
catch(std::exception &e) {
BOOST_LOG(error) << "validating user credentials: "sv << e.what();
}
return false;
}
int reload_user_creds(const std::string &file) {
pt::ptree inputTree;
try {
pt::read_json(file, inputTree);
config::sunshine.username = inputTree.get<std::string>("username");
config::sunshine.password = inputTree.get<std::string>("password");
config::sunshine.salt = inputTree.get<std::string>("salt");
}
catch(std::exception &e) {
BOOST_LOG(error) << "loading user credentials: "sv << e.what();
return -1;
}
return 0;
}
int create_creds(const std::string &pkey, const std::string &cert) {
fs::path pkey_path = pkey;
fs::path cert_path = cert;
auto creds = crypto::gen_creds("Sunshine Gamestream Host"sv, 2048);
auto pkey_dir = pkey_path;
auto cert_dir = cert_path;
pkey_dir.remove_filename();
cert_dir.remove_filename();
std::error_code err_code {};
fs::create_directories(pkey_dir, err_code);
if(err_code) {
BOOST_LOG(error) << "Couldn't create directory ["sv << pkey_dir << "] :"sv << err_code.message();
return -1;
}
fs::create_directories(cert_dir, err_code);
if(err_code) {
BOOST_LOG(error) << "Couldn't create directory ["sv << cert_dir << "] :"sv << err_code.message();
return -1;
}
if(write_file(pkey.c_str(), creds.pkey)) {
BOOST_LOG(error) << "Couldn't open ["sv << config::nvhttp.pkey << ']';
return -1;
}
if(write_file(cert.c_str(), creds.x509)) {
BOOST_LOG(error) << "Couldn't open ["sv << config::nvhttp.cert << ']';
return -1;
}
fs::permissions(pkey_path,
fs::perms::owner_read | fs::perms::owner_write,
fs::perm_options::replace, err_code);
if(err_code) {
BOOST_LOG(error) << "Couldn't change permissions of ["sv << config::nvhttp.pkey << "] :"sv << err_code.message();
return -1;
}
fs::permissions(cert_path,
fs::perms::owner_read | fs::perms::group_read | fs::perms::others_read | fs::perms::owner_write,
fs::perm_options::replace, err_code);
if(err_code) {
BOOST_LOG(error) << "Couldn't change permissions of ["sv << config::nvhttp.cert << "] :"sv << err_code.message();
return -1;
}
return 0;
}
} // namespace http

19
sunshine/httpcommon.h Normal file
View File

@@ -0,0 +1,19 @@
#include "network.h"
#include "thread_safe.h"
namespace http {
int init();
int create_creds(const std::string &pkey, const std::string &cert);
int save_user_creds(
const std::string &file,
const std::string &username,
const std::string &password,
bool run_our_mouth = false);
int reload_user_creds(const std::string &file);
extern std::string unique_id;
extern net::net_e origin_pin_allowed;
extern net::net_e origin_web_ui_allowed;
} // namespace http

View File

@@ -2,26 +2,123 @@
// Created by loki on 6/20/19.
//
// define uint32_t for <moonlight-common-c/src/Input.h>
#include <cstdint>
extern "C" {
#include <moonlight-common-c/src/Input.h>
}
#include <cstring>
#include <bitset>
#include "main.h"
#include "config.h"
#include "input.h"
#include "main.h"
#include "platform/common.h"
#include "thread_pool.h"
#include "utility.h"
namespace input {
constexpr auto MAX_GAMEPADS = std::min((std::size_t)platf::MAX_GAMEPADS, sizeof(std::int16_t) * 8);
#define DISABLE_LEFT_BUTTON_DELAY ((util::ThreadPool::task_id_t)0x01)
#define ENABLE_LEFT_BUTTON_DELAY nullptr
enum class button_state_e {
NONE,
DOWN,
UP
};
template<std::size_t N>
int alloc_id(std::bitset<N> &gamepad_mask) {
for(int x = 0; x < gamepad_mask.size(); ++x) {
if(!gamepad_mask[x]) {
gamepad_mask[x] = true;
return x;
}
}
return -1;
}
template<std::size_t N>
void free_id(std::bitset<N> &gamepad_mask, int id) {
gamepad_mask[id] = false;
}
static util::TaskPool::task_id_t task_id {};
static std::unordered_map<short, bool> key_press {};
static std::array<std::uint8_t, 5> mouse_press {};
static platf::input_t platf_input;
static std::bitset<platf::MAX_GAMEPADS> gamepadMask {};
void free_gamepad(platf::input_t &platf_input, int id) {
platf::gamepad(platf_input, id, platf::gamepad_state_t {});
platf::free_gamepad(platf_input, id);
free_id(gamepadMask, id);
}
struct gamepad_t {
gamepad_t() : gamepad_state {}, back_timeout_id {}, id { -1 }, back_button_state { button_state_e::NONE } {}
~gamepad_t() {
if(id >= 0) {
task_pool.push([id = this->id]() {
free_gamepad(platf_input, id);
});
}
}
platf::gamepad_state_t gamepad_state;
util::ThreadPool::task_id_t back_timeout_id;
int id;
// When emulating the HOME button, we may need to artificially release the back button.
// Afterwards, the gamepad state on sunshine won't match the state on Moonlight.
// To prevent Sunshine from sending erronious input data to the active application,
// Sunshine forces the button to be in a specific state until the gamepad state matches that of
// Moonlight once more.
button_state_e back_button_state;
};
struct input_t {
input_t(safe::mail_raw_t::event_t<input::touch_port_t> touch_port_event)
: active_gamepad_state {},
gamepads(MAX_GAMEPADS),
touch_port_event { std::move(touch_port_event) },
mouse_left_button_timeout {},
touch_port { 0, 0, 0, 0, 0, 0, 1.0f } {}
std::uint16_t active_gamepad_state;
std::vector<gamepad_t> gamepads;
safe::mail_raw_t::event_t<input::touch_port_t> touch_port_event;
util::ThreadPool::task_id_t mouse_left_button_timeout;
input::touch_port_t touch_port;
};
using namespace std::literals;
void print(PNV_MOUSE_MOVE_PACKET packet) {
void print(PNV_REL_MOUSE_MOVE_PACKET packet) {
BOOST_LOG(debug)
<< "--begin mouse move packet--"sv << std::endl
<< "--begin relative mouse move packet--"sv << std::endl
<< "deltaX ["sv << util::endian::big(packet->deltaX) << ']' << std::endl
<< "deltaY ["sv << util::endian::big(packet->deltaY) << ']' << std::endl
<< "--end mouse move packet--"sv;
<< "--end relative mouse move packet--"sv;
}
void print(PNV_ABS_MOUSE_MOVE_PACKET packet) {
BOOST_LOG(debug)
<< "--begin absolute mouse move packet--"sv << std::endl
<< "x ["sv << util::endian::big(packet->x) << ']' << std::endl
<< "y ["sv << util::endian::big(packet->y) << ']' << std::endl
<< "width ["sv << util::endian::big(packet->width) << ']' << std::endl
<< "height ["sv << util::endian::big(packet->height) << ']' << std::endl
<< "--end absolute mouse move packet--"sv;
}
void print(PNV_MOUSE_BUTTON_PACKET packet) {
@@ -65,80 +162,284 @@ void print(PNV_MULTI_CONTROLLER_PACKET packet) {
constexpr int PACKET_TYPE_SCROLL_OR_KEYBOARD = PACKET_TYPE_SCROLL;
void print(void *input) {
int input_type = util::endian::big(*(int*)input);
int input_type = util::endian::big(*(int *)input);
switch(input_type) {
case PACKET_TYPE_MOUSE_MOVE:
print((PNV_MOUSE_MOVE_PACKET)input);
break;
case PACKET_TYPE_MOUSE_BUTTON:
print((PNV_MOUSE_BUTTON_PACKET)input);
break;
case PACKET_TYPE_SCROLL_OR_KEYBOARD:
{
char *tmp_input = (char*)input + 4;
if(tmp_input[0] == 0x0A) {
print((PNV_SCROLL_PACKET)input);
}
else {
print((PNV_KEYBOARD_PACKET)input);
}
break;
case PACKET_TYPE_REL_MOUSE_MOVE:
print((PNV_REL_MOUSE_MOVE_PACKET)input);
break;
case PACKET_TYPE_ABS_MOUSE_MOVE:
print((PNV_ABS_MOUSE_MOVE_PACKET)input);
break;
case PACKET_TYPE_MOUSE_BUTTON:
print((PNV_MOUSE_BUTTON_PACKET)input);
break;
case PACKET_TYPE_SCROLL_OR_KEYBOARD: {
char *tmp_input = (char *)input + 4;
if(tmp_input[0] == 0x0A) {
print((PNV_SCROLL_PACKET)input);
}
case PACKET_TYPE_MULTI_CONTROLLER:
print((PNV_MULTI_CONTROLLER_PACKET)input);
break;
else {
print((PNV_KEYBOARD_PACKET)input);
}
break;
}
case PACKET_TYPE_MULTI_CONTROLLER:
print((PNV_MULTI_CONTROLLER_PACKET)input);
break;
}
}
void passthrough(platf::input_t &input, PNV_MOUSE_MOVE_PACKET packet) {
void passthrough(std::shared_ptr<input_t> &input, PNV_REL_MOUSE_MOVE_PACKET packet) {
display_cursor = true;
platf::move_mouse(input, util::endian::big(packet->deltaX), util::endian::big(packet->deltaY));
input->mouse_left_button_timeout = DISABLE_LEFT_BUTTON_DELAY;
platf::move_mouse(platf_input, util::endian::big(packet->deltaX), util::endian::big(packet->deltaY));
}
void passthrough(std::shared_ptr<input_t> &input, PNV_ABS_MOUSE_MOVE_PACKET packet) {
display_cursor = true;
if(input->mouse_left_button_timeout == DISABLE_LEFT_BUTTON_DELAY) {
input->mouse_left_button_timeout = ENABLE_LEFT_BUTTON_DELAY;
}
auto &touch_port_event = input->touch_port_event;
auto &touch_port = input->touch_port;
if(touch_port_event->peek()) {
touch_port = *touch_port_event->pop();
}
float x = util::endian::big(packet->x);
float y = util::endian::big(packet->y);
// Prevent divide by zero
// Don't expect it to happen, but just in case
if(!packet->width || !packet->height) {
BOOST_LOG(warning) << "Moonlight passed invalid dimensions"sv;
return;
}
int width = util::endian::big(packet->width);
int height = util::endian::big(packet->height);
auto offsetX = (width - (float)touch_port.width) * 0.5f;
auto offsetY = (height - (float)touch_port.height) * 0.5f;
std::clamp(x, offsetX, width - offsetX);
std::clamp(y, offsetX, height - offsetY);
platf::touch_port_t abs_port {
touch_port.offset_x, touch_port.offset_y,
touch_port.env_width, touch_port.env_height
};
platf::abs_mouse(platf_input, abs_port, (x - offsetX) * touch_port.scalar_inv, (y - offsetY) * touch_port.scalar_inv);
}
void passthrough(std::shared_ptr<input_t> &input, PNV_MOUSE_BUTTON_PACKET packet) {
auto constexpr BUTTON_RELEASED = 0x09;
auto constexpr BUTTON_LEFT = 0x01;
auto constexpr BUTTON_RIGHT = 0x03;
display_cursor = true;
auto button = util::endian::big(packet->button);
if(button > 0 && button < input->mouse_press.size()) {
input->mouse_press[button] = packet->action != BUTTON_RELEASED;
}
auto release = packet->action == BUTTON_RELEASED;
platf::button_mouse(input->input, button, packet->action == BUTTON_RELEASED);
auto button = util::endian::big(packet->button);
if(button > 0 && button < mouse_press.size()) {
if(mouse_press[button] != release) {
// button state is already what we want
return;
}
mouse_press[button] = !release;
}
///////////////////////////////////
/*/
* When Moonlight sends mouse input through absolute coordinates,
* it's possible that BUTTON_RIGHT is pressed down immediately after releasing BUTTON_LEFT.
* As a result, Sunshine will left click on hyperlinks in the browser before right clicking
*
* This can be solved by delaying BUTTON_LEFT, however, any delay on input is undesirable during gaming
* As a compromise, Sunshine will only put delays on BUTTON_LEFT when
* absolute mouse coordinates have been send.
*
* Try to make sure BUTTON_RIGHT gets called before BUTTON_LEFT is released.
*
* input->mouse_left_button_timeout can only be nullptr
* when the last mouse coordinates were absolute
/*/
if(button == BUTTON_LEFT && release && !input->mouse_left_button_timeout) {
auto f = [=]() {
auto left_released = mouse_press[BUTTON_LEFT];
if(left_released) {
// Already released left button
return;
}
platf::button_mouse(platf_input, BUTTON_LEFT, release);
mouse_press[BUTTON_LEFT] = false;
input->mouse_left_button_timeout = nullptr;
};
input->mouse_left_button_timeout = task_pool.pushDelayed(std::move(f), 10ms).task_id;
return;
}
if(
button == BUTTON_RIGHT && !release &&
input->mouse_left_button_timeout > DISABLE_LEFT_BUTTON_DELAY) {
platf::button_mouse(platf_input, BUTTON_RIGHT, false);
platf::button_mouse(platf_input, BUTTON_RIGHT, true);
mouse_press[BUTTON_RIGHT] = false;
return;
}
///////////////////////////////////
platf::button_mouse(platf_input, button, release);
}
void repeat_key(short key_code) {
// If key no longer pressed, stop repeating
if(!key_press[key_code]) {
task_id = nullptr;
return;
}
platf::keyboard(platf_input, key_code & 0x00FF, false);
task_id = task_pool.pushDelayed(repeat_key, config::input.key_repeat_period, key_code).task_id;
}
short map_keycode(short keycode) {
keycode &= 0x00FF;
switch(keycode) {
case 0x10:
return 0xA0;
case 0x11:
return 0xA2;
case 0x12:
return 0xA4;
}
return keycode;
}
void passthrough(std::shared_ptr<input_t> &input, PNV_KEYBOARD_PACKET packet) {
auto constexpr BUTTON_RELEASED = 0x04;
input->key_press[packet->keyCode] = packet->keyAction != BUTTON_RELEASED;
platf::keyboard(input->input, packet->keyCode & 0x00FF, packet->keyAction == BUTTON_RELEASED);
auto release = packet->keyAction == BUTTON_RELEASED;
auto &pressed = key_press[packet->keyCode];
if(!pressed) {
if(!release) {
if(task_id) {
task_pool.cancel(task_id);
}
if(config::input.key_repeat_delay.count() > 0) {
task_id = task_pool.pushDelayed(repeat_key, config::input.key_repeat_delay, packet->keyCode).task_id;
}
}
else {
// Already released
return;
}
}
else if(!release) {
// Already pressed down key
return;
}
pressed = !release;
platf::keyboard(platf_input, map_keycode(packet->keyCode), release);
}
void passthrough(platf::input_t &input, PNV_SCROLL_PACKET packet) {
void passthrough(PNV_SCROLL_PACKET packet) {
display_cursor = true;
platf::scroll(input, util::endian::big(packet->scrollAmt1));
platf::scroll(platf_input, util::endian::big(packet->scrollAmt1));
}
int updateGamepads(std::vector<gamepad_t> &gamepads, std::int16_t old_state, std::int16_t new_state) {
auto xorGamepadMask = old_state ^ new_state;
if(!xorGamepadMask) {
return 0;
}
for(int x = 0; x < sizeof(std::int16_t) * 8; ++x) {
if((xorGamepadMask >> x) & 1) {
auto &gamepad = gamepads[x];
if((old_state >> x) & 1) {
if(gamepad.id < 0) {
return -1;
}
free_gamepad(platf_input, gamepad.id);
gamepad.id = -1;
}
else {
auto id = alloc_id(gamepadMask);
if(id < 0) {
// Out of gamepads
return -1;
}
if(platf::alloc_gamepad(platf_input, id)) {
free_id(gamepadMask, id);
// allocating a gamepad failed: solution: ignore gamepads
// The implementations of platf::alloc_gamepad already has logging
return -1;
}
gamepad.id = id;
}
}
}
return 0;
}
void passthrough(std::shared_ptr<input_t> &input, PNV_MULTI_CONTROLLER_PACKET packet) {
if(updateGamepads(input->gamepads, input->active_gamepad_state, packet->activeGamepadMask)) {
return;
}
input->active_gamepad_state = packet->activeGamepadMask;
if(packet->controllerNumber < 0 || packet->controllerNumber >= input->gamepads.size()) {
BOOST_LOG(warning) << "ControllerNumber out of range ["sv << packet->controllerNumber << ']';
return;
}
if(!((input->active_gamepad_state >> packet->controllerNumber) & 1)) {
BOOST_LOG(warning) << "ControllerNumber ["sv << packet->controllerNumber << "] not allocated"sv;
return;
}
auto &gamepad = input->gamepads[packet->controllerNumber];
// If this gamepad has not been initialized, ignore it.
// This could happen when platf::alloc_gamepad fails
if(gamepad.id < 0) {
return;
}
display_cursor = false;
std::uint16_t bf;
std::memcpy(&bf, &packet->buttonFlags, sizeof(std::uint16_t));
platf::gamepad_state_t gamepad_state{
std::uint16_t bf = packet->buttonFlags;
platf::gamepad_state_t gamepad_state {
bf,
packet->leftTrigger,
packet->rightTrigger,
@@ -150,31 +451,30 @@ void passthrough(std::shared_ptr<input_t> &input, PNV_MULTI_CONTROLLER_PACKET pa
auto bf_new = gamepad_state.buttonFlags;
switch(gamepad.back_button_state) {
case button_state_e::UP:
if(!(platf::BACK & bf_new)) {
gamepad.back_button_state = button_state_e::NONE;
}
gamepad_state.buttonFlags &= ~platf::BACK;
break;
case button_state_e::DOWN:
if(platf::BACK & bf_new) {
gamepad.back_button_state = button_state_e::NONE;
}
gamepad_state.buttonFlags |= platf::BACK;
break;
case button_state_e::NONE:
break;
case button_state_e::UP:
if(!(platf::BACK & bf_new)) {
gamepad.back_button_state = button_state_e::NONE;
}
gamepad_state.buttonFlags &= ~platf::BACK;
break;
case button_state_e::DOWN:
if(platf::BACK & bf_new) {
gamepad.back_button_state = button_state_e::NONE;
}
gamepad_state.buttonFlags |= platf::BACK;
break;
case button_state_e::NONE:
break;
}
bf = gamepad_state.buttonFlags ^ gamepad.gamepad_state.buttonFlags;
bf = gamepad_state.buttonFlags ^ gamepad.gamepad_state.buttonFlags;
bf_new = gamepad_state.buttonFlags;
if (platf::BACK & bf) {
if (platf::BACK & bf_new) {
if(platf::BACK & bf) {
if(platf::BACK & bf_new) {
// Don't emulate home button if timeout < 0
if(config::input.back_button_timeout >= 0ms) {
gamepad.back_timeout_id = task_pool.pushDelayed([input, controller=packet->controllerNumber]() {
auto f = [input, controller = packet->controllerNumber]() {
auto &gamepad = input->gamepads[controller];
auto &state = gamepad.gamepad_state;
@@ -182,27 +482,29 @@ void passthrough(std::shared_ptr<input_t> &input, PNV_MULTI_CONTROLLER_PACKET pa
// Force the back button up
gamepad.back_button_state = button_state_e::UP;
state.buttonFlags &= ~platf::BACK;
platf::gamepad(input->input, controller, state);
platf::gamepad(platf_input, gamepad.id, state);
// Press Home button
state.buttonFlags |= platf::HOME;
platf::gamepad(input->input, controller, state);
platf::gamepad(platf_input, gamepad.id, state);
// Release Home button
state.buttonFlags &= ~platf::HOME;
platf::gamepad(input->input, controller, state);
platf::gamepad(platf_input, gamepad.id, state);
gamepad.back_timeout_id = nullptr;
}, config::input.back_button_timeout).task_id;
};
gamepad.back_timeout_id = task_pool.pushDelayed(std::move(f), config::input.back_button_timeout).task_id;
}
}
else if (gamepad.back_timeout_id) {
else if(gamepad.back_timeout_id) {
task_pool.cancel(gamepad.back_timeout_id);
gamepad.back_timeout_id = nullptr;
}
}
platf::gamepad(input->input, packet->controllerNumber, gamepad_state);
platf::gamepad(platf_input, gamepad.id, gamepad_state);
gamepad.gamepad_state = gamepad_state;
}
@@ -210,60 +512,33 @@ void passthrough(std::shared_ptr<input_t> &input, PNV_MULTI_CONTROLLER_PACKET pa
void passthrough_helper(std::shared_ptr<input_t> input, std::vector<std::uint8_t> &&input_data) {
void *payload = input_data.data();
int input_type = util::endian::big(*(int*)payload);
int input_type = util::endian::big(*(int *)payload);
switch(input_type) {
case PACKET_TYPE_MOUSE_MOVE:
passthrough(input->input, (PNV_MOUSE_MOVE_PACKET)payload);
break;
case PACKET_TYPE_MOUSE_BUTTON:
passthrough(input, (PNV_MOUSE_BUTTON_PACKET)payload);
break;
case PACKET_TYPE_SCROLL_OR_KEYBOARD:
{
char *tmp_input = (char*)payload + 4;
if(tmp_input[0] == 0x0A) {
passthrough(input->input, (PNV_SCROLL_PACKET)payload);
}
else {
passthrough(input, (PNV_KEYBOARD_PACKET)payload);
}
break;
case PACKET_TYPE_REL_MOUSE_MOVE:
passthrough(input, (PNV_REL_MOUSE_MOVE_PACKET)payload);
break;
case PACKET_TYPE_ABS_MOUSE_MOVE:
passthrough(input, (PNV_ABS_MOUSE_MOVE_PACKET)payload);
break;
case PACKET_TYPE_MOUSE_BUTTON:
passthrough(input, (PNV_MOUSE_BUTTON_PACKET)payload);
break;
case PACKET_TYPE_SCROLL_OR_KEYBOARD: {
char *tmp_input = (char *)payload + 4;
if(tmp_input[0] == 0x0A) {
passthrough((PNV_SCROLL_PACKET)payload);
}
case PACKET_TYPE_MULTI_CONTROLLER:
passthrough(input, (PNV_MULTI_CONTROLLER_PACKET)payload);
break;
}
}
void reset_helper(std::shared_ptr<input_t> input) {
for(auto &[key_press, key_down] : input->key_press) {
if(key_down) {
key_down = false;
platf::keyboard(input->input, key_press & 0x00FF, true);
else {
passthrough(input, (PNV_KEYBOARD_PACKET)payload);
}
break;
}
auto &mouse_press = input->mouse_press;
for(int x = 0; x < mouse_press.size(); ++x) {
if(mouse_press[x]) {
mouse_press[x] = false;
platf::button_mouse(input->input, x + 1, true);
}
case PACKET_TYPE_MULTI_CONTROLLER:
passthrough(input, (PNV_MULTI_CONTROLLER_PACKET)payload);
break;
}
NV_MULTI_CONTROLLER_PACKET fake_packet;
fake_packet.buttonFlags = 0;
fake_packet.leftStickX = 0;
fake_packet.leftStickY = 0;
fake_packet.rightStickX = 0;
fake_packet.rightStickY = 0;
fake_packet.leftTrigger = 0;
fake_packet.rightTrigger = 0;
passthrough(input, &fake_packet);
}
void passthrough(std::shared_ptr<input_t> &input, std::vector<std::uint8_t> &&input_data) {
@@ -271,9 +546,39 @@ void passthrough(std::shared_ptr<input_t> &input, std::vector<std::uint8_t> &&in
}
void reset(std::shared_ptr<input_t> &input) {
task_pool.push(reset_helper, input);
task_pool.cancel(task_id);
task_pool.cancel(input->mouse_left_button_timeout);
// Ensure input is synchronous, by using the task_pool
task_pool.push([]() {
for(int x = 0; x < mouse_press.size(); ++x) {
if(mouse_press[x]) {
platf::button_mouse(platf_input, x, true);
mouse_press[x] = false;
}
}
for(auto &kp : key_press) {
platf::keyboard(platf_input, kp.first & 0x00FF, true);
key_press[kp.first] = false;
}
});
}
input_t::input_t() : mouse_press {}, input { platf::input() }, gamepads(platf::MAX_GAMEPADS) {}
gamepad_t::gamepad_t() : gamepad_state {}, back_timeout_id {}, back_button_state { button_state_e::NONE } {}
void init() {
platf_input = platf::input();
}
std::shared_ptr<input_t> alloc(safe::mail_t mail) {
auto input = std::make_shared<input_t>(mail->event<input::touch_port_t>(mail::touch_port));
// Workaround to ensure new frames will be captured when a client connects
task_pool.pushDelayed([]() {
platf::move_mouse(platf_input, 1, 1);
platf::move_mouse(platf_input, -1, -1);
},
100ms);
return input;
}
} // namespace input

View File

@@ -6,43 +6,27 @@
#define SUNSHINE_INPUT_H
#include "platform/common.h"
#include "thread_pool.h"
#include "thread_safe.h"
namespace input {
enum class button_state_e {
NONE,
DOWN,
UP
};
struct gamepad_t {
gamepad_t();
platf::gamepad_state_t gamepad_state;
util::ThreadPool::task_id_t back_timeout_id;
// When emulating the HOME button, we may need to artificially release the back button.
// Afterwards, the gamepad state on sunshine won't match the state on Moonlight.
// To prevent Sunshine from sending erronious input data to the active application,
// Sunshine forces the button to be in a specific state until the gamepad state matches that of
// Moonlight once more.
button_state_e back_button_state;
};
struct input_t {
input_t();
std::unordered_map<short, bool> key_press;
std::array<std::uint8_t, 5> mouse_press;
platf::input_t input;
std::vector<gamepad_t> gamepads;
};
struct input_t;
void print(void *input);
void passthrough(std::shared_ptr<input_t> &input, std::vector<std::uint8_t> &&input_data);
void reset(std::shared_ptr<input_t> &input);
}
void passthrough(std::shared_ptr<input_t> &input, std::vector<std::uint8_t> &&input_data);
void init();
std::shared_ptr<input_t> alloc(safe::mail_t mail);
struct touch_port_t : public platf::touch_port_t {
int env_width, env_height;
// inverse of scalar used for aspect ratio
float scalar_inv;
};
} // namespace input
#endif //SUNSHINE_INPUT_H

View File

@@ -4,26 +4,36 @@
#include "process.h"
#include <thread>
#include <filesystem>
#include <iostream>
#include <csignal>
#include <filesystem>
#include <fstream>
#include <iostream>
#include <thread>
#include <boost/log/attributes/clock.hpp>
#include <boost/log/common.hpp>
#include <boost/log/sinks.hpp>
#include <boost/log/expressions.hpp>
#include <boost/log/sinks.hpp>
#include <boost/log/sources/severity_logger.hpp>
#include "nvhttp.h"
#include "stream.h"
#include "config.h"
#include "confighttp.h"
#include "httpcommon.h"
#include "main.h"
#include "nvhttp.h"
#include "rtsp.h"
#include "thread_pool.h"
#include "upnp.h"
#include "video.h"
#include "platform/common.h"
extern "C" {
#include <libavutil/log.h>
#include <rs.h>
}
safe::mail_t mail::man;
using namespace std::literals;
namespace bl = boost::log;
@@ -46,6 +56,30 @@ struct NoDelete {
BOOST_LOG_ATTRIBUTE_KEYWORD(severity, "Severity", int)
void print_help(const char *name) {
std::cout
<< "Usage: "sv << name << " [options] [/path/to/configuration_file] [--cmd]"sv << std::endl
<< " Any configurable option can be overwritten with: \"name=value\""sv << std::endl
<< std::endl
<< " --help | print help"sv << std::endl
<< " --creds username password | set user credentials for the Web manager" << std::endl
<< std::endl
<< " flags"sv << std::endl
<< " -0 | Read PIN from stdin"sv << std::endl
<< " -1 | Do not load previously saved state and do retain any state after shutdown"sv << std::endl
<< " | Effectively starting as if for the first time without overwriting any pairings with your devices"sv << std::endl
<< " -2 | Force replacement of headers in video stream" << std::endl
<< " -p | Enable/Disable UPnP" << std::endl
<< std::endl;
}
namespace help {
int entry(const char *name, int argc, char *argv[]) {
print_help(name);
return 0;
}
} // namespace help
void log_flush() {
sink->flush();
}
@@ -62,17 +96,36 @@ void on_signal(int sig, FN &&fn) {
std::signal(sig, on_signal_forwarder);
}
int main(int argc, char *argv[]) {
const char *config_file = SUNSHINE_ASSETS_DIR "/sunshine.conf";
if(argc > 1) {
config_file = argv[1];
namespace gen_creds {
int entry(const char *name, int argc, char *argv[]) {
if(argc < 2 || argv[0] == "help"sv || argv[1] == "help"sv) {
print_help(name);
return 0;
}
if(!std::filesystem::exists(config_file)) {
std::cout << "Warning: Couldn't find configuration file ["sv << config_file << ']' << std::endl;
http::save_user_creds(config::sunshine.credentials_file, argv[0], argv[1]);
return 0;
}
} // namespace gen_creds
std::map<std::string_view, std::function<int(const char *name, int argc, char **argv)>> cmd_to_func {
{ "creds"sv, gen_creds::entry },
{ "help"sv, help::entry }
};
int main(int argc, char *argv[]) {
mail::man = std::make_shared<safe::mail_raw_t>();
if(config::parse(argc, argv)) {
return 0;
}
if(config::sunshine.min_log_level >= 1) {
av_log_set_level(AV_LOG_QUIET);
}
else {
config::parse_file(config_file);
av_log_set_level(AV_LOG_DEBUG);
}
sink = boost::make_shared<text_sink>();
@@ -81,59 +134,164 @@ int main(int argc, char *argv[]) {
sink->locked_backend()->add_stream(stream);
sink->set_filter(severity >= config::sunshine.min_log_level);
sink->set_formatter([severity="Severity"s](const bl::record_view &view, bl::formatting_ostream &os) {
sink->set_formatter([message = "Message"s, severity = "Severity"s](const bl::record_view &view, bl::formatting_ostream &os) {
constexpr int DATE_BUFFER_SIZE = 21 + 2 + 1; // Full string plus ": \0"
auto log_level = view.attribute_values()[severity].extract<int>().get();
std::string_view log_type;
switch(log_level) {
case 0:
log_type = "Verbose: "sv;
break;
case 1:
log_type = "Debug: "sv;
break;
case 2:
log_type = "Info: "sv;
break;
case 3:
log_type = "Warning: "sv;
break;
case 4:
log_type = "Error: "sv;
break;
case 5:
log_type = "Fatal: "sv;
break;
case 0:
log_type = "Verbose: "sv;
break;
case 1:
log_type = "Debug: "sv;
break;
case 2:
log_type = "Info: "sv;
break;
case 3:
log_type = "Warning: "sv;
break;
case 4:
log_type = "Error: "sv;
break;
case 5:
log_type = "Fatal: "sv;
break;
};
os << log_type << view.attribute_values()["Message"].extract<std::string>();
char _date[DATE_BUFFER_SIZE];
std::time_t t = std::time(nullptr);
strftime(_date, DATE_BUFFER_SIZE, "[%Y:%m:%d:%H:%M:%S]: ", std::localtime(&t));
os << _date << log_type << view.attribute_values()[message].extract<std::string>();
});
bl::core::get()->add_sink(sink);
auto fg = util::fail_guard(log_flush);
// Create signal handler after logging has been initialized
auto shutdown_event = std::make_shared<safe::event_t<bool>>();
on_signal(SIGINT, [shutdown_event]() {
BOOST_LOG(info) << "Interrupt handler called"sv;
shutdown_event->raise(true);
});
if(!config::sunshine.cmd.name.empty()) {
auto fn = cmd_to_func.find(config::sunshine.cmd.name);
if(fn == std::end(cmd_to_func)) {
BOOST_LOG(fatal) << "Unknown command: "sv << config::sunshine.cmd.name;
auto proc_opt = proc::parse(config::stream.file_apps);
if(!proc_opt) {
return 7;
BOOST_LOG(info) << "Possible commands:"sv;
for(auto &[key, _] : cmd_to_func) {
BOOST_LOG(info) << '\t' << key;
}
return 7;
}
return fn->second(argv[0], config::sunshine.cmd.argc, config::sunshine.cmd.argv);
}
proc::proc = std::move(*proc_opt);
reed_solomon_init();
task_pool.start(1);
std::thread httpThread { nvhttp::start, shutdown_event };
stream::rtpThread(shutdown_event);
// Create signal handler after logging has been initialized
auto shutdown_event = mail::man->event<bool>(mail::shutdown);
on_signal(SIGINT, [shutdown_event]() {
BOOST_LOG(info) << "Interrupt handler called"sv;
task_pool.pushDelayed([]() {
BOOST_LOG(fatal) << "10 seconds passed, yet Sunshine's still running: Forcing shutdown"sv;
log_flush();
std::abort();
},
10s);
shutdown_event->raise(true);
});
on_signal(SIGTERM, [shutdown_event]() {
BOOST_LOG(info) << "Terminate handler called"sv;
task_pool.pushDelayed([]() {
BOOST_LOG(fatal) << "10 seconds passed, yet Sunshine's still running: Forcing shutdown"sv;
log_flush();
std::abort();
},
10s);
shutdown_event->raise(true);
});
proc::refresh(config::stream.file_apps);
auto deinit_guard = platf::init();
if(!deinit_guard) {
return 4;
}
reed_solomon_init();
input::init();
if(video::init()) {
return 2;
}
if(http::init()) {
return 3;
}
std::unique_ptr<platf::deinit_t> mDNS;
auto sync_mDNS = std::async(std::launch::async, [&mDNS]() {
mDNS = platf::publish::start();
});
std::unique_ptr<platf::deinit_t> upnp_unmap;
auto sync_upnp = std::async(std::launch::async, [&upnp_unmap]() {
upnp_unmap = upnp::start();
});
//FIXME: Temporary workaround: Simple-Web_server needs to be updated or replaced
if(shutdown_event->peek()) {
return 0;
}
std::thread httpThread { nvhttp::start };
std::thread configThread { confighttp::start };
stream::rtpThread();
httpThread.join();
configThread.join();
task_pool.stop();
task_pool.join();
return 0;
}
std::string read_file(const char *path) {
if(!std::filesystem::exists(path)) {
return {};
}
std::ifstream in(path);
std::string input;
std::string base64_cert;
while(!in.eof()) {
std::getline(in, input);
base64_cert += input + '\n';
}
return base64_cert;
}
int write_file(const char *path, const std::string_view &contents) {
std::ofstream out(path);
if(!out.is_open()) {
return -1;
}
out << contents;
return 0;
}
std::uint16_t map_port(int port) {
return (std::uint16_t)((int)config::sunshine.port + port);
}

View File

@@ -5,8 +5,12 @@
#ifndef SUNSHINE_MAIN_H
#define SUNSHINE_MAIN_H
#include <boost/log/common.hpp>
#include <string_view>
#include "thread_pool.h"
#include "thread_safe.h"
#include <boost/log/common.hpp>
extern util::ThreadPool task_pool;
extern bool display_cursor;
@@ -19,4 +23,32 @@ extern boost::log::sources::severity_logger<int> error;
extern boost::log::sources::severity_logger<int> fatal;
void log_flush();
void print_help(const char *name);
std::string read_file(const char *path);
int write_file(const char *path, const std::string_view &contents);
std::uint16_t map_port(int port);
namespace mail {
#define MAIL(x) \
constexpr auto x = std::string_view { #x }
extern safe::mail_t man;
// Global mail
MAIL(shutdown);
MAIL(broadcast_shutdown);
MAIL(video_packets);
MAIL(audio_packets);
// Local mail
MAIL(touch_port);
MAIL(idr);
#undef MAIL
} // namespace mail
#endif //SUNSHINE_MAIN_H

View File

@@ -11,23 +11,24 @@ template<class T>
class MoveByCopy {
public:
typedef T move_type;
private:
move_type _to_move;
public:
explicit MoveByCopy(move_type &&to_move) : _to_move(std::move(to_move)) { }
public:
explicit MoveByCopy(move_type &&to_move) : _to_move(std::move(to_move)) {}
MoveByCopy(MoveByCopy &&other) = default;
MoveByCopy(const MoveByCopy &other) {
*this = other;
}
MoveByCopy& operator=(MoveByCopy &&other) = default;
MoveByCopy& operator=(const MoveByCopy &other) {
this->_to_move = std::move(const_cast<MoveByCopy&>(other)._to_move);
MoveByCopy &operator=(MoveByCopy &&other) = default;
MoveByCopy &operator=(const MoveByCopy &other) {
this->_to_move = std::move(const_cast<MoveByCopy &>(other)._to_move);
return *this;
}
@@ -44,7 +45,7 @@ MoveByCopy<T> cmove(T &movable) {
// Do NOT use this unless you are absolutely certain the object to be moved is no longer used by the caller
template<class T>
MoveByCopy<T> const_cmove(const T &movable) {
return MoveByCopy<T>(std::move(const_cast<T&>(movable)));
}
return MoveByCopy<T>(std::move(const_cast<T &>(movable)));
}
} // namespace util
#endif

View File

@@ -2,12 +2,12 @@
// Created by loki on 12/27/19.
//
#include <algorithm>
#include "network.h"
#include "utility.h"
#include <algorithm>
namespace net {
using namespace std::literals;
namespace net {
// In the format "xxx.xxx.xxx.xxx/x"
std::pair<std::uint32_t, std::uint32_t> ip_block(const std::string_view &ip);
@@ -16,22 +16,22 @@ std::vector<std::pair<std::uint32_t, std::uint32_t>> pc_ips {
};
std::vector<std::tuple<std::uint32_t, std::uint32_t>> lan_ips {
ip_block("192.168.0.0/16"sv),
ip_block("172.16.0.0/12"),
ip_block("172.16.0.0/12"sv),
ip_block("10.0.0.0/8"sv)
};
std::uint32_t ip(const std::string_view &ip_str) {
auto begin = std::begin(ip_str);
auto end = std::end(ip_str);
auto begin = std::begin(ip_str);
auto end = std::end(ip_str);
auto temp_end = std::find(begin, end, '.');
std::uint32_t ip = 0;
auto shift = 24;
auto shift = 24;
while(temp_end != end) {
ip += (util::from_chars(begin, temp_end) << shift);
shift -= 8;
begin = temp_end + 1;
begin = temp_end + 1;
temp_end = std::find(begin, end, '.');
}
@@ -43,7 +43,7 @@ std::uint32_t ip(const std::string_view &ip_str) {
// In the format "xxx.xxx.xxx.xxx/x"
std::pair<std::uint32_t, std::uint32_t> ip_block(const std::string_view &ip_str) {
auto begin = std::begin(ip_str);
auto end = std::find(begin, std::end(ip_str), '/');
auto end = std::find(begin, std::end(ip_str), '/');
auto addr = ip({ begin, (std::size_t)(end - begin) });
@@ -82,15 +82,34 @@ net_e from_address(const std::string_view &view) {
std::string_view to_enum_string(net_e net) {
switch(net) {
case PC:
return "pc"sv;
case LAN:
return "lan"sv;
case WAN:
return "wan"sv;
case PC:
return "pc"sv;
case LAN:
return "lan"sv;
case WAN:
return "wan"sv;
}
// avoid warning
return "wan"sv;
}
}
host_t host_create(ENetAddress &addr, std::size_t peers, std::uint16_t port) {
enet_address_set_host(&addr, "0.0.0.0");
enet_address_set_port(&addr, port);
return host_t { enet_host_create(AF_INET, &addr, peers, 1, 0, 0) };
}
void free_host(ENetHost *host) {
std::for_each(host->peers, host->peers + host->peerCount, [](ENetPeer &peer_ref) {
ENetPeer *peer = &peer_ref;
if(peer) {
enet_peer_disconnect_now(peer, 0);
}
});
enet_host_destroy(host);
}
} // namespace net

View File

@@ -6,7 +6,18 @@
#define SUNSHINE_NETWORK_H
#include <tuple>
#include <enet/enet.h>
#include "utility.h"
namespace net {
void free_host(ENetHost *host);
using host_t = util::safe_ptr<ENetHost, free_host>;
using peer_t = ENetPeer *;
using packet_t = util::safe_ptr<ENetPacket, enet_packet_destroy>;
enum net_e : int {
PC,
LAN,
@@ -17,6 +28,8 @@ net_e from_enum_string(const std::string_view &view);
std::string_view to_enum_string(net_e net);
net_e from_address(const std::string_view &view);
}
host_t host_create(ENetAddress &addr, std::size_t peers, std::uint16_t port);
} // namespace net
#endif //SUNSHINE_NETWORK_H

View File

@@ -2,13 +2,15 @@
// Created by loki on 6/3/19.
//
#define BOOST_BIND_GLOBAL_PLACEHOLDERS
#include "process.h"
#include <filesystem>
#include <boost/property_tree/json_parser.hpp>
#include <boost/property_tree/ptree.hpp>
#include <boost/property_tree/xml_parser.hpp>
#include <boost/property_tree/json_parser.hpp>
#include <boost/asio/ssl/context.hpp>
@@ -16,30 +18,27 @@
#include <Simple-Web-Server/server_https.hpp>
#include <boost/asio/ssl/context_base.hpp>
#include "config.h"
#include "utility.h"
#include "stream.h"
#include "crypto.h"
#include "httpcommon.h"
#include "main.h"
#include "network.h"
#include "nvhttp.h"
#include "platform/common.h"
#include "network.h"
#include "rtsp.h"
#include "utility.h"
#include "uuid.h"
#include "main.h"
namespace nvhttp {
using namespace std::literals;
constexpr auto PORT_HTTP = 47989;
constexpr auto PORT_HTTPS = 47984;
namespace nvhttp {
constexpr auto VERSION = "7.1.400.0";
constexpr auto GFE_VERSION = "2.0.0.1";
constexpr auto GFE_VERSION = "3.12.0.1";
namespace fs = std::filesystem;
namespace pt = boost::property_tree;
std::string read_file(const char *path);
int write_file(const char *path, const std::string_view &contents);
using https_server_t = SimpleWeb::Server<SimpleWeb::HTTPS>;
using http_server_t = SimpleWeb::Server<SimpleWeb::HTTP>;
@@ -68,8 +67,8 @@ struct pair_session_t {
struct {
util::Either<
std::shared_ptr<typename SimpleWeb::ServerBase<SimpleWeb::HTTP>::Response>,
std::shared_ptr<typename SimpleWeb::ServerBase<SimpleWeb::HTTPS>::Response>
> response;
std::shared_ptr<typename SimpleWeb::ServerBase<SimpleWeb::HTTPS>::Response>>
response;
std::string salt;
} async_insert_pin;
};
@@ -77,14 +76,12 @@ struct pair_session_t {
// uniqueID, session
std::unordered_map<std::string, pair_session_t> map_id_sess;
std::unordered_map<std::string, client_t> map_id_client;
std::string unique_id;
net::net_e origin_pin_allowed;
using args_t = SimpleWeb::CaseInsensitiveMultimap;
using args_t = SimpleWeb::CaseInsensitiveMultimap;
using resp_https_t = std::shared_ptr<typename SimpleWeb::ServerBase<SimpleWeb::HTTPS>::Response>;
using req_https_t = std::shared_ptr<typename SimpleWeb::ServerBase<SimpleWeb::HTTPS>::Request>;
using resp_http_t = std::shared_ptr<typename SimpleWeb::ServerBase<SimpleWeb::HTTP>::Response>;
using req_http_t = std::shared_ptr<typename SimpleWeb::ServerBase<SimpleWeb::HTTP>::Request>;
using req_https_t = std::shared_ptr<typename SimpleWeb::ServerBase<SimpleWeb::HTTPS>::Request>;
using resp_http_t = std::shared_ptr<typename SimpleWeb::ServerBase<SimpleWeb::HTTP>::Response>;
using req_http_t = std::shared_ptr<typename SimpleWeb::ServerBase<SimpleWeb::HTTP>::Request>;
enum class op_e {
ADD,
@@ -94,9 +91,21 @@ enum class op_e {
void save_state() {
pt::ptree root;
root.put("root.uniqueid", unique_id);
if(fs::exists(config::nvhttp.file_state)) {
try {
pt::read_json(config::nvhttp.file_state, root);
}
catch(std::exception &e) {
BOOST_LOG(error) << "Couldn't read "sv << config::nvhttp.file_state << ": "sv << e.what();
return;
}
}
root.erase("root"s);
root.put("root.uniqueid", http::unique_id);
auto &nodes = root.add_child("root.devices", pt::ptree {});
for(auto &[_,client] : map_id_client) {
for(auto &[_, client] : map_id_client) {
pt::ptree node;
node.put("uniqueid"s, client.uniqueID);
@@ -112,31 +121,44 @@ void save_state() {
nodes.push_back(std::make_pair(""s, node));
}
pt::write_json(config::nvhttp.file_state, root);
try {
pt::write_json(config::nvhttp.file_state, root);
}
catch(std::exception &e) {
BOOST_LOG(error) << "Couldn't write "sv << config::nvhttp.file_state << ": "sv << e.what();
return;
}
}
void load_state() {
auto file_state = fs::current_path() / config::nvhttp.file_state;
if(!fs::exists(file_state)) {
unique_id = util::uuid_t::generate().string();
if(!fs::exists(config::nvhttp.file_state)) {
BOOST_LOG(info) << "DOENST EXIST"sv;
http::unique_id = util::uuid_t::generate().string();
return;
}
pt::ptree root;
try {
pt::read_json(config::nvhttp.file_state, root);
} catch (std::exception &e) {
BOOST_LOG(warning) << e.what();
}
catch(std::exception &e) {
BOOST_LOG(error) << "Couldn't read "sv << config::nvhttp.file_state << ": "sv << e.what();
return;
}
unique_id = root.get<std::string>("root.uniqueid");
auto unique_id_p = root.get_optional<std::string>("root.uniqueid");
if(!unique_id_p) {
// This file doesn't contain moonlight credentials
http::unique_id = util::uuid_t::generate().string();
return;
}
http::unique_id = std::move(*unique_id_p);
auto device_nodes = root.get_child("root.devices");
for(auto &[_,device_node] : device_nodes) {
auto uniqID = device_node.get<std::string>("uniqueid");
for(auto &[_, device_node] : device_nodes) {
auto uniqID = device_node.get<std::string>("uniqueid");
auto &client = map_id_client.emplace(uniqID, client_t {}).first->second;
client.uniqueID = uniqID;
@@ -149,25 +171,47 @@ void load_state() {
void update_id_client(const std::string &uniqueID, std::string &&cert, op_e op) {
switch(op) {
case op_e::ADD:
{
auto &client = map_id_client[uniqueID];
client.certs.emplace_back(std::move(cert));
client.uniqueID = uniqueID;
}
break;
case op_e::REMOVE:
map_id_client.erase(uniqueID);
break;
case op_e::ADD: {
auto &client = map_id_client[uniqueID];
client.certs.emplace_back(std::move(cert));
client.uniqueID = uniqueID;
} break;
case op_e::REMOVE:
map_id_client.erase(uniqueID);
break;
}
save_state();
if(!config::sunshine.flags[config::flag::FRESH_STATE]) {
save_state();
}
}
stream::launch_session_t make_launch_session(bool host_audio, const args_t &args) {
stream::launch_session_t launch_session;
launch_session.host_audio = host_audio;
launch_session.gcm_key = *util::from_hex<crypto::aes_t>(args.at("rikey"s), true);
uint32_t prepend_iv = util::endian::big<uint32_t>(util::from_view(args.at("rikeyid"s)));
auto prepend_iv_p = (uint8_t *)&prepend_iv;
auto next = std::copy(prepend_iv_p, prepend_iv_p + sizeof(prepend_iv), std::begin(launch_session.iv));
std::fill(next, std::end(launch_session.iv), 0);
return launch_session;
}
void getservercert(pair_session_t &sess, pt::ptree &tree, const std::string &pin) {
auto salt = util::from_hex<std::array<uint8_t, 16>>(sess.async_insert_pin.salt, true);
if(sess.async_insert_pin.salt.size() < 32) {
tree.put("root.paired", 0);
tree.put("root.<xmlattr>.status_code", 400);
return;
}
auto key = crypto::gen_aes_key(*salt, pin);
std::string_view salt_view { sess.async_insert_pin.salt.data(), 32 };
auto salt = util::from_hex<std::array<uint8_t, 16>>(salt_view, true);
auto key = crypto::gen_aes_key(*salt, pin);
sess.cipher_key = std::make_unique<crypto::aes_t>(key);
tree.put("root.paired", 1);
@@ -186,7 +230,7 @@ void serverchallengeresp(pair_session_t &sess, pt::ptree &tree, const args_t &ar
sess.clienthash = std::move(decrypted);
auto serversecret = sess.serversecret;
auto sign = crypto::sign256(crypto::pkey(conf_intern.pkey), serversecret);
auto sign = crypto::sign256(crypto::pkey(conf_intern.pkey), serversecret);
serversecret.insert(std::end(serversecret), std::begin(sign), std::end(sign));
@@ -204,14 +248,14 @@ void clientchallenge(pair_session_t &sess, pt::ptree &tree, const args_t &args)
std::vector<uint8_t> decrypted;
cipher.decrypt(challenge, decrypted);
auto x509 = crypto::x509(conf_intern.servercert);
auto sign = crypto::signature(x509);
auto x509 = crypto::x509(conf_intern.servercert);
auto sign = crypto::signature(x509);
auto serversecret = crypto::rand(16);
decrypted.insert(std::end(decrypted), std::begin(sign), std::end(sign));
decrypted.insert(std::end(decrypted), std::begin(serversecret), std::end(serversecret));
auto hash = crypto::hash({ (char*)decrypted.data(), decrypted.size() });
auto hash = crypto::hash({ (char *)decrypted.data(), decrypted.size() });
auto serverchallenge = crypto::rand(16);
std::string plaintext;
@@ -241,7 +285,7 @@ void clientpairingsecret(std::shared_ptr<safe::queue_t<crypto::x509_t>> &add_cer
assert((secret.size() + sign.size()) == pairingsecret.size());
auto x509 = crypto::x509(client.cert);
auto x509 = crypto::x509(client.cert);
auto x509_sign = crypto::signature(x509);
std::string data;
@@ -295,8 +339,8 @@ template<class T>
void print_req(std::shared_ptr<typename SimpleWeb::ServerBase<T>::Request> request) {
BOOST_LOG(debug) << "TUNNEL :: "sv << tunnel<T>::to_string;
BOOST_LOG(debug) << "METHOD :: "sv << request->method;
BOOST_LOG(debug) << "DESTINATION :: "sv << request->path;
BOOST_LOG(debug) << "METHOD :: "sv << request->method;
BOOST_LOG(debug) << "DESTINATION :: "sv << request->path;
for(auto &[name, val] : request->header) {
BOOST_LOG(debug) << name << " -- " << val;
@@ -323,33 +367,56 @@ void not_found(std::shared_ptr<typename SimpleWeb::ServerBase<T>::Response> resp
pt::write_xml(data, tree);
response->write(data.str());
*response << "HTTP/1.1 404 NOT FOUND\r\n" << data.str();
*response << "HTTP/1.1 404 NOT FOUND\r\n"
<< data.str();
}
template<class T>
void pair(std::shared_ptr<safe::queue_t<crypto::x509_t>> &add_cert, std::shared_ptr<typename SimpleWeb::ServerBase<T>::Response> response, std::shared_ptr<typename SimpleWeb::ServerBase<T>::Request> request) {
print_req<T>(request);
pt::ptree tree;
auto args = request->parse_query_string();
if(args.find("uniqueid"s) == std::end(args)) {
tree.put("root.<xmlattr>.status_code", 400);
return;
}
auto uniqID { std::move(args.at("uniqueid"s)) };
auto sess_it = map_id_sess.find(uniqID);
pt::ptree tree;
args_t::const_iterator it;
if(it = args.find("phrase"); it != std::end(args)) {
if(it->second == "getservercert"sv) {
pair_session_t sess;
sess.client.uniqueID = std::move(uniqID);
sess.client.cert = util::from_hex_vec(args.at("clientcert"s), true);
sess.client.cert = util::from_hex_vec(args.at("clientcert"s), true);
BOOST_LOG(debug) << sess.client.cert;
auto ptr = map_id_sess.emplace(sess.client.uniqueID, std::move(sess)).first;
ptr->second.async_insert_pin.salt = std::move(args.at("salt"s));
ptr->second.async_insert_pin.response = std::move(response);
return;
if(config::sunshine.flags[config::flag::CONST_PIN]) {
std::string pin("6174");
getservercert(ptr->second, tree, pin);
}
else if(config::sunshine.flags[config::flag::PIN_STDIN]) {
std::string pin;
std::cout << "Please insert pin: "sv;
std::getline(std::cin, pin);
getservercert(ptr->second, tree, pin);
}
else {
ptr->second.async_insert_pin.response = std::move(response);
return;
}
}
else if(it->second == "pairchallenge"sv) {
tree.put("root.paired", 1);
@@ -375,30 +442,14 @@ void pair(std::shared_ptr<safe::queue_t<crypto::x509_t>> &add_cert, std::shared_
response->write(data.str());
}
template<class T>
void pin(std::shared_ptr<typename SimpleWeb::ServerBase<T>::Response> response, std::shared_ptr<typename SimpleWeb::ServerBase<T>::Request> request) {
print_req<T>(request);
auto address = request->remote_endpoint_address();
auto ip_type = net::from_address(address);
if(ip_type > origin_pin_allowed) {
BOOST_LOG(info) << '[' << address << "] -- denied"sv;
response->write(SimpleWeb::StatusCode::client_error_forbidden);
return;
}
bool pin(std::string pin) {
pt::ptree tree;
if(map_id_sess.empty()) {
response->write(SimpleWeb::StatusCode::client_error_im_a_teapot);
return;
return false;
}
auto &sess = std::begin(map_id_sess)->second;
getservercert(sess, tree, request->path_match[1]);
getservercert(sess, tree, pin);
// response to the request for pin
std::ostringstream data;
@@ -408,19 +459,40 @@ void pin(std::shared_ptr<typename SimpleWeb::ServerBase<T>::Response> response,
if(async_response.has_left() && async_response.left()) {
async_response.left()->write(data.str());
}
else if(async_response.has_right() && async_response.right()){
else if(async_response.has_right() && async_response.right()) {
async_response.right()->write(data.str());
}
else {
response->write(SimpleWeb::StatusCode::client_error_im_a_teapot);
return;
return false;
}
// reset async_response
async_response = std::decay_t<decltype(async_response.left())>();
// response to the current request
response->write(SimpleWeb::StatusCode::success_ok);
return true;
}
template<class T>
void pin(std::shared_ptr<typename SimpleWeb::ServerBase<T>::Response> response, std::shared_ptr<typename SimpleWeb::ServerBase<T>::Request> request) {
print_req<T>(request);
auto address = request->remote_endpoint_address();
auto ip_type = net::from_address(address);
if(ip_type > http::origin_pin_allowed) {
BOOST_LOG(info) << "/pin: ["sv << address << "] -- denied"sv;
response->write(SimpleWeb::StatusCode::client_error_forbidden);
return;
}
bool pinResponse = pin(request->path_match[1]);
if(pinResponse) {
response->write(SimpleWeb::StatusCode::success_ok);
}
else {
response->write(SimpleWeb::StatusCode::client_error_im_a_teapot);
}
}
template<class T>
@@ -428,13 +500,13 @@ void serverinfo(std::shared_ptr<typename SimpleWeb::ServerBase<T>::Response> res
print_req<T>(request);
int pair_status = 0;
if constexpr (std::is_same_v<SimpleWeb::HTTPS, T>) {
auto args = request->parse_query_string();
if constexpr(std::is_same_v<SimpleWeb::HTTPS, T>) {
auto args = request->parse_query_string();
auto clientID = args.find("uniqueid"s);
if(clientID != std::end(args)) {
if (auto it = map_id_client.find(clientID->second); it != std::end(map_id_client)) {
if(auto it = map_id_client.find(clientID->second); it != std::end(map_id_client)) {
pair_status = 1;
}
}
@@ -447,15 +519,15 @@ void serverinfo(std::shared_ptr<typename SimpleWeb::ServerBase<T>::Response> res
tree.put("root.appversion", VERSION);
tree.put("root.GfeVersion", GFE_VERSION);
tree.put("root.uniqueid", unique_id);
tree.put("root.uniqueid", http::unique_id);
tree.put("root.mac", platf::get_mac_address(request->local_endpoint_address()));
tree.put("root.MaxLumaPixelsHEVC", config::video.hevc_mode > 0 ? "1869449984" : "0");
tree.put("root.MaxLumaPixelsHEVC", config::video.hevc_mode > 1 ? "1869449984" : "0");
tree.put("root.LocalIP", request->local_endpoint_address());
if(config::video.hevc_mode == 2) {
if(config::video.hevc_mode == 3) {
tree.put("root.ServerCodecModeSupport", "3843");
}
else if(config::video.hevc_mode == 1) {
else if(config::video.hevc_mode == 2) {
tree.put("root.ServerCodecModeSupport", "259");
}
else {
@@ -466,10 +538,35 @@ void serverinfo(std::shared_ptr<typename SimpleWeb::ServerBase<T>::Response> res
tree.put("root.ExternalIP", config::nvhttp.external_ip);
}
pt::ptree display_nodes;
for(auto &resolution : config::nvhttp.resolutions) {
auto pred = [](auto ch) { return ch == ' ' || ch == '\t' || ch == 'x'; };
auto middle = std::find_if(std::begin(resolution), std::end(resolution), pred);
if(middle == std::end(resolution)) {
BOOST_LOG(warning) << resolution << " is not in the proper format for a resolution: WIDTHxHEIGHT"sv;
continue;
}
auto width = util::from_chars(&*std::begin(resolution), &*middle);
auto height = util::from_chars(&*(middle + 1), &*std::end(resolution));
for(auto fps : config::nvhttp.fps) {
pt::ptree display_node;
display_node.put("Width", width);
display_node.put("Height", height);
display_node.put("RefreshRate", fps);
display_nodes.add_child("DisplayMode", display_node);
}
}
if(!config::nvhttp.resolutions.empty()) {
tree.add_child("root.SupportedDisplayMode", display_nodes);
}
auto current_appid = proc::proc.running();
tree.put("root.PairStatus", pair_status);
tree.put("root.currentgame", current_appid >= 0 ? current_appid + 2 : 0);
tree.put("root.state", "_SERVER_BUSY");
tree.put("root.currentgame", current_appid >= 0 ? current_appid + 1 : 0);
tree.put("root.state", current_appid >= 0 ? "SUNSHINE_SERVER_BUSY" : "SUNSHINE_SERVER_FREE");
std::ostringstream data;
@@ -480,9 +577,6 @@ void serverinfo(std::shared_ptr<typename SimpleWeb::ServerBase<T>::Response> res
void applist(resp_https_t response, req_https_t request) {
print_req<SimpleWeb::HTTPS>(request);
auto args = request->parse_query_string();
auto clientID = args.at("uniqueid"s);
pt::ptree tree;
auto g = util::fail_guard([&]() {
@@ -492,6 +586,15 @@ void applist(resp_https_t response, req_https_t request) {
response->write(data.str());
});
auto args = request->parse_query_string();
if(args.find("uniqueid"s) == std::end(args)) {
tree.put("root.<xmlattr>.status_code", 400);
return;
}
auto clientID = args.at("uniqueid"s);
auto client = map_id_client.find(clientID);
if(client == std::end(map_id_client)) {
tree.put("root.<xmlattr>.status_code", 501);
@@ -501,28 +604,21 @@ void applist(resp_https_t response, req_https_t request) {
auto &apps = tree.add_child("root", pt::ptree {});
pt::ptree desktop;
apps.put("<xmlattr>.status_code", 200);
desktop.put("IsHdrSupported"s, config::video.hevc_mode == 2 ? 1 : 0);
desktop.put("AppTitle"s, "Desktop");
desktop.put("ID"s, 1);
int x = 2;
int x = 0;
for(auto &proc : proc::proc.get_apps()) {
pt::ptree app;
app.put("IsHdrSupported"s, config::video.hevc_mode == 2 ? 1 : 0);
app.put("IsHdrSupported"s, config::video.hevc_mode == 3 ? 1 : 0);
app.put("AppTitle"s, proc.name);
app.put("ID"s, x++);
app.put("ID"s, ++x);
apps.push_back(std::make_pair("App", std::move(app)));
}
apps.push_back(std::make_pair("App", desktop));
}
void launch(resp_https_t response, req_https_t request) {
void launch(bool &host_audio, resp_https_t response, req_https_t request) {
print_req<SimpleWeb::HTTPS>(request);
pt::ptree tree;
@@ -533,20 +629,37 @@ void launch(resp_https_t response, req_https_t request) {
response->write(data.str());
});
auto args = request->parse_query_string();
auto appid = util::from_view(args.at("appid")) -2;
stream::launch_session_t launch_session;
if(stream::session_state != stream::state_e::STOPPED) {
if(stream::session_count() == config::stream.channels) {
tree.put("root.resume", 0);
tree.put("root.<xmlattr>.status_code", 503);
tree.put("root.gamesession", 0);
return;
}
auto args = request->parse_query_string();
if(
args.find("rikey"s) == std::end(args) ||
args.find("rikeyid"s) == std::end(args) ||
args.find("localAudioPlayMode"s) == std::end(args) ||
args.find("appid"s) == std::end(args)) {
tree.put("root.resume", 0);
tree.put("root.<xmlattr>.status_code", 400);
return;
}
auto appid = util::from_view(args.at("appid")) - 1;
auto current_appid = proc::proc.running();
if(appid >= 0 && appid != current_appid) {
if(current_appid != -1) {
tree.put("root.resume", 0);
tree.put("root.<xmlattr>.status_code", 400);
return;
}
if(appid >= 0) {
auto err = proc::proc.execute(appid);
if(err) {
tree.put("root.<xmlattr>.status_code", err);
@@ -554,36 +667,16 @@ void launch(resp_https_t response, req_https_t request) {
return;
}
current_appid = appid;
}
// Needed to determine if session must be closed when no process is running in proc::proc
launch_session.has_process = current_appid >= 0;
auto clientID = args.at("uniqueid"s);
launch_session.gcm_key = *util::from_hex<crypto::aes_t>(args.at("rikey"s), true);
uint32_t prepend_iv = util::endian::big<uint32_t>(util::from_view(args.at("rikeyid"s)));
auto prepend_iv_p = (uint8_t*)&prepend_iv;
auto next = std::copy(prepend_iv_p, prepend_iv_p + sizeof(prepend_iv), std::begin(launch_session.iv));
std::fill(next, std::end(launch_session.iv), 0);
stream::launch_event.raise(launch_session);
/*
bool sops = args.at("sops"s) == "1";
std::optional<int> gcmap { std::nullopt };
if(auto it = args.find("gcmap"s); it != std::end(args)) {
gcmap = std::stoi(it->second);
}
*/
host_audio = util::from_view(args.at("localAudioPlayMode"));
stream::launch_session_raise(make_launch_session(host_audio, args));
tree.put("root.<xmlattr>.status_code", 200);
tree.put("root.gamesession", 1);
}
void resume(resp_https_t response, req_https_t request) {
void resume(bool &host_audio, resp_https_t response, req_https_t request) {
print_req<SimpleWeb::HTTPS>(request);
pt::ptree tree;
@@ -594,28 +687,35 @@ void resume(resp_https_t response, req_https_t request) {
response->write(data.str());
});
auto current_appid = proc::proc.running();
if(current_appid == -1 || stream::session_state != stream::state_e::STOPPED) {
// It is possible that due a race condition that this if-statement gives a false negative,
// that is automatically resolved in rtsp_server_t
if(stream::session_count() == config::stream.channels) {
tree.put("root.resume", 0);
tree.put("root.<xmlattr>.status_code", 503);
return;
}
stream::launch_session_t launch_session;
// Needed to determine if session must be closed when no process is running in proc::proc
launch_session.has_process = current_appid >= 0;
auto current_appid = proc::proc.running();
if(current_appid == -1) {
tree.put("root.resume", 0);
tree.put("root.<xmlattr>.status_code", 503);
return;
}
auto args = request->parse_query_string();
auto clientID = args.at("uniqueid"s);
launch_session.gcm_key = *util::from_hex<crypto::aes_t>(args.at("rikey"s), true);
uint32_t prepend_iv = util::endian::big<uint32_t>(util::from_view(args.at("rikeyid"s)));
auto prepend_iv_p = (uint8_t*)&prepend_iv;
if(
args.find("rikey"s) == std::end(args) ||
args.find("rikeyid"s) == std::end(args)) {
auto next = std::copy(prepend_iv_p, prepend_iv_p + sizeof(prepend_iv), std::begin(launch_session.iv));
std::fill(next, std::end(launch_session.iv), 0);
tree.put("root.resume", 0);
tree.put("root.<xmlattr>.status_code", 400);
stream::launch_event.raise(launch_session);
return;
}
stream::launch_session_raise(make_launch_session(host_audio, args));
tree.put("root.<xmlattr>.status_code", 200);
tree.put("root.resume", 1);
@@ -632,24 +732,21 @@ void cancel(resp_https_t response, req_https_t request) {
response->write(data.str());
});
if(proc::proc.running() == -1) {
tree.put("root.cancel", 1);
tree.put("root.<xmlattr>.status_code", 200);
return;
}
if(stream::session_state != stream::state_e::STOPPED) {
// It is possible that due a race condition that this if-statement gives a false positive,
// the client should try again
if(stream::session_count() != 0) {
tree.put("root.resume", 0);
tree.put("root.<xmlattr>.status_code", 503);
tree.put("root.cancel", 0);
return;
}
proc::proc.terminate();
tree.put("root.cancel", 1);
tree.put("root.<xmlattr>.status_code", 200);
if(proc::proc.running() != -1) {
proc::proc.terminate();
}
}
void appasset(resp_https_t response, req_https_t request) {
@@ -659,73 +756,19 @@ void appasset(resp_https_t response, req_https_t request) {
response->write(SimpleWeb::StatusCode::success_ok, in);
}
int create_creds(const std::string &pkey, const std::string &cert) {
fs::path pkey_path = pkey;
fs::path cert_path = cert;
void start() {
auto shutdown_event = mail::man->event<bool>(mail::shutdown);
auto creds = crypto::gen_creds("Sunshine Gamestream Host"sv, 2048);
auto port_http = map_port(PORT_HTTP);
auto port_https = map_port(PORT_HTTPS);
auto pkey_dir = pkey_path;
auto cert_dir = cert_path;
pkey_dir.remove_filename();
cert_dir.remove_filename();
bool clean_slate = config::sunshine.flags[config::flag::FRESH_STATE];
std::error_code err_code{};
fs::create_directories(pkey_dir, err_code);
if (err_code) {
BOOST_LOG(fatal) << "Couldn't create directory ["sv << pkey_dir << "] :"sv << err_code.message();
return -1;
if(!clean_slate) {
load_state();
}
fs::create_directories(cert_dir, err_code);
if (err_code) {
BOOST_LOG(fatal) << "Couldn't create directory ["sv << cert_dir << "] :"sv << err_code.message();
return -1;
}
if (write_file(pkey.c_str(), creds.pkey)) {
BOOST_LOG(fatal) << "Couldn't open ["sv << config::nvhttp.pkey << ']';
return -1;
}
if (write_file(cert.c_str(), creds.x509)) {
BOOST_LOG(fatal) << "Couldn't open ["sv << config::nvhttp.cert << ']';
return -1;
}
fs::permissions(pkey_path,
fs::perms::owner_read | fs::perms::owner_write,
fs::perm_options::replace, err_code);
if (err_code) {
BOOST_LOG(fatal) << "Couldn't change permissions of ["sv << config::nvhttp.pkey << "] :"sv << err_code.message();
return -1;
}
fs::permissions(cert_path,
fs::perms::owner_read | fs::perms::group_read | fs::perms::others_read | fs::perms::owner_write,
fs::perm_options::replace, err_code);
if (err_code) {
BOOST_LOG(fatal) << "Couldn't change permissions of ["sv << config::nvhttp.cert << "] :"sv << err_code.message();
return -1;
}
return 0;
}
void start(std::shared_ptr<safe::event_t<bool>> shutdown_event) {
if(!fs::exists(config::nvhttp.pkey) || !fs::exists(config::nvhttp.cert)) {
if(create_creds(config::nvhttp.pkey, config::nvhttp.cert)) {
shutdown_event->raise(true);
return;
}
}
origin_pin_allowed = net::from_enum_string(config::nvhttp.origin_pin_allowed);
load_state();
conf_intern.pkey = read_file(config::nvhttp.pkey.c_str());
conf_intern.pkey = read_file(config::nvhttp.pkey.c_str());
conf_intern.servercert = read_file(config::nvhttp.cert.c_str());
auto ctx = std::make_shared<boost::asio::ssl::context>(boost::asio::ssl::context::tls);
@@ -733,13 +776,13 @@ void start(std::shared_ptr<safe::event_t<bool>> shutdown_event) {
ctx->use_private_key_file(config::nvhttp.pkey, boost::asio::ssl::context::pem);
crypto::cert_chain_t cert_chain;
for(auto &[_,client] : map_id_client) {
for(auto &[_, client] : map_id_client) {
for(auto &cert : client.certs) {
cert_chain.add(crypto::x509(cert));
}
}
auto add_cert = std::make_shared<safe::queue_t<crypto::x509_t>>();
auto add_cert = std::make_shared<safe::queue_t<crypto::x509_t>>(30);
// Ugly hack for verifying certificates, see crypto::cert_chain_t::verify() for details
ctx->set_verify_callback([&cert_chain, add_cert](int verified, boost::asio::ssl::verify_context &ctx) {
@@ -777,35 +820,64 @@ void start(std::shared_ptr<safe::event_t<bool>> shutdown_event) {
return 1;
});
// /resume doesn't get the parameter "localAudioPlayMode"
// /launch will store it in host_audio
bool host_audio {};
https_server_t https_server { ctx, boost::asio::ssl::verify_peer | boost::asio::ssl::verify_fail_if_no_peer_cert | boost::asio::ssl::verify_client_once };
http_server_t http_server;
https_server.default_resource = not_found<SimpleWeb::HTTPS>;
https_server.resource["^/serverinfo$"]["GET"] = serverinfo<SimpleWeb::HTTPS>;
https_server.resource["^/pair$"]["GET"] = [&add_cert](auto resp, auto req) { pair<SimpleWeb::HTTPS>(add_cert, resp, req); };
https_server.resource["^/applist$"]["GET"] = applist;
https_server.resource["^/appasset$"]["GET"] = appasset;
https_server.resource["^/launch$"]["GET"] = launch;
https_server.default_resource = not_found<SimpleWeb::HTTPS>;
https_server.resource["^/serverinfo$"]["GET"] = serverinfo<SimpleWeb::HTTPS>;
https_server.resource["^/pair$"]["GET"] = [&add_cert](auto resp, auto req) { pair<SimpleWeb::HTTPS>(add_cert, resp, req); };
https_server.resource["^/applist$"]["GET"] = applist;
https_server.resource["^/appasset$"]["GET"] = appasset;
https_server.resource["^/launch$"]["GET"] = [&host_audio](auto resp, auto req) { launch(host_audio, resp, req); };
https_server.resource["^/pin/([0-9]+)$"]["GET"] = pin<SimpleWeb::HTTPS>;
https_server.resource["^/resume$"]["GET"] = resume;
https_server.resource["^/cancel$"]["GET"] = cancel;
https_server.resource["^/resume$"]["GET"] = [&host_audio](auto resp, auto req) { resume(host_audio, resp, req); };
https_server.resource["^/cancel$"]["GET"] = cancel;
https_server.config.reuse_address = true;
https_server.config.address = "0.0.0.0"s;
https_server.config.port = PORT_HTTPS;
https_server.config.address = "0.0.0.0"s;
https_server.config.port = port_https;
http_server.default_resource = not_found<SimpleWeb::HTTP>;
http_server.resource["^/serverinfo$"]["GET"] = serverinfo<SimpleWeb::HTTP>;
http_server.resource["^/pair$"]["GET"] = [&add_cert](auto resp, auto req) { pair<SimpleWeb::HTTP>(add_cert, resp, req); };
http_server.default_resource = not_found<SimpleWeb::HTTP>;
http_server.resource["^/serverinfo$"]["GET"] = serverinfo<SimpleWeb::HTTP>;
http_server.resource["^/pair$"]["GET"] = [&add_cert](auto resp, auto req) { pair<SimpleWeb::HTTP>(add_cert, resp, req); };
http_server.resource["^/pin/([0-9]+)$"]["GET"] = pin<SimpleWeb::HTTP>;
http_server.config.reuse_address = true;
http_server.config.address = "0.0.0.0"s;
http_server.config.port = PORT_HTTP;
http_server.config.address = "0.0.0.0"s;
http_server.config.port = port_http;
std::thread ssl { &https_server_t::start, &https_server };
std::thread tcp { &http_server_t::start, &http_server };
try {
https_server.bind();
http_server.bind();
}
catch(boost::system::system_error &err) {
BOOST_LOG(fatal) << "Couldn't bind http server to ports ["sv << port_http << ", "sv << port_http << "]: "sv << err.what();
shutdown_event->raise(true);
return;
}
auto accept_and_run = [&](auto *http_server) {
try {
http_server->accept_and_run();
}
catch(boost::system::system_error &err) {
// It's possible the exception gets thrown after calling http_server->stop() from a different thread
if(shutdown_event->peek()) {
return;
}
BOOST_LOG(fatal) << "Couldn't start http server to ports ["sv << port_https << ", "sv << port_https << "]: "sv << err.what();
shutdown_event->raise(true);
return;
}
};
std::thread ssl { accept_and_run, &https_server };
std::thread tcp { accept_and_run, &http_server };
// Wait for any event
shutdown_event->view();
@@ -816,31 +888,4 @@ void start(std::shared_ptr<safe::event_t<bool>> shutdown_event) {
ssl.join();
tcp.join();
}
int write_file(const char *path, const std::string_view &contents) {
std::ofstream out(path);
if(!out.is_open()) {
return -1;
}
out << contents;
return 0;
}
std::string read_file(const char *path) {
std::ifstream in(path);
std::string input;
std::string base64_cert;
//FIXME: Being unable to read file could result in infinite loop
while(!in.eof()) {
std::getline(in, input);
base64_cert += input + '\n';
}
return base64_cert;
}
}
} // namespace nvhttp

View File

@@ -5,17 +5,15 @@
#ifndef SUNSHINE_NVHTTP_H
#define SUNSHINE_NVHTTP_H
#include <functional>
#include "thread_safe.h"
#include <string>
#include "thread_safe.h"
#define CA_DIR SUNSHINE_ASSETS_DIR "/demoCA"
#define PRIVATE_KEY_FILE CA_DIR "/cakey.pem"
#define CERTIFICATE_FILE CA_DIR "/cacert.pem"
namespace nvhttp {
void start(std::shared_ptr<safe::event_t<bool>> shutdown_event);
}
constexpr auto PORT_HTTP = 0;
constexpr auto PORT_HTTPS = -5;
void start();
bool pin(std::string pin);
} // namespace nvhttp
#endif //SUNSHINE_NVHTTP_H

View File

@@ -5,11 +5,18 @@
#ifndef SUNSHINE_COMMON_H
#define SUNSHINE_COMMON_H
#include <bitset>
#include <filesystem>
#include <mutex>
#include <string>
#include "sunshine/utility.h"
struct sockaddr;
struct AVFrame;
namespace platf {
constexpr auto MAX_GAMEPADS = 2;
constexpr auto MAX_GAMEPADS = 32;
constexpr std::uint16_t DPAD_UP = 0x0001;
constexpr std::uint16_t DPAD_DOWN = 0x0002;
@@ -27,6 +34,81 @@ constexpr std::uint16_t B = 0x2000;
constexpr std::uint16_t X = 0x4000;
constexpr std::uint16_t Y = 0x8000;
namespace speaker {
enum speaker_e {
FRONT_LEFT,
FRONT_RIGHT,
FRONT_CENTER,
LOW_FREQUENCY,
BACK_LEFT,
BACK_RIGHT,
SIDE_LEFT,
SIDE_RIGHT,
MAX_SPEAKERS,
};
constexpr std::uint8_t map_stereo[] {
FRONT_LEFT, FRONT_RIGHT
};
constexpr std::uint8_t map_surround51[] {
FRONT_LEFT,
FRONT_RIGHT,
FRONT_CENTER,
LOW_FREQUENCY,
BACK_LEFT,
BACK_RIGHT,
};
constexpr std::uint8_t map_surround71[] {
FRONT_LEFT,
FRONT_RIGHT,
FRONT_CENTER,
LOW_FREQUENCY,
LOW_FREQUENCY,
BACK_LEFT,
BACK_RIGHT,
SIDE_LEFT,
SIDE_RIGHT,
};
} // namespace speaker
enum class mem_type_e {
system,
vaapi,
dxgi,
unknown
};
enum class pix_fmt_e {
yuv420p,
yuv420p10,
nv12,
p010,
unknown
};
inline std::string_view from_pix_fmt(pix_fmt_e pix_fmt) {
using namespace std::literals;
#define _CONVERT(x) \
case pix_fmt_e::x: \
return #x##sv
switch(pix_fmt) {
_CONVERT(yuv420p);
_CONVERT(yuv420p10);
_CONVERT(nv12);
_CONVERT(p010);
_CONVERT(unknown);
}
#undef _CONVERT
return "unknown"sv;
}
// Dimensions for touchscreen input
struct touch_port_t {
int offset_x, offset_y;
int width, height;
};
struct gamepad_state_t {
std::uint16_t buttonFlags;
std::uint8_t lt;
@@ -37,21 +119,61 @@ struct gamepad_state_t {
std::int16_t rsY;
};
class deinit_t {
public:
virtual ~deinit_t() = default;
};
struct img_t {
public:
std::uint8_t *data {};
std::int32_t width {};
std::uint8_t *data {};
std::int32_t width {};
std::int32_t height {};
std::int32_t pixel_pitch {};
std::int32_t row_pitch {};
img_t() = default;
img_t(const img_t&) = delete;
img_t(img_t&&) = delete;
img_t() = default;
img_t(const img_t &) = delete;
img_t(img_t &&) = delete;
virtual ~img_t() = default;
};
struct sink_t {
// Play on host PC
std::string host;
// On Windows, it is not possible to create a virtual sink
// Therefore, it is optional
struct null_t {
std::string stereo;
std::string surround51;
std::string surround71;
};
std::optional<null_t> null;
};
struct hwdevice_t {
void *data {};
AVFrame *frame {};
virtual int convert(platf::img_t &img) {
return -1;
}
/**
* implementations must take ownership of 'frame'
*/
virtual int set_frame(AVFrame *frame) {
std::abort(); // ^ This function must never be called
return -1;
};
virtual void set_colorspace(std::uint32_t colorspace, std::uint32_t color_range) {};
virtual ~hwdevice_t() = default;
};
enum class capture_e : int {
ok,
reinit,
@@ -61,10 +183,23 @@ enum class capture_e : int {
class display_t {
public:
virtual capture_e snapshot(img_t *img, bool cursor) = 0;
virtual std::unique_ptr<img_t> alloc_img() = 0;
display_t() noexcept : offset_x { 0 }, offset_y { 0 } {}
virtual capture_e snapshot(img_t *img, std::chrono::milliseconds timeout, bool cursor) = 0;
virtual std::shared_ptr<img_t> alloc_img() = 0;
virtual int dummy_img(img_t *img) = 0;
virtual std::shared_ptr<hwdevice_t> make_hwdevice(pix_fmt_e pix_fmt) {
return std::make_shared<hwdevice_t>();
}
virtual ~display_t() = default;
// Offsets for when streaming a specific monitor. By default, they are 0.
int offset_x, offset_y;
int env_width, env_height;
int width, height;
};
class mic_t {
@@ -74,22 +209,50 @@ public:
virtual ~mic_t() = default;
};
class audio_control_t {
public:
virtual int set_sink(const std::string &sink) = 0;
void freeInput(void*);
virtual std::unique_ptr<mic_t> microphone(const std::uint8_t *mapping, int channels, std::uint32_t sample_rate, std::uint32_t frame_size) = 0;
virtual std::optional<sink_t> sink_info() = 0;
virtual ~audio_control_t() = default;
};
void freeInput(void *);
using input_t = util::safe_ptr<void, freeInput>;
std::filesystem::path appdata();
std::string get_mac_address(const std::string_view &address);
std::unique_ptr<mic_t> microphone(std::uint32_t sample_rate);
std::shared_ptr<display_t> display();
std::string from_sockaddr(const sockaddr *const);
std::pair<std::uint16_t, std::string> from_sockaddr_ex(const sockaddr *const);
std::unique_ptr<audio_control_t> audio_control();
std::shared_ptr<display_t> display(mem_type_e hwdevice_type);
input_t input();
void move_mouse(input_t &input, int deltaX, int deltaY);
void abs_mouse(input_t &input, const touch_port_t &touch_port, float x, float y);
void button_mouse(input_t &input, int button, bool release);
void scroll(input_t &input, int distance);
void keyboard(input_t &input, uint16_t modcode, bool release);
void gamepad(input_t &input, int nr, const gamepad_state_t &gamepad_state);
int alloc_gamepad(input_t &input, int nr);
void free_gamepad(input_t &input, int nr);
#define SERVICE_NAME "Sunshine"
#define SERVICE_TYPE "_nvstream._tcp"
namespace publish {
[[nodiscard]] std::unique_ptr<deinit_t> start();
}
[[nodiscard]] std::unique_ptr<deinit_t> init();
} // namespace platf
#endif //SUNSHINE_COMMON_H

View File

@@ -1,416 +0,0 @@
//
// Created by loki on 6/21/19.
//
#include "common.h"
#include "../main.h"
#include <fstream>
#include <arpa/inet.h>
#include <ifaddrs.h>
#include <net/if.h>
#include <X11/X.h>
#include <X11/Xlib.h>
#include <X11/Xutil.h>
#include <X11/extensions/Xfixes.h>
#include <xcb/shm.h>
#include <xcb/xfixes.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <pulse/simple.h>
#include <pulse/error.h>
#include <bitset>
#include <sunshine/task_pool.h>
#include <sunshine/config.h>
namespace platf {
using namespace std::literals;
void freeImage(XImage *);
void freeX(XFixesCursorImage *);
using ifaddr_t = util::safe_ptr<ifaddrs, freeifaddrs>;
using xcb_connect_t = util::safe_ptr<xcb_connection_t, xcb_disconnect>;
using xcb_img_t = util::c_ptr<xcb_shm_get_image_reply_t>;
using xcb_cursor_img = util::c_ptr<xcb_xfixes_get_cursor_image_reply_t>;
using xdisplay_t = util::safe_ptr_v2<Display, int, XCloseDisplay>;
using ximg_t = util::safe_ptr<XImage, freeImage>;
using xcursor_t = util::safe_ptr<XFixesCursorImage, freeX>;
class shm_id_t {
public:
shm_id_t() : id { -1 } {}
shm_id_t(int id) : id {id } {}
shm_id_t(shm_id_t &&other) noexcept : id(other.id) {
other.id = -1;
}
~shm_id_t() {
if(id != -1) {
shmctl(id, IPC_RMID, nullptr);
id = -1;
}
}
int id;
};
class shm_data_t {
public:
shm_data_t() : data {(void*)-1 } {}
shm_data_t(void *data) : data {data } {}
shm_data_t(shm_data_t &&other) noexcept : data(other.data) {
other.data = (void*)-1;
}
~shm_data_t() {
if((std::uintptr_t)data != -1) {
shmdt(data);
data = (void*)-1;
}
}
void *data;
};
struct x11_img_t : public img_t {
ximg_t img;
};
struct shm_img_t : public img_t {
~shm_img_t() override {
delete[] data;
data = nullptr;
}
};
void blend_cursor(Display *display, img_t &img) {
xcursor_t overlay { XFixesGetCursorImage(display) };
if(!overlay) {
BOOST_LOG(error) << "Couldn't get cursor from XFixesGetCursorImage"sv;
return;
}
overlay->x -= overlay->xhot;
overlay->y -= overlay->yhot;
overlay->x = std::max((short)0, overlay->x);
overlay->y = std::max((short)0, overlay->y);
auto pixels = (int*)img.data;
auto screen_height = img.height;
auto screen_width = img.width;
auto delta_height = std::min<uint16_t>(overlay->height, std::max(0, screen_height - overlay->y));
auto delta_width = std::min<uint16_t>(overlay->width, std::max(0, screen_width - overlay->x));
for(auto y = 0; y < delta_height; ++y) {
auto overlay_begin = &overlay->pixels[y * overlay->width];
auto overlay_end = &overlay->pixels[y * overlay->width + delta_width];
auto pixels_begin = &pixels[(y + overlay->y) * (img.row_pitch / img.pixel_pitch) + overlay->x];
std::for_each(overlay_begin, overlay_end, [&](long pixel) {
int *pixel_p = (int*)&pixel;
auto colors_in = (uint8_t*)pixels_begin;
auto alpha = (*(uint*)pixel_p) >> 24u;
if(alpha == 255) {
*pixels_begin = *pixel_p;
}
else {
auto colors_out = (uint8_t*)pixel_p;
colors_in[0] = colors_out[0] + (colors_in[0] * (255 - alpha) + 255/2) / 255;
colors_in[1] = colors_out[1] + (colors_in[1] * (255 - alpha) + 255/2) / 255;
colors_in[2] = colors_out[2] + (colors_in[2] * (255 - alpha) + 255/2) / 255;
}
++pixels_begin;
});
}
}
struct x11_attr_t : public display_t {
x11_attr_t() : xdisplay {XOpenDisplay(nullptr) }, xwindow { }, xattr {} {
if(!xdisplay) {
BOOST_LOG(fatal) << "Could not open x11 display"sv;
log_flush();
std::abort();
}
xwindow = DefaultRootWindow(xdisplay.get());
refresh();
}
void refresh() {
XGetWindowAttributes(xdisplay.get(), xwindow, &xattr);
}
capture_e snapshot(img_t *img_out_base, bool cursor) override {
refresh();
XImage *img { XGetImage(
xdisplay.get(),
xwindow,
0, 0,
xattr.width, xattr.height,
AllPlanes, ZPixmap)
};
auto img_out = (x11_img_t*)img_out_base;
img_out->width = img->width;
img_out->height = img->height;
img_out->data = (uint8_t*)img->data;
img_out->row_pitch = img->bytes_per_line;
img_out->pixel_pitch = img->bits_per_pixel / 8;
img_out->img.reset(img);
if(cursor) {
blend_cursor(xdisplay.get(), *img_out_base);
}
return capture_e::ok;
}
std::unique_ptr<img_t> alloc_img() override {
return std::make_unique<x11_img_t>();
}
xdisplay_t xdisplay;
Window xwindow;
XWindowAttributes xattr;
};
struct shm_attr_t : public x11_attr_t {
xdisplay_t shm_xdisplay; // Prevent race condition with x11_attr_t::xdisplay
xcb_connect_t xcb;
xcb_screen_t *display;
std::uint32_t seg;
shm_id_t shm_id;
shm_data_t data;
util::TaskPool::task_id_t refresh_task_id;
void delayed_refresh() {
refresh();
refresh_task_id = task_pool.pushDelayed(&shm_attr_t::delayed_refresh, 2s, this).task_id;
}
shm_attr_t() : x11_attr_t(), shm_xdisplay {XOpenDisplay(nullptr) } {
refresh_task_id = task_pool.pushDelayed(&shm_attr_t::delayed_refresh, 2s, this).task_id;
}
~shm_attr_t() override {
while(!task_pool.cancel(refresh_task_id));
}
capture_e snapshot(img_t *img, bool cursor) override {
if(display->width_in_pixels != xattr.width || display->height_in_pixels != xattr.height) {
return capture_e::reinit;
}
auto img_cookie = xcb_shm_get_image_unchecked(
xcb.get(),
display->root,
0, 0,
display->width_in_pixels, display->height_in_pixels,
~0,
XCB_IMAGE_FORMAT_Z_PIXMAP,
seg,
0
);
xcb_img_t img_reply { xcb_shm_get_image_reply(xcb.get(), img_cookie, nullptr) };
if(!img_reply) {
BOOST_LOG(error) << "Could not get image reply"sv;
return capture_e::reinit;
}
if(img->width != display->width_in_pixels || img->height != display->height_in_pixels) {
delete[] img->data;
img->data = new std::uint8_t[frame_size()];
img->width = display->width_in_pixels;
img->height = display->height_in_pixels;
img->pixel_pitch = 4;
img->row_pitch = img->width * img->pixel_pitch;
}
std::copy_n((std::uint8_t*)data.data, frame_size(), img->data);
if(cursor) {
blend_cursor(shm_xdisplay.get(), *img);
}
return capture_e::ok;
}
std::unique_ptr<img_t> alloc_img() override {
return std::make_unique<shm_img_t>();
}
int init() {
shm_xdisplay.reset(XOpenDisplay(nullptr));
xcb.reset(xcb_connect(nullptr, nullptr));
if(xcb_connection_has_error(xcb.get())) {
return -1;
}
if(!xcb_get_extension_data(xcb.get(), &xcb_shm_id)->present) {
BOOST_LOG(error) << "Missing SHM extension"sv;
return -1;
}
auto iter = xcb_setup_roots_iterator(xcb_get_setup(xcb.get()));
display = iter.data;
seg = xcb_generate_id(xcb.get());
shm_id.id = shmget(IPC_PRIVATE, frame_size(), IPC_CREAT | 0777);
if(shm_id.id == -1) {
BOOST_LOG(error) << "shmget failed"sv;
return -1;
}
xcb_shm_attach(xcb.get(), seg, shm_id.id, false);
data.data = shmat(shm_id.id, nullptr, 0);
if ((uintptr_t)data.data == -1) {
BOOST_LOG(error) << "shmat failed"sv;
return -1;
}
return 0;
}
std::uint32_t frame_size() {
return display->height_in_pixels * display->width_in_pixels * 4;
}
};
struct mic_attr_t : public mic_t {
pa_sample_spec ss;
util::safe_ptr<pa_simple, pa_simple_free> mic;
explicit mic_attr_t(const pa_sample_spec& ss) : ss(ss), mic {} {}
capture_e sample(std::vector<std::int16_t> &sample_buf) override {
auto sample_size = sample_buf.size();
auto buf = sample_buf.data();
int status;
if(pa_simple_read(mic.get(), buf, sample_size * 2, &status)) {
BOOST_LOG(error) << "pa_simple_read() failed: "sv << pa_strerror(status);
return capture_e::error;
}
return capture_e::ok;
}
};
std::unique_ptr<display_t> shm_display() {
auto shm = std::make_unique<shm_attr_t>();
if(shm->init()) {
return nullptr;
}
return shm;
}
std::shared_ptr<display_t> display() {
auto shm_disp = shm_display();
if(!shm_disp) {
return std::unique_ptr<display_t> { new x11_attr_t {} };
}
return shm_disp;
}
std::unique_ptr<mic_t> microphone(std::uint32_t sample_rate) {
std::unique_ptr<mic_attr_t> mic {
new mic_attr_t {
{ PA_SAMPLE_S16LE, sample_rate, 2 }
}
};
int status;
const char *audio_sink = nullptr;
if(!config::audio.sink.empty()) {
audio_sink = config::audio.sink.c_str();
}
else {
audio_sink = "@DEFAULT_MONITOR@";
}
mic->mic.reset(
pa_simple_new(nullptr, "sunshine", pa_stream_direction_t::PA_STREAM_RECORD, audio_sink, "sunshine_record", &mic->ss, nullptr, nullptr, &status)
);
if(!mic->mic) {
auto err_str = pa_strerror(status);
BOOST_LOG(error) << "pa_simple_new() failed: "sv << err_str;
log_flush();
std::abort();
}
return mic;
}
ifaddr_t get_ifaddrs() {
ifaddrs *p { nullptr };
getifaddrs(&p);
return ifaddr_t { p };
}
std::string from_sockaddr(const sockaddr *const ip_addr) {
char data[INET6_ADDRSTRLEN];
auto family = ip_addr->sa_family;
if(family == AF_INET6) {
inet_ntop(AF_INET6, &((sockaddr_in6*)ip_addr)->sin6_addr, data, INET6_ADDRSTRLEN);
}
if(family == AF_INET) {
inet_ntop(AF_INET, &((sockaddr_in*)ip_addr)->sin_addr, data, INET_ADDRSTRLEN);
}
return std::string { data };
}
std::string get_mac_address(const std::string_view &address) {
auto ifaddrs = get_ifaddrs();
for(auto pos = ifaddrs.get(); pos != nullptr; pos = pos->ifa_next) {
if(pos->ifa_addr && address == from_sockaddr(pos->ifa_addr)) {
std::ifstream mac_file("/sys/class/net/"s + pos->ifa_name + "/address");
if(mac_file.good()) {
std::string mac_address;
std::getline(mac_file, mac_address);
return mac_address;
}
}
}
BOOST_LOG(warning) << "Unable to find MAC address for "sv << address;
return "00:00:00:00:00:00"s;
}
void freeImage(XImage *p) {
XDestroyImage(p);
}
void freeX(XFixesCursorImage *p) {
XFree(p);
}
}

View File

@@ -0,0 +1,437 @@
//
// Created by loki on 5/16/21.
//
#include <bitset>
#include <sstream>
#include <boost/regex.hpp>
#include <pulse/error.h>
#include <pulse/pulseaudio.h>
#include <pulse/simple.h>
#include "sunshine/platform/common.h"
#include "sunshine/config.h"
#include "sunshine/main.h"
#include "sunshine/thread_safe.h"
namespace platf {
using namespace std::literals;
constexpr pa_channel_position_t position_mapping[] {
PA_CHANNEL_POSITION_FRONT_LEFT,
PA_CHANNEL_POSITION_FRONT_RIGHT,
PA_CHANNEL_POSITION_FRONT_CENTER,
PA_CHANNEL_POSITION_LFE,
PA_CHANNEL_POSITION_REAR_LEFT,
PA_CHANNEL_POSITION_REAR_RIGHT,
PA_CHANNEL_POSITION_SIDE_LEFT,
PA_CHANNEL_POSITION_SIDE_RIGHT,
};
std::string to_string(const char *name, const std::uint8_t *mapping, int channels) {
std::stringstream ss;
ss << "rate=48000 sink_name="sv << name << " format=s16le channels="sv << channels << " channel_map="sv;
std::for_each_n(mapping, channels - 1, [&ss](std::uint8_t pos) {
ss << pa_channel_position_to_string(position_mapping[pos]) << ',';
});
ss << pa_channel_position_to_string(position_mapping[mapping[channels - 1]]);
ss << " sink_properties=device.description="sv << name;
auto result = ss.str();
BOOST_LOG(debug) << "null-sink args: "sv << result;
return result;
}
struct mic_attr_t : public mic_t {
util::safe_ptr<pa_simple, pa_simple_free> mic;
capture_e sample(std::vector<std::int16_t> &sample_buf) override {
auto sample_size = sample_buf.size();
auto buf = sample_buf.data();
int status;
if(pa_simple_read(mic.get(), buf, sample_size * 2, &status)) {
BOOST_LOG(error) << "pa_simple_read() failed: "sv << pa_strerror(status);
return capture_e::error;
}
return capture_e::ok;
}
};
std::unique_ptr<mic_t> microphone(const std::uint8_t *mapping, int channels, std::uint32_t sample_rate) {
auto mic = std::make_unique<mic_attr_t>();
pa_sample_spec ss { PA_SAMPLE_S16LE, sample_rate, (std::uint8_t)channels };
pa_channel_map pa_map;
pa_map.channels = channels;
std::for_each_n(pa_map.map, pa_map.channels, [mapping](auto &channel) mutable {
channel = position_mapping[*mapping++];
});
int status;
const char *audio_sink = "@DEFAULT_MONITOR@";
if(!config::audio.sink.empty()) {
audio_sink = config::audio.sink.c_str();
}
mic->mic.reset(
pa_simple_new(nullptr, "sunshine",
pa_stream_direction_t::PA_STREAM_RECORD, audio_sink,
"sunshine-record", &ss, &pa_map, nullptr, &status));
if(!mic->mic) {
auto err_str = pa_strerror(status);
BOOST_LOG(error) << "pa_simple_new() failed: "sv << err_str;
log_flush();
std::abort();
}
return mic;
}
namespace pa {
template<bool B, class T>
struct add_const_helper;
template<class T>
struct add_const_helper<true, T> {
using type = const std::remove_pointer_t<T> *;
};
template<class T>
struct add_const_helper<false, T> {
using type = const T *;
};
template<class T>
using add_const_t = typename add_const_helper<std::is_pointer_v<T>, T>::type;
template<class T>
void pa_free(T *p) {
pa_xfree(p);
}
using ctx_t = util::safe_ptr<pa_context, pa_context_unref>;
using loop_t = util::safe_ptr<pa_mainloop, pa_mainloop_free>;
using op_t = util::safe_ptr<pa_operation, pa_operation_unref>;
using string_t = util::safe_ptr<char, pa_free<char>>;
template<class T>
using cb_t = std::function<void(ctx_t::pointer, add_const_t<T> i, int eol)>;
template<class T>
void cb(ctx_t::pointer ctx, add_const_t<T> i, int eol, void *userdata) {
auto &f = *(cb_t<T> *)userdata;
// For some reason, pulseaudio calls this callback after disconnecting
if(i && eol) {
return;
}
f(ctx, i, eol);
}
void cb_i(ctx_t::pointer ctx, std::uint32_t i, void *userdata) {
auto alarm = (safe::alarm_raw_t<int> *)userdata;
alarm->ring(i);
}
void ctx_state_cb(ctx_t::pointer ctx, void *userdata) {
auto &f = *(std::function<void(ctx_t::pointer)> *)userdata;
f(ctx);
}
void success_cb(ctx_t::pointer ctx, int status, void *userdata) {
assert(userdata != nullptr);
auto alarm = (safe::alarm_raw_t<int> *)userdata;
alarm->ring(status ? 0 : 1);
}
class server_t : public audio_control_t {
enum ctx_event_e : int {
ready,
terminated,
failed
};
public:
loop_t loop;
ctx_t ctx;
struct {
std::uint32_t stereo = PA_INVALID_INDEX;
std::uint32_t surround51 = PA_INVALID_INDEX;
std::uint32_t surround71 = PA_INVALID_INDEX;
} index;
std::unique_ptr<safe::event_t<ctx_event_e>> events;
std::unique_ptr<std::function<void(ctx_t::pointer)>> events_cb;
std::thread worker;
int init() {
events = std::make_unique<safe::event_t<ctx_event_e>>();
loop.reset(pa_mainloop_new());
ctx.reset(pa_context_new(pa_mainloop_get_api(loop.get()), "sunshine"));
events_cb = std::make_unique<std::function<void(ctx_t::pointer)>>([this](ctx_t::pointer ctx) {
switch(pa_context_get_state(ctx)) {
case PA_CONTEXT_READY:
events->raise(ready);
break;
case PA_CONTEXT_TERMINATED:
BOOST_LOG(debug) << "Pulseadio context terminated"sv;
events->raise(terminated);
break;
case PA_CONTEXT_FAILED:
BOOST_LOG(debug) << "Pulseadio context failed"sv;
events->raise(failed);
break;
case PA_CONTEXT_CONNECTING:
BOOST_LOG(debug) << "Connecting to pulseaudio"sv;
case PA_CONTEXT_UNCONNECTED:
case PA_CONTEXT_AUTHORIZING:
case PA_CONTEXT_SETTING_NAME:
break;
}
});
pa_context_set_state_callback(ctx.get(), ctx_state_cb, events_cb.get());
auto status = pa_context_connect(ctx.get(), nullptr, PA_CONTEXT_NOFLAGS, nullptr);
if(status) {
BOOST_LOG(error) << "Couldn't connect to pulseaudio: "sv << pa_strerror(status);
return -1;
}
worker = std::thread {
[](loop_t::pointer loop) {
int retval;
auto status = pa_mainloop_run(loop, &retval);
if(status < 0) {
BOOST_LOG(fatal) << "Couldn't run pulseaudio main loop"sv;
log_flush();
std::abort();
}
},
loop.get()
};
auto event = events->pop();
if(event == failed) {
return -1;
}
return 0;
}
int load_null(const char *name, const std::uint8_t *channel_mapping, int channels) {
auto alarm = safe::make_alarm<int>();
op_t op {
pa_context_load_module(
ctx.get(),
"module-null-sink",
to_string(name, channel_mapping, channels).c_str(),
cb_i,
alarm.get()),
};
alarm->wait();
return *alarm->status();
}
int unload_null(std::uint32_t i) {
if(i == PA_INVALID_INDEX) {
return 0;
}
auto alarm = safe::make_alarm<int>();
op_t op {
pa_context_unload_module(ctx.get(), i, success_cb, alarm.get())
};
alarm->wait();
if(*alarm->status()) {
BOOST_LOG(error) << "Couldn't unload null-sink with index ["sv << i << "]: "sv << pa_strerror(pa_context_errno(ctx.get()));
return -1;
}
return 0;
}
std::optional<sink_t> sink_info() override {
constexpr auto stereo = "sink-sunshine-stereo";
constexpr auto surround51 = "sink-sunshine-surround51";
constexpr auto surround71 = "sink-sunshine-surround71";
auto alarm = safe::make_alarm<int>();
sink_t sink;
// If hardware sink with more channels found, set that as host
int channels = 0;
// Count of all virtual sinks that are created by us
int nullcount = 0;
cb_t<pa_sink_info *> f = [&](ctx_t::pointer ctx, const pa_sink_info *sink_info, int eol) {
if(!sink_info) {
if(!eol) {
BOOST_LOG(error) << "Couldn't get pulseaudio sink info: "sv << pa_strerror(pa_context_errno(ctx));
alarm->ring(-1);
}
alarm->ring(0);
return;
}
if(sink_info->active_port != nullptr) {
sink.host = sink_info->name;
channels = sink_info->channel_map.channels;
}
// Ensure Sunshine won't create a sink that already exists.
if(!std::strcmp(sink_info->name, stereo)) {
index.stereo = sink_info->owner_module;
++nullcount;
}
else if(!std::strcmp(sink_info->name, surround51)) {
index.surround51 = sink_info->owner_module;
++nullcount;
}
else if(!std::strcmp(sink_info->name, surround71)) {
index.surround71 = sink_info->owner_module;
++nullcount;
}
};
op_t op { pa_context_get_sink_info_list(ctx.get(), cb<pa_sink_info *>, &f) };
if(!op) {
BOOST_LOG(error) << "Couldn't create card info operation: "sv << pa_strerror(pa_context_errno(ctx.get()));
return std::nullopt;
}
alarm->wait();
if(*alarm->status()) {
return std::nullopt;
}
if(!channels) {
BOOST_LOG(warning) << "Couldn't find an active sink"sv;
}
if(index.stereo == PA_INVALID_INDEX) {
index.stereo = load_null(stereo, speaker::map_stereo, sizeof(speaker::map_stereo));
if(index.stereo == PA_INVALID_INDEX) {
BOOST_LOG(warning) << "Couldn't create virtual sink for stereo: "sv << pa_strerror(pa_context_errno(ctx.get()));
}
else {
++nullcount;
}
}
if(index.surround51 == PA_INVALID_INDEX) {
index.surround51 = load_null(surround51, speaker::map_surround51, sizeof(speaker::map_surround51));
if(index.surround51 == PA_INVALID_INDEX) {
BOOST_LOG(warning) << "Couldn't create virtual sink for surround-51: "sv << pa_strerror(pa_context_errno(ctx.get()));
}
else {
++nullcount;
}
}
if(index.surround71 == PA_INVALID_INDEX) {
index.surround71 = load_null(surround71, speaker::map_surround71, sizeof(speaker::map_surround71));
if(index.surround71 == PA_INVALID_INDEX) {
BOOST_LOG(warning) << "Couldn't create virtual sink for surround-71: "sv << pa_strerror(pa_context_errno(ctx.get()));
}
else {
++nullcount;
}
}
if(nullcount == 3) {
sink.null = std::make_optional(sink_t::null_t { stereo, surround51, surround71 });
}
return std::make_optional(std::move(sink));
}
std::unique_ptr<mic_t> microphone(const std::uint8_t *mapping, int channels, std::uint32_t sample_rate, std::uint32_t frame_size) override {
return ::platf::microphone(mapping, channels, sample_rate);
}
int set_sink(const std::string &sink) override {
auto alarm = safe::make_alarm<int>();
op_t op {
pa_context_set_default_sink(
ctx.get(), sink.c_str(), success_cb, alarm.get()),
};
if(!op) {
BOOST_LOG(error) << "Couldn't create set default-sink operation: "sv << pa_strerror(pa_context_errno(ctx.get()));
return -1;
}
alarm->wait();
if(*alarm->status()) {
BOOST_LOG(error) << "Couldn't set default-sink ["sv << sink << "]: "sv << pa_strerror(pa_context_errno(ctx.get()));
return -1;
}
return 0;
}
~server_t() override {
unload_null(index.stereo);
unload_null(index.surround51);
unload_null(index.surround71);
if(worker.joinable()) {
pa_context_disconnect(ctx.get());
KITTY_WHILE_LOOP(auto event = events->pop(), event != terminated && event != failed, {
event = events->pop();
})
pa_mainloop_quit(loop.get(), 0);
worker.join();
}
}
};
} // namespace pa
std::unique_ptr<audio_control_t> audio_control() {
auto audio = std::make_unique<pa::server_t>();
if(audio->init()) {
return nullptr;
}
return audio;
}
} // namespace platf

View File

@@ -0,0 +1,408 @@
//
// Created by loki on 6/21/19.
//
#include "sunshine/platform/common.h"
#include <fstream>
#include <X11/X.h>
#include <X11/Xlib.h>
#include <X11/Xutil.h>
#include <X11/extensions/Xfixes.h>
#include <X11/extensions/Xrandr.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <xcb/shm.h>
#include <xcb/xfixes.h>
#include "sunshine/config.h"
#include "sunshine/main.h"
#include "sunshine/task_pool.h"
#include "vaapi.h"
using namespace std::literals;
namespace platf {
void freeImage(XImage *);
void freeX(XFixesCursorImage *);
using xcb_connect_t = util::safe_ptr<xcb_connection_t, xcb_disconnect>;
using xcb_img_t = util::c_ptr<xcb_shm_get_image_reply_t>;
using xdisplay_t = util::safe_ptr_v2<Display, int, XCloseDisplay>;
using ximg_t = util::safe_ptr<XImage, freeImage>;
using xcursor_t = util::safe_ptr<XFixesCursorImage, freeX>;
using crtc_info_t = util::safe_ptr<_XRRCrtcInfo, XRRFreeCrtcInfo>;
using output_info_t = util::safe_ptr<_XRROutputInfo, XRRFreeOutputInfo>;
using screen_res_t = util::safe_ptr<_XRRScreenResources, XRRFreeScreenResources>;
class shm_id_t {
public:
shm_id_t() : id { -1 } {}
shm_id_t(int id) : id { id } {}
shm_id_t(shm_id_t &&other) noexcept : id(other.id) {
other.id = -1;
}
~shm_id_t() {
if(id != -1) {
shmctl(id, IPC_RMID, nullptr);
id = -1;
}
}
int id;
};
class shm_data_t {
public:
shm_data_t() : data { (void *)-1 } {}
shm_data_t(void *data) : data { data } {}
shm_data_t(shm_data_t &&other) noexcept : data(other.data) {
other.data = (void *)-1;
}
~shm_data_t() {
if((std::uintptr_t)data != -1) {
shmdt(data);
}
}
void *data;
};
struct x11_img_t : public img_t {
ximg_t img;
};
struct shm_img_t : public img_t {
~shm_img_t() override {
delete[] data;
data = nullptr;
}
};
void blend_cursor(Display *display, img_t &img, int offsetX, int offsetY) {
xcursor_t overlay { XFixesGetCursorImage(display) };
if(!overlay) {
BOOST_LOG(error) << "Couldn't get cursor from XFixesGetCursorImage"sv;
return;
}
overlay->x -= overlay->xhot;
overlay->y -= overlay->yhot;
overlay->x -= offsetX;
overlay->y -= offsetY;
overlay->x = std::max((short)0, overlay->x);
overlay->y = std::max((short)0, overlay->y);
auto pixels = (int *)img.data;
auto screen_height = img.height;
auto screen_width = img.width;
auto delta_height = std::min<uint16_t>(overlay->height, std::max(0, screen_height - overlay->y));
auto delta_width = std::min<uint16_t>(overlay->width, std::max(0, screen_width - overlay->x));
for(auto y = 0; y < delta_height; ++y) {
auto overlay_begin = &overlay->pixels[y * overlay->width];
auto overlay_end = &overlay->pixels[y * overlay->width + delta_width];
auto pixels_begin = &pixels[(y + overlay->y) * (img.row_pitch / img.pixel_pitch) + overlay->x];
std::for_each(overlay_begin, overlay_end, [&](long pixel) {
int *pixel_p = (int *)&pixel;
auto colors_in = (uint8_t *)pixels_begin;
auto alpha = (*(uint *)pixel_p) >> 24u;
if(alpha == 255) {
*pixels_begin = *pixel_p;
}
else {
auto colors_out = (uint8_t *)pixel_p;
colors_in[0] = colors_out[0] + (colors_in[0] * (255 - alpha) + 255 / 2) / 255;
colors_in[1] = colors_out[1] + (colors_in[1] * (255 - alpha) + 255 / 2) / 255;
colors_in[2] = colors_out[2] + (colors_in[2] * (255 - alpha) + 255 / 2) / 255;
}
++pixels_begin;
});
}
}
struct x11_attr_t : public display_t {
xdisplay_t xdisplay;
Window xwindow;
XWindowAttributes xattr;
mem_type_e mem_type;
/*
* Last X (NOT the streamed monitor!) size.
* This way we can trigger reinitialization if the dimensions changed while streaming
*/
// int env_width, env_height;
x11_attr_t(mem_type_e mem_type) : xdisplay { XOpenDisplay(nullptr) }, xwindow {}, xattr {}, mem_type { mem_type } {
XInitThreads();
}
int init() {
if(!xdisplay) {
BOOST_LOG(error) << "Could not open X11 display"sv;
return -1;
}
xwindow = DefaultRootWindow(xdisplay.get());
refresh();
int streamedMonitor = -1;
if(!config::video.output_name.empty()) {
streamedMonitor = (int)util::from_view(config::video.output_name);
}
if(streamedMonitor != -1) {
BOOST_LOG(info) << "Configuring selected monitor ("sv << streamedMonitor << ") to stream"sv;
screen_res_t screenr { XRRGetScreenResources(xdisplay.get(), xwindow) };
int output = screenr->noutput;
output_info_t result;
int monitor = 0;
for(int x = 0; x < output; ++x) {
output_info_t out_info { XRRGetOutputInfo(xdisplay.get(), screenr.get(), screenr->outputs[x]) };
if(out_info && out_info->connection == RR_Connected) {
if(monitor++ == streamedMonitor) {
result = std::move(out_info);
break;
}
}
}
if(!result) {
BOOST_LOG(error) << "Could not stream display number ["sv << streamedMonitor << "], there are only ["sv << monitor << "] displays."sv;
return -1;
}
crtc_info_t crt_info { XRRGetCrtcInfo(xdisplay.get(), screenr.get(), result->crtc) };
BOOST_LOG(info)
<< "Streaming display: "sv << result->name << " with res "sv << crt_info->width << 'x' << crt_info->height << " offset by "sv << crt_info->x << 'x' << crt_info->y;
width = crt_info->width;
height = crt_info->height;
offset_x = crt_info->x;
offset_y = crt_info->y;
}
else {
width = xattr.width;
height = xattr.height;
}
env_width = xattr.width;
env_height = xattr.height;
return 0;
}
/**
* Called when the display attributes should change.
*/
void refresh() {
XGetWindowAttributes(xdisplay.get(), xwindow, &xattr); //Update xattr's
}
capture_e snapshot(img_t *img_out_base, std::chrono::milliseconds timeout, bool cursor) override {
refresh();
//The whole X server changed, so we gotta reinit everything
if(xattr.width != env_width || xattr.height != env_height) {
BOOST_LOG(warning) << "X dimensions changed in non-SHM mode, request reinit"sv;
return capture_e::reinit;
}
XImage *img { XGetImage(xdisplay.get(), xwindow, offset_x, offset_y, width, height, AllPlanes, ZPixmap) };
auto img_out = (x11_img_t *)img_out_base;
img_out->width = img->width;
img_out->height = img->height;
img_out->data = (uint8_t *)img->data;
img_out->row_pitch = img->bytes_per_line;
img_out->pixel_pitch = img->bits_per_pixel / 8;
img_out->img.reset(img);
if(cursor) {
blend_cursor(xdisplay.get(), *img_out_base, offset_x, offset_y);
}
return capture_e::ok;
}
std::shared_ptr<img_t> alloc_img() override {
return std::make_shared<x11_img_t>();
}
std::shared_ptr<hwdevice_t> make_hwdevice(pix_fmt_e pix_fmt) override {
if(mem_type == mem_type_e::vaapi) {
return egl::make_hwdevice(width, height);
}
return std::make_shared<hwdevice_t>();
}
int dummy_img(img_t *img) override {
snapshot(img, 0s, true);
return 0;
}
};
struct shm_attr_t : public x11_attr_t {
xdisplay_t shm_xdisplay; // Prevent race condition with x11_attr_t::xdisplay
xcb_connect_t xcb;
xcb_screen_t *display;
std::uint32_t seg;
shm_id_t shm_id;
shm_data_t data;
util::TaskPool::task_id_t refresh_task_id;
void delayed_refresh() {
refresh();
refresh_task_id = task_pool.pushDelayed(&shm_attr_t::delayed_refresh, 2s, this).task_id;
}
shm_attr_t(mem_type_e mem_type) : x11_attr_t(mem_type), shm_xdisplay { XOpenDisplay(nullptr) } {
refresh_task_id = task_pool.pushDelayed(&shm_attr_t::delayed_refresh, 2s, this).task_id;
}
~shm_attr_t() override {
while(!task_pool.cancel(refresh_task_id))
;
}
capture_e snapshot(img_t *img, std::chrono::milliseconds timeout, bool cursor) override {
//The whole X server changed, so we gotta reinit everything
if(xattr.width != env_width || xattr.height != env_height) {
BOOST_LOG(warning) << "X dimensions changed in SHM mode, request reinit"sv;
return capture_e::reinit;
}
else {
auto img_cookie = xcb_shm_get_image_unchecked(xcb.get(), display->root, offset_x, offset_y, width, height, ~0, XCB_IMAGE_FORMAT_Z_PIXMAP, seg, 0);
xcb_img_t img_reply { xcb_shm_get_image_reply(xcb.get(), img_cookie, nullptr) };
if(!img_reply) {
BOOST_LOG(error) << "Could not get image reply"sv;
return capture_e::reinit;
}
std::copy_n((std::uint8_t *)data.data, frame_size(), img->data);
if(cursor) {
blend_cursor(shm_xdisplay.get(), *img, offset_x, offset_y);
}
return capture_e::ok;
}
}
std::shared_ptr<img_t> alloc_img() override {
auto img = std::make_shared<shm_img_t>();
img->width = width;
img->height = height;
img->pixel_pitch = 4;
img->row_pitch = img->pixel_pitch * width;
img->data = new std::uint8_t[height * img->row_pitch];
return img;
}
int dummy_img(platf::img_t *img) override {
return 0;
}
int init() {
if(x11_attr_t::init()) {
return 1;
}
shm_xdisplay.reset(XOpenDisplay(nullptr));
xcb.reset(xcb_connect(nullptr, nullptr));
if(xcb_connection_has_error(xcb.get())) {
return -1;
}
if(!xcb_get_extension_data(xcb.get(), &xcb_shm_id)->present) {
BOOST_LOG(error) << "Missing SHM extension"sv;
return -1;
}
auto iter = xcb_setup_roots_iterator(xcb_get_setup(xcb.get()));
display = iter.data;
seg = xcb_generate_id(xcb.get());
shm_id.id = shmget(IPC_PRIVATE, frame_size(), IPC_CREAT | 0777);
if(shm_id.id == -1) {
BOOST_LOG(error) << "shmget failed"sv;
return -1;
}
xcb_shm_attach(xcb.get(), seg, shm_id.id, false);
data.data = shmat(shm_id.id, nullptr, 0);
if((uintptr_t)data.data == -1) {
BOOST_LOG(error) << "shmat failed"sv;
return -1;
}
return 0;
}
std::uint32_t frame_size() {
return width * height * 4;
}
};
std::shared_ptr<display_t> display(platf::mem_type_e hwdevice_type) {
if(hwdevice_type != platf::mem_type_e::system && hwdevice_type != platf::mem_type_e::vaapi) {
BOOST_LOG(error) << "Could not initialize display with the given hw device type."sv;
return nullptr;
}
// Attempt to use shared memory X11 to avoid copying the frame
auto shm_disp = std::make_shared<shm_attr_t>(hwdevice_type);
auto status = shm_disp->init();
if(status > 0) {
// x11_attr_t::init() failed, don't bother trying again.
return nullptr;
}
if(status == 0) {
return shm_disp;
}
// Fallback
auto x11_disp = std::make_shared<x11_attr_t>(hwdevice_type);
if(x11_disp->init()) {
return nullptr;
}
return x11_disp;
}
void freeImage(XImage *p) {
XDestroyImage(p);
}
void freeX(XFixesCursorImage *p) {
XFree(p);
}
} // namespace platf

View File

@@ -1,16 +1,17 @@
#include <libevdev/libevdev.h>
#include <libevdev/libevdev-uinput.h>
#include <libevdev/libevdev.h>
#include <X11/X.h>
#include <X11/Xlib.h>
#include <X11/Xutil.h>
#include <X11/extensions/XTest.h>
#include <cmath>
#include <cstring>
#include <filesystem>
#include "common.h"
#include "sunshine/main.h"
#include "sunshine/platform/common.h"
#include "sunshine/utility.h"
// Support older versions
@@ -24,13 +25,27 @@
namespace platf {
using namespace std::literals;
using evdev_t = util::safe_ptr<libevdev, libevdev_free>;
using evdev_t = util::safe_ptr<libevdev, libevdev_free>;
using uinput_t = util::safe_ptr<libevdev_uinput, libevdev_uinput_destroy>;
using keyboard_t = util::safe_ptr_v2<Display, int, XCloseDisplay>;
constexpr touch_port_t target_touch_port {
0, 0,
19200, 12000
};
struct input_raw_t {
public:
void clear_touchscreen() {
std::filesystem::path touch_path { "sunshine_touchscreen"sv };
if(std::filesystem::is_symlink(touch_path)) {
std::filesystem::remove(touch_path);
}
touch_input.reset();
}
void clear_mouse() {
std::filesystem::path mouse_path { "sunshine_mouse"sv };
@@ -51,13 +66,11 @@ public:
std::filesystem::remove(gamepad_path);
}
gamepads[nr].first.reset();
gamepads[nr] = std::make_pair(uinput_t {}, gamepad_state_t {});
}
int create_mouse() {
libevdev_uinput *buf;
int err = libevdev_uinput_create_from_device(mouse_dev.get(), LIBEVDEV_UINPUT_OPEN_MANAGED, &buf);
mouse_input.reset(buf);
int err = libevdev_uinput_create_from_device(mouse_dev.get(), LIBEVDEV_UINPUT_OPEN_MANAGED, &mouse_input);
if(err) {
BOOST_LOG(error) << "Could not create Sunshine Mouse: "sv << strerror(-err);
@@ -69,13 +82,24 @@ public:
return 0;
}
int create_gamepad(int nr) {
int create_touchscreen() {
int err = libevdev_uinput_create_from_device(touch_dev.get(), LIBEVDEV_UINPUT_OPEN_MANAGED, &touch_input);
if(err) {
BOOST_LOG(error) << "Could not create Sunshine Touchscreen: "sv << strerror(-err);
return -1;
}
std::filesystem::create_symlink(libevdev_uinput_get_devnode(touch_input.get()), "sunshine_touchscreen"sv);
return 0;
}
int alloc_gamepad(int nr) {
TUPLE_2D_REF(input, gamepad_state, gamepads[nr]);
libevdev_uinput *buf;
int err = libevdev_uinput_create_from_device(gamepad_dev.get(), LIBEVDEV_UINPUT_OPEN_MANAGED, &buf);
int err = libevdev_uinput_create_from_device(gamepad_dev.get(), LIBEVDEV_UINPUT_OPEN_MANAGED, &input);
input.reset(buf);
gamepad_state = gamepad_state_t {};
if(err) {
@@ -86,12 +110,17 @@ public:
std::stringstream ss;
ss << "sunshine_gamepad_"sv << nr;
std::filesystem::path gamepad_path { ss.str() };
std::filesystem::create_symlink(libevdev_uinput_get_devnode(input.get()), gamepad_path);
if(std::filesystem::is_symlink(gamepad_path)) {
std::filesystem::remove(gamepad_path);
}
std::filesystem::create_symlink(libevdev_uinput_get_devnode(input.get()), gamepad_path);
return 0;
}
void clear() {
clear_touchscreen();
clear_mouse();
for(int x = 0; x < gamepads.size(); ++x) {
clear_gamepad(x);
@@ -102,18 +131,33 @@ public:
clear();
}
evdev_t gamepad_dev;
std::vector<std::pair<uinput_t, gamepad_state_t>> gamepads;
evdev_t mouse_dev;
uinput_t mouse_input;
uinput_t touch_input;
evdev_t gamepad_dev;
evdev_t touch_dev;
evdev_t mouse_dev;
keyboard_t keyboard;
};
void abs_mouse(input_t &input, const touch_port_t &touch_port, float x, float y) {
auto touchscreen = ((input_raw_t *)input.get())->touch_input.get();
auto scaled_x = (int)std::lround((x + touch_port.offset_x) * ((float)target_touch_port.width / (float)touch_port.width));
auto scaled_y = (int)std::lround((y + touch_port.offset_y) * ((float)target_touch_port.height / (float)touch_port.height));
libevdev_uinput_write_event(touchscreen, EV_ABS, ABS_X, scaled_x);
libevdev_uinput_write_event(touchscreen, EV_ABS, ABS_Y, scaled_y);
libevdev_uinput_write_event(touchscreen, EV_KEY, BTN_TOOL_FINGER, 1);
libevdev_uinput_write_event(touchscreen, EV_KEY, BTN_TOOL_FINGER, 0);
libevdev_uinput_write_event(touchscreen, EV_SYN, SYN_REPORT, 0);
}
void move_mouse(input_t &input, int deltaX, int deltaY) {
auto mouse = ((input_raw_t*)input.get())->mouse_input.get();
auto mouse = ((input_raw_t *)input.get())->mouse_input.get();
if(deltaX) {
libevdev_uinput_write_event(mouse, EV_REL, REL_X, deltaX);
@@ -132,26 +176,26 @@ void button_mouse(input_t &input, int button, bool release) {
if(button == 1) {
btn_type = BTN_LEFT;
scan = 90001;
scan = 90001;
}
else if(button == 2) {
btn_type = BTN_MIDDLE;
scan = 90003;
scan = 90003;
}
else if(button == 3) {
btn_type = BTN_RIGHT;
scan = 90002;
scan = 90002;
}
else if(button == 4) {
btn_type = BTN_SIDE;
scan = 90004;
scan = 90004;
}
else {
btn_type = BTN_EXTRA;
scan = 90005;
scan = 90005;
}
auto mouse = ((input_raw_t*)input.get())->mouse_input.get();
auto mouse = ((input_raw_t *)input.get())->mouse_input.get();
libevdev_uinput_write_event(mouse, EV_MSC, MSC_SCAN, scan);
libevdev_uinput_write_event(mouse, EV_KEY, btn_type, release ? 0 : 1);
libevdev_uinput_write_event(mouse, EV_SYN, SYN_REPORT, 0);
@@ -160,7 +204,7 @@ void button_mouse(input_t &input, int button, bool release) {
void scroll(input_t &input, int high_res_distance) {
int distance = high_res_distance / 120;
auto mouse = ((input_raw_t*)input.get())->mouse_input.get();
auto mouse = ((input_raw_t *)input.get())->mouse_input.get();
libevdev_uinput_write_event(mouse, EV_REL, REL_WHEEL, distance);
libevdev_uinput_write_event(mouse, EV_REL, REL_WHEEL_HI_RES, high_res_distance);
libevdev_uinput_write_event(mouse, EV_SYN, SYN_REPORT, 0);
@@ -168,7 +212,7 @@ void scroll(input_t &input, int high_res_distance) {
uint16_t keysym(uint16_t modcode) {
constexpr auto VK_NUMPAD = 0x60;
constexpr auto VK_F1 = 0x70;
constexpr auto VK_F1 = 0x70;
if(modcode >= VK_NUMPAD && modcode < VK_NUMPAD + 10) {
return XK_KP_0 + (modcode - VK_NUMPAD);
@@ -180,108 +224,108 @@ uint16_t keysym(uint16_t modcode) {
switch(modcode) {
case 0x08:
return XK_BackSpace;
case 0x09:
return XK_Tab;
case 0x0D:
return XK_Return;
case 0x13:
return XK_Pause;
case 0x14:
return XK_Caps_Lock;
case 0x1B:
return XK_Escape;
case 0x21:
return XK_Page_Up;
case 0x22:
return XK_Page_Down;
case 0x23:
return XK_End;
case 0x24:
return XK_Home;
case 0x25:
return XK_Left;
case 0x26:
return XK_Up;
case 0x27:
return XK_Right;
case 0x28:
return XK_Down;
case 0x29:
return XK_Select;
case 0x2B:
return XK_Execute;
case 0x2C:
return XK_Print;
case 0x2D:
return XK_Insert;
case 0x2E:
return XK_Delete;
case 0x2F:
return XK_Help;
case 0x6A:
return XK_KP_Multiply;
case 0x6B:
return XK_KP_Add;
case 0x6C:
return XK_KP_Separator;
case 0x6D:
return XK_KP_Subtract;
case 0x6E:
return XK_KP_Decimal;
case 0x6F:
return XK_KP_Divide;
case 0x90:
return XK_Num_Lock;
case 0x91:
return XK_Scroll_Lock;
case 0xA0:
return XK_Shift_L;
case 0xA1:
return XK_Shift_R;
case 0xA2:
return XK_Control_L;
case 0xA3:
return XK_Control_R;
case 0xA4:
return XK_Alt_L;
case 0xA5: /* return XK_Alt_R; */
return XK_Super_L;
case 0x5B:
return XK_Super_L;
case 0x5C:
return XK_Super_R;
case 0xBA:
return XK_semicolon;
case 0xBB:
return XK_equal;
case 0xBC:
return XK_comma;
case 0xBD:
return XK_minus;
case 0xBE:
return XK_period;
case 0xBF:
return XK_slash;
case 0xC0:
return XK_grave;
case 0xDB:
return XK_bracketleft;
case 0xDC:
return XK_backslash;
case 0xDD:
return XK_bracketright;
case 0xDE:
return XK_apostrophe;
case 0x08:
return XK_BackSpace;
case 0x09:
return XK_Tab;
case 0x0D:
return XK_Return;
case 0x13:
return XK_Pause;
case 0x14:
return XK_Caps_Lock;
case 0x1B:
return XK_Escape;
case 0x21:
return XK_Page_Up;
case 0x22:
return XK_Page_Down;
case 0x23:
return XK_End;
case 0x24:
return XK_Home;
case 0x25:
return XK_Left;
case 0x26:
return XK_Up;
case 0x27:
return XK_Right;
case 0x28:
return XK_Down;
case 0x29:
return XK_Select;
case 0x2B:
return XK_Execute;
case 0x2C:
return XK_Print;
case 0x2D:
return XK_Insert;
case 0x2E:
return XK_Delete;
case 0x2F:
return XK_Help;
case 0x6A:
return XK_KP_Multiply;
case 0x6B:
return XK_KP_Add;
case 0x6C:
return XK_KP_Separator;
case 0x6D:
return XK_KP_Subtract;
case 0x6E:
return XK_KP_Decimal;
case 0x6F:
return XK_KP_Divide;
case 0x90:
return XK_Num_Lock;
case 0x91:
return XK_Scroll_Lock;
case 0xA0:
return XK_Shift_L;
case 0xA1:
return XK_Shift_R;
case 0xA2:
return XK_Control_L;
case 0xA3:
return XK_Control_R;
case 0xA4:
return XK_Alt_L;
case 0xA5: /* return XK_Alt_R; */
return XK_Super_L;
case 0x5B:
return XK_Super_L;
case 0x5C:
return XK_Super_R;
case 0xBA:
return XK_semicolon;
case 0xBB:
return XK_equal;
case 0xBC:
return XK_comma;
case 0xBD:
return XK_minus;
case 0xBE:
return XK_period;
case 0xBF:
return XK_slash;
case 0xC0:
return XK_grave;
case 0xDB:
return XK_bracketleft;
case 0xDC:
return XK_backslash;
case 0xDD:
return XK_bracketright;
case 0xDE:
return XK_apostrophe;
}
return modcode;
}
void keyboard(input_t &input, uint16_t modcode, bool release) {
auto &keyboard = ((input_raw_t*)input.get())->keyboard;
KeyCode kc = XKeysymToKeycode(keyboard.get(), keysym(modcode));
auto &keyboard = ((input_raw_t *)input.get())->keyboard;
KeyCode kc = XKeysymToKeycode(keyboard.get(), keysym(modcode));
if(!kc) {
return;
@@ -293,11 +337,19 @@ void keyboard(input_t &input, uint16_t modcode, bool release) {
XFlush(keyboard.get());
}
int alloc_gamepad(input_t &input, int nr) {
return ((input_raw_t *)input.get())->alloc_gamepad(nr);
}
void free_gamepad(input_t &input, int nr) {
((input_raw_t *)input.get())->clear_gamepad(nr);
}
void gamepad(input_t &input, int nr, const gamepad_state_t &gamepad_state) {
TUPLE_2D_REF(uinput, gamepad_state_old, ((input_raw_t*)input.get())->gamepads[nr]);
TUPLE_2D_REF(uinput, gamepad_state_old, ((input_raw_t *)input.get())->gamepads[nr]);
auto bf = gamepad_state.buttonFlags ^ gamepad_state_old.buttonFlags;
auto bf = gamepad_state.buttonFlags ^ gamepad_state_old.buttonFlags;
auto bf_new = gamepad_state.buttonFlags;
if(bf) {
@@ -314,17 +366,17 @@ void gamepad(input_t &input, int nr, const gamepad_state_t &gamepad_state) {
libevdev_uinput_write_event(uinput.get(), EV_ABS, ABS_HAT0X, button_state);
}
if(START & bf) libevdev_uinput_write_event(uinput.get(), EV_KEY, BTN_START, bf_new & START ? 1 : 0);
if(BACK & bf) libevdev_uinput_write_event(uinput.get(), EV_KEY, BTN_SELECT, bf_new & BACK ? 1 : 0);
if(LEFT_STICK & bf) libevdev_uinput_write_event(uinput.get(), EV_KEY, BTN_THUMBL, bf_new & LEFT_STICK ? 1 : 0);
if(RIGHT_STICK & bf) libevdev_uinput_write_event(uinput.get(), EV_KEY, BTN_THUMBR, bf_new & RIGHT_STICK ? 1 : 0);
if(LEFT_BUTTON & bf) libevdev_uinput_write_event(uinput.get(), EV_KEY, BTN_TL, bf_new & LEFT_BUTTON ? 1 : 0);
if(RIGHT_BUTTON & bf) libevdev_uinput_write_event(uinput.get(), EV_KEY, BTN_TR, bf_new & RIGHT_BUTTON ? 1 : 0);
if(HOME & bf) libevdev_uinput_write_event(uinput.get(), EV_KEY, BTN_MODE, bf_new & HOME ? 1 : 0);
if(A & bf) libevdev_uinput_write_event(uinput.get(), EV_KEY, BTN_SOUTH, bf_new & A ? 1 : 0);
if(B & bf) libevdev_uinput_write_event(uinput.get(), EV_KEY, BTN_EAST, bf_new & B ? 1 : 0);
if(X & bf) libevdev_uinput_write_event(uinput.get(), EV_KEY, BTN_NORTH, bf_new & X ? 1 : 0);
if(Y & bf) libevdev_uinput_write_event(uinput.get(), EV_KEY, BTN_WEST, bf_new & Y ? 1 : 0);
if(START & bf) libevdev_uinput_write_event(uinput.get(), EV_KEY, BTN_START, bf_new & START ? 1 : 0);
if(BACK & bf) libevdev_uinput_write_event(uinput.get(), EV_KEY, BTN_SELECT, bf_new & BACK ? 1 : 0);
if(LEFT_STICK & bf) libevdev_uinput_write_event(uinput.get(), EV_KEY, BTN_THUMBL, bf_new & LEFT_STICK ? 1 : 0);
if(RIGHT_STICK & bf) libevdev_uinput_write_event(uinput.get(), EV_KEY, BTN_THUMBR, bf_new & RIGHT_STICK ? 1 : 0);
if(LEFT_BUTTON & bf) libevdev_uinput_write_event(uinput.get(), EV_KEY, BTN_TL, bf_new & LEFT_BUTTON ? 1 : 0);
if(RIGHT_BUTTON & bf) libevdev_uinput_write_event(uinput.get(), EV_KEY, BTN_TR, bf_new & RIGHT_BUTTON ? 1 : 0);
if(HOME & bf) libevdev_uinput_write_event(uinput.get(), EV_KEY, BTN_MODE, bf_new & HOME ? 1 : 0);
if(A & bf) libevdev_uinput_write_event(uinput.get(), EV_KEY, BTN_SOUTH, bf_new & A ? 1 : 0);
if(B & bf) libevdev_uinput_write_event(uinput.get(), EV_KEY, BTN_EAST, bf_new & B ? 1 : 0);
if(X & bf) libevdev_uinput_write_event(uinput.get(), EV_KEY, BTN_NORTH, bf_new & X ? 1 : 0);
if(Y & bf) libevdev_uinput_write_event(uinput.get(), EV_KEY, BTN_WEST, bf_new & Y ? 1 : 0);
}
if(gamepad_state_old.lt != gamepad_state.lt) {
@@ -356,7 +408,7 @@ void gamepad(input_t &input, int nr, const gamepad_state_t &gamepad_state) {
}
evdev_t mouse() {
evdev_t dev { libevdev_new() };
evdev_t dev { libevdev_new() };
libevdev_set_uniq(dev.get(), "Sunshine Mouse");
libevdev_set_id_product(dev.get(), 0x4038);
@@ -397,6 +449,48 @@ evdev_t mouse() {
return dev;
}
evdev_t touchscreen() {
evdev_t dev { libevdev_new() };
libevdev_set_uniq(dev.get(), "Sunshine Touch");
libevdev_set_id_product(dev.get(), 0xDEAD);
libevdev_set_id_vendor(dev.get(), 0xBEEF);
libevdev_set_id_bustype(dev.get(), 0x3);
libevdev_set_id_version(dev.get(), 0x111);
libevdev_set_name(dev.get(), "Touchscreen passthrough");
libevdev_enable_property(dev.get(), INPUT_PROP_DIRECT);
libevdev_enable_event_type(dev.get(), EV_KEY);
libevdev_enable_event_code(dev.get(), EV_KEY, BTN_TOUCH, nullptr);
libevdev_enable_event_code(dev.get(), EV_KEY, BTN_TOOL_PEN, nullptr); // Needed to be enabled for BTN_TOOL_FINGER to work.
libevdev_enable_event_code(dev.get(), EV_KEY, BTN_TOOL_FINGER, nullptr);
input_absinfo absx {
0,
0,
target_touch_port.width,
1,
0,
28
};
input_absinfo absy {
0,
0,
target_touch_port.height,
1,
0,
28
};
libevdev_enable_event_type(dev.get(), EV_ABS);
libevdev_enable_event_code(dev.get(), EV_ABS, ABS_X, &absx);
libevdev_enable_event_code(dev.get(), EV_ABS, ABS_Y, &absy);
return dev;
}
evdev_t x360() {
evdev_t dev { libevdev_new() };
@@ -459,9 +553,8 @@ evdev_t x360() {
input_t input() {
input_t result { new input_raw_t() };
auto &gp = *(input_raw_t*)result.get();
auto &gp = *(input_raw_t *)result.get();
gp.gamepads.resize(MAX_GAMEPADS);
gp.keyboard.reset(XOpenDisplay(nullptr));
// If we do not have a keyboard, gamepad or mouse, no input is possible and we should abort
@@ -471,19 +564,15 @@ input_t input() {
std::abort();
}
gp.gamepads.resize(MAX_GAMEPADS);
// Ensure starting from clean slate
gp.clear();
gp.mouse_dev = mouse();
gp.touch_dev = touchscreen();
gp.mouse_dev = mouse();
gp.gamepad_dev = x360();
for(int x = 0; x < gp.gamepads.size(); ++x) {
if(gp.create_gamepad(x)) {
log_flush();
std::abort();
}
}
if(gp.create_mouse()) {
if(gp.create_mouse() || gp.create_touchscreen()) {
log_flush();
std::abort();
}
@@ -492,7 +581,7 @@ input_t input() {
}
void freeInput(void *p) {
auto *input = (input_raw_t*)p;
auto *input = (input_raw_t *)p;
delete input;
}
}
} // namespace platf

View File

@@ -0,0 +1,138 @@
#include <arpa/inet.h>
#include <dlfcn.h>
#include <fcntl.h>
#include <ifaddrs.h>
#include <pwd.h>
#include <unistd.h>
#include <fstream>
#include "misc.h"
#include "sunshine/main.h"
#include "sunshine/platform/common.h"
#ifdef __GNUC__
#define SUNSHINE_GNUC_EXTENSION __extension__
#else
#define SUNSHINE_GNUC_EXTENSION
#endif
using namespace std::literals;
namespace fs = std::filesystem;
namespace platf {
using ifaddr_t = util::safe_ptr<ifaddrs, freeifaddrs>;
ifaddr_t get_ifaddrs() {
ifaddrs *p { nullptr };
getifaddrs(&p);
return ifaddr_t { p };
}
fs::path appdata() {
const char *homedir;
if((homedir = getenv("HOME")) == nullptr) {
homedir = getpwuid(geteuid())->pw_dir;
}
return fs::path { homedir } / ".config/sunshine"sv;
}
std::string from_sockaddr(const sockaddr *const ip_addr) {
char data[INET6_ADDRSTRLEN];
auto family = ip_addr->sa_family;
if(family == AF_INET6) {
inet_ntop(AF_INET6, &((sockaddr_in6 *)ip_addr)->sin6_addr, data,
INET6_ADDRSTRLEN);
}
if(family == AF_INET) {
inet_ntop(AF_INET, &((sockaddr_in *)ip_addr)->sin_addr, data,
INET_ADDRSTRLEN);
}
return std::string { data };
}
std::pair<std::uint16_t, std::string> from_sockaddr_ex(const sockaddr *const ip_addr) {
char data[INET6_ADDRSTRLEN];
auto family = ip_addr->sa_family;
std::uint16_t port;
if(family == AF_INET6) {
inet_ntop(AF_INET6, &((sockaddr_in6 *)ip_addr)->sin6_addr, data,
INET6_ADDRSTRLEN);
port = ((sockaddr_in6 *)ip_addr)->sin6_port;
}
if(family == AF_INET) {
inet_ntop(AF_INET, &((sockaddr_in *)ip_addr)->sin_addr, data,
INET_ADDRSTRLEN);
port = ((sockaddr_in *)ip_addr)->sin_port;
}
return { port, std::string { data } };
}
std::string get_mac_address(const std::string_view &address) {
auto ifaddrs = get_ifaddrs();
for(auto pos = ifaddrs.get(); pos != nullptr; pos = pos->ifa_next) {
if(pos->ifa_addr && address == from_sockaddr(pos->ifa_addr)) {
std::ifstream mac_file("/sys/class/net/"s + pos->ifa_name + "/address");
if(mac_file.good()) {
std::string mac_address;
std::getline(mac_file, mac_address);
return mac_address;
}
}
}
BOOST_LOG(warning) << "Unable to find MAC address for "sv << address;
return "00:00:00:00:00:00"s;
}
} // namespace platf
namespace dyn {
void *handle(const std::vector<const char *> &libs) {
void *handle;
for(auto lib : libs) {
handle = dlopen(lib, RTLD_LAZY | RTLD_LOCAL);
if(handle) {
return handle;
}
}
std::stringstream ss;
ss << "Couldn't find any of the following libraries: ["sv << libs.front();
std::for_each(std::begin(libs) + 1, std::end(libs), [&](auto lib) {
ss << ", "sv << lib;
});
ss << ']';
BOOST_LOG(error) << ss.str();
return nullptr;
}
int load(void *handle, const std::vector<std::tuple<apiproc *, const char *>> &funcs, bool strict) {
int err = 0;
for(auto &func : funcs) {
TUPLE_2D_REF(fn, name, func);
*fn = SUNSHINE_GNUC_EXTENSION(apiproc) dlsym(handle, name);
if(!*fn && strict) {
BOOST_LOG(error) << "Couldn't find function: "sv << name;
err = -1;
}
}
return err;
}
} // namespace dyn

View File

@@ -0,0 +1,14 @@
#ifndef SUNSHINE_PLATFORM_MISC_H
#define SUNSHINE_PLATFORM_MISC_H
#include <vector>
namespace dyn {
typedef void (*apiproc)(void);
int load(void *handle, const std::vector<std::tuple<apiproc *, const char *>> &funcs, bool strict = true);
void *handle(const std::vector<const char *> &libs);
} // namespace dyn
#endif

View File

@@ -0,0 +1,429 @@
// adapted from https://www.avahi.org/doxygen/html/client-publish-service_8c-example.html
#include <thread>
#include "misc.h"
#include "sunshine/main.h"
#include "sunshine/nvhttp.h"
#include "sunshine/platform/common.h"
#include "sunshine/utility.h"
using namespace std::literals;
namespace avahi {
/** Error codes used by avahi */
enum err_e {
OK = 0, /**< OK */
ERR_FAILURE = -1, /**< Generic error code */
ERR_BAD_STATE = -2, /**< Object was in a bad state */
ERR_INVALID_HOST_NAME = -3, /**< Invalid host name */
ERR_INVALID_DOMAIN_NAME = -4, /**< Invalid domain name */
ERR_NO_NETWORK = -5, /**< No suitable network protocol available */
ERR_INVALID_TTL = -6, /**< Invalid DNS TTL */
ERR_IS_PATTERN = -7, /**< RR key is pattern */
ERR_COLLISION = -8, /**< Name collision */
ERR_INVALID_RECORD = -9, /**< Invalid RR */
ERR_INVALID_SERVICE_NAME = -10, /**< Invalid service name */
ERR_INVALID_SERVICE_TYPE = -11, /**< Invalid service type */
ERR_INVALID_PORT = -12, /**< Invalid port number */
ERR_INVALID_KEY = -13, /**< Invalid key */
ERR_INVALID_ADDRESS = -14, /**< Invalid address */
ERR_TIMEOUT = -15, /**< Timeout reached */
ERR_TOO_MANY_CLIENTS = -16, /**< Too many clients */
ERR_TOO_MANY_OBJECTS = -17, /**< Too many objects */
ERR_TOO_MANY_ENTRIES = -18, /**< Too many entries */
ERR_OS = -19, /**< OS error */
ERR_ACCESS_DENIED = -20, /**< Access denied */
ERR_INVALID_OPERATION = -21, /**< Invalid operation */
ERR_DBUS_ERROR = -22, /**< An unexpected D-Bus error occurred */
ERR_DISCONNECTED = -23, /**< Daemon connection failed */
ERR_NO_MEMORY = -24, /**< Memory exhausted */
ERR_INVALID_OBJECT = -25, /**< The object passed to this function was invalid */
ERR_NO_DAEMON = -26, /**< Daemon not running */
ERR_INVALID_INTERFACE = -27, /**< Invalid interface */
ERR_INVALID_PROTOCOL = -28, /**< Invalid protocol */
ERR_INVALID_FLAGS = -29, /**< Invalid flags */
ERR_NOT_FOUND = -30, /**< Not found */
ERR_INVALID_CONFIG = -31, /**< Configuration error */
ERR_VERSION_MISMATCH = -32, /**< Verson mismatch */
ERR_INVALID_SERVICE_SUBTYPE = -33, /**< Invalid service subtype */
ERR_INVALID_PACKET = -34, /**< Invalid packet */
ERR_INVALID_DNS_ERROR = -35, /**< Invlaid DNS return code */
ERR_DNS_FORMERR = -36, /**< DNS Error: Form error */
ERR_DNS_SERVFAIL = -37, /**< DNS Error: Server Failure */
ERR_DNS_NXDOMAIN = -38, /**< DNS Error: No such domain */
ERR_DNS_NOTIMP = -39, /**< DNS Error: Not implemented */
ERR_DNS_REFUSED = -40, /**< DNS Error: Operation refused */
ERR_DNS_YXDOMAIN = -41,
ERR_DNS_YXRRSET = -42,
ERR_DNS_NXRRSET = -43,
ERR_DNS_NOTAUTH = -44, /**< DNS Error: Not authorized */
ERR_DNS_NOTZONE = -45,
ERR_INVALID_RDATA = -46, /**< Invalid RDATA */
ERR_INVALID_DNS_CLASS = -47, /**< Invalid DNS class */
ERR_INVALID_DNS_TYPE = -48, /**< Invalid DNS type */
ERR_NOT_SUPPORTED = -49, /**< Not supported */
ERR_NOT_PERMITTED = -50, /**< Operation not permitted */
ERR_INVALID_ARGUMENT = -51, /**< Invalid argument */
ERR_IS_EMPTY = -52, /**< Is empty */
ERR_NO_CHANGE = -53, /**< The requested operation is invalid because it is redundant */
ERR_MAX = -54
};
constexpr auto IF_UNSPEC = -1;
enum proto {
PROTO_INET = 0, /**< IPv4 */
PROTO_INET6 = 1, /**< IPv6 */
PROTO_UNSPEC = -1 /**< Unspecified/all protocol(s) */
};
enum ServerState {
SERVER_INVALID, /**< Invalid state (initial) */
SERVER_REGISTERING, /**< Host RRs are being registered */
SERVER_RUNNING, /**< All host RRs have been established */
SERVER_COLLISION, /**< There is a collision with a host RR. All host RRs have been withdrawn, the user should set a new host name via avahi_server_set_host_name() */
SERVER_FAILURE /**< Some fatal failure happened, the server is unable to proceed */
};
enum ClientState {
CLIENT_S_REGISTERING = SERVER_REGISTERING, /**< Server state: REGISTERING */
CLIENT_S_RUNNING = SERVER_RUNNING, /**< Server state: RUNNING */
CLIENT_S_COLLISION = SERVER_COLLISION, /**< Server state: COLLISION */
CLIENT_FAILURE = 100, /**< Some kind of error happened on the client side */
CLIENT_CONNECTING = 101 /**< We're still connecting. This state is only entered when AVAHI_CLIENT_NO_FAIL has been passed to avahi_client_new() and the daemon is not yet available. */
};
enum EntryGroupState {
ENTRY_GROUP_UNCOMMITED, /**< The group has not yet been commited, the user must still call avahi_entry_group_commit() */
ENTRY_GROUP_REGISTERING, /**< The entries of the group are currently being registered */
ENTRY_GROUP_ESTABLISHED, /**< The entries have successfully been established */
ENTRY_GROUP_COLLISION, /**< A name collision for one of the entries in the group has been detected, the entries have been withdrawn */
ENTRY_GROUP_FAILURE /**< Some kind of failure happened, the entries have been withdrawn */
};
enum ClientFlags {
CLIENT_IGNORE_USER_CONFIG = 1, /**< Don't read user configuration */
CLIENT_NO_FAIL = 2 /**< Don't fail if the daemon is not available when avahi_client_new() is called, instead enter CLIENT_CONNECTING state and wait for the daemon to appear */
};
/** Some flags for publishing functions */
enum PublishFlags {
PUBLISH_UNIQUE = 1, /**< For raw records: The RRset is intended to be unique */
PUBLISH_NO_PROBE = 2, /**< For raw records: Though the RRset is intended to be unique no probes shall be sent */
PUBLISH_NO_ANNOUNCE = 4, /**< For raw records: Do not announce this RR to other hosts */
PUBLISH_ALLOW_MULTIPLE = 8, /**< For raw records: Allow multiple local records of this type, even if they are intended to be unique */
/** \cond fulldocs */
PUBLISH_NO_REVERSE = 16, /**< For address records: don't create a reverse (PTR) entry */
PUBLISH_NO_COOKIE = 32, /**< For service records: do not implicitly add the local service cookie to TXT data */
/** \endcond */
PUBLISH_UPDATE = 64, /**< Update existing records instead of adding new ones */
/** \cond fulldocs */
PUBLISH_USE_WIDE_AREA = 128, /**< Register the record using wide area DNS (i.e. unicast DNS update) */
PUBLISH_USE_MULTICAST = 256 /**< Register the record using multicast DNS */
/** \endcond */
};
using IfIndex = int;
using Protocol = int;
struct EntryGroup;
struct Poll;
struct SimplePoll;
struct Client;
typedef void (*ClientCallback)(Client *, ClientState, void *userdata);
typedef void (*EntryGroupCallback)(EntryGroup *g, EntryGroupState state, void *userdata);
typedef void (*free_fn)(void *);
typedef Client *(*client_new_fn)(const Poll *poll_api, ClientFlags flags, ClientCallback callback, void *userdata, int *error);
typedef void (*client_free_fn)(Client *);
typedef char *(*alternative_service_name_fn)(char *);
typedef Client *(*entry_group_get_client_fn)(EntryGroup *);
typedef EntryGroup *(*entry_group_new_fn)(Client *, EntryGroupCallback, void *userdata);
typedef int (*entry_group_add_service_fn)(
EntryGroup *group,
IfIndex interface,
Protocol protocol,
PublishFlags flags,
const char *name,
const char *type,
const char *domain,
const char *host,
uint16_t port,
...);
typedef int (*entry_group_is_empty_fn)(EntryGroup *);
typedef int (*entry_group_reset_fn)(EntryGroup *);
typedef int (*entry_group_commit_fn)(EntryGroup *);
typedef char *(*strdup_fn)(const char *);
typedef char *(*strerror_fn)(int);
typedef int (*client_errno_fn)(Client *);
typedef Poll *(*simple_poll_get_fn)(SimplePoll *);
typedef int (*simple_poll_loop_fn)(SimplePoll *);
typedef void (*simple_poll_quit_fn)(SimplePoll *);
typedef SimplePoll *(*simple_poll_new_fn)();
typedef void (*simple_poll_free_fn)(SimplePoll *);
free_fn free;
client_new_fn client_new;
client_free_fn client_free;
alternative_service_name_fn alternative_service_name;
entry_group_get_client_fn entry_group_get_client;
entry_group_new_fn entry_group_new;
entry_group_add_service_fn entry_group_add_service;
entry_group_is_empty_fn entry_group_is_empty;
entry_group_reset_fn entry_group_reset;
entry_group_commit_fn entry_group_commit;
strdup_fn strdup;
strerror_fn strerror;
client_errno_fn client_errno;
simple_poll_get_fn simple_poll_get;
simple_poll_loop_fn simple_poll_loop;
simple_poll_quit_fn simple_poll_quit;
simple_poll_new_fn simple_poll_new;
simple_poll_free_fn simple_poll_free;
int init_common() {
static void *handle { nullptr };
static bool funcs_loaded = false;
if(funcs_loaded) return 0;
if(!handle) {
handle = dyn::handle({ "libavahi-common.so.3", "libavahi-common.so" });
if(!handle) {
return -1;
}
}
std::vector<std::tuple<dyn::apiproc *, const char *>> funcs {
{ (dyn::apiproc *)&alternative_service_name, "avahi_alternative_service_name" },
{ (dyn::apiproc *)&free, "avahi_free" },
{ (dyn::apiproc *)&strdup, "avahi_strdup" },
{ (dyn::apiproc *)&strerror, "avahi_strerror" },
{ (dyn::apiproc *)&simple_poll_get, "avahi_simple_poll_get" },
{ (dyn::apiproc *)&simple_poll_loop, "avahi_simple_poll_loop" },
{ (dyn::apiproc *)&simple_poll_quit, "avahi_simple_poll_quit" },
{ (dyn::apiproc *)&simple_poll_new, "avahi_simple_poll_new" },
{ (dyn::apiproc *)&simple_poll_free, "avahi_simple_poll_free" },
};
if(dyn::load(handle, funcs)) {
return -1;
}
funcs_loaded = true;
return 0;
}
int init_client() {
if(init_common()) {
return -1;
}
static void *handle { nullptr };
static bool funcs_loaded = false;
if(funcs_loaded) return 0;
if(!handle) {
handle = dyn::handle({ "libavahi-client.so.3", "libavahi-client.so" });
if(!handle) {
return -1;
}
}
std::vector<std::tuple<dyn::apiproc *, const char *>> funcs {
{ (dyn::apiproc *)&client_new, "avahi_client_new" },
{ (dyn::apiproc *)&client_free, "avahi_client_free" },
{ (dyn::apiproc *)&entry_group_get_client, "avahi_entry_group_get_client" },
{ (dyn::apiproc *)&entry_group_new, "avahi_entry_group_new" },
{ (dyn::apiproc *)&entry_group_add_service, "avahi_entry_group_add_service" },
{ (dyn::apiproc *)&entry_group_is_empty, "avahi_entry_group_is_empty" },
{ (dyn::apiproc *)&entry_group_reset, "avahi_entry_group_reset" },
{ (dyn::apiproc *)&entry_group_commit, "avahi_entry_group_commit" },
{ (dyn::apiproc *)&client_errno, "avahi_client_errno" },
};
if(dyn::load(handle, funcs)) {
return -1;
}
funcs_loaded = true;
return 0;
}
} // namespace avahi
namespace platf::publish {
template<class T>
void free(T *p) {
avahi::free(p);
}
template<class T>
using ptr_t = util::safe_ptr<T, free<T>>;
using client_t = util::dyn_safe_ptr<avahi::Client, &avahi::client_free>;
using poll_t = util::dyn_safe_ptr<avahi::SimplePoll, &avahi::simple_poll_free>;
avahi::EntryGroup *group = nullptr;
poll_t poll;
client_t client;
ptr_t<char> name;
void create_services(avahi::Client *c);
void entry_group_callback(avahi::EntryGroup *g, avahi::EntryGroupState state, void *) {
group = g;
switch(state) {
case avahi::ENTRY_GROUP_ESTABLISHED:
BOOST_LOG(info) << "Avahi service " << name.get() << " successfully established.";
break;
case avahi::ENTRY_GROUP_COLLISION:
name.reset(avahi::alternative_service_name(name.get()));
BOOST_LOG(info) << "Avahi service name collision, renaming service to " << name.get();
create_services(avahi::entry_group_get_client(g));
break;
case avahi::ENTRY_GROUP_FAILURE:
BOOST_LOG(error) << "Avahi entry group failure: " << avahi::strerror(avahi::client_errno(avahi::entry_group_get_client(g)));
avahi::simple_poll_quit(poll.get());
break;
case avahi::ENTRY_GROUP_UNCOMMITED:
case avahi::ENTRY_GROUP_REGISTERING:;
}
}
void create_services(avahi::Client *c) {
int ret;
auto fg = util::fail_guard([]() {
avahi::simple_poll_quit(poll.get());
});
if(!group) {
if(!(group = avahi::entry_group_new(c, entry_group_callback, nullptr))) {
BOOST_LOG(error) << "avahi::entry_group_new() failed: "sv << avahi::strerror(avahi::client_errno(c));
return;
}
}
if(avahi::entry_group_is_empty(group)) {
BOOST_LOG(info) << "Adding avahi service "sv << name.get();
ret = avahi::entry_group_add_service(
group,
avahi::IF_UNSPEC, avahi::PROTO_UNSPEC,
avahi::PublishFlags(0),
name.get(),
SERVICE_TYPE,
nullptr, nullptr,
map_port(nvhttp::PORT_HTTP),
nullptr);
if(ret < 0) {
if(ret == avahi::ERR_COLLISION) {
// A service name collision with a local service happened. Let's pick a new name
name.reset(avahi::alternative_service_name(name.get()));
BOOST_LOG(info) << "Service name collision, renaming service to "sv << name.get();
avahi::entry_group_reset(group);
create_services(c);
fg.disable();
return;
}
BOOST_LOG(error) << "Failed to add "sv << SERVICE_TYPE << " service: "sv << avahi::strerror(ret);
return;
}
ret = avahi::entry_group_commit(group);
if(ret < 0) {
BOOST_LOG(error) << "Failed to commit entry group: "sv << avahi::strerror(ret);
return;
}
}
fg.disable();
}
void client_callback(avahi::Client *c, avahi::ClientState state, void *) {
switch(state) {
case avahi::CLIENT_S_RUNNING:
create_services(c);
break;
case avahi::CLIENT_FAILURE:
BOOST_LOG(error) << "Client failure: "sv << avahi::strerror(avahi::client_errno(c));
avahi::simple_poll_quit(poll.get());
break;
case avahi::CLIENT_S_COLLISION:
case avahi::CLIENT_S_REGISTERING:
if(group)
avahi::entry_group_reset(group);
break;
case avahi::CLIENT_CONNECTING:;
}
}
class deinit_t : public ::platf::deinit_t {
public:
std::thread poll_thread;
deinit_t(std::thread poll_thread) : poll_thread { std::move(poll_thread) } {}
~deinit_t() override {
if(avahi::simple_poll_quit && poll) {
avahi::simple_poll_quit(poll.get());
}
if(poll_thread.joinable()) {
poll_thread.join();
}
}
};
[[nodiscard]] std::unique_ptr<::platf::deinit_t> start() {
if(avahi::init_client()) {
return nullptr;
}
int avhi_error;
poll.reset(avahi::simple_poll_new());
if(!poll) {
BOOST_LOG(error) << "Failed to create simple poll object."sv;
return nullptr;
}
name.reset(avahi::strdup(SERVICE_NAME));
client.reset(
avahi::client_new(avahi::simple_poll_get(poll.get()), avahi::ClientFlags(0), client_callback, nullptr, &avhi_error));
if(!client) {
BOOST_LOG(error) << "Failed to create client: "sv << avahi::strerror(avhi_error);
return nullptr;
}
return std::make_unique<deinit_t>(std::thread { avahi::simple_poll_loop, poll.get() });
}
}; // namespace platf::publish

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,8 @@
#ifndef SUNSHINE_DISPLAY_H
#define SUNSHINE_DISPLAY_H
#include "sunshine/platform/common.h"
namespace platf::egl {
std::shared_ptr<hwdevice_t> make_hwdevice(int width, int height);
} // namespace platf::egl
#endif

View File

@@ -1,291 +0,0 @@
#include <thread>
#include <sstream>
#include <iomanip>
#include <Ws2tcpip.h>
#include <Winsock2.h>
#include <windows.h>
#include <winuser.h>
#include <iphlpapi.h>
#include <ViGEm/Client.h>
#include "sunshine/main.h"
#include "common.h"
namespace platf {
using namespace std::literals;
using adapteraddrs_t = util::c_ptr<IP_ADAPTER_ADDRESSES>;
class vigem_t {
public:
using client_t = util::safe_ptr<_VIGEM_CLIENT_T, vigem_free>;
using target_t = util::safe_ptr<_VIGEM_TARGET_T, vigem_target_free>;
int init() {
VIGEM_ERROR status;
client.reset(vigem_alloc());
status = vigem_connect(client.get());
if(!VIGEM_SUCCESS(status)) {
BOOST_LOG(warning) << "Couldn't setup connection to ViGEm for gamepad support ["sv << util::hex(status).to_string_view() << ']';
return -1;
}
x360s.resize(MAX_GAMEPADS);
for(auto &x360 : x360s) {
x360.reset(vigem_target_x360_alloc());
status = vigem_target_add(client.get(), x360.get());
if(!VIGEM_SUCCESS(status)) {
BOOST_LOG(error) << "Couldn't add Gamepad to ViGEm connection ["sv << util::hex(status).to_string_view() << ']';
return -1;
}
}
return 0;
}
~vigem_t() {
if(client) {
for(auto &x360 : x360s) {
if(x360 && vigem_target_is_attached(x360.get())) {
auto status = vigem_target_remove(client.get(), x360.get());
if(!VIGEM_SUCCESS(status)) {
BOOST_LOG(warning) << "Couldn't detach gamepad from ViGEm ["sv << util::hex(status).to_string_view() << ']';
}
}
}
vigem_disconnect(client.get());
}
}
std::vector<target_t> x360s;
client_t client;
};
std::string from_socket_address(const SOCKET_ADDRESS &socket_address) {
char data[INET6_ADDRSTRLEN];
auto family = socket_address.lpSockaddr->sa_family;
if(family == AF_INET6) {
inet_ntop(AF_INET6, &((sockaddr_in6*)socket_address.lpSockaddr)->sin6_addr, data, INET6_ADDRSTRLEN);
}
if(family == AF_INET) {
inet_ntop(AF_INET, &((sockaddr_in*)socket_address.lpSockaddr)->sin_addr, data, INET_ADDRSTRLEN);
}
return std::string { data };
}
adapteraddrs_t get_adapteraddrs() {
adapteraddrs_t info { nullptr };
ULONG size = 0;
while(GetAdaptersAddresses(AF_UNSPEC, 0, nullptr, info.get(), &size) == ERROR_BUFFER_OVERFLOW) {
info.reset((PIP_ADAPTER_ADDRESSES)malloc(size));
}
return info;
}
std::string get_mac_address(const std::string_view &address) {
adapteraddrs_t info = get_adapteraddrs();
for(auto adapter_pos = info.get(); adapter_pos != nullptr; adapter_pos = adapter_pos->Next) {
for(auto addr_pos = adapter_pos->FirstUnicastAddress; addr_pos != nullptr; addr_pos = addr_pos->Next) {
if(adapter_pos->PhysicalAddressLength != 0 && address == from_socket_address(addr_pos->Address)) {
std::stringstream mac_addr;
mac_addr << std::hex;
for(int i = 0; i < adapter_pos->PhysicalAddressLength; i++) {
if(i > 0) {
mac_addr << ':';
}
mac_addr << std::setw(2) << std::setfill('0') << (int)adapter_pos->PhysicalAddress[i];
}
return mac_addr.str();
}
}
}
BOOST_LOG(warning) << "Unable to find MAC address for "sv << address;
return "00:00:00:00:00:00"s;
}
input_t input() {
input_t result { new vigem_t {} };
auto vigem = (vigem_t*)result.get();
if(vigem->init()) {
return nullptr;
}
return result;
}
void move_mouse(input_t &input, int deltaX, int deltaY) {
INPUT i {};
i.type = INPUT_MOUSE;
auto &mi = i.mi;
mi.dwFlags = MOUSEEVENTF_MOVE;
mi.dx = deltaX;
mi.dy = deltaY;
auto send = SendInput(1, &i, sizeof(INPUT));
if(send != 1) {
BOOST_LOG(warning) << "Couldn't send mouse movement input"sv;
}
}
void button_mouse(input_t &input, int button, bool release) {
constexpr SHORT KEY_STATE_DOWN = 0x8000;
INPUT i {};
i.type = INPUT_MOUSE;
auto &mi = i.mi;
int mouse_button;
if(button == 1) {
mi.dwFlags = release ? MOUSEEVENTF_LEFTUP : MOUSEEVENTF_LEFTDOWN;
mouse_button = VK_LBUTTON;
}
else if(button == 2) {
mi.dwFlags = release ? MOUSEEVENTF_MIDDLEUP : MOUSEEVENTF_MIDDLEDOWN;
mouse_button = VK_MBUTTON;
}
else if(button == 3) {
mi.dwFlags = release ? MOUSEEVENTF_RIGHTUP : MOUSEEVENTF_RIGHTDOWN;
mouse_button = VK_RBUTTON;
}
else if(button == 4) {
mi.dwFlags = release ? MOUSEEVENTF_XUP : MOUSEEVENTF_XDOWN;
mi.mouseData = XBUTTON1;
mouse_button = VK_XBUTTON1;
}
else {
mi.dwFlags = release ? MOUSEEVENTF_XUP : MOUSEEVENTF_XDOWN;
mi.mouseData = XBUTTON2;
mouse_button = VK_XBUTTON2;
}
auto key_state = GetAsyncKeyState(mouse_button);
bool key_state_down = (key_state & KEY_STATE_DOWN) != 0;
if(key_state_down != release) {
BOOST_LOG(warning) << "Button state of mouse_button ["sv << button << "] does not match the desired state"sv;
return;
}
auto send = SendInput(1, &i, sizeof(INPUT));
if(send != 1) {
BOOST_LOG(warning) << "Couldn't send mouse button input"sv;
}
}
void scroll(input_t &input, int distance) {
INPUT i {};
i.type = INPUT_MOUSE;
auto &mi = i.mi;
mi.dwFlags = MOUSEEVENTF_WHEEL;
mi.mouseData = distance;
auto send = SendInput(1, &i, sizeof(INPUT));
if(send != 1) {
BOOST_LOG(warning) << "Couldn't send moue movement input"sv;
}
}
void keyboard(input_t &input, uint16_t modcode, bool release) {
constexpr SHORT KEY_STATE_DOWN = 0x8000;
if(modcode == VK_RMENU) {
modcode = VK_LWIN;
}
auto key_state = GetAsyncKeyState(modcode);
bool key_state_down = (key_state & KEY_STATE_DOWN) != 0;
if(key_state_down != release) {
BOOST_LOG(warning) << "Key state of vkey ["sv << util::hex(modcode).to_string_view() << "] does not match the desired state ["sv << (release ? "on]"sv : "off]"sv);
return;
}
INPUT i {};
i.type = INPUT_KEYBOARD;
auto &ki = i.ki;
// For some reason, MapVirtualKey(VK_LWIN, MAPVK_VK_TO_VSC) doesn't seem to work :/
if(modcode != VK_LWIN && modcode != VK_RWIN && modcode != VK_PAUSE) {
ki.wScan = MapVirtualKey(modcode, MAPVK_VK_TO_VSC);
ki.dwFlags = KEYEVENTF_SCANCODE;
}
else {
ki.wVk = modcode;
}
// https://docs.microsoft.com/en-us/windows/win32/inputdev/about-keyboard-input#keystroke-message-flags
switch(modcode) {
case VK_RMENU:
case VK_RCONTROL:
case VK_INSERT:
case VK_DELETE:
case VK_HOME:
case VK_END:
case VK_PRIOR:
case VK_NEXT:
case VK_UP:
case VK_DOWN:
case VK_LEFT:
case VK_RIGHT:
case VK_DIVIDE:
ki.dwFlags |= KEYEVENTF_EXTENDEDKEY;
break;
default:
break;
}
if(release) {
ki.dwFlags |= KEYEVENTF_KEYUP;
}
auto send = SendInput(1, &i, sizeof(INPUT));
if(send != 1) {
BOOST_LOG(warning) << "Couldn't send moue movement input"sv;
}
}
void gamepad(input_t &input, int nr, const gamepad_state_t &gamepad_state) {
// If there is no gamepad support
if(!input) {
return;
}
auto vigem = (vigem_t*)input.get();
auto &xusb = *(PXUSB_REPORT)&gamepad_state;
auto &x360 = vigem->x360s[nr];
auto status = vigem_target_x360_update(vigem->client.get(), x360.get(), xusb);
if(!VIGEM_SUCCESS(status)) {
BOOST_LOG(fatal) << "Couldn't send gamepad input to ViGEm ["sv << util::hex(status).to_string_view() << ']';
log_flush();
std::abort();
}
}
void freeInput(void *p) {
auto vigem = (vigem_t*)p;
delete vigem;
}
}

View File

@@ -0,0 +1,164 @@
// ----------------------------------------------------------------------------
// PolicyConfig.h
// Undocumented COM-interface IPolicyConfig.
// Use for set default audio render endpoint
// @author EreTIk
// http://eretik.omegahg.com/
// ----------------------------------------------------------------------------
#pragma once
#ifdef __MINGW32__
#undef DEFINE_GUID
#ifdef __cplusplus
#define DEFINE_GUID(name, l, w1, w2, b1, b2, b3, b4, b5, b6, b7, b8) EXTERN_C const GUID DECLSPEC_SELECTANY name = { l, w1, w2, { b1, b2, b3, b4, b5, b6, b7, b8 } }
#else
#define DEFINE_GUID(name, l, w1, w2, b1, b2, b3, b4, b5, b6, b7, b8) const GUID DECLSPEC_SELECTANY name = { l, w1, w2, { b1, b2, b3, b4, b5, b6, b7, b8 } }
#endif
DEFINE_GUID(IID_IPolicyConfig, 0xf8679f50, 0x850a, 0x41cf, 0x9c, 0x72, 0x43, 0x0f, 0x29, 0x02, 0x90, 0xc8);
DEFINE_GUID(CLSID_CPolicyConfigClient, 0x870af99c, 0x171d, 0x4f9e, 0xaf, 0x0d, 0xe6, 0x3d, 0xf4, 0x0c, 0x2b, 0xc9);
#endif
interface DECLSPEC_UUID("f8679f50-850a-41cf-9c72-430f290290c8") IPolicyConfig;
class DECLSPEC_UUID("870af99c-171d-4f9e-af0d-e63df40c2bc9") CPolicyConfigClient;
// ----------------------------------------------------------------------------
// class CPolicyConfigClient
// {870af99c-171d-4f9e-af0d-e63df40c2bc9}
//
// interface IPolicyConfig
// {f8679f50-850a-41cf-9c72-430f290290c8}
//
// Query interface:
// CComPtr<IPolicyConfig> PolicyConfig;
// PolicyConfig.CoCreateInstance(__uuidof(CPolicyConfigClient));
//
// @compatible: Windows 7 and Later
// ----------------------------------------------------------------------------
interface IPolicyConfig : public IUnknown {
public:
virtual HRESULT GetMixFormat(
PCWSTR,
WAVEFORMATEX **);
virtual HRESULT STDMETHODCALLTYPE GetDeviceFormat(
PCWSTR,
INT,
WAVEFORMATEX **);
virtual HRESULT STDMETHODCALLTYPE ResetDeviceFormat(
PCWSTR);
virtual HRESULT STDMETHODCALLTYPE SetDeviceFormat(
PCWSTR,
WAVEFORMATEX *,
WAVEFORMATEX *);
virtual HRESULT STDMETHODCALLTYPE GetProcessingPeriod(
PCWSTR,
INT,
PINT64,
PINT64);
virtual HRESULT STDMETHODCALLTYPE SetProcessingPeriod(
PCWSTR,
PINT64);
virtual HRESULT STDMETHODCALLTYPE GetShareMode(
PCWSTR,
struct DeviceShareMode *);
virtual HRESULT STDMETHODCALLTYPE SetShareMode(
PCWSTR,
struct DeviceShareMode *);
virtual HRESULT STDMETHODCALLTYPE GetPropertyValue(
PCWSTR,
const PROPERTYKEY &,
PROPVARIANT *);
virtual HRESULT STDMETHODCALLTYPE SetPropertyValue(
PCWSTR,
const PROPERTYKEY &,
PROPVARIANT *);
virtual HRESULT STDMETHODCALLTYPE SetDefaultEndpoint(
PCWSTR wszDeviceId,
ERole eRole);
virtual HRESULT STDMETHODCALLTYPE SetEndpointVisibility(
PCWSTR,
INT);
};
interface DECLSPEC_UUID("568b9108-44bf-40b4-9006-86afe5b5a620") IPolicyConfigVista;
class DECLSPEC_UUID("294935CE-F637-4E7C-A41B-AB255460B862") CPolicyConfigVistaClient;
// ----------------------------------------------------------------------------
// class CPolicyConfigVistaClient
// {294935CE-F637-4E7C-A41B-AB255460B862}
//
// interface IPolicyConfigVista
// {568b9108-44bf-40b4-9006-86afe5b5a620}
//
// Query interface:
// CComPtr<IPolicyConfigVista> PolicyConfig;
// PolicyConfig.CoCreateInstance(__uuidof(CPolicyConfigVistaClient));
//
// @compatible: Windows Vista and Later
// ----------------------------------------------------------------------------
interface IPolicyConfigVista : public IUnknown {
public:
virtual HRESULT GetMixFormat(
PCWSTR,
WAVEFORMATEX **); // not available on Windows 7, use method from IPolicyConfig
virtual HRESULT STDMETHODCALLTYPE GetDeviceFormat(
PCWSTR,
INT,
WAVEFORMATEX **);
virtual HRESULT STDMETHODCALLTYPE SetDeviceFormat(
PCWSTR,
WAVEFORMATEX *,
WAVEFORMATEX *);
virtual HRESULT STDMETHODCALLTYPE GetProcessingPeriod(
PCWSTR,
INT,
PINT64,
PINT64); // not available on Windows 7, use method from IPolicyConfig
virtual HRESULT STDMETHODCALLTYPE SetProcessingPeriod(
PCWSTR,
PINT64); // not available on Windows 7, use method from IPolicyConfig
virtual HRESULT STDMETHODCALLTYPE GetShareMode(
PCWSTR,
struct DeviceShareMode *); // not available on Windows 7, use method from IPolicyConfig
virtual HRESULT STDMETHODCALLTYPE SetShareMode(
PCWSTR,
struct DeviceShareMode *); // not available on Windows 7, use method from IPolicyConfig
virtual HRESULT STDMETHODCALLTYPE GetPropertyValue(
PCWSTR,
const PROPERTYKEY &,
PROPVARIANT *);
virtual HRESULT STDMETHODCALLTYPE SetPropertyValue(
PCWSTR,
const PROPERTYKEY &,
PROPVARIANT *);
virtual HRESULT STDMETHODCALLTYPE SetDefaultEndpoint(
PCWSTR wszDeviceId,
ERole eRole);
virtual HRESULT STDMETHODCALLTYPE SetEndpointVisibility(
PCWSTR,
INT); // not available on Windows 7, use method from IPolicyConfig
};
// ----------------------------------------------------------------------------

View File

@@ -0,0 +1,988 @@
//
// Created by loki on 1/12/20.
//
#include <audioclient.h>
#include <mmdeviceapi.h>
#include <roapi.h>
#include <codecvt>
#include <synchapi.h>
#define INITGUID
#include <propkeydef.h>
#undef INITGUID
#include "sunshine/config.h"
#include "sunshine/main.h"
#include "sunshine/platform/common.h"
// Must be the last included file
// clang-format off
#include "PolicyConfig.h"
// clang-format on
DEFINE_PROPERTYKEY(PKEY_Device_DeviceDesc, 0xa45c254e, 0xdf1c, 0x4efd, 0x80, 0x20, 0x67, 0xd1, 0x46, 0xa8, 0x50, 0xe0, 2); // DEVPROP_TYPE_STRING
DEFINE_PROPERTYKEY(PKEY_Device_FriendlyName, 0xa45c254e, 0xdf1c, 0x4efd, 0x80, 0x20, 0x67, 0xd1, 0x46, 0xa8, 0x50, 0xe0, 14); // DEVPROP_TYPE_STRING
DEFINE_PROPERTYKEY(PKEY_DeviceInterface_FriendlyName, 0x026e516e, 0xb814, 0x414b, 0x83, 0xcd, 0x85, 0x6d, 0x6f, 0xef, 0x48, 0x22, 2);
const CLSID CLSID_MMDeviceEnumerator = __uuidof(MMDeviceEnumerator);
const IID IID_IMMDeviceEnumerator = __uuidof(IMMDeviceEnumerator);
const IID IID_IAudioClient = __uuidof(IAudioClient);
const IID IID_IAudioCaptureClient = __uuidof(IAudioCaptureClient);
using namespace std::literals;
namespace platf::audio {
constexpr auto SAMPLE_RATE = 48000;
template<class T>
void Release(T *p) {
p->Release();
}
template<class T>
void co_task_free(T *p) {
CoTaskMemFree((LPVOID)p);
}
using device_enum_t = util::safe_ptr<IMMDeviceEnumerator, Release<IMMDeviceEnumerator>>;
using device_t = util::safe_ptr<IMMDevice, Release<IMMDevice>>;
using collection_t = util::safe_ptr<IMMDeviceCollection, Release<IMMDeviceCollection>>;
using audio_client_t = util::safe_ptr<IAudioClient, Release<IAudioClient>>;
using audio_capture_t = util::safe_ptr<IAudioCaptureClient, Release<IAudioCaptureClient>>;
using wave_format_t = util::safe_ptr<WAVEFORMATEX, co_task_free<WAVEFORMATEX>>;
using wstring_t = util::safe_ptr<WCHAR, co_task_free<WCHAR>>;
using handle_t = util::safe_ptr_v2<void, BOOL, CloseHandle>;
using policy_t = util::safe_ptr<IPolicyConfig, Release<IPolicyConfig>>;
using prop_t = util::safe_ptr<IPropertyStore, Release<IPropertyStore>>;
class co_init_t : public deinit_t {
public:
co_init_t() {
CoInitializeEx(nullptr, COINIT_MULTITHREADED | COINIT_SPEED_OVER_MEMORY);
}
~co_init_t() override {
CoUninitialize();
}
};
class prop_var_t {
public:
prop_var_t() {
PropVariantInit(&prop);
}
~prop_var_t() {
PropVariantClear(&prop);
}
PROPVARIANT prop;
};
class audio_pipe_t {
public:
static constexpr auto stereo = 2;
static constexpr auto channels51 = 6;
static constexpr auto channels71 = 8;
using samples_t = std::vector<std::int16_t>;
using buf_t = util::buffer_t<std::int16_t>;
virtual void to_stereo(samples_t &out, const buf_t &in) = 0;
virtual void to_51(samples_t &out, const buf_t &in) = 0;
virtual void to_71(samples_t &out, const buf_t &in) = 0;
};
class mono_t : public audio_pipe_t {
public:
void to_stereo(samples_t &out, const buf_t &in) override {
auto sample_in_pos = std::begin(in);
auto sample_end = std::begin(out) + out.size();
for(auto sample_out_p = std::begin(out); sample_out_p != sample_end;) {
*sample_out_p++ = *sample_in_pos * 7 / 10;
*sample_out_p++ = *sample_in_pos++ * 7 / 10;
}
}
void to_51(samples_t &out, const buf_t &in) override {
using namespace speaker;
auto sample_in_pos = std::begin(in);
auto sample_end = std::begin(out) + out.size();
for(auto sample_out_p = std::begin(out); sample_out_p != sample_end; sample_out_p += channels51) {
int left = *sample_in_pos++;
auto fl = (left * 7 / 10);
sample_out_p[FRONT_LEFT] = fl;
sample_out_p[FRONT_RIGHT] = fl;
sample_out_p[FRONT_CENTER] = fl * 6;
sample_out_p[LOW_FREQUENCY] = fl / 10;
sample_out_p[BACK_LEFT] = left * 4 / 10;
sample_out_p[BACK_RIGHT] = left * 4 / 10;
}
}
void to_71(samples_t &out, const buf_t &in) override {
using namespace speaker;
auto sample_in_pos = std::begin(in);
auto sample_end = std::begin(out) + out.size();
for(auto sample_out_p = std::begin(out); sample_out_p != sample_end; sample_out_p += channels71) {
int left = *sample_in_pos++;
auto fl = (left * 7 / 10);
sample_out_p[FRONT_LEFT] = fl;
sample_out_p[FRONT_RIGHT] = fl;
sample_out_p[FRONT_CENTER] = fl * 6;
sample_out_p[LOW_FREQUENCY] = fl / 10;
sample_out_p[BACK_LEFT] = left * 4 / 10;
sample_out_p[BACK_RIGHT] = left * 4 / 10;
sample_out_p[SIDE_LEFT] = left * 5 / 10;
sample_out_p[SIDE_RIGHT] = left * 5 / 10;
}
}
};
class stereo_t : public audio_pipe_t {
public:
void to_stereo(samples_t &out, const buf_t &in) override {
std::copy_n(std::begin(in), out.size(), std::begin(out));
}
void to_51(samples_t &out, const buf_t &in) override {
using namespace speaker;
auto sample_in_pos = std::begin(in);
auto sample_end = std::begin(out) + out.size();
for(auto sample_out_p = std::begin(out); sample_out_p != sample_end; sample_out_p += channels51) {
int left = sample_in_pos[speaker::FRONT_LEFT];
int right = sample_in_pos[speaker::FRONT_RIGHT];
sample_in_pos += 2;
auto fl = (left * 7 / 10);
auto fr = (right * 7 / 10);
auto mix = (fl + fr) / 2;
sample_out_p[FRONT_LEFT] = fl;
sample_out_p[FRONT_RIGHT] = fr;
sample_out_p[FRONT_CENTER] = mix;
sample_out_p[LOW_FREQUENCY] = mix / 2;
sample_out_p[BACK_LEFT] = left * 4 / 10;
sample_out_p[BACK_RIGHT] = right * 4 / 10;
}
}
void to_71(samples_t &out, const buf_t &in) override {
using namespace speaker;
auto sample_in_pos = std::begin(in);
auto sample_end = std::begin(out) + out.size();
for(auto sample_out_p = std::begin(out); sample_out_p != sample_end; sample_out_p += channels71) {
int left = sample_in_pos[speaker::FRONT_LEFT];
int right = sample_in_pos[speaker::FRONT_RIGHT];
sample_in_pos += 2;
auto fl = (left * 7 / 10);
auto fr = (right * 7 / 10);
auto mix = (fl + fr) / 2;
sample_out_p[FRONT_LEFT] = fl;
sample_out_p[FRONT_RIGHT] = fr;
sample_out_p[FRONT_CENTER] = mix;
sample_out_p[LOW_FREQUENCY] = mix / 2;
sample_out_p[BACK_LEFT] = left * 4 / 10;
sample_out_p[BACK_RIGHT] = right * 4 / 10;
sample_out_p[SIDE_LEFT] = left * 5 / 10;
sample_out_p[SIDE_RIGHT] = right * 5 / 10;
}
}
};
class surr51_t : public audio_pipe_t {
public:
void to_stereo(samples_t &out, const buf_t &in) {
using namespace speaker;
auto sample_in_pos = std::begin(in);
auto sample_end = std::begin(out) + out.size();
for(auto sample_out_p = std::begin(out); sample_out_p != sample_end; sample_out_p += stereo) {
int left {}, right {};
left += sample_in_pos[FRONT_LEFT];
left += sample_in_pos[FRONT_CENTER] * 9 / 10;
left += sample_in_pos[LOW_FREQUENCY] * 3 / 10;
left += sample_in_pos[BACK_LEFT] * 7 / 10;
left += sample_in_pos[BACK_RIGHT] * 3 / 10;
right += sample_in_pos[FRONT_RIGHT];
right += sample_in_pos[FRONT_CENTER] * 9 / 10;
right += sample_in_pos[LOW_FREQUENCY] * 3 / 10;
right += sample_in_pos[BACK_LEFT] * 3 / 10;
right += sample_in_pos[BACK_RIGHT] * 7 / 10;
sample_out_p[0] = left;
sample_out_p[1] = right;
sample_in_pos += channels51;
}
}
void to_51(samples_t &out, const buf_t &in) override {
std::copy_n(std::begin(in), out.size(), std::begin(out));
}
void to_71(samples_t &out, const buf_t &in) override {
using namespace speaker;
auto sample_in_pos = std::begin(in);
auto sample_end = std::begin(out) + out.size();
for(auto sample_out_p = std::begin(out); sample_out_p != sample_end; sample_out_p += channels71) {
int fl = sample_in_pos[FRONT_LEFT];
int fr = sample_in_pos[FRONT_RIGHT];
int bl = sample_in_pos[BACK_LEFT];
int br = sample_in_pos[BACK_RIGHT];
auto mix_l = (fl + bl) / 2;
auto mix_r = (bl + br) / 2;
sample_out_p[FRONT_LEFT] = fl;
sample_out_p[FRONT_RIGHT] = fr;
sample_out_p[FRONT_CENTER] = sample_in_pos[FRONT_CENTER];
sample_out_p[LOW_FREQUENCY] = sample_in_pos[LOW_FREQUENCY];
sample_out_p[BACK_LEFT] = bl;
sample_out_p[BACK_RIGHT] = br;
sample_out_p[SIDE_LEFT] = mix_l;
sample_out_p[SIDE_RIGHT] = mix_r;
sample_in_pos += channels51;
}
}
};
class surr71_t : public audio_pipe_t {
public:
void to_stereo(samples_t &out, const buf_t &in) {
using namespace speaker;
auto sample_in_pos = std::begin(in);
auto sample_end = std::begin(out) + out.size();
for(auto sample_out_p = std::begin(out); sample_out_p != sample_end; sample_out_p += stereo) {
int left {}, right {};
left += sample_in_pos[FRONT_LEFT];
left += sample_in_pos[FRONT_CENTER] * 9 / 10;
left += sample_in_pos[LOW_FREQUENCY] * 3 / 10;
left += sample_in_pos[BACK_LEFT] * 7 / 10;
left += sample_in_pos[BACK_RIGHT] * 3 / 10;
left += sample_in_pos[SIDE_LEFT];
right += sample_in_pos[FRONT_RIGHT];
right += sample_in_pos[FRONT_CENTER] * 9 / 10;
right += sample_in_pos[LOW_FREQUENCY] * 3 / 10;
right += sample_in_pos[BACK_LEFT] * 3 / 10;
right += sample_in_pos[BACK_RIGHT] * 7 / 10;
right += sample_in_pos[SIDE_RIGHT];
sample_out_p[0] = left;
sample_out_p[1] = right;
sample_in_pos += channels71;
}
}
void to_51(samples_t &out, const buf_t &in) override {
using namespace speaker;
auto sample_in_pos = std::begin(in);
auto sample_end = std::begin(out) + out.size();
for(auto sample_out_p = std::begin(out); sample_out_p != sample_end; sample_out_p += channels51) {
auto sl = (int)sample_out_p[SIDE_LEFT] * 3 / 10;
auto sr = (int)sample_out_p[SIDE_RIGHT] * 3 / 10;
sample_out_p[FRONT_LEFT] = sample_in_pos[FRONT_LEFT] + sl;
sample_out_p[FRONT_RIGHT] = sample_in_pos[FRONT_RIGHT] + sr;
sample_out_p[FRONT_CENTER] = sample_in_pos[FRONT_CENTER];
sample_out_p[LOW_FREQUENCY] = sample_in_pos[LOW_FREQUENCY];
sample_out_p[BACK_LEFT] = sample_in_pos[BACK_LEFT] + sl;
sample_out_p[BACK_RIGHT] = sample_in_pos[BACK_RIGHT] + sr;
sample_in_pos += channels71;
}
}
void to_71(samples_t &out, const buf_t &in) override {
std::copy_n(std::begin(in), out.size(), std::begin(out));
}
};
static std::wstring_convert<std::codecvt_utf8_utf16<wchar_t>, wchar_t> converter;
struct format_t {
enum type_e : int {
none,
mono,
stereo,
surr51,
surr71,
} type;
std::string_view name;
int channels;
int channel_mask;
} formats[] {
{
format_t::mono,
"Mono"sv,
1,
SPEAKER_FRONT_CENTER,
},
{
format_t::stereo,
"Stereo"sv,
2,
SPEAKER_FRONT_LEFT | SPEAKER_FRONT_RIGHT,
},
{
format_t::surr51,
"Surround 5.1"sv,
6,
SPEAKER_FRONT_LEFT |
SPEAKER_FRONT_RIGHT |
SPEAKER_FRONT_CENTER |
SPEAKER_LOW_FREQUENCY |
SPEAKER_BACK_LEFT |
SPEAKER_BACK_RIGHT,
},
{
format_t::surr71,
"Surround 7.1"sv,
8,
SPEAKER_FRONT_LEFT |
SPEAKER_FRONT_RIGHT |
SPEAKER_FRONT_CENTER |
SPEAKER_LOW_FREQUENCY |
SPEAKER_BACK_LEFT |
SPEAKER_BACK_RIGHT |
SPEAKER_SIDE_LEFT |
SPEAKER_SIDE_RIGHT,
},
};
static format_t surround_51_side_speakers {
format_t::surr51,
"Surround 5.1"sv,
6,
SPEAKER_FRONT_LEFT |
SPEAKER_FRONT_RIGHT |
SPEAKER_FRONT_CENTER |
SPEAKER_LOW_FREQUENCY |
SPEAKER_SIDE_LEFT |
SPEAKER_SIDE_RIGHT,
};
void set_wave_format(audio::wave_format_t &wave_format, const format_t &format) {
wave_format->nChannels = format.channels;
wave_format->nBlockAlign = wave_format->nChannels * wave_format->wBitsPerSample / 8;
wave_format->nAvgBytesPerSec = wave_format->nSamplesPerSec * wave_format->nBlockAlign;
if(wave_format->wFormatTag == WAVE_FORMAT_EXTENSIBLE) {
((PWAVEFORMATEXTENSIBLE)wave_format.get())->dwChannelMask = format.channel_mask;
}
}
int init_wave_format(audio::wave_format_t &wave_format, DWORD sample_rate) {
wave_format->wBitsPerSample = 16;
wave_format->nSamplesPerSec = sample_rate;
switch(wave_format->wFormatTag) {
case WAVE_FORMAT_PCM:
break;
case WAVE_FORMAT_IEEE_FLOAT:
break;
case WAVE_FORMAT_EXTENSIBLE: {
auto wave_ex = (PWAVEFORMATEXTENSIBLE)wave_format.get();
if(IsEqualGUID(KSDATAFORMAT_SUBTYPE_IEEE_FLOAT, wave_ex->SubFormat)) {
wave_ex->Samples.wValidBitsPerSample = 16;
wave_ex->SubFormat = KSDATAFORMAT_SUBTYPE_PCM;
break;
}
BOOST_LOG(error) << "Unsupported Sub Format for WAVE_FORMAT_EXTENSIBLE: [0x"sv << util::hex(wave_ex->SubFormat).to_string_view() << ']';
}
default:
BOOST_LOG(error) << "Unsupported Wave Format: [0x"sv << util::hex(wave_format->wFormatTag).to_string_view() << ']';
return -1;
};
return 0;
}
audio_client_t make_audio_client(device_t &device, const format_t &format, int sample_rate) {
audio_client_t audio_client;
auto status = device->Activate(
IID_IAudioClient,
CLSCTX_ALL,
nullptr,
(void **)&audio_client);
if(FAILED(status)) {
BOOST_LOG(error) << "Couldn't activate Device: [0x"sv << util::hex(status).to_string_view() << ']';
return nullptr;
}
wave_format_t wave_format;
status = audio_client->GetMixFormat(&wave_format);
if(FAILED(status)) {
BOOST_LOG(error) << "Couldn't acquire Wave Format [0x"sv << util::hex(status).to_string_view() << ']';
return nullptr;
}
if(init_wave_format(wave_format, sample_rate)) {
return nullptr;
}
set_wave_format(wave_format, format);
status = audio_client->Initialize(
AUDCLNT_SHAREMODE_SHARED,
AUDCLNT_STREAMFLAGS_LOOPBACK | AUDCLNT_STREAMFLAGS_EVENTCALLBACK,
0, 0,
wave_format.get(),
nullptr);
if(status) {
BOOST_LOG(debug) << "Couldn't initialize audio client for ["sv << format.name << "]: [0x"sv << util::hex(status).to_string_view() << ']';
return nullptr;
}
return audio_client;
}
const wchar_t *no_null(const wchar_t *str) {
return str ? str : L"Unknown";
}
format_t::type_e validate_device(device_t &device, int sample_rate) {
for(const auto &format : formats) {
// Ensure WaveFromat is compatible
auto audio_client = make_audio_client(device, format, sample_rate);
BOOST_LOG(debug) << format.name << ": "sv << (!audio_client ? "unsupported"sv : "supported"sv);
if(audio_client) {
return format.type;
}
}
return format_t::none;
}
device_t default_device(device_enum_t &device_enum) {
device_t device;
HRESULT status;
status = device_enum->GetDefaultAudioEndpoint(
eRender,
eConsole,
&device);
if(FAILED(status)) {
BOOST_LOG(error) << "Couldn't create audio Device [0x"sv << util::hex(status).to_string_view() << ']';
return nullptr;
}
return device;
}
class mic_wasapi_t : public mic_t {
public:
capture_e sample(std::vector<std::int16_t> &sample_out) override {
auto sample_size = sample_out.size() / channels_out * channels_in;
while(sample_buf_pos - std::begin(sample_buf) < sample_size) {
//FIXME: Use IAudioClient3 instead of IAudioClient, that would allows for adjusting the latency of the audio samples
auto capture_result = _fill_buffer();
if(capture_result != capture_e::ok) {
return capture_result;
}
}
switch(channels_out) {
case 2:
pipe->to_stereo(sample_out, sample_buf);
break;
case 6:
pipe->to_51(sample_out, sample_buf);
break;
case 8:
pipe->to_71(sample_out, sample_buf);
break;
default:
BOOST_LOG(error) << "converting to ["sv << channels_out << "] channels is not supported"sv;
return capture_e::error;
}
// The excess samples should be in front of the queue
std::move(&sample_buf[sample_size], sample_buf_pos, std::begin(sample_buf));
sample_buf_pos -= sample_size;
return capture_e::ok;
}
int init(std::uint32_t sample_rate, std::uint32_t frame_size, std::uint32_t channels_out) {
audio_event.reset(CreateEventA(nullptr, FALSE, FALSE, nullptr));
if(!audio_event) {
BOOST_LOG(error) << "Couldn't create Event handle"sv;
return -1;
}
HRESULT status;
status = CoCreateInstance(
CLSID_MMDeviceEnumerator,
nullptr,
CLSCTX_ALL,
IID_IMMDeviceEnumerator,
(void **)&device_enum);
if(FAILED(status)) {
BOOST_LOG(error) << "Couldn't create Device Enumerator [0x"sv << util::hex(status).to_string_view() << ']';
return -1;
}
auto device = default_device(device_enum);
if(!device) {
return -1;
}
for(auto &format : formats) {
BOOST_LOG(debug) << "Trying audio format ["sv << format.name << ']';
audio_client = make_audio_client(device, format, sample_rate);
if(audio_client) {
BOOST_LOG(debug) << "Found audio format ["sv << format.name << ']';
channels_in = format.channels;
this->channels_out = channels_out;
switch(channels_in) {
case 1:
pipe = std::make_unique<mono_t>();
break;
case 2:
pipe = std::make_unique<stereo_t>();
break;
case 6:
pipe = std::make_unique<surr51_t>();
break;
case 8:
pipe = std::make_unique<surr71_t>();
break;
default:
BOOST_LOG(error) << "converting from ["sv << channels_in << "] channels is not supported"sv;
return -1;
}
break;
}
}
if(!audio_client) {
BOOST_LOG(error) << "Couldn't find supported format for audio"sv;
return -1;
}
REFERENCE_TIME default_latency;
audio_client->GetDevicePeriod(&default_latency, nullptr);
default_latency_ms = default_latency / 1000;
std::uint32_t frames;
status = audio_client->GetBufferSize(&frames);
if(FAILED(status)) {
BOOST_LOG(error) << "Couldn't acquire the number of audio frames [0x"sv << util::hex(status).to_string_view() << ']';
return -1;
}
// *2 --> needs to fit double
sample_buf = util::buffer_t<std::int16_t> { std::max(frames, frame_size) * 2 * channels_in };
sample_buf_pos = std::begin(sample_buf);
status = audio_client->GetService(IID_IAudioCaptureClient, (void **)&audio_capture);
if(FAILED(status)) {
BOOST_LOG(error) << "Couldn't initialize audio capture client [0x"sv << util::hex(status).to_string_view() << ']';
return -1;
}
status = audio_client->SetEventHandle(audio_event.get());
if(FAILED(status)) {
BOOST_LOG(error) << "Couldn't set event handle [0x"sv << util::hex(status).to_string_view() << ']';
return -1;
}
status = audio_client->Start();
if(FAILED(status)) {
BOOST_LOG(error) << "Couldn't start recording [0x"sv << util::hex(status).to_string_view() << ']';
return -1;
}
return 0;
}
~mic_wasapi_t() override {
if(audio_client) {
audio_client->Stop();
}
}
private:
capture_e _fill_buffer() {
HRESULT status;
// Total number of samples
struct sample_aligned_t {
std::uint32_t uninitialized;
std::int16_t *samples;
} sample_aligned;
// number of samples / number of channels
struct block_aligned_t {
std::uint32_t audio_sample_size;
} block_aligned;
status = WaitForSingleObjectEx(audio_event.get(), default_latency_ms, FALSE);
switch(status) {
case WAIT_OBJECT_0:
break;
case WAIT_TIMEOUT:
return capture_e::timeout;
default:
BOOST_LOG(error) << "Couldn't wait for audio event: [0x"sv << util::hex(status).to_string_view() << ']';
return capture_e::error;
}
std::uint32_t packet_size {};
for(
status = audio_capture->GetNextPacketSize(&packet_size);
SUCCEEDED(status) && packet_size > 0;
status = audio_capture->GetNextPacketSize(&packet_size)) {
DWORD buffer_flags;
status = audio_capture->GetBuffer(
(BYTE **)&sample_aligned.samples,
&block_aligned.audio_sample_size,
&buffer_flags,
nullptr, nullptr);
switch(status) {
case S_OK:
break;
case AUDCLNT_E_DEVICE_INVALIDATED:
return capture_e::reinit;
default:
BOOST_LOG(error) << "Couldn't capture audio [0x"sv << util::hex(status).to_string_view() << ']';
return capture_e::error;
}
sample_aligned.uninitialized = std::end(sample_buf) - sample_buf_pos;
auto n = std::min(sample_aligned.uninitialized, block_aligned.audio_sample_size * channels_in);
if(buffer_flags & AUDCLNT_BUFFERFLAGS_SILENT) {
std::fill_n(sample_buf_pos, n, 0);
}
else {
std::copy_n(sample_aligned.samples, n, sample_buf_pos);
}
sample_buf_pos += n;
audio_capture->ReleaseBuffer(block_aligned.audio_sample_size);
}
if(status == AUDCLNT_E_DEVICE_INVALIDATED) {
return capture_e::reinit;
}
if(FAILED(status)) {
return capture_e::error;
}
return capture_e::ok;
}
public:
handle_t audio_event;
device_enum_t device_enum;
device_t device;
audio_client_t audio_client;
audio_capture_t audio_capture;
REFERENCE_TIME default_latency_ms;
util::buffer_t<std::int16_t> sample_buf;
std::int16_t *sample_buf_pos;
// out --> our audio output
int channels_out;
// in --> our wasapi input
int channels_in;
std::unique_ptr<audio_pipe_t> pipe;
};
class audio_control_t : public ::platf::audio_control_t {
public:
std::optional<sink_t> sink_info() override {
auto virtual_adapter_name = L"Steam Streaming Speakers"sv;
sink_t sink;
audio::device_enum_t device_enum;
auto status = CoCreateInstance(
CLSID_MMDeviceEnumerator,
nullptr,
CLSCTX_ALL,
IID_IMMDeviceEnumerator,
(void **)&device_enum);
if(FAILED(status)) {
BOOST_LOG(error) << "Couldn't create Device Enumerator: [0x"sv << util::hex(status).to_string_view() << ']';
return std::nullopt;
}
auto device = default_device(device_enum);
if(!device) {
return std::nullopt;
}
audio::wstring_t wstring;
device->GetId(&wstring);
sink.host = converter.to_bytes(wstring.get());
collection_t collection;
status = device_enum->EnumAudioEndpoints(eRender, DEVICE_STATE_ACTIVE, &collection);
if(FAILED(status)) {
BOOST_LOG(error) << "Couldn't enumerate: [0x"sv << util::hex(status).to_string_view() << ']';
return std::nullopt;
}
UINT count;
collection->GetCount(&count);
std::string virtual_device_id = config::audio.virtual_sink;
for(auto x = 0; x < count; ++x) {
audio::device_t device;
collection->Item(x, &device);
auto type = validate_device(device, SAMPLE_RATE);
if(type == format_t::none) {
continue;
}
audio::wstring_t wstring;
device->GetId(&wstring);
audio::prop_t prop;
device->OpenPropertyStore(STGM_READ, &prop);
prop_var_t adapter_friendly_name;
prop_var_t device_friendly_name;
prop_var_t device_desc;
prop->GetValue(PKEY_Device_FriendlyName, &device_friendly_name.prop);
prop->GetValue(PKEY_DeviceInterface_FriendlyName, &adapter_friendly_name.prop);
prop->GetValue(PKEY_Device_DeviceDesc, &device_desc.prop);
auto adapter_name = no_null((LPWSTR)adapter_friendly_name.prop.pszVal);
BOOST_LOG(verbose)
<< L"===== Device ====="sv << std::endl
<< L"Device ID : "sv << wstring.get() << std::endl
<< L"Device name : "sv << no_null((LPWSTR)device_friendly_name.prop.pszVal) << std::endl
<< L"Adapter name : "sv << adapter_name << std::endl
<< L"Device description : "sv << no_null((LPWSTR)device_desc.prop.pszVal) << std::endl
<< std::endl;
if(virtual_device_id.empty() && adapter_name == virtual_adapter_name) {
virtual_device_id = converter.to_bytes(wstring.get());
}
}
if(!virtual_device_id.empty()) {
sink.null = std::make_optional(sink_t::null_t {
"virtual-"s.append(formats[format_t::stereo - 1].name) + virtual_device_id,
"virtual-"s.append(formats[format_t::surr51 - 1].name) + virtual_device_id,
"virtual-"s.append(formats[format_t::surr71 - 1].name) + virtual_device_id,
});
}
return sink;
}
std::unique_ptr<mic_t> microphone(const std::uint8_t *mapping, int channels, std::uint32_t sample_rate, std::uint32_t frame_size) override {
auto mic = std::make_unique<mic_wasapi_t>();
if(mic->init(sample_rate, frame_size, channels)) {
return nullptr;
}
return mic;
}
/**
* If the requested sink is a virtual sink, meaning no speakers attached to
* the host, then we can seamlessly set the format to stereo and surround sound.
*
* Any virtual sink detected will be prefixed by:
* virtual-(format name)
* If it doesn't contain that prefix, then the format will not be changed
*/
std::optional<std::wstring> set_format(const std::string &sink) {
std::string_view sv { sink.c_str(), sink.size() };
format_t::type_e type = format_t::none;
// sink format:
// [virtual-(format name)]device_id
auto prefix = "virtual-"sv;
if(sv.find(prefix) == 0) {
sv = sv.substr(prefix.size(), sv.size() - prefix.size());
for(auto &format : formats) {
auto &name = format.name;
if(sv.find(name) == 0) {
type = format.type;
sv = sv.substr(name.size(), sv.size() - name.size());
break;
}
}
}
auto wstring_device_id = converter.from_bytes(sv.data());
if(type == format_t::none) {
// wstring_device_id does not contain virtual-(format name)
// It's a simple deviceId, just pass it back
return std::make_optional(std::move(wstring_device_id));
}
wave_format_t wave_format;
auto status = policy->GetMixFormat(wstring_device_id.c_str(), &wave_format);
if(FAILED(status)) {
BOOST_LOG(error) << "Couldn't acquire Wave Format [0x"sv << util::hex(status).to_string_view() << ']';
return std::nullopt;
}
if(init_wave_format(wave_format, SAMPLE_RATE)) {
return std::nullopt;
}
set_wave_format(wave_format, formats[(int)type - 1]);
WAVEFORMATEXTENSIBLE p {};
status = policy->SetDeviceFormat(wstring_device_id.c_str(), wave_format.get(), (WAVEFORMATEX *)&p);
// Surround 5.1 might contain side-{left, right} instead of speaker in the back
// Try again with different speaker mask.
if(status == 0x88890008 && type == format_t::surr51) {
set_wave_format(wave_format, surround_51_side_speakers);
status = policy->SetDeviceFormat(wstring_device_id.c_str(), wave_format.get(), (WAVEFORMATEX *)&p);
}
if(FAILED(status)) {
BOOST_LOG(error) << "Couldn't set Wave Format [0x"sv << util::hex(status).to_string_view() << ']';
return std::nullopt;
}
return std::make_optional(std::move(wstring_device_id));
}
int set_sink(const std::string &sink) override {
auto wstring_device_id = set_format(sink);
if(!wstring_device_id) {
return -1;
}
int failure {};
for(int x = 0; x < (int)ERole_enum_count; ++x) {
auto status = policy->SetDefaultEndpoint(wstring_device_id->c_str(), (ERole)x);
if(status) {
BOOST_LOG(warning) << "Couldn't set ["sv << sink << "] to role ["sv << x << ']';
++failure;
}
}
return failure;
}
int init() {
auto status = CoCreateInstance(
CLSID_CPolicyConfigClient,
nullptr,
CLSCTX_ALL,
IID_IPolicyConfig,
(void **)&policy);
if(FAILED(status)) {
BOOST_LOG(error) << "Couldn't create audio policy config: [0x"sv << util::hex(status).to_string_view() << ']';
return -1;
}
return 0;
}
~audio_control_t() override {}
policy_t policy;
};
} // namespace platf::audio
namespace platf {
// It's not big enough to justify it's own source file :/
namespace dxgi {
int init();
}
std::unique_ptr<audio_control_t> audio_control() {
auto control = std::make_unique<audio::audio_control_t>();
if(control->init()) {
return nullptr;
}
return control;
}
std::unique_ptr<deinit_t> init() {
if(dxgi::init()) {
return nullptr;
}
return std::make_unique<platf::audio::co_init_t>();
}
} // namespace platf

View File

@@ -0,0 +1,128 @@
//
// Created by loki on 4/23/20.
//
#ifndef SUNSHINE_DISPLAY_H
#define SUNSHINE_DISPLAY_H
#include <d3d11.h>
#include <d3d11_4.h>
#include <d3dcommon.h>
#include <dxgi.h>
#include <dxgi1_2.h>
#include "sunshine/platform/common.h"
#include "sunshine/utility.h"
namespace platf::dxgi {
extern const char *format_str[];
template<class T>
void Release(T *dxgi) {
dxgi->Release();
}
using factory1_t = util::safe_ptr<IDXGIFactory1, Release<IDXGIFactory1>>;
using dxgi_t = util::safe_ptr<IDXGIDevice, Release<IDXGIDevice>>;
using dxgi1_t = util::safe_ptr<IDXGIDevice1, Release<IDXGIDevice1>>;
using device_t = util::safe_ptr<ID3D11Device, Release<ID3D11Device>>;
using device_ctx_t = util::safe_ptr<ID3D11DeviceContext, Release<ID3D11DeviceContext>>;
using adapter_t = util::safe_ptr<IDXGIAdapter1, Release<IDXGIAdapter1>>;
using output_t = util::safe_ptr<IDXGIOutput, Release<IDXGIOutput>>;
using output1_t = util::safe_ptr<IDXGIOutput1, Release<IDXGIOutput1>>;
using dup_t = util::safe_ptr<IDXGIOutputDuplication, Release<IDXGIOutputDuplication>>;
using texture2d_t = util::safe_ptr<ID3D11Texture2D, Release<ID3D11Texture2D>>;
using texture1d_t = util::safe_ptr<ID3D11Texture1D, Release<ID3D11Texture1D>>;
using resource_t = util::safe_ptr<IDXGIResource, Release<IDXGIResource>>;
using multithread_t = util::safe_ptr<ID3D11Multithread, Release<ID3D11Multithread>>;
namespace video {
using device_t = util::safe_ptr<ID3D11VideoDevice, Release<ID3D11VideoDevice>>;
using ctx_t = util::safe_ptr<ID3D11VideoContext, Release<ID3D11VideoContext>>;
using processor_t = util::safe_ptr<ID3D11VideoProcessor, Release<ID3D11VideoProcessor>>;
using processor_out_t = util::safe_ptr<ID3D11VideoProcessorOutputView, Release<ID3D11VideoProcessorOutputView>>;
using processor_in_t = util::safe_ptr<ID3D11VideoProcessorInputView, Release<ID3D11VideoProcessorInputView>>;
using processor_enum_t = util::safe_ptr<ID3D11VideoProcessorEnumerator, Release<ID3D11VideoProcessorEnumerator>>;
} // namespace video
class hwdevice_t;
struct cursor_t {
std::vector<std::uint8_t> img_data;
DXGI_OUTDUPL_POINTER_SHAPE_INFO shape_info;
int x, y;
bool visible;
};
struct gpu_cursor_t {
texture2d_t texture;
LONG width, height;
};
class duplication_t {
public:
dup_t dup;
bool has_frame {};
capture_e next_frame(DXGI_OUTDUPL_FRAME_INFO &frame_info, std::chrono::milliseconds timeout, resource_t::pointer *res_p);
capture_e reset(dup_t::pointer dup_p = dup_t::pointer());
capture_e release_frame();
~duplication_t();
};
class display_base_t : public display_t {
public:
int init();
factory1_t factory;
adapter_t adapter;
output_t output;
device_t device;
device_ctx_t device_ctx;
duplication_t dup;
DXGI_FORMAT format;
D3D_FEATURE_LEVEL feature_level;
typedef enum _D3DKMT_SCHEDULINGPRIORITYCLASS {
D3DKMT_SCHEDULINGPRIORITYCLASS_IDLE,
D3DKMT_SCHEDULINGPRIORITYCLASS_BELOW_NORMAL,
D3DKMT_SCHEDULINGPRIORITYCLASS_NORMAL,
D3DKMT_SCHEDULINGPRIORITYCLASS_ABOVE_NORMAL,
D3DKMT_SCHEDULINGPRIORITYCLASS_HIGH,
D3DKMT_SCHEDULINGPRIORITYCLASS_REALTIME
} D3DKMT_SCHEDULINGPRIORITYCLASS;
typedef NTSTATUS WINAPI (*PD3DKMTSetProcessSchedulingPriorityClass)(HANDLE, D3DKMT_SCHEDULINGPRIORITYCLASS);
};
class display_ram_t : public display_base_t {
public:
capture_e snapshot(img_t *img, std::chrono::milliseconds timeout, bool cursor_visible) override;
std::shared_ptr<img_t> alloc_img() override;
int dummy_img(img_t *img) override;
int init();
cursor_t cursor;
D3D11_MAPPED_SUBRESOURCE img_info;
texture2d_t texture;
};
class display_vram_t : public display_base_t, public std::enable_shared_from_this<display_vram_t> {
public:
capture_e snapshot(img_t *img, std::chrono::milliseconds timeout, bool cursor_visible) override;
std::shared_ptr<img_t> alloc_img() override;
int dummy_img(img_t *img_base) override;
std::shared_ptr<platf::hwdevice_t> make_hwdevice(pix_fmt_e pix_fmt) override;
gpu_cursor_t cursor;
std::vector<hwdevice_t *> hwdevices;
};
} // namespace platf::dxgi
#endif

View File

@@ -0,0 +1,449 @@
//
// Created by loki on 1/12/20.
//
#include <codecvt>
#include "sunshine/config.h"
#include "sunshine/main.h"
#include "sunshine/platform/common.h"
#include "display.h"
namespace platf {
using namespace std::literals;
}
namespace platf::dxgi {
capture_e duplication_t::next_frame(DXGI_OUTDUPL_FRAME_INFO &frame_info, std::chrono::milliseconds timeout, resource_t::pointer *res_p) {
auto capture_status = release_frame();
if(capture_status != capture_e::ok) {
return capture_status;
}
auto status = dup->AcquireNextFrame(timeout.count(), &frame_info, res_p);
switch(status) {
case S_OK:
has_frame = true;
return capture_e::ok;
case DXGI_ERROR_WAIT_TIMEOUT:
return capture_e::timeout;
case WAIT_ABANDONED:
case DXGI_ERROR_ACCESS_LOST:
case DXGI_ERROR_ACCESS_DENIED:
return capture_e::reinit;
default:
BOOST_LOG(error) << "Couldn't acquire next frame [0x"sv << util::hex(status).to_string_view();
return capture_e::error;
}
}
capture_e duplication_t::reset(dup_t::pointer dup_p) {
auto capture_status = release_frame();
dup.reset(dup_p);
return capture_status;
}
capture_e duplication_t::release_frame() {
if(!has_frame) {
return capture_e::ok;
}
auto status = dup->ReleaseFrame();
switch(status) {
case S_OK:
has_frame = false;
return capture_e::ok;
case DXGI_ERROR_WAIT_TIMEOUT:
return capture_e::timeout;
case WAIT_ABANDONED:
case DXGI_ERROR_ACCESS_LOST:
case DXGI_ERROR_ACCESS_DENIED:
has_frame = false;
return capture_e::reinit;
default:
BOOST_LOG(error) << "Couldn't release frame [0x"sv << util::hex(status).to_string_view();
return capture_e::error;
}
}
duplication_t::~duplication_t() {
release_frame();
}
int display_base_t::init() {
/* Uncomment when use of IDXGIOutput5 is implemented
std::call_once(windows_cpp_once_flag, []() {
DECLARE_HANDLE(DPI_AWARENESS_CONTEXT);
const auto DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2 = ((DPI_AWARENESS_CONTEXT)-4);
typedef BOOL (*User32_SetProcessDpiAwarenessContext)(DPI_AWARENESS_CONTEXT value);
auto user32 = LoadLibraryA("user32.dll");
auto f = (User32_SetProcessDpiAwarenessContext)GetProcAddress(user32, "SetProcessDpiAwarenessContext");
if(f) {
f(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2);
}
FreeLibrary(user32);
});
*/
// Get rectangle of full desktop for absolute mouse coordinates
env_width = GetSystemMetrics(SM_CXVIRTUALSCREEN);
env_height = GetSystemMetrics(SM_CYVIRTUALSCREEN);
HRESULT status;
status = CreateDXGIFactory1(IID_IDXGIFactory1, (void **)&factory);
if(FAILED(status)) {
BOOST_LOG(error) << "Failed to create DXGIFactory1 [0x"sv << util::hex(status).to_string_view() << ']';
return -1;
}
std::wstring_convert<std::codecvt_utf8_utf16<wchar_t>, wchar_t> converter;
auto adapter_name = converter.from_bytes(config::video.adapter_name);
auto output_name = converter.from_bytes(config::video.output_name);
adapter_t::pointer adapter_p;
for(int x = 0; factory->EnumAdapters1(x, &adapter_p) != DXGI_ERROR_NOT_FOUND; ++x) {
dxgi::adapter_t adapter_tmp { adapter_p };
DXGI_ADAPTER_DESC1 adapter_desc;
adapter_tmp->GetDesc1(&adapter_desc);
if(!adapter_name.empty() && adapter_desc.Description != adapter_name) {
continue;
}
dxgi::output_t::pointer output_p;
for(int y = 0; adapter_tmp->EnumOutputs(y, &output_p) != DXGI_ERROR_NOT_FOUND; ++y) {
dxgi::output_t output_tmp { output_p };
DXGI_OUTPUT_DESC desc;
output_tmp->GetDesc(&desc);
if(!output_name.empty() && desc.DeviceName != output_name) {
continue;
}
if(desc.AttachedToDesktop) {
output = std::move(output_tmp);
offset_x = desc.DesktopCoordinates.left;
offset_y = desc.DesktopCoordinates.top;
width = desc.DesktopCoordinates.right - offset_x;
height = desc.DesktopCoordinates.bottom - offset_y;
// left and bottom may be negative, yet absolute mouse coordinates start at 0x0
// Ensure offset starts at 0x0
offset_x -= GetSystemMetrics(SM_XVIRTUALSCREEN);
offset_y -= GetSystemMetrics(SM_YVIRTUALSCREEN);
}
}
if(output) {
adapter = std::move(adapter_tmp);
break;
}
}
if(!output) {
BOOST_LOG(error) << "Failed to locate an output device"sv;
return -1;
}
D3D_FEATURE_LEVEL featureLevels[] {
D3D_FEATURE_LEVEL_11_1,
D3D_FEATURE_LEVEL_11_0,
D3D_FEATURE_LEVEL_10_1,
D3D_FEATURE_LEVEL_10_0,
D3D_FEATURE_LEVEL_9_3,
D3D_FEATURE_LEVEL_9_2,
D3D_FEATURE_LEVEL_9_1
};
status = adapter->QueryInterface(IID_IDXGIAdapter, (void **)&adapter_p);
if(FAILED(status)) {
BOOST_LOG(error) << "Failed to query IDXGIAdapter interface"sv;
return -1;
}
status = D3D11CreateDevice(
adapter_p,
D3D_DRIVER_TYPE_UNKNOWN,
nullptr,
D3D11_CREATE_DEVICE_VIDEO_SUPPORT,
featureLevels, sizeof(featureLevels) / sizeof(D3D_FEATURE_LEVEL),
D3D11_SDK_VERSION,
&device,
&feature_level,
&device_ctx);
adapter_p->Release();
if(FAILED(status)) {
BOOST_LOG(error) << "Failed to create D3D11 device [0x"sv << util::hex(status).to_string_view() << ']';
return -1;
}
DXGI_ADAPTER_DESC adapter_desc;
adapter->GetDesc(&adapter_desc);
auto description = converter.to_bytes(adapter_desc.Description);
BOOST_LOG(info)
<< std::endl
<< "Device Description : " << description << std::endl
<< "Device Vendor ID : 0x"sv << util::hex(adapter_desc.VendorId).to_string_view() << std::endl
<< "Device Device ID : 0x"sv << util::hex(adapter_desc.DeviceId).to_string_view() << std::endl
<< "Device Video Mem : "sv << adapter_desc.DedicatedVideoMemory / 1048576 << " MiB"sv << std::endl
<< "Device Sys Mem : "sv << adapter_desc.DedicatedSystemMemory / 1048576 << " MiB"sv << std::endl
<< "Share Sys Mem : "sv << adapter_desc.SharedSystemMemory / 1048576 << " MiB"sv << std::endl
<< "Feature Level : 0x"sv << util::hex(feature_level).to_string_view() << std::endl
<< "Capture size : "sv << width << 'x' << height << std::endl
<< "Offset : "sv << offset_x << 'x' << offset_y << std::endl
<< "Virtual Desktop : "sv << env_width << 'x' << env_height;
// Bump up thread priority
{
const DWORD flags = TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY;
TOKEN_PRIVILEGES tp;
HANDLE token;
LUID val;
if(OpenProcessToken(GetCurrentProcess(), flags, &token) &&
!!LookupPrivilegeValue(NULL, SE_INC_BASE_PRIORITY_NAME, &val)) {
tp.PrivilegeCount = 1;
tp.Privileges[0].Luid = val;
tp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;
if(!AdjustTokenPrivileges(token, false, &tp, sizeof(tp), NULL, NULL)) {
BOOST_LOG(warning) << "Could not set privilege to increase GPU priority";
}
}
CloseHandle(token);
HMODULE gdi32 = GetModuleHandleA("GDI32");
if(gdi32) {
PD3DKMTSetProcessSchedulingPriorityClass fn =
(PD3DKMTSetProcessSchedulingPriorityClass)GetProcAddress(gdi32, "D3DKMTSetProcessSchedulingPriorityClass");
if(fn) {
status = fn(GetCurrentProcess(), D3DKMT_SCHEDULINGPRIORITYCLASS_REALTIME);
if(FAILED(status)) {
BOOST_LOG(warning) << "Failed to set realtime GPU priority. Please run application as administrator for optimal performance.";
}
}
}
dxgi::dxgi_t dxgi;
status = device->QueryInterface(IID_IDXGIDevice, (void **)&dxgi);
if(FAILED(status)) {
BOOST_LOG(warning) << "Failed to query DXGI interface from device [0x"sv << util::hex(status).to_string_view() << ']';
return -1;
}
dxgi->SetGPUThreadPriority(7);
}
// Try to reduce latency
{
dxgi::dxgi1_t dxgi {};
status = device->QueryInterface(IID_IDXGIDevice, (void **)&dxgi);
if(FAILED(status)) {
BOOST_LOG(error) << "Failed to query DXGI interface from device [0x"sv << util::hex(status).to_string_view() << ']';
return -1;
}
status = dxgi->SetMaximumFrameLatency(1);
if(FAILED(status)) {
BOOST_LOG(warning) << "Failed to set maximum frame latency [0x"sv << util::hex(status).to_string_view() << ']';
}
}
//FIXME: Duplicate output on RX580 in combination with DOOM (2016) --> BSOD
//TODO: Use IDXGIOutput5 for improved performance
{
dxgi::output1_t output1 {};
status = output->QueryInterface(IID_IDXGIOutput1, (void **)&output1);
if(FAILED(status)) {
BOOST_LOG(error) << "Failed to query IDXGIOutput1 from the output"sv;
return -1;
}
// We try this twice, in case we still get an error on reinitialization
for(int x = 0; x < 2; ++x) {
status = output1->DuplicateOutput((IUnknown *)device.get(), &dup.dup);
if(SUCCEEDED(status)) {
break;
}
std::this_thread::sleep_for(200ms);
}
if(FAILED(status)) {
BOOST_LOG(error) << "DuplicateOutput Failed [0x"sv << util::hex(status).to_string_view() << ']';
return -1;
}
}
DXGI_OUTDUPL_DESC dup_desc;
dup.dup->GetDesc(&dup_desc);
format = dup_desc.ModeDesc.Format;
BOOST_LOG(debug) << "Source format ["sv << format_str[dup_desc.ModeDesc.Format] << ']';
return 0;
}
const char *format_str[] = {
"DXGI_FORMAT_UNKNOWN",
"DXGI_FORMAT_R32G32B32A32_TYPELESS",
"DXGI_FORMAT_R32G32B32A32_FLOAT",
"DXGI_FORMAT_R32G32B32A32_UINT",
"DXGI_FORMAT_R32G32B32A32_SINT",
"DXGI_FORMAT_R32G32B32_TYPELESS",
"DXGI_FORMAT_R32G32B32_FLOAT",
"DXGI_FORMAT_R32G32B32_UINT",
"DXGI_FORMAT_R32G32B32_SINT",
"DXGI_FORMAT_R16G16B16A16_TYPELESS",
"DXGI_FORMAT_R16G16B16A16_FLOAT",
"DXGI_FORMAT_R16G16B16A16_UNORM",
"DXGI_FORMAT_R16G16B16A16_UINT",
"DXGI_FORMAT_R16G16B16A16_SNORM",
"DXGI_FORMAT_R16G16B16A16_SINT",
"DXGI_FORMAT_R32G32_TYPELESS",
"DXGI_FORMAT_R32G32_FLOAT",
"DXGI_FORMAT_R32G32_UINT",
"DXGI_FORMAT_R32G32_SINT",
"DXGI_FORMAT_R32G8X24_TYPELESS",
"DXGI_FORMAT_D32_FLOAT_S8X24_UINT",
"DXGI_FORMAT_R32_FLOAT_X8X24_TYPELESS",
"DXGI_FORMAT_X32_TYPELESS_G8X24_UINT",
"DXGI_FORMAT_R10G10B10A2_TYPELESS",
"DXGI_FORMAT_R10G10B10A2_UNORM",
"DXGI_FORMAT_R10G10B10A2_UINT",
"DXGI_FORMAT_R11G11B10_FLOAT",
"DXGI_FORMAT_R8G8B8A8_TYPELESS",
"DXGI_FORMAT_R8G8B8A8_UNORM",
"DXGI_FORMAT_R8G8B8A8_UNORM_SRGB",
"DXGI_FORMAT_R8G8B8A8_UINT",
"DXGI_FORMAT_R8G8B8A8_SNORM",
"DXGI_FORMAT_R8G8B8A8_SINT",
"DXGI_FORMAT_R16G16_TYPELESS",
"DXGI_FORMAT_R16G16_FLOAT",
"DXGI_FORMAT_R16G16_UNORM",
"DXGI_FORMAT_R16G16_UINT",
"DXGI_FORMAT_R16G16_SNORM",
"DXGI_FORMAT_R16G16_SINT",
"DXGI_FORMAT_R32_TYPELESS",
"DXGI_FORMAT_D32_FLOAT",
"DXGI_FORMAT_R32_FLOAT",
"DXGI_FORMAT_R32_UINT",
"DXGI_FORMAT_R32_SINT",
"DXGI_FORMAT_R24G8_TYPELESS",
"DXGI_FORMAT_D24_UNORM_S8_UINT",
"DXGI_FORMAT_R24_UNORM_X8_TYPELESS",
"DXGI_FORMAT_X24_TYPELESS_G8_UINT",
"DXGI_FORMAT_R8G8_TYPELESS",
"DXGI_FORMAT_R8G8_UNORM",
"DXGI_FORMAT_R8G8_UINT",
"DXGI_FORMAT_R8G8_SNORM",
"DXGI_FORMAT_R8G8_SINT",
"DXGI_FORMAT_R16_TYPELESS",
"DXGI_FORMAT_R16_FLOAT",
"DXGI_FORMAT_D16_UNORM",
"DXGI_FORMAT_R16_UNORM",
"DXGI_FORMAT_R16_UINT",
"DXGI_FORMAT_R16_SNORM",
"DXGI_FORMAT_R16_SINT",
"DXGI_FORMAT_R8_TYPELESS",
"DXGI_FORMAT_R8_UNORM",
"DXGI_FORMAT_R8_UINT",
"DXGI_FORMAT_R8_SNORM",
"DXGI_FORMAT_R8_SINT",
"DXGI_FORMAT_A8_UNORM",
"DXGI_FORMAT_R1_UNORM",
"DXGI_FORMAT_R9G9B9E5_SHAREDEXP",
"DXGI_FORMAT_R8G8_B8G8_UNORM",
"DXGI_FORMAT_G8R8_G8B8_UNORM",
"DXGI_FORMAT_BC1_TYPELESS",
"DXGI_FORMAT_BC1_UNORM",
"DXGI_FORMAT_BC1_UNORM_SRGB",
"DXGI_FORMAT_BC2_TYPELESS",
"DXGI_FORMAT_BC2_UNORM",
"DXGI_FORMAT_BC2_UNORM_SRGB",
"DXGI_FORMAT_BC3_TYPELESS",
"DXGI_FORMAT_BC3_UNORM",
"DXGI_FORMAT_BC3_UNORM_SRGB",
"DXGI_FORMAT_BC4_TYPELESS",
"DXGI_FORMAT_BC4_UNORM",
"DXGI_FORMAT_BC4_SNORM",
"DXGI_FORMAT_BC5_TYPELESS",
"DXGI_FORMAT_BC5_UNORM",
"DXGI_FORMAT_BC5_SNORM",
"DXGI_FORMAT_B5G6R5_UNORM",
"DXGI_FORMAT_B5G5R5A1_UNORM",
"DXGI_FORMAT_B8G8R8A8_UNORM",
"DXGI_FORMAT_B8G8R8X8_UNORM",
"DXGI_FORMAT_R10G10B10_XR_BIAS_A2_UNORM",
"DXGI_FORMAT_B8G8R8A8_TYPELESS",
"DXGI_FORMAT_B8G8R8A8_UNORM_SRGB",
"DXGI_FORMAT_B8G8R8X8_TYPELESS",
"DXGI_FORMAT_B8G8R8X8_UNORM_SRGB",
"DXGI_FORMAT_BC6H_TYPELESS",
"DXGI_FORMAT_BC6H_UF16",
"DXGI_FORMAT_BC6H_SF16",
"DXGI_FORMAT_BC7_TYPELESS",
"DXGI_FORMAT_BC7_UNORM",
"DXGI_FORMAT_BC7_UNORM_SRGB",
"DXGI_FORMAT_AYUV",
"DXGI_FORMAT_Y410",
"DXGI_FORMAT_Y416",
"DXGI_FORMAT_NV12",
"DXGI_FORMAT_P010",
"DXGI_FORMAT_P016",
"DXGI_FORMAT_420_OPAQUE",
"DXGI_FORMAT_YUY2",
"DXGI_FORMAT_Y210",
"DXGI_FORMAT_Y216",
"DXGI_FORMAT_NV11",
"DXGI_FORMAT_AI44",
"DXGI_FORMAT_IA44",
"DXGI_FORMAT_P8",
"DXGI_FORMAT_A8P8",
"DXGI_FORMAT_B4G4R4A4_UNORM",
NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL,
"DXGI_FORMAT_P208",
"DXGI_FORMAT_V208",
"DXGI_FORMAT_V408"
};
} // namespace platf::dxgi
namespace platf {
std::shared_ptr<display_t> display(mem_type_e hwdevice_type) {
if(hwdevice_type == mem_type_e::dxgi) {
auto disp = std::make_shared<dxgi::display_vram_t>();
if(!disp->init()) {
return disp;
}
}
else if(hwdevice_type == mem_type_e::system) {
auto disp = std::make_shared<dxgi::display_ram_t>();
if(!disp->init()) {
return disp;
}
}
return nullptr;
}
} // namespace platf

View File

@@ -0,0 +1,297 @@
#include "display.h"
#include "sunshine/main.h"
namespace platf {
using namespace std::literals;
}
namespace platf::dxgi {
struct img_t : public ::platf::img_t {
~img_t() override {
delete[] data;
data = nullptr;
}
};
void blend_cursor_monochrome(const cursor_t &cursor, img_t &img) {
int height = cursor.shape_info.Height / 2;
int width = cursor.shape_info.Width;
int pitch = cursor.shape_info.Pitch;
// img cursor.{x,y} < 0, skip parts of the cursor.img_data
auto cursor_skip_y = -std::min(0, cursor.y);
auto cursor_skip_x = -std::min(0, cursor.x);
// img cursor.{x,y} > img.{x,y}, truncate parts of the cursor.img_data
auto cursor_truncate_y = std::max(0, cursor.y - img.height);
auto cursor_truncate_x = std::max(0, cursor.x - img.width);
auto cursor_width = width - cursor_skip_x - cursor_truncate_x;
auto cursor_height = height - cursor_skip_y - cursor_truncate_y;
if(cursor_height > height || cursor_width > width) {
return;
}
auto img_skip_y = std::max(0, cursor.y);
auto img_skip_x = std::max(0, cursor.x);
auto cursor_img_data = cursor.img_data.data() + cursor_skip_y * pitch;
int delta_height = std::min(cursor_height - cursor_truncate_y, std::max(0, img.height - img_skip_y));
int delta_width = std::min(cursor_width - cursor_truncate_x, std::max(0, img.width - img_skip_x));
auto pixels_per_byte = width / pitch;
auto bytes_per_row = delta_width / pixels_per_byte;
auto img_data = (int *)img.data;
for(int i = 0; i < delta_height; ++i) {
auto and_mask = &cursor_img_data[i * pitch];
auto xor_mask = &cursor_img_data[(i + height) * pitch];
auto img_pixel_p = &img_data[(i + img_skip_y) * (img.row_pitch / img.pixel_pitch) + img_skip_x];
auto skip_x = cursor_skip_x;
for(int x = 0; x < bytes_per_row; ++x) {
for(auto bit = 0u; bit < 8; ++bit) {
if(skip_x > 0) {
--skip_x;
continue;
}
int and_ = *and_mask & (1 << (7 - bit)) ? -1 : 0;
int xor_ = *xor_mask & (1 << (7 - bit)) ? -1 : 0;
*img_pixel_p &= and_;
*img_pixel_p ^= xor_;
++img_pixel_p;
}
++and_mask;
++xor_mask;
}
}
}
void apply_color_alpha(int *img_pixel_p, int cursor_pixel) {
auto colors_out = (std::uint8_t *)&cursor_pixel;
auto colors_in = (std::uint8_t *)img_pixel_p;
//TODO: When use of IDXGIOutput5 is implemented, support different color formats
auto alpha = colors_out[3];
if(alpha == 255) {
*img_pixel_p = cursor_pixel;
}
else {
colors_in[0] = colors_out[0] + (colors_in[0] * (255 - alpha) + 255 / 2) / 255;
colors_in[1] = colors_out[1] + (colors_in[1] * (255 - alpha) + 255 / 2) / 255;
colors_in[2] = colors_out[2] + (colors_in[2] * (255 - alpha) + 255 / 2) / 255;
}
}
void apply_color_masked(int *img_pixel_p, int cursor_pixel) {
//TODO: When use of IDXGIOutput5 is implemented, support different color formats
auto alpha = ((std::uint8_t *)&cursor_pixel)[3];
if(alpha == 0xFF) {
*img_pixel_p ^= cursor_pixel;
}
else {
*img_pixel_p = cursor_pixel;
}
}
void blend_cursor_color(const cursor_t &cursor, img_t &img, const bool masked) {
int height = cursor.shape_info.Height;
int width = cursor.shape_info.Width;
int pitch = cursor.shape_info.Pitch;
// img cursor.y < 0, skip parts of the cursor.img_data
auto cursor_skip_y = -std::min(0, cursor.y);
auto cursor_skip_x = -std::min(0, cursor.x);
// img cursor.{x,y} > img.{x,y}, truncate parts of the cursor.img_data
auto cursor_truncate_y = std::max(0, cursor.y - img.height);
auto cursor_truncate_x = std::max(0, cursor.x - img.width);
auto img_skip_y = std::max(0, cursor.y);
auto img_skip_x = std::max(0, cursor.x);
auto cursor_width = width - cursor_skip_x - cursor_truncate_x;
auto cursor_height = height - cursor_skip_y - cursor_truncate_y;
if(cursor_height > height || cursor_width > width) {
return;
}
auto cursor_img_data = (int *)&cursor.img_data[cursor_skip_y * pitch];
int delta_height = std::min(cursor_height - cursor_truncate_y, std::max(0, img.height - img_skip_y));
int delta_width = std::min(cursor_width - cursor_truncate_x, std::max(0, img.width - img_skip_x));
auto img_data = (int *)img.data;
for(int i = 0; i < delta_height; ++i) {
auto cursor_begin = &cursor_img_data[i * cursor.shape_info.Width + cursor_skip_x];
auto cursor_end = &cursor_begin[delta_width];
auto img_pixel_p = &img_data[(i + img_skip_y) * (img.row_pitch / img.pixel_pitch) + img_skip_x];
std::for_each(cursor_begin, cursor_end, [&](int cursor_pixel) {
if(masked) {
apply_color_masked(img_pixel_p, cursor_pixel);
}
else {
apply_color_alpha(img_pixel_p, cursor_pixel);
}
++img_pixel_p;
});
}
}
void blend_cursor(const cursor_t &cursor, img_t &img) {
switch(cursor.shape_info.Type) {
case DXGI_OUTDUPL_POINTER_SHAPE_TYPE_COLOR:
blend_cursor_color(cursor, img, false);
break;
case DXGI_OUTDUPL_POINTER_SHAPE_TYPE_MONOCHROME:
blend_cursor_monochrome(cursor, img);
break;
case DXGI_OUTDUPL_POINTER_SHAPE_TYPE_MASKED_COLOR:
blend_cursor_color(cursor, img, true);
break;
default:
BOOST_LOG(warning) << "Unsupported cursor format ["sv << cursor.shape_info.Type << ']';
}
}
capture_e display_ram_t::snapshot(::platf::img_t *img_base, std::chrono::milliseconds timeout, bool cursor_visible) {
auto img = (img_t *)img_base;
HRESULT status;
DXGI_OUTDUPL_FRAME_INFO frame_info;
resource_t::pointer res_p {};
auto capture_status = dup.next_frame(frame_info, timeout, &res_p);
resource_t res { res_p };
if(capture_status != capture_e::ok) {
return capture_status;
}
if(frame_info.PointerShapeBufferSize > 0) {
auto &img_data = cursor.img_data;
img_data.resize(frame_info.PointerShapeBufferSize);
UINT dummy;
status = dup.dup->GetFramePointerShape(img_data.size(), img_data.data(), &dummy, &cursor.shape_info);
if(FAILED(status)) {
BOOST_LOG(error) << "Failed to get new pointer shape [0x"sv << util::hex(status).to_string_view() << ']';
return capture_e::error;
}
}
if(frame_info.LastMouseUpdateTime.QuadPart) {
cursor.x = frame_info.PointerPosition.Position.x;
cursor.y = frame_info.PointerPosition.Position.y;
cursor.visible = frame_info.PointerPosition.Visible;
}
// If frame has been updated
if(frame_info.LastPresentTime.QuadPart != 0) {
{
texture2d_t src {};
status = res->QueryInterface(IID_ID3D11Texture2D, (void **)&src);
if(FAILED(status)) {
BOOST_LOG(error) << "Couldn't query interface [0x"sv << util::hex(status).to_string_view() << ']';
return capture_e::error;
}
//Copy from GPU to CPU
device_ctx->CopyResource(texture.get(), src.get());
}
if(img_info.pData) {
device_ctx->Unmap(texture.get(), 0);
img_info.pData = nullptr;
}
status = device_ctx->Map(texture.get(), 0, D3D11_MAP_READ, 0, &img_info);
if(FAILED(status)) {
BOOST_LOG(error) << "Failed to map texture [0x"sv << util::hex(status).to_string_view() << ']';
return capture_e::error;
}
}
const bool mouse_update =
(frame_info.LastMouseUpdateTime.QuadPart || frame_info.PointerShapeBufferSize > 0) &&
(cursor_visible && cursor.visible);
const bool update_flag = frame_info.LastPresentTime.QuadPart != 0 || mouse_update;
if(!update_flag) {
return capture_e::timeout;
}
std::copy_n((std::uint8_t *)img_info.pData, height * img_info.RowPitch, (std::uint8_t *)img->data);
if(cursor_visible && cursor.visible) {
blend_cursor(cursor, *img);
}
return capture_e::ok;
}
std::shared_ptr<platf::img_t> display_ram_t::alloc_img() {
auto img = std::make_shared<img_t>();
img->pixel_pitch = 4;
img->row_pitch = img_info.RowPitch;
img->width = width;
img->height = height;
img->data = new std::uint8_t[img->row_pitch * height];
return img;
}
int display_ram_t::dummy_img(platf::img_t *img) {
return 0;
}
int display_ram_t::init() {
if(display_base_t::init()) {
return -1;
}
D3D11_TEXTURE2D_DESC t {};
t.Width = width;
t.Height = height;
t.MipLevels = 1;
t.ArraySize = 1;
t.SampleDesc.Count = 1;
t.Usage = D3D11_USAGE_STAGING;
t.Format = format;
t.CPUAccessFlags = D3D11_CPU_ACCESS_READ;
auto status = device->CreateTexture2D(&t, nullptr, &texture);
if(FAILED(status)) {
BOOST_LOG(error) << "Failed to create texture [0x"sv << util::hex(status).to_string_view() << ']';
return -1;
}
// map the texture simply to get the pitch and stride
status = device_ctx->Map(texture.get(), 0, D3D11_MAP_READ, 0, &img_info);
if(FAILED(status)) {
BOOST_LOG(error) << "Failed to map the texture [0x"sv << util::hex(status).to_string_view() << ']';
return -1;
}
return 0;
}
} // namespace platf::dxgi

View File

@@ -0,0 +1,914 @@
#include <cmath>
#include <codecvt>
#include <d3dcompiler.h>
#include <directxmath.h>
extern "C" {
#include <libavcodec/avcodec.h>
#include <libavutil/hwcontext_d3d11va.h>
}
#include "display.h"
#include "sunshine/main.h"
#include "sunshine/video.h"
#define SUNSHINE_SHADERS_DIR SUNSHINE_ASSETS_DIR "/shaders/directx"
namespace platf {
using namespace std::literals;
}
static void free_frame(AVFrame *frame) {
av_frame_free(&frame);
}
using frame_t = util::safe_ptr<AVFrame, free_frame>;
namespace platf::dxgi {
using input_layout_t = util::safe_ptr<ID3D11InputLayout, Release<ID3D11InputLayout>>;
using render_target_t = util::safe_ptr<ID3D11RenderTargetView, Release<ID3D11RenderTargetView>>;
using shader_res_t = util::safe_ptr<ID3D11ShaderResourceView, Release<ID3D11ShaderResourceView>>;
using buf_t = util::safe_ptr<ID3D11Buffer, Release<ID3D11Buffer>>;
using blend_t = util::safe_ptr<ID3D11BlendState, Release<ID3D11BlendState>>;
using raster_state_t = util::safe_ptr<ID3D11RasterizerState, Release<ID3D11RasterizerState>>;
using sampler_state_t = util::safe_ptr<ID3D11SamplerState, Release<ID3D11SamplerState>>;
using vs_t = util::safe_ptr<ID3D11VertexShader, Release<ID3D11VertexShader>>;
using ps_t = util::safe_ptr<ID3D11PixelShader, Release<ID3D11PixelShader>>;
using blob_t = util::safe_ptr<ID3DBlob, Release<ID3DBlob>>;
using depth_stencil_state_t = util::safe_ptr<ID3D11DepthStencilState, Release<ID3D11DepthStencilState>>;
using depth_stencil_view_t = util::safe_ptr<ID3D11DepthStencilView, Release<ID3D11DepthStencilView>>;
template<class T>
buf_t make_buffer(device_t::pointer device, const T &t) {
static_assert(sizeof(T) % 16 == 0, "Buffer needs to be aligned on a 16-byte alignment");
D3D11_BUFFER_DESC buffer_desc {
sizeof(T),
D3D11_USAGE_IMMUTABLE,
D3D11_BIND_CONSTANT_BUFFER
};
D3D11_SUBRESOURCE_DATA init_data {
&t
};
buf_t::pointer buf_p;
auto status = device->CreateBuffer(&buffer_desc, &init_data, &buf_p);
if(status) {
BOOST_LOG(error) << "Failed to create buffer: [0x"sv << util::hex(status).to_string_view() << ']';
return nullptr;
}
return buf_t { buf_p };
}
blend_t make_blend(device_t::pointer device, bool enable) {
D3D11_BLEND_DESC bdesc {};
auto &rt = bdesc.RenderTarget[0];
rt.BlendEnable = enable;
rt.RenderTargetWriteMask = D3D11_COLOR_WRITE_ENABLE_ALL;
if(enable) {
rt.BlendOp = D3D11_BLEND_OP_ADD;
rt.BlendOpAlpha = D3D11_BLEND_OP_ADD;
rt.SrcBlend = D3D11_BLEND_SRC_ALPHA;
rt.DestBlend = D3D11_BLEND_INV_SRC_ALPHA;
rt.SrcBlendAlpha = D3D11_BLEND_ZERO;
rt.DestBlendAlpha = D3D11_BLEND_ZERO;
}
blend_t blend;
auto status = device->CreateBlendState(&bdesc, &blend);
if(status) {
BOOST_LOG(error) << "Failed to create blend state: [0x"sv << util::hex(status).to_string_view() << ']';
return nullptr;
}
return blend;
}
blob_t convert_UV_vs_hlsl;
blob_t convert_UV_ps_hlsl;
blob_t scene_vs_hlsl;
blob_t convert_Y_ps_hlsl;
blob_t scene_ps_hlsl;
struct img_d3d_t : public platf::img_t {
shader_res_t input_res;
texture2d_t texture;
std::shared_ptr<platf::display_t> display;
~img_d3d_t() override = default;
};
util::buffer_t<std::uint8_t> make_cursor_image(util::buffer_t<std::uint8_t> &&img_data, DXGI_OUTDUPL_POINTER_SHAPE_INFO shape_info) {
constexpr std::uint32_t black = 0xFF000000;
constexpr std::uint32_t white = 0xFFFFFFFF;
constexpr std::uint32_t transparent = 0;
switch(shape_info.Type) {
case DXGI_OUTDUPL_POINTER_SHAPE_TYPE_MASKED_COLOR:
std::for_each((std::uint32_t *)std::begin(img_data), (std::uint32_t *)std::end(img_data), [](auto &pixel) {
if(pixel & 0xFF000000) {
pixel = transparent;
}
});
case DXGI_OUTDUPL_POINTER_SHAPE_TYPE_COLOR:
return std::move(img_data);
default:
break;
}
shape_info.Height /= 2;
util::buffer_t<std::uint8_t> cursor_img { shape_info.Width * shape_info.Height * 4 };
auto bytes = shape_info.Pitch * shape_info.Height;
auto pixel_begin = (std::uint32_t *)std::begin(cursor_img);
auto pixel_data = pixel_begin;
auto and_mask = std::begin(img_data);
auto xor_mask = std::begin(img_data) + bytes;
for(auto x = 0; x < bytes; ++x) {
for(auto c = 7; c >= 0; --c) {
auto bit = 1 << c;
auto color_type = ((*and_mask & bit) ? 1 : 0) + ((*xor_mask & bit) ? 2 : 0);
switch(color_type) {
case 0: //black
*pixel_data = black;
break;
case 2: //white
*pixel_data = white;
break;
case 1: //transparent
{
*pixel_data = transparent;
break;
}
case 3: //inverse
{
auto top_p = pixel_data - shape_info.Width;
auto left_p = pixel_data - 1;
auto right_p = pixel_data + 1;
auto bottom_p = pixel_data + shape_info.Width;
// Get the x coordinate of the pixel
auto column = (pixel_data - pixel_begin) % shape_info.Width != 0;
if(top_p >= pixel_begin && *top_p == transparent) {
*top_p = black;
}
if(column != 0 && left_p >= pixel_begin && *left_p == transparent) {
*left_p = black;
}
if(bottom_p < (std::uint32_t *)std::end(cursor_img)) {
*bottom_p = black;
}
if(column != shape_info.Width - 1) {
*right_p = black;
}
*pixel_data = white;
}
}
++pixel_data;
}
++and_mask;
++xor_mask;
}
return cursor_img;
}
blob_t compile_shader(LPCSTR file, LPCSTR entrypoint, LPCSTR shader_model) {
blob_t::pointer msg_p = nullptr;
blob_t::pointer compiled_p;
DWORD flags = D3DCOMPILE_ENABLE_STRICTNESS;
#ifndef NDEBUG
flags |= D3DCOMPILE_DEBUG | D3DCOMPILE_SKIP_OPTIMIZATION;
#endif
std::wstring_convert<std::codecvt_utf8_utf16<wchar_t>, wchar_t> converter;
auto wFile = converter.from_bytes(file);
auto status = D3DCompileFromFile(wFile.c_str(), nullptr, nullptr, entrypoint, shader_model, flags, 0, &compiled_p, &msg_p);
if(msg_p) {
BOOST_LOG(warning) << std::string_view { (const char *)msg_p->GetBufferPointer(), msg_p->GetBufferSize() - 1 };
msg_p->Release();
}
if(status) {
BOOST_LOG(error) << "Couldn't compile ["sv << file << "] [0x"sv << util::hex(status).to_string_view() << ']';
return nullptr;
}
return blob_t { compiled_p };
}
blob_t compile_pixel_shader(LPCSTR file) {
return compile_shader(file, "main_ps", "ps_5_0");
}
blob_t compile_vertex_shader(LPCSTR file) {
return compile_shader(file, "main_vs", "vs_5_0");
}
class hwdevice_t : public platf::hwdevice_t {
public:
hwdevice_t(std::vector<hwdevice_t *> *hwdevices_p) : hwdevices_p { hwdevices_p } {}
hwdevice_t() = delete;
void set_cursor_pos(LONG rel_x, LONG rel_y, bool visible) {
cursor_visible = visible;
if(!visible) {
return;
}
auto x = ((float)rel_x);
auto y = ((float)rel_y);
cursor_view.TopLeftX = x;
cursor_view.TopLeftY = y;
}
int set_cursor_texture(texture2d_t::pointer texture, LONG width, LONG height) {
auto device = (device_t::pointer)data;
cursor_view.Width = width;
cursor_view.Height = height;
D3D11_SHADER_RESOURCE_VIEW_DESC desc {
DXGI_FORMAT_B8G8R8A8_UNORM,
D3D11_SRV_DIMENSION_TEXTURE2D
};
desc.Texture2D.MipLevels = 1;
auto status = device->CreateShaderResourceView(texture, &desc, &img.input_res);
if(FAILED(status)) {
BOOST_LOG(error) << "Failed to create cursor shader resource view [0x"sv << util::hex(status).to_string_view() << ']';
return -1;
}
return 0;
}
int convert(platf::img_t &img_base) override {
auto &img = (img_d3d_t &)img_base;
if(!img.input_res) {
auto device = (device_t::pointer)data;
D3D11_SHADER_RESOURCE_VIEW_DESC desc {
DXGI_FORMAT_B8G8R8A8_UNORM,
D3D11_SRV_DIMENSION_TEXTURE2D
};
desc.Texture2D.MipLevels = 1;
auto status = device->CreateShaderResourceView(img.texture.get(), &desc, &img.input_res);
if(FAILED(status)) {
BOOST_LOG(error) << "Failed to create input shader resource view [0x"sv << util::hex(status).to_string_view() << ']';
return -1;
}
}
auto input_res_p = img.input_res.get();
if(cursor_visible) {
_init_view_port(img.width, img.height);
device_ctx_p->OMSetRenderTargets(1, &scene_rt, nullptr);
device_ctx_p->VSSetShader(scene_vs.get(), nullptr, 0);
device_ctx_p->PSSetShader(scene_ps.get(), nullptr, 0);
device_ctx_p->PSSetShaderResources(0, 1, &input_res_p);
device_ctx_p->Draw(3, 0);
device_ctx_p->OMSetBlendState(blend_enable.get(), nullptr, 0xFFFFFFFFu);
device_ctx_p->RSSetViewports(1, &cursor_view);
device_ctx_p->PSSetShaderResources(0, 1, &this->img.input_res);
device_ctx_p->Draw(3, 0);
device_ctx_p->OMSetBlendState(blend_disable.get(), nullptr, 0xFFFFFFFFu);
input_res_p = scene_sr.get();
}
_init_view_port(this->img.width, this->img.height);
device_ctx_p->OMSetRenderTargets(1, &nv12_Y_rt, nullptr);
device_ctx_p->VSSetShader(scene_vs.get(), nullptr, 0);
device_ctx_p->PSSetShader(convert_Y_ps.get(), nullptr, 0);
device_ctx_p->PSSetShaderResources(0, 1, &back_img.input_res);
device_ctx_p->Draw(3, 0);
device_ctx_p->RSSetViewports(1, &outY_view);
device_ctx_p->PSSetShaderResources(0, 1, &input_res_p);
device_ctx_p->Draw(3, 0);
// Artifacts start appearing on the rendered image if Sunshine doesn't flush
// before rendering on the UV part of the image.
device_ctx_p->Flush();
_init_view_port(this->img.width / 2, this->img.height / 2);
device_ctx_p->OMSetRenderTargets(1, &nv12_UV_rt, nullptr);
device_ctx_p->VSSetShader(convert_UV_vs.get(), nullptr, 0);
device_ctx_p->PSSetShader(convert_UV_ps.get(), nullptr, 0);
device_ctx_p->PSSetShaderResources(0, 1, &back_img.input_res);
device_ctx_p->Draw(3, 0);
device_ctx_p->RSSetViewports(1, &outUV_view);
device_ctx_p->PSSetShaderResources(0, 1, &input_res_p);
device_ctx_p->Draw(3, 0);
device_ctx_p->Flush();
return 0;
}
void set_colorspace(std::uint32_t colorspace, std::uint32_t color_range) override {
switch(colorspace) {
case 5: // SWS_CS_SMPTE170M
color_p = &::video::colors[0];
break;
case 1: // SWS_CS_ITU709
color_p = &::video::colors[2];
break;
case 9: // SWS_CS_BT2020
default:
BOOST_LOG(warning) << "Colorspace: ["sv << colorspace << "] not yet supported: switching to default"sv;
color_p = &::video::colors[0];
};
if(color_range > 1) {
// Full range
++color_p;
}
auto color_matrix = make_buffer((device_t::pointer)data, *color_p);
if(!color_matrix) {
BOOST_LOG(warning) << "Failed to create color matrix"sv;
return;
}
device_ctx_p->PSSetConstantBuffers(0, 1, &color_matrix);
this->color_matrix = std::move(color_matrix);
}
int set_frame(AVFrame *frame) {
this->hwframe.reset(frame);
this->frame = frame;
auto device_p = (device_t::pointer)data;
auto out_width = frame->width;
auto out_height = frame->height;
float in_width = img.display->width;
float in_height = img.display->height;
// // Ensure aspect ratio is maintained
auto scalar = std::fminf(out_width / in_width, out_height / in_height);
auto out_width_f = in_width * scalar;
auto out_height_f = in_height * scalar;
// result is always positive
auto offsetX = (out_width - out_width_f) / 2;
auto offsetY = (out_height - out_height_f) / 2;
outY_view = D3D11_VIEWPORT { offsetX, offsetY, out_width_f, out_height_f, 0.0f, 1.0f };
outUV_view = D3D11_VIEWPORT { offsetX / 2, offsetY / 2, out_width_f / 2, out_height_f / 2, 0.0f, 1.0f };
D3D11_TEXTURE2D_DESC t {};
t.Width = out_width;
t.Height = out_height;
t.MipLevels = 1;
t.ArraySize = 1;
t.SampleDesc.Count = 1;
t.Usage = D3D11_USAGE_DEFAULT;
t.Format = format;
t.BindFlags = D3D11_BIND_RENDER_TARGET;
auto status = device_p->CreateTexture2D(&t, nullptr, &img.texture);
if(FAILED(status)) {
BOOST_LOG(error) << "Failed to create render target texture [0x"sv << util::hex(status).to_string_view() << ']';
return -1;
}
img.width = out_width;
img.height = out_height;
img.data = (std::uint8_t *)img.texture.get();
img.row_pitch = out_width;
img.pixel_pitch = 1;
float info_in[16 / sizeof(float)] { 1.0f / (float)out_width }; //aligned to 16-byte
info_scene = make_buffer(device_p, info_in);
if(!info_in) {
BOOST_LOG(error) << "Failed to create info scene buffer"sv;
return -1;
}
D3D11_RENDER_TARGET_VIEW_DESC nv12_rt_desc {
DXGI_FORMAT_R8_UNORM,
D3D11_RTV_DIMENSION_TEXTURE2D
};
status = device_p->CreateRenderTargetView(img.texture.get(), &nv12_rt_desc, &nv12_Y_rt);
if(FAILED(status)) {
BOOST_LOG(error) << "Failed to create render target view [0x"sv << util::hex(status).to_string_view() << ']';
return -1;
}
nv12_rt_desc.Format = DXGI_FORMAT_R8G8_UNORM;
status = device_p->CreateRenderTargetView(img.texture.get(), &nv12_rt_desc, &nv12_UV_rt);
if(FAILED(status)) {
BOOST_LOG(error) << "Failed to create render target view [0x"sv << util::hex(status).to_string_view() << ']';
return -1;
}
// Need to have something refcounted
if(!frame->buf[0]) {
frame->buf[0] = av_buffer_allocz(sizeof(AVD3D11FrameDescriptor));
}
auto desc = (AVD3D11FrameDescriptor *)frame->buf[0]->data;
desc->texture = (ID3D11Texture2D *)img.data;
desc->index = 0;
frame->data[0] = img.data;
frame->data[1] = 0;
frame->linesize[0] = img.row_pitch;
frame->height = img.height;
frame->width = img.width;
return 0;
}
int init(
std::shared_ptr<platf::display_t> display, device_t::pointer device_p, device_ctx_t::pointer device_ctx_p,
pix_fmt_e pix_fmt) {
HRESULT status;
device_p->AddRef();
data = device_p;
this->device_ctx_p = device_ctx_p;
cursor_visible = false;
cursor_view.MinDepth = 0.0f;
cursor_view.MaxDepth = 1.0f;
format = (pix_fmt == pix_fmt_e::nv12 ? DXGI_FORMAT_NV12 : DXGI_FORMAT_P010);
status = device_p->CreateVertexShader(scene_vs_hlsl->GetBufferPointer(), scene_vs_hlsl->GetBufferSize(), nullptr, &scene_vs);
if(status) {
BOOST_LOG(error) << "Failed to create scene vertex shader [0x"sv << util::hex(status).to_string_view() << ']';
return -1;
}
status = device_p->CreatePixelShader(convert_Y_ps_hlsl->GetBufferPointer(), convert_Y_ps_hlsl->GetBufferSize(), nullptr, &convert_Y_ps);
if(status) {
BOOST_LOG(error) << "Failed to create convertY pixel shader [0x"sv << util::hex(status).to_string_view() << ']';
return -1;
}
status = device_p->CreatePixelShader(convert_UV_ps_hlsl->GetBufferPointer(), convert_UV_ps_hlsl->GetBufferSize(), nullptr, &convert_UV_ps);
if(status) {
BOOST_LOG(error) << "Failed to create convertUV pixel shader [0x"sv << util::hex(status).to_string_view() << ']';
return -1;
}
status = device_p->CreateVertexShader(convert_UV_vs_hlsl->GetBufferPointer(), convert_UV_vs_hlsl->GetBufferSize(), nullptr, &convert_UV_vs);
if(status) {
BOOST_LOG(error) << "Failed to create convertUV vertex shader [0x"sv << util::hex(status).to_string_view() << ']';
return -1;
}
status = device_p->CreatePixelShader(scene_ps_hlsl->GetBufferPointer(), scene_ps_hlsl->GetBufferSize(), nullptr, &scene_ps);
if(status) {
BOOST_LOG(error) << "Failed to create scene pixel shader [0x"sv << util::hex(status).to_string_view() << ']';
return -1;
}
blend_disable = make_blend(device_p, false);
blend_enable = make_blend(device_p, true);
if(!blend_disable || !blend_enable) {
return -1;
}
if(_init_rt(scene_sr, scene_rt, display->width, display->height, DXGI_FORMAT_B8G8R8A8_UNORM)) {
return -1;
}
color_matrix = make_buffer(device_p, ::video::colors[0]);
if(!color_matrix) {
BOOST_LOG(error) << "Failed to create color matrix buffer"sv;
return -1;
}
D3D11_INPUT_ELEMENT_DESC layout_desc {
"SV_Position", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 0, D3D11_INPUT_PER_VERTEX_DATA, 0
};
status = device_p->CreateInputLayout(
&layout_desc, 1,
convert_UV_vs_hlsl->GetBufferPointer(), convert_UV_vs_hlsl->GetBufferSize(),
&input_layout);
img.display = std::move(display);
D3D11_SAMPLER_DESC sampler_desc {};
sampler_desc.Filter = D3D11_FILTER_MIN_MAG_MIP_LINEAR;
sampler_desc.AddressU = D3D11_TEXTURE_ADDRESS_CLAMP;
sampler_desc.AddressV = D3D11_TEXTURE_ADDRESS_CLAMP;
sampler_desc.AddressW = D3D11_TEXTURE_ADDRESS_WRAP;
sampler_desc.ComparisonFunc = D3D11_COMPARISON_NEVER;
sampler_desc.MinLOD = 0;
sampler_desc.MaxLOD = D3D11_FLOAT32_MAX;
status = device_p->CreateSamplerState(&sampler_desc, &sampler_linear);
if(FAILED(status)) {
BOOST_LOG(error) << "Failed to create point sampler state [0x"sv << util::hex(status).to_string_view() << ']';
return -1;
}
// Color the background black, so that the padding for keeping the aspect ratio
// is black
if(img.display->dummy_img(&back_img)) {
BOOST_LOG(warning) << "Couldn't create an image to set background color to black"sv;
return -1;
}
D3D11_SHADER_RESOURCE_VIEW_DESC desc {
DXGI_FORMAT_B8G8R8A8_UNORM,
D3D11_SRV_DIMENSION_TEXTURE2D
};
desc.Texture2D.MipLevels = 1;
status = device_p->CreateShaderResourceView(back_img.texture.get(), &desc, &back_img.input_res);
if(FAILED(status)) {
BOOST_LOG(error) << "Failed to create input shader resource view [0x"sv << util::hex(status).to_string_view() << ']';
return -1;
}
device_ctx_p->OMSetBlendState(blend_disable.get(), nullptr, 0xFFFFFFFFu);
device_ctx_p->PSSetSamplers(0, 1, &sampler_linear);
device_ctx_p->PSSetConstantBuffers(0, 1, &color_matrix);
device_ctx_p->VSSetConstantBuffers(0, 1, &info_scene);
device_ctx_p->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLESTRIP);
device_ctx_p->IASetInputLayout(input_layout.get());
return 0;
}
~hwdevice_t() override {
if(data) {
((ID3D11Device *)data)->Release();
}
auto it = std::find(std::begin(*hwdevices_p), std::end(*hwdevices_p), this);
if(it != std::end(*hwdevices_p)) {
hwdevices_p->erase(it);
}
}
private:
void _init_view_port(float x, float y, float width, float height) {
D3D11_VIEWPORT view {
x, y,
width, height,
0.0f, 1.0f
};
device_ctx_p->RSSetViewports(1, &view);
}
void _init_view_port(float width, float height) {
_init_view_port(0.0f, 0.0f, width, height);
}
int _init_rt(shader_res_t &shader_res, render_target_t &render_target, int width, int height, DXGI_FORMAT format) {
D3D11_TEXTURE2D_DESC desc {};
desc.Width = width;
desc.Height = height;
desc.Format = format;
desc.Usage = D3D11_USAGE_DEFAULT;
desc.BindFlags = D3D11_BIND_RENDER_TARGET | D3D11_BIND_SHADER_RESOURCE;
desc.MipLevels = 1;
desc.ArraySize = 1;
desc.SampleDesc.Count = 1;
auto device = (device_t::pointer)data;
texture2d_t tex;
auto status = device->CreateTexture2D(&desc, nullptr, &tex);
if(status) {
BOOST_LOG(error) << "Failed to create render target texture for luma [0x"sv << util::hex(status).to_string_view() << ']';
return -1;
}
D3D11_SHADER_RESOURCE_VIEW_DESC shader_resource_desc {
format,
D3D11_SRV_DIMENSION_TEXTURE2D
};
shader_resource_desc.Texture2D.MipLevels = 1;
device->CreateShaderResourceView(tex.get(), &shader_resource_desc, &shader_res);
if(status) {
BOOST_LOG(error) << "Failed to create render target texture for luma [0x"sv << util::hex(status).to_string_view() << ']';
return -1;
}
D3D11_RENDER_TARGET_VIEW_DESC render_target_desc {
format,
D3D11_RTV_DIMENSION_TEXTURE2D
};
device->CreateRenderTargetView(tex.get(), &render_target_desc, &render_target);
if(status) {
BOOST_LOG(error) << "Failed to create render target view [0x"sv << util::hex(status).to_string_view() << ']';
return -1;
}
return 0;
}
public:
frame_t hwframe;
::video::color_t *color_p;
blend_t blend_enable;
blend_t blend_disable;
buf_t info_scene;
buf_t color_matrix;
sampler_state_t sampler_linear;
input_layout_t input_layout;
render_target_t nv12_Y_rt;
render_target_t nv12_UV_rt;
render_target_t scene_rt;
shader_res_t scene_sr;
img_d3d_t img;
// Clear nv12 render target to black
img_d3d_t back_img;
vs_t convert_UV_vs;
ps_t convert_UV_ps;
ps_t convert_Y_ps;
ps_t scene_ps;
vs_t scene_vs;
D3D11_VIEWPORT outY_view;
D3D11_VIEWPORT outUV_view;
D3D11_VIEWPORT cursor_view;
bool cursor_visible;
DXGI_FORMAT format;
device_ctx_t::pointer device_ctx_p;
// The destructor will remove itself from the list of hardware devices, this is done synchronously
std::vector<hwdevice_t *> *hwdevices_p;
};
capture_e display_vram_t::snapshot(platf::img_t *img_base, std::chrono::milliseconds timeout, bool cursor_visible) {
auto img = (img_d3d_t *)img_base;
HRESULT status;
DXGI_OUTDUPL_FRAME_INFO frame_info;
resource_t::pointer res_p {};
auto capture_status = dup.next_frame(frame_info, timeout, &res_p);
resource_t res { res_p };
if(capture_status != capture_e::ok) {
return capture_status;
}
const bool mouse_update_flag = frame_info.LastMouseUpdateTime.QuadPart != 0 || frame_info.PointerShapeBufferSize > 0;
const bool frame_update_flag = frame_info.AccumulatedFrames != 0 || frame_info.LastPresentTime.QuadPart != 0;
const bool update_flag = mouse_update_flag || frame_update_flag;
if(!update_flag) {
return capture_e::timeout;
}
if(frame_info.PointerShapeBufferSize > 0) {
DXGI_OUTDUPL_POINTER_SHAPE_INFO shape_info {};
util::buffer_t<std::uint8_t> img_data { frame_info.PointerShapeBufferSize };
UINT dummy;
status = dup.dup->GetFramePointerShape(img_data.size(), std::begin(img_data), &dummy, &shape_info);
if(FAILED(status)) {
BOOST_LOG(error) << "Failed to get new pointer shape [0x"sv << util::hex(status).to_string_view() << ']';
return capture_e::error;
}
auto cursor_img = make_cursor_image(std::move(img_data), shape_info);
D3D11_SUBRESOURCE_DATA data {
std::begin(cursor_img),
4 * shape_info.Width,
0
};
// Create texture for cursor
D3D11_TEXTURE2D_DESC t {};
t.Width = shape_info.Width;
t.Height = cursor_img.size() / data.SysMemPitch;
t.MipLevels = 1;
t.ArraySize = 1;
t.SampleDesc.Count = 1;
t.Usage = D3D11_USAGE_DEFAULT;
t.Format = DXGI_FORMAT_B8G8R8A8_UNORM;
t.BindFlags = D3D11_BIND_SHADER_RESOURCE;
texture2d_t texture;
auto status = device->CreateTexture2D(&t, &data, &texture);
if(FAILED(status)) {
BOOST_LOG(error) << "Failed to create mouse texture [0x"sv << util::hex(status).to_string_view() << ']';
return capture_e::error;
}
for(auto *hwdevice : hwdevices) {
if(hwdevice->set_cursor_texture(texture.get(), t.Width, t.Height)) {
return capture_e::error;
}
}
cursor.texture = std::move(texture);
cursor.width = t.Width;
cursor.height = t.Height;
}
if(frame_info.LastMouseUpdateTime.QuadPart) {
for(auto *hwdevice : hwdevices) {
hwdevice->set_cursor_pos(frame_info.PointerPosition.Position.x, frame_info.PointerPosition.Position.y, frame_info.PointerPosition.Visible && cursor_visible);
}
}
if(frame_update_flag) {
texture2d_t src;
status = res->QueryInterface(IID_ID3D11Texture2D, (void **)&src);
if(FAILED(status)) {
BOOST_LOG(error) << "Couldn't query interface [0x"sv << util::hex(status).to_string_view() << ']';
return capture_e::error;
}
device_ctx->CopyResource(img->texture.get(), src.get());
}
return capture_e::ok;
}
std::shared_ptr<platf::img_t> display_vram_t::alloc_img() {
auto img = std::make_shared<img_d3d_t>();
D3D11_TEXTURE2D_DESC t {};
t.Width = width;
t.Height = height;
t.MipLevels = 1;
t.ArraySize = 1;
t.SampleDesc.Count = 1;
t.Usage = D3D11_USAGE_DEFAULT;
t.Format = format;
t.BindFlags = D3D11_BIND_SHADER_RESOURCE;
auto status = device->CreateTexture2D(&t, nullptr, &img->texture);
if(FAILED(status)) {
BOOST_LOG(error) << "Failed to create img buf texture [0x"sv << util::hex(status).to_string_view() << ']';
return nullptr;
}
img->data = (std::uint8_t *)img->texture.get();
img->row_pitch = 0;
img->pixel_pitch = 4;
img->width = 0;
img->height = 0;
img->display = shared_from_this();
return img;
}
int display_vram_t::dummy_img(platf::img_t *img_base) {
auto img = (img_d3d_t *)img_base;
img->row_pitch = width * 4;
auto dummy_data = std::make_unique<int[]>(width * height);
D3D11_SUBRESOURCE_DATA data {
dummy_data.get(),
(UINT)img->row_pitch
};
std::fill_n(dummy_data.get(), width * height, 0);
D3D11_TEXTURE2D_DESC t {};
t.Width = width;
t.Height = height;
t.MipLevels = 1;
t.ArraySize = 1;
t.SampleDesc.Count = 1;
t.Usage = D3D11_USAGE_DEFAULT;
t.Format = format;
t.BindFlags = D3D11_BIND_SHADER_RESOURCE;
dxgi::texture2d_t tex;
auto status = device->CreateTexture2D(&t, &data, &tex);
if(FAILED(status)) {
BOOST_LOG(error) << "Failed to create dummy texture [0x"sv << util::hex(status).to_string_view() << ']';
return -1;
}
img->texture = std::move(tex);
img->data = (std::uint8_t *)img->texture.get();
img->height = height;
img->width = width;
img->pixel_pitch = 4;
return 0;
}
std::shared_ptr<platf::hwdevice_t> display_vram_t::make_hwdevice(pix_fmt_e pix_fmt) {
if(pix_fmt != platf::pix_fmt_e::nv12) {
BOOST_LOG(error) << "display_vram_t doesn't support pixel format ["sv << from_pix_fmt(pix_fmt) << ']';
return nullptr;
}
auto hwdevice = std::make_shared<hwdevice_t>(&hwdevices);
auto ret = hwdevice->init(
shared_from_this(),
device.get(),
device_ctx.get(),
pix_fmt);
if(ret) {
return nullptr;
}
if(cursor.texture && hwdevice->set_cursor_texture(cursor.texture.get(), cursor.width, cursor.height)) {
return nullptr;
}
hwdevices.emplace_back(hwdevice.get());
return hwdevice;
}
int init() {
BOOST_LOG(info) << "Compiling shaders..."sv;
scene_vs_hlsl = compile_vertex_shader(SUNSHINE_SHADERS_DIR "/SceneVS.hlsl");
if(!scene_vs_hlsl) {
return -1;
}
convert_Y_ps_hlsl = compile_pixel_shader(SUNSHINE_SHADERS_DIR "/ConvertYPS.hlsl");
if(!convert_Y_ps_hlsl) {
return -1;
}
convert_UV_ps_hlsl = compile_pixel_shader(SUNSHINE_SHADERS_DIR "/ConvertUVPS.hlsl");
if(!convert_UV_ps_hlsl) {
return -1;
}
convert_UV_vs_hlsl = compile_vertex_shader(SUNSHINE_SHADERS_DIR "/ConvertUVVS.hlsl");
if(!convert_UV_vs_hlsl) {
return -1;
}
scene_ps_hlsl = compile_pixel_shader(SUNSHINE_SHADERS_DIR "/ScenePS.hlsl");
if(!scene_ps_hlsl) {
return -1;
}
BOOST_LOG(info) << "Compiled shaders"sv;
return 0;
}
} // namespace platf::dxgi

View File

@@ -0,0 +1,312 @@
#include <windows.h>
#include <cmath>
#include <ViGEm/Client.h>
#include "sunshine/main.h"
#include "sunshine/platform/common.h"
namespace platf {
using namespace std::literals;
volatile HDESK _lastKnownInputDesktop = NULL;
constexpr touch_port_t target_touch_port {
0, 0,
65535, 65535
};
HDESK pairInputDesktop();
class vigem_t {
public:
using client_t = util::safe_ptr<_VIGEM_CLIENT_T, vigem_free>;
using target_t = util::safe_ptr<_VIGEM_TARGET_T, vigem_target_free>;
int init() {
VIGEM_ERROR status;
client.reset(vigem_alloc());
status = vigem_connect(client.get());
if(!VIGEM_SUCCESS(status)) {
BOOST_LOG(warning) << "Couldn't setup connection to ViGEm for gamepad support ["sv << util::hex(status).to_string_view() << ']';
return -1;
}
x360s.resize(MAX_GAMEPADS);
return 0;
}
int alloc_x360(int nr) {
auto &x360 = x360s[nr];
assert(!x360);
x360.reset(vigem_target_x360_alloc());
auto status = vigem_target_add(client.get(), x360.get());
if(!VIGEM_SUCCESS(status)) {
BOOST_LOG(error) << "Couldn't add Gamepad to ViGEm connection ["sv << util::hex(status).to_string_view() << ']';
return -1;
}
return 0;
}
void free_target(int nr) {
auto &x360 = x360s[nr];
if(x360 && vigem_target_is_attached(x360.get())) {
auto status = vigem_target_remove(client.get(), x360.get());
if(!VIGEM_SUCCESS(status)) {
BOOST_LOG(warning) << "Couldn't detach gamepad from ViGEm ["sv << util::hex(status).to_string_view() << ']';
}
}
x360.reset();
}
~vigem_t() {
if(client) {
for(auto &x360 : x360s) {
if(x360 && vigem_target_is_attached(x360.get())) {
auto status = vigem_target_remove(client.get(), x360.get());
if(!VIGEM_SUCCESS(status)) {
BOOST_LOG(warning) << "Couldn't detach gamepad from ViGEm ["sv << util::hex(status).to_string_view() << ']';
}
}
}
vigem_disconnect(client.get());
}
}
std::vector<target_t> x360s;
client_t client;
};
input_t input() {
input_t result { new vigem_t {} };
auto vigem = (vigem_t *)result.get();
if(vigem->init()) {
return nullptr;
}
return result;
}
void send_input(INPUT &i) {
retry:
auto send = SendInput(1, &i, sizeof(INPUT));
if(send != 1) {
auto hDesk = pairInputDesktop();
if(_lastKnownInputDesktop != hDesk) {
_lastKnownInputDesktop = hDesk;
goto retry;
}
BOOST_LOG(warning) << "Couldn't send input"sv;
}
}
void abs_mouse(input_t &input, const touch_port_t &touch_port, float x, float y) {
INPUT i {};
i.type = INPUT_MOUSE;
auto &mi = i.mi;
mi.dwFlags =
MOUSEEVENTF_MOVE |
MOUSEEVENTF_ABSOLUTE |
// MOUSEEVENTF_VIRTUALDESK maps to the entirety of the desktop rather than the primary desktop
MOUSEEVENTF_VIRTUALDESK;
auto scaled_x = std::lround((x + touch_port.offset_x) * ((float)target_touch_port.width / (float)touch_port.width));
auto scaled_y = std::lround((y + touch_port.offset_y) * ((float)target_touch_port.height / (float)touch_port.height));
mi.dx = scaled_x;
mi.dy = scaled_y;
send_input(i);
}
void move_mouse(input_t &input, int deltaX, int deltaY) {
INPUT i {};
i.type = INPUT_MOUSE;
auto &mi = i.mi;
mi.dwFlags = MOUSEEVENTF_MOVE;
mi.dx = deltaX;
mi.dy = deltaY;
send_input(i);
}
void button_mouse(input_t &input, int button, bool release) {
constexpr auto KEY_STATE_DOWN = (SHORT)0x8000;
INPUT i {};
i.type = INPUT_MOUSE;
auto &mi = i.mi;
int mouse_button;
if(button == 1) {
mi.dwFlags = release ? MOUSEEVENTF_LEFTUP : MOUSEEVENTF_LEFTDOWN;
mouse_button = VK_LBUTTON;
}
else if(button == 2) {
mi.dwFlags = release ? MOUSEEVENTF_MIDDLEUP : MOUSEEVENTF_MIDDLEDOWN;
mouse_button = VK_MBUTTON;
}
else if(button == 3) {
mi.dwFlags = release ? MOUSEEVENTF_RIGHTUP : MOUSEEVENTF_RIGHTDOWN;
mouse_button = VK_RBUTTON;
}
else if(button == 4) {
mi.dwFlags = release ? MOUSEEVENTF_XUP : MOUSEEVENTF_XDOWN;
mi.mouseData = XBUTTON1;
mouse_button = VK_XBUTTON1;
}
else {
mi.dwFlags = release ? MOUSEEVENTF_XUP : MOUSEEVENTF_XDOWN;
mi.mouseData = XBUTTON2;
mouse_button = VK_XBUTTON2;
}
auto key_state = GetAsyncKeyState(mouse_button);
bool key_state_down = (key_state & KEY_STATE_DOWN) != 0;
if(key_state_down != release) {
BOOST_LOG(warning) << "Button state of mouse_button ["sv << button << "] does not match the desired state"sv;
return;
}
send_input(i);
}
void scroll(input_t &input, int distance) {
INPUT i {};
i.type = INPUT_MOUSE;
auto &mi = i.mi;
mi.dwFlags = MOUSEEVENTF_WHEEL;
mi.mouseData = distance;
send_input(i);
}
void keyboard(input_t &input, uint16_t modcode, bool release) {
if(modcode == VK_RMENU) {
modcode = VK_LWIN;
}
INPUT i {};
i.type = INPUT_KEYBOARD;
auto &ki = i.ki;
// For some reason, MapVirtualKey(VK_LWIN, MAPVK_VK_TO_VSC) doesn't seem to work :/
if(modcode != VK_LWIN && modcode != VK_RWIN && modcode != VK_PAUSE) {
ki.wScan = MapVirtualKey(modcode, MAPVK_VK_TO_VSC);
ki.dwFlags = KEYEVENTF_SCANCODE;
}
else {
ki.wVk = modcode;
}
// https://docs.microsoft.com/en-us/windows/win32/inputdev/about-keyboard-input#keystroke-message-flags
switch(modcode) {
case VK_RMENU:
case VK_RCONTROL:
case VK_INSERT:
case VK_DELETE:
case VK_HOME:
case VK_END:
case VK_PRIOR:
case VK_NEXT:
case VK_UP:
case VK_DOWN:
case VK_LEFT:
case VK_RIGHT:
case VK_DIVIDE:
ki.dwFlags |= KEYEVENTF_EXTENDEDKEY;
break;
default:
break;
}
if(release) {
ki.dwFlags |= KEYEVENTF_KEYUP;
}
send_input(i);
}
int alloc_gamepad(input_t &input, int nr) {
if(!input) {
return 0;
}
return ((vigem_t *)input.get())->alloc_x360(nr);
}
void free_gamepad(input_t &input, int nr) {
if(!input) {
return;
}
((vigem_t *)input.get())->free_target(nr);
}
void gamepad(input_t &input, int nr, const gamepad_state_t &gamepad_state) {
// If there is no gamepad support
if(!input) {
return;
}
auto vigem = (vigem_t *)input.get();
auto &xusb = *(PXUSB_REPORT)&gamepad_state;
auto &x360 = vigem->x360s[nr];
auto status = vigem_target_x360_update(vigem->client.get(), x360.get(), xusb);
if(!VIGEM_SUCCESS(status)) {
BOOST_LOG(fatal) << "Couldn't send gamepad input to ViGEm ["sv << util::hex(status).to_string_view() << ']';
log_flush();
std::abort();
}
}
int thread_priority() {
return SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_HIGHEST) ? 0 : 1;
}
HDESK pairInputDesktop() {
auto hDesk = OpenInputDesktop(DF_ALLOWOTHERACCOUNTHOOK, FALSE, GENERIC_ALL);
if(NULL == hDesk) {
auto err = GetLastError();
BOOST_LOG(error) << "Failed to OpenInputDesktop [0x"sv << util::hex(err).to_string_view() << ']';
}
else {
BOOST_LOG(info) << std::endl
<< "Opened desktop [0x"sv << util::hex(hDesk).to_string_view() << ']';
if(!SetThreadDesktop(hDesk)) {
auto err = GetLastError();
BOOST_LOG(error) << "Failed to SetThreadDesktop [0x"sv << util::hex(err).to_string_view() << ']';
}
CloseDesktop(hDesk);
}
return hDesk;
}
void freeInput(void *p) {
auto vigem = (vigem_t *)p;
delete vigem;
}
} // namespace platf

View File

@@ -0,0 +1,90 @@
#include <filesystem>
#include <iomanip>
#include <sstream>
// prevent clang format from "optimizing" the header include order
// clang-format off
#include <winsock2.h>
#include <iphlpapi.h>
#include <windows.h>
#include <winuser.h>
#include <ws2tcpip.h>
// clang-format on
#include "sunshine/main.h"
#include "sunshine/utility.h"
using namespace std::literals;
namespace platf {
using adapteraddrs_t = util::c_ptr<IP_ADAPTER_ADDRESSES>;
std::filesystem::path appdata() {
return L"."sv;
}
std::string from_sockaddr(const sockaddr *const socket_address) {
char data[INET6_ADDRSTRLEN];
auto family = socket_address->sa_family;
if(family == AF_INET6) {
inet_ntop(AF_INET6, &((sockaddr_in6 *)socket_address)->sin6_addr, data, INET6_ADDRSTRLEN);
}
if(family == AF_INET) {
inet_ntop(AF_INET, &((sockaddr_in *)socket_address)->sin_addr, data, INET_ADDRSTRLEN);
}
return std::string { data };
}
std::pair<std::uint16_t, std::string> from_sockaddr_ex(const sockaddr *const ip_addr) {
char data[INET6_ADDRSTRLEN];
auto family = ip_addr->sa_family;
std::uint16_t port;
if(family == AF_INET6) {
inet_ntop(AF_INET6, &((sockaddr_in6 *)ip_addr)->sin6_addr, data, INET6_ADDRSTRLEN);
port = ((sockaddr_in6 *)ip_addr)->sin6_port;
}
if(family == AF_INET) {
inet_ntop(AF_INET, &((sockaddr_in *)ip_addr)->sin_addr, data, INET_ADDRSTRLEN);
port = ((sockaddr_in *)ip_addr)->sin_port;
}
return { port, std::string { data } };
}
adapteraddrs_t get_adapteraddrs() {
adapteraddrs_t info { nullptr };
ULONG size = 0;
while(GetAdaptersAddresses(AF_UNSPEC, 0, nullptr, info.get(), &size) == ERROR_BUFFER_OVERFLOW) {
info.reset((PIP_ADAPTER_ADDRESSES)malloc(size));
}
return info;
}
std::string get_mac_address(const std::string_view &address) {
adapteraddrs_t info = get_adapteraddrs();
for(auto adapter_pos = info.get(); adapter_pos != nullptr; adapter_pos = adapter_pos->Next) {
for(auto addr_pos = adapter_pos->FirstUnicastAddress; addr_pos != nullptr; addr_pos = addr_pos->Next) {
if(adapter_pos->PhysicalAddressLength != 0 && address == from_sockaddr(addr_pos->Address.lpSockaddr)) {
std::stringstream mac_addr;
mac_addr << std::hex;
for(int i = 0; i < adapter_pos->PhysicalAddressLength; i++) {
if(i > 0) {
mac_addr << ':';
}
mac_addr << std::setw(2) << std::setfill('0') << (int)adapter_pos->PhysicalAddress[i];
}
return mac_addr.str();
}
}
}
BOOST_LOG(warning) << "Unable to find MAC address for "sv << address;
return "00:00:00:00:00:00"s;
}
} // namespace platf

View File

@@ -0,0 +1,194 @@
#include <winsock2.h>
#include <windows.h>
#include <windns.h>
#include <winerror.h>
#include <boost/asio/ip/host_name.hpp>
#include "sunshine/config.h"
#include "sunshine/main.h"
#include "sunshine/nvhttp.h"
#include "sunshine/platform/common.h"
#include "sunshine/thread_safe.h"
#include "sunshine/network.h"
using namespace std::literals;
#define __SV(quote) L##quote##sv
#define SV(quote) __SV(quote)
extern "C" {
constexpr auto DNS_REQUEST_PENDING = 9506L;
constexpr auto DNS_QUERY_REQUEST_VERSION1 = 0x1;
constexpr auto DNS_QUERY_RESULTS_VERSION1 = 0x1;
#define SERVICE_DOMAIN "local"
constexpr auto SERVICE_INSTANCE_NAME = SV(SERVICE_NAME "." SERVICE_TYPE "." SERVICE_DOMAIN);
constexpr auto SERVICE_TYPE_DOMAIN = SV(SERVICE_TYPE "." SERVICE_DOMAIN);
typedef struct _DNS_SERVICE_INSTANCE {
LPWSTR pszInstanceName;
LPWSTR pszHostName;
IP4_ADDRESS *ip4Address;
IP6_ADDRESS *ip6Address;
WORD wPort;
WORD wPriority;
WORD wWeight;
// Property list
DWORD dwPropertyCount;
PWSTR *keys;
PWSTR *values;
DWORD dwInterfaceIndex;
} DNS_SERVICE_INSTANCE, *PDNS_SERVICE_INSTANCE;
typedef VOID WINAPI DNS_SERVICE_REGISTER_COMPLETE(
_In_ DWORD Status,
_In_ PVOID pQueryContext,
_In_ PDNS_SERVICE_INSTANCE pInstance);
typedef DNS_SERVICE_REGISTER_COMPLETE *PDNS_SERVICE_REGISTER_COMPLETE;
typedef struct _DNS_SERVICE_CANCEL {
PVOID reserved;
} DNS_SERVICE_CANCEL, *PDNS_SERVICE_CANCEL;
typedef struct _DNS_SERVICE_REGISTER_REQUEST {
ULONG Version;
ULONG InterfaceIndex;
PDNS_SERVICE_INSTANCE pServiceInstance;
PDNS_SERVICE_REGISTER_COMPLETE pRegisterCompletionCallback;
PVOID pQueryContext;
HANDLE hCredentials;
BOOL unicastEnabled;
} DNS_SERVICE_REGISTER_REQUEST, *PDNS_SERVICE_REGISTER_REQUEST;
VOID DnsServiceFreeInstance(
_In_ PDNS_SERVICE_INSTANCE pInstance);
DWORD DnsServiceDeRegister(
_In_ PDNS_SERVICE_REGISTER_REQUEST pRequest,
_Inout_opt_ PDNS_SERVICE_CANCEL pCancel);
DWORD DnsServiceRegister(
_In_ PDNS_SERVICE_REGISTER_REQUEST pRequest,
_Inout_opt_ PDNS_SERVICE_CANCEL pCancel);
PDNS_SERVICE_INSTANCE DnsServiceConstructInstance(
_In_ PCWSTR pServiceName,
_In_ PCWSTR pHostName,
_In_opt_ PIP4_ADDRESS pIp4,
_In_opt_ PIP6_ADDRESS pIp6,
_In_ WORD wPort,
_In_ WORD wPriority,
_In_ WORD wWeight,
_In_ DWORD dwPropertiesCount,
_In_reads_(dwPropertiesCount) PCWSTR *keys,
_In_reads_(dwPropertiesCount) PCWSTR *values);
} /* extern "C" */
void print_status(const std::string_view &prefix, HRESULT status) {
char err_string[1024];
DWORD bytes = FormatMessage(FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS,
nullptr,
status,
MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
err_string,
sizeof(err_string),
nullptr);
BOOST_LOG(error) << prefix << ": "sv << std::string_view { err_string, bytes };
}
namespace platf::publish {
VOID WINAPI register_cb(DWORD status, PVOID pQueryContext, PDNS_SERVICE_INSTANCE pInstance) {
auto alarm = (safe::alarm_t<DNS_STATUS>::element_type *)pQueryContext;
auto fg = util::fail_guard([&]() {
if(pInstance) {
DnsServiceFreeInstance(pInstance);
}
});
if(status) {
print_status("register_cb()"sv, status);
alarm->ring(-1);
return;
}
alarm->ring(0);
}
static int service(bool enable) {
auto alarm = safe::make_alarm<DNS_STATUS>();
std::wstring_convert<std::codecvt_utf8_utf16<wchar_t>, wchar_t> converter;
std::wstring name { SERVICE_INSTANCE_NAME.data(), SERVICE_INSTANCE_NAME.size() };
std::wstring domain { SERVICE_TYPE_DOMAIN.data(), SERVICE_TYPE_DOMAIN.size() };
auto host = converter.from_bytes(boost::asio::ip::host_name() + ".local");
DNS_SERVICE_INSTANCE instance {};
instance.pszInstanceName = name.data();
instance.wPort = map_port(nvhttp::PORT_HTTP);
instance.pszHostName = host.data();
DNS_SERVICE_REGISTER_REQUEST req {};
req.Version = DNS_QUERY_REQUEST_VERSION1;
req.pQueryContext = alarm.get();
req.pServiceInstance = &instance;
req.pRegisterCompletionCallback = register_cb;
DNS_STATUS status {};
if(enable) {
status = DnsServiceRegister(&req, nullptr);
}
else {
status = DnsServiceDeRegister(&req, nullptr);
}
alarm->wait();
status = *alarm->status();
if(status) {
BOOST_LOG(error) << "No mDNS service"sv;
return -1;
}
return 0;
}
class deinit_t : public ::platf::deinit_t {
public:
~deinit_t() override {
if(service(false)) {
std::abort();
}
BOOST_LOG(info) << "Unregistered Sunshine Gamestream service"sv;
}
};
std::unique_ptr<::platf::deinit_t> start() {
if(service(true)) {
return nullptr;
}
BOOST_LOG(info) << "Registered Sunshine Gamestream service"sv;
return std::make_unique<deinit_t>();
}
} // namespace platf::publish

View File

@@ -1,729 +0,0 @@
//
// Created by loki on 1/12/20.
//
#include <dxgi.h>
#include <d3d11.h>
#include <d3dcommon.h>
#include <dxgi1_2.h>
#include <codecvt>
#include <sunshine/config.h>
#include "sunshine/main.h"
#include "common.h"
namespace platf {
using namespace std::literals;
}
namespace platf::dxgi {
template<class T>
void Release(T *dxgi) {
dxgi->Release();
}
using factory1_t = util::safe_ptr<IDXGIFactory1, Release<IDXGIFactory1>>;
using dxgi_t = util::safe_ptr<IDXGIDevice, Release<IDXGIDevice>>;
using dxgi1_t = util::safe_ptr<IDXGIDevice1, Release<IDXGIDevice1>>;
using device_t = util::safe_ptr<ID3D11Device, Release<ID3D11Device>>;
using device_ctx_t = util::safe_ptr<ID3D11DeviceContext, Release<ID3D11DeviceContext>>;
using adapter_t = util::safe_ptr<IDXGIAdapter1, Release<IDXGIAdapter1>>;
using output_t = util::safe_ptr<IDXGIOutput, Release<IDXGIOutput>>;
using output1_t = util::safe_ptr<IDXGIOutput1, Release<IDXGIOutput1>>;
using dup_t = util::safe_ptr<IDXGIOutputDuplication, Release<IDXGIOutputDuplication>>;
using texture2d_t = util::safe_ptr<ID3D11Texture2D, Release<ID3D11Texture2D>>;
using resource_t = util::safe_ptr<IDXGIResource, Release<IDXGIResource>>;
extern const char *format_str[];
class duplication_t {
public:
dup_t dup;
bool has_frame {};
capture_e next_frame(DXGI_OUTDUPL_FRAME_INFO &frame_info, resource_t::pointer *res_p) {
auto capture_status = release_frame();
if(capture_status != capture_e::ok) {
return capture_status;
}
auto status = dup->AcquireNextFrame(1000, &frame_info, res_p);
switch(status) {
case S_OK:
has_frame = true;
return capture_e::ok;
case DXGI_ERROR_WAIT_TIMEOUT:
return capture_e::timeout;
case WAIT_ABANDONED:
case DXGI_ERROR_ACCESS_LOST:
case DXGI_ERROR_ACCESS_DENIED:
return capture_e::reinit;
default:
BOOST_LOG(error) << "Couldn't acquire next frame [0x"sv << util::hex(status).to_string_view();
return capture_e::error;
}
}
capture_e reset(dup_t::pointer dup_p = dup_t::pointer()) {
auto capture_status = release_frame();
dup.reset(dup_p);
return capture_status;
}
capture_e release_frame() {
if(!has_frame) {
return capture_e::ok;
}
auto status = dup->ReleaseFrame();
switch (status) {
case S_OK:
has_frame = false;
return capture_e::ok;
case DXGI_ERROR_WAIT_TIMEOUT:
return capture_e::timeout;
case WAIT_ABANDONED:
case DXGI_ERROR_ACCESS_LOST:
case DXGI_ERROR_ACCESS_DENIED:
has_frame = false;
return capture_e::reinit;
default:
BOOST_LOG(error) << "Couldn't release frame [0x"sv << util::hex(status).to_string_view();
return capture_e::error;
}
}
~duplication_t() {
release_frame();
}
};
class display_t;
struct img_t : public ::platf::img_t {
~img_t() override {
delete[] data;
data = nullptr;
}
};
struct cursor_t {
std::vector<std::uint8_t> img_data;
DXGI_OUTDUPL_POINTER_SHAPE_INFO shape_info;
int x, y;
bool visible;
};
void blend_cursor_monochrome(const cursor_t &cursor, img_t &img) {
int height = cursor.shape_info.Height / 2;
int width = cursor.shape_info.Width;
int pitch = cursor.shape_info.Pitch;
// img cursor.y < 0, skip parts of the cursor.img_data
auto cursor_skip_y = std::min(0, cursor.y);
auto cursor_skip_x = std::min(0, cursor.x);
auto img_skip_y = std::max(0, cursor.y);
auto img_skip_x = std::max(0, cursor.x);
if(cursor_skip_y > height || cursor_skip_x > width) {
return;
}
height -= cursor_skip_y;
width -= cursor_skip_x;
auto cursor_img_data = cursor.img_data.data() + cursor_skip_y * pitch;
int delta_height = std::min(height, std::max(0, img.height - img_skip_y));
int delta_width = std::min(width, std::max(0, img.width - img_skip_x));
auto pixels_per_byte = width / pitch;
auto bytes_per_row = delta_width / pixels_per_byte;
auto img_data = (int*)img.data;
for(int i = 0; i < delta_height; ++i) {
auto and_mask = &cursor_img_data[i * pitch];
auto xor_mask = &cursor_img_data[(i + height) * pitch];
auto img_pixel_p = &img_data[(i + img_skip_y) * (img.row_pitch / img.pixel_pitch) + img_skip_x];
auto skip_y = cursor_skip_y;
for(int x = 0; x < bytes_per_row; ++x) {
for(auto bit = 0u; bit < 8; ++bit) {
if(skip_y > 0) {
--skip_y;
continue;
}
int and_ = *and_mask & (1 << (7 - bit)) ? -1 : 0;
int xor_ = *xor_mask & (1 << (7 - bit)) ? -1 : 0;
*img_pixel_p &= and_;
*img_pixel_p ^= xor_;
++img_pixel_p;
}
++and_mask;
++xor_mask;
}
}
}
void blend_cursor_color(const cursor_t &cursor, img_t &img) {
int height = cursor.shape_info.Height;
int width = cursor.shape_info.Width;
int pitch = cursor.shape_info.Pitch;
// img cursor.y < 0, skip parts of the cursor.img_data
auto cursor_skip_y = std::min(0, cursor.y);
auto cursor_skip_x = std::min(0, cursor.x);
auto img_skip_y = std::max(0, cursor.y);
auto img_skip_x = std::max(0, cursor.x);
if(cursor_skip_y > height || cursor_skip_x > width) {
return;
}
height -= cursor_skip_y;
width -= cursor_skip_x;
auto cursor_img_data = (int*)&cursor.img_data[cursor_skip_y * pitch];
int delta_height = std::min(height, std::max(0, img.height - img_skip_y));
int delta_width = std::min(width, std::max(0, img.width - img_skip_x));
auto img_data = (int*)img.data;
for(int i = 0; i < delta_height; ++i) {
auto cursor_begin = &cursor_img_data[i * width + cursor_skip_x];
auto cursor_end = &cursor_begin[delta_width];
auto img_pixel_p = &img_data[(i + img_skip_y) * (img.row_pitch / img.pixel_pitch) + img_skip_x];
std::for_each(cursor_begin, cursor_end, [&](int cursor_pixel) {
auto colors_out = (std::uint8_t*)&cursor_pixel;
auto colors_in = (std::uint8_t*)img_pixel_p;
//TODO: When use of IDXGIOutput5 is implemented, support different color formats
auto alpha = colors_out[3];
if(alpha == 255) {
*img_pixel_p = cursor_pixel;
}
else {
colors_in[0] = colors_out[0] + (colors_in[0] * (255 - alpha) + 255/2) / 255;
colors_in[1] = colors_out[1] + (colors_in[1] * (255 - alpha) + 255/2) / 255;
colors_in[2] = colors_out[2] + (colors_in[2] * (255 - alpha) + 255/2) / 255;
}
++img_pixel_p;
});
}
}
void blend_cursor(const cursor_t &cursor, img_t &img) {
switch(cursor.shape_info.Type) {
case DXGI_OUTDUPL_POINTER_SHAPE_TYPE_COLOR:
blend_cursor_color(cursor, img);
break;
case DXGI_OUTDUPL_POINTER_SHAPE_TYPE_MONOCHROME:
blend_cursor_monochrome(cursor, img);
break;
case DXGI_OUTDUPL_POINTER_SHAPE_TYPE_MASKED_COLOR:
default:
BOOST_LOG(warning) << "Unsupported cursor format ["sv << cursor.shape_info.Type << ']';
}
}
class display_t : public ::platf::display_t {
public:
capture_e snapshot(::platf::img_t *img_base, bool cursor_visible) override {
auto img = (img_t *) img_base;
HRESULT status;
DXGI_OUTDUPL_FRAME_INFO frame_info;
resource_t::pointer res_p {};
auto capture_status = dup.next_frame(frame_info, &res_p);
resource_t res{res_p};
if (capture_status != capture_e::ok) {
return capture_status;
}
if (frame_info.PointerShapeBufferSize > 0) {
auto &img_data = cursor.img_data;
img_data.resize(frame_info.PointerShapeBufferSize);
UINT dummy;
status = dup.dup->GetFramePointerShape(img_data.size(), img_data.data(), &dummy, &cursor.shape_info);
if (FAILED(status)) {
BOOST_LOG(error) << "Failed to get new pointer shape [0x"sv << util::hex(status).to_string_view() << ']';
return capture_e::error;
}
}
if(frame_info.LastMouseUpdateTime.QuadPart) {
cursor.x = frame_info.PointerPosition.Position.x;
cursor.y = frame_info.PointerPosition.Position.y;
cursor.visible = frame_info.PointerPosition.Visible;
}
// If frame has been updated
if (frame_info.LastPresentTime.QuadPart != 0) {
{
texture2d_t::pointer src_p {};
status = res->QueryInterface(IID_ID3D11Texture2D, (void **)&src_p);
texture2d_t src{src_p};
if (FAILED(status)) {
BOOST_LOG(error) << "Couldn't query interface [0x"sv << util::hex(status).to_string_view() << ']';
return capture_e::error;
}
//Copy from GPU to CPU
device_ctx->CopyResource(texture.get(), src.get());
}
if(current_img.pData) {
device_ctx->Unmap(texture.get(), 0);
current_img.pData = nullptr;
}
status = device_ctx->Map(texture.get(), 0, D3D11_MAP_READ, 0, &current_img);
if (FAILED(status)) {
BOOST_LOG(error) << "Failed to map texture [0x"sv << util::hex(status).to_string_view() << ']';
return capture_e::error;
}
}
const bool update_flag =
frame_info.LastMouseUpdateTime.QuadPart ||
frame_info.LastPresentTime.QuadPart != 0 ||
frame_info.PointerShapeBufferSize > 0;
if(!update_flag) {
return capture_e::timeout;
}
if(img->width != width || img->height != height) {
delete[] img->data;
img->data = new std::uint8_t[height * current_img.RowPitch];
img->width = width;
img->height = height;
img->row_pitch = current_img.RowPitch;
}
std::copy_n((std::uint8_t*)current_img.pData, height * current_img.RowPitch, (std::uint8_t*)img->data);
if(cursor_visible && cursor.visible) {
blend_cursor(cursor, *img);
}
return capture_e::ok;
}
std::unique_ptr<::platf::img_t> alloc_img() override {
auto img = std::make_unique<img_t>();
img->data = nullptr;
img->height = 0;
img->width = 0;
img->row_pitch = 0;
img->pixel_pitch = 4;
return img;
}
int init() {
/* Uncomment when use of IDXGIOutput5 is implemented
std::call_once(windows_cpp_once_flag, []() {
DECLARE_HANDLE(DPI_AWARENESS_CONTEXT);
const auto DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2 = ((DPI_AWARENESS_CONTEXT)-4);
typedef BOOL (*User32_SetProcessDpiAwarenessContext)(DPI_AWARENESS_CONTEXT value);
auto user32 = LoadLibraryA("user32.dll");
auto f = (User32_SetProcessDpiAwarenessContext)GetProcAddress(user32, "SetProcessDpiAwarenessContext");
if(f) {
f(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2);
}
FreeLibrary(user32);
});
*/
current_img.pData = nullptr; // current_img is not yet mapped
dxgi::factory1_t::pointer factory_p {};
dxgi::adapter_t::pointer adapter_p {};
dxgi::output_t::pointer output_p {};
dxgi::device_t::pointer device_p {};
dxgi::device_ctx_t::pointer device_ctx_p {};
HRESULT status;
status = CreateDXGIFactory1(IID_IDXGIFactory1, (void**)&factory_p);
factory.reset(factory_p);
if(FAILED(status)) {
BOOST_LOG(error) << "Failed to create DXGIFactory1 [0x"sv << util::hex(status).to_string_view() << ']';
return -1;
}
std::wstring_convert<std::codecvt_utf8_utf16<wchar_t>, wchar_t> converter;
auto adapter_name = converter.from_bytes(config::video.adapter_name);
auto output_name = converter.from_bytes(config::video.output_name);
for(int x = 0; factory_p->EnumAdapters1(x, &adapter_p) != DXGI_ERROR_NOT_FOUND; ++x) {
dxgi::adapter_t adapter_tmp { adapter_p };
DXGI_ADAPTER_DESC1 adapter_desc;
adapter_tmp->GetDesc1(&adapter_desc);
if(!adapter_name.empty() && adapter_desc.Description != adapter_name) {
continue;
}
for(int y = 0; adapter_tmp->EnumOutputs(y, &output_p) != DXGI_ERROR_NOT_FOUND; ++y) {
dxgi::output_t output_tmp {output_p };
DXGI_OUTPUT_DESC desc;
output_tmp->GetDesc(&desc);
if(!output_name.empty() && desc.DeviceName != output_name) {
continue;
}
if(desc.AttachedToDesktop) {
output = std::move(output_tmp);
width = desc.DesktopCoordinates.right - desc.DesktopCoordinates.left;
height = desc.DesktopCoordinates.bottom - desc.DesktopCoordinates.top;
}
}
if(output) {
adapter = std::move(adapter_tmp);
break;
}
}
if(!output) {
BOOST_LOG(error) << "Failed to locate an output device"sv;
return -1;
}
D3D_FEATURE_LEVEL featureLevels[] {
D3D_FEATURE_LEVEL_12_1,
D3D_FEATURE_LEVEL_12_0,
D3D_FEATURE_LEVEL_11_1,
D3D_FEATURE_LEVEL_11_0,
D3D_FEATURE_LEVEL_10_1,
D3D_FEATURE_LEVEL_10_0,
D3D_FEATURE_LEVEL_9_3,
D3D_FEATURE_LEVEL_9_2,
D3D_FEATURE_LEVEL_9_1
};
status = adapter->QueryInterface(IID_IDXGIAdapter, (void**)&adapter_p);
if(FAILED(status)) {
BOOST_LOG(error) << "Failed to query IDXGIAdapter interface"sv;
return -1;
}
status = D3D11CreateDevice(
adapter_p,
D3D_DRIVER_TYPE_UNKNOWN,
nullptr,
D3D11_CREATE_DEVICE_VIDEO_SUPPORT,
featureLevels, sizeof(featureLevels) / sizeof(D3D_FEATURE_LEVEL),
D3D11_SDK_VERSION,
&device_p,
&feature_level,
&device_ctx_p);
adapter_p->Release();
device.reset(device_p);
device_ctx.reset(device_ctx_p);
if(FAILED(status)) {
BOOST_LOG(error) << "Failed to create D3D11 device [0x"sv << util::hex(status).to_string_view() << ']';
return -1;
}
DXGI_ADAPTER_DESC adapter_desc;
adapter->GetDesc(&adapter_desc);
auto description = converter.to_bytes(adapter_desc.Description);
BOOST_LOG(info) << std::endl
<< "Device Description : " << description << std::endl
<< "Device Vendor ID : 0x"sv << util::hex(adapter_desc.VendorId).to_string_view() << std::endl
<< "Device Device ID : 0x"sv << util::hex(adapter_desc.DeviceId).to_string_view() << std::endl
<< "Device Video Mem : "sv << adapter_desc.DedicatedVideoMemory / 1048576 << " MiB"sv << std::endl
<< "Device Sys Mem : "sv << adapter_desc.DedicatedSystemMemory / 1048576 << " MiB"sv << std::endl
<< "Share Sys Mem : "sv << adapter_desc.SharedSystemMemory / 1048576 << " MiB"sv << std::endl
<< "Feature Level : 0x"sv << util::hex(feature_level).to_string_view() << std::endl
<< "Capture size : "sv << width << 'x' << height;
// Bump up thread priority
{
dxgi::dxgi_t::pointer dxgi_p {};
status = device->QueryInterface(IID_IDXGIDevice, (void**)&dxgi_p);
dxgi::dxgi_t dxgi { dxgi_p };
if(FAILED(status)) {
BOOST_LOG(error) << "Failed to query DXGI interface from device [0x"sv << util::hex(status).to_string_view() << ']';
return -1;
}
dxgi->SetGPUThreadPriority(7);
}
// Try to reduce latency
{
dxgi::dxgi1_t::pointer dxgi_p {};
status = device->QueryInterface(IID_IDXGIDevice, (void**)&dxgi_p);
dxgi::dxgi1_t dxgi { dxgi_p };
if(FAILED(status)) {
BOOST_LOG(error) << "Failed to query DXGI interface from device [0x"sv << util::hex(status).to_string_view() << ']';
return -1;
}
dxgi->SetMaximumFrameLatency(1);
}
//FIXME: Duplicate output on RX580 in combination with DOOM (2016) --> BSOD
//TODO: Use IDXGIOutput5 for improved performance
{
dxgi::output1_t::pointer output1_p {};
status = output->QueryInterface(IID_IDXGIOutput1, (void**)&output1_p);
dxgi::output1_t output1 {output1_p };
if(FAILED(status)) {
BOOST_LOG(error) << "Failed to query IDXGIOutput1 from the output"sv;
return -1;
}
// We try this twice, in case we still get an error on reinitialization
for(int x = 0; x < 2; ++x) {
dxgi::dup_t::pointer dup_p {};
status = output1->DuplicateOutput((IUnknown*)device.get(), &dup_p);
if(SUCCEEDED(status)) {
dup.reset(dup_p);
break;
}
std::this_thread::sleep_for(200ms);
}
if(FAILED(status)) {
BOOST_LOG(error) << "DuplicateOutput Failed [0x"sv << util::hex(status).to_string_view() << ']';
return -1;
}
}
DXGI_OUTDUPL_DESC dup_desc;
dup.dup->GetDesc(&dup_desc);
format = dup_desc.ModeDesc.Format;
BOOST_LOG(debug) << "Source format ["sv << format_str[dup_desc.ModeDesc.Format] << ']';
D3D11_TEXTURE2D_DESC t {};
t.Width = width;
t.Height = height;
t.MipLevels = 1;
t.ArraySize = 1;
t.SampleDesc.Count = 1;
t.Usage = D3D11_USAGE_STAGING;
t.Format = format;
t.CPUAccessFlags = D3D11_CPU_ACCESS_READ;
dxgi::texture2d_t::pointer tex_p {};
status = device->CreateTexture2D(&t, nullptr, &tex_p);
texture.reset(tex_p);
if(FAILED(status)) {
BOOST_LOG(error) << "Failed to create texture [0x"sv << util::hex(status).to_string_view() << ']';
return -1;
}
// map the texture simply to get the pitch and stride
status = device_ctx->Map(texture.get(), 0, D3D11_MAP_READ, 0, &current_img);
if(FAILED(status)) {
BOOST_LOG(error) << "Error: Failed to map the texture [0x"sv << util::hex(status).to_string_view() << ']';
return -1;
}
return 0;
}
~display_t() override {
if(current_img.pData) {
device_ctx->Unmap(texture.get(), 0);
current_img.pData = nullptr;
}
}
factory1_t factory;
adapter_t adapter;
output_t output;
device_t device;
device_ctx_t device_ctx;
duplication_t dup;
cursor_t cursor;
texture2d_t texture;
int width, height;
DXGI_FORMAT format;
D3D_FEATURE_LEVEL feature_level;
D3D11_MAPPED_SUBRESOURCE current_img;
};
const char *format_str[] = {
"DXGI_FORMAT_UNKNOWN",
"DXGI_FORMAT_R32G32B32A32_TYPELESS",
"DXGI_FORMAT_R32G32B32A32_FLOAT",
"DXGI_FORMAT_R32G32B32A32_UINT",
"DXGI_FORMAT_R32G32B32A32_SINT",
"DXGI_FORMAT_R32G32B32_TYPELESS",
"DXGI_FORMAT_R32G32B32_FLOAT",
"DXGI_FORMAT_R32G32B32_UINT",
"DXGI_FORMAT_R32G32B32_SINT",
"DXGI_FORMAT_R16G16B16A16_TYPELESS",
"DXGI_FORMAT_R16G16B16A16_FLOAT",
"DXGI_FORMAT_R16G16B16A16_UNORM",
"DXGI_FORMAT_R16G16B16A16_UINT",
"DXGI_FORMAT_R16G16B16A16_SNORM",
"DXGI_FORMAT_R16G16B16A16_SINT",
"DXGI_FORMAT_R32G32_TYPELESS",
"DXGI_FORMAT_R32G32_FLOAT",
"DXGI_FORMAT_R32G32_UINT",
"DXGI_FORMAT_R32G32_SINT",
"DXGI_FORMAT_R32G8X24_TYPELESS",
"DXGI_FORMAT_D32_FLOAT_S8X24_UINT",
"DXGI_FORMAT_R32_FLOAT_X8X24_TYPELESS",
"DXGI_FORMAT_X32_TYPELESS_G8X24_UINT",
"DXGI_FORMAT_R10G10B10A2_TYPELESS",
"DXGI_FORMAT_R10G10B10A2_UNORM",
"DXGI_FORMAT_R10G10B10A2_UINT",
"DXGI_FORMAT_R11G11B10_FLOAT",
"DXGI_FORMAT_R8G8B8A8_TYPELESS",
"DXGI_FORMAT_R8G8B8A8_UNORM",
"DXGI_FORMAT_R8G8B8A8_UNORM_SRGB",
"DXGI_FORMAT_R8G8B8A8_UINT",
"DXGI_FORMAT_R8G8B8A8_SNORM",
"DXGI_FORMAT_R8G8B8A8_SINT",
"DXGI_FORMAT_R16G16_TYPELESS",
"DXGI_FORMAT_R16G16_FLOAT",
"DXGI_FORMAT_R16G16_UNORM",
"DXGI_FORMAT_R16G16_UINT",
"DXGI_FORMAT_R16G16_SNORM",
"DXGI_FORMAT_R16G16_SINT",
"DXGI_FORMAT_R32_TYPELESS",
"DXGI_FORMAT_D32_FLOAT",
"DXGI_FORMAT_R32_FLOAT",
"DXGI_FORMAT_R32_UINT",
"DXGI_FORMAT_R32_SINT",
"DXGI_FORMAT_R24G8_TYPELESS",
"DXGI_FORMAT_D24_UNORM_S8_UINT",
"DXGI_FORMAT_R24_UNORM_X8_TYPELESS",
"DXGI_FORMAT_X24_TYPELESS_G8_UINT",
"DXGI_FORMAT_R8G8_TYPELESS",
"DXGI_FORMAT_R8G8_UNORM",
"DXGI_FORMAT_R8G8_UINT",
"DXGI_FORMAT_R8G8_SNORM",
"DXGI_FORMAT_R8G8_SINT",
"DXGI_FORMAT_R16_TYPELESS",
"DXGI_FORMAT_R16_FLOAT",
"DXGI_FORMAT_D16_UNORM",
"DXGI_FORMAT_R16_UNORM",
"DXGI_FORMAT_R16_UINT",
"DXGI_FORMAT_R16_SNORM",
"DXGI_FORMAT_R16_SINT",
"DXGI_FORMAT_R8_TYPELESS",
"DXGI_FORMAT_R8_UNORM",
"DXGI_FORMAT_R8_UINT",
"DXGI_FORMAT_R8_SNORM",
"DXGI_FORMAT_R8_SINT",
"DXGI_FORMAT_A8_UNORM",
"DXGI_FORMAT_R1_UNORM",
"DXGI_FORMAT_R9G9B9E5_SHAREDEXP",
"DXGI_FORMAT_R8G8_B8G8_UNORM",
"DXGI_FORMAT_G8R8_G8B8_UNORM",
"DXGI_FORMAT_BC1_TYPELESS",
"DXGI_FORMAT_BC1_UNORM",
"DXGI_FORMAT_BC1_UNORM_SRGB",
"DXGI_FORMAT_BC2_TYPELESS",
"DXGI_FORMAT_BC2_UNORM",
"DXGI_FORMAT_BC2_UNORM_SRGB",
"DXGI_FORMAT_BC3_TYPELESS",
"DXGI_FORMAT_BC3_UNORM",
"DXGI_FORMAT_BC3_UNORM_SRGB",
"DXGI_FORMAT_BC4_TYPELESS",
"DXGI_FORMAT_BC4_UNORM",
"DXGI_FORMAT_BC4_SNORM",
"DXGI_FORMAT_BC5_TYPELESS",
"DXGI_FORMAT_BC5_UNORM",
"DXGI_FORMAT_BC5_SNORM",
"DXGI_FORMAT_B5G6R5_UNORM",
"DXGI_FORMAT_B5G5R5A1_UNORM",
"DXGI_FORMAT_B8G8R8A8_UNORM",
"DXGI_FORMAT_B8G8R8X8_UNORM",
"DXGI_FORMAT_R10G10B10_XR_BIAS_A2_UNORM",
"DXGI_FORMAT_B8G8R8A8_TYPELESS",
"DXGI_FORMAT_B8G8R8A8_UNORM_SRGB",
"DXGI_FORMAT_B8G8R8X8_TYPELESS",
"DXGI_FORMAT_B8G8R8X8_UNORM_SRGB",
"DXGI_FORMAT_BC6H_TYPELESS",
"DXGI_FORMAT_BC6H_UF16",
"DXGI_FORMAT_BC6H_SF16",
"DXGI_FORMAT_BC7_TYPELESS",
"DXGI_FORMAT_BC7_UNORM",
"DXGI_FORMAT_BC7_UNORM_SRGB",
"DXGI_FORMAT_AYUV",
"DXGI_FORMAT_Y410",
"DXGI_FORMAT_Y416",
"DXGI_FORMAT_NV12",
"DXGI_FORMAT_P010",
"DXGI_FORMAT_P016",
"DXGI_FORMAT_420_OPAQUE",
"DXGI_FORMAT_YUY2",
"DXGI_FORMAT_Y210",
"DXGI_FORMAT_Y216",
"DXGI_FORMAT_NV11",
"DXGI_FORMAT_AI44",
"DXGI_FORMAT_IA44",
"DXGI_FORMAT_P8",
"DXGI_FORMAT_A8P8",
"DXGI_FORMAT_B4G4R4A4_UNORM",
NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL,
"DXGI_FORMAT_P208",
"DXGI_FORMAT_V208",
"DXGI_FORMAT_V408"
};
}
namespace platf {
std::shared_ptr<display_t> display() {
auto disp = std::make_unique<dxgi::display_t>();
if (disp->init()) {
return nullptr;
}
return disp;
}
}

View File

@@ -1,329 +0,0 @@
//
// Created by loki on 1/12/20.
//
#include <roapi.h>
#include <mmdeviceapi.h>
#include <audioclient.h>
#include <codecvt>
#include <synchapi.h>
#include "sunshine/config.h"
#include "sunshine/main.h"
#include "common.h"
const CLSID CLSID_MMDeviceEnumerator = __uuidof(MMDeviceEnumerator);
const IID IID_IMMDeviceEnumerator = __uuidof(IMMDeviceEnumerator);
const IID IID_IAudioClient = __uuidof(IAudioClient);
const IID IID_IAudioCaptureClient = __uuidof(IAudioCaptureClient);
using namespace std::literals;
namespace platf::audio {
template<class T>
void Release(T *p) {
p->Release();
}
template<class T>
void co_task_free(T *p) {
CoTaskMemFree((LPVOID)p);
}
using device_enum_t = util::safe_ptr<IMMDeviceEnumerator, Release<IMMDeviceEnumerator>>;
using device_t = util::safe_ptr<IMMDevice, Release<IMMDevice>>;
using audio_client_t = util::safe_ptr<IAudioClient, Release<IAudioClient>>;
using audio_capture_t = util::safe_ptr<IAudioCaptureClient, Release<IAudioCaptureClient>>;
using wave_format_t = util::safe_ptr<WAVEFORMATEX, co_task_free<WAVEFORMATEX>>;
using handle_t = util::safe_ptr_v2<void, BOOL, CloseHandle>;
class mic_wasapi_t : public mic_t {
public:
capture_e sample(std::vector<std::int16_t> &sample_in) override {
while(sample_buf_pos - std::begin(sample_buf) < sample_in.size()) {
//FIXME: Use IAudioClient3 instead of IAudioClient, that would allows for adjusting the latency of the audio samples
auto capture_result = _fill_buffer();
if(capture_result != capture_e::ok) {
return capture_result;
}
}
std::copy_n(std::begin(sample_buf), sample_in.size(), std::begin(sample_in));
// The excess samples should be in front of the queue
std::move(&sample_buf[sample_in.size()], sample_buf_pos, std::begin(sample_buf));
sample_buf_pos -= sample_in.size();
return capture_e::ok;
}
int init(std::uint32_t sample_rate) {
audio_event.reset(CreateEventA(nullptr, FALSE, FALSE, nullptr));
if(!audio_event) {
BOOST_LOG(error) << "Couldn't create Event handle"sv;
return -1;
}
HRESULT status;
device_enum_t::pointer device_enum_p{};
status = CoCreateInstance(
CLSID_MMDeviceEnumerator,
nullptr,
CLSCTX_ALL,
IID_IMMDeviceEnumerator,
(void **) &device_enum_p);
device_enum.reset(device_enum_p);
if (FAILED(status)) {
BOOST_LOG(error) << "Couldn't create Device Enumerator [0x"sv << util::hex(status).to_string_view() << ']';
return -1;
}
device_t::pointer device_p{};
if(config::audio.sink.empty()) {
status = device_enum->GetDefaultAudioEndpoint(
eRender,
eConsole,
&device_p);
}
else {
std::wstring_convert<std::codecvt_utf8_utf16<wchar_t>, wchar_t> converter;
auto wstring_device_id = converter.from_bytes(config::audio.sink);
status = device_enum->GetDevice(wstring_device_id.c_str(), &device_p);
}
device.reset(device_p);
if (FAILED(status)) {
BOOST_LOG(error) << "Couldn't create audio Device [0x"sv << util::hex(status).to_string_view() << ']';
return -1;
}
audio_client_t::pointer audio_client_p{};
status = device->Activate(
IID_IAudioClient,
CLSCTX_ALL,
nullptr,
(void **) &audio_client_p);
audio_client.reset(audio_client_p);
if (FAILED(status)) {
BOOST_LOG(error) << "Couldn't activate audio Device [0x"sv << util::hex(status).to_string_view() << ']';
return -1;
}
wave_format_t::pointer wave_format_p{};
status = audio_client->GetMixFormat(&wave_format_p);
wave_format.reset(wave_format_p);
if (FAILED(status)) {
BOOST_LOG(error) << "Couldn't acquire Wave Format [0x"sv << util::hex(status).to_string_view() << ']';
return -1;
}
wave_format->nChannels = 2;
wave_format->wBitsPerSample = 16;
wave_format->nSamplesPerSec = sample_rate;
wave_format->nBlockAlign = wave_format->nChannels * wave_format->wBitsPerSample / 8;
wave_format->nAvgBytesPerSec = wave_format->nSamplesPerSec * wave_format->nBlockAlign;
switch(wave_format->wFormatTag) {
case WAVE_FORMAT_PCM:
break;
case WAVE_FORMAT_IEEE_FLOAT:
wave_format->wFormatTag = WAVE_FORMAT_PCM;
break;
case WAVE_FORMAT_EXTENSIBLE: {
auto wave_ex = (PWAVEFORMATEXTENSIBLE) wave_format.get();
if (IsEqualGUID(KSDATAFORMAT_SUBTYPE_IEEE_FLOAT, wave_ex->SubFormat)) {
wave_ex->SubFormat = KSDATAFORMAT_SUBTYPE_PCM;
wave_ex->Samples.wValidBitsPerSample = 16;
break;
}
BOOST_LOG(error) << "Unsupported Sub Format for WAVE_FORMAT_EXTENSIBLE: [0x"sv << util::hex(wave_ex->SubFormat).to_string_view() << ']';
return -1;
}
default:
BOOST_LOG(error) << "Unsupported Wave Format: [0x"sv << util::hex(wave_format->wFormatTag).to_string_view() << ']';
return -1;
};
REFERENCE_TIME default_latency;
audio_client->GetDevicePeriod(&default_latency, nullptr);
default_latency_ms = default_latency / 10;
status = audio_client->Initialize(
AUDCLNT_SHAREMODE_SHARED,
AUDCLNT_STREAMFLAGS_LOOPBACK | AUDCLNT_STREAMFLAGS_EVENTCALLBACK,
0, 0,
wave_format.get(),
nullptr);
if (FAILED(status)) {
BOOST_LOG(error) << "Couldn't initialize audio client [0x"sv << util::hex(status).to_string_view() << ']';
return -1;
}
std::uint32_t frames;
status = audio_client->GetBufferSize(&frames);
if (FAILED(status)) {
BOOST_LOG(error) << "Couldn't acquire the number of audio frames [0x"sv << util::hex(status).to_string_view() << ']';
return -1;
}
sample_buf = util::buffer_t<std::int16_t> { frames };
sample_buf_pos = std::begin(sample_buf);
audio_capture_t::pointer audio_capture_p {};
status = audio_client->GetService(IID_IAudioCaptureClient, (void**)&audio_capture_p);
audio_capture.reset(audio_capture_p);
if (FAILED(status)) {
BOOST_LOG(error) << "Couldn't initialize audio capture client [0x"sv << util::hex(status).to_string_view() << ']';
return -1;
}
status = audio_client->SetEventHandle(audio_event.get());
if (FAILED(status)) {
BOOST_LOG(error) << "Couldn't set event handle [0x"sv << util::hex(status).to_string_view() << ']';
return -1;
}
status = audio_client->Start();
if (FAILED(status)) {
BOOST_LOG(error) << "Couldn't start recording [0x"sv << util::hex(status).to_string_view() << ']';
return -1;
}
return 0;
}
~mic_wasapi_t() override {
if(audio_client) {
audio_client->Stop();
}
}
private:
capture_e _fill_buffer() {
HRESULT status;
// Total number of samples
struct sample_aligned_t {
std::uint32_t uninitialized;
std::int16_t *samples;
} sample_aligned;
// number of samples / number of channels
struct block_aligned_t {
std::uint32_t audio_sample_size;
} block_aligned;
status = WaitForSingleObjectEx(audio_event.get(), default_latency_ms, FALSE);
switch (status) {
case WAIT_OBJECT_0:
break;
case WAIT_TIMEOUT:
std::fill_n(std::begin(sample_buf), sample_buf.size(), 0);
return capture_e::timeout;
default:
BOOST_LOG(error) << "Couldn't wait for audio event: [0x"sv << util::hex(status).to_string_view() << ']';
return capture_e::error;
}
std::uint32_t packet_size{};
for (
status = audio_capture->GetNextPacketSize(&packet_size);
SUCCEEDED(status) && packet_size > 0;
status = audio_capture->GetNextPacketSize(&packet_size)
) {
DWORD buffer_flags;
status = audio_capture->GetBuffer(
(BYTE **) &sample_aligned.samples,
&block_aligned.audio_sample_size,
&buffer_flags,
nullptr, nullptr);
switch (status) {
case S_OK:
break;
case AUDCLNT_E_DEVICE_INVALIDATED:
return capture_e::reinit;
default:
BOOST_LOG(error) << "Couldn't capture audio [0x"sv << util::hex(status).to_string_view() << ']';
return capture_e::error;
}
sample_aligned.uninitialized = std::end(sample_buf) - sample_buf_pos;
auto n = std::min(sample_aligned.uninitialized, block_aligned.audio_sample_size * wave_format->nChannels);
if (buffer_flags & AUDCLNT_BUFFERFLAGS_SILENT) {
std::fill_n(sample_buf_pos, n, 0);
} else {
std::copy_n(sample_aligned.samples, n, sample_buf_pos);
}
sample_buf_pos += n;
audio_capture->ReleaseBuffer(block_aligned.audio_sample_size);
}
if (status == AUDCLNT_E_DEVICE_INVALIDATED) {
return capture_e::reinit;
}
if (FAILED(status)) {
return capture_e::error;
}
return capture_e::ok;
}
public:
handle_t audio_event;
device_enum_t device_enum;
device_t device;
audio_client_t audio_client;
wave_format_t wave_format;
audio_capture_t audio_capture;
REFERENCE_TIME default_latency_ms;
util::buffer_t<std::int16_t> sample_buf;
std::int16_t *sample_buf_pos;
};
}
namespace platf {
class dummy_mic_t : public mic_t {
public:
capture_e sample(std::vector<std::int16_t> &sample_buf) override {
return capture_e::ok;
}
};
std::unique_ptr<mic_t> microphone(std::uint32_t sample_rate) {
Windows::Foundation::Initialize(RO_INIT_MULTITHREADED);
auto mic = std::make_unique<audio::mic_wasapi_t>();
if(mic->init(sample_rate)) {
return std::make_unique<dummy_mic_t>();
}
return mic;
}
}

View File

@@ -2,16 +2,18 @@
// Created by loki on 12/14/19.
//
#define BOOST_BIND_GLOBAL_PLACEHOLDERS
#include "process.h"
#include <vector>
#include <string>
#include <vector>
#include <boost/property_tree/ptree.hpp>
#include <boost/property_tree/json_parser.hpp>
#include <boost/property_tree/ptree.hpp>
#include "utility.h"
#include "main.h"
#include "utility.h"
namespace proc {
using namespace std::literals;
@@ -48,7 +50,7 @@ int proc_t::execute(int app_id) {
_app_id = -1;
}
if(app_id >= _apps.size()) {
if(app_id < 0 || app_id >= _apps.size()) {
BOOST_LOG(error) << "Couldn't find app with ID ["sv << app_id << ']';
return 404;
@@ -57,11 +59,11 @@ int proc_t::execute(int app_id) {
// Ensure starting from a clean slate
terminate();
_app_id = app_id;
_app_id = app_id;
auto &proc = _apps[app_id];
_undo_begin = std::begin(proc.prep_cmds);
_undo_it = _undo_begin;
_undo_it = _undo_begin;
if(!proc.output.empty() && proc.output != "null"sv) {
_pipe.reset(fopen(proc.output.c_str(), "a"));
@@ -80,17 +82,31 @@ int proc_t::execute(int app_id) {
auto ret = exe(cmd, _env, _pipe, ec);
if(ec) {
BOOST_LOG(error) << "System: "sv << ec.message();
BOOST_LOG(error) << "Couldn't run ["sv << cmd << "]: System: "sv << ec.message();
return -1;
}
if(ret != 0) {
BOOST_LOG(error) << "Return code ["sv << ret << ']';
BOOST_LOG(error) << '[' << cmd << "] failed with code ["sv << ret << ']';
return -1;
}
}
BOOST_LOG(debug) << "Starting ["sv << proc.cmd << ']';
for(auto &cmd : proc.detached) {
BOOST_LOG(info) << "Spawning ["sv << cmd << ']';
if(proc.output.empty()) {
bp::spawn(cmd, _env, bp::std_out > bp::null, bp::std_err > bp::null, ec);
}
else {
bp::spawn(cmd, _env, bp::std_out > _pipe.get(), bp::std_err > _pipe.get(), ec);
}
if(ec) {
BOOST_LOG(warning) << "Couldn't spawn ["sv << cmd << "]: System: "sv << ec.message();
}
}
BOOST_LOG(info) << "Executing: ["sv << proc.cmd << ']';
if(proc.cmd.empty()) {
placebo = true;
}
@@ -102,7 +118,7 @@ int proc_t::execute(int app_id) {
}
if(ec) {
BOOST_LOG(info) << "System: "sv << ec.message();
BOOST_LOG(warning) << "Couldn't run ["sv << proc.cmd << "]: System: "sv << ec.message();
return -1;
}
@@ -133,7 +149,7 @@ void proc_t::terminate() {
std::abort();
}
for(;_undo_it != _undo_begin; --_undo_it) {
for(; _undo_it != _undo_begin; --_undo_it) {
auto &cmd = (_undo_it - 1)->undo_cmd;
if(cmd.empty()) {
@@ -163,6 +179,9 @@ void proc_t::terminate() {
const std::vector<ctx_t> &proc_t::get_apps() const {
return _apps;
}
std::vector<ctx_t> &proc_t::get_apps() {
return _apps;
}
proc_t::~proc_t() {
terminate();
@@ -175,9 +194,11 @@ std::string_view::iterator find_match(std::string_view::iterator begin, std::str
do {
++begin;
switch(*begin) {
case '(': ++stack;
case '(':
++stack;
break;
case ')': --stack;
case ')':
--stack;
}
} while(begin != end && stack != 0);
@@ -188,7 +209,7 @@ std::string_view::iterator find_match(std::string_view::iterator begin, std::str
}
std::string parse_env_val(bp::native_environment &env, const std::string_view &val_raw) {
auto pos = std::begin(val_raw);
auto pos = std::begin(val_raw);
auto dollar = std::find(pos, std::end(val_raw), '$');
std::stringstream ss;
@@ -197,23 +218,23 @@ std::string parse_env_val(bp::native_environment &env, const std::string_view &v
auto next = dollar + 1;
if(next != std::end(val_raw)) {
switch(*next) {
case '(': {
ss.write(pos, (dollar - pos));
auto var_begin = next + 1;
auto var_end = find_match(next, std::end(val_raw));
case '(': {
ss.write(pos, (dollar - pos));
auto var_begin = next + 1;
auto var_end = find_match(next, std::end(val_raw));
ss << env[std::string { var_begin, var_end }].to_string();
ss << env[std::string { var_begin, var_end }].to_string();
pos = var_end + 1;
next = var_end;
pos = var_end + 1;
next = var_end;
break;
}
case '$':
ss.write(pos, (next - pos));
pos = next + 1;
++next;
break;
break;
}
case '$':
ss.write(pos, (next - pos));
pos = next + 1;
++next;
break;
}
dollar = std::find(next, std::end(val_raw), '$');
@@ -228,7 +249,7 @@ std::string parse_env_val(bp::native_environment &env, const std::string_view &v
return ss.str();
}
std::optional<proc::proc_t> parse(const std::string& file_name) {
std::optional<proc::proc_t> parse(const std::string &file_name) {
pt::ptree tree;
try {
@@ -239,26 +260,45 @@ std::optional<proc::proc_t> parse(const std::string& file_name) {
auto this_env = boost::this_process::environment();
for(auto &[name, val] : env_vars) {
this_env[name] = parse_env_val(this_env, val.get_value<std::string>());
}
std::vector<proc::ctx_t> apps;
for(auto &[_,app_node] : apps_node) {
for(auto &[_, app_node] : apps_node) {
proc::ctx_t ctx;
auto &prep_nodes = app_node.get_child("prep-cmd"s);
auto output = app_node.get_optional<std::string>("output"s);
auto name = parse_env_val(this_env, app_node.get<std::string>("name"s));
auto cmd = app_node.get_optional<std::string>("cmd"s);
auto prep_nodes_opt = app_node.get_child_optional("prep-cmd"s);
auto detached_nodes_opt = app_node.get_child_optional("detached"s);
auto output = app_node.get_optional<std::string>("output"s);
auto name = parse_env_val(this_env, app_node.get<std::string>("name"s));
auto cmd = app_node.get_optional<std::string>("cmd"s);
std::vector<proc::cmd_t> prep_cmds;
prep_cmds.reserve(prep_nodes.size());
for(auto &[_, prep_node] : prep_nodes) {
auto do_cmd = parse_env_val(this_env, prep_node.get<std::string>("do"s));
auto undo_cmd = prep_node.get_optional<std::string>("undo"s);
if(prep_nodes_opt) {
auto &prep_nodes = *prep_nodes_opt;
if(undo_cmd) {
prep_cmds.emplace_back(std::move(do_cmd), parse_env_val(this_env, *undo_cmd));
prep_cmds.reserve(prep_nodes.size());
for(auto &[_, prep_node] : prep_nodes) {
auto do_cmd = parse_env_val(this_env, prep_node.get<std::string>("do"s));
auto undo_cmd = prep_node.get_optional<std::string>("undo"s);
if(undo_cmd) {
prep_cmds.emplace_back(std::move(do_cmd), parse_env_val(this_env, *undo_cmd));
}
else {
prep_cmds.emplace_back(std::move(do_cmd));
}
}
else {
prep_cmds.emplace_back(std::move(do_cmd));
}
std::vector<std::string> detached;
if(detached_nodes_opt) {
auto &detached_nodes = *detached_nodes_opt;
detached.reserve(detached_nodes.size());
for(auto &[_, detached_val] : detached_nodes) {
detached.emplace_back(parse_env_val(this_env, detached_val.get_value<std::string>()));
}
}
@@ -270,21 +310,18 @@ std::optional<proc::proc_t> parse(const std::string& file_name) {
ctx.cmd = parse_env_val(this_env, *cmd);
}
ctx.name = std::move(name);
ctx.name = std::move(name);
ctx.prep_cmds = std::move(prep_cmds);
ctx.detached = std::move(detached);
apps.emplace_back(std::move(ctx));
}
bp::environment env = boost::this_process::environment();
for(auto &[name,val] : env_vars) {
this_env[name] = parse_env_val(this_env, val.get_value<std::string>());
}
return proc::proc_t {
std::move(this_env), std::move(apps)
};
} catch (std::exception &e) {
}
catch(std::exception &e) {
BOOST_LOG(error) << e.what();
}
@@ -295,7 +332,12 @@ void refresh(const std::string &file_name) {
auto proc_opt = proc::parse(file_name);
if(proc_opt) {
{
proc::ctx_t ctx;
ctx.name = "Desktop"s;
proc_opt->get_apps().emplace(std::begin(proc_opt->get_apps()), std::move(ctx));
}
proc = std::move(*proc_opt);
}
}
}
} // namespace proc

View File

@@ -9,8 +9,8 @@
#define __kernel_entry
#endif
#include <unordered_map>
#include <optional>
#include <unordered_map>
#include <boost/process.hpp>
@@ -30,6 +30,7 @@ struct cmd_t {
};
/*
* pre_cmds -- guaranteed to be executed unless any of the commands fail.
* detached -- commands detached from Sunshine
* cmd -- Runs indefinitely until:
* No session is running and a different set of commands it to be executed
* Command exits
@@ -41,6 +42,14 @@ struct cmd_t {
struct ctx_t {
std::vector<cmd_t> prep_cmds;
/**
* Some applications, such as Steam,
* either exit quickly, or keep running indefinitely.
* Steam.exe is one such application.
* That is why some applications need be run and forgotten about
*/
std::vector<std::string> detached;
std::string name;
std::string cmd;
std::string output;
@@ -52,10 +61,9 @@ public:
proc_t(
boost::process::environment &&env,
std::vector<ctx_t> &&apps) :
_app_id(-1),
_env(std::move(env)),
_apps(std::move(apps)) {}
std::vector<ctx_t> &&apps) : _app_id(-1),
_env(std::move(env)),
_apps(std::move(apps)) {}
int execute(int app_id);
@@ -67,6 +75,8 @@ public:
~proc_t();
const std::vector<ctx_t> &get_apps() const;
std::vector<ctx_t> &get_apps();
void terminate();
private:
@@ -87,8 +97,8 @@ private:
};
void refresh(const std::string &file_name);
std::optional<proc::proc_t> parse(const std::string& file_name);
std::optional<proc::proc_t> parse(const std::string &file_name);
extern proc_t proc;
}
} // namespace proc
#endif //SUNSHINE_PROCESS_H

156
sunshine/round_robin.h Normal file
View File

@@ -0,0 +1,156 @@
#ifndef KITTY_UTIL_ITERATOR_H
#define KITTY_UTIL_ITERATOR_H
#include <iterator>
namespace util {
template<class V, class T>
class it_wrap_t : public std::iterator<std::random_access_iterator_tag, V> {
public:
typedef T iterator;
typedef typename std::iterator<std::random_access_iterator_tag, V>::value_type class_t;
typedef class_t &reference;
typedef class_t *pointer;
typedef std::ptrdiff_t diff_t;
iterator operator+=(diff_t step) {
while(step-- > 0) {
++_this();
}
return _this();
}
iterator operator-=(diff_t step) {
while(step-- > 0) {
--_this();
}
return _this();
}
iterator operator+(diff_t step) {
iterator new_ = _this();
return new_ += step;
}
iterator operator-(diff_t step) {
iterator new_ = _this();
return new_ -= step;
}
diff_t operator-(iterator first) {
diff_t step = 0;
while(first != _this()) {
++step;
++first;
}
return step;
}
iterator operator++() {
_this().inc();
return _this();
}
iterator operator--() {
_this().dec();
return _this();
}
iterator operator++(int) {
iterator new_ = _this();
++_this();
return new_;
}
iterator operator--(int) {
iterator new_ = _this();
--_this();
return new_;
}
reference operator*() { return *_this().get(); }
const reference operator*() const { return *_this().get(); }
pointer operator->() { return &*_this(); }
const pointer operator->() const { return &*_this(); }
bool operator!=(const iterator &other) const {
return !(_this() == other);
}
bool operator<(const iterator &other) const {
return !(_this() >= other);
}
bool operator>=(const iterator &other) const {
return _this() == other || _this() > other;
}
bool operator<=(const iterator &other) const {
return _this() == other || _this() < other;
}
bool operator==(const iterator &other) const { return _this().eq(other); };
bool operator>(const iterator &other) const { return _this().gt(other); }
private:
iterator &_this() { return *static_cast<iterator *>(this); }
const iterator &_this() const { return *static_cast<const iterator *>(this); }
};
template<class V, class It>
class round_robin_t : public it_wrap_t<V, round_robin_t<V, It>> {
public:
using iterator = It;
using pointer = V *;
round_robin_t(iterator begin, iterator end) : _begin(begin), _end(end), _pos(begin) {}
void inc() {
++_pos;
if(_pos == _end) {
_pos = _begin;
}
}
void dec() {
if(_pos == _begin) {
_pos = _end;
}
--_pos;
}
bool eq(const round_robin_t &other) const {
return *_pos == *other._pos;
}
pointer get() const {
return &*_pos;
}
private:
It _begin;
It _end;
It _pos;
};
template<class V, class It>
round_robin_t<V, It> make_round_robin(It begin, It end) {
return round_robin_t<V, It>(begin, end);
}
} // namespace util
#endif

571
sunshine/rtsp.cpp Normal file
View File

@@ -0,0 +1,571 @@
//
// Created by loki on 2/2/20.
//
extern "C" {
#include <moonlight-common-c/src/Rtsp.h>
}
#include "config.h"
#include "input.h"
#include "main.h"
#include "network.h"
#include "rtsp.h"
#include "stream.h"
#include "sync.h"
namespace asio = boost::asio;
using asio::ip::tcp;
using asio::ip::udp;
using namespace std::literals;
namespace stream {
//FIXME: Quick and dirty workaround for bug in MinGW 9.3 causing a linker error when using std::to_string
template<class T>
std::string to_string(T &&t) {
std::stringstream ss;
ss << std::forward<T>(t);
return ss.str();
}
void free_msg(PRTSP_MESSAGE msg) {
freeMessage(msg);
delete msg;
}
class rtsp_server_t;
using msg_t = util::safe_ptr<RTSP_MESSAGE, free_msg>;
using cmd_func_t = std::function<void(rtsp_server_t *, net::peer_t, msg_t &&)>;
void print_msg(PRTSP_MESSAGE msg);
void cmd_not_found(net::host_t::pointer host, net::peer_t peer, msg_t &&req);
class rtsp_server_t {
public:
~rtsp_server_t() {
if(_host) {
clear();
}
}
int bind(std::uint16_t port) {
{
auto lg = _session_slots.lock();
_session_slots->resize(config::stream.channels);
_slot_count = config::stream.channels;
}
_host = net::host_create(_addr, 1, port);
return !(bool)_host;
}
void session_raise(launch_session_t launch_session) {
//FIXME: If client abandons us at this stage, _slot_count won't be raised again.
--_slot_count;
launch_event.raise(launch_session);
}
int session_count() const {
return config::stream.channels - _slot_count;
}
template<class T, class X>
void iterate(std::chrono::duration<T, X> timeout) {
ENetEvent event;
auto res = enet_host_service(_host.get(), &event, std::chrono::floor<std::chrono::milliseconds>(timeout).count());
if(res > 0) {
switch(event.type) {
case ENET_EVENT_TYPE_RECEIVE: {
net::packet_t packet { event.packet };
net::peer_t peer { event.peer };
msg_t req { new msg_t::element_type };
//TODO: compare addresses of the peers
if(_queue_packet.second == nullptr) {
parseRtspMessage(req.get(), (char *)packet->data, packet->dataLength);
for(auto option = req->options; option != nullptr; option = option->next) {
if("Content-length"sv == option->option) {
_queue_packet = std::make_pair(peer, std::move(packet));
return;
}
}
}
else {
std::vector<char> full_payload;
auto old_msg = std::move(_queue_packet);
auto &old_packet = old_msg.second;
std::string_view new_payload { (char *)packet->data, packet->dataLength };
std::string_view old_payload { (char *)old_packet->data, old_packet->dataLength };
full_payload.resize(new_payload.size() + old_payload.size());
std::copy(std::begin(old_payload), std::end(old_payload), std::begin(full_payload));
std::copy(std::begin(new_payload), std::end(new_payload), std::begin(full_payload) + old_payload.size());
parseRtspMessage(req.get(), full_payload.data(), full_payload.size());
}
print_msg(req.get());
msg_t resp;
auto func = _map_cmd_cb.find(req->message.request.command);
if(func != std::end(_map_cmd_cb)) {
func->second(this, peer, std::move(req));
}
else {
cmd_not_found(host(), peer, std::move(req));
}
return;
}
case ENET_EVENT_TYPE_CONNECT:
BOOST_LOG(info) << "CLIENT CONNECTED TO RTSP"sv;
break;
case ENET_EVENT_TYPE_DISCONNECT:
BOOST_LOG(info) << "CLIENT DISCONNECTED FROM RTSP"sv;
break;
case ENET_EVENT_TYPE_NONE:
break;
}
}
}
void map(const std::string_view &type, cmd_func_t cb) {
_map_cmd_cb.emplace(type, std::move(cb));
}
void clear(bool all = true) {
auto lg = _session_slots.lock();
for(auto &slot : *_session_slots) {
if(slot && (all || session::state(*slot) == session::state_e::STOPPING)) {
session::stop(*slot);
session::join(*slot);
slot.reset();
++_slot_count;
}
}
if(all) {
std::for_each(_host->peers, _host->peers + _host->peerCount, [](auto &peer) {
enet_peer_disconnect_now(&peer, 0);
});
}
}
void clear(std::shared_ptr<session_t> *session_p) {
auto lg = _session_slots.lock();
session_p->reset();
++_slot_count;
}
std::shared_ptr<session_t> *accept(std::shared_ptr<session_t> &session) {
auto lg = _session_slots.lock();
for(auto &slot : *_session_slots) {
if(!slot) {
slot = session;
return &slot;
}
}
return nullptr;
}
net::host_t::pointer host() const {
return _host.get();
}
safe::event_t<launch_session_t> launch_event;
private:
// named _queue_packet because I want to make it an actual queue
// It's like this for convenience sake
std::pair<net::peer_t, net::packet_t> _queue_packet;
std::unordered_map<std::string_view, cmd_func_t> _map_cmd_cb;
util::sync_t<std::vector<std::shared_ptr<session_t>>> _session_slots;
int _slot_count;
ENetAddress _addr;
net::host_t _host;
};
rtsp_server_t server;
void launch_session_raise(launch_session_t launch_session) {
server.session_raise(launch_session);
}
int session_count() {
// Ensure session_count is up to date
server.clear(false);
return server.session_count();
}
void respond(net::host_t::pointer host, net::peer_t peer, msg_t &resp) {
auto payload = std::make_pair(resp->payload, resp->payloadLength);
auto lg = util::fail_guard([&]() {
resp->payload = payload.first;
resp->payloadLength = payload.second;
});
resp->payload = nullptr;
resp->payloadLength = 0;
int serialized_len;
util::c_ptr<char> raw_resp { serializeRtspMessage(resp.get(), &serialized_len) };
BOOST_LOG(debug)
<< "---Begin Response---"sv << std::endl
<< std::string_view { raw_resp.get(), (std::size_t)serialized_len } << std::endl
<< std::string_view { payload.first, (std::size_t)payload.second } << std::endl
<< "---End Response---"sv << std::endl;
std::string_view tmp_resp { raw_resp.get(), (size_t)serialized_len };
{
auto packet = enet_packet_create(tmp_resp.data(), tmp_resp.size(), ENET_PACKET_FLAG_RELIABLE);
if(enet_peer_send(peer, 0, packet)) {
enet_packet_destroy(packet);
return;
}
enet_host_flush(host);
}
if(payload.second > 0) {
auto packet = enet_packet_create(payload.first, payload.second, ENET_PACKET_FLAG_RELIABLE);
if(enet_peer_send(peer, 0, packet)) {
enet_packet_destroy(packet);
return;
}
enet_host_flush(host);
}
}
void respond(net::host_t::pointer host, net::peer_t peer, POPTION_ITEM options, int statuscode, const char *status_msg, int seqn, const std::string_view &payload) {
msg_t resp { new msg_t::element_type };
createRtspResponse(resp.get(), nullptr, 0, const_cast<char *>("RTSP/1.0"), statuscode, const_cast<char *>(status_msg), seqn, options, const_cast<char *>(payload.data()), (int)payload.size());
respond(host, peer, resp);
}
void cmd_not_found(net::host_t::pointer host, net::peer_t peer, msg_t &&req) {
respond(host, peer, nullptr, 404, "NOT FOUND", req->sequenceNumber, {});
}
void cmd_option(rtsp_server_t *server, net::peer_t peer, msg_t &&req) {
OPTION_ITEM option {};
// I know these string literals will not be modified
option.option = const_cast<char *>("CSeq");
auto seqn_str = to_string(req->sequenceNumber);
option.content = const_cast<char *>(seqn_str.c_str());
respond(server->host(), peer, &option, 200, "OK", req->sequenceNumber, {});
}
void cmd_describe(rtsp_server_t *server, net::peer_t peer, msg_t &&req) {
OPTION_ITEM option {};
// I know these string literals will not be modified
option.option = const_cast<char *>("CSeq");
auto seqn_str = to_string(req->sequenceNumber);
option.content = const_cast<char *>(seqn_str.c_str());
std::stringstream ss;
if(config::video.hevc_mode != 1) {
ss << "sprop-parameter-sets=AAAAAU"sv << std::endl;
}
for(int x = 0; x < audio::MAX_STREAM_CONFIG; ++x) {
auto &stream_config = audio::stream_configs[x];
std::uint8_t mapping[platf::speaker::MAX_SPEAKERS];
auto mapping_p = stream_config.mapping;
/**
* GFE advertises incorrect mapping for normal quality configurations,
* as a result, Moonlight rotates all channels from index '3' to the right
* To work around this, rotate channels to the left from index '3'
*/
if(x == audio::SURROUND51 || x == audio::SURROUND71) {
std::copy_n(mapping_p, stream_config.channelCount, mapping);
std::rotate(mapping + 3, mapping + 4, mapping + audio::MAX_STREAM_CONFIG);
mapping_p = mapping;
}
ss << "a=fmtp:97 surround-params="sv << stream_config.channelCount << stream_config.streams << stream_config.coupledStreams;
std::for_each_n(mapping_p, stream_config.channelCount, [&ss](std::uint8_t digit) {
ss << (char)(digit + '0');
});
ss << std::endl;
}
respond(server->host(), peer, &option, 200, "OK", req->sequenceNumber, ss.str());
}
void cmd_setup(rtsp_server_t *server, net::peer_t peer, msg_t &&req) {
OPTION_ITEM options[2] {};
auto &seqn = options[0];
auto &session_option = options[1];
seqn.option = const_cast<char *>("CSeq");
auto seqn_str = to_string(req->sequenceNumber);
seqn.content = const_cast<char *>(seqn_str.c_str());
std::string_view target { req->message.request.target };
auto begin = std::find(std::begin(target), std::end(target), '=') + 1;
auto end = std::find(begin, std::end(target), '/');
std::string_view type { begin, (size_t)std::distance(begin, end) };
if(type == "audio"sv) {
seqn.next = &session_option;
session_option.option = const_cast<char *>("Session");
session_option.content = const_cast<char *>("DEADBEEFCAFE;timeout = 90");
}
else if(type != "video"sv && type != "control"sv) {
cmd_not_found(server->host(), peer, std::move(req));
return;
}
respond(server->host(), peer, &seqn, 200, "OK", req->sequenceNumber, {});
}
void cmd_announce(rtsp_server_t *server, net::peer_t peer, msg_t &&req) {
OPTION_ITEM option {};
// I know these string literals will not be modified
option.option = const_cast<char *>("CSeq");
auto seqn_str = to_string(req->sequenceNumber);
option.content = const_cast<char *>(seqn_str.c_str());
if(!server->launch_event.peek()) {
// /launch has not been used
respond(server->host(), peer, &option, 503, "Service Unavailable", req->sequenceNumber, {});
return;
}
auto launch_session { server->launch_event.pop() };
std::string_view payload { req->payload, (size_t)req->payloadLength };
std::vector<std::string_view> lines;
auto whitespace = [](char ch) {
return ch == '\n' || ch == '\r';
};
{
auto pos = std::begin(payload);
auto begin = pos;
while(pos != std::end(payload)) {
if(whitespace(*pos++)) {
lines.emplace_back(begin, pos - begin - 1);
while(pos != std::end(payload) && whitespace(*pos)) { ++pos; }
begin = pos;
}
}
}
std::string_view client;
std::unordered_map<std::string_view, std::string_view> args;
for(auto line : lines) {
auto type = line.substr(0, 2);
if(type == "s="sv) {
client = line.substr(2);
}
else if(type == "a=") {
auto pos = line.find(':');
auto name = line.substr(2, pos - 2);
auto val = line.substr(pos + 1);
if(val[val.size() - 1] == ' ') {
val = val.substr(0, val.size() - 1);
}
args.emplace(name, val);
}
}
// Initialize any omitted parameters to defaults
args.try_emplace("x-nv-video[0].encoderCscMode"sv, "0"sv);
args.try_emplace("x-nv-vqos[0].bitStreamFormat"sv, "0"sv);
args.try_emplace("x-nv-video[0].dynamicRangeMode"sv, "0"sv);
args.try_emplace("x-nv-aqos.packetDuration"sv, "5"sv);
config_t config;
config.audio.flags[audio::config_t::HOST_AUDIO] = launch_session->host_audio;
try {
config.audio.channels = util::from_view(args.at("x-nv-audio.surround.numChannels"sv));
config.audio.mask = util::from_view(args.at("x-nv-audio.surround.channelMask"sv));
config.audio.packetDuration = util::from_view(args.at("x-nv-aqos.packetDuration"sv));
config.audio.flags[audio::config_t::HIGH_QUALITY] =
util::from_view(args.at("x-nv-audio.surround.AudioQuality"sv));
config.packetsize = util::from_view(args.at("x-nv-video[0].packetSize"sv));
config.monitor.height = util::from_view(args.at("x-nv-video[0].clientViewportHt"sv));
config.monitor.width = util::from_view(args.at("x-nv-video[0].clientViewportWd"sv));
config.monitor.framerate = util::from_view(args.at("x-nv-video[0].maxFPS"sv));
config.monitor.bitrate = util::from_view(args.at("x-nv-vqos[0].bw.maximumBitrateKbps"sv));
config.monitor.slicesPerFrame = util::from_view(args.at("x-nv-video[0].videoEncoderSlicesPerFrame"sv));
config.monitor.numRefFrames = util::from_view(args.at("x-nv-video[0].maxNumReferenceFrames"sv));
config.monitor.encoderCscMode = util::from_view(args.at("x-nv-video[0].encoderCscMode"sv));
config.monitor.videoFormat = util::from_view(args.at("x-nv-vqos[0].bitStreamFormat"sv));
config.monitor.dynamicRange = util::from_view(args.at("x-nv-video[0].dynamicRangeMode"sv));
}
catch(std::out_of_range &) {
respond(server->host(), peer, &option, 400, "BAD REQUEST", req->sequenceNumber, {});
return;
}
if(config.monitor.videoFormat != 0 && config::video.hevc_mode == 1) {
BOOST_LOG(warning) << "HEVC is disabled, yet the client requested HEVC"sv;
respond(server->host(), peer, &option, 400, "BAD REQUEST", req->sequenceNumber, {});
return;
}
auto session = session::alloc(config, launch_session->gcm_key, launch_session->iv);
auto slot = server->accept(session);
if(!slot) {
BOOST_LOG(info) << "Ran out of slots for client from ["sv << ']';
respond(server->host(), peer, &option, 503, "Service Unavailable", req->sequenceNumber, {});
return;
}
if(session::start(*session, platf::from_sockaddr((sockaddr *)&peer->address.address))) {
BOOST_LOG(error) << "Failed to start a streaming session"sv;
server->clear(slot);
respond(server->host(), peer, &option, 500, "Internal Server Error", req->sequenceNumber, {});
return;
}
respond(server->host(), peer, &option, 200, "OK", req->sequenceNumber, {});
}
void cmd_play(rtsp_server_t *server, net::peer_t peer, msg_t &&req) {
OPTION_ITEM option {};
// I know these string literals will not be modified
option.option = const_cast<char *>("CSeq");
auto seqn_str = to_string(req->sequenceNumber);
option.content = const_cast<char *>(seqn_str.c_str());
respond(server->host(), peer, &option, 200, "OK", req->sequenceNumber, {});
}
void rtpThread() {
auto shutdown_event = mail::man->event<bool>(mail::shutdown);
auto broadcast_shutdown_event = mail::man->event<bool>(mail::broadcast_shutdown);
server.map("OPTIONS"sv, &cmd_option);
server.map("DESCRIBE"sv, &cmd_describe);
server.map("SETUP"sv, &cmd_setup);
server.map("ANNOUNCE"sv, &cmd_announce);
server.map("PLAY"sv, &cmd_play);
if(server.bind(map_port(RTSP_SETUP_PORT))) {
BOOST_LOG(fatal) << "Couldn't bind RTSP server to port ["sv << map_port(RTSP_SETUP_PORT) << "], likely another process already bound to the port"sv;
shutdown_event->raise(true);
return;
}
while(!shutdown_event->peek()) {
server.iterate(std::min(500ms, config::stream.ping_timeout));
if(broadcast_shutdown_event->peek()) {
server.clear();
}
else {
// cleanup all stopped sessions
server.clear(false);
}
}
server.clear();
}
void print_msg(PRTSP_MESSAGE msg) {
std::string_view type = msg->type == TYPE_RESPONSE ? "RESPONSE"sv : "REQUEST"sv;
std::string_view payload { msg->payload, (size_t)msg->payloadLength };
std::string_view protocol { msg->protocol };
auto seqnm = msg->sequenceNumber;
std::string_view messageBuffer { msg->messageBuffer };
BOOST_LOG(debug) << "type ["sv << type << ']';
BOOST_LOG(debug) << "sequence number ["sv << seqnm << ']';
BOOST_LOG(debug) << "protocol :: "sv << protocol;
BOOST_LOG(debug) << "payload :: "sv << payload;
if(msg->type == TYPE_RESPONSE) {
auto &resp = msg->message.response;
auto statuscode = resp.statusCode;
std::string_view status { resp.statusString };
BOOST_LOG(debug) << "statuscode :: "sv << statuscode;
BOOST_LOG(debug) << "status :: "sv << status;
}
else {
auto &req = msg->message.request;
std::string_view command { req.command };
std::string_view target { req.target };
BOOST_LOG(debug) << "command :: "sv << command;
BOOST_LOG(debug) << "target :: "sv << target;
}
for(auto option = msg->options; option != nullptr; option = option->next) {
std::string_view content { option->content };
std::string_view name { option->option };
BOOST_LOG(debug) << name << " :: "sv << content;
}
BOOST_LOG(debug) << "---Begin MessageBuffer---"sv << std::endl
<< messageBuffer << std::endl
<< "---End MessageBuffer---"sv << std::endl;
}
} // namespace stream

30
sunshine/rtsp.h Normal file
View File

@@ -0,0 +1,30 @@
//
// Created by loki on 2/2/20.
//
#ifndef SUNSHINE_RTSP_H
#define SUNSHINE_RTSP_H
#include <atomic>
#include "crypto.h"
#include "thread_safe.h"
namespace stream {
constexpr auto RTSP_SETUP_PORT = 21;
struct launch_session_t {
crypto::aes_t gcm_key;
crypto::aes_t iv;
bool host_audio;
};
void launch_session_raise(launch_session_t launch_session);
int session_count();
void rtpThread();
} // namespace stream
#endif //SUNSHINE_RTSP_H

File diff suppressed because it is too large Load Diff

View File

@@ -5,13 +5,27 @@
#ifndef SUNSHINE_STREAM_H
#define SUNSHINE_STREAM_H
#include <atomic>
#include <boost/asio.hpp>
#include "audio.h"
#include "crypto.h"
#include "thread_safe.h"
#include "video.h"
namespace stream {
constexpr auto VIDEO_STREAM_PORT = 9;
constexpr auto CONTROL_PORT = 10;
constexpr auto AUDIO_STREAM_PORT = 11;
struct session_t;
struct config_t {
audio::config_t audio;
video::config_t monitor;
int packetsize;
std::optional<int> gcmap;
};
namespace session {
enum class state_e : int {
STOPPED,
STOPPING,
@@ -19,18 +33,12 @@ enum class state_e : int {
RUNNING,
};
struct launch_session_t {
crypto::aes_t gcm_key;
crypto::aes_t iv;
bool has_process;
};
extern safe::event_t<launch_session_t> launch_event;
extern std::atomic<state_e> session_state;
void rtpThread(std::shared_ptr<safe::event_t<bool>> shutdown_event);
}
std::shared_ptr<session_t> alloc(config_t &config, crypto::aes_t &gcm_key, crypto::aes_t &iv);
int start(session_t &session, const std::string &addr_string);
void stop(session_t &session);
void join(session_t &session);
state_e state(session_t &session);
} // namespace session
} // namespace stream
#endif //SUNSHINE_STREAM_H

95
sunshine/sync.h Normal file
View File

@@ -0,0 +1,95 @@
//
// Created by loki on 16-4-19.
//
#ifndef SUNSHINE_SYNC_H
#define SUNSHINE_SYNC_H
#include <array>
#include <mutex>
#include <utility>
namespace util {
template<class T, class M = std::mutex>
class sync_t {
public:
using value_t = T;
using mutex_t = M;
std::lock_guard<mutex_t> lock() {
return std::lock_guard { _lock };
}
template<class... Args>
sync_t(Args &&...args) : raw { std::forward<Args>(args)... } {}
sync_t &operator=(sync_t &&other) noexcept {
std::lock(_lock, other._lock);
raw = std::move(other.raw);
_lock.unlock();
other._lock.unlock();
return *this;
}
sync_t &operator=(sync_t &other) noexcept {
std::lock(_lock, other._lock);
raw = other.raw;
_lock.unlock();
other._lock.unlock();
return *this;
}
template<class V>
sync_t &operator=(V &&val) {
auto lg = lock();
raw = val;
return *this;
}
sync_t &operator=(const value_t &val) noexcept {
auto lg = lock();
raw = val;
return *this;
}
sync_t &operator=(value_t &&val) noexcept {
auto lg = lock();
raw = std::move(val);
return *this;
}
value_t *operator->() {
return &raw;
}
value_t &operator*() {
return raw;
}
const value_t &operator*() const {
return raw;
}
value_t raw;
private:
mutex_t _lock;
};
} // namespace util
#endif //T_MAN_SYNC_H

View File

@@ -1,18 +1,18 @@
#ifndef KITTY_TASK_POOL_H
#define KITTY_TASK_POOL_H
#include <deque>
#include <vector>
#include <future>
#include <chrono>
#include <utility>
#include <deque>
#include <functional>
#include <future>
#include <mutex>
#include <type_traits>
#include <optional>
#include <type_traits>
#include <utility>
#include <vector>
#include "utility.h"
#include "move_by_copy.h"
#include "utility.h"
namespace util {
class _ImplBase {
@@ -29,8 +29,7 @@ class _Impl : public _ImplBase {
Function _func;
public:
_Impl(Function&& f) : _func(std::forward<Function>(f)) { }
_Impl(Function &&f) : _func(std::forward<Function>(f)) {}
void run() override {
_func();
@@ -40,7 +39,7 @@ public:
class TaskPool {
public:
typedef std::unique_ptr<_ImplBase> __task;
typedef _ImplBase* task_id_t;
typedef _ImplBase *task_id_t;
typedef std::chrono::steady_clock::time_point __time_point;
@@ -53,9 +52,10 @@ public:
timer_task_t(task_id_t task_id, std::future<R> &future) : task_id { task_id }, future { std::move(future) } {}
};
protected:
std::deque<__task> _tasks;
std::vector<std::pair<__time_point, __task>> _timer_tasks;
std::vector<std::pair<__time_point, __task>> _timer_tasks;
std::mutex _task_mutex;
public:
@@ -70,8 +70,8 @@ public:
}
template<class Function, class... Args>
auto push(Function && newTask, Args &&... args) {
static_assert(std::is_invocable_v<Function, Args&&...>, "arguments don't match the function");
auto push(Function &&newTask, Args &&...args) {
static_assert(std::is_invocable_v<Function, Args &&...>, "arguments don't match the function");
using __return = std::invoke_result_t<Function, Args &&...>;
using task_t = std::packaged_task<__return()>;
@@ -81,12 +81,12 @@ public:
};
task_t task(std::move(bind));
auto future = task.get_future();
std::lock_guard<std::mutex> lg(_task_mutex);
_tasks.emplace_back(toRunnable(std::move(task)));
return future;
}
@@ -107,13 +107,19 @@ public:
* @return an id to potentially delay the task
*/
template<class Function, class X, class Y, class... Args>
auto pushDelayed(Function &&newTask, std::chrono::duration<X, Y> duration, Args &&... args) {
static_assert(std::is_invocable_v<Function, Args&&...>, "arguments don't match the function");
auto pushDelayed(Function &&newTask, std::chrono::duration<X, Y> duration, Args &&...args) {
static_assert(std::is_invocable_v<Function, Args &&...>, "arguments don't match the function");
using __return = std::invoke_result_t<Function, Args &&...>;
using task_t = std::packaged_task<__return()>;
__time_point time_point = std::chrono::steady_clock::now() + duration;
__time_point time_point;
if constexpr(std::is_floating_point_v<X>) {
time_point = std::chrono::steady_clock::now() + std::chrono::duration_cast<std::chrono::nanoseconds>(duration);
}
else {
time_point = std::chrono::steady_clock::now() + duration;
}
auto bind = [task = std::forward<Function>(newTask), tuple_args = std::make_tuple(std::forward<Args>(args)...)]() mutable {
return std::apply(task, std::move(tuple_args));
@@ -121,7 +127,7 @@ public:
task_t task(std::move(bind));
auto future = task.get_future();
auto future = task.get_future();
auto runnable = toRunnable(std::move(task));
task_id_t task_id = &*runnable;
@@ -154,13 +160,14 @@ public:
}
// smaller time goes to the back
auto prev = it -1;
auto prev = it - 1;
while(it > _timer_tasks.cbegin()) {
if(std::get<0>(*it) > std::get<0>(*prev)) {
std::swap(*it, *prev);
}
--prev; --it;
--prev;
--it;
}
}
@@ -195,20 +202,20 @@ public:
std::optional<__task> pop() {
std::lock_guard lg(_task_mutex);
if(!_tasks.empty()) {
__task task = std::move(_tasks.front());
_tasks.pop_front();
return std::move(task);
}
if(!_timer_tasks.empty() && std::get<0>(_timer_tasks.back()) <= std::chrono::steady_clock::now()) {
__task task = std::move(std::get<1>(_timer_tasks.back()));
_timer_tasks.pop_back();
return std::move(task);
}
return std::nullopt;
}
@@ -227,12 +234,12 @@ public:
return std::get<0>(_timer_tasks.back());
}
private:
private:
template<class Function>
std::unique_ptr<_ImplBase> toRunnable(Function &&f) {
return std::make_unique<_Impl<Function>>(std::forward<Function&&>(f));
return std::make_unique<_Impl<Function>>(std::forward<Function &&>(f));
}
};
}
} // namespace util
#endif

View File

@@ -1,8 +1,8 @@
#ifndef KITTY_THREAD_POOL_H
#define KITTY_THREAD_POOL_H
#include <thread>
#include "task_pool.h"
#include <thread>
namespace util {
/*
@@ -12,32 +12,33 @@ namespace util {
class ThreadPool : public TaskPool {
public:
typedef TaskPool::__task __task;
private:
std::vector<std::thread> _thread;
std::condition_variable _cv;
std::mutex _lock;
bool _continue;
public:
ThreadPool() : _continue { false } {}
explicit ThreadPool(int threads) : _thread(threads), _continue { true } {
for (auto &t : _thread) {
for(auto &t : _thread) {
t = std::thread(&ThreadPool::_main, this);
}
}
~ThreadPool() noexcept {
if (!_continue) return;
if(!_continue) return;
stop();
join();
}
template<class Function, class... Args>
auto push(Function && newTask, Args &&... args) {
auto push(Function &&newTask, Args &&...args) {
std::lock_guard lg(_lock);
auto future = TaskPool::push(std::forward<Function>(newTask), std::forward<Args>(args)...);
@@ -52,7 +53,7 @@ public:
}
template<class Function, class X, class Y, class... Args>
auto pushDelayed(Function &&newTask, std::chrono::duration<X, Y> duration, Args &&... args) {
auto pushDelayed(Function &&newTask, std::chrono::duration<X, Y> duration, Args &&...args) {
std::lock_guard lg(_lock);
auto future = TaskPool::pushDelayed(std::forward<Function>(newTask), duration, std::forward<Args>(args)...);
@@ -79,15 +80,14 @@ public:
}
void join() {
for (auto & t : _thread) {
for(auto &t : _thread) {
t.join();
}
}
public:
void _main() {
while (_continue) {
while(_continue) {
if(auto task = this->pop()) {
(*task)->run();
}
@@ -117,5 +117,5 @@ public:
}
}
};
}
} // namespace util
#endif

View File

@@ -5,63 +5,91 @@
#ifndef SUNSHINE_THREAD_SAFE_H
#define SUNSHINE_THREAD_SAFE_H
#include <vector>
#include <mutex>
#include <atomic>
#include <condition_variable>
#include <functional>
#include <map>
#include <mutex>
#include <vector>
#include "utility.h"
namespace safe {
template<class T>
class event_t {
public:
using status_t = util::optional_t<T>;
public:
template<class...Args>
template<class... Args>
void raise(Args &&...args) {
std::lock_guard lg { _lock };
if(!_continue) {
return;
}
_status = status_t { std::forward<Args>(args)... };
if constexpr(std::is_same_v<std::optional<T>, status_t>) {
_status = std::make_optional<T>(std::forward<Args>(args)...);
}
else {
_status = status_t { std::forward<Args>(args)... };
}
_cv.notify_all();
}
// pop and view shoud not be used interchangebly
status_t pop() {
std::unique_lock ul{_lock};
std::unique_lock ul { _lock };
if (!_continue) {
if(!_continue) {
return util::false_v<status_t>;
}
while (!_status) {
while(!_status) {
_cv.wait(ul);
if (!_continue) {
if(!_continue) {
return util::false_v<status_t>;
}
}
auto val = std::move(_status);
_status = util::false_v<status_t>;
_status = util::false_v<status_t>;
return val;
}
// pop and view shoud not be used interchangebly
template<class Rep, class Period>
status_t pop(std::chrono::duration<Rep, Period> delay) {
std::unique_lock ul { _lock };
if(!_continue) {
return util::false_v<status_t>;
}
while(!_status) {
if(!_continue || _cv.wait_for(ul, delay) == std::cv_status::timeout) {
return util::false_v<status_t>;
}
}
auto val = std::move(_status);
_status = util::false_v<status_t>;
return val;
}
// pop and view shoud not be used interchangebly
const status_t &view() {
std::unique_lock ul{_lock};
std::unique_lock ul { _lock };
if (!_continue) {
if(!_continue) {
return util::false_v<status_t>;
}
while (!_status) {
while(!_status) {
_cv.wait(ul);
if (!_continue) {
if(!_continue) {
return util::false_v<status_t>;
}
}
@@ -70,66 +98,183 @@ public:
}
bool peek() {
std::lock_guard lg { _lock };
return (bool)_status;
return _continue && (bool)_status;
}
void stop() {
std::lock_guard lg{_lock};
std::lock_guard lg { _lock };
_continue = false;
_cv.notify_all();
}
bool running() const {
void reset() {
std::lock_guard lg { _lock };
_continue = true;
_status = util::false_v<status_t>;
}
[[nodiscard]] bool running() const {
return _continue;
}
private:
bool _continue{true};
status_t _status;
private:
bool _continue { true };
status_t _status { util::false_v<status_t> };
std::condition_variable _cv;
std::mutex _lock;
};
template<class T>
class queue_t {
class alarm_raw_t {
public:
using status_t = util::optional_t<T>;
alarm_raw_t() : _status { util::false_v<status_t> } {}
void ring(const status_t &status) {
std::lock_guard lg(_lock);
_status = status;
_cv.notify_one();
}
void ring(status_t &&status) {
std::lock_guard lg(_lock);
_status = std::move(status);
_cv.notify_one();
}
template<class Rep, class Period>
auto wait_for(const std::chrono::duration<Rep, Period> &rel_time) {
std::unique_lock ul(_lock);
return _cv.wait_for(ul, rel_time, [this]() { return (bool)status(); });
}
template<class Rep, class Period, class Pred>
auto wait_for(const std::chrono::duration<Rep, Period> &rel_time, Pred &&pred) {
std::unique_lock ul(_lock);
return _cv.wait_for(ul, rel_time, [this, &pred]() { return (bool)status() || pred(); });
}
template<class Rep, class Period>
auto wait_until(const std::chrono::duration<Rep, Period> &rel_time) {
std::unique_lock ul(_lock);
return _cv.wait_until(ul, rel_time, [this]() { return (bool)status(); });
}
template<class Rep, class Period, class Pred>
auto wait_until(const std::chrono::duration<Rep, Period> &rel_time, Pred &&pred) {
std::unique_lock ul(_lock);
return _cv.wait_until(ul, rel_time, [this, &pred]() { return (bool)status() || pred(); });
}
auto wait() {
std::unique_lock ul(_lock);
_cv.wait(ul, [this]() { return (bool)status(); });
}
template<class Pred>
auto wait(Pred &&pred) {
std::unique_lock ul(_lock);
_cv.wait(ul, [this, &pred]() { return (bool)status() || pred(); });
}
const status_t &status() const {
return _status;
}
status_t &status() {
return _status;
}
void reset() {
_status = status_t {};
}
private:
std::mutex _lock;
std::condition_variable _cv;
status_t _status;
};
template<class T>
using alarm_t = std::shared_ptr<alarm_raw_t<T>>;
template<class T>
alarm_t<T> make_alarm() {
return std::make_shared<alarm_raw_t<T>>();
}
template<class T>
class queue_t {
public:
template<class ...Args>
void raise(Args &&... args) {
std::lock_guard lg{_lock};
using status_t = util::optional_t<T>;
queue_t(std::uint32_t max_elements) : _max_elements { max_elements } {}
template<class... Args>
void raise(Args &&...args) {
std::lock_guard ul { _lock };
if(!_continue) {
return;
}
if(_queue.size() == _max_elements) {
_queue.clear();
}
_queue.emplace_back(std::forward<Args>(args)...);
_cv.notify_all();
}
bool peek() {
std::lock_guard lg { _lock };
return !_queue.empty();
return _continue && !_queue.empty();
}
status_t pop() {
std::unique_lock ul{_lock};
template<class Rep, class Period>
status_t pop(std::chrono::duration<Rep, Period> delay) {
std::unique_lock ul { _lock };
if (!_continue) {
if(!_continue) {
return util::false_v<status_t>;
}
while (_queue.empty()) {
while(_queue.empty()) {
if(!_continue || _cv.wait_for(ul, delay) == std::cv_status::timeout) {
return util::false_v<status_t>;
}
}
auto val = std::move(_queue.front());
_queue.erase(std::begin(_queue));
return val;
}
status_t pop() {
std::unique_lock ul { _lock };
if(!_continue) {
return util::false_v<status_t>;
}
while(_queue.empty()) {
_cv.wait(ul);
if (!_continue) {
if(!_continue) {
return util::false_v<status_t>;
}
}
@@ -145,7 +290,7 @@ public:
}
void stop() {
std::lock_guard lg{_lock};
std::lock_guard lg { _lock };
_continue = false;
@@ -157,14 +302,210 @@ public:
}
private:
bool _continue{true};
bool _continue { true };
std::uint32_t _max_elements;
std::mutex _lock;
std::condition_variable _cv;
std::vector<T> _queue;
};
template<class T>
class shared_t {
public:
using element_type = T;
using construct_f = std::function<int(element_type &)>;
using destruct_f = std::function<void(element_type &)>;
struct ptr_t {
shared_t *owner;
ptr_t() : owner { nullptr } {}
explicit ptr_t(shared_t *owner) : owner { owner } {}
ptr_t(ptr_t &&ptr) noexcept : owner { ptr.owner } {
ptr.owner = nullptr;
}
ptr_t(const ptr_t &ptr) noexcept : owner { ptr.owner } {
if(!owner) {
return;
}
auto tmp = ptr.owner->ref();
tmp.owner = nullptr;
}
ptr_t &operator=(const ptr_t &ptr) noexcept {
if(!ptr.owner) {
release();
return *this;
}
return *this = std::move(*ptr.owner->ref());
}
ptr_t &operator=(ptr_t &&ptr) noexcept {
if(owner) {
release();
}
std::swap(owner, ptr.owner);
return *this;
}
~ptr_t() {
if(owner) {
release();
}
}
operator bool() const {
return owner != nullptr;
}
void release() {
std::lock_guard lg { owner->_lock };
if(!--owner->_count) {
owner->_destruct(*get());
(*this)->~element_type();
}
owner = nullptr;
}
element_type *get() const {
return reinterpret_cast<element_type *>(owner->_object_buf.data());
}
element_type *operator->() {
return reinterpret_cast<element_type *>(owner->_object_buf.data());
}
};
template<class FC, class FD>
shared_t(FC &&fc, FD &&fd) : _construct { std::forward<FC>(fc) }, _destruct { std::forward<FD>(fd) } {}
[[nodiscard]] ptr_t ref() {
std::lock_guard lg { _lock };
if(!_count) {
new(_object_buf.data()) element_type;
if(_construct(*reinterpret_cast<element_type *>(_object_buf.data()))) {
return ptr_t { nullptr };
}
}
++_count;
return ptr_t { this };
}
private:
construct_f _construct;
destruct_f _destruct;
std::array<std::uint8_t, sizeof(element_type)> _object_buf;
std::uint32_t _count;
std::mutex _lock;
};
template<class T, class F_Construct, class F_Destruct>
auto make_shared(F_Construct &&fc, F_Destruct &&fd) {
return shared_t<T> {
std::forward<F_Construct>(fc), std::forward<F_Destruct>(fd)
};
}
using signal_t = event_t<bool>;
class mail_raw_t;
using mail_t = std::shared_ptr<mail_raw_t>;
void cleanup(mail_raw_t *);
template<class T>
class post_t : public T {
public:
template<class... Args>
post_t(mail_t mail, Args &&...args) : T(std::forward<Args>(args)...), mail { std::move(mail) } {}
mail_t mail;
~post_t() {
cleanup(mail.get());
}
};
template<class T>
inline auto lock(const std::weak_ptr<void> &wp) {
return std::reinterpret_pointer_cast<typename T::element_type>(wp.lock());
}
class mail_raw_t : public std::enable_shared_from_this<mail_raw_t> {
public:
template<class T>
using event_t = std::shared_ptr<post_t<event_t<T>>>;
template<class T>
using queue_t = std::shared_ptr<post_t<queue_t<T>>>;
template<class T>
event_t<T> event(const std::string_view &id) {
std::lock_guard lg { mutex };
auto it = id_to_post.find(id);
if(it != std::end(id_to_post)) {
return lock<event_t<T>>(it->second);
}
auto post = std::make_shared<typename event_t<T>::element_type>(shared_from_this());
id_to_post.emplace(std::pair<std::string, std::weak_ptr<void>> { std::string { id }, post });
return post;
}
template<class T>
queue_t<T> queue(const std::string_view &id) {
std::lock_guard lg { mutex };
auto it = id_to_post.find(id);
if(it != std::end(id_to_post)) {
return lock<queue_t<T>>(it->second);
}
auto post = std::make_shared<typename queue_t<T>::element_type>(shared_from_this(), 32);
id_to_post.emplace(std::pair<std::string, std::weak_ptr<void>> { std::string { id }, post });
return post;
}
void cleanup() {
std::lock_guard lg { mutex };
for(auto it = std::begin(id_to_post); it != std::end(id_to_post); ++it) {
auto &weak = it->second;
if(weak.expired()) {
id_to_post.erase(it);
return;
}
}
}
std::mutex mutex;
std::map<std::string, std::weak_ptr<void>, std::less<>> id_to_post;
};
inline void cleanup(mail_raw_t *mail) {
mail->cleanup();
}
} // namespace safe
#endif //SUNSHINE_THREAD_SAFE_H

184
sunshine/upnp.cpp Normal file
View File

@@ -0,0 +1,184 @@
#include <miniupnpc/miniupnpc.h>
#include <miniupnpc/upnpcommands.h>
#include "config.h"
#include "confighttp.h"
#include "main.h"
#include "network.h"
#include "nvhttp.h"
#include "rtsp.h"
#include "stream.h"
#include "upnp.h"
#include "utility.h"
using namespace std::literals;
namespace upnp {
constexpr auto INET6_ADDRESS_STRLEN = 46;
constexpr auto IPv4 = 0;
constexpr auto IPv6 = 1;
using device_t = util::safe_ptr<UPNPDev, freeUPNPDevlist>;
KITTY_USING_MOVE_T(urls_t, UPNPUrls, , {
FreeUPNPUrls(&el);
});
struct mapping_t {
struct {
std::string wan;
std::string lan;
} port;
std::string description;
bool tcp;
};
void unmap(
const urls_t &urls,
const IGDdatas &data,
std::vector<mapping_t>::const_reverse_iterator begin,
std::vector<mapping_t>::const_reverse_iterator end) {
BOOST_LOG(debug) << "Unmapping UPNP ports"sv;
for(auto it = begin; it != end; ++it) {
auto status = UPNP_DeletePortMapping(
urls->controlURL,
data.first.servicetype,
it->port.wan.c_str(),
it->tcp ? "TCP" : "UDP",
nullptr);
if(status) {
BOOST_LOG(warning) << "Failed to unmap port ["sv << it->port.wan << "] to port ["sv << it->port.lan << "]: error code ["sv << status << ']';
break;
}
}
}
class deinit_t : public platf::deinit_t {
public:
using iter_t = std::vector<mapping_t>::const_reverse_iterator;
deinit_t(urls_t &&urls, IGDdatas data, std::vector<mapping_t> &&mapping)
: urls { std::move(urls) }, data { data }, mapping { std::move(mapping) } {}
~deinit_t() {
BOOST_LOG(info) << "Unmapping UPNP ports..."sv;
unmap(urls, data, std::rbegin(mapping), std::rend(mapping));
}
urls_t urls;
IGDdatas data;
std::vector<mapping_t> mapping;
};
static std::string_view status_string(int status) {
switch(status) {
case 0:
return "No IGD device found"sv;
case 1:
return "Valid IGD device found"sv;
case 2:
return "A UPnP device has been found, but it wasn't recognized as an IGD"sv;
}
return "Unknown status"sv;
}
std::unique_ptr<platf::deinit_t> start() {
int err {};
device_t device { upnpDiscover(2000, nullptr, nullptr, 0, IPv4, 2, &err) };
if(!device || err) {
BOOST_LOG(error) << "Couldn't discover any UPNP devices"sv;
return nullptr;
}
for(auto dev = device.get(); dev != nullptr; dev = dev->pNext) {
BOOST_LOG(debug) << "Found device: "sv << dev->descURL;
}
std::array<char, INET6_ADDRESS_STRLEN> lan_addr;
std::array<char, INET6_ADDRESS_STRLEN> wan_addr;
urls_t urls;
IGDdatas data;
auto status = UPNP_GetValidIGD(device.get(), &urls.el, &data, lan_addr.data(), lan_addr.size());
if(status != 1) {
BOOST_LOG(error) << status_string(status);
return nullptr;
}
BOOST_LOG(debug) << "Found valid IGD device: "sv << urls->rootdescURL;
if(UPNP_GetExternalIPAddress(urls->controlURL, data.first.servicetype, wan_addr.data())) {
BOOST_LOG(warning) << "Could not get external ip"sv;
}
else {
BOOST_LOG(debug) << "Found external ip: "sv << wan_addr.data();
if(config::nvhttp.external_ip.empty()) {
config::nvhttp.external_ip = wan_addr.data();
}
}
if(!config::sunshine.flags[config::flag::UPNP]) {
return nullptr;
}
auto rtsp = std::to_string(map_port(stream::RTSP_SETUP_PORT));
auto video = std::to_string(map_port(stream::VIDEO_STREAM_PORT));
auto audio = std::to_string(map_port(stream::AUDIO_STREAM_PORT));
auto control = std::to_string(map_port(stream::CONTROL_PORT));
auto gs_http = std::to_string(map_port(nvhttp::PORT_HTTP));
auto gs_https = std::to_string(map_port(nvhttp::PORT_HTTPS));
auto wm_http = std::to_string(map_port(confighttp::PORT_HTTPS));
std::vector<mapping_t> mappings {
{ rtsp, rtsp, "RTSP setup port"s, false },
{ video, video, "Video stream port"s, false },
{ audio, audio, "Control stream port"s, false },
{ control, control, "Audio stream port"s, false },
{ gs_http, gs_http, "Gamestream http port"s, true },
{ gs_https, gs_https, "Gamestream https port"s, true },
};
// Only map port for the Web Manager if it is configured to accept connection from WAN
if(net::from_enum_string(config::nvhttp.origin_web_ui_allowed) > net::LAN) {
mappings.emplace_back(mapping_t { wm_http, wm_http, "Sunshine Web UI port"s, true });
}
auto it = std::begin(mappings);
status = 0;
for(; it != std::end(mappings); ++it) {
status = UPNP_AddPortMapping(
urls->controlURL,
data.first.servicetype,
it->port.wan.c_str(),
it->port.lan.c_str(),
lan_addr.data(),
it->description.c_str(),
it->tcp ? "TCP" : "UDP",
nullptr,
"86400");
if(status) {
BOOST_LOG(error) << "Failed to map port ["sv << it->port.wan << "] to port ["sv << it->port.lan << "]: error code ["sv << status << ']';
break;
}
}
if(status) {
unmap(urls, data, std::make_reverse_iterator(it), std::rend(mappings));
return nullptr;
}
return std::make_unique<deinit_t>(std::move(urls), data, std::move(mappings));
}
} // namespace upnp

10
sunshine/upnp.h Normal file
View File

@@ -0,0 +1,10 @@
#ifndef SUNSHINE_UPNP_H
#define SUNSHINE_UPNP_H
#include "platform/common.h"
namespace upnp {
[[nodiscard]] std::unique_ptr<platf::deinit_t> start();
}
#endif

View File

@@ -1,61 +1,109 @@
#ifndef UTILITY_H
#define UTILITY_H
#include <algorithm>
#include <condition_variable>
#include <memory>
#include <mutex>
#include <optional>
#include <string_view>
#include <type_traits>
#include <variant>
#include <vector>
#include <memory>
#include <type_traits>
#include <algorithm>
#include <optional>
#include <mutex>
#include <condition_variable>
#include <string_view>
#define KITTY_DECL_CONSTR(x)\
x(x&&) noexcept = default;\
x&operator=(x&&) noexcept = default;\
#define KITTY_WHILE_LOOP(x, y, z) \
{ \
x; \
while(y) z \
}
template<typename T>
struct argument_type;
template<typename T, typename U>
struct argument_type<T(U)> { typedef U type; };
#define KITTY_USING_MOVE_T(move_t, t, init_val, z) \
class move_t { \
public: \
using element_type = typename argument_type<void(t)>::type; \
\
move_t() : el { init_val } {} \
template<class... Args> \
move_t(Args &&...args) : el { std::forward<Args>(args)... } {} \
move_t(const move_t &) = delete; \
\
move_t(move_t &&other) noexcept : el { std::move(other.el) } { \
other.el = element_type { init_val }; \
} \
\
move_t &operator=(const move_t &) = delete; \
\
move_t &operator=(move_t &&other) { \
std::swap(el, other.el); \
return *this; \
} \
element_type *operator->() { return &el; } \
const element_type *operator->() const { return &el; } \
\
~move_t() z \
\
element_type el; \
}
#define KITTY_DECL_CONSTR(x) \
x(x &&) noexcept = default; \
x &operator=(x &&) noexcept = default; \
x();
#define KITTY_DEFAULT_CONSTR(x)\
x(x&&) noexcept = default;\
x&operator=(x&&) noexcept = default;\
x() = default;
#define KITTY_DEFAULT_CONSTR(x) \
x(x &&) noexcept = default; \
x &operator=(x &&) noexcept = default; \
x() = default;
#define KITTY_DEFAULT_CONSTR_THROW(x)\
x(x&&) = default;\
x&operator=(x&&) = default;\
x() = default;
#define KITTY_DEFAULT_CONSTR_THROW(x) \
x(x &&) = default; \
x &operator=(x &&) = default; \
x() = default;
#define TUPLE_2D(a,b, expr)\
decltype(expr) a##_##b = expr;\
auto &a = std::get<0>(a##_##b);\
auto &b = std::get<1>(a##_##b)
#define TUPLE_2D(a, b, expr) \
decltype(expr) a##_##b = expr; \
auto &a = std::get<0>(a##_##b); \
auto &b = std::get<1>(a##_##b)
#define TUPLE_2D_REF(a,b, expr)\
auto &a##_##b = expr;\
auto &a = std::get<0>(a##_##b);\
auto &b = std::get<1>(a##_##b)
#define TUPLE_2D_REF(a, b, expr) \
auto &a##_##b = expr; \
auto &a = std::get<0>(a##_##b); \
auto &b = std::get<1>(a##_##b)
#define TUPLE_3D(a,b,c, expr)\
decltype(expr) a##_##b##_##c = expr;\
auto &a = std::get<0>(a##_##b##_##c);\
auto &b = std::get<1>(a##_##b##_##c);\
auto &c = std::get<2>(a##_##b##_##c)
#define TUPLE_3D(a, b, c, expr) \
decltype(expr) a##_##b##_##c = expr; \
auto &a = std::get<0>(a##_##b##_##c); \
auto &b = std::get<1>(a##_##b##_##c); \
auto &c = std::get<2>(a##_##b##_##c)
#define TUPLE_3D_REF(a,b,c, expr)\
auto &a##_##b##_##c = expr;\
auto &a = std::get<0>(a##_##b##_##c);\
auto &b = std::get<1>(a##_##b##_##c);\
auto &c = std::get<2>(a##_##b##_##c)
#define TUPLE_3D_REF(a, b, c, expr) \
auto &a##_##b##_##c = expr; \
auto &a = std::get<0>(a##_##b##_##c); \
auto &b = std::get<1>(a##_##b##_##c); \
auto &c = std::get<2>(a##_##b##_##c)
#define TUPLE_EL(a, b, expr) \
decltype(expr) a##_ = expr; \
auto &a = std::get<b>(a##_)
#define TUPLE_EL_REF(a, b, expr) \
auto &a = std::get<b>(expr)
namespace util {
template<template<typename...> class X, class...Y>
template<template<typename...> class X, class... Y>
struct __instantiation_of : public std::false_type {};
template<template<typename...> class X, class... Y>
struct __instantiation_of<X, X<Y...>> : public std::true_type {};
template<template<typename...> class X, class T, class...Y>
template<template<typename...> class X, class T, class... Y>
static constexpr auto instantiation_of_v = __instantiation_of<X, T, Y...>::value;
template<bool V, class X, class Y>
@@ -74,42 +122,16 @@ struct __either<false, X, Y> {
template<bool V, class X, class Y>
using either_t = typename __either<V, X, Y>::type;
template<class T, class V = void>
struct __false_v;
template<class T>
struct __false_v<T, std::enable_if_t<instantiation_of_v<std::optional, T>>> {
static constexpr std::nullopt_t value = std::nullopt;
};
template<class T>
struct __false_v<T, std::enable_if_t<
(std::is_pointer_v<T> || instantiation_of_v<std::unique_ptr, T> || instantiation_of_v<std::shared_ptr, T>)
>> {
static constexpr std::nullptr_t value = nullptr;
};
template<class T>
struct __false_v<T, std::enable_if_t<std::is_same_v<T, bool>>> {
static constexpr bool value = false;
};
template<class T>
static constexpr auto false_v = __false_v<T>::value;
template<class T>
using optional_t = either_t<
(std::is_same_v<T, bool> ||
instantiation_of_v<std::unique_ptr, T> ||
instantiation_of_v<std::shared_ptr, T> ||
std::is_pointer_v<T>),
T, std::optional<T>>;
template<class... Ts>
struct overloaded : Ts... { using Ts::operator()...; };
template<class... Ts>
overloaded(Ts...) -> overloaded<Ts...>;
template<class T>
class FailGuard {
public:
FailGuard() = delete;
FailGuard(T && f) noexcept : _func { std::forward<T>(f) } {}
FailGuard(T &&f) noexcept : _func { std::forward<T>(f) } {}
FailGuard(FailGuard &&other) noexcept : _func { std::move(other._func) } {
this->failure = other.failure;
@@ -129,12 +151,13 @@ public:
void disable() { failure = false; }
bool failure { true };
private:
T _func;
};
template<class T>
[[nodiscard]] auto fail_guard(T && f) {
[[nodiscard]] auto fail_guard(T &&f) {
return FailGuard<T> { std::forward<T>(f) };
}
@@ -144,9 +167,9 @@ void append_struct(std::vector<uint8_t> &buf, const T &_struct) {
buf.reserve(data_len);
auto *data = (uint8_t *) & _struct;
auto *data = (uint8_t *)&_struct;
for (size_t x = 0; x < data_len; ++x) {
for(size_t x = 0; x < data_len; ++x) {
buf.push_back(data[x]);
}
}
@@ -155,24 +178,26 @@ template<class T>
class Hex {
public:
typedef T elem_type;
private:
const char _bits[16] {
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'
};
char _hex[sizeof(elem_type) * 2];
public:
Hex(const elem_type &elem, bool rev) {
if(!rev) {
const uint8_t *data = reinterpret_cast<const uint8_t *>(&elem) + sizeof(elem_type) - 1;
for (auto it = begin(); it < cend();) {
for(auto it = begin(); it < cend();) {
*it++ = _bits[*data / 16];
*it++ = _bits[*data-- % 16];
}
}
else {
const uint8_t *data = reinterpret_cast<const uint8_t *>(&elem);
for (auto it = begin(); it < cend();) {
for(auto it = begin(); it < cend();) {
*it++ = _bits[*data / 16];
*it++ = _bits[*data++ % 16];
}
@@ -204,7 +229,7 @@ Hex<T> hex(const T &elem, bool rev = false) {
template<class It>
std::string hex_vec(It begin, It end, bool rev = false) {
auto str_size = 2*std::distance(begin, end);
auto str_size = 2 * std::distance(begin, end);
std::string hex;
@@ -215,14 +240,14 @@ std::string hex_vec(It begin, It end, bool rev = false) {
};
if(rev) {
for (auto it = std::begin(hex); it < std::end(hex);) {
for(auto it = std::begin(hex); it < std::end(hex);) {
*it++ = _bits[((uint8_t)*begin) / 16];
*it++ = _bits[((uint8_t)*begin++) % 16];
}
}
else {
--end;
for (auto it = std::begin(hex); it < std::end(hex);) {
for(auto it = std::begin(hex); it < std::end(hex);) {
*it++ = _bits[((uint8_t)*end) / 16];
*it++ = _bits[((uint8_t)*end--) % 16];
}
@@ -233,7 +258,7 @@ std::string hex_vec(It begin, It end, bool rev = false) {
}
template<class C>
std::string hex_vec(C&& c, bool rev = false) {
std::string hex_vec(C &&c, bool rev = false) {
return hex_vec(std::begin(c), std::end(c), rev);
}
@@ -242,7 +267,7 @@ std::optional<T> from_hex(const std::string_view &hex, bool rev = false) {
std::uint8_t buf[sizeof(T)];
static char constexpr shift_bit = 'a' - 'A';
auto is_convertable = [] (char ch) -> bool {
auto is_convertable = [](char ch) -> bool {
if(isdigit(ch)) {
return true;
}
@@ -261,9 +286,9 @@ std::optional<T> from_hex(const std::string_view &hex, bool rev = false) {
return std::nullopt;
}
const char *data = hex.data() + hex.size() -1;
const char *data = hex.data() + hex.size() - 1;
auto convert = [] (char ch) -> std::uint8_t {
auto convert = [](char ch) -> std::uint8_t {
if(ch >= '0' && ch <= '9') {
return (std::uint8_t)ch - '0';
}
@@ -292,7 +317,7 @@ inline std::string from_hex_vec(const std::string &hex, bool rev = false) {
std::string buf;
static char constexpr shift_bit = 'a' - 'A';
auto is_convertable = [] (char ch) -> bool {
auto is_convertable = [](char ch) -> bool {
if(isdigit(ch)) {
return true;
}
@@ -309,9 +334,9 @@ inline std::string from_hex_vec(const std::string &hex, bool rev = false) {
auto buf_size = std::count_if(std::begin(hex), std::end(hex), is_convertable) / 2;
buf.resize(buf_size);
const char *data = hex.data() + hex.size() -1;
const char *data = hex.data() + hex.size() - 1;
auto convert = [] (char ch) -> std::uint8_t {
auto convert = [](char ch) -> std::uint8_t {
if(ch >= '0' && ch <= '9') {
return (std::uint8_t)ch - '0';
}
@@ -343,112 +368,20 @@ public:
std::size_t operator()(const value_type &value) const {
const auto *p = reinterpret_cast<const char *>(&value);
return std::hash<std::string_view>{}(std::string_view { p, sizeof(value_type) });
return std::hash<std::string_view> {}(std::string_view { p, sizeof(value_type) });
}
};
template<class T>
auto enm(const T& val) -> const std::underlying_type_t<T>& {
return *reinterpret_cast<const std::underlying_type_t<T>*>(&val);
auto enm(const T &val) -> const std::underlying_type_t<T> & {
return *reinterpret_cast<const std::underlying_type_t<T> *>(&val);
}
template<class T>
auto enm(T& val) -> std::underlying_type_t<T>& {
return *reinterpret_cast<std::underlying_type_t<T>*>(&val);
auto enm(T &val) -> std::underlying_type_t<T> & {
return *reinterpret_cast<std::underlying_type_t<T> *>(&val);
}
template<class ReturnType, class ...Args>
struct Function {
typedef ReturnType (*type)(Args...);
};
template<class T, class ReturnType, typename Function<ReturnType, T>::type function>
struct Destroy {
typedef T pointer;
void operator()(pointer p) {
function(p);
}
};
template<class T, typename Function<void, T*>::type function>
using safe_ptr = std::unique_ptr<T, Destroy<T*, void, function>>;
// You cannot specialize an alias
template<class T, class ReturnType, typename Function<ReturnType, T*>::type function>
using safe_ptr_v2 = std::unique_ptr<T, Destroy<T*, ReturnType, function>>;
template<class T>
void c_free(T *p) {
free(p);
}
template<class T>
using c_ptr = safe_ptr<T, c_free<T>>;
template<class T>
class FakeContainer {
typedef T pointer;
pointer _begin;
pointer _end;
public:
FakeContainer(pointer begin, pointer end) : _begin(begin), _end(end) {}
pointer begin() { return _begin; }
pointer end() { return _end; }
const pointer begin() const { return _begin; }
const pointer end() const { return _end; }
const pointer cbegin() const { return _begin; }
const pointer cend() const { return _end; }
pointer data() { return begin(); }
const pointer data() const { return cbegin(); }
std::size_t size() const { return std::distance(begin(), end()); }
};
template<class T>
FakeContainer<T> toContainer(T begin, T end) {
return { begin, end };
}
template<class T>
FakeContainer<T> toContainer(T begin, std::size_t end) {
return { begin, begin + end };
}
template<class T>
FakeContainer<T*> toContainer(T * const begin) {
T *end = begin;
auto default_val = T();
while(*end != default_val) {
++end;
}
return toContainer(begin, end);
}
template<class T, class H>
struct _init_helper;
template<template<class...> class T, class H, class... Args>
struct _init_helper<T<Args...>, H> {
using type = T<Args...>;
static type move(Args&&... args, H&&) {
return std::make_tuple(std::move(args)...);
}
static type copy(const Args&... args, const H&) {
return std::make_tuple(args...);
}
};
inline std::int64_t from_chars(const char *begin, const char *end) {
std::int64_t res {};
std::int64_t mul = 1;
@@ -494,15 +427,281 @@ public:
}
};
// Compared to std::unique_ptr, it adds the ability to get the address of the pointer itself
template<typename T, typename D = std::default_delete<T>>
class uniq_ptr {
public:
using element_type = T;
using pointer = element_type *;
using deleter_type = D;
constexpr uniq_ptr() noexcept : _p { nullptr } {}
constexpr uniq_ptr(std::nullptr_t) noexcept : _p { nullptr } {}
uniq_ptr(const uniq_ptr &other) noexcept = delete;
uniq_ptr &operator=(const uniq_ptr &other) noexcept = delete;
template<class V>
uniq_ptr(V *p) noexcept : _p { p } {
static_assert(std::is_same_v<element_type, void> || std::is_same_v<element_type, V> || std::is_base_of_v<element_type, V>, "element_type must be base class of V");
}
template<class V>
uniq_ptr(std::unique_ptr<V, deleter_type> &&uniq) noexcept : _p { uniq.release() } {
static_assert(std::is_same_v<element_type, void> || std::is_same_v<T, V> || std::is_base_of_v<element_type, V>, "element_type must be base class of V");
}
template<class V>
uniq_ptr(uniq_ptr<V, deleter_type> &&other) noexcept : _p { other.release() } {
static_assert(std::is_same_v<element_type, void> || std::is_same_v<T, V> || std::is_base_of_v<element_type, V>, "element_type must be base class of V");
}
template<class V>
uniq_ptr &operator=(uniq_ptr<V, deleter_type> &&other) noexcept {
static_assert(std::is_same_v<element_type, void> || std::is_same_v<T, V> || std::is_base_of_v<element_type, V>, "element_type must be base class of V");
reset(other.release());
return *this;
}
template<class V>
uniq_ptr &operator=(std::unique_ptr<V, deleter_type> &&uniq) noexcept {
static_assert(std::is_same_v<element_type, void> || std::is_same_v<T, V> || std::is_base_of_v<element_type, V>, "element_type must be base class of V");
reset(uniq.release());
return *this;
}
~uniq_ptr() {
reset();
}
void reset(pointer p = pointer()) {
if(_p) {
_deleter(_p);
}
_p = p;
}
pointer release() {
auto tmp = _p;
_p = nullptr;
return tmp;
}
pointer get() {
return _p;
}
const pointer get() const {
return _p;
}
const std::add_lvalue_reference_t<element_type> operator*() const {
return *_p;
}
std::add_lvalue_reference_t<element_type> operator*() {
return *_p;
}
const pointer operator->() const {
return _p;
}
pointer operator->() {
return _p;
}
pointer *operator&() const {
return &_p;
}
pointer *operator&() {
return &_p;
}
deleter_type &get_deleter() {
return _deleter;
}
const deleter_type &get_deleter() const {
return _deleter;
}
explicit operator bool() const {
return _p != nullptr;
}
protected:
pointer _p;
deleter_type _deleter;
};
template<class T1, class D1, class T2, class D2>
bool operator==(const uniq_ptr<T1, D1> &x, const uniq_ptr<T2, D2> &y) {
return x.get() == y.get();
}
template<class T1, class D1, class T2, class D2>
bool operator!=(const uniq_ptr<T1, D1> &x, const uniq_ptr<T2, D2> &y) {
return x.get() != y.get();
}
template<class T1, class D1, class T2, class D2>
bool operator==(const std::unique_ptr<T1, D1> &x, const uniq_ptr<T2, D2> &y) {
return x.get() == y.get();
}
template<class T1, class D1, class T2, class D2>
bool operator!=(const std::unique_ptr<T1, D1> &x, const uniq_ptr<T2, D2> &y) {
return x.get() != y.get();
}
template<class T1, class D1, class T2, class D2>
bool operator==(const uniq_ptr<T1, D1> &x, const std::unique_ptr<T1, D1> &y) {
return x.get() == y.get();
}
template<class T1, class D1, class T2, class D2>
bool operator!=(const uniq_ptr<T1, D1> &x, const std::unique_ptr<T1, D1> &y) {
return x.get() != y.get();
}
template<class T, class D>
bool operator==(const uniq_ptr<T, D> &x, std::nullptr_t) {
return !(bool)x;
}
template<class T, class D>
bool operator!=(const uniq_ptr<T, D> &x, std::nullptr_t) {
return (bool)x;
}
template<class T, class D>
bool operator==(std::nullptr_t, const uniq_ptr<T, D> &y) {
return !(bool)y;
}
template<class T, class D>
bool operator!=(std::nullptr_t, const uniq_ptr<T, D> &y) {
return (bool)y;
}
template<class T>
class wrap_ptr {
public:
using element_type = T;
using pointer = element_type *;
using reference = element_type &;
wrap_ptr() : _own_ptr { false }, _p { nullptr } {}
wrap_ptr(pointer p) : _own_ptr { false }, _p { p } {}
wrap_ptr(std::unique_ptr<element_type> &&uniq_p) : _own_ptr { true }, _p { uniq_p.release() } {}
wrap_ptr(wrap_ptr &&other) : _own_ptr { other._own_ptr }, _p { other._p } {
other._own_ptr = false;
}
wrap_ptr &operator=(wrap_ptr &&other) noexcept {
if(_own_ptr) {
delete _p;
}
_p = other._p;
_own_ptr = other._own_ptr;
other._own_ptr = false;
return *this;
}
template<class V>
wrap_ptr &operator=(std::unique_ptr<V> &&uniq_ptr) {
static_assert(std::is_base_of_v<element_type, V>, "element_type must be base class of V");
_own_ptr = true;
_p = uniq_ptr.release();
return *this;
}
wrap_ptr &operator=(pointer p) {
if(_own_ptr) {
delete _p;
}
_p = p;
_own_ptr = false;
return *this;
}
~wrap_ptr() {
if(_own_ptr) {
delete _p;
}
_own_ptr = false;
}
const reference operator*() const {
return *_p;
}
reference operator*() {
return *_p;
}
const pointer operator->() const {
return _p;
}
pointer operator->() {
return _p;
}
private:
bool _own_ptr;
pointer _p;
};
template<class T>
constexpr bool is_pointer_v =
instantiation_of_v<std::unique_ptr, T> ||
instantiation_of_v<std::shared_ptr, T> ||
instantiation_of_v<uniq_ptr, T> ||
std::is_pointer_v<T>;
template<class T, class V = void>
struct __false_v;
template<class T>
struct __false_v<T, std::enable_if_t<instantiation_of_v<std::optional, T>>> {
static constexpr std::nullopt_t value = std::nullopt;
};
template<class T>
struct __false_v<T, std::enable_if_t<is_pointer_v<T>>> {
static constexpr std::nullptr_t value = nullptr;
};
template<class T>
struct __false_v<T, std::enable_if_t<std::is_same_v<T, bool>>> {
static constexpr bool value = false;
};
template<class T>
static constexpr auto false_v = __false_v<T>::value;
template<class T>
using optional_t = either_t<
(std::is_same_v<T, bool> || is_pointer_v<T>),
T, std::optional<T>>;
template<class T>
class buffer_t {
public:
buffer_t() : _els { 0 } {};
buffer_t(buffer_t&&) noexcept = default;
buffer_t &operator=(buffer_t&& other) noexcept {
std::swap(_els, other._els);
_buf = std::move(other._buf);
buffer_t(buffer_t &&o) noexcept : _els { o._els }, _buf { std::move(o._buf) } {
o._els = 0;
}
buffer_t &operator=(buffer_t &&o) noexcept {
std::swap(_els, o._els);
std::swap(_buf, o._buf);
return *this;
};
@@ -549,7 +748,6 @@ private:
std::unique_ptr<T[]> _buf;
};
template<class T>
T either(std::optional<T> &&l, T &&r) {
if(l) {
@@ -559,27 +757,77 @@ T either(std::optional<T> &&l, T &&r) {
return std::forward<T>(r);
}
template<class ReturnType, class... Args>
struct Function {
typedef ReturnType (*type)(Args...);
};
template<class T, class ReturnType, typename Function<ReturnType, T>::type function>
struct Destroy {
typedef T pointer;
void operator()(pointer p) {
function(p);
}
};
template<class T, typename Function<void, T *>::type function>
using safe_ptr = uniq_ptr<T, Destroy<T *, void, function>>;
// You cannot specialize an alias
template<class T, class ReturnType, typename Function<ReturnType, T *>::type function>
using safe_ptr_v2 = uniq_ptr<T, Destroy<T *, ReturnType, function>>;
template<class T>
void c_free(T *p) {
free(p);
}
template<class T, class ReturnType, ReturnType (**function)(T *)>
void dynamic(T *p) {
(*function)(p);
}
template<class T, void (**function)(T *)>
using dyn_safe_ptr = safe_ptr<T, dynamic<T, void, function>>;
template<class T, class ReturnType, ReturnType (**function)(T *)>
using dyn_safe_ptr_v2 = safe_ptr<T, dynamic<T, ReturnType, function>>;
template<class T>
using c_ptr = safe_ptr<T, c_free<T>>;
template<class It>
std::string_view view(It begin, It end) {
return std::string_view { (const char *)begin, (std::size_t)(end - begin) };
}
template<class T>
std::string_view view(const T &data) {
return std::string_view((const char *)&data, sizeof(T));
}
namespace endian {
template<class T = void>
struct endianness {
enum : bool {
#if defined(__BYTE_ORDER) && __BYTE_ORDER == __BIG_ENDIAN || \
defined(__BIG_ENDIAN__) || \
defined(__ARMEB__) || \
defined(__THUMBEB__) || \
defined(__AARCH64EB__) || \
defined(_MIBSEB) || defined(__MIBSEB) || defined(__MIBSEB__)
defined(__BIG_ENDIAN__) || \
defined(__ARMEB__) || \
defined(__THUMBEB__) || \
defined(__AARCH64EB__) || \
defined(_MIBSEB) || defined(__MIBSEB) || defined(__MIBSEB__)
// It's a big-endian target architecture
little = false,
#elif defined(__BYTE_ORDER) && __BYTE_ORDER == __LITTLE_ENDIAN || \
defined(__LITTLE_ENDIAN__) || \
defined(__ARMEL__) || \
defined(__THUMBEL__) || \
defined(__AARCH64EL__) || \
defined(_MIPSEL) || defined(__MIPSEL) || defined(__MIPSEL__) || \
defined(_WIN32)
defined(__LITTLE_ENDIAN__) || \
defined(__ARMEL__) || \
defined(__THUMBEL__) || \
defined(__AARCH64EL__) || \
defined(_MIPSEL) || defined(__MIPSEL) || defined(__MIPSEL__) || \
defined(_WIN32)
// It's a little-endian target architecture
little = true,
little = true,
#else
#error "Unknown Endianness"
#endif
@@ -588,15 +836,14 @@ struct endianness {
};
template<class T, class S = void>
struct endian_helper { };
struct endian_helper {};
template<class T>
struct endian_helper<T, std::enable_if_t<
!(instantiation_of_v<std::optional, T>)
>> {
!(instantiation_of_v<std::optional, T>)>> {
static inline T big(T x) {
if constexpr (endianness<T>::little) {
uint8_t *data = reinterpret_cast<uint8_t*>(&x);
if constexpr(endianness<T>::little) {
uint8_t *data = reinterpret_cast<uint8_t *>(&x);
std::reverse(data, data + sizeof(x));
}
@@ -605,8 +852,8 @@ struct endian_helper<T, std::enable_if_t<
}
static inline T little(T x) {
if constexpr (endianness<T>::big) {
uint8_t *data = reinterpret_cast<uint8_t*>(&x);
if constexpr(endianness<T>::big) {
uint8_t *data = reinterpret_cast<uint8_t *>(&x);
std::reverse(data, data + sizeof(x));
}
@@ -617,32 +864,31 @@ struct endian_helper<T, std::enable_if_t<
template<class T>
struct endian_helper<T, std::enable_if_t<
instantiation_of_v<std::optional, T>
>> {
static inline T little(T x) {
if(!x) return x;
instantiation_of_v<std::optional, T>>> {
static inline T little(T x) {
if(!x) return x;
if constexpr (endianness<T>::big) {
auto *data = reinterpret_cast<uint8_t*>(&*x);
if constexpr(endianness<T>::big) {
auto *data = reinterpret_cast<uint8_t *>(&*x);
std::reverse(data, data + sizeof(*x));
std::reverse(data, data + sizeof(*x));
}
return x;
}
return x;
}
static inline T big(T x) {
if(!x) return x;
static inline T big(T x) {
if(!x) return x;
if constexpr(endianness<T>::big) {
auto *data = reinterpret_cast<uint8_t *>(&*x);
if constexpr (endianness<T>::big) {
auto *data = reinterpret_cast<uint8_t*>(&*x);
std::reverse(data, data + sizeof(*x));
}
std::reverse(data, data + sizeof(*x));
return x;
}
return x;
}
};
template<class T>
@@ -650,7 +896,6 @@ inline auto little(T x) { return endian_helper<T>::little(x); }
template<class T>
inline auto big(T x) { return endian_helper<T>::big(x); }
} /* endian */
} /* util */
} // namespace endian
} // namespace util
#endif

View File

@@ -18,12 +18,12 @@ union uuid_t {
std::uniform_int_distribution<std::uint8_t> dist(0, std::numeric_limits<std::uint8_t>::max());
uuid_t buf;
for (auto &el : buf.b8) {
for(auto &el : buf.b8) {
el = dist(engine);
}
buf.b8[7] &= (std::uint8_t) 0b00101111;
buf.b8[9] &= (std::uint8_t) 0b10011111;
buf.b8[7] &= (std::uint8_t)0b00101111;
buf.b8[9] &= (std::uint8_t)0b10011111;
return buf;
}
@@ -31,7 +31,7 @@ union uuid_t {
static uuid_t generate() {
std::random_device r;
std::default_random_engine engine{r()};
std::default_random_engine engine { r() };
return generate(engine);
}
@@ -41,7 +41,7 @@ union uuid_t {
result.reserve(sizeof(uuid_t) * 2 + 4);
auto hex = util::hex(*this, true);
auto hex = util::hex(*this, true);
auto hex_view = hex.to_string_view();
std::string_view slices[] = {
@@ -75,5 +75,5 @@ union uuid_t {
return (b64[0] > other.b64[0] || (b64[0] == other.b64[0] && b64[1] > other.b64[1]));
}
};
}
} // namespace util
#endif //T_MAN_UUID_H

File diff suppressed because it is too large Load Diff

View File

@@ -5,15 +5,59 @@
#ifndef SUNSHINE_VIDEO_H
#define SUNSHINE_VIDEO_H
#include "input.h"
#include "platform/common.h"
#include "thread_safe.h"
extern "C" {
#include <libavcodec/avcodec.h>
}
struct AVPacket;
namespace video {
void free_packet(AVPacket *packet);
using packet_t = util::safe_ptr<AVPacket, free_packet>;
using packet_queue_t = std::shared_ptr<safe::queue_t<packet_t>>;
using idr_event_t = std::shared_ptr<safe::event_t<std::pair<int64_t, int64_t>>>;
struct packet_raw_t : public AVPacket {
void init_packet() {
pts = AV_NOPTS_VALUE;
dts = AV_NOPTS_VALUE;
pos = -1;
duration = 0;
flags = 0;
stream_index = 0;
buf = nullptr;
side_data = nullptr;
side_data_elems = 0;
}
template<class P>
explicit packet_raw_t(P *user_data) : channel_data { user_data } {
init_packet();
}
explicit packet_raw_t(std::nullptr_t) : channel_data { nullptr } {
init_packet();
}
~packet_raw_t() {
av_packet_unref(this);
}
struct replace_t {
std::string_view old;
std::string_view _new;
KITTY_DEFAULT_CONSTR(replace_t)
replace_t(std::string_view old, std::string_view _new) noexcept : old { std::move(old) }, _new { std::move(_new) } {}
};
std::vector<replace_t> *replacements;
void *channel_data;
};
using packet_t = std::unique_ptr<packet_raw_t>;
using idr_t = std::pair<int64_t, int64_t>;
struct config_t {
int width;
@@ -27,7 +71,26 @@ struct config_t {
int dynamicRange;
};
void capture_display(packet_queue_t packets, idr_event_t idr_events, config_t config);
}
using float4 = float[4];
using float3 = float[3];
using float2 = float[2];
struct __attribute__((__aligned__(16))) color_t {
float4 color_vec_y;
float4 color_vec_u;
float4 color_vec_v;
float2 range_y;
float2 range_uv;
};
extern color_t colors[4];
void capture(
safe::mail_t mail,
config_t config,
void *channel_data);
int init();
} // namespace video
#endif //SUNSHINE_VIDEO_H

69
third-party/cbs/CMakeLists.txt vendored Normal file
View File

@@ -0,0 +1,69 @@
cmake_minimum_required(VERSION 3.0)
project(CBS)
SET(CBS_SOURCE_FILES
include/cbs/av1.h
include/cbs/cbs_av1.h
include/cbs/cbs_bsf.h
include/cbs/cbs.h
include/cbs/cbs_h2645.h
include/cbs/cbs_h264.h
include/cbs/cbs_h265.h
include/cbs/cbs_jpeg.h
include/cbs/cbs_mpeg2.h
include/cbs/cbs_sei.h
include/cbs/cbs_vp9.h
include/cbs/h2645_parse.h
include/cbs/h264.h
include/cbs/hevc.h
include/cbs/sei.h
include/cbs/video_levels.h
cbs.c
cbs_h2645.c
cbs_av1.c
cbs_vp9.c
cbs_mpeg2.c
cbs_jpeg.c
cbs_sei.c
h2645_parse.c
video_levels.c
bytestream.h
cbs_internal.h
defs.h
get_bits.h
h264_ps.h
h264_sei.h
hevc_sei.h
intmath.h
mathops.h
put_bits.h
vlc.h
config.h
)
include_directories(include)
if(DEFINED FFMPEG_INCLUDE_DIRS)
include_directories(${FFMPEG_INCLUDE_DIRS})
endif()
add_compile_definitions(
HAVE_THREADS=1
HAVE_FAST_UNALIGNED
PIC=1
CONFIG_CBS_AV1=1
CONFIG_CBS_H264=1
CONFIG_CBS_H265=1
CONFIG_CBS_JPEG=1
CONFIG_CBS_MPEG2=1
CONFIG_CBS_VP9=1
)
add_library(cbs ${CBS_SOURCE_FILES})
target_compile_options(cbs PRIVATE -Wall -Wno-incompatible-pointer-types -Wno-maybe-uninitialized -Wno-format -Wno-format-extra-args)

351
third-party/cbs/bytestream.h vendored Normal file
View File

@@ -0,0 +1,351 @@
/*
* Bytestream functions
* copyright (c) 2006 Baptiste Coudurier <baptiste.coudurier@free.fr>
* Copyright (c) 2012 Aneesh Dogra (lionaneesh) <lionaneesh@gmail.com>
*
* This file is part of FFmpeg.
*
* FFmpeg is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* FFmpeg is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with FFmpeg; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
#ifndef AVCODEC_BYTESTREAM_H
#define AVCODEC_BYTESTREAM_H
#include "config.h"
#include <stdint.h>
#include <string.h>
#include <libavutil/avassert.h>
#include <libavutil/common.h>
#include <libavutil/intreadwrite.h>
typedef struct GetByteContext {
const uint8_t *buffer, *buffer_end, *buffer_start;
} GetByteContext;
typedef struct PutByteContext {
uint8_t *buffer, *buffer_end, *buffer_start;
int eof;
} PutByteContext;
#define DEF(type, name, bytes, read, write) \
static av_always_inline type bytestream_get_##name(const uint8_t **b) { \
(*b) += bytes; \
return read(*b - bytes); \
} \
static av_always_inline void bytestream_put_##name(uint8_t **b, \
const type value) { \
write(*b, value); \
(*b) += bytes; \
} \
static av_always_inline void bytestream2_put_##name##u(PutByteContext *p, \
const type value) { \
bytestream_put_##name(&p->buffer, value); \
} \
static av_always_inline void bytestream2_put_##name(PutByteContext *p, \
const type value) { \
if(!p->eof && (p->buffer_end - p->buffer >= bytes)) { \
write(p->buffer, value); \
p->buffer += bytes; \
} \
else \
p->eof = 1; \
} \
static av_always_inline type bytestream2_get_##name##u(GetByteContext *g) { \
return bytestream_get_##name(&g->buffer); \
} \
static av_always_inline type bytestream2_get_##name(GetByteContext *g) { \
if(g->buffer_end - g->buffer < bytes) { \
g->buffer = g->buffer_end; \
return 0; \
} \
return bytestream2_get_##name##u(g); \
} \
static av_always_inline type bytestream2_peek_##name##u(GetByteContext *g) { \
return read(g->buffer); \
} \
static av_always_inline type bytestream2_peek_##name(GetByteContext *g) { \
if(g->buffer_end - g->buffer < bytes) \
return 0; \
return bytestream2_peek_##name##u(g); \
}
DEF(uint64_t, le64, 8, AV_RL64, AV_WL64)
DEF(unsigned int, le32, 4, AV_RL32, AV_WL32)
DEF(unsigned int, le24, 3, AV_RL24, AV_WL24)
DEF(unsigned int, le16, 2, AV_RL16, AV_WL16)
DEF(uint64_t, be64, 8, AV_RB64, AV_WB64)
DEF(unsigned int, be32, 4, AV_RB32, AV_WB32)
DEF(unsigned int, be24, 3, AV_RB24, AV_WB24)
DEF(unsigned int, be16, 2, AV_RB16, AV_WB16)
DEF(unsigned int, byte, 1, AV_RB8, AV_WB8)
#if AV_HAVE_BIGENDIAN
#define bytestream2_get_ne16 bytestream2_get_be16
#define bytestream2_get_ne24 bytestream2_get_be24
#define bytestream2_get_ne32 bytestream2_get_be32
#define bytestream2_get_ne64 bytestream2_get_be64
#define bytestream2_get_ne16u bytestream2_get_be16u
#define bytestream2_get_ne24u bytestream2_get_be24u
#define bytestream2_get_ne32u bytestream2_get_be32u
#define bytestream2_get_ne64u bytestream2_get_be64u
#define bytestream2_put_ne16 bytestream2_put_be16
#define bytestream2_put_ne24 bytestream2_put_be24
#define bytestream2_put_ne32 bytestream2_put_be32
#define bytestream2_put_ne64 bytestream2_put_be64
#define bytestream2_peek_ne16 bytestream2_peek_be16
#define bytestream2_peek_ne24 bytestream2_peek_be24
#define bytestream2_peek_ne32 bytestream2_peek_be32
#define bytestream2_peek_ne64 bytestream2_peek_be64
#else
#define bytestream2_get_ne16 bytestream2_get_le16
#define bytestream2_get_ne24 bytestream2_get_le24
#define bytestream2_get_ne32 bytestream2_get_le32
#define bytestream2_get_ne64 bytestream2_get_le64
#define bytestream2_get_ne16u bytestream2_get_le16u
#define bytestream2_get_ne24u bytestream2_get_le24u
#define bytestream2_get_ne32u bytestream2_get_le32u
#define bytestream2_get_ne64u bytestream2_get_le64u
#define bytestream2_put_ne16 bytestream2_put_le16
#define bytestream2_put_ne24 bytestream2_put_le24
#define bytestream2_put_ne32 bytestream2_put_le32
#define bytestream2_put_ne64 bytestream2_put_le64
#define bytestream2_peek_ne16 bytestream2_peek_le16
#define bytestream2_peek_ne24 bytestream2_peek_le24
#define bytestream2_peek_ne32 bytestream2_peek_le32
#define bytestream2_peek_ne64 bytestream2_peek_le64
#endif
static av_always_inline void bytestream2_init(GetByteContext *g,
const uint8_t *buf,
int buf_size) {
av_assert0(buf_size >= 0);
g->buffer = buf;
g->buffer_start = buf;
g->buffer_end = buf + buf_size;
}
static av_always_inline void bytestream2_init_writer(PutByteContext *p,
uint8_t *buf,
int buf_size) {
av_assert0(buf_size >= 0);
p->buffer = buf;
p->buffer_start = buf;
p->buffer_end = buf + buf_size;
p->eof = 0;
}
static av_always_inline int bytestream2_get_bytes_left(GetByteContext *g) {
return g->buffer_end - g->buffer;
}
static av_always_inline int bytestream2_get_bytes_left_p(PutByteContext *p) {
return p->buffer_end - p->buffer;
}
static av_always_inline void bytestream2_skip(GetByteContext *g,
unsigned int size) {
g->buffer += FFMIN(g->buffer_end - g->buffer, size);
}
static av_always_inline void bytestream2_skipu(GetByteContext *g,
unsigned int size) {
g->buffer += size;
}
static av_always_inline void bytestream2_skip_p(PutByteContext *p,
unsigned int size) {
int size2;
if(p->eof)
return;
size2 = FFMIN(p->buffer_end - p->buffer, size);
if(size2 != size)
p->eof = 1;
p->buffer += size2;
}
static av_always_inline int bytestream2_tell(GetByteContext *g) {
return (int)(g->buffer - g->buffer_start);
}
static av_always_inline int bytestream2_tell_p(PutByteContext *p) {
return (int)(p->buffer - p->buffer_start);
}
static av_always_inline int bytestream2_size(GetByteContext *g) {
return (int)(g->buffer_end - g->buffer_start);
}
static av_always_inline int bytestream2_size_p(PutByteContext *p) {
return (int)(p->buffer_end - p->buffer_start);
}
static av_always_inline int bytestream2_seek(GetByteContext *g,
int offset,
int whence) {
switch(whence) {
case SEEK_CUR:
offset = av_clip(offset, -(g->buffer - g->buffer_start),
g->buffer_end - g->buffer);
g->buffer += offset;
break;
case SEEK_END:
offset = av_clip(offset, -(g->buffer_end - g->buffer_start), 0);
g->buffer = g->buffer_end + offset;
break;
case SEEK_SET:
offset = av_clip(offset, 0, g->buffer_end - g->buffer_start);
g->buffer = g->buffer_start + offset;
break;
default:
return AVERROR(EINVAL);
}
return bytestream2_tell(g);
}
static av_always_inline int bytestream2_seek_p(PutByteContext *p,
int offset,
int whence) {
p->eof = 0;
switch(whence) {
case SEEK_CUR:
if(p->buffer_end - p->buffer < offset)
p->eof = 1;
offset = av_clip(offset, -(p->buffer - p->buffer_start),
p->buffer_end - p->buffer);
p->buffer += offset;
break;
case SEEK_END:
if(offset > 0)
p->eof = 1;
offset = av_clip(offset, -(p->buffer_end - p->buffer_start), 0);
p->buffer = p->buffer_end + offset;
break;
case SEEK_SET:
if(p->buffer_end - p->buffer_start < offset)
p->eof = 1;
offset = av_clip(offset, 0, p->buffer_end - p->buffer_start);
p->buffer = p->buffer_start + offset;
break;
default:
return AVERROR(EINVAL);
}
return bytestream2_tell_p(p);
}
static av_always_inline unsigned int bytestream2_get_buffer(GetByteContext *g,
uint8_t *dst,
unsigned int size) {
int size2 = FFMIN(g->buffer_end - g->buffer, size);
memcpy(dst, g->buffer, size2);
g->buffer += size2;
return size2;
}
static av_always_inline unsigned int bytestream2_get_bufferu(GetByteContext *g,
uint8_t *dst,
unsigned int size) {
memcpy(dst, g->buffer, size);
g->buffer += size;
return size;
}
static av_always_inline unsigned int bytestream2_put_buffer(PutByteContext *p,
const uint8_t *src,
unsigned int size) {
int size2;
if(p->eof)
return 0;
size2 = FFMIN(p->buffer_end - p->buffer, size);
if(size2 != size)
p->eof = 1;
memcpy(p->buffer, src, size2);
p->buffer += size2;
return size2;
}
static av_always_inline unsigned int bytestream2_put_bufferu(PutByteContext *p,
const uint8_t *src,
unsigned int size) {
memcpy(p->buffer, src, size);
p->buffer += size;
return size;
}
static av_always_inline void bytestream2_set_buffer(PutByteContext *p,
const uint8_t c,
unsigned int size) {
int size2;
if(p->eof)
return;
size2 = FFMIN(p->buffer_end - p->buffer, size);
if(size2 != size)
p->eof = 1;
memset(p->buffer, c, size2);
p->buffer += size2;
}
static av_always_inline void bytestream2_set_bufferu(PutByteContext *p,
const uint8_t c,
unsigned int size) {
memset(p->buffer, c, size);
p->buffer += size;
}
static av_always_inline unsigned int bytestream2_get_eof(PutByteContext *p) {
return p->eof;
}
static av_always_inline unsigned int bytestream2_copy_bufferu(PutByteContext *p,
GetByteContext *g,
unsigned int size) {
memcpy(p->buffer, g->buffer, size);
p->buffer += size;
g->buffer += size;
return size;
}
static av_always_inline unsigned int bytestream2_copy_buffer(PutByteContext *p,
GetByteContext *g,
unsigned int size) {
int size2;
if(p->eof)
return 0;
size = FFMIN(g->buffer_end - g->buffer, size);
size2 = FFMIN(p->buffer_end - p->buffer, size);
if(size2 != size)
p->eof = 1;
return bytestream2_copy_bufferu(p, g, size2);
}
static av_always_inline unsigned int bytestream_get_buffer(const uint8_t **b,
uint8_t *dst,
unsigned int size) {
memcpy(dst, *b, size);
(*b) += size;
return size;
}
static av_always_inline void bytestream_put_buffer(uint8_t **b,
const uint8_t *src,
unsigned int size) {
memcpy(*b, src, size);
(*b) += size;
}
#endif /* AVCODEC_BYTESTREAM_H */

1050
third-party/cbs/cbs.c vendored Normal file

File diff suppressed because it is too large Load Diff

1323
third-party/cbs/cbs_av1.c vendored Normal file

File diff suppressed because it is too large Load Diff

2063
third-party/cbs/cbs_av1_syntax_template.c vendored Normal file

File diff suppressed because it is too large Load Diff

1632
third-party/cbs/cbs_h2645.c vendored Normal file

File diff suppressed because it is too large Load Diff

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