Compare commits

..

372 Commits

Author SHA1 Message Date
loki-47-6F-64
e4c9c292e5 Fix package generation when building with the scripts 2021-10-05 13:53:58 +02:00
loki-47-6F-64
fe7a7f4d77 Fix build scripts when used on Ubuntu 21.04 2021-10-04 20:53:40 +02:00
loki
cb0b32f90a Add icon to windows executable 2021-10-03 21:12:06 +02:00
loki-47-6F-64
9e93bb2dd8 Merge branch 'master' into crct 2021-10-03 19:21:02 +02:00
loki-47-6F-64
0e6c41f823 Merge pull request #240 from luk1337/master
Explicitely link to Boost::log on non-win32 platforms
2021-10-03 19:19:32 +02:00
loki-47-6F-64
725212b8a4 Create scripts that handle building from source automatically 2021-10-03 18:46:34 +02:00
LuK1337
a275ee6b65 Explicitely link to Boost::log on non-win32 platforms
This fixes linker errors related to boost log library.
2021-10-03 00:18:05 +02:00
loki-47-6F-64
997095ce39 Resolve merge conflicts 2021-10-01 11:30:12 +02:00
loki-47-6F-64
8b9cd51134 Merge branch 'master' of github.com:loki-47-6F-64/sunshine 2021-09-27 19:12:51 +02:00
loki-47-6F-64
e7cbfb3ee9 Fix dependencies for debian bullseye 2021-09-27 19:54:32 +02:00
loki-47-6F-64
1f7bdb1b2a Fix segfault when multiple controllers connected 2021-09-27 19:35:06 +02:00
loki-47-6F-64
9f14b2278d Fix rate control for hevc with amdvce 2021-09-27 19:12:42 +02:00
loki-47-6F-64
4177b02064 Allow cuda kernel to run in parallell 2021-09-27 17:58:35 +02:00
loki-47-6F-64
847d7b6980 Fix minor error in README 2021-09-26 23:45:44 +02:00
diadatp
ef1114512f test 3 2021-09-27 01:29:52 +05:30
diadatp
38c36c00c9 test 2 2021-09-27 00:54:53 +05:30
diadatp
846820f4ec test 1 2021-09-27 00:24:24 +05:30
diadatp
c39e90b9b0 Trying again with full cuda install. 2021-09-26 22:36:22 +05:30
diadatp
096d7587c3 Forgot sudo in appveyor config file. 2021-09-26 19:06:29 +05:30
diadatp
27f5bb60e5 Testing AppVeyor CUDA install. 2021-09-26 18:55:28 +05:30
diadatp
70ed5ce829 Merge branch 'master' into cmake_cuda 2021-09-26 17:48:51 +05:30
diadatp
e58dc8e446 Fix CUDA build via CMakeLists flags. 2021-09-26 16:01:59 +05:30
loki-47-6F-64
60e3538adc Attempt to fix ubuntu 20.04 build, again 2021-09-26 11:39:36 +02:00
loki-47-6F-64
e2fb02323c Attempt to fix ubuntu 20.04 build 2021-09-26 10:57:43 +02:00
loki-47-6F-64
57c7945847 Fix typo in README 2021-09-26 00:18:49 +02:00
loki-47-6F-64
68c723e135 Resolve merge conflicts 2021-09-26 00:16:14 +02:00
loki-47-6F-64
0241414dfb Merge branch 'cuda' of github.com:loki-47-6F-64/sunshine into cuda 2021-09-25 22:31:48 +02:00
loki-47-6F-64
c5a356f3e7 Fix compilation on ubuntu 20.04 2021-09-25 22:31:39 +02:00
loki-47-6F-64
deecd19af2 Update appveyor.yml 2021-09-25 21:50:47 +02:00
loki-47-6F-64
f4cb13aa0e Merge pull request #229 from hadicharara/master
Added warning for low version of ffmpeg
2021-09-25 21:16:26 +02:00
Hadi Charara
4385014049 Added warning for low version of ffmpeg 2021-09-25 15:09:03 -04:00
loki-47-6F-64
fcb84132f4 Sharper images when not scaling the image 2021-09-25 19:18:43 +02:00
loki-47-6F-64
50bd3094b4 More accurate capture rate for NvFBC 2021-09-25 17:46:26 +02:00
loki-47-6F-64
d7cb71f877 Update README 2021-09-25 15:35:44 +02:00
loki-47-6F-64
e287404992 Handle acquiring display names based on encoder 2021-09-25 14:44:38 +02:00
loki-47-6F-64
d332f11101 Update instructions in the config file 2021-09-25 13:12:13 +02:00
loki-47-6F-64
f78a9e2ccf Fix downscaling image when using cuda 2021-09-25 12:44:45 +02:00
loki-47-6F-64
bd7294e672 Fix cuda kernel launch when encoding in 4K 2021-09-22 19:12:20 +02:00
loki-47-6F-64
b3304a059d Target older cuda architecture for compatibility 2021-09-22 14:49:49 +02:00
loki-47-6F-64
d0529fb234 Make dependency on cuda development files optional 2021-09-22 14:17:08 +02:00
loki-47-6F-64
bb912786bd Added NvFBC on Linux 2021-09-22 11:36:59 +02:00
loki-47-6F-64
8f47190ffc Test for availability of crct info before using it 2021-09-20 18:37:51 +02:00
loki-47-6F-64
196f1f7471 Make changes in brightness of the color more visible 2021-09-20 00:21:54 +02:00
loki-47-6F-64
e3cc25f23f Use linear interpolation with the cuda kernel 2021-09-20 00:03:33 +02:00
loki-47-6F-64
a963b31c1d Ensure the background color is black 2021-09-19 23:00:42 +02:00
loki-47-6F-64
4d1689d6e9 Each cuda::sws_t has it's own color matrix 2021-09-19 21:27:31 +02:00
loki-47-6F-64
fed329568c Use an actual cuda kernel to convert RGB to NV12 2021-09-19 20:40:34 +02:00
loki-47-6F-64
e3f642ac25 Reduce cpu usage with x11grab 2021-09-15 12:10:12 +02:00
loki-47-6F-64
c94d922282 Fix windows build 2021-09-14 19:16:29 +02:00
loki-47-6F-64
7563a0fa1f Merge branch 'master' into cuda 2021-09-14 15:09:21 +02:00
loki-47-6F-64
f5db0e438b The background is black instead of green 2021-09-14 15:07:34 +02:00
loki-47-6F-64
9982ae4675 Convert images on the GPU on Linux for NVidia cards 2021-09-14 11:58:21 +02:00
loki
69eba9c493 Merge branch 'master' of github.com:loki-47-6F-64/sunshine 2021-09-13 20:55:16 +02:00
loki-47-6F-64
2c67d73f08 Merge pull request #214 from exetico/patch-1
Adding note about `sink` in pipewire
2021-09-13 20:54:35 +02:00
loki
7f4b9cf36c Change where keycode is mapped 2021-09-13 20:34:45 +02:00
loki
9c0ea17ada Update Linux specific code for fix in previous commit 2021-09-13 20:33:17 +02:00
loki
91a6e55f74 Fix segfault when connecting multiple controllers at the same ttime 2021-09-13 20:24:04 +02:00
Tobias Nordahl Kristensen
fea650fbe4 Typo ipewire 2021-09-13 17:17:32 +02:00
Tobias Nordahl Kristensen
35e0497f86 Adding note about sink in pipewire
Also, splitting it up to two points, so it's a bit easier to read.
2021-09-11 16:07:52 +02:00
loki-47-6F-64
fbc3735f44 Merge pull request #210 from WisdomCode/master
Apply key rewrite also when keeping key pressed
2021-09-09 21:23:22 +02:00
WisdomCode
9be80c103e Shortcut Flags need to be adjusted as well 2021-09-09 21:15:35 +02:00
WisdomCode
640f2b1c55 Syntax 2021-09-09 15:05:04 +02:00
WisdomCode
d8df57130a Apply key rewrite also when keeping key pressed 2021-09-09 14:57:04 +02:00
loki-47-6F-64
3ba0533773 Merge pull request #207 from WisdomCode/master
Add non US Backslash for international keyboards on Linux
2021-09-08 21:59:44 +02:00
Christian Rupp
b1224032a1 Add non US Backslash for international keyboards on Linux 2021-09-08 17:18:27 +02:00
loki-47-6F-64
1a7ed53559 Merge pull request #205 from sethicis/patch-1
Update README.md
2021-09-07 09:10:37 +02:00
Kyle Blagg
9e9487617a Update README.md
Added a troubleshooting note for an issue encountered on Manjaro Linux due to Avahi.
2021-09-06 20:49:20 -04:00
Loki
d68b8138a5 Detect x11/wayland at runtime 2021-09-06 20:40:41 +02:00
Loki
10ca72f934 Refactor egl::surface_descriptor_t, removed obj_count and plane_indices 2021-09-06 19:46:42 +02:00
loki-47-6F-64
9dbf0df67d Merge pull request #202 from angelnu/patch-1
libcap-dev is associated to libcap2
2021-09-05 11:18:31 +02:00
Loki
b072af3082 Fix bad file descriptor when using wlgrab 2021-09-05 11:16:59 +02:00
Angel Nunez Mencias
35c8b74bb4 libcap-dev is associated to libcap2
at least since Ubuntu 18.04
2021-09-05 01:30:55 +02:00
Loki
7fbe9ba34f Added warning if only one of libcap or libdrm has been found 2021-09-04 20:15:08 +02:00
Loki
81c6ca5915 Merge branch 'master' into setcap-p 2021-09-04 18:30:29 +02:00
loki
d73a4a38e5 Fix multi-monitor setup with KMSgrab 2021-09-04 18:16:36 +02:00
David Rosca
6309f478a2 kmsgrab: Only gain effective CAP_SYS_ADMIN when needed 2021-09-04 16:28:59 +02:00
loki-47-6F-64
4ca2c0e740 Merge branch 'cpu-usage' 2021-09-04 12:09:27 +02:00
loki-47-6F-64
ea9ada8d20 Trade slightly higher rumble latency for lower cpu usage 2021-09-04 12:09:12 +02:00
loki-47-6F-64
302b61090b Merge pull request #200 from nowrep/pabuf
pulse: Set pa_buffer_attr.maxlength when creating stream
2021-09-03 15:45:56 +02:00
David Rosca
75d224cd67 pulse: Set pa_buffer_attr.maxlength when creating stream 2021-09-03 14:53:53 +02:00
David Rosca
2ff9a129c0 Reduce CPU usage 2021-09-03 14:03:59 +02:00
loki-47-6F-64
a1a4ce1af8 Merge pull request #196 from nowrep/wlsoftware
wlgrab: Fix ram (software encoding) snapshot
2021-09-03 11:23:52 +02:00
David Rosca
44ac873100 wlgrab: Fix ram (software encoding) snapshot 2021-09-03 11:02:38 +02:00
loki-47-6F-64
3d179a869a Push the right change this time xD 2021-09-03 10:20:29 +02:00
loki-47-6F-64
530f2de79e Revert "Test for capability of modifiers when importing framebuffer to openGL"
This reverts commit 276aa23a61.
2021-09-03 10:16:46 +02:00
loki-47-6F-64
512e581d56 Revert "Revert "Linux: DRM modifier 0 is valid""
This reverts commit e5c2ad2069.
2021-09-03 10:16:10 +02:00
loki-47-6F-64
e5c2ad2069 Revert "Linux: DRM modifier 0 is valid"
This reverts commit e33a7ff53b.
2021-09-03 10:12:16 +02:00
loki-47-6F-64
6af961199e Remove redundent code 2021-09-03 10:01:51 +02:00
loki-47-6F-64
276aa23a61 Test for capability of modifiers when importing framebuffer to openGL 2021-09-02 20:38:45 +02:00
David Rosca
e33a7ff53b Linux: DRM modifier 0 is valid
DRM_FORMAT_MOD_LINEAR = 0
2021-09-02 12:34:27 +02:00
David Rosca
d9d50d8943 kmsgrab: Fix multi-plane import 2021-09-02 12:34:19 +02:00
loki-47-6F-64
7c753e2289 Merge branch 'master' into wlroots-drm 2021-09-01 21:50:24 +02:00
loki-47-6F-64
ff81a286bb Merge pull request #185 from TheElixZammuto/web-ui-troubleshooting
Web UI - Troubleshooting Page
2021-09-01 21:48:34 +02:00
Loki
c7c3ac7c9c Merged with master 2021-09-01 14:22:41 +02:00
Loki
74f673e23c Fix build for SUNSHINE_ENABLE_WAYLAND=OFF 2021-08-31 21:19:02 +02:00
Loki
7c51fbfd18 Correlate KMS output to wayland xdg-output 2021-08-31 20:46:50 +02:00
Elia Zammuto
7839ff8057 Minor tweaks to troubleshooting page 2021-08-30 20:46:14 +02:00
loki-47-6F-64
8b91e168e5 Merge pull request #179 from nowrep/kmsgrab-multiplane
kmsgrab: Support multi-plane formats
2021-08-29 21:07:04 +02:00
David Rosca
08f056bb3f kmsgrab: Support multi-plane formats 2021-08-29 20:26:11 +02:00
Elia Zammuto
808af7fce1 Fix Alignment 2021-08-29 18:48:43 +02:00
Elia Zammuto
090d353f3d Added Visual Feedback on action 2021-08-29 18:42:09 +02:00
Elia Zammuto
c4b371ccc9 Merge branch 'master' into web-ui-troubleshooting 2021-08-29 18:25:03 +02:00
Loki
06a1119512 Fix segfault for wlroots based capturing 2021-08-29 09:34:00 +02:00
Loki
b80c4253f0 Make KMSgrab smooth on Wayland 2021-08-28 22:22:02 +02:00
Loki
3f306de5e1 Prevent OpenGL error when switching cursor image 2021-08-28 20:30:07 +02:00
Loki
39f9506446 Fix blank video for vaapi 2021-08-28 17:48:28 +02:00
Loki
00de30d336 Omit single copy of frame with VAAPI if possible 2021-08-26 23:59:32 +02:00
Loki
b59df48dde Keep image on vram if at all possible with wlroots based compositors 2021-08-26 22:06:59 +02:00
loki-47-6F-64
3840b3c561 Merge pull request #169 from gamozolabs/master
Make Sunshine usable fully offline
2021-08-26 08:35:53 +02:00
loki-47-6F-64
b5424ec671 Merge pull request #171 from felipejfc/starting_dir
feat: add support to specifying start_dir to processes
2021-08-26 08:35:23 +02:00
Loki
ec184fb2ab Screencast wlroots based compositors 2021-08-25 16:09:42 +02:00
Felipe Cavalcanti
4a750c7b16 fix: rename starting dir with working dir 2021-08-24 21:13:33 -03:00
Felipe Cavalcanti
f38bbf90bb feat: add support to specifying start_dir to processes 2021-08-24 15:32:18 -03:00
Loki
05dcff4f87 Ask Wayland what monitor outputs are available 2021-08-23 18:22:59 +02:00
Brandon Falk
b458118e34 Pulled in remote assets locally such that Sunshine can be used fully offline 2021-08-22 05:26:39 -07:00
loki
7a920da06d Removed unnecessary references to drmModeFB2 2021-08-21 20:35:36 +02:00
loki
ec84f43b80 Bump up version number of debian package 2021-08-21 20:28:14 +02:00
loki
03d572fe10 Fix generated debian package 2021-08-20 22:21:13 +02:00
loki
c41df22c88 Fix build errors when disabling KMS 2021-08-20 22:00:26 +02:00
loki
3b3b9e2bd9 Fix reinitializing KMS framebuffer 2021-08-20 21:01:33 +02:00
loki
ebf9dbe931 Merge branch 'kmsgrab' 2021-08-19 22:19:46 +02:00
loki
0b3b78891b Update debian package 2021-08-19 22:16:02 +02:00
loki
12af30b75b Update README 2021-08-19 22:09:09 +02:00
loki-47-6F-64
a9775a0686 Merge pull request #164 from Jorys-Paulin/feature/welcome
Tweaks to the welcome page
2021-08-19 21:42:20 +02:00
loki
ca9809ca7e Fix stream based on KMS freezing when switching resolutions 2021-08-19 21:40:14 +02:00
Jorys Paulin
97e14de63e Update welcome page layout 2021-08-19 12:54:40 +02:00
loki
0f4cdc2d21 Fix hanging when switching monitors 2021-08-18 21:13:55 +02:00
loki
869b6ed89d Fix VAAPI with intel iGPU's 2021-08-18 20:19:15 +02:00
loki
1876de0a68 Fix cursor disappearing on Windows 2021-08-18 11:05:14 +02:00
Jorys Paulin
019d064d8e Disable submit button when loading 2021-08-18 10:46:57 +02:00
Jorys Paulin
d6b45eb825 Added autocompletion on welcome page 2021-08-18 10:39:24 +02:00
Jorys Paulin
556a6aaf33 Fixed labels on welcome page 2021-08-18 10:33:24 +02:00
loki
fc7ec9e538 Better validation of vaapi capability 2021-08-17 21:15:38 +02:00
Elia Zammuto
1f239214a1 Troubleshooting page 2021-08-17 20:22:47 +02:00
loki-47-6F-64
9e224987f7 Merge pull request #162 from TheElixZammuto/web-ui-prettier
Fixed Formatting of HTML pages, added Prettier Support
2021-08-17 19:15:54 +02:00
Elia Zammuto
81317ce672 Fixed Formatting of HTML pages, added Prettier Support 2021-08-17 19:12:15 +02:00
loki
fce23c482c Fix incorrect cursor location 2021-08-15 22:25:34 +02:00
loki
1d2e042240 Use standard function for create egl images 2021-08-15 22:19:08 +02:00
loki
d852bb82a3 Only use graphics card connected to monitor if it's capable of h264 encoding 2021-08-15 22:15:24 +02:00
loki
fdb7754043 Attempt to render cursor when X11 is available 2021-08-15 20:38:30 +02:00
loki-47-6F-64
62c3faaacb Merge branch 'master' of https://github.com/loki-47-6F-64/sunshine 2021-08-15 00:36:40 +02:00
loki-47-6F-64
b58279beea Omit one copy for display_vrm on Windows 2021-08-15 00:36:31 +02:00
loki
898d62bad9 Filter out cursors from drm planes 2021-08-13 16:09:05 +02:00
loki
446c8ace82 Merge branch 'master' into kmsgrab 2021-08-12 22:08:06 +02:00
loki
e007ee9976 Handle monitors in different GPU's 2021-08-12 22:07:00 +02:00
loki
6721155155 Omit copy to RAM when possible with VAAPI 2021-08-12 21:11:40 +02:00
loki-47-6F-64
b80619fb0f Merge pull request #154 from guanzhangrtk/master
Update README about MSYS2 requirement on Windows
2021-08-10 15:38:53 +02:00
GuanZhang
e75e26467f Merge branch 'loki-47-6F-64:master' into master 2021-08-09 15:54:26 +09:00
GuanZhang
2187f0b198 Merge pull request #1 from guanzhangrtk/guanzhangrtk-msys2-readme
MSYS2 is needed to build under Windows
2021-08-09 15:53:47 +09:00
GuanZhang
3382a5d03c MSYS2 is needed to build under Windows 2021-08-09 15:52:49 +09:00
loki-47-6F-64
fff9145680 Merge pull request #152 from guanzhangrtk/master
Add missing requirements for clean MSYS2 environment
2021-08-09 08:32:23 +02:00
GuanZhang
13e57bb183 Merge branch 'loki-47-6F-64:master' into master 2021-08-09 11:42:02 +09:00
GuanZhang
948500ae41 Added missing sv as requested by loki-47-6F-64 2021-08-09 11:38:12 +09:00
loki-47-6F-64
17bcdd7902 Merge pull request #153 from TheElixZammuto/password-validation
Welcome: Password Validation
2021-08-08 17:43:39 +02:00
Elia Zammuto
03837a9308 Password Validation 2021-08-08 16:47:38 +02:00
Loki
b8bfc13cf9 Prevent segfault on empty string_view 2021-08-08 13:59:43 +02:00
loki
24403cdd25 Fix segfault when switching monitors with kmsgrab 2021-08-08 13:42:25 +02:00
loki
13d0106feb Don't shutdown stream if audio capture fails 2021-08-08 13:41:09 +02:00
GuanZhang
dce64fc487 Add more detailed log message when config::nvhttp.file_state doesn't exist upon first run 2021-08-08 18:20:43 +09:00
GuanZhang
0629fe7846 Add missing requirements for clean MSYS2 environment 2021-08-08 18:04:48 +09:00
loki
315ec47523 Display single monitor only with kmsgrab 2021-08-07 21:31:25 +02:00
loki
9ed2141fc8 Fix X11 screengrabbing with vaapi 2021-08-07 14:39:18 +02:00
loki
5b40c5008f Merge branch 'master' of github.com:loki-47-6F-64/sunshine 2021-08-07 12:12:27 +02:00
loki
62db9ae01a fix reading config option for log level 2021-08-07 12:12:18 +02:00
loki-47-6F-64
8c2cd2f60d Load mDNS at runtime on Windows 2021-08-07 00:05:38 +02:00
loki
0812f6f3c3 Merge branch 'master' into kmsgrab 2021-08-06 15:56:10 +02:00
loki
065e9e718a Choose between x11grab and kmsgrab at runtime 2021-08-06 15:55:38 +02:00
loki-47-6F-64
27f1dc318b Merge pull request #137 from cgutman/win32_service
Add Win32 service to run Sunshine as LocalSystem
2021-08-06 09:26:39 +02:00
Loki
ac5f439839 Skeleton of grabbing image with kms 2021-08-05 21:24:52 +02:00
Cameron Gutman
b4255e22aa Write Sunshine log output to disk and hide the window 2021-08-03 19:27:23 -05:00
Cameron Gutman
d15c1af152 Add uninstall-service.bat 2021-08-03 18:41:23 -05:00
Cameron Gutman
0140989f3a Add Win32 service to run Sunshine as LocalSystem on login 2021-08-03 18:41:23 -05:00
Loki
793e329fa5 Merge with master 2021-08-03 20:35:57 +02:00
Loki
6702802829 Load X11 libraries at runtime 2021-08-03 20:31:27 +02:00
loki
dae9a67fe2 update README 2021-08-03 15:28:32 +02:00
loki
b8e11b1272 Merge branch 'master' of github.com:loki-47-6F-64/sunshine 2021-08-03 15:24:15 +02:00
loki
f08b6abc96 Make keybindings configurable 2021-08-03 15:24:04 +02:00
loki-47-6F-64
965812bc19 Merge pull request #146 from TheElixZammuto/web-ui-welcome
Show a Welcome Page if credentials are created the first time
2021-08-03 13:07:08 +02:00
Elia Zammuto
7f643345ce welcome now puts credentials instead of generating 2021-07-30 16:06:59 +02:00
Elia Zammuto
28fecbc50c Show a Welcome Page if credentials are created the first time Sunshine is started 2021-07-29 18:55:34 +02:00
loki
6c11e9f27d Switch monitors on Windows 2021-07-29 17:27:22 +02:00
loki
2af179630a Switch monitors based on keyboard shortcuts 2021-07-29 16:48:03 +02:00
loki
c243e82047 Add shortcut for hiding and showing the cursor 2021-07-28 22:03:17 +02:00
loki
3de77b1849 Fix error 104 in Moonlight when starting Steam Bigpicture 2021-07-27 23:05:49 +02:00
loki
37d09b0bdb Merge branch 'capture_callback' 2021-07-27 18:57:11 +02:00
loki
9a5d23ebde Fix setting default SUNSHINE_DEFAULT_DIR 2021-07-27 18:55:41 +02:00
loki-47-6F-64
da3ed5ff79 Merge pull request #142 from JacekJagosz/master
Make Sunshine on Linux stateless
2021-07-27 16:01:15 +02:00
Jacek Jagosz
2e8b462fe5 Not create unnecessary variables and make fs use strings directly instead of converting them 2021-07-27 15:58:01 +02:00
loki
87b2b708f8 Update Linux build for callback code 2021-07-26 20:56:32 +02:00
loki
7ddf8bbe94 Capturing images by callback, rather than pulling 2021-07-26 18:09:07 +02:00
loki
a87782b025 Fix stuttering on Windows 2021-07-26 14:37:57 +02:00
Jacek Jagosz
c39f2b0c1f If config files don't exist in user specified directory copy files from default, not override user choice 2021-07-25 20:39:45 +02:00
Jacek Jagosz
a07ad3e479 If CONFIG and DEFAULT directories haven't been configured, make them point to ASSETS 2021-07-25 14:09:47 +02:00
Jacek Jagosz
c58f95b79e Try making sunshine on Linux stateless 2021-07-25 14:02:58 +02:00
loki
d9f7952710 Fix weird bug where Sunshine couldn't accept user input from terminal on Linux 2021-07-25 13:16:17 +02:00
Loki
ab70a056fc Add CQP for older intel iGPU's 2021-07-24 19:33:23 +01:00
loki
844ba53f54 Remove dependency on X11 for keyboard input 2021-07-24 14:51:33 +02:00
loki
63b920cd7b Merge branch 'master' into rumble 2021-07-23 20:07:03 +02:00
loki
2a5fd78789 Fix crackling audio on Linux 2021-07-23 18:43:35 +02:00
loki
bc52fe9b82 Improve fps for software encoding on Windows 2021-07-23 17:36:32 +02:00
loki
667c113d5b Merge branch 'master' into rumble 2021-07-22 21:14:53 +02:00
loki
38915859ba Don't overwrite config files with debian package 2021-07-22 21:14:36 +02:00
loki
70d0be4b9a Add rumble effect on Linux 2021-07-22 18:19:25 +02:00
loki
dad446ea41 Print request for rumble on the terminal for Linux 2021-07-21 20:24:23 +02:00
loki
d283900e43 add config options for select gamepad to emulate 2021-07-18 15:46:46 +02:00
loki
4b043e31fe Support ds4 controller on Windows 2021-07-18 15:32:26 +02:00
loki
f65882e42a Merge branch 'master' into rumble 2021-07-18 11:06:08 +02:00
loki
1fda8f6219 Support Rumble on Windows 2021-07-18 11:05:34 +02:00
loki-47-6F-64
620c629bb4 Merge pull request #135 from cgutman/partial_chain
Fix X509_V_ERR_UNABLE_TO_VERIFY_LEAF_SIGNATURE error with some embedded clients
2021-07-18 10:46:33 +02:00
Cameron Gutman
a1f63da057 Fix client auth error with some embedded clients 2021-07-17 19:34:08 -05:00
loki
5ff5942258 Fix video packet corruption after error correction 2021-07-11 16:18:49 +02:00
loki
8c803e6a34 Fix lack of audio for second client when multiple clients are connected simultaniously 2021-07-11 15:27:12 +02:00
loki
a93a640d42 Merge branch 'master' into multi-block-fec 2021-07-10 23:49:56 +02:00
loki
62abd1ee19 Merge branch 'master' of github.com:loki-47-6F-64/sunshine 2021-07-10 23:49:38 +02:00
loki
d55c6ee84c Fix incorrect scaling of absolute mouse coordinates (again) 2021-07-10 23:49:04 +02:00
loki-47-6F-64
d3419697a5 Merge pull request #125 from cgutman/yuv420_linux_nvenc
Reduce CPU usage with Linux NVENC
2021-07-10 19:10:58 +02:00
Cameron Gutman
a7171d77db Reduce CPU usage with Linux NVENC
The internal assembly routines inside libswscale perform the
RGB->YUV conversion using a fully planar output format, then
have to perform an additional YUV420->NV12 conversion step.

NVENC can directly consume YUV420 input frames, so we can
completely avoid this NV12 conversion and save quite a bit of
CPU time in the process.
2021-07-10 11:24:13 -05:00
loki
fccb8d080d Merge branch 'master' into multi-block-fec 2021-07-10 17:37:47 +02:00
loki
24acfabe07 Prevent fec encoding from exceeding DATA_SHARDS_MAX 2021-07-10 17:33:25 +02:00
loki-47-6F-64
61be0eb051 Merge pull request #120 from cgutman/nvenc_linux
Add basic NVENC support on Linux
2021-07-10 17:30:51 +02:00
loki-47-6F-64
c4b4393177 Merge pull request #123 from cgutman/longer_ping_timeout
Increase default ping timeout to 10 seconds
2021-07-10 17:30:20 +02:00
Cameron Gutman
388e4696ff Increase default ping timeout to 10 seconds
During periods of poor connectivity, the ping timeout of 2 seconds
can easily be exceeded, especially with ENet's RTO backoff active.

This causes an unnecessary disconnection when the connection would
have recovered on its own in a few seconds. Increasing the timeout
to 10 seconds should prevent spurious disconnections in most cases.
2021-07-09 23:45:59 -05:00
Cameron Gutman
43dc7cf7c0 Fix video bitstream corruption when matching replacement isn't found
NVENC doesn't always include the HEVC bitstream prefix that Sunshine
is looking to patch. When this happens replace() appends a spurious
00 00 00 01 28 NALU prefix to the end of the bitstream rather than
simply doing nothing.

This causes varying degrees of malfunctioning depending on the client,
with the worst being complete video corruption on iOS.
2021-07-09 20:04:07 -05:00
Cameron Gutman
e8feb00b33 Use a valid RTP version to fix Wireshark parsing 2021-07-09 20:01:39 -05:00
Cameron Gutman
42472bec85 Enable NVENC configuration page on Linux 2021-07-09 20:01:39 -05:00
loki-47-6F-64
769c4c8f99 Merge pull request #121 from cgutman/update_gfe_version
Update GfeVersion to be consistent with AppVersion
2021-07-09 19:05:06 +02:00
Cameron Gutman
049044f768 Update GfeVersion to be consistent with AppVersion 2021-07-09 08:47:44 -05:00
Cameron Gutman
971c784f14 Add basic NVENC support on Linux
We're not offloading scaling and YUV conversion from the CPU yet,
so the gains aren't as high as one of the fully accelerated
backends like Windows NVENC or Linux VAAPI.

Still, offloading the H.264/HEVC encoding itself is an improvement
over doing everything on the CPU.
2021-07-09 08:13:05 -05:00
loki
747ba7f23a fix incorrect pix_fmt 2021-07-09 14:21:57 +02:00
loki
4a3f3622b8 Merge branch 'master' into encrypt_control_and_audio 2021-07-09 14:19:21 +02:00
loki
7d2705424a Use extended termination error code 2021-07-09 13:26:39 +02:00
loki
b228fd371c Send back encrypted termination code 2021-07-09 12:27:38 +02:00
loki
cbd2a8269c Encrypt audio when requested by Moonlight 2021-07-08 22:44:35 +02:00
loki
7f9467d759 Add support for IDR frame request for encrypted control 2021-07-07 17:10:06 +02:00
loki
9cf7c14a26 Improve speed quitting apps 2021-07-07 17:00:12 +02:00
loki
f09aea81fe Update moonlight-common-c and Simple-Web-Server 2021-07-07 16:55:52 +02:00
loki
8249d41848 Let Moonlight know it's unauthorized, rather than just resetting the connection 2021-07-07 16:39:16 +02:00
loki-47-6F-64
d33f742241 Merge pull request #119 from cgutman/remove_superkey_workaround
Remove super key workaround
2021-07-07 07:36:54 +02:00
Cameron Gutman
6d0499b0e3 Remove super key workaround
Moonlight can now pass the super key natively
2021-07-06 23:34:11 -05:00
loki
fd7a6d070b Merge branch 'master' into encrypt_control_and_audio 2021-07-06 20:54:52 +02:00
loki
3d81b0fe7a Remove redundant code 2021-07-06 20:54:29 +02:00
loki
1d37a19468 Merge branch 'master' into encrypt_control_and_audio 2021-07-06 19:31:18 +02:00
loki-47-6F-64
cc3cf60015 Merge pull request #116 from cgutman/fix_expired_certs
Allow expired or not-yet-valid client certificates
2021-07-06 19:30:08 +02:00
loki-47-6F-64
8d4e55fcbb Merge pull request #118 from cgutman/idr_frame_delay
Fix IDR frame delays after packet loss or client request
2021-07-06 19:29:47 +02:00
loki
25aa8b41a5 Don't keep reinitializing the cipher context for gcm 2021-07-06 19:11:16 +02:00
Cameron Gutman
667878d2eb Fix IDR frame delays after packet loss or client request
This can lead to spurious poor connection warnings in Moonlight.
2021-07-05 17:55:32 -05:00
Cameron Gutman
53a9f365ce Allow expired or not-yet-valid client certificates 2021-07-05 16:42:59 -05:00
loki
a587338701 Fully encrypt control data 2021-07-05 20:58:53 +02:00
loki
a52f547726 Correctly identify size of rtsp packet 2021-07-04 21:25:10 +02:00
loki
136e9c43a5 Handle RTSP packets that are broken up in more than two pieces 2021-07-04 18:57:55 +02:00
loki
616f62fb9f Add support for periodic ping 2021-07-04 17:12:41 +02:00
loki
9d10eaeef4 Merge branch 'master' into min_fec_packets 2021-07-04 16:30:40 +02:00
loki
f23fcee382 Merge branch 'audio_fec' into min_fec_packets 2021-07-04 16:29:34 +02:00
loki
21b1a4a336 If all content has been read in RTSP request, no need for waiting for next message 2021-07-04 16:06:03 +02:00
loki
b346ac2eb0 Use tcp instead of udp for RTSP setup 2021-07-04 15:50:28 +02:00
Cameron Gutman
db5a9363ba Add support for minimum required FEC packets attribute 2021-07-04 00:14:22 -05:00
Cameron Gutman
0a637b2272 Parse additional SDP options 2021-07-03 23:38:45 -05:00
Cameron Gutman
169a53b568 Increase default FEC percentage to match GFE
Also increase the range to the maximum of 255
2021-07-03 23:37:43 -05:00
Cameron Gutman
109b48a108 Implement audio FEC support 2021-07-03 18:32:33 -05:00
Cameron Gutman
118707f37a Improve audio RTP header data to better match GFE 2021-07-03 16:05:18 -05:00
loki
e169259f6f install systemd service for debian packages 2021-07-03 18:13:05 +02:00
loki
355df9a615 Listening on arbitrary ports now compatible with Moonlight 2021-07-03 13:14:23 +02:00
loki
96cfb1f368 add missing file 2021-07-01 12:17:10 +02:00
loki
545cca792b Fix inabillity to display secure desktop even when runnig as system account 2021-06-30 22:03:21 +02:00
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
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
Elia Zammuto
03236a50e5 UI for Application Config 2021-05-16 20:06:06 +02:00
Elia Zammuto
27a1144217 Moved Common HTTPS Initialization Logic in a common file 2021-05-11 23:38:45 +02:00
Elia Zammuto
04421d84a3 Fix Indentations and Shutdown Handling 2021-05-11 22:19:29 +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
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
170 changed files with 59684 additions and 2685 deletions

12
.gitmodules vendored
View File

@@ -1,9 +1,15 @@
[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 "third-party/miniupnp"]
path = third-party/miniupnp
url = https://github.com/miniupnp/miniupnp
[submodule "third-party/nv-codec-headers"]
path = third-party/nv-codec-headers
url = https://github.com/FFmpeg/nv-codec-headers

1
.prettierrc.json Normal file
View File

@@ -0,0 +1 @@
{}

View File

@@ -2,9 +2,7 @@ cmake_minimum_required(VERSION 3.0)
project(Sunshine)
set(CMAKE_MODULE_PATH ${CMAKE_CURRENT_SOURCE_DIR})
add_subdirectory(Simple-Web-Server)
set(CMAKE_MODULE_PATH ${CMAKE_CURRENT_SOURCE_DIR}/cmake)
if(WIN32)
# Ugly hack to compile with #include <qos2.h>
@@ -13,14 +11,26 @@ if(WIN32)
PQOS_FLOWID=UINT32*
QOS_NON_ADAPTIVE_FLOW=2)
endif()
add_subdirectory(moonlight-common-c/enet)
add_subdirectory(third-party/moonlight-common-c/enet)
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)
find_package(Threads REQUIRED)
find_package(OpenSSL REQUIRED)
set(Boost_USE_STATIC_LIBS ON)
find_package(Boost COMPONENTS log filesystem REQUIRED)
list(APPEND SUNSHINE_COMPILE_OPTIONS -fPIC -Wall -Wno-missing-braces -Wno-maybe-uninitialized -Wno-sign-compare)
list(APPEND SUNSHINE_COMPILE_OPTIONS -Wall -Wno-missing-braces -Wno-maybe-uninitialized -Wno-sign-compare)
if(WIN32)
enable_language(RC)
set(CMAKE_RC_COMPILER windres)
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
@@ -35,24 +45,33 @@ if(WIN32)
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)
if(NOT DEFINED SUNSHINE_ICON_PATH)
set(SUNSHINE_ICON_PATH "${CMAKE_CURRENT_SOURCE_DIR}/sunshine.ico")
endif()
configure_file(sunshine/platform/windows/windows.rs.in windows.rc @ONLY)
set(PLATFORM_TARGET_FILES
"${CMAKE_CURRENT_BINARY_DIR}/windows.rc"
sunshine/platform/windows/publish.cpp
sunshine/platform/windows/misc.h
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
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)
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
@@ -78,62 +97,177 @@ if(WIN32)
libstdc++.a
libwinpthread.a
libssp.a
Qwave
winmm
ksuser
wsock32
ws2_32
iphlpapi
d3d11 dxgi D3DCompiler
setupapi
)
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/display.cpp
option(SUNSHINE_ENABLE_DRM "Enable KMS grab if available" ON)
option(SUNSHINE_ENABLE_X11 "Enable X11 grab if available" ON)
option(SUNSHINE_ENABLE_WAYLAND "Enable building wayland specific code" ON)
option(SUNSHINE_ENABLE_CUDA "Enable cuda specific code" ON)
if(${SUNSHINE_ENABLE_X11})
find_package(X11)
else()
set(X11_FOUND OFF)
endif()
set(CUDA_FOUND OFF)
if(${SUNSHINE_ENABLE_CUDA})
include(CheckLanguage)
check_language(CUDA)
if(CMAKE_CUDA_COMPILER)
if(NOT DEFINED CMAKE_CUDA_ARCHITECTURES)
set(CMAKE_CUDA_ARCHITECTURES 35)
endif()
set(CUDA_FOUND ON)
enable_language(CUDA)
endif()
endif()
if(${SUNSHINE_ENABLE_DRM})
find_package(LIBDRM)
find_package(LIBCAP)
else()
set(LIBDRM_FOUND OFF)
set(LIBCAP_FOUND OFF)
endif()
if(${SUNSHINE_ENABLE_WAYLAND})
find_package(Wayland)
else()
set(WAYLAND_FOUND OFF)
endif()
find_package(FFMPEG REQUIRED)
if(X11_FOUND)
add_compile_definitions(SUNSHINE_BUILD_X11)
include_directories(${X11_INCLUDE_DIR})
list(APPEND PLATFORM_TARGET_FILES sunshine/platform/linux/x11grab.cpp)
endif()
if(CUDA_FOUND)
include_directories(third-party/nvfbc)
list(APPEND PLATFORM_TARGET_FILES
sunshine/platform/linux/cuda.cu
sunshine/platform/linux/cuda.cpp
third-party/nvfbc/NvFBC.h)
add_compile_definitions(SUNSHINE_BUILD_CUDA)
endif()
if(LIBDRM_FOUND AND LIBCAP_FOUND)
add_compile_definitions(SUNSHINE_BUILD_DRM)
include_directories(${LIBDRM_INCLUDE_DIRS} ${LIBCAP_INCLUDE_DIRS})
list(APPEND PLATFORM_LIBRARIES ${LIBDRM_LIBRARIES} ${LIBCAP_LIBRARIES})
list(APPEND PLATFORM_TARGET_FILES sunshine/platform/linux/kmsgrab.cpp)
list(APPEND SUNSHINE_DEFINITIONS EGL_NO_X11=1)
elseif(LIBDRM_FOUND)
message(WARNING "Found libdrm, yet there is no libcap")
elseif(LIBDRM_FOUND)
message(WARNING "Found libcap, yet there is no libdrm")
endif()
if(WAYLAND_FOUND)
add_compile_definitions(SUNSHINE_BUILD_WAYLAND)
macro(genWayland FILENAME)
make_directory(${CMAKE_BINARY_DIR}/generated-src)
message("wayland-scanner private-code ${CMAKE_SOURCE_DIR}/third-party/wayland-protocols/${FILENAME}.xml ${CMAKE_BINARY_DIR}/generated-src/${FILENAME}.c")
message("wayland-scanner client-header ${CMAKE_SOURCE_DIR}/third-party/wayland-protocols/${FILENAME}.xml ${CMAKE_BINARY_DIR}/generated-src/${FILENAME}.h")
execute_process(
COMMAND wayland-scanner private-code ${CMAKE_SOURCE_DIR}/third-party/wayland-protocols/${FILENAME}.xml ${CMAKE_BINARY_DIR}/generated-src/${FILENAME}.c
COMMAND wayland-scanner client-header ${CMAKE_SOURCE_DIR}/third-party/wayland-protocols/${FILENAME}.xml ${CMAKE_BINARY_DIR}/generated-src/${FILENAME}.h
RESULT_VARIABLE EXIT_INT
)
if(NOT ${EXIT_INT} EQUAL 0)
message(FATAL_ERROR "wayland-scanner failed")
endif()
list(APPEND PLATFORM_TARGET_FILES
${CMAKE_BINARY_DIR}/generated-src/${FILENAME}.c
${CMAKE_BINARY_DIR}/generated-src/${FILENAME}.h
)
endmacro()
genWayland(xdg-output-unstable-v1)
genWayland(wlr-export-dmabuf-unstable-v1)
include_directories(
${WAYLAND_INCLUDE_DIRS}
${CMAKE_BINARY_DIR}/generated-src
)
list(APPEND PLATFORM_LIBRARIES ${WAYLAND_LIBRARIES})
list(APPEND PLATFORM_TARGET_FILES
sunshine/platform/linux/wlgrab.cpp
sunshine/platform/linux/wayland.cpp)
endif()
if(NOT ${X11_FOUND} AND NOT (${LIBDRM_FOUND} AND ${LIBCAP_FOUND}) AND NOT ${WAYLAND_FOUND} AND NOT ${})
message(FATAL_ERROR "Couldn't find either x11, wayland, cuda or (libdrm and libcap)")
endif()
list(APPEND PLATFORM_TARGET_FILES
sunshine/platform/linux/publish.cpp
sunshine/platform/linux/vaapi.h
sunshine/platform/linux/vaapi.cpp
sunshine/platform/linux/cuda.h
sunshine/platform/linux/graphics.h
sunshine/platform/linux/graphics.cpp
sunshine/platform/linux/misc.h
sunshine/platform/linux/misc.cpp
sunshine/platform/linux/audio.cpp
sunshine/platform/linux/input.cpp)
set(PLATFORM_LIBRARIES
Xfixes
Xtst
xcb
xcb-shm
xcb-xfixes
Xrandr
${X11_LIBRARIES}
sunshine/platform/linux/input.cpp
sunshine/platform/linux/x11grab.h
sunshine/platform/linux/wayland.h
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)
list(APPEND PLATFORM_LIBRARIES
dl
evdev
pulse
pulse-simple
)
set(PLATFORM_INCLUDE_DIRS
${X11_INCLUDE_DIR}
/usr/include/libevdev-1.0)
include_directories(
/usr/include/libevdev-1.0
third-party/nv-codec-headers/include
third-party/glad/include)
if(NOT DEFINED SUNSHINE_EXECUTABLE_PATH)
set(SUNSHINE_EXECUTABLE_PATH "${CMAKE_CURRENT_BINARY_DIR}/sunshine")
set(SUNSHINE_EXECUTABLE_PATH "sunshine")
endif()
configure_file(gen-deb.in gen-deb @ONLY)
configure_file(sunshine.service.in sunshine.service @ONLY)
endif()
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
@@ -144,6 +278,10 @@ 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
@@ -167,18 +305,23 @@ set(SUNSHINE_TARGET_FILES
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}
)
add_subdirectory(third-party/cbs)
string(TOUPPER "x${CMAKE_BUILD_TYPE}" BUILD_TYPE)
if("${BUILD_TYPE}" STREQUAL "XDEBUG")
list(APPEND SUNSHINE_COMPILE_OPTIONS -O0 -pedantic -ggdb3)
list(APPEND SUNSHINE_COMPILE_OPTIONS -O0 -ggdb3)
if(WIN32)
set_source_files_properties(sunshine/nvhttp.cpp PROPERTIES COMPILE_FLAGS -O2)
endif()
@@ -191,7 +334,20 @@ if(NOT SUNSHINE_ASSETS_DIR)
set(SUNSHINE_ASSETS_DIR "${CMAKE_CURRENT_SOURCE_DIR}/assets")
endif()
if(NOT SUNSHINE_CONFIG_DIR)
set(SUNSHINE_CONFIG_DIR "${SUNSHINE_ASSETS_DIR}")
endif()
if(NOT SUNSHINE_DEFAULT_DIR)
set(SUNSHINE_DEFAULT_DIR "${SUNSHINE_ASSETS_DIR}")
endif()
list(APPEND CBS_EXTERNAL_LIBRARIES
cbs)
list(APPEND SUNSHINE_EXTERNAL_LIBRARIES
libminiupnpc-static
${CBS_EXTERNAL_LIBRARIES}
${CMAKE_THREAD_LIBS_INIT}
stdc++fs
enet
@@ -201,10 +357,25 @@ list(APPEND SUNSHINE_EXTERNAL_LIBRARIES
${OPENSSL_LIBRARIES}
${PLATFORM_LIBRARIES})
if (NOT WIN32)
list(APPEND SUNSHINE_EXTERNAL_LIBRARIES Boost::log)
endif()
list(APPEND SUNSHINE_DEFINITIONS SUNSHINE_ASSETS_DIR="${SUNSHINE_ASSETS_DIR}")
list(APPEND SUNSHINE_DEFINITIONS SUNSHINE_CONFIG_DIR="${SUNSHINE_CONFIG_DIR}")
list(APPEND SUNSHINE_DEFINITIONS SUNSHINE_DEFAULT_DIR="${SUNSHINE_DEFAULT_DIR}")
add_executable(sunshine ${SUNSHINE_TARGET_FILES})
target_link_libraries(sunshine ${SUNSHINE_EXTERNAL_LIBRARIES})
target_compile_definitions(sunshine PUBLIC ${SUNSHINE_DEFINITIONS})
set_target_properties(sunshine PROPERTIES CXX_STANDARD 17)
target_compile_options(sunshine PRIVATE ${SUNSHINE_COMPILE_OPTIONS})
if(NOT DEFINED CMAKE_CUDA_STANDARD)
set(CMAKE_CUDA_STANDARD 17)
set(CMAKE_CUDA_STANDARD_REQUIRED ON)
endif()
foreach(flag IN LISTS SUNSHINE_COMPILE_OPTIONS)
list(APPEND SUNSHINE_COMPILE_OPTIONS_CUDA "$<$<COMPILE_LANGUAGE:CUDA>:--compiler-options=${flag}>")
endforeach()
target_compile_options(sunshine PRIVATE $<$<COMPILE_LANGUAGE:CXX>:${SUNSHINE_COMPILE_OPTIONS}>;$<$<COMPILE_LANGUAGE:CUDA>:${SUNSHINE_COMPILE_OPTIONS_CUDA};-std=c++17>)

View File

@@ -1,3 +1,4 @@
![Sunshine icon](gamepad.png "Sunshine")
# Introduction
Sunshine is a Gamestream host for Moonlight
@@ -13,29 +14,62 @@ Sunshine is a Gamestream host for Moonlight
## Linux
If you do not wish to clutter your PC with development files, yet you want the very latest version...
You can use these [build scripts](scripts/README.md)
They make use of docker to handle building Sunshine automatically
### Requirements:
Ubuntu 20.04:
Install the following
Install the following:
#### Common
```
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
sudo apt install cmake gcc-10 g++-10 libssl-dev libavdevice-dev libboost-thread-dev libboost-filesystem-dev libboost-log-dev libpulse-dev libopus-dev libevdev-dev
```
#### X11
```
sudo apt install libxtst-dev libx11-dev libxrandr-dev libxfixes-dev libxcb1-dev libxcb-shm0-dev libxcb-xfixes0-dev
```
#### KMS
This requires additional [setup](README.md#Setup).
```
sudo apt install libdrm-dev libcap-dev
```
#### Wayland
This is for wlroots based compositores, such as Sway
```
sudo apt install libwayland-dev
```
#### Cuda + NvFBC
This requires proprietary software
On Ubuntu 20.04, the cuda compiler will fail since it's version is too old, it's recommended you compile the sources with the [build scripts](scripts/README.md)
```
sudo apt install nvidia-cuda-dev nvidia-cuda-toolkit
```
#### Warning:
You might require ffmpeg version >= 4.3. Check the troubleshooting section for more information.
### Compilation:
- `git clone https://github.com/loki-47-6F-64/sunshine.git --recurse-submodules`
- `cd sunshine && mkdir build && cd build`
- `cmake ..`
- `cmake -DCMAKE_C_COMPILER=gcc-10 -DCMAKE_CXX_COMPILER=g++-10 ..`
- `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"`
`KERNEL=="uinput", GROUP="input", MODE="0660"`
- Save the file and exit
1. `CTRL+X` to start exit
2. `Y` to save modifications
@@ -52,26 +86,41 @@ sunshine needs access to uinput to create mouse and gamepad events:
- `assets/apps.json` is an [example](README.md#application-list) of a list of applications that are started just before running a stream
#### Additional Setup for KMS:
Please note that `cap_sys_admin` may as well be root, except you don't need to be root to run it.
It's necessary to allow Sunshine to use KMS
- `sudo setcap cap_sys_admin+p sunshine`
### 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:"
1. Check whether you're using Pulseaudio or Pipewire
- Pulseaudio: Use `pacmd list-sources | grep "name:"`
- Pipewire: Use `pactl info | grep Source`. In some causes you'd need to use the `sink` device. Try `pactl info | grep Sink`, if _Source_ doesn't work.
2. Copy the name to the configuration option "audio_sink"
3. restart sunshine
3. Restart sunshine
- If you get "Error: Failed to create client: Daemon not running", ensure that your avahi-daemon is running:
- `systemctl status avahi-daemon`
- If you use hardware acceleration on Linux using an Intel or an AMD GPU (with VAAPI), you will get tons of [graphical issues](https://github.com/loki-47-6F-64/sunshine/issues/228) if your ffmpeg version is < 4.3. If it is not available in your distribution's repositories, consider using a newer version of your distribution.
- Ubuntu started to ship ffmpeg 4.3 starting with groovy (20.10). If you're using an older version, you could use [this PPA](https://launchpad.net/%7Esavoury1/+archive/ubuntu/ffmpeg4) instead of upgrading. **Using PPAs is dangerous and may break your system. Use it at your own risk.**
## 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 yasm nasm diffutils make
First you need to install [MSYS2](https://www.msys2.org), then startup "MSYS2 MinGW 64-bit" and install the following packages using `pacman -S`:
mingw-w64-x86_64-binutils 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 cmake make gcc
### Compilation:
- `git clone https://github.com/loki-47-6F-64/sunshine.git --recursive`
- `cd sunshine && mkdir build && cd build`
- `cmake -G"Unix Makefiles" ..`
- `make`
- `mingw32-make`
### Setup:
- **OPTIONAL** Gamepad support: Download and run 'ViGEmBus_Setup_1.16.116.exe' from [https://github.com/ViGEm/ViGEmBus/releases]
@@ -82,32 +131,33 @@ sunshine needs access to uinput to create mouse and gamepad events:
## 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, either:
- Type in the URL bar of your browser: `xxx.xxx.xxx.xxx:47989/pin/####`
- `wget xxx.xxx.xxx.xxx:47989/pin/####`
- The x's are the IP of your instance, `####` is the pin
- 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"
- You should compile the next page with a new username and a password, needed to login into the next step
- Press "Save" and log in using the credentials given above
- 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 :)
## Shortcuts:
## 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.
All shortcuts start with CTRL + ALT + SHIFT, just like Moonlight
- CTRL + ALT + SHIFT + N --> Hide/Unhide the cursor (This may be usefull for Remote Desktop Mode for Moonlight)
- CTRL + ALT + SHIFT + F1/F13 --> Switch to different monitor for Streaming
## 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)
- [Twitter emoji](https://github.com/twitter/twemoji/blob/master/LICENSE-GRAPHICS) (Sunshine's icon is made of twemoji)
## 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)

Submodule ViGEmClient deleted from 52682b59c4

View File

@@ -1,38 +1,44 @@
image:
- Ubuntu2004
- Visual Studio 2019
services:
- docker
environment:
matrix:
- BUILD_TYPE: Debug
- BUILD_TYPE: Release
- APPVEYOR_BUILD_WORKER_IMAGE: Ubuntu2004
DOCKERFILE: Dockerfile-2004
- APPVEYOR_BUILD_WORKER_IMAGE: Ubuntu2004
DOCKERFILE: Dockerfile-2104
- APPVEYOR_BUILD_WORKER_IMAGE: Ubuntu2004
DOCKERFILE: Dockerfile-debian
- APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2019
BUILD_TYPE: Release
install:
- 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"
- cmd: C:\msys64\usr\bin\bash -lc "pacman --needed --noconfirm -S mingw-w64-x86_64-binutils 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:
- git submodule update --init --recursive
- mkdir build
- cd build
- cmd: git submodule update --init --recursive
- cmd: mkdir build
- cmd: cd build
- sh: cd scripts
- sh: ./build-container.sh -f $DOCKERFILE
build_script:
- cmd: set OLDPATH=%PATH%
- cmd: set PATH=C:\msys64\mingw64\bin
- 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%
- sh: ./build-sunshine.sh -pu
after_build:
- sh: ./gen-deb
- cmd: Del ..\assets\apps_linux.json
- cmd: 7z a Sunshine-Windows.zip ..\assets
- cmd: 7z a Sunshine-Windows.zip sunshine.exe
- cmd: 7z a Sunshine-Windows.zip tools\dxgi-info.exe
- cmd: 7z a Sunshine-Windows.zip tools\audio-info.exe
- cmd: 7z a Sunshine-Windows.zip tools\sunshinesvc.exe
- cmd: 7z a Sunshine-Windows.zip ..\tools\install-service.bat
- cmd: 7z a Sunshine-Windows.zip ..\tools\uninstall-service.bat
- cmd: appveyor PushArtifact Sunshine-Windows.zip
- sh: appveyor PushArtifact package-deb/sunshine.deb
- sh: appveyor PushArtifact sunshine.service
- sh: appveyor PushArtifact sunshine-build/sunshine.deb

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,233 +1,286 @@
# If no external IP address is given, the local IP address is used
# external_ip = 123.456.789.12
# The private key must be 2048 bits
# pkey = /dir/pkey.pem
# The certificate must be signed with a 2048 bit key
# cert = /dir/cert.pem
# The name displayed by Moonlight
# If not specified, the PC's hostname is used
# sunshine_name = Sunshine
# The minimum log level printed to standard out
#
# none -> no logs are printed to standard out
#
# verbose = [0]
# debug = [1]
# info = [2]
# warning = [3]
# error = [4]
# fatal = [5]
# none = [6]
#
# min_log_level = info
# The origin of the remote endpoint address that is not denied for HTTP method /pin
# Could be any of the following values:
# pc|lan|wan
# pc: Only localhost may access /pin
# lan: Only those in LAN may access /pin
# wan: Anyone may access /pin
#
# origin_pin_allowed = lan
# The file where current state of Sunshine is stored
# file_state = 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
# The file where configuration for the different applications that Sunshine can run during a stream
# file_apps = apps.json
# How much error correcting packets must be send for every video
# This is just some random number, don't know the optimal value
# The higher fec_percentage, the lower space for the actual data to send per frame there is
#
# 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
# If, after the timeout, the back button is still pressed down, Home/Guide button press is emulated.
# 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-sinks | grep "name:"
# 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}
#
# 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:
# The appropriate values can be found using the following command:
# tools\dxgi-info.exe
# adapter_name = Radeon RX 580 Series
# output_name = \\.\DISPLAY1
# !! Linux only !!
# Set the display number to stream. I have no idea how they are numbered. They start from 0, usually.
# output_name = 0
###############################################
# FFmpeg software encoding parameters
# Honestly, I have no idea what the optimal values would be.
# Play around with this :)
# 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
# Higher value means more compression, but less quality
# If crf == 0, then use QP directly instead
# crf = 0
# Quantitization Parameter
# Higher value means more compression, but less quality
# If crf != 0, then this parameter is ignored
# qp = 28
# Minimum number of threads used by ffmpeg to encode the video.
# 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 = 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 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
# 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
##############################################
# 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 = 01
#
# See: sunshine --help for all options under the header: flags
# 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
# The certificate must be signed with a 2048 bit key
# cert = /dir/cert.pem
# The name displayed by Moonlight
# If not specified, the PC's hostname is used
# sunshine_name = Sunshine
# The minimum log level printed to standard out
#
# none -> no logs are printed to standard out
#
# verbose = [0]
# debug = [1]
# info = [2]
# warning = [3]
# error = [4]
# fatal = [5]
# none = [6]
#
# min_log_level = info
# The origin of the remote endpoint address that is not denied for HTTP method /pin
# Could be any of the following values:
# pc|lan|wan
# pc: Only localhost may access /pin
# lan: Only those in LAN may access /pin
# wan: Anyone may access /pin
#
# 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,
# ]
# Sometimes it may be usefull to map keybindings.
# Wayland won't allow clients to capture the Win Key for example
#
# See https://docs.microsoft.com/en-us/windows/win32/inputdev/virtual-key-codes
#
# Note:
# keybindings needs to have a multiple of two elements
# keybindings = [
# 0x10, 0xA0,
# 0x11, 0xA2,
# 0x12, 0xA4,
# 0x4A, 0x4B
# ]
# How long to wait in milliseconds for data from moonlight before shutting down the stream
# ping_timeout = 10000
# The file where configuration for the different applications that Sunshine can run during a stream
# file_apps = apps.json
# Percentage of error correcting packets per data packet in each video frame
# Higher values can correct for more network packet loss, but at the cost of increasing bandwidth usage
# The default value of 20 is what GeForce Experience uses
#
# The value must be greater than 0 and lower than or equal to 255
# fec_percentage = 20
# 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
# If, after the timeout, the back button is still pressed down, Home/Guide button press is emulated.
# If back_button_timeout < 0, then the Home/Guide button will not be emulated
# back_button_timeout = 2000
# !! Windows only !!
# Gamepads supported by Sunshine
# Possible values:
# x360 -- xbox 360 controller
# ds4 -- dualshock controller (PS4)
# gamepad = x360
# 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-sinks | grep "name:" if running vanilla pulseaudio
# pPipewire: Use `pactl info | grep Source`. In some causes you'd need to use the `sink` device. Try `pactl info | grep Sink`, if _Source_ doesn't work
# 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}
#
# 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:
# The appropriate values can be found using the following command:
# tools\dxgi-info.exe
# 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
# Honestly, I have no idea what the optimal values would be.
# Play around with this :)
# Quantitization Parameter
# Some devices don't support Constant Bit Rate. For those devices, QP is used instead
# Higher value means more compression, but less quality
# qp = 28
# Minimum number of threads used by ffmpeg to encode the video.
# 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 = 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 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
# 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

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

@@ -0,0 +1,260 @@
<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>
<!--working dir-->
<div class="mb-3">
<label for="appWorkingDir" class="form-label">Working Directory</label>
<input
type="text"
class="form-control monospace"
id="appWorkingDir"
aria-describedby="appWorkingDirHelp"
v-model="editForm['working-dir']"
/>
<div id="appWorkingDirHelp" class="form-text">
The working directory that should be passed to the process.
For example, some applications use the working directory to search for configuration files.
If not set, Sunshine will default to the parent directory of the command
</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>

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

@@ -0,0 +1,886 @@
<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>
<!--Gamepads-->
<div class="mb-3" v-if="platform === 'windows'">
<label for="gamepad" class="form-label">Gamepads</label>
<select id="gamepad" class="form-select" v-model="config.gamepad">
<option value="ds4">DS4 (PS4)</option>
<option value="x360">X360 (Xbox 360)</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="10000"
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>
<!-- Mapping Key AltRight to Key Windows -->
<div class="mb-3">
<label for="mapkey" class="form-label"
>Map Right Alt key to Windows key</label
>
<select
id="mapkey"
class="form-select"
v-model="config.key_rightalt_to_key_win"
>
<option value="disabled">Disabled</option>
<option value="enabled">Enabled</option>
</select>
</div>
<div class="form-text">
It may be possible that you cannot send the Windows Key from Moonlight
directly.<br />
In those cases it may be usefull to make Sunshine think the Right Alt
key is the Windows key
</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>
<!-- 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 />
Some devices may not support Constant Bit Rate.<br />
For those devices, QP is used instead.<br />
Higher value means more compression, but less quality<br />
</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="20"
v-model="config.fec_percentage"
/>
<div class="form-text">
Percentage of error correcting packets per data packet in each video
frame.<br />
Higher values can correct for more network packet loss, but at the
cost of increasing bandwidth usage.<br />
The default value of 20 is what GeForce Experience uses.
</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 !== "amd";
});
}
delete this.config.status;
delete this.config.platform;
//Populate default values if not present in config
this.config.key_rightalt_to_key_win =
this.config.key_rightalt_to_key_win || "disabled";
this.config.gamepad = this.config.gamepad || "x360";
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>

View File

@@ -0,0 +1,14 @@
<!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="/third_party/bootstrap.min.css" rel="stylesheet" />
<script src="/third_party/bootstrap.bundle.min.js"></script>
<script src="/third_party/vue.js"></script>
</head>
<body></body>
</html>

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

@@ -0,0 +1,56 @@
<!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="/third_party/bootstrap.min.css" rel="stylesheet" />
<script src="/third_party/bootstrap.bundle.min.js"></script>
<script src="/third_party/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>
<li class="nav-item">
<a class="nav-link" href="/troubleshooting">Troubleshooting</a>
</li>
</ul>
</div>
</div>
</nav>
</body>
</html>

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

@@ -0,0 +1,11 @@
<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>

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

@@ -0,0 +1,133 @@
<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>

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

@@ -0,0 +1,42 @@
<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>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

11965
assets/web/third_party/vue.js vendored Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,92 @@
<div id="app" class="container">
<h1 class="my-4">Troubleshooting</h1>
<!--Force Close App-->
<div class="card p-2 my-4">
<div class="card-body">
<h2>Force Close</h2>
<br />
<p>
If Moonlight complains about an app currently running, force closing the
app should fix the issue
</p>
<div class="alert alert-success" v-if="closeAppStatus === true">
Application Closed Successfuly!
</div>
<div class="alert alert-danger" v-if="closeAppStatus === false">
Error while closing Appplication
</div>
<div>
<button
class="btn btn-warning"
:disabled="closeAppPressed"
@click="closeApp"
>
Force Close
</button>
</div>
</div>
</div>
<!--Unpair all Clients-->
<div class="card p-2 my-4">
<div class="card-body">
<h2>Unpair All Clients</h2>
<br />
<p>Remove all your paired devices</p>
<div class="alert alert-success" v-if="unpairAllStatus === true">
Unpair Successful!
</div>
<div class="alert alert-danger" v-if="unpairAllStatus === false">
Error while unpairing
</div>
<div>
<button
class="btn btn-danger"
:disabled="unpairAllPressed"
@click="unpairAll"
>
Unpair All
</button>
</div>
</div>
</div>
</div>
<script>
new Vue({
el: "#app",
data() {
return {
closeAppPressed: false,
closeAppStatus: null,
unpairAllPressed: false,
unpairAllStatus: null,
};
},
methods: {
closeApp() {
this.closeAppPressed = true;
fetch("/api/apps/close", { method: "POST" })
.then((r) => r.json())
.then((r) => {
this.closeAppPressed = false;
this.closeAppStatus = r.status.toString() === "true";
setTimeout(() => {
this.closeAppStatus = null;
}, 5000);
});
},
unpairAll() {
this.unpairAllPressed = true;
fetch("/api/clients/unpair", { method: "POST" })
.then((r) => r.json())
.then((r) => {
this.unpairAllPressed = false;
this.unpairAllStatus = r.status.toString() === "true";
setTimeout(() => {
this.unpairAllStatus = null;
}, 5000);
});
},
},
});
</script>

105
assets/web/welcome.html Normal file
View File

@@ -0,0 +1,105 @@
<main role="main" id="app" style="max-width: 600px; margin: 0 auto">
<header>
<h1 class="mb-0">Welcome to Sunshine!</h1>
<p class="mb-0 align-self-start">
Before Getting Started, write down below these credentials
</p>
</header>
<div class="alert alert-warning">
These Credentials down below are needed to access the rest of the
application.<br />
Keep them safe, since <b>you will never see them again!</b>
</div>
<form @submit.prevent="save">
<div class="mb-2">
<label for="usernameInput" class="form-label">Username:</label>
<input
type="text"
class="form-control"
id="usernameInput"
autocomplete="username"
v-model="passwordData.newUsername"
/>
</div>
<div class="mb-2">
<label for="passwordInput" class="form-label">Password:</label>
<input
type="password"
class="form-control"
id="passwordInput"
autocomplete="new-password"
v-model="passwordData.newPassword"
required
/>
</div>
<div class="mb-2">
<label for="confirmPasswordInput" class="form-label"
>Password (confirm):</label
>
<input
type="password"
class="form-control"
id="confirmPasswordInput"
autocomplete="new-password"
v-model="passwordData.confirmNewPassword"
required
/>
</div>
<button
type="submit"
class="btn btn-primary w-100 mb-2"
v-bind:disabled="loading"
>
Login
</button>
<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>
</form>
</main>
<script>
new Vue({
el: "#app",
data() {
return {
error: null,
success: false,
loading: false,
passwordData: {
newUsername: "sunshine",
newPassword: "",
confirmNewPassword: "",
},
};
},
methods: {
save() {
this.error = null;
this.loading = true;
fetch("/api/password", {
method: "POST",
body: JSON.stringify(this.passwordData),
}).then((r) => {
this.loading = false;
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>

21
cmake/FindLIBCAP.cmake Normal file
View File

@@ -0,0 +1,21 @@
# - Try to find Libcap
# Once done this will define
#
# LIBCAP_FOUND - system has Libcap
# LIBCAP_INCLUDE_DIRS - the Libcap include directory
# LIBCAP_LIBRARIES - the libraries needed to use Libcap
# LIBCAP_DEFINITIONS - Compiler switches required for using Libcap
# Use pkg-config to get the directories and then use these values
# in the find_path() and find_library() calls
find_package(PkgConfig)
pkg_check_modules(PC_LIBCAP libcap)
set(LIBCAP_DEFINITIONS ${PC_LIBCAP_CFLAGS})
find_path(LIBCAP_INCLUDE_DIRS sys/capability.h PATHS ${PC_LIBCAP_INCLUDEDIR} ${PC_LIBCAP_INCLUDE_DIRS})
find_library(LIBCAP_LIBRARIES NAMES libcap.so PATHS ${PC_LIBCAP_LIBDIR} ${PC_LIBCAP_LIBRARY_DIRS})
mark_as_advanced(LIBCAP_INCLUDE_DIRS LIBCAP_LIBRARIES)
include(FindPackageHandleStandardArgs)
find_package_handle_standard_args(LIBCAP REQUIRED_VARS LIBCAP_LIBRARIES LIBCAP_INCLUDE_DIRS)

21
cmake/FindLIBDRM.cmake Normal file
View File

@@ -0,0 +1,21 @@
# - Try to find Libdrm
# Once done this will define
#
# LIBDRM_FOUND - system has Libdrm
# LIBDRM_INCLUDE_DIRS - the Libdrm include directory
# LIBDRM_LIBRARIES - the libraries needed to use Libdrm
# LIBDRM_DEFINITIONS - Compiler switches required for using Libdrm
# Use pkg-config to get the directories and then use these values
# in the find_path() and find_library() calls
find_package(PkgConfig)
pkg_check_modules(PC_LIBDRM libdrm)
set(LIBDRM_DEFINITIONS ${PC_LIBDRM_CFLAGS})
find_path(LIBDRM_INCLUDE_DIRS drm.h PATHS ${PC_LIBDRM_INCLUDEDIR} ${PC_LIBDRM_INCLUDE_DIRS} PATH_SUFFIXES libdrm)
find_library(LIBDRM_LIBRARIES NAMES libdrm.so PATHS ${PC_LIBDRM_LIBDIR} ${PC_LIBDRM_LIBRARY_DIRS})
mark_as_advanced(LIBDRM_INCLUDE_DIRS LIBDRM_LIBRARIES)
include(FindPackageHandleStandardArgs)
find_package_handle_standard_args(LIBDRM REQUIRED_VARS LIBDRM_LIBRARIES LIBDRM_INCLUDE_DIRS)

78
cmake/FindWayland.cmake Normal file
View File

@@ -0,0 +1,78 @@
# Try to find Wayland on a Unix system
#
# This will define:
#
# WAYLAND_FOUND - True if Wayland is found
# WAYLAND_LIBRARIES - Link these to use Wayland
# WAYLAND_INCLUDE_DIRS - Include directory for Wayland
# WAYLAND_DEFINITIONS - Compiler flags for using Wayland
#
# In addition the following more fine grained variables will be defined:
#
# Wayland_Client_FOUND WAYLAND_CLIENT_INCLUDE_DIRS WAYLAND_CLIENT_LIBRARIES
# Wayland_Server_FOUND WAYLAND_SERVER_INCLUDE_DIRS WAYLAND_SERVER_LIBRARIES
# Wayland_EGL_FOUND WAYLAND_EGL_INCLUDE_DIRS WAYLAND_EGL_LIBRARIES
# Wayland_Cursor_FOUND WAYLAND_CURSOR_INCLUDE_DIRS WAYLAND_CURSOR_LIBRARIES
#
# Copyright (c) 2013 Martin Gräßlin <mgraesslin@kde.org>
# 2020 Georges Basile Stavracas Neto <georges.stavracas@gmail.com>
#
# Redistribution and use is allowed according to the terms of the BSD license.
# For details see the accompanying COPYING-CMAKE-SCRIPTS file.
IF (NOT WIN32)
# Use pkg-config to get the directories and then use these values
# in the find_path() and find_library() calls
find_package(PkgConfig)
PKG_CHECK_MODULES(PKG_WAYLAND QUIET wayland-client wayland-server wayland-egl wayland-cursor)
set(WAYLAND_DEFINITIONS ${PKG_WAYLAND_CFLAGS})
find_path(WAYLAND_CLIENT_INCLUDE_DIRS NAMES wayland-client.h HINTS ${PKG_WAYLAND_INCLUDE_DIRS})
find_library(WAYLAND_CLIENT_LIBRARIES NAMES wayland-client HINTS ${PKG_WAYLAND_LIBRARY_DIRS})
if(WAYLAND_CLIENT_INCLUDE_DIRS AND WAYLAND_CLIENT_LIBRARIES)
set(Wayland_Client_FOUND TRUE)
else()
set(Wayland_Client_FOUND FALSE)
endif()
mark_as_advanced(WAYLAND_CLIENT_INCLUDE_DIRS WAYLAND_CLIENT_LIBRARIES)
find_path(WAYLAND_CURSOR_INCLUDE_DIRS NAMES wayland-cursor.h HINTS ${PKG_WAYLAND_INCLUDE_DIRS})
find_library(WAYLAND_CURSOR_LIBRARIES NAMES wayland-cursor HINTS ${PKG_WAYLAND_LIBRARY_DIRS})
if(WAYLAND_CURSOR_INCLUDE_DIRS AND WAYLAND_CURSOR_LIBRARIES)
set(Wayland_Cursor_FOUND TRUE)
else()
set(Wayland_Cursor_FOUND FALSE)
endif()
mark_as_advanced(WAYLAND_CURSOR_INCLUDE_DIRS WAYLAND_CURSOR_LIBRARIES)
find_path(WAYLAND_EGL_INCLUDE_DIRS NAMES wayland-egl.h HINTS ${PKG_WAYLAND_INCLUDE_DIRS})
find_library(WAYLAND_EGL_LIBRARIES NAMES wayland-egl HINTS ${PKG_WAYLAND_LIBRARY_DIRS})
if(WAYLAND_EGL_INCLUDE_DIRS AND WAYLAND_EGL_LIBRARIES)
set(Wayland_EGL_FOUND TRUE)
else()
set(Wayland_EGL_FOUND FALSE)
endif()
mark_as_advanced(WAYLAND_EGL_INCLUDE_DIRS WAYLAND_EGL_LIBRARIES)
find_path(WAYLAND_SERVER_INCLUDE_DIRS NAMES wayland-server.h HINTS ${PKG_WAYLAND_INCLUDE_DIRS})
find_library(WAYLAND_SERVER_LIBRARIES NAMES wayland-server HINTS ${PKG_WAYLAND_LIBRARY_DIRS})
if(WAYLAND_SERVER_INCLUDE_DIRS AND WAYLAND_SERVER_LIBRARIES)
set(Wayland_Server_FOUND TRUE)
else()
set(Wayland_Server_FOUND FALSE)
endif()
mark_as_advanced(WAYLAND_SERVER_INCLUDE_DIRS WAYLAND_SERVER_LIBRARIES)
set(WAYLAND_INCLUDE_DIRS ${WAYLAND_CLIENT_INCLUDE_DIRS} ${WAYLAND_SERVER_INCLUDE_DIRS} ${WAYLAND_EGL_INCLUDE_DIRS} ${WAYLAND_CURSOR_INCLUDE_DIRS})
set(WAYLAND_LIBRARIES ${WAYLAND_CLIENT_LIBRARIES} ${WAYLAND_SERVER_LIBRARIES} ${WAYLAND_EGL_LIBRARIES} ${WAYLAND_CURSOR_LIBRARIES})
mark_as_advanced(WAYLAND_INCLUDE_DIRS WAYLAND_LIBRARIES)
list(REMOVE_DUPLICATES WAYLAND_INCLUDE_DIRS)
include(FindPackageHandleStandardArgs)
find_package_handle_standard_args(Wayland REQUIRED_VARS WAYLAND_LIBRARIES WAYLAND_INCLUDE_DIRS HANDLE_COMPONENTS)
ENDIF ()

BIN
gamepad.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

View File

@@ -10,15 +10,17 @@ if [ -d package-deb ]; then
rm -rf package-deb
fi
export DEBIAN=package-deb/sunshine/DEBIAN
export RULES=package-deb/sunshine/etc/udev/rules.d
export BIN=package-deb/sunshine/usr/bin
export ASSETS=package-deb/sunshine/etc/sunshine
export DEBIAN=@CMAKE_CURRENT_BINARY_DIR@/package-deb/sunshine/DEBIAN
export RULES=@CMAKE_CURRENT_BINARY_DIR@/package-deb/sunshine/etc/udev/rules.d
export BIN=@CMAKE_CURRENT_BINARY_DIR@/package-deb/sunshine/usr/bin
export SERVICE=@CMAKE_CURRENT_BINARY_DIR@/package-deb/sunshine/usr/lib/systemd/user
export ASSETS=@CMAKE_CURRENT_BINARY_DIR@/package-deb/sunshine/etc/sunshine
mkdir -p $DEBIAN
mkdir -p $RULES
mkdir -p $BIN
mkdir -p $ASSETS
mkdir -p $ASSETS/shaders
mkdir -p $SERVICE
if [ ! -f sunshine ]; then
echo "Error: Can't find sunshine"
@@ -35,11 +37,22 @@ Package: sunshine
Architecture: amd64
Maintainer: @loki
Priority: optional
Version: 0.4.1
Depends: libssl1.1, libavdevice58, libboost-thread1.71.0, libboost-filesystem1.71.0, libboost-log1.71.0, libpulse0, libopus0, libxcb-shm0, libxcb-xfixes0
Version: 0.11.1
Depends: libssl1.1, libavdevice58, libboost-thread1.67.0 | libboost-thread1.71.0 | libboost-thread1.74.0, libboost-filesystem1.67.0 | libboost-filesystem1.71.0 | libboost-filesystem1.74.0, libboost-log1.67.0 | libboost-log1.71.0 | libboost-log1.74.0, libpulse0, libopus0, libxcb-shm0, libxcb-xfixes0, libxtst6, libevdev2, libdrm2, libcap2
Description: Gamestream host for Moonlight
EOF
cat << 'EOF' > $DEBIAN/preinst
#Store backup for old config files to prevent it from being overwritten
if [ -f /etc/sunshine/sunshine.conf ]; then
cp /etc/sunshine/sunshine.conf /etc/sunshine/sunshine.conf.old
fi
if [ -f /etc/sunshine/apps_linux.json ]; then
cp /etc/sunshine/apps_linux.json /etc/sunshine/apps_linux.json.old
fi
EOF
cat << 'EOF' > $DEBIAN/postinst
#!/bin/sh
@@ -54,6 +67,34 @@ if [ -f /etc/group ]; then
else
echo "Warning: /etc/group not found"
fi
if [ -f /etc/sunshine/sunshine.conf.old ]; then
echo "Restoring old sunshine.conf"
mv /etc/sunshine/sunshine.conf.old /etc/sunshine/sunshine.conf
fi
if [ -f /etc/sunshine/apps_linux.json.old ]; then
echo "Restoring old apps_linux.json"
mv /etc/sunshine/apps_linux.json.old /etc/sunshine/apps_linux.json
fi
# 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
# Ensure Sunshine can grab images from KMS
path_to_setcap=$(which setcap)
if [ -x "$path_to_setcap" ] ; then
echo "$path_to_setcap cap_sys_admin+p /usr/bin/sunshine"
$path_to_setcap cap_sys_admin+p /usr/bin/sunshine
fi
EOF
cat << 'EOF' > $RULES/85-sunshine-rules.rules
@@ -63,10 +104,16 @@ EOF
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 @CMAKE_CURRENT_BINARY_DIR@/sunshine.service $SERVICE/sunshine.service
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 $DEBIAN/preinst
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

18
scripts/Dockerfile-2004 Normal file
View File

@@ -0,0 +1,18 @@
FROM ubuntu:20.04 AS sunshine-2004
ARG DEBIAN_FRONTEND=noninteractive
ARG TZ="Europe/London"
RUN apt-get update -y && \
apt-get install -y \
git wget gcc-10 g++-10 build-essential 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 libdrm-dev libcap-dev libwayland-dev
RUN cp /usr/bin/gcc-10 /usr/bin/gcc && cp /usr/bin/g++-10 /usr/bin/gcc-10
RUN wget https://developer.download.nvidia.com/compute/cuda/11.4.2/local_installers/cuda_11.4.2_470.57.02_linux.run --progress=bar:force:noscroll -q --show-progress -O /root/cuda.run && chmod a+x /root/cuda.run
RUN /root/cuda.run --silent --toolkit --toolkitpath=/usr --no-opengl-libs --no-man-page --no-drm && rm /root/cuda.run
COPY build-private.sh /root/build.sh
ENTRYPOINT ["/root/build.sh"]

13
scripts/Dockerfile-2104 Normal file
View File

@@ -0,0 +1,13 @@
FROM ubuntu:21.04 AS sunshine-2104
ARG DEBIAN_FRONTEND=noninteractive
ARG TZ="Europe/London"
RUN apt-get update -y && \
apt-get install -y \
git build-essential 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 libdrm-dev libcap-dev libwayland-dev nvidia-cuda-dev nvidia-cuda-toolkit
COPY build-private.sh /root/build.sh
ENTRYPOINT ["/root/build.sh"]

14
scripts/Dockerfile-debian Normal file
View File

@@ -0,0 +1,14 @@
FROM debian:bullseye AS sunshine-debian
ARG DEBIAN_FRONTEND=noninteractive
ARG TZ="Europe/London"
RUN echo deb http://deb.debian.org/debian/ bullseye main contrib non-free | tee /etc/apt/sources.list.d/non-free.list
RUN apt-get update -y && \
apt-get install -y \
git build-essential 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 libdrm-dev libcap-dev libwayland-dev nvidia-cuda-dev nvidia-cuda-toolkit
COPY build-private.sh /root/build.sh
ENTRYPOINT ["/root/build.sh"]

53
scripts/README.md Normal file
View File

@@ -0,0 +1,53 @@
# 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)
You may wish to simply build sunshine from source, without bloating your OS with development files.
These scripts will create a docker images that have the necessary packages. As a result, removing the development files after you're done is a single command away.
These scripts use docker under the hood, as such, they can only be used to compile the Linux version
#### Requirements
```
sudo apt install docker
```
#### instructions
You'll require one of the following Dockerfiles:
* Dockerfile-2004 --> Ubuntu 20.04
* Dockerfile-2104 --> Ubuntu 21.04
* Dockerfile-debian --> Debian Bullseye
Depending on your system, the build-* scripts may need root privilleges
First, the docker container needs to be created:
```
cd scripts
./build-container.sh -f Dockerfile-<name>
```
Then, the sources will be compiled and the debian package generated:
```
./build-sunshine -p -s ..
```
You can run `build-sunshine -p -s ..` again as long as the docker container exists.
```
git pull
./build-sunshine -p -s ..
```
Optionally, the docker container can be removed after you're finished:
```
./build-container.sh -c delete
```
Finally install the resulting package:
```
sudo apt install -f sunshine-build/sunshine.deb
```

178
scripts/build-container.sh Executable file
View File

@@ -0,0 +1,178 @@
#/bin/bash -e
usage() {
echo "Usage: $0 [OPTIONS]"
echo " -c: command --> default [build]"
echo " | delete --> Delete the container, Dockerfile isn't mandatory"
echo " | build --> Build the container, Dockerfile is mandatory"
echo " | compile --> Builds the container, then compiles it. Dockerfile is mandatory"
echo ""
echo " -s: path: The path to the source for compilation"
echo " -n: name: Docker container name --> default [sunshine]"
echo " --> all: Build/Compile/Delete all available docker containers"
echo " -f: Dockerfile: The name of the docker file"
}
# Attempt to turn relative paths into absolute paths
absolute_path() {
RELATIVE_PATH=$1
if which realpath >/dev/null 2>/dev/null
then
RELATIVE_PATH=$(realpath $RELATIVE_PATH)
else
echo "Warning: realpath is not installed on your system, ensure [$1] is absolute"
fi
RETURN=$RELATIVE_PATH
}
CONTAINER_NAME=sunshine
COMMAND=BUILD
build_container() {
CONTAINER_NAME=$1
DOCKER_FILE=$2
if [ ! -f "$DOCKER_FILE" ]
then
echo "Error: $DOCKER_FILE doesn't exist"
exit 7
fi
echo "docker build . -t $CONTAINER_NAME -f $DOCKER_FILE"
docker build . -t "$CONTAINER_NAME" -f "$DOCKER_FILE"
}
delete() {
CONTAINER_NAME_UPPER=$(echo "$CONTAINER_NAME" | tr '[:lower:]' '[:upper:]')
if [ "$CONTAINER_NAME_UPPER" = "ALL" ]
then
shopt -s nullglob
for file in $(find . -maxdepth 1 -iname "Dockerfile-*" -type f)
do
CURRENT_CONTAINER="sunshine-$(echo $file | cut -c 14-)"
if docker inspect "$CURRENT_CONTAINER" > /dev/null 2> /dev/null
then
echo "docker rmi $CURRENT_CONTAINER"
docker rmi "$CURRENT_CONTAINER"
fi
done
shopt -u nullglob #revert nullglob back to it's normal default state
else
if docker inspect "$CONTAINER_NAME" > /dev/null 2> /dev/null
then
echo "docker rmi $CONTAINER_NAME"
docker rmi $CONTAINER_NAME
fi
fi
}
build() {
CONTAINER_NAME_UPPER=$(echo "$CONTAINER_NAME" | tr '[:lower:]' '[:upper:]')
if [ "$CONTAINER_NAME_UPPER" = "ALL" ]
then
shopt -s nullglob
for file in $(find . -maxdepth 1 -iname "Dockerfile-*" -type f)
do
CURRENT_CONTAINER="sunshine-$(echo $file | cut -c 14-)"
build_container "$CURRENT_CONTAINER" "$file"
done
shopt -u nullglob #revert nullglob back to it's normal default state
else
if [[ -z "$DOCKER_FILE" ]]
then
echo "Error: if container name isn't equal to 'all', you need to specify the Dockerfile"
exit 6
fi
build_container "$CONTAINER_NAME" "$DOCKER_FILE"
fi
}
abort() {
echo "$1"
exit 10
}
compile() {
CONTAINER_NAME_UPPER=$(echo "$CONTAINER_NAME" | tr '[:lower:]' '[:upper:]')
if [ "$CONTAINER_NAME_UPPER" = "ALL" ]
then
shopt -s nullglob
# If any docker container doesn't exist, we cannot compile all of them
for file in $(find . -maxdepth 1 -iname "Dockerfile-*" -type f)
do
CURRENT_CONTAINER="sunshine-$(echo $file | cut -c 14-)"
# If container doesn't exist --> abort.
docker inspect "$CURRENT_CONTAINER" > /dev/null 2> /dev/null || abort "Error: container image [$CURRENT_CONTAINER] doesn't exist"
done
for file in $(find . -maxdepth 1 -iname "Dockerfile-*" -type f)
do
CURRENT_CONTAINER="sunshine-$(echo $file | cut -c 14-)"
echo "$PWD/build-sunshine.sh -p -n $CURRENT_CONTAINER $SUNSHINE_SOURCES"
"$PWD/build-sunshine.sh" -p -n "$CURRENT_CONTAINER" $SUNSHINE_SOURCES
done
shopt -u nullglob #revert nullglob back to it's normal default state
else
# If container exists
if docker inspect "$CONTAINER_NAME" > /dev/null 2> /dev/null
then
echo "$PWD/build-sunshine.sh -p -n $CONTAINER_NAME $SUNSHINE_SOURCES"
"$PWD/build-sunshine.sh" -p -n "$CONTAINER_NAME" $SUNSHINE_SOURCES
else
echo "Error: container image [$CONTAINER_NAME] doesn't exist"
exit 9
fi
fi
}
while getopts ":c:hn:f:s:" arg; do
case ${arg} in
s)
SUNSHINE_SOURCES="-s $OPTARG"
;;
c)
COMMAND=$(echo $OPTARG | tr '[:lower:]' '[:upper:]')
;;
n)
echo "Container name: $OPTARG"
CONTAINER_NAME="$OPTARG"
;;
f)
echo "Using Dockerfile [$OPTARG]"
DOCKER_FILE="$OPTARG"
;;
h)
usage
exit 0
;;
esac
done
echo "$0 set to $(echo $COMMAND | tr '[:upper:]' '[:lower:]')"
if [ "$COMMAND" = "BUILD" ]
then
echo "Start building..."
delete
build
echo "Done."
elif [ "$COMMAND" = "COMPILE" ]
then
echo "Start compiling..."
compile
echo "Done."
elif [ "$COMMAND" = "DELETE" ]
then
echo "Start deleting..."
delete
echo "Done."
else
echo "Unknown command [$(echo $COMMAND | tr '[:upper:]' '[:lower:]')]"
exit 4
fi

34
scripts/build-private.sh Executable file
View File

@@ -0,0 +1,34 @@
#!/bin/bash -e
CMAKE_BUILD_TYPE="${CMAKE_BUILD_TYPE:-Release}"
SUNSHINE_EXECUTABLE_PATH="${SUNSHINE_EXECUTABLE_PATH:-/usr/bin/sunshine}"
SUNSHINE_ASSETS_DIR="${SUNSHINE_ASSETS_DIR:-/etc/sunshine}"
SUNSHINE_ROOT="${SUNSHINE_ROOT:-/root/sunshine}"
SUNSHINE_TAG="${SUNSHINE_TAG:-master}"
SUNSHINE_GIT_URL="${SUNSHINE_GIT_URL:-https://github.com/loki-47-6F-64/sunshine.git}"
SUNSHINE_ENABLE_WAYLAND=${SUNSHINE_ENABLE_WAYLAND:-ON}
SUNSHINE_ENABLE_X11=${SUNSHINE_ENABLE_X11:-ON}
SUNSHINE_ENABLE_DRM=${SUNSHINE_ENABLE_DRM:-ON}
SUNSHINE_ENABLE_CUDA=${SUNSHINE_ENABLE_CUDA:-ON}
# For debugging, it would be usefull to have the sources on the host.
if [[ ! -d "$SUNSHINE_ROOT" ]]
then
git clone --depth 1 --branch "$SUNSHINE_TAG" "$SUNSHINE_GIT_URL" --recurse-submodules "$SUNSHINE_ROOT"
fi
if [[ ! -d /root/sunshine-build ]]
then
mkdir -p /root/sunshine-build
fi
cd /root/sunshine-build
cmake "-DCMAKE_BUILD_TYPE=$CMAKE_BUILD_TYPE" "-DSUNSHINE_EXECUTABLE_PATH=$SUNSHINE_EXECUTABLE_PATH" "-DSUNSHINE_ASSETS_DIR=$SUNSHINE_ASSETS_DIR" "-DSUNSHINE_ENABLE_WAYLAND=$SUNSHINE_ENABLE_WAYLAND" "-DSUNSHINE_ENABLE_X11=$SUNSHINE_ENABLE_X11" "-DSUNSHINE_ENABLE_DRM=$SUNSHINE_ENABLE_DRM" "-DSUNSHINE_ENABLE_CUDA=$SUNSHINE_ENABLE_CUDA" "$SUNSHINE_ROOT"
make -j ${nproc}
./gen-deb

114
scripts/build-sunshine.sh Executable file
View File

@@ -0,0 +1,114 @@
#/bin/bash -e
usage() {
echo "Usage: $0"
echo " -d: Generate a debug build"
echo " -p: Generate a debian package"
echo " -u: The input device is not a TTY"
echo " -n name: Docker container name --> default [sunshine]"
echo " -s path/to/sources/sunshine: Use local sources instead of a git repository"
echo " -c path/to/cmake/binary/dir: Store cmake output on host OS"
}
# Attempt to turn relative paths into absolute paths
absolute_path() {
RELATIVE_PATH=$1
if which realpath >/dev/null 2>/dev/null
then
RELATIVE_PATH=$(realpath $RELATIVE_PATH)
else
echo "Warning: realpath is not installed on your system, ensure [$1] is absolute"
fi
RETURN=$RELATIVE_PATH
}
CMAKE_BUILD_TYPE="-e CMAKE_BUILD_TYPE=Release"
SUNSHINE_PACKAGE_BUILD=OFF
SUNSHINE_GIT_URL=https://github.com/loki-47-6F-64/sunshine.git
CONTAINER_NAME=sunshine
# Docker will fail if ctrl+c is passed through and the input is not a tty
DOCKER_INTERACTIVE=-ti
while getopts ":dpuhc:s:n:" arg; do
case ${arg} in
u)
echo "Input device is not a TTY"
USERNAME="$USER"
unset DOCKER_INTERACTIVE
;;
d)
echo "Creating debug build"
CMAKE_BUILD_TYPE="-e CMAKE_BUILD_TYPE=Debug"
;;
p)
echo "Creating package build"
SUNSHINE_PACKAGE_BUILD=ON
SUNSHINE_ASSETS_DIR="-e SUNSHINE_ASSETS_DIR=/etc/sunshine"
SUNSHINE_EXECUTABLE_PATH="-e SUNSHINE_EXECUTABLE_PATH=/usr/bin/sunshine"
;;
s)
absolute_path "$OPTARG"
OPTARG="$RETURN"
echo "Using sources from $OPTARG"
SUNSHINE_ROOT="-v $OPTARG:/root/sunshine"
;;
c)
[ "$USERNAME" == "" ] && USERNAME=$(logname)
absolute_path "$OPTARG"
OPTARG="$RETURN"
echo "Using $OPTARG as cmake binary dir"
if [[ ! -d $OPTARG ]]
then
echo "cmake binary dir doesn't exist, a new one will be created."
mkdir -p "$OPTARG"
[ "$USERNAME" == "$USER"] || chown $USERNAME:$USERNAME "$OPTARG"
fi
CMAKE_ROOT="-v $OPTARG:/root/sunshine-build"
;;
n)
echo "Container name: $OPTARG"
CONTAINER_NAME=$OPTARG
;;
h)
usage
exit 0
;;
esac
done
[ "$USERNAME" = "" ] && USERNAME=$(logname)
BUILD_DIR="$PWD/$CONTAINER_NAME-build"
[ "$SUNSHINE_ASSETS_DIR" = "" ] && SUNSHINE_ASSETS_DIR="-e SUNSHINE_ASSETS_DIR=$BUILD_DIR/assets"
[ "$SUNSHINE_EXECUTABLE_PATH" = "" ] && SUNSHINE_EXECUTABLE_PATH="-e SUNSHINE_EXECUTABLE_PATH=$BUILD_DIR/sunshine"
echo "docker run $DOCKER_INTERACTIVE --privileged $SUNSHINE_ROOT $CMAKE_ROOT $SUNSHINE_ASSETS_DIR $SUNSHINE_EXECUTABLE_PATH $CMAKE_BUILD_TYPE --name $CONTAINER_NAME $CONTAINER_NAME"
docker run $DOCKER_INTERACTIVE --privileged $SUNSHINE_ROOT $CMAKE_ROOT $SUNSHINE_ASSETS_DIR $SUNSHINE_EXECUTABLE_PATH $CMAKE_BUILD_TYPE --name $CONTAINER_NAME $CONTAINER_NAME
exit_code=$?
if [ $exit_code -eq 0 ]
then
mkdir -p $BUILD_DIR
case $SUNSHINE_PACKAGE_BUILD in
ON)
echo "Downloading package to: $BUILD_DIR/$CONTAINER_NAME.deb"
docker cp $CONTAINER_NAME:/root/sunshine-build/package-deb/sunshine.deb "$BUILD_DIR/$CONTAINER_NAME.deb"
;;
*)
echo "Downloading binary and assets to: $BUILD_DIR"
docker cp $CONTAINER_NAME:/root/sunshine/assets "$BUILD_DIR"
docker cp $CONTAINER_NAME:/root/sunshine-build/sunshine "$BUILD_DIR"
;;
esac
echo "chown --recursive $USERNAME:$USERNAME $BUILD_DIR"
chown --recursive $USERNAME:$USERNAME "$BUILD_DIR"
fi
echo "Removing docker container $CONTAINER_NAME"
docker rm $CONTAINER_NAME

BIN
sunshine.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

View File

@@ -72,7 +72,9 @@ opus_stream_config_t stream_configs[MAX_STREAM_CONFIG] {
auto control_shared = safe::make_shared<audio_ctx_t>(start_audio_control, stop_audio_control);
void encodeThread(packet_queue_t packets, sample_queue_t samples, config_t config, void *channel_data) {
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 = &stream_configs[map_stream(config.channels, config.flags[config_t::HIGH_QUALITY])];
@@ -85,24 +87,39 @@ void encodeThread(packet_queue_t packets, sample_queue_t samples, config_t confi
OPUS_APPLICATION_AUDIO,
nullptr) };
// For some reason, audio is crackling when the encoder is set to constant bitstream.
// We simulate a constant bitstream with OPUS_SET_BITRATE(OPUS_BITRATE_MAX) -->
// which tries to occupy as much space as possible in the packet
opus_multistream_encoder_ctl(opus.get(), OPUS_SET_BITRATE(OPUS_BITRATE_MAX));
auto frame_size = config.packetDuration * stream->sampleRate / 1000;
while(auto sample = samples->pop()) {
packet_t packet { 16 * 1024 }; // 16KB
buffer_t packet { 1400 }; // 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;
}
// Even with OPUS_SET_BITRATE(OPUS_BITRATE_MAX), silent packets are smaller than the rest
// Drop silent packets to ensure Moonlight won't complain
// A packet size of 128 seems a reasonable enough threshold
if(bytes < 128) {
BOOST_LOG(verbose) << "Dropped silent packet"sv;
continue;
}
packet.fake_resize(bytes);
packets->raise(channel_data, std::move(packet));
}
}
void capture(safe::signal_t *shutdown_event, packet_queue_t packets, config_t config, void *channel_data) {
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 = &stream_configs[map_stream(config.channels, config.flags[config_t::HIGH_QUALITY])];
@@ -113,7 +130,7 @@ void capture(safe::signal_t *shutdown_event, packet_queue_t packets, config_t co
auto &control = ref->control;
if(!control) {
BOOST_LOG(error) << "Couldn't create audio control"sv;
shutdown_event->view();
return;
}
@@ -146,7 +163,7 @@ void capture(safe::signal_t *shutdown_event, packet_queue_t packets, config_t co
}
auto samples = std::make_shared<sample_queue_t::element_type>(30);
std::thread thread { encodeThread, packets, samples, config, channel_data };
std::thread thread { encodeThread, samples, config, channel_data };
auto fg = util::fail_guard([&]() {
samples->stop();
@@ -206,23 +223,32 @@ int map_stream(int channels, bool quality) {
}
int start_audio_control(audio_ctx_t &ctx) {
auto fg = util::fail_guard([]() {
BOOST_LOG(warning) << "There will be no audio"sv;
});
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;
if(!(ctx.control = platf::audio_control())) {
return 0;
}
auto sink = ctx.control->sink_info();
if(!sink) {
// Let the calling code know it failed
ctx.control.reset();
return 0;
}
ctx.sink = std::move(*sink);
fg.disable();
return 0;
}
void stop_audio_control(audio_ctx_t &ctx) {
// restore audio-sink if applicable
if(!ctx.restore_sink) {

View File

@@ -37,9 +37,9 @@ struct config_t {
std::bitset<MAX_FLAGS> flags;
};
using packet_t = util::buffer_t<std::uint8_t>;
using packet_queue_t = std::shared_ptr<safe::queue_t<std::pair<void *, packet_t>>>;
void capture(safe::signal_t *shutdown_event, packet_queue_t packets, config_t config, void *channel_data);
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,4 +1,5 @@
#include <algorithm>
#include <filesystem>
#include <fstream>
#include <functional>
#include <iostream>
@@ -7,15 +8,20 @@
#include <boost/asio.hpp>
#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"
#define CERTIFICATE_FILE CA_DIR "/cacert.pem"
#define APPS_JSON_PATH SUNSHINE_ASSETS_DIR "/" APPS_JSON
#define APPS_JSON_PATH SUNSHINE_CONFIG_DIR "/" APPS_JSON
namespace config {
using namespace std::literals;
namespace nv {
enum preset_e : int {
@@ -93,16 +99,22 @@ enum quality_e : int {
_default = 0,
speed,
balanced,
//quality2,
};
enum rc_e : int {
enum class rc_hevc_e : int {
constqp, /**< Constant QP mode */
vbr_latency, /**< Latency Constrained Variable Bitrate */
vbr_peak, /**< Peak Contrained Variable Bitrate */
cbr, /**< Constant bitrate mode */
};
enum class rc_h264_e : int {
constqp, /**< Constant QP mode */
cbr, /**< Constant bitrate mode */
vbr_peak, /**< Peak Contrained Variable Bitrate */
vbr_latency, /**< Latency Constrained Variable Bitrate */
};
enum coder_e : int {
_auto = 0,
cabac,
@@ -114,15 +126,25 @@ std::optional<quality_e> quality_from_view(const std::string_view &quality) {
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) {
std::optional<int> rc_h264_from_view(const std::string_view &rc) {
#define _CONVERT_(x) \
if(rc == #x##sv) return x
if(rc == #x##sv) return (int)rc_h264_e::x
_CONVERT_(constqp);
_CONVERT_(vbr_latency);
_CONVERT_(vbr_peak);
_CONVERT_(cbr);
#undef _CONVERT_
return std::nullopt;
}
std::optional<int> rc_hevc_from_view(const std::string_view &rc) {
#define _CONVERT_(x) \
if(rc == #x##sv) return (int)rc_hevc_e::x
_CONVERT_(constqp);
_CONVERT_(vbr_latency);
_CONVERT_(vbr_peak);
@@ -141,7 +163,6 @@ int coder_from_view(const std::string_view &coder) {
} // namespace amd
video_t video {
0, // crf
28, // qp
0, // hevc_mode
@@ -160,6 +181,7 @@ video_t video {
{
amd::balanced,
std::nullopt,
std::nullopt,
-1 }, // amd
{}, // encoder
@@ -170,16 +192,18 @@ video_t video {
audio_t audio {};
stream_t stream {
2s, // ping_timeout
10s, // ping_timeout
APPS_JSON_PATH,
10, // fecPercentage
20, // fecPercentage
1 // channels
};
nvhttp_t nvhttp {
"lan", // origin_pin
"pc", // origin_pin
"lan", // origin web manager
PRIVATE_KEY_FILE,
CERTIFICATE_FILE,
@@ -203,14 +227,31 @@ nvhttp_t nvhttp {
};
input_t input {
2s, // back_button_timeout
500ms, // key_repeat_delay
std::chrono::duration<double> { 1 / 24.9 } // key_repeat_period
{
{ 0x10, 0xA0 },
{ 0x11, 0xA2 },
{ 0x12, 0xA4 },
},
2s, // back_button_timeout
500ms, // key_repeat_delay
std::chrono::duration<double> { 1 / 24.9 }, // key_repeat_period
{
platf::supported_gamepads().front().data(),
platf::supported_gamepads().front().size(),
}, // Default gamepad
};
sunshine_t sunshine {
2, // min_log_level
0 // flags
2, // min_log_level
0, // flags
{}, // User file
{}, // Username
{}, // Password
{}, // Password Salt
SUNSHINE_CONFIG_DIR "/sunshine.conf", // config file
{}, // cmd args
47989,
};
bool endline(char ch) {
@@ -293,7 +334,7 @@ parse_option(std::string_view::const_iterator begin, std::string_view::const_ite
std::make_pair(to_string(begin, end_name), to_string(begin_val, endl)));
}
std::unordered_map<std::string, std::string> parse_config(std::string_view file_content) {
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);
@@ -341,6 +382,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);
@@ -348,8 +421,20 @@ void int_f(std::unordered_map<std::string, std::string> &vars, const std::string
return;
}
auto &val = it->second;
input = util::from_chars(&val[0], &val[0] + val.size());
std::string_view val = it->second;
// If value is something like: "756" instead of 756
if(val.size() >= 2 && val[0] == '"') {
val = val.substr(1, val.size() - 2);
}
// If that integer is in hexadecimal
if(val.size() >= 2 && val.substr(0, 2) == "0x"sv) {
input = util::from_hex<int>(val.substr(2));
}
else {
input = util::from_view(val);
}
vars.erase(it);
}
@@ -361,8 +446,20 @@ void int_f(std::unordered_map<std::string, std::string> &vars, const std::string
return;
}
auto &val = it->second;
input = util::from_chars(&val[0], &val[0] + val.size());
std::string_view val = it->second;
// If value is something like: "756" instead of 756
if(val.size() >= 2 && val[0] == '"') {
val = val.substr(1, val.size() - 2);
}
// If that integer is in hexadecimal
if(val.size() >= 2 && val.substr(0, 2) == "0x"sv) {
input = util::from_hex<int>(val.substr(2));
}
else {
input = util::from_view(val);
}
vars.erase(it);
}
@@ -402,9 +499,12 @@ bool to_bool(std::string &boolean) {
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, int &input) {
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);
@@ -412,7 +512,7 @@ void bool_f(std::unordered_map<std::string, std::string> &vars, const std::strin
return;
}
input = to_bool(tmp) ? 1 : 0;
input = to_bool(tmp);
}
void double_f(std::unordered_map<std::string, std::string> &vars, const std::string &name, double &input) {
@@ -491,21 +591,43 @@ void list_int_f(std::unordered_map<std::string, std::string> &vars, const std::s
list_string_f(vars, name, list);
for(auto &el : list) {
input.emplace_back(util::from_view(el));
std::string_view val = el;
// If value is something like: "756" instead of 756
if(val.size() >= 2 && val[0] == '"') {
val = val.substr(1, val.size() - 2);
}
int tmp;
// If the integer is a hexadecimal
if(val.size() >= 2 && val.substr(0, 2) == "0x"sv) {
tmp = util::from_hex<int>(val.substr(2));
}
else {
tmp = util::from_view(val);
}
input.emplace_back(tmp);
}
}
void print_help(const char *name) {
std::cout
<< "Usage: "sv << name << " [options] [/path/to/configuration_file]"sv << std::endl
<< " Any configurable option can be overwritten with: \"name=value\""sv << std::endl
<< std::endl
<< " --help | print help"sv << 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;
void map_int_int_f(std::unordered_map<std::string, std::string> &vars, const std::string &name, std::unordered_map<int, int> &input) {
std::vector<int> list;
list_int_f(vars, name, list);
// The list needs to be a multiple of 2
if(list.size() % 2) {
std::cout << "Warning: expected "sv << name << " to have a multiple of two elements --> not "sv << list.size() << std::endl;
return;
}
int x = 0;
while(x < list.size()) {
auto key = list[x++];
auto val = list[x++];
input.emplace(key, val);
}
}
int apply_flags(const char *line) {
@@ -518,8 +640,11 @@ int apply_flags(const char *line) {
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::CONST_PIN].flip();
config::sunshine.flags[config::flag::UPNP].flip();
break;
default:
std::cout << "Warning: Unrecognized flag: ["sv << *line << ']' << std::endl;
@@ -533,11 +658,14 @@ int apply_flags(const char *line) {
}
void apply_config(std::unordered_map<std::string, std::string> &&vars) {
if(!fs::exists(stream.file_apps.c_str())) {
fs::copy_file(SUNSHINE_DEFAULT_DIR "/" APPS_JSON, stream.file_apps);
}
for(auto &[name, val] : vars) {
std::cout << "["sv << name << "] -- ["sv << val << ']' << std::endl;
}
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, 3 });
@@ -548,17 +676,29 @@ void apply_config(std::unordered_map<std::string, std::string> &&vars) {
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);
std::string rc;
string_f(vars, "amd_rc", rc);
int_f(vars, "amd_coder", video.amd.coder, amd::coder_from_view);
if(!rc.empty()) {
video.amd.rc_h264 = amd::rc_h264_from_view(rc);
video.amd.rc_hevc = amd::rc_hevc_from_view(rc);
}
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);
@@ -567,6 +707,7 @@ void apply_config(std::unordered_map<std::string, std::string> &&vars) {
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_web_ui_allowed", nvhttp.origin_web_ui_allowed, { "pc"sv, "lan"sv, "wan"sv });
int to = -1;
int_between_f(vars, "ping_timeout", to, { -1, std::numeric_limits<int>::max() });
@@ -576,8 +717,19 @@ void apply_config(std::unordered_map<std::string, std::string> &&vars) {
int_between_f(vars, "channels", stream.channels, { 1, std::numeric_limits<int>::max() });
string_f(vars, "file_apps", stream.file_apps);
int_between_f(vars, "fec_percentage", stream.fec_percentage, { 1, 100 });
path_f(vars, "file_apps", stream.file_apps);
int_between_f(vars, "fec_percentage", stream.fec_percentage, { 1, 255 });
map_int_int_f(vars, "keybindings"s, input.keybindings);
// This config option will only be used by the UI
// When editing in the config file itself, use "keybindings"
bool map_rightalt_to_win = false;
bool_f(vars, "key_rightalt_to_key_win", map_rightalt_to_win);
if(map_rightalt_to_win) {
input.keybindings.emplace(0xA5, 0x5B);
}
to = std::numeric_limits<int>::min();
int_f(vars, "back_button_timeout", to);
@@ -599,8 +751,21 @@ void apply_config(std::unordered_map<std::string, std::string> &&vars) {
input.key_repeat_delay = std::chrono::milliseconds { to };
}
string_restricted_f(vars, "gamepad"s, input.gamepad, platf::supported_gamepads());
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_f(vars, "min_log_level", log_level_string);
if(!log_level_string.empty()) {
if(log_level_string == "verbose"sv) {
@@ -624,6 +789,13 @@ void apply_config(std::unordered_map<std::string, std::string> &&vars) {
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);
@@ -641,11 +813,9 @@ void apply_config(std::unordered_map<std::string, std::string> &&vars) {
}
int parse(int argc, char *argv[]) {
const char *config_file = SUNSHINE_ASSETS_DIR "/sunshine.conf";
std::unordered_map<std::string, std::string> cmd_vars;
for(auto x = argc - 1; x > 0; --x) {
for(auto x = 1; x < argc; ++x) {
auto line = argv[x];
if(line == "--help"sv) {
@@ -653,6 +823,13 @@ int parse(int argc, char *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;
@@ -663,7 +840,7 @@ int parse(int argc, char *argv[]) {
auto pos = std::find(line, line_end, '=');
if(pos == line_end) {
config_file = line;
sunshine.config_file = line;
}
else {
TUPLE_EL(var, 1, parse_option(line, line_end));
@@ -672,23 +849,23 @@ int parse(int argc, char *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));
}
}
}
std::ifstream in { config_file };
if(!in.is_open()) {
std::cout << "Error: Couldn't open "sv << config_file << std::endl;
return -1;
if(!fs::exists(sunshine.config_file)) {
fs::copy_file(SUNSHINE_DEFAULT_DIR "/sunshine.conf", sunshine.config_file);
}
auto vars = parse_config(std::string {
// Quick and dirty
std::istreambuf_iterator<char>(in),
std::istreambuf_iterator<char>() });
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));

View File

@@ -5,13 +5,13 @@
#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 qp; // higher == more compression and less quality
int hevc_mode;
@@ -29,7 +29,8 @@ struct video_t {
struct {
std::optional<int> quality;
std::optional<int> rc;
std::optional<int> rc_h264;
std::optional<int> rc_hevc;
int coder;
} amd;
@@ -58,6 +59,7 @@ 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
@@ -72,24 +74,44 @@ struct nvhttp_t {
};
struct input_t {
std::unordered_map<int, int> keybindings;
std::chrono::milliseconds back_button_timeout;
std::chrono::milliseconds key_repeat_delay;
std::chrono::duration<double> key_repeat_period;
std::string gamepad;
};
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
CONST_PIN = 4, // Use "universal" pin
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;
@@ -100,6 +122,6 @@ extern input_t input;
extern sunshine_t sunshine;
int parse(int argc, char *argv[]);
std::unordered_map<std::string, std::string> parse_config(const std::string_view &file_content);
} // namespace config
#endif

616
sunshine/confighttp.cpp Normal file
View File

@@ -0,0 +1,616 @@
//
// 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);
}
void send_redirect(resp_https_t response, req_https_t request, const char *path) {
auto address = request->remote_endpoint_address();
BOOST_LOG(info) << "Web UI: ["sv << address << "] -- not authorized"sv;
const SimpleWeb::CaseInsensitiveMultimap headers {
{ "Location", path }
};
response->write(SimpleWeb::StatusCode::redirection_temporary_redirect, 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;
}
//If credentials are shown, redirect the user to a /welcome page
if(config::sunshine.username.empty()){
send_redirect(response,request,"/welcome");
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 getWelcomePage(resp_https_t response, req_https_t request) {
print_req(request);
if(!config::sunshine.username.empty()){
send_redirect(response,request,"/");
return;
}
std::string header = read_file(WEB_DIR "header-no-nav.html");
std::string content = read_file(WEB_DIR "welcome.html");
response->write(header + content);
}
void getTroubleshootingPage(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 "troubleshooting.html");
response->write(header + content);
}
void getBootstrapCss(resp_https_t response, req_https_t request) {
print_req(request);
std::string content = read_file(WEB_DIR "third_party/bootstrap.min.css");
response->write(content);
}
void getBootstrapJs(resp_https_t response, req_https_t request) {
print_req(request);
std::string content = read_file(WEB_DIR "third_party/bootstrap.bundle.min.js");
response->write(content);
}
void getVueJs(resp_https_t response, req_https_t request) {
print_req(request);
std::string content = read_file(WEB_DIR "third_party/vue.js");
response->write(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(!config::sunshine.username.empty() && !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.count("currentUsername") > 0 ? inputTree.get<std::string>("currentUsername") : "";
auto newUsername = inputTree.get<std::string>("newUsername");
auto password = inputTree.count("currentPassword") > 0 ? inputTree.get<std::string>("currentPassword") : "";
auto newPassword = inputTree.count("newPassword") > 0 ? inputTree.get<std::string>("newPassword") : "";
auto confirmPassword = inputTree.count("confirmNewPassword") > 0 ? inputTree.get<std::string>("confirmNewPassword") : "";
if(newUsername.length() == 0) newUsername = username;
if(newUsername.length() == 0){
outputTree.put("status", false);
outputTree.put("error", "Invalid Username");
} else {
auto hash = util::hex(crypto::hash(password + config::sunshine.salt)).to_string();
if(config::sunshine.username.empty() || (username == config::sunshine.username && hash == config::sunshine.password)) {
if(newPassword.empty() || newPassword != confirmPassword) {
outputTree.put("status", false);
outputTree.put("error", "Password Mismatch");
} else {
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 unpairAll(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());
});
nvhttp::erase_all_clients();
outputTree.put("status", true);
}
void closeApp(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());
});
proc::proc.terminate();
outputTree.put("status", true);
}
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["^/welcome$"]["GET"] = getWelcomePage;
server.resource["^/troubleshooting$"]["GET"] = getTroubleshootingPage;
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.resource["^/api/clients/unpair$"]["POST"] = unpairAll;
server.resource["^/api/apps/close"]["POST"] = closeApp;
server.resource["^/third_party/bootstrap.min.css$"]["GET"] = getBootstrapCss;
server.resource["^/third_party/bootstrap.bundle.min.js$"]["GET"] = getBootstrapJs;
server.resource["^/third_party/vue.js$"]["GET"] = getVueJs;
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

@@ -4,8 +4,9 @@
#include "crypto.h"
#include <openssl/pem.h>
namespace crypto {
using big_num_t = util::safe_ptr<BIGNUM, BN_free>;
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>;
@@ -17,6 +18,26 @@ void cert_chain_t::add(x509_t &&cert) {
_certs.emplace_back(std::make_pair(std::move(cert), std::move(x509_store)));
}
static int openssl_verify_cb(int ok, X509_STORE_CTX *ctx) {
int err_code = X509_STORE_CTX_get_error(ctx);
switch(err_code) {
//FIXME: Checking for X509_V_ERR_UNABLE_TO_GET_ISSUER_CERT_LOCALLY is a temporary workaround to get mmonlight-embedded to work on the raspberry pi
case X509_V_ERR_UNABLE_TO_GET_ISSUER_CERT_LOCALLY:
return 1;
// Expired or not-yet-valid certificates are fine. Sometimes Moonlight is running on embedded devices
// that don't have accurate clocks (or haven't yet synchronized by the time Moonlight first runs).
// This behavior also matches what GeForce Experience does.
case X509_V_ERR_CERT_NOT_YET_VALID:
case X509_V_ERR_CERT_HAS_EXPIRED:
return 1;
default:
return ok;
}
}
/*
* When certificates from two or more instances of Moonlight have been added to x509_store_t,
* only one of them will be verified by X509_verify_cert, resulting in only a single instance of
@@ -31,8 +52,13 @@ const char *cert_chain_t::verify(x509_t::element_type *cert) {
X509_STORE_CTX_cleanup(_cert_ctx.get());
});
X509_STORE_CTX_init(_cert_ctx.get(), x509_store.get(), nullptr, nullptr);
X509_STORE_CTX_set_cert(_cert_ctx.get(), cert);
X509_STORE_CTX_init(_cert_ctx.get(), x509_store.get(), cert, nullptr);
X509_STORE_CTX_set_verify_cb(_cert_ctx.get(), openssl_verify_cb);
// We don't care to validate the entire chain for the purposes of client auth.
// Some versions of clients forked from Moonlight Embedded produce client certs
// that OpenSSL doesn't detect as self-signed due to some X509v3 extensions.
X509_STORE_CTX_set_flags(_cert_ctx.get(), X509_V_FLAG_PARTIAL_CHAIN);
auto err = X509_verify_cert(_cert_ctx.get());
@@ -42,10 +68,6 @@ const char *cert_chain_t::verify(x509_t::element_type *cert) {
err_code = X509_STORE_CTX_get_error(_cert_ctx.get());
//FIXME: Checking for X509_V_ERR_UNABLE_TO_GET_ISSUER_CERT_LOCALLY is a temporary workaround to get mmonlight-embedded to work on the raspberry pi
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) {
return X509_verify_cert_error_string(err_code);
}
@@ -54,71 +76,91 @@ const char *cert_chain_t::verify(x509_t::element_type *cert) {
return X509_verify_cert_error_string(err_code);
}
cipher_t::cipher_t(const crypto::aes_t &key) : ctx { EVP_CIPHER_CTX_new() }, key { key }, padding { true } {}
int cipher_t::decrypt(const std::string_view &cipher, std::vector<std::uint8_t> &plaintext) {
int len;
namespace cipher {
auto fg = util::fail_guard([this]() {
EVP_CIPHER_CTX_reset(ctx.get());
});
static int init_decrypt_gcm(cipher_ctx_t &ctx, aes_t *key, aes_t *iv, bool padding) {
ctx.reset(EVP_CIPHER_CTX_new());
// Gen 7 servers use 128-bit AES ECB
if(EVP_DecryptInit_ex(ctx.get(), EVP_aes_128_ecb(), nullptr, key.data(), nullptr) != 1) {
if(!ctx) {
return -1;
}
EVP_CIPHER_CTX_set_padding(ctx.get(), padding);
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) {
return -1;
}
if(EVP_DecryptFinal_ex(ctx.get(), plaintext.data(), &len) != 1) {
return -1;
}
plaintext.resize(len + size);
return 0;
}
int cipher_t::decrypt_gcm(aes_t &iv, const std::string_view &tagged_cipher,
std::vector<std::uint8_t> &plaintext) {
auto cipher = tagged_cipher.substr(16);
auto tag = tagged_cipher.substr(0, 16);
auto fg = util::fail_guard([this]() {
EVP_CIPHER_CTX_reset(ctx.get());
});
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;
}
EVP_CIPHER_CTX_set_padding(ctx.get(), padding);
return 0;
}
static int init_encrypt_gcm(cipher_ctx_t &ctx, aes_t *key, aes_t *iv, bool padding) {
ctx.reset(EVP_CIPHER_CTX_new());
// Gen 7 servers use 128-bit AES ECB
if(EVP_EncryptInit_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) {
return -1;
}
if(EVP_EncryptInit_ex(ctx.get(), nullptr, nullptr, key->data(), iv->data()) != 1) {
return -1;
}
EVP_CIPHER_CTX_set_padding(ctx.get(), padding);
return 0;
}
static int init_encrypt_cbc(cipher_ctx_t &ctx, aes_t *key, aes_t *iv, bool padding) {
ctx.reset(EVP_CIPHER_CTX_new());
// Gen 7 servers use 128-bit AES ECB
if(EVP_EncryptInit_ex(ctx.get(), EVP_aes_128_cbc(), nullptr, key->data(), iv->data()) != 1) {
return -1;
}
EVP_CIPHER_CTX_set_padding(ctx.get(), padding);
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) {
return 0;
}
int gcm_t::decrypt(const std::string_view &tagged_cipher, std::vector<std::uint8_t> &plaintext, aes_t *iv) {
if(!decrypt_ctx && init_decrypt_gcm(decrypt_ctx, &key, iv, padding)) {
return -1;
}
if(EVP_CIPHER_CTX_ctrl(ctx.get(), EVP_CTRL_GCM_SET_TAG, tag.size(), const_cast<char *>(tag.data())) != 1) {
// Calling with cipher == nullptr results in a parameter change
// without requiring a reallocation of the internal cipher ctx.
if(EVP_DecryptInit_ex(decrypt_ctx.get(), nullptr, nullptr, nullptr, iv->data()) != 1) {
return false;
}
auto cipher = tagged_cipher.substr(tag_size);
auto tag = tagged_cipher.substr(0, tag_size);
plaintext.resize((cipher.size() + 15) / 16 * 16);
int size;
if(EVP_DecryptUpdate(decrypt_ctx.get(), plaintext.data(), &size, (const std::uint8_t *)cipher.data(), cipher.size()) != 1) {
return -1;
}
if(EVP_CIPHER_CTX_ctrl(decrypt_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(decrypt_ctx.get(), plaintext.data() + size, &len) != 1) {
return -1;
}
@@ -126,28 +168,92 @@ int cipher_t::decrypt_gcm(aes_t &iv, const std::string_view &tagged_cipher,
return 0;
}
int cipher_t::encrypt(const std::string_view &plaintext, std::vector<std::uint8_t> &cipher) {
int gcm_t::encrypt(const std::string_view &plaintext, std::uint8_t *tagged_cipher, aes_t *iv) {
if(!encrypt_ctx && init_encrypt_gcm(encrypt_ctx, &key, iv, padding)) {
return -1;
}
// Calling with cipher == nullptr results in a parameter change
// without requiring a reallocation of the internal cipher ctx.
if(EVP_EncryptInit_ex(encrypt_ctx.get(), nullptr, nullptr, nullptr, iv->data()) != 1) {
return -1;
}
auto tag = tagged_cipher;
auto cipher = tag + tag_size;
int len;
int size = round_to_pkcs7_padded(plaintext.size());
// Encrypt into the caller's buffer
if(EVP_EncryptUpdate(encrypt_ctx.get(), cipher, &size, (const std::uint8_t *)plaintext.data(), plaintext.size()) != 1) {
return -1;
}
// GCM encryption won't ever fill ciphertext here but we have to call it anyway
if(EVP_EncryptFinal_ex(encrypt_ctx.get(), cipher + size, &len) != 1) {
return -1;
}
if(EVP_CIPHER_CTX_ctrl(encrypt_ctx.get(), EVP_CTRL_GCM_GET_TAG, tag_size, tag) != 1) {
return -1;
}
return len + size;
}
int ecb_t::decrypt(const std::string_view &cipher, std::vector<std::uint8_t> &plaintext) {
int len;
auto fg = util::fail_guard([this]() {
EVP_CIPHER_CTX_reset(ctx.get());
EVP_CIPHER_CTX_reset(decrypt_ctx.get());
});
// 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_DecryptInit_ex(decrypt_ctx.get(), EVP_aes_128_ecb(), nullptr, key.data(), nullptr) != 1) {
return -1;
}
EVP_CIPHER_CTX_set_padding(ctx.get(), padding);
EVP_CIPHER_CTX_set_padding(decrypt_ctx.get(), padding);
plaintext.resize((cipher.size() + 15) / 16 * 16);
auto size = (int)plaintext.size();
// Decrypt into the caller's buffer, leaving room for the auth tag to be prepended
if(EVP_DecryptUpdate(decrypt_ctx.get(), plaintext.data(), &size, (const std::uint8_t *)cipher.data(), cipher.size()) != 1) {
return -1;
}
if(EVP_DecryptFinal_ex(decrypt_ctx.get(), plaintext.data(), &len) != 1) {
return -1;
}
plaintext.resize(len + size);
return 0;
}
int ecb_t::encrypt(const std::string_view &plaintext, std::vector<std::uint8_t> &cipher) {
auto fg = util::fail_guard([this]() {
EVP_CIPHER_CTX_reset(encrypt_ctx.get());
});
// Gen 7 servers use 128-bit AES ECB
if(EVP_EncryptInit_ex(encrypt_ctx.get(), EVP_aes_128_ecb(), nullptr, key.data(), nullptr) != 1) {
return -1;
}
EVP_CIPHER_CTX_set_padding(encrypt_ctx.get(), padding);
int len;
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(encrypt_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(encrypt_ctx.get(), cipher.data() + size, &len) != 1) {
return -1;
}
@@ -155,6 +261,44 @@ int cipher_t::encrypt(const std::string_view &plaintext, std::vector<std::uint8_
return 0;
}
int cbc_t::encrypt(const std::string_view &plaintext, std::uint8_t *cipher, aes_t *iv) {
if(!encrypt_ctx && init_encrypt_cbc(encrypt_ctx, &key, iv, padding)) {
return -1;
}
// Calling with cipher == nullptr results in a parameter change
// without requiring a reallocation of the internal cipher ctx.
if(EVP_EncryptInit_ex(encrypt_ctx.get(), nullptr, nullptr, nullptr, iv->data()) != 1) {
return false;
}
int len;
int size = plaintext.size(); //round_to_pkcs7_padded(plaintext.size());
// Encrypt into the caller's buffer
if(EVP_EncryptUpdate(encrypt_ctx.get(), cipher, &size, (const std::uint8_t *)plaintext.data(), plaintext.size()) != 1) {
return -1;
}
if(EVP_EncryptFinal_ex(encrypt_ctx.get(), cipher + size, &len) != 1) {
return -1;
}
return size + len;
}
ecb_t::ecb_t(const aes_t &key, bool padding)
: cipher_t { EVP_CIPHER_CTX_new(), EVP_CIPHER_CTX_new(), key, padding } {}
cbc_t::cbc_t(const aes_t &key, bool padding)
: cipher_t { nullptr, nullptr, key, padding } {}
gcm_t::gcm_t(const crypto::aes_t &key, bool padding)
: cipher_t { nullptr, nullptr, key, padding } {}
} // namespace cipher
aes_t gen_aes_key(const std::array<uint8_t, 16> &salt, const std::string_view &pin) {
aes_t key;
@@ -338,4 +482,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);
}
} // namespace crypto
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

@@ -6,7 +6,6 @@
#define SUNSHINE_CRYPTO_H
#include <array>
#include <cassert>
#include <openssl/evp.h>
#include <openssl/rand.h>
#include <openssl/sha.h>
@@ -35,6 +34,7 @@ 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:
@@ -64,24 +66,70 @@ private:
x509_store_ctx_t _cert_ctx;
};
namespace cipher {
constexpr std::size_t tag_size = 16;
constexpr std::size_t round_to_pkcs7_padded(std::size_t size) {
return ((size + 15) / 16) * 16;
}
class cipher_t {
public:
cipher_t(const aes_t &key);
cipher_t(cipher_t &&) noexcept = default;
cipher_t &operator=(cipher_t &&) noexcept = default;
cipher_ctx_t decrypt_ctx;
cipher_ctx_t encrypt_ctx;
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;
public:
bool padding;
};
class ecb_t : public cipher_t {
public:
ecb_t() = default;
ecb_t(ecb_t &&) noexcept = default;
ecb_t &operator=(ecb_t &&) noexcept = default;
ecb_t(const aes_t &key, bool padding = true);
int encrypt(const std::string_view &plaintext, std::vector<std::uint8_t> &cipher);
int decrypt(const std::string_view &cipher, std::vector<std::uint8_t> &plaintext);
};
class gcm_t : public cipher_t {
public:
gcm_t() = default;
gcm_t(gcm_t &&) noexcept = default;
gcm_t &operator=(gcm_t &&) noexcept = default;
gcm_t(const crypto::aes_t &key, bool padding = true);
/**
* length of cipher must be at least: round_to_pkcs7_padded(plaintext.size()) + crypto::cipher::tag_size
*
* return -1 on error
* return bytes written on success
*/
int encrypt(const std::string_view &plaintext, std::uint8_t *tagged_cipher, aes_t *iv);
int decrypt(const std::string_view &cipher, std::vector<std::uint8_t> &plaintext, aes_t *iv);
};
class cbc_t : public cipher_t {
public:
cbc_t() = default;
cbc_t(cbc_t &&) noexcept = default;
cbc_t &operator=(cbc_t &&) noexcept = default;
cbc_t(const crypto::aes_t &key, bool padding = true);
/**
* length of cipher must be at least: round_to_pkcs7_padded(plaintext.size())
*
* return -1 on error
* return bytes written on success
*/
int encrypt(const std::string_view &plaintext, std::uint8_t *cipher, aes_t *iv);
};
} // namespace cipher
} // namespace crypto
#endif //SUNSHINE_CRYPTO_H

182
sunshine/httpcommon.cpp Normal file
View File

@@ -0,0 +1,182 @@
#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(reload_user_creds(config::sunshine.credentials_file)) return -1;
} else {
BOOST_LOG(info) << "Open the Web UI to set your new username and password and getting started";
}
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;
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

@@ -17,12 +17,23 @@ extern "C" {
#include "thread_pool.h"
#include "utility.h"
using namespace std::literals;
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
constexpr auto VKEY_SHIFT = 0x10;
constexpr auto VKEY_LSHIFT = 0xA0;
constexpr auto VKEY_RSHIFT = 0xA1;
constexpr auto VKEY_CONTROL = 0x11;
constexpr auto VKEY_LCONTROL = 0xA2;
constexpr auto VKEY_RCONTROL = 0xA3;
constexpr auto VKEY_MENU = 0x12;
constexpr auto VKEY_LMENU = 0xA4;
constexpr auto VKEY_RMENU = 0xA5;
enum class button_state_e {
NONE,
DOWN,
@@ -46,12 +57,7 @@ void free_id(std::bitset<N> &gamepad_mask, int id) {
gamepad_mask[id] = false;
}
touch_port_event_t touch_port_event;
platf::touch_port_t touch_port {
0, 0, 0, 0
};
static util::TaskPool::task_id_t task_id {};
static util::TaskPool::task_id_t key_press_repeat_id {};
static std::unordered_map<short, bool> key_press {};
static std::array<std::uint8_t, 5> mouse_press {};
@@ -89,15 +95,65 @@ struct gamepad_t {
};
struct input_t {
input_t() : active_gamepad_state {}, gamepads(MAX_GAMEPADS), mouse_left_button_timeout {} {}
enum shortkey_e {
CTRL = 0x1,
ALT = 0x2,
SHIFT = 0x4,
SHORTCUT = CTRL | ALT | SHIFT
};
input_t(
safe::mail_raw_t::event_t<input::touch_port_t> touch_port_event,
platf::rumble_queue_t rumble_queue)
: shortcutFlags {},
active_gamepad_state {},
gamepads(MAX_GAMEPADS),
touch_port_event { std::move(touch_port_event) },
rumble_queue { std::move(rumble_queue) },
mouse_left_button_timeout {},
touch_port { 0, 0, 0, 0, 0, 0, 1.0f } {}
// Keep track of alt+ctrl+shift key combo
int shortcutFlags;
std::uint16_t active_gamepad_state;
std::vector<gamepad_t> gamepads;
safe::mail_raw_t::event_t<input::touch_port_t> touch_port_event;
platf::rumble_queue_t rumble_queue;
util::ThreadPool::task_id_t mouse_left_button_timeout;
input::touch_port_t touch_port;
};
using namespace std::literals;
/**
* Apply shortcut based on VKEY
* On success
* return > 0
* On nothing
* return 0
*/
inline int apply_shortcut(short keyCode) {
constexpr auto VK_F1 = 0x70;
constexpr auto VK_F13 = 0x7C;
BOOST_LOG(debug) << "Apply Shortcut: 0x"sv << util::hex((std::uint8_t)keyCode).to_string_view();
if(keyCode >= VK_F1 && keyCode <= VK_F13) {
mail::man->event<int>(mail::switch_display)->raise(keyCode - VK_F1);
return 1;
}
switch(keyCode) {
case 0x4E /* VKEY_N */:
display_cursor = !display_cursor;
return 1;
}
return 0;
}
void print(PNV_REL_MOUSE_MOVE_PACKET packet) {
BOOST_LOG(debug)
@@ -142,7 +198,8 @@ void print(PNV_KEYBOARD_PACKET packet) {
}
void print(PNV_MULTI_CONTROLLER_PACKET packet) {
BOOST_LOG(debug)
// Moonlight spams controller packet even when not necessary
BOOST_LOG(verbose)
<< "--begin controller packet--"sv << std::endl
<< "controllerNumber ["sv << packet->controllerNumber << ']' << std::endl
<< "activeGamepadMask ["sv << util::hex(packet->activeGamepadMask).to_string_view() << ']' << std::endl
@@ -188,19 +245,17 @@ void print(void *input) {
}
void passthrough(std::shared_ptr<input_t> &input, PNV_REL_MOUSE_MOVE_PACKET packet) {
display_cursor = true;
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();
}
@@ -216,13 +271,27 @@ void passthrough(std::shared_ptr<input_t> &input, PNV_ABS_MOUSE_MOVE_PACKET pack
return;
}
float width = util::endian::big(packet->width);
float height = util::endian::big(packet->height);
auto width = (float)util::endian::big(packet->width);
auto height = (float)util::endian::big(packet->height);
auto scale_x = (float)touch_port.width / width;
auto scale_y = (float)touch_port.height / height;
auto scalarX = touch_port.width / width;
auto scalarY = touch_port.height / height;
platf::abs_mouse(platf_input, touch_port, x * scale_x, y * scale_y);
x *= scalarX;
y *= scalarY;
auto offsetX = touch_port.client_offsetX;
auto offsetY = touch_port.client_offsetY;
std::clamp(x, offsetX, width - offsetX);
std::clamp(y, offsetY, 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) {
@@ -231,8 +300,6 @@ void passthrough(std::shared_ptr<input_t> &input, PNV_MOUSE_BUTTON_PACKET packet
auto constexpr BUTTON_LEFT = 0x01;
auto constexpr BUTTON_RIGHT = 0x03;
display_cursor = true;
auto release = packet->action == BUTTON_RELEASED;
auto button = util::endian::big(packet->button);
@@ -291,32 +358,86 @@ void passthrough(std::shared_ptr<input_t> &input, PNV_MOUSE_BUTTON_PACKET packet
platf::button_mouse(platf_input, button, release);
}
short map_keycode(short keycode) {
auto it = config::input.keybindings.find(keycode);
if(it != std::end(config::input.keybindings)) {
return it->second;
}
return keycode;
}
/**
* Update flags for keyboard shortcut combo's
*/
inline void update_shortcutFlags(int *flags, short keyCode, bool release) {
switch(keyCode) {
case VKEY_SHIFT:
case VKEY_LSHIFT:
case VKEY_RSHIFT:
if(release) {
*flags &= ~input_t::SHIFT;
}
else {
*flags |= input_t::SHIFT;
}
break;
case VKEY_CONTROL:
case VKEY_LCONTROL:
case VKEY_RCONTROL:
if(release) {
*flags &= ~input_t::CTRL;
}
else {
*flags |= input_t::CTRL;
}
break;
case VKEY_MENU:
case VKEY_LMENU:
case VKEY_RMENU:
if(release) {
*flags &= ~input_t::ALT;
}
else {
*flags |= input_t::ALT;
}
break;
}
}
void repeat_key(short key_code) {
// If key no longer pressed, stop repeating
if(!key_press[key_code]) {
task_id = nullptr;
key_press_repeat_id = nullptr;
return;
}
platf::keyboard(platf_input, key_code & 0x00FF, false);
platf::keyboard(platf_input, map_keycode(key_code), false);
task_id = task_pool.pushDelayed(repeat_key, config::input.key_repeat_period, key_code).task_id;
key_press_repeat_id = task_pool.pushDelayed(repeat_key, config::input.key_repeat_period, key_code).task_id;
}
void passthrough(std::shared_ptr<input_t> &input, PNV_KEYBOARD_PACKET packet) {
auto constexpr BUTTON_RELEASED = 0x04;
auto release = packet->keyAction == BUTTON_RELEASED;
auto keyCode = packet->keyCode & 0x00FF;
auto &pressed = key_press[packet->keyCode];
auto &pressed = key_press[keyCode];
if(!pressed) {
if(!release) {
if(task_id) {
task_pool.cancel(task_id);
// A new key has been pressed down, we need to check for key combo's
// If a keycombo has been pressed down, don't pass it through
if(input->shortcutFlags == input_t::SHORTCUT && apply_shortcut(keyCode) > 0) {
return;
}
if(key_press_repeat_id) {
task_pool.cancel(key_press_repeat_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;
key_press_repeat_id = task_pool.pushDelayed(repeat_key, config::input.key_repeat_delay, keyCode).task_id;
}
}
else {
@@ -330,16 +451,16 @@ void passthrough(std::shared_ptr<input_t> &input, PNV_KEYBOARD_PACKET packet) {
}
pressed = !release;
platf::keyboard(platf_input, packet->keyCode & 0x00FF, release);
update_shortcutFlags(&input->shortcutFlags, map_keycode(keyCode), release);
platf::keyboard(platf_input, map_keycode(keyCode), release);
}
void passthrough(PNV_SCROLL_PACKET packet) {
display_cursor = true;
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) {
int updateGamepads(std::vector<gamepad_t> &gamepads, std::int16_t old_state, std::int16_t new_state, const platf::rumble_queue_t &rumble_queue) {
auto xorGamepadMask = old_state ^ new_state;
if(!xorGamepadMask) {
return 0;
@@ -365,7 +486,7 @@ int updateGamepads(std::vector<gamepad_t> &gamepads, std::int16_t old_state, std
return -1;
}
if(platf::alloc_gamepad(platf_input, id)) {
if(platf::alloc_gamepad(platf_input, id, rumble_queue)) {
free_id(gamepadMask, id);
// allocating a gamepad failed: solution: ignore gamepads
// The implementations of platf::alloc_gamepad already has logging
@@ -381,7 +502,7 @@ int updateGamepads(std::vector<gamepad_t> &gamepads, std::int16_t old_state, std
}
void passthrough(std::shared_ptr<input_t> &input, PNV_MULTI_CONTROLLER_PACKET packet) {
if(updateGamepads(input->gamepads, input->active_gamepad_state, packet->activeGamepadMask)) {
if(updateGamepads(input->gamepads, input->active_gamepad_state, packet->activeGamepadMask, input->rumble_queue)) {
return;
}
@@ -407,8 +528,6 @@ void passthrough(std::shared_ptr<input_t> &input, PNV_MULTI_CONTROLLER_PACKET pa
return;
}
display_cursor = false;
std::uint16_t bf = packet->buttonFlags;
platf::gamepad_state_t gamepad_state {
bf,
@@ -517,7 +636,7 @@ void passthrough(std::shared_ptr<input_t> &input, std::vector<std::uint8_t> &&in
}
void reset(std::shared_ptr<input_t> &input) {
task_pool.cancel(task_id);
task_pool.cancel(key_press_repeat_id);
task_pool.cancel(input->mouse_left_button_timeout);
// Ensure input is synchronous, by using the task_pool
@@ -536,13 +655,23 @@ void reset(std::shared_ptr<input_t> &input) {
});
}
void init() {
touch_port_event = std::make_unique<touch_port_event_t::element_type>();
platf_input = platf::input();
class deinit_t : public platf::deinit_t {
public:
~deinit_t() override {
platf_input.reset();
}
};
[[nodiscard]] std::unique_ptr<platf::deinit_t> init() {
platf_input = platf::input();
return std::make_unique<deinit_t>();
}
std::shared_ptr<input_t> alloc() {
auto input = std::make_shared<input_t>();
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),
mail->queue<platf::rumble_t>(mail::rumble));
// Workaround to ensure new frames will be captured when a client connects
task_pool.pushDelayed([]() {

View File

@@ -5,10 +5,12 @@
#ifndef SUNSHINE_INPUT_H
#define SUNSHINE_INPUT_H
#include <functional>
#include "platform/common.h"
#include "thread_safe.h"
namespace input {
namespace input {
struct input_t;
void print(void *input);
@@ -16,12 +18,18 @@ 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();
[[nodiscard]] std::unique_ptr<platf::deinit_t> init();
std::shared_ptr<input_t> alloc();
std::shared_ptr<input_t> alloc(safe::mail_t mail);
using touch_port_event_t = std::unique_ptr<safe::event_t<platf::touch_port_t>>;
extern touch_port_event_t touch_port_event;
struct touch_port_t : public platf::touch_port_t {
int env_width, env_height;
// Offset x and y coordinates of the client
float client_offsetX, client_offsetY;
float scalar_inv;
};
} // namespace input
#endif //SUNSHINE_INPUT_H

View File

@@ -5,6 +5,8 @@
#include "process.h"
#include <csignal>
#include <filesystem>
#include <fstream>
#include <iostream>
#include <thread>
@@ -15,9 +17,13 @@
#include <boost/log/sources/severity_logger.hpp>
#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"
@@ -26,6 +32,8 @@ extern "C" {
#include <rs.h>
}
safe::mail_t mail::man;
using namespace std::literals;
namespace bl = boost::log;
@@ -37,7 +45,7 @@ bl::sources::severity_logger<int> warning(3); // Strange events
bl::sources::severity_logger<int> error(4); // Recoverable errors
bl::sources::severity_logger<int> fatal(5); // Unrecoverable errors
bool display_cursor;
bool display_cursor = true;
using text_sink = bl::sinks::asynchronous_sink<bl::sinks::text_ostream_backend>;
boost::shared_ptr<text_sink> sink;
@@ -48,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();
}
@@ -64,14 +96,54 @@ void on_signal(int sig, FN &&fn) {
std::signal(sig, on_signal_forwarder);
}
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;
}
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[]) {
util::TaskPool::task_id_t force_shutdown = nullptr;
bool shutdown_by_interrupt = false;
auto exit_guard = util::fail_guard([&shutdown_by_interrupt, &force_shutdown]() {
if(!shutdown_by_interrupt) {
return;
}
task_pool.cancel(force_shutdown);
std::cout << "Sunshine exited: Press enter to continue"sv << std::endl;
std::string _;
std::getline(std::cin, _);
});
mail::man = std::make_shared<safe::mail_raw_t>();
if(config::parse(argc, argv)) {
return 0;
}
if(config::sunshine.min_log_level >= 2) {
if(config::sunshine.min_log_level >= 1) {
av_log_set_level(AV_LOG_QUIET);
}
else {
av_log_set_level(AV_LOG_DEBUG);
}
sink = boost::make_shared<text_sink>();
@@ -113,28 +185,61 @@ int main(int argc, char *argv[]) {
os << _date << log_type << view.attribute_values()[message].extract<std::string>();
});
// Flush after each log record to ensure log file contents on disk isn't stale.
// This is particularly important when running from a Windows service.
sink->locked_backend()->auto_flush(true);
bl::core::get()->add_sink(sink);
auto fg = util::fail_guard(log_flush);
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;
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);
}
task_pool.start(1);
// Create signal handler after logging has been initialized
auto shutdown_event = std::make_shared<safe::event_t<bool>>();
on_signal(SIGINT, [shutdown_event]() {
auto shutdown_event = mail::man->event<bool>(mail::shutdown);
on_signal(SIGINT, [&shutdown_by_interrupt, &force_shutdown, shutdown_event]() {
BOOST_LOG(info) << "Interrupt handler called"sv;
auto task = []() {
BOOST_LOG(fatal) << "10 seconds passed, yet Sunshine's still running: Forcing shutdown"sv;
log_flush();
std::abort();
};
force_shutdown = task_pool.pushDelayed(task, 10s).task_id;
shutdown_by_interrupt = true;
shutdown_event->raise(true);
});
auto proc_opt = proc::parse(config::stream.file_apps);
if(!proc_opt) {
return 7;
}
on_signal(SIGTERM, [&force_shutdown, shutdown_event]() {
BOOST_LOG(info) << "Terminate handler called"sv;
{
proc::ctx_t ctx;
ctx.name = "Desktop"s;
proc_opt->get_apps().emplace(std::begin(proc_opt->get_apps()), std::move(ctx));
}
auto task = []() {
BOOST_LOG(fatal) << "10 seconds passed, yet Sunshine's still running: Forcing shutdown"sv;
log_flush();
std::abort();
};
force_shutdown = task_pool.pushDelayed(task, 10s).task_id;
proc::proc = std::move(*proc_opt);
shutdown_event->raise(true);
});
proc::refresh(config::stream.file_apps);
auto deinit_guard = platf::init();
if(!deinit_guard) {
@@ -142,18 +247,73 @@ int main(int argc, char *argv[]) {
}
reed_solomon_init();
auto input_deinit_guard = input::init();
if(video::init()) {
return 2;
}
if(http::init()) {
return 3;
}
task_pool.start(1);
std::unique_ptr<platf::deinit_t> mDNS;
auto sync_mDNS = std::async(std::launch::async, [&mDNS]() {
mDNS = platf::publish::start();
});
std::thread httpThread { nvhttp::start, shutdown_event };
stream::rtpThread(shutdown_event);
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,7 +5,11 @@
#ifndef SUNSHINE_MAIN_H
#define SUNSHINE_MAIN_H
#include <string_view>
#include "thread_pool.h"
#include "thread_safe.h"
#include <boost/log/common.hpp>
extern util::ThreadPool task_pool;
@@ -19,4 +23,35 @@ 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);
MAIL(switch_display);
// Local mail
MAIL(touch_port);
MAIL(idr);
MAIL(rumble);
#undef MAIL
} // namespace mail
#endif //SUNSHINE_MAIN_H

View File

@@ -6,8 +6,8 @@
#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,7 +16,7 @@ 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)
};

View File

@@ -18,8 +18,10 @@
#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"
@@ -28,21 +30,15 @@
#include "utility.h"
#include "uuid.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 = "3.12.0.1";
constexpr auto VERSION = "7.1.431.0";
constexpr auto GFE_VERSION = "3.23.0.74";
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>;
@@ -80,8 +76,6 @@ 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 resp_https_t = std::shared_ptr<typename SimpleWeb::ServerBase<SimpleWeb::HTTPS>::Response>;
@@ -97,7 +91,19 @@ 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) {
pt::ptree node;
@@ -115,12 +121,19 @@ 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() {
if(!fs::exists(config::nvhttp.file_state)) {
unique_id = util::uuid_t::generate().string();
BOOST_LOG(info) << "File "sv << config::nvhttp.file_state << " doesn't exist"sv;
http::unique_id = util::uuid_t::generate().string();
return;
}
@@ -129,12 +142,19 @@ void load_state() {
pt::read_json(config::nvhttp.file_state, root);
}
catch(std::exception &e) {
BOOST_LOG(warning) << e.what();
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) {
@@ -170,7 +190,7 @@ 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);
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;
@@ -191,7 +211,7 @@ void getservercert(pair_session_t &sess, pt::ptree &tree, const std::string &pin
auto salt = util::from_hex<std::array<uint8_t, 16>>(salt_view, true);
auto key = crypto::gen_aes_key(*salt, pin);
auto key = crypto::gen_aes_key(salt, pin);
sess.cipher_key = std::make_unique<crypto::aes_t>(key);
tree.put("root.paired", 1);
@@ -202,8 +222,7 @@ void serverchallengeresp(pair_session_t &sess, pt::ptree &tree, const args_t &ar
auto encrypted_response = util::from_hex_vec(args.at("serverchallengeresp"s), true);
std::vector<uint8_t> decrypted;
crypto::cipher_t cipher(*sess.cipher_key);
cipher.padding = false;
crypto::cipher::ecb_t cipher(*sess.cipher_key, false);
cipher.decrypt(encrypted_response, decrypted);
@@ -222,8 +241,7 @@ void serverchallengeresp(pair_session_t &sess, pt::ptree &tree, const args_t &ar
void clientchallenge(pair_session_t &sess, pt::ptree &tree, const args_t &args) {
auto challenge = util::from_hex_vec(args.at("clientchallenge"s), true);
crypto::cipher_t cipher(*sess.cipher_key);
cipher.padding = false;
crypto::cipher::ecb_t cipher(*sess.cipher_key, false);
std::vector<uint8_t> decrypted;
cipher.decrypt(challenge, decrypted);
@@ -347,8 +365,11 @@ 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();
response->close_connection_after_response = true;
}
template<class T>
@@ -357,6 +378,14 @@ void pair(std::shared_ptr<safe::queue_t<crypto::x509_t>> &add_cert, std::shared_
pt::ptree tree;
auto fg = util::fail_guard([&]() {
std::ostringstream data;
pt::write_xml(data, tree);
response->write(data.str());
response->close_connection_after_response = true;
});
auto args = request->parse_query_string();
if(args.find("uniqueid"s) == std::end(args)) {
tree.put("root.<xmlattr>.status_code", 400);
@@ -380,11 +409,7 @@ void pair(std::shared_ptr<safe::queue_t<crypto::x509_t>> &add_cert, std::shared_
ptr->second.async_insert_pin.salt = std::move(args.at("salt"s));
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]) {
if(config::sunshine.flags[config::flag::PIN_STDIN]) {
std::string pin;
std::cout << "Please insert pin: "sv;
@@ -395,6 +420,7 @@ void pair(std::shared_ptr<safe::queue_t<crypto::x509_t>> &add_cert, std::shared_
else {
ptr->second.async_insert_pin.response = std::move(response);
fg.disable();
return;
}
}
@@ -415,37 +441,16 @@ void pair(std::shared_ptr<safe::queue_t<crypto::x509_t>> &add_cert, std::shared_
else {
tree.put("root.<xmlattr>.status_code", 404);
}
std::ostringstream data;
pt::write_xml(data, tree);
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;
@@ -459,15 +464,38 @@ void pin(std::shared_ptr<typename SimpleWeb::ServerBase<T>::Response> response,
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);
response->close_connection_after_response = true;
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>
@@ -494,7 +522,9 @@ 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.HttpsPort", map_port(PORT_HTTPS));
tree.put("root.ExternalPort", map_port(PORT_HTTP));
tree.put("root.mac", platf::get_mac_address(request->local_endpoint_address()));
tree.put("root.MaxLumaPixelsHEVC", config::video.hevc_mode > 1 ? "1869449984" : "0");
tree.put("root.LocalIP", request->local_endpoint_address());
@@ -547,6 +577,7 @@ void serverinfo(std::shared_ptr<typename SimpleWeb::ServerBase<T>::Response> res
pt::write_xml(data, tree);
response->write(data.str());
response->close_connection_after_response = true;
}
void applist(resp_https_t response, req_https_t request) {
@@ -559,6 +590,7 @@ void applist(resp_https_t response, req_https_t request) {
pt::write_xml(data, tree);
response->write(data.str());
response->close_connection_after_response = true;
});
auto args = request->parse_query_string();
@@ -602,6 +634,7 @@ void launch(bool &host_audio, resp_https_t response, req_https_t request) {
pt::write_xml(data, tree);
response->write(data.str());
response->close_connection_after_response = true;
});
if(stream::session_count() == config::stream.channels) {
@@ -648,6 +681,7 @@ void launch(bool &host_audio, resp_https_t response, req_https_t request) {
stream::launch_session_raise(make_launch_session(host_audio, args));
tree.put("root.<xmlattr>.status_code", 200);
tree.put("root.sessionUrl0", "rtsp://"s + request->local_endpoint_address() + ':' + std::to_string(map_port(stream::RTSP_SETUP_PORT)));
tree.put("root.gamesession", 1);
}
@@ -660,6 +694,7 @@ void resume(bool &host_audio, resp_https_t response, req_https_t request) {
pt::write_xml(data, tree);
response->write(data.str());
response->close_connection_after_response = true;
});
// It is possible that due a race condition that this if-statement gives a false negative,
@@ -693,6 +728,7 @@ void resume(bool &host_audio, resp_https_t response, req_https_t request) {
stream::launch_session_raise(make_launch_session(host_audio, args));
tree.put("root.<xmlattr>.status_code", 200);
tree.put("root.sessionUrl0", "rtsp://"s + request->local_endpoint_address() + ':' + std::to_string(map_port(stream::RTSP_SETUP_PORT)));
tree.put("root.resume", 1);
}
@@ -705,6 +741,7 @@ void cancel(resp_https_t response, req_https_t request) {
pt::write_xml(data, tree);
response->write(data.str());
response->close_connection_after_response = true;
});
// It is possible that due a race condition that this if-statement gives a false positive,
@@ -729,83 +766,16 @@ void appasset(resp_https_t response, req_https_t request) {
std::ifstream in(SUNSHINE_ASSETS_DIR "/box.png");
response->write(SimpleWeb::StatusCode::success_ok, in);
response->close_connection_after_response = true;
}
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();
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;
}
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::signal_t> shutdown_event) {
bool clean_slate = config::sunshine.flags[config::flag::FRESH_STATE];
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)) {
shutdown_event->raise(true);
return;
}
}
origin_pin_allowed = net::from_enum_string(config::nvhttp.origin_pin_allowed);
if(!clean_slate) {
load_state();
@@ -827,22 +797,38 @@ void start(std::shared_ptr<safe::signal_t> shutdown_event) {
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) {
ctx->set_verify_callback([](int verified, boost::asio::ssl::verify_context &ctx) {
// To respond with an error message, a connection must be established
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;
// Verify certificates after establishing connection
https_server.verify = [&cert_chain, add_cert](SSL *ssl) {
auto x509 = SSL_get_peer_certificate(ssl);
if(!x509) {
BOOST_LOG(info) << "unknown -- denied"sv;
return 0;
}
int verified = 0;
auto fg = util::fail_guard([&]() {
char subject_name[256];
auto x509 = ctx.native_handle();
X509_NAME_oneline(X509_get_subject_name(X509_STORE_CTX_get_current_cert(x509)), subject_name, sizeof(subject_name));
X509_NAME_oneline(X509_get_subject_name(x509), subject_name, sizeof(subject_name));
BOOST_LOG(info) << subject_name << " -- "sv << (verified ? "verfied"sv : "denied"sv);
});
if(verified) {
return 1;
}
while(add_cert->peek()) {
char subject_name[256];
@@ -853,22 +839,32 @@ void start(std::shared_ptr<safe::signal_t> shutdown_event) {
cert_chain.add(std::move(cert));
}
auto err_str = cert_chain.verify(X509_STORE_CTX_get_current_cert(ctx.native_handle()));
auto err_str = cert_chain.verify(x509);
if(err_str) {
BOOST_LOG(warning) << "SSL Verification error :: "sv << err_str;
return 0;
return verified;
}
verified = 1;
return 1;
});
// /resume doesn't get the parameter "localAudioPlayMode"
// /launch will store it in host_audio
bool host_audio {};
return verified;
};
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.on_verify_failed = [](resp_https_t resp, req_https_t req) {
pt::ptree tree;
auto g = util::fail_guard([&]() {
std::ostringstream data;
pt::write_xml(data, tree);
resp->write(data.str());
resp->close_connection_after_response = true;
});
tree.put("root.<xmlattr>.status_code"s, 401);
tree.put("root.<xmlattr>.query"s, req->path);
tree.put("root.<xmlattr>.status_message"s, "The client is not authorized. Certificate verification failed."s);
};
https_server.default_resource = not_found<SimpleWeb::HTTPS>;
https_server.resource["^/serverinfo$"]["GET"] = serverinfo<SimpleWeb::HTTPS>;
@@ -882,7 +878,7 @@ void start(std::shared_ptr<safe::signal_t> shutdown_event) {
https_server.config.reuse_address = true;
https_server.config.address = "0.0.0.0"s;
https_server.config.port = PORT_HTTPS;
https_server.config.port = port_https;
http_server.default_resource = not_found<SimpleWeb::HTTP>;
http_server.resource["^/serverinfo$"]["GET"] = serverinfo<SimpleWeb::HTTP>;
@@ -891,14 +887,14 @@ void start(std::shared_ptr<safe::signal_t> shutdown_event) {
http_server.config.reuse_address = true;
http_server.config.address = "0.0.0.0"s;
http_server.config.port = PORT_HTTP;
http_server.config.port = port_http;
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_HTTPS << ", "sv << PORT_HTTP << "]: "sv << err.what();
BOOST_LOG(fatal) << "Couldn't bind http server to ports ["sv << port_http << ", "sv << port_http << "]: "sv << err.what();
shutdown_event->raise(true);
return;
@@ -914,7 +910,7 @@ void start(std::shared_ptr<safe::signal_t> shutdown_event) {
return;
}
BOOST_LOG(fatal) << "Couldn't start http server to ports ["sv << PORT_HTTPS << ", "sv << PORT_HTTP << "]: "sv << err.what();
BOOST_LOG(fatal) << "Couldn't start http server to ports ["sv << port_https << ", "sv << port_https << "]: "sv << err.what();
shutdown_event->raise(true);
return;
}
@@ -932,30 +928,8 @@ void start(std::shared_ptr<safe::signal_t> shutdown_event) {
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;
void erase_all_clients(){
map_id_client.clear();
save_state();
}
} // namespace nvhttp

View File

@@ -5,17 +5,16 @@
#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::signal_t> shutdown_event);
}
constexpr auto PORT_HTTP = 0;
constexpr auto PORT_HTTPS = -5;
void start();
bool pin(std::string pin);
void erase_all_clients();
} // namespace nvhttp
#endif //SUNSHINE_NVHTTP_H

View File

@@ -5,12 +5,18 @@
#ifndef SUNSHINE_COMMON_H
#define SUNSHINE_COMMON_H
#include "sunshine/utility.h"
#include <bitset>
#include <filesystem>
#include <functional>
#include <mutex>
#include <string>
#include "sunshine/thread_safe.h"
#include "sunshine/utility.h"
struct sockaddr;
struct AVFrame;
namespace platf {
constexpr auto MAX_GAMEPADS = 32;
@@ -30,6 +36,18 @@ constexpr std::uint16_t B = 0x2000;
constexpr std::uint16_t X = 0x4000;
constexpr std::uint16_t Y = 0x8000;
struct rumble_t {
KITTY_DEFAULT_CONSTR(rumble_t)
rumble_t(std::uint16_t id, std::uint16_t lowfreq, std::uint16_t highfreq)
: id { id }, lowfreq { lowfreq }, highfreq { highfreq } {}
std::uint16_t id;
std::uint16_t lowfreq;
std::uint16_t highfreq;
};
using rumble_queue_t = safe::mail_raw_t::queue_t<rumble_t>;
namespace speaker {
enum speaker_e {
FRONT_LEFT,
@@ -67,9 +85,11 @@ constexpr std::uint8_t map_surround71[] {
};
} // namespace speaker
enum class dev_type_e {
none,
enum class mem_type_e {
system,
vaapi,
dxgi,
cuda,
unknown
};
@@ -100,13 +120,8 @@ inline std::string_view from_pix_fmt(pix_fmt_e pix_fmt) {
// Dimensions for touchscreen input
struct touch_port_t {
std::uint32_t offset_x, offset_y;
std::uint32_t width, height;
constexpr touch_port_t(
std::uint32_t offset_x, std::uint32_t offset_y,
std::uint32_t width, std::uint32_t height) noexcept : offset_x { offset_x }, offset_y { offset_y },
width { width }, height { height } {};
int offset_x, offset_y;
int width, height;
};
struct gamepad_state_t {
@@ -126,16 +141,19 @@ public:
struct img_t {
public:
img_t() = default;
img_t(img_t &&) = delete;
img_t(const img_t &) = delete;
img_t &operator=(img_t &&) = delete;
img_t &operator=(const img_t &) = delete;
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;
virtual ~img_t() = default;
};
@@ -155,12 +173,20 @@ struct sink_t {
struct hwdevice_t {
void *data {};
platf::img_t *img {};
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;
@@ -175,13 +201,37 @@ enum class capture_e : int {
class display_t {
public:
/**
* When display has a new image ready, this callback will be called with the new image.
*
* On Break Request -->
* Returns nullptr
*
* On Success -->
* Returns the image object that should be filled next.
* This may or may not be the image send with the callback
*/
using snapshot_cb_t = std::function<std::shared_ptr<img_t>(std::shared_ptr<img_t> &img)>;
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;
/**
* snapshot_cb --> the callback
* std::shared_ptr<img_t> img --> The first image to use
* bool *cursor --> A pointer to the flag that indicates wether the cursor should be captured as well
*
* Returns either:
* capture_e::ok when stopping
* capture_e::error on error
* capture_e::reinit when need of reinitialization
*/
virtual capture_e capture(snapshot_cb_t &&snapshot_cb, std::shared_ptr<img_t> img, 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(int width, int height, pix_fmt_e pix_fmt) {
virtual std::shared_ptr<hwdevice_t> make_hwdevice(pix_fmt_e pix_fmt) {
return std::make_shared<hwdevice_t>();
}
@@ -189,6 +239,7 @@ public:
// 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;
};
@@ -215,13 +266,28 @@ 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::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(dev_type_e hwdevice_type);
/**
* display_name --> The name of the monitor that SHOULD be displayed
* If display_name is empty --> Use the first monitor that's compatible you can find
* If you require to use this parameter in a seperate thread --> make a copy of it.
*
* framerate --> The peak number of images per second
*
* Returns display_t based on hwdevice_type
*/
std::shared_ptr<display_t> display(mem_type_e hwdevice_type, const std::string &display_name, int framerate);
// A list of names of displays accepted as display_name with the mem_type_e
std::vector<std::string> display_names(mem_type_e hwdevice_type);
input_t input();
void move_mouse(input_t &input, int deltaX, int deltaY);
@@ -231,10 +297,19 @@ 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);
int alloc_gamepad(input_t &input, int nr, rumble_queue_t rumble_queue);
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();
std::vector<std::string_view> &supported_gamepads();
} // namespace platf
#endif //SUNSHINE_COMMON_H

View File

@@ -2,6 +2,8 @@
// Created by loki on 5/16/21.
//
#include <bitset>
#include <sstream>
#include <boost/regex.hpp>
#include <pulse/error.h>
@@ -63,7 +65,7 @@ struct mic_attr_t : public mic_t {
}
};
std::unique_ptr<mic_t> microphone(const std::uint8_t *mapping, int channels, std::uint32_t sample_rate) {
std::unique_ptr<mic_t> microphone(const std::uint8_t *mapping, int channels, std::uint32_t sample_rate, std::uint32_t frame_size) {
auto mic = std::make_unique<mic_attr_t>();
pa_sample_spec ss { PA_SAMPLE_S16LE, sample_rate, (std::uint8_t)channels };
@@ -74,6 +76,9 @@ std::unique_ptr<mic_t> microphone(const std::uint8_t *mapping, int channels, std
channel = position_mapping[*mapping++];
});
pa_buffer_attr pa_attr = {};
pa_attr.maxlength = frame_size * 8;
int status;
const char *audio_sink = "@DEFAULT_MONITOR@";
@@ -84,7 +89,7 @@ std::unique_ptr<mic_t> microphone(const std::uint8_t *mapping, int channels, std
mic->mic.reset(
pa_simple_new(nullptr, "sunshine",
pa_stream_direction_t::PA_STREAM_RECORD, audio_sink,
"sunshine-record", &ss, &pa_map, nullptr, &status));
"sunshine-record", &ss, &pa_map, &pa_attr, &status));
if(!mic->mic) {
auto err_str = pa_strerror(status);
@@ -283,7 +288,7 @@ public:
sink_t sink;
// If hardware sink with more channels found, set that as host
int channels = 0;
int channels = 0;
// Count of all virtual sinks that are created by us
int nullcount = 0;
@@ -299,9 +304,7 @@ public:
return;
}
if(sink_info->flags & PA_SINK_HARDWARE &&
sink_info->channel_map.channels > channels) {
if(sink_info->active_port != nullptr) {
sink.host = sink_info->name;
channels = sink_info->channel_map.channels;
}
@@ -339,7 +342,7 @@ public:
}
if(!channels) {
BOOST_LOG(warning) << "Couldn't find hardware sink"sv;
BOOST_LOG(warning) << "Couldn't find an active sink"sv;
}
if(index.stereo == PA_INVALID_INDEX) {
@@ -380,7 +383,7 @@ public:
}
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);
return ::platf::microphone(mapping, channels, sample_rate, frame_size);
}
int set_sink(const std::string &sink) override {
@@ -434,8 +437,4 @@ std::unique_ptr<audio_control_t> audio_control() {
return audio;
}
std::unique_ptr<deinit_t> init() {
return std::make_unique<deinit_t>();
}
} // namespace platf

View File

@@ -0,0 +1,717 @@
#include <bitset>
#include <NvFBC.h>
#include <ffnvcodec/dynlink_loader.h>
extern "C" {
#include <libavcodec/avcodec.h>
#include <libavutil/hwcontext_cuda.h>
#include <libavutil/imgutils.h>
}
#include "cuda.h"
#include "graphics.h"
#include "sunshine/main.h"
#include "sunshine/utility.h"
#include "wayland.h"
#define SUNSHINE_STRINGVIEW_HELPER(x) x##sv
#define SUNSHINE_STRINGVIEW(x) SUNSHINE_STRINGVIEW_HELPER(x)
#define CU_CHECK(x, y) \
if(check((x), SUNSHINE_STRINGVIEW(y ": "))) return -1
#define CU_CHECK_IGNORE(x, y) \
check((x), SUNSHINE_STRINGVIEW(y ": "))
using namespace std::literals;
namespace cuda {
constexpr auto cudaDevAttrMaxThreadsPerBlock = (CUdevice_attribute)1;
constexpr auto cudaDevAttrMaxThreadsPerMultiProcessor = (CUdevice_attribute)39;
void pass_error(const std::string_view &sv, const char *name, const char *description) {
BOOST_LOG(error) << sv << name << ':' << description;
}
void cff(CudaFunctions *cf) {
cuda_free_functions(&cf);
}
using cdf_t = util::safe_ptr<CudaFunctions, cff>;
static cdf_t cdf;
inline static int check(CUresult result, const std::string_view &sv) {
if(result != CUDA_SUCCESS) {
const char *name;
const char *description;
cdf->cuGetErrorName(result, &name);
cdf->cuGetErrorString(result, &description);
BOOST_LOG(error) << sv << name << ':' << description;
return -1;
}
return 0;
}
void freeStream(CUstream stream) {
CU_CHECK_IGNORE(cdf->cuStreamDestroy(stream), "Couldn't destroy cuda stream");
}
class img_t : public platf::img_t {
public:
tex_t tex;
};
int init() {
auto status = cuda_load_functions(&cdf, nullptr);
if(status) {
BOOST_LOG(error) << "Couldn't load cuda: "sv << status;
return -1;
}
CU_CHECK(cdf->cuInit(0), "Couldn't initialize cuda");
return 0;
}
class cuda_t : public platf::hwdevice_t {
public:
int init(int in_width, int in_height) {
if(!cdf) {
BOOST_LOG(warning) << "cuda not initialized"sv;
return -1;
}
data = (void *)0x1;
width = in_width;
height = in_height;
return 0;
}
int set_frame(AVFrame *frame) override {
this->hwframe.reset(frame);
this->frame = frame;
auto hwframe_ctx = (AVHWFramesContext *)frame->hw_frames_ctx->data;
if(hwframe_ctx->sw_format != AV_PIX_FMT_NV12) {
BOOST_LOG(error) << "cuda::cuda_t doesn't support any format other than AV_PIX_FMT_NV12"sv;
return -1;
}
if(av_hwframe_get_buffer(frame->hw_frames_ctx, frame, 0)) {
BOOST_LOG(error) << "Couldn't get hwframe for NVENC"sv;
return -1;
}
auto cuda_ctx = (AVCUDADeviceContext *)hwframe_ctx->device_ctx->hwctx;
stream = make_stream();
if(!stream) {
return -1;
}
cuda_ctx->stream = stream.get();
auto sws_opt = sws_t::make(width, height, frame->width, frame->height, width * 4);
if(!sws_opt) {
return -1;
}
sws = std::move(*sws_opt);
linear_interpolation = width != frame->width || height != frame->height;
return 0;
}
void set_colorspace(std::uint32_t colorspace, std::uint32_t color_range) override {
sws.set_colorspace(colorspace, color_range);
auto tex = tex_t::make(height, width * 4);
if(!tex) {
return;
}
// The default green color is ugly.
// Update the background color
platf::img_t img;
img.width = width;
img.height = height;
img.pixel_pitch = 4;
img.row_pitch = img.width * img.pixel_pitch;
std::vector<std::uint8_t> image_data;
image_data.resize(img.row_pitch * img.height);
img.data = image_data.data();
if(sws.load_ram(img, tex->array)) {
return;
}
sws.convert(frame->data[0], frame->data[1], frame->linesize[0], frame->linesize[1], tex->texture.linear, stream.get(), { frame->width, frame->height, 0, 0 });
}
cudaTextureObject_t tex_obj(const tex_t &tex) const {
return linear_interpolation ? tex.texture.linear : tex.texture.point;
}
stream_t stream;
frame_t hwframe;
int width, height;
// When heigth and width don't change, it's not necessary to use linear interpolation
bool linear_interpolation;
sws_t sws;
};
class cuda_ram_t : public cuda_t {
public:
int convert(platf::img_t &img) override {
return sws.load_ram(img, tex.array) || sws.convert(frame->data[0], frame->data[1], frame->linesize[0], frame->linesize[1], tex_obj(tex), stream.get());
}
int set_frame(AVFrame *frame) {
if(cuda_t::set_frame(frame)) {
return -1;
}
auto tex_opt = tex_t::make(height, width * 4);
if(!tex_opt) {
return -1;
}
tex = std::move(*tex_opt);
return 0;
}
tex_t tex;
};
class cuda_vram_t : public cuda_t {
public:
int convert(platf::img_t &img) override {
return sws.convert(frame->data[0], frame->data[1], frame->linesize[0], frame->linesize[1], tex_obj(((img_t *)&img)->tex), stream.get());
}
};
std::shared_ptr<platf::hwdevice_t> make_hwdevice(int width, int height, bool vram) {
if(init()) {
return nullptr;
}
std::shared_ptr<cuda_t> cuda;
if(vram) {
cuda = std::make_shared<cuda_vram_t>();
}
else {
cuda = std::make_shared<cuda_ram_t>();
}
if(cuda->init(width, height)) {
return nullptr;
}
return cuda;
}
namespace nvfbc {
static PNVFBCCREATEINSTANCE createInstance {};
static NVFBC_API_FUNCTION_LIST func { NVFBC_VERSION };
static constexpr inline NVFBC_BOOL nv_bool(bool b) {
return b ? NVFBC_TRUE : NVFBC_FALSE;
}
static void *handle { nullptr };
int init() {
static bool funcs_loaded = false;
if(funcs_loaded) return 0;
if(!handle) {
handle = dyn::handle({ "libnvidia-fbc.so.1", "libnvidia-fbc.so" });
if(!handle) {
return -1;
}
}
std::vector<std::tuple<dyn::apiproc *, const char *>> funcs {
{ (dyn::apiproc *)&createInstance, "NvFBCCreateInstance" },
};
if(dyn::load(handle, funcs)) {
dlclose(handle);
handle = nullptr;
return -1;
}
auto status = cuda::nvfbc::createInstance(&cuda::nvfbc::func);
if(status) {
BOOST_LOG(error) << "Unable to create NvFBC instance"sv;
dlclose(handle);
handle = nullptr;
return -1;
}
funcs_loaded = true;
return 0;
}
class ctx_t {
public:
ctx_t(NVFBC_SESSION_HANDLE handle) {
NVFBC_BIND_CONTEXT_PARAMS params { NVFBC_BIND_CONTEXT_PARAMS_VER };
if(func.nvFBCBindContext(handle, &params)) {
BOOST_LOG(error) << "Couldn't bind NvFBC context to current thread: " << func.nvFBCGetLastErrorStr(handle);
}
this->handle = handle;
}
~ctx_t() {
NVFBC_RELEASE_CONTEXT_PARAMS params { NVFBC_RELEASE_CONTEXT_PARAMS_VER };
if(func.nvFBCReleaseContext(handle, &params)) {
BOOST_LOG(error) << "Couldn't release NvFBC context from current thread: " << func.nvFBCGetLastErrorStr(handle);
}
}
NVFBC_SESSION_HANDLE handle;
};
class handle_t {
enum flag_e {
SESSION_HANDLE,
SESSION_CAPTURE,
MAX_FLAGS,
};
public:
handle_t() = default;
handle_t(handle_t &&other) : handle_flags { other.handle_flags }, handle { other.handle } {
other.handle_flags.reset();
}
handle_t &operator=(handle_t &&other) {
std::swap(handle_flags, other.handle_flags);
std::swap(handle, other.handle);
return *this;
}
static std::optional<handle_t> make() {
NVFBC_CREATE_HANDLE_PARAMS params { NVFBC_CREATE_HANDLE_PARAMS_VER };
handle_t handle;
auto status = func.nvFBCCreateHandle(&handle.handle, &params);
if(status) {
BOOST_LOG(error) << "Failed to create session: "sv << handle.last_error();
return std::nullopt;
}
handle.handle_flags[SESSION_HANDLE] = true;
return std::move(handle);
}
const char *last_error() {
return func.nvFBCGetLastErrorStr(handle);
}
std::optional<NVFBC_GET_STATUS_PARAMS> status() {
NVFBC_GET_STATUS_PARAMS params { NVFBC_GET_STATUS_PARAMS_VER };
auto status = func.nvFBCGetStatus(handle, &params);
if(status) {
BOOST_LOG(error) << "Failed to get NvFBC status: "sv << last_error();
return std::nullopt;
}
return params;
}
int capture(NVFBC_CREATE_CAPTURE_SESSION_PARAMS &capture_params) {
if(func.nvFBCCreateCaptureSession(handle, &capture_params)) {
BOOST_LOG(error) << "Failed to start capture session: "sv << last_error();
return -1;
}
handle_flags[SESSION_CAPTURE] = true;
NVFBC_TOCUDA_SETUP_PARAMS setup_params {
NVFBC_TOCUDA_SETUP_PARAMS_VER,
NVFBC_BUFFER_FORMAT_BGRA,
};
if(func.nvFBCToCudaSetUp(handle, &setup_params)) {
BOOST_LOG(error) << "Failed to setup cuda interop with nvFBC: "sv << last_error();
return -1;
}
return 0;
}
int stop() {
if(!handle_flags[SESSION_CAPTURE]) {
return 0;
}
NVFBC_DESTROY_CAPTURE_SESSION_PARAMS params { NVFBC_DESTROY_CAPTURE_SESSION_PARAMS_VER };
if(func.nvFBCDestroyCaptureSession(handle, &params)) {
BOOST_LOG(error) << "Couldn't destroy capture session: "sv << last_error();
return -1;
}
handle_flags[SESSION_CAPTURE] = false;
return 0;
}
int reset() {
if(!handle_flags[SESSION_HANDLE]) {
return 0;
}
stop();
NVFBC_DESTROY_HANDLE_PARAMS params { NVFBC_DESTROY_HANDLE_PARAMS_VER };
if(func.nvFBCDestroyHandle(handle, &params)) {
BOOST_LOG(error) << "Couldn't destroy session handle: "sv << func.nvFBCGetLastErrorStr(handle);
}
handle_flags[SESSION_HANDLE] = false;
return 0;
}
~handle_t() {
reset();
}
std::bitset<MAX_FLAGS> handle_flags;
NVFBC_SESSION_HANDLE handle;
};
class display_t : public platf::display_t {
public:
int init(const std::string_view &display_name, int framerate) {
auto handle = handle_t::make();
if(!handle) {
return -1;
}
ctx_t ctx { handle->handle };
auto status_params = handle->status();
if(!status_params) {
return -1;
}
int streamedMonitor = -1;
if(!display_name.empty()) {
if(status_params->bXRandRAvailable) {
auto monitor_nr = util::from_view(display_name);
if(monitor_nr < 0 || monitor_nr >= status_params->dwOutputNum) {
BOOST_LOG(warning) << "Can't stream monitor ["sv << monitor_nr << "], it needs to be between [0] and ["sv << status_params->dwOutputNum - 1 << "], defaulting to virtual desktop"sv;
}
else {
streamedMonitor = monitor_nr;
}
}
else {
BOOST_LOG(warning) << "XrandR not available, streaming entire virtual desktop"sv;
}
}
delay = std::chrono::nanoseconds { 1s } / framerate;
capture_params = NVFBC_CREATE_CAPTURE_SESSION_PARAMS { NVFBC_CREATE_CAPTURE_SESSION_PARAMS_VER };
capture_params.eCaptureType = NVFBC_CAPTURE_SHARED_CUDA;
capture_params.bDisableAutoModesetRecovery = nv_bool(true);
capture_params.dwSamplingRateMs = 1000 /* ms */ / framerate;
if(streamedMonitor != -1) {
auto &output = status_params->outputs[streamedMonitor];
width = output.trackedBox.w;
height = output.trackedBox.h;
offset_x = output.trackedBox.x;
offset_y = output.trackedBox.y;
capture_params.eTrackingType = NVFBC_TRACKING_OUTPUT;
capture_params.dwOutputId = output.dwId;
}
else {
capture_params.eTrackingType = NVFBC_TRACKING_SCREEN;
width = status_params->screenSize.w;
height = status_params->screenSize.h;
}
env_width = status_params->screenSize.w;
env_height = status_params->screenSize.h;
this->handle = std::move(*handle);
return 0;
}
platf::capture_e capture(snapshot_cb_t &&snapshot_cb, std::shared_ptr<platf::img_t> img, bool *cursor) override {
auto next_frame = std::chrono::steady_clock::now();
// Force display_t::capture to initialize handle_t::capture
cursor_visible = !*cursor;
ctx_t ctx { handle.handle };
auto fg = util::fail_guard([&]() {
handle.reset();
});
while(img) {
auto now = std::chrono::steady_clock::now();
if(next_frame > now) {
std::this_thread::sleep_for((next_frame - now) / 3 * 2);
}
while(next_frame > now) {
std::this_thread::sleep_for(1ns);
now = std::chrono::steady_clock::now();
}
next_frame = now + delay;
auto status = snapshot(img.get(), 150ms, *cursor);
switch(status) {
case platf::capture_e::reinit:
case platf::capture_e::error:
return status;
case platf::capture_e::timeout:
std::this_thread::sleep_for(1ms);
continue;
case platf::capture_e::ok:
img = snapshot_cb(img);
break;
default:
BOOST_LOG(error) << "Unrecognized capture status ["sv << (int)status << ']';
return status;
}
}
return platf::capture_e::ok;
}
// Reinitialize the capture session.
platf::capture_e reinit(bool cursor) {
if(handle.stop()) {
return platf::capture_e::error;
}
cursor_visible = cursor;
if(cursor) {
capture_params.bPushModel = nv_bool(false);
capture_params.bWithCursor = nv_bool(true);
capture_params.bAllowDirectCapture = nv_bool(false);
}
else {
capture_params.bPushModel = nv_bool(true);
capture_params.bWithCursor = nv_bool(false);
capture_params.bAllowDirectCapture = nv_bool(true);
}
if(handle.capture(capture_params)) {
return platf::capture_e::error;
}
// If trying to capture directly, test if it actually does.
if(capture_params.bAllowDirectCapture) {
CUdeviceptr device_ptr;
NVFBC_FRAME_GRAB_INFO info;
NVFBC_TOCUDA_GRAB_FRAME_PARAMS grab {
NVFBC_TOCUDA_GRAB_FRAME_PARAMS_VER,
NVFBC_TOCUDA_GRAB_FLAGS_NOWAIT,
&device_ptr,
&info,
0,
};
// Direct Capture may fail the first few times, even if it's possible
for(int x = 0; x < 3; ++x) {
if(auto status = func.nvFBCToCudaGrabFrame(handle.handle, &grab)) {
if(status == NVFBC_ERR_MUST_RECREATE) {
return platf::capture_e::reinit;
}
BOOST_LOG(error) << "Couldn't capture nvFramebuffer: "sv << handle.last_error();
return platf::capture_e::error;
}
if(info.bDirectCapture) {
break;
}
BOOST_LOG(debug) << "Direct capture failed attempt ["sv << x << ']';
}
if(!info.bDirectCapture) {
BOOST_LOG(debug) << "Direct capture failed, trying the extra copy method"sv;
// Direct capture failed
capture_params.bPushModel = nv_bool(false);
capture_params.bWithCursor = nv_bool(false);
capture_params.bAllowDirectCapture = nv_bool(false);
if(handle.stop() || handle.capture(capture_params)) {
return platf::capture_e::error;
}
}
}
return platf::capture_e::ok;
}
platf::capture_e snapshot(platf::img_t *img, std::chrono::milliseconds timeout, bool cursor) {
if(cursor != cursor_visible) {
auto status = reinit(cursor);
if(status != platf::capture_e::ok) {
return status;
}
}
CUdeviceptr device_ptr;
NVFBC_FRAME_GRAB_INFO info;
NVFBC_TOCUDA_GRAB_FRAME_PARAMS grab {
NVFBC_TOCUDA_GRAB_FRAME_PARAMS_VER,
NVFBC_TOCUDA_GRAB_FLAGS_NOWAIT,
&device_ptr,
&info,
(std::uint32_t)timeout.count(),
};
if(auto status = func.nvFBCToCudaGrabFrame(handle.handle, &grab)) {
if(status == NVFBC_ERR_MUST_RECREATE) {
return platf::capture_e::reinit;
}
BOOST_LOG(error) << "Couldn't capture nvFramebuffer: "sv << handle.last_error();
return platf::capture_e::error;
}
if(((img_t *)img)->tex.copy((std::uint8_t *)device_ptr, img->height, img->row_pitch)) {
return platf::capture_e::error;
}
return platf::capture_e::ok;
}
std::shared_ptr<platf::hwdevice_t> make_hwdevice(platf::pix_fmt_e pix_fmt) override {
return ::cuda::make_hwdevice(width, height, true);
}
std::shared_ptr<platf::img_t> alloc_img() override {
auto img = std::make_shared<cuda::img_t>();
img->data = nullptr;
img->width = width;
img->height = height;
img->pixel_pitch = 4;
img->row_pitch = img->width * img->pixel_pitch;
auto tex_opt = tex_t::make(height, width * img->pixel_pitch);
if(!tex_opt) {
return nullptr;
}
img->tex = std::move(*tex_opt);
return img;
};
int dummy_img(platf::img_t *) override {
return 0;
}
std::chrono::nanoseconds delay;
bool cursor_visible;
handle_t handle;
NVFBC_CREATE_CAPTURE_SESSION_PARAMS capture_params;
};
} // namespace nvfbc
} // namespace cuda
namespace platf {
std::shared_ptr<display_t> nvfbc_display(mem_type_e hwdevice_type, const std::string &display_name, int framerate) {
if(hwdevice_type != mem_type_e::cuda) {
BOOST_LOG(error) << "Could not initialize nvfbc display with the given hw device type"sv;
return nullptr;
}
auto display = std::make_shared<cuda::nvfbc::display_t>();
if(display->init(display_name, framerate)) {
return nullptr;
}
return display;
}
std::vector<std::string> nvfbc_display_names() {
if(cuda::init() || cuda::nvfbc::init()) {
return {};
}
std::vector<std::string> display_names;
auto handle = cuda::nvfbc::handle_t::make();
if(!handle) {
return {};
}
auto status_params = handle->status();
if(!status_params) {
return {};
}
if(!status_params->bIsCapturePossible) {
BOOST_LOG(error) << "NVidia driver doesn't support NvFBC screencasting"sv;
}
BOOST_LOG(info) << "Found ["sv << status_params->dwOutputNum << "] outputs"sv;
BOOST_LOG(info) << "Virtual Desktop: "sv << status_params->screenSize.w << 'x' << status_params->screenSize.h;
BOOST_LOG(info) << "XrandR: "sv << (status_params->bXRandRAvailable ? "available"sv : "unavailable"sv);
for(auto x = 0; x < status_params->dwOutputNum; ++x) {
auto &output = status_params->outputs[x];
BOOST_LOG(info) << "-- Output --"sv;
BOOST_LOG(debug) << " ID: "sv << output.dwId;
BOOST_LOG(debug) << " Name: "sv << output.name;
BOOST_LOG(info) << " Resolution: "sv << output.trackedBox.w << 'x' << output.trackedBox.h;
BOOST_LOG(info) << " Offset: "sv << output.trackedBox.x << 'x' << output.trackedBox.y;
display_names.emplace_back(std::to_string(x));
}
return display_names;
}
} // namespace platf

View File

@@ -0,0 +1,331 @@
// #include <algorithm>
#include <helper_math.h>
#include <limits>
#include <memory>
#include <optional>
#include <string_view>
#include "cuda.h"
using namespace std::literals;
#define SUNSHINE_STRINGVIEW_HELPER(x) x##sv
#define SUNSHINE_STRINGVIEW(x) SUNSHINE_STRINGVIEW_HELPER(x)
#define CU_CHECK(x, y) \
if(check((x), SUNSHINE_STRINGVIEW(y ": "))) return -1
#define CU_CHECK_VOID(x, y) \
if(check((x), SUNSHINE_STRINGVIEW(y ": "))) return;
#define CU_CHECK_PTR(x, y) \
if(check((x), SUNSHINE_STRINGVIEW(y ": "))) return nullptr;
#define CU_CHECK_OPT(x, y) \
if(check((x), SUNSHINE_STRINGVIEW(y ": "))) return std::nullopt;
#define CU_CHECK_IGNORE(x, y) \
check((x), SUNSHINE_STRINGVIEW(y ": "))
using namespace std::literals;
//////////////////// Special desclarations
/**
* NVCC segfaults when including <chrono>
* Therefore, some declarations need to be added explicitely
*/
namespace platf {
struct img_t {
public:
std::uint8_t *data {};
std::int32_t width {};
std::int32_t height {};
std::int32_t pixel_pitch {};
std::int32_t row_pitch {};
virtual ~img_t() = default;
};
} // namespace platf
namespace video {
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;
};
struct __attribute__((__aligned__(16))) color_extern_t {
__float4 color_vec_y;
__float4 color_vec_u;
__float4 color_vec_v;
__float2 range_y;
__float2 range_uv;
};
static_assert(sizeof(video::color_t) == sizeof(video::color_extern_t), "color matrix struct mismatch");
extern color_t colors[4];
} // namespace video
//////////////////// End special declarations
namespace cuda {
auto constexpr INVALID_TEXTURE = std::numeric_limits<cudaTextureObject_t>::max();
template<class T>
inline T div_align(T l, T r) {
return (l + r - 1) / r;
}
void pass_error(const std::string_view &sv, const char *name, const char *description);
inline static int check(cudaError_t result, const std::string_view &sv) {
if(result) {
auto name = cudaGetErrorName(result);
auto description = cudaGetErrorString(result);
pass_error(sv, name, description);
return -1;
}
return 0;
}
template<class T>
ptr_t make_ptr() {
void *p;
CU_CHECK_PTR(cudaMalloc(&p, sizeof(T)), "Couldn't allocate color matrix");
ptr_t ptr { p };
return ptr;
}
void freeCudaPtr_t::operator()(void *ptr) {
CU_CHECK_IGNORE(cudaFree(ptr), "Couldn't free cuda device pointer");
}
void freeCudaStream_t::operator()(cudaStream_t ptr) {
CU_CHECK_IGNORE(cudaStreamDestroy(ptr), "Couldn't free cuda stream");
}
stream_t make_stream(int flags) {
cudaStream_t stream;
if(!flags) {
CU_CHECK_PTR(cudaStreamCreate(&stream), "Couldn't create cuda stream");
}
else {
CU_CHECK_PTR(cudaStreamCreateWithFlags(&stream, flags), "Couldn't create cuda stream with flags");
}
return stream_t { stream };
}
inline __device__ float3 bgra_to_rgb(uchar4 vec) {
return make_float3((float)vec.z, (float)vec.y, (float)vec.x);
}
inline __device__ float3 bgra_to_rgb(float4 vec) {
return make_float3(vec.z, vec.y, vec.x);
}
inline __device__ float2 calcUV(float3 pixel, const video::color_t *const color_matrix) {
float4 vec_u = color_matrix->color_vec_u;
float4 vec_v = color_matrix->color_vec_v;
float u = dot(pixel, make_float3(vec_u)) + vec_u.w;
float v = dot(pixel, make_float3(vec_v)) + vec_v.w;
u = u * color_matrix->range_uv.x + color_matrix->range_uv.y;
v = (v * color_matrix->range_uv.x + color_matrix->range_uv.y) * 224.0f / 256.0f + 0.0625f;
return make_float2(u, v);
}
inline __device__ float calcY(float3 pixel, const video::color_t *const color_matrix) {
float4 vec_y = color_matrix->color_vec_y;
return (dot(pixel, make_float3(vec_y)) + vec_y.w) * color_matrix->range_y.x + color_matrix->range_y.y;
}
__global__ void RGBA_to_NV12(
cudaTextureObject_t srcImage, std::uint8_t *dstY, std::uint8_t *dstUV,
std::uint32_t dstPitchY, std::uint32_t dstPitchUV,
float scale, const viewport_t viewport, const video::color_t *const color_matrix) {
int idX = (threadIdx.x + blockDim.x * blockIdx.x) * 2;
int idY = (threadIdx.y + blockDim.y * blockIdx.y);
if(idX >= viewport.width) return;
if(idY >= viewport.height) return;
float x = idX * scale;
float y = idY * scale;
idX += viewport.offsetX;
idY += viewport.offsetY;
dstY = dstY + idX + idY * dstPitchY;
dstUV = dstUV + idX + (idY / 2 * dstPitchUV);
float3 rgb_l = bgra_to_rgb(tex2D<float4>(srcImage, x, y));
float3 rgb_r = bgra_to_rgb(tex2D<float4>(srcImage, x + scale, y));
float2 uv = calcUV((rgb_l + rgb_r) * 0.5f, color_matrix) * 256.0f;
dstUV[0] = uv.x;
dstUV[1] = uv.y;
dstY[0] = calcY(rgb_l, color_matrix) * 245.0f; // 245.0f is a magic number to ensure slight changes in luminosity are more visisble
dstY[1] = calcY(rgb_r, color_matrix) * 245.0f; // 245.0f is a magic number to ensure slight changes in luminosity are more visisble
}
int tex_t::copy(std::uint8_t *src, int height, int pitch) {
CU_CHECK(cudaMemcpy2DToArray(array, 0, 0, src, pitch, pitch, height, cudaMemcpyDeviceToDevice), "Couldn't copy to cuda array from deviceptr");
return 0;
}
std::optional<tex_t> tex_t::make(int height, int pitch) {
tex_t tex;
auto format = cudaCreateChannelDesc<uchar4>();
CU_CHECK_OPT(cudaMallocArray(&tex.array, &format, pitch, height, cudaArrayDefault), "Couldn't allocate cuda array");
cudaResourceDesc res {};
res.resType = cudaResourceTypeArray;
res.res.array.array = tex.array;
cudaTextureDesc desc {};
desc.readMode = cudaReadModeNormalizedFloat;
desc.filterMode = cudaFilterModePoint;
desc.normalizedCoords = false;
std::fill_n(std::begin(desc.addressMode), 2, cudaAddressModeClamp);
CU_CHECK_OPT(cudaCreateTextureObject(&tex.texture.point, &res, &desc, nullptr), "Couldn't create cuda texture that uses point interpolation");
desc.filterMode = cudaFilterModeLinear;
CU_CHECK_OPT(cudaCreateTextureObject(&tex.texture.linear, &res, &desc, nullptr), "Couldn't create cuda texture that uses linear interpolation");
return std::move(tex);
}
tex_t::tex_t() : array {}, texture { INVALID_TEXTURE } {}
tex_t::tex_t(tex_t &&other) : array { other.array }, texture { other.texture } {
other.array = 0;
other.texture.point = INVALID_TEXTURE;
other.texture.linear = INVALID_TEXTURE;
}
tex_t &tex_t::operator=(tex_t &&other) {
std::swap(array, other.array);
std::swap(texture, other.texture);
return *this;
}
tex_t::~tex_t() {
if(texture.point != INVALID_TEXTURE) {
CU_CHECK_IGNORE(cudaDestroyTextureObject(texture.point), "Couldn't deallocate cuda texture that uses point interpolation");
texture.point = INVALID_TEXTURE;
}
if(texture.linear != INVALID_TEXTURE) {
CU_CHECK_IGNORE(cudaDestroyTextureObject(texture.linear), "Couldn't deallocate cuda texture that uses linear interpolation");
texture.linear = INVALID_TEXTURE;
}
if(array) {
CU_CHECK_IGNORE(cudaFreeArray(array), "Couldn't deallocate cuda array");
array = cudaArray_t {};
}
}
sws_t::sws_t(int in_width, int in_height, int out_width, int out_height, int pitch, int threadsPerBlock, ptr_t &&color_matrix)
: threadsPerBlock { threadsPerBlock }, color_matrix { std::move(color_matrix) } {
// Ensure aspect ratio is maintained
auto scalar = std::fminf(out_width / (float)in_width, out_height / (float)in_height);
auto out_width_f = in_width * scalar;
auto out_height_f = in_height * scalar;
// result is always positive
auto offsetX_f = (out_width - out_width_f) / 2;
auto offsetY_f = (out_height - out_height_f) / 2;
viewport.width = out_width_f;
viewport.height = out_height_f;
viewport.offsetX = offsetX_f;
viewport.offsetY = offsetY_f;
scale = 1.0f / scalar;
}
std::optional<sws_t> sws_t::make(int in_width, int in_height, int out_width, int out_height, int pitch) {
cudaDeviceProp props;
int device;
CU_CHECK_OPT(cudaGetDevice(&device), "Couldn't get cuda device");
CU_CHECK_OPT(cudaGetDeviceProperties(&props, device), "Couldn't get cuda device properties");
auto ptr = make_ptr<video::color_t>();
if(!ptr) {
return std::nullopt;
}
return std::make_optional<sws_t>(in_width, in_height, out_width, out_height, pitch, props.maxThreadsPerMultiProcessor / props.maxBlocksPerMultiProcessor, std::move(ptr));
}
int sws_t::convert(std::uint8_t *Y, std::uint8_t *UV, std::uint32_t pitchY, std::uint32_t pitchUV, cudaTextureObject_t texture, stream_t::pointer stream) {
return convert(Y, UV, pitchY, pitchUV, texture, stream, viewport);
}
int sws_t::convert(std::uint8_t *Y, std::uint8_t *UV, std::uint32_t pitchY, std::uint32_t pitchUV, cudaTextureObject_t texture, stream_t::pointer stream, const viewport_t &viewport) {
int threadsX = viewport.width / 2;
int threadsY = viewport.height;
dim3 block(threadsPerBlock);
dim3 grid(div_align(threadsX, threadsPerBlock), threadsY);
RGBA_to_NV12<<<grid, block, 0, stream>>>(texture, Y, UV, pitchY, pitchUV, scale, viewport, (video::color_t *)color_matrix.get());
return CU_CHECK_IGNORE(cudaGetLastError(), "RGBA_to_NV12 failed");
}
void sws_t::set_colorspace(std::uint32_t colorspace, std::uint32_t color_range) {
video::color_t *color_p;
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:
color_p = &video::colors[0];
};
if(color_range > 1) {
// Full range
++color_p;
}
CU_CHECK_IGNORE(cudaMemcpy(color_matrix.get(), color_p, sizeof(video::color_t), cudaMemcpyHostToDevice), "Couldn't copy color matrix to cuda");
}
int sws_t::load_ram(platf::img_t &img, cudaArray_t array) {
return CU_CHECK_IGNORE(cudaMemcpy2DToArray(array, 0, 0, img.data, img.row_pitch, img.width * img.pixel_pitch, img.height, cudaMemcpyHostToDevice), "Couldn't copy to cuda array");
}
} // namespace cuda

View File

@@ -0,0 +1,107 @@
#if !defined(SUNSHINE_PLATFORM_CUDA_H) && defined(SUNSHINE_BUILD_CUDA)
#define SUNSHINE_PLATFORM_CUDA_H
#include <memory>
#include <optional>
#include <string>
#include <vector>
namespace platf {
class hwdevice_t;
class img_t;
} // namespace platf
namespace cuda {
namespace nvfbc {
std::vector<std::string> display_names();
}
std::shared_ptr<platf::hwdevice_t> make_hwdevice(int width, int height, bool vram);
int init();
} // namespace cuda
typedef struct cudaArray *cudaArray_t;
#if !defined(__CUDACC__)
typedef struct CUstream_st *cudaStream_t;
typedef unsigned long long cudaTextureObject_t;
#else /* defined(__CUDACC__) */
typedef __location__(device_builtin) struct CUstream_st *cudaStream_t;
typedef __location__(device_builtin) unsigned long long cudaTextureObject_t;
#endif /* !defined(__CUDACC__) */
namespace cuda {
class freeCudaPtr_t {
public:
void operator()(void *ptr);
};
class freeCudaStream_t {
public:
void operator()(cudaStream_t ptr);
};
using ptr_t = std::unique_ptr<void, freeCudaPtr_t>;
using stream_t = std::unique_ptr<CUstream_st, freeCudaStream_t>;
stream_t make_stream(int flags = 0);
struct viewport_t {
int width, height;
int offsetX, offsetY;
};
class tex_t {
public:
static std::optional<tex_t> make(int height, int pitch);
tex_t();
tex_t(tex_t &&);
tex_t &operator=(tex_t &&other);
~tex_t();
int copy(std::uint8_t *src, int height, int pitch);
cudaArray_t array;
struct texture {
cudaTextureObject_t point;
cudaTextureObject_t linear;
} texture;
};
class sws_t {
public:
sws_t() = default;
sws_t(int in_width, int in_height, int out_width, int out_height, int pitch, int threadsPerBlock, ptr_t &&color_matrix);
/**
* in_width, in_height -- The width and height of the captured image in pixels
* out_width, out_height -- the width and height of the NV12 image in pixels
*
* pitch -- The size of a single row of pixels in bytes
*/
static std::optional<sws_t> make(int in_width, int in_height, int out_width, int out_height, int pitch);
// Converts loaded image into a CUDevicePtr
int convert(std::uint8_t *Y, std::uint8_t *UV, std::uint32_t pitchY, std::uint32_t pitchUV, cudaTextureObject_t texture, stream_t::pointer stream);
int convert(std::uint8_t *Y, std::uint8_t *UV, std::uint32_t pitchY, std::uint32_t pitchUV, cudaTextureObject_t texture, stream_t::pointer stream, const viewport_t &viewport);
void set_colorspace(std::uint32_t colorspace, std::uint32_t color_range);
int load_ram(platf::img_t &img, cudaArray_t array);
ptr_t color_matrix;
int threadsPerBlock;
viewport_t viewport;
float scale;
};
} // namespace cuda
#endif

View File

@@ -1,454 +0,0 @@
//
// Created by loki on 6/21/19.
//
#include "sunshine/platform/common.h"
#include <fstream>
#include <arpa/inet.h>
#include <ifaddrs.h>
#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"
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 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;
/*
* Last X (NOT the streamed monitor!) size.
* This way we can trigger reinitialization if the dimensions changed while streaming
*/
int lastWidth, lastHeight;
x11_attr_t() : xdisplay { XOpenDisplay(nullptr) }, xwindow {}, xattr {} {
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;
if(streamedMonitor >= output) {
BOOST_LOG(error) << "Could not stream display number ["sv << streamedMonitor << "], there are only ["sv << output << "] displays."sv;
return -1;
}
output_info_t out_info { XRRGetOutputInfo(xdisplay.get(), screenr.get(), screenr->outputs[streamedMonitor]) };
if(!out_info || out_info->connection != RR_Connected) {
BOOST_LOG(error) << "Could not stream selected display because it doesn't seem to be connected"sv;
return -1;
}
crtc_info_t crt_info { XRRGetCrtcInfo(xdisplay.get(), screenr.get(), out_info->crtc) };
BOOST_LOG(info)
<< "Streaming display: "sv << out_info->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;
}
lastWidth = xattr.width;
lastHeight = 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 != lastWidth || xattr.height != lastHeight) {
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>();
}
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() : 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, std::chrono::milliseconds timeout, bool cursor) override {
//The whole X server changed, so we gotta reinit everything
if(xattr.width != lastWidth || xattr.height != lastHeight) {
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::dev_type_e hwdevice_type) {
if(hwdevice_type != platf::dev_type_e::none) {
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>();
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>();
if(x11_disp->init()) {
return nullptr;
}
return x11_disp;
}
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::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;
}
void freeImage(XImage *p) {
XDestroyImage(p);
}
void freeX(XFixesCursorImage *p) {
XFree(p);
}
} // namespace platf

View File

@@ -0,0 +1,866 @@
#include "graphics.h"
#include "sunshine/video.h"
#include <fcntl.h>
// I want to have as little build dependencies as possible
// There aren't that many DRM_FORMAT I need to use, so define them here
//
// They aren't likely to change any time soon.
#define fourcc_code(a, b, c, d) ((std::uint32_t)(a) | ((std::uint32_t)(b) << 8) | \
((std::uint32_t)(c) << 16) | ((std::uint32_t)(d) << 24))
#define fourcc_mod_code(vendor, val) ((((uint64_t)vendor) << 56) | ((val)&0x00ffffffffffffffULL))
#define DRM_FORMAT_R8 fourcc_code('R', '8', ' ', ' ') /* [7:0] R */
#define DRM_FORMAT_GR88 fourcc_code('G', 'R', '8', '8') /* [15:0] G:R 8:8 little endian */
#define DRM_FORMAT_ARGB8888 fourcc_code('A', 'R', '2', '4') /* [31:0] A:R:G:B 8:8:8:8 little endian */
#define DRM_FORMAT_XRGB8888 fourcc_code('X', 'R', '2', '4') /* [31:0] x:R:G:B 8:8:8:8 little endian */
#define DRM_FORMAT_XBGR8888 fourcc_code('X', 'B', '2', '4') /* [31:0] x:B:G:R 8:8:8:8 little endian */
#define DRM_FORMAT_MOD_INVALID fourcc_mod_code(0, ((1ULL << 56) - 1))
#define SUNSHINE_SHADERS_DIR SUNSHINE_ASSETS_DIR "/shaders/opengl"
using namespace std::literals;
namespace gl {
GladGLContext ctx;
void drain_errors(const std::string_view &prefix) {
GLenum err;
while((err = ctx.GetError()) != GL_NO_ERROR) {
BOOST_LOG(error) << "GL: "sv << prefix << ": ["sv << util::hex(err).to_string_view() << ']';
}
}
tex_t::~tex_t() {
if(!size() == 0) {
ctx.DeleteTextures(size(), begin());
}
}
tex_t tex_t::make(std::size_t count) {
tex_t textures { count };
ctx.GenTextures(textures.size(), textures.begin());
float color[] = { 0.0f, 0.0f, 0.0f, 1.0f };
for(auto tex : textures) {
gl::ctx.BindTexture(GL_TEXTURE_2D, tex);
gl::ctx.TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); // x
gl::ctx.TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); // y
gl::ctx.TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
gl::ctx.TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
gl::ctx.TexParameterfv(GL_TEXTURE_2D, GL_TEXTURE_BORDER_COLOR, color);
}
return textures;
}
frame_buf_t::~frame_buf_t() {
if(begin()) {
ctx.DeleteFramebuffers(size(), begin());
}
}
frame_buf_t frame_buf_t::make(std::size_t count) {
frame_buf_t frame_buf { count };
ctx.GenFramebuffers(frame_buf.size(), frame_buf.begin());
return frame_buf;
}
void frame_buf_t::copy(int id, int texture, int offset_x, int offset_y, int width, int height) {
gl::ctx.BindFramebuffer(GL_FRAMEBUFFER, (*this)[id]);
gl::ctx.ReadBuffer(GL_COLOR_ATTACHMENT0 + id);
gl::ctx.BindTexture(GL_TEXTURE_2D, texture);
gl::ctx.CopyTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, offset_x, offset_y, width, height);
}
std::string shader_t::err_str() {
int length;
ctx.GetShaderiv(handle(), GL_INFO_LOG_LENGTH, &length);
std::string string;
string.resize(length);
ctx.GetShaderInfoLog(handle(), length, &length, string.data());
string.resize(length - 1);
return string;
}
util::Either<shader_t, std::string> shader_t::compile(const std::string_view &source, GLenum type) {
shader_t shader;
auto data = source.data();
GLint length = source.length();
shader._shader.el = ctx.CreateShader(type);
ctx.ShaderSource(shader.handle(), 1, &data, &length);
ctx.CompileShader(shader.handle());
int status = 0;
ctx.GetShaderiv(shader.handle(), GL_COMPILE_STATUS, &status);
if(!status) {
return shader.err_str();
}
return shader;
}
GLuint shader_t::handle() const {
return _shader.el;
}
buffer_t buffer_t::make(util::buffer_t<GLint> &&offsets, const char *block, const std::string_view &data) {
buffer_t buffer;
buffer._block = block;
buffer._size = data.size();
buffer._offsets = std::move(offsets);
ctx.GenBuffers(1, &buffer._buffer.el);
ctx.BindBuffer(GL_UNIFORM_BUFFER, buffer.handle());
ctx.BufferData(GL_UNIFORM_BUFFER, data.size(), (const std::uint8_t *)data.data(), GL_DYNAMIC_DRAW);
return buffer;
}
GLuint buffer_t::handle() const {
return _buffer.el;
}
const char *buffer_t::block() const {
return _block;
}
void buffer_t::update(const std::string_view &view, std::size_t offset) {
ctx.BindBuffer(GL_UNIFORM_BUFFER, handle());
ctx.BufferSubData(GL_UNIFORM_BUFFER, offset, view.size(), (const void *)view.data());
}
void buffer_t::update(std::string_view *members, std::size_t count, std::size_t offset) {
util::buffer_t<std::uint8_t> buffer { _size };
for(int x = 0; x < count; ++x) {
auto val = members[x];
std::copy_n((const std::uint8_t *)val.data(), val.size(), &buffer[_offsets[x]]);
}
update(util::view(buffer.begin(), buffer.end()), offset);
}
std::string program_t::err_str() {
int length;
ctx.GetProgramiv(handle(), GL_INFO_LOG_LENGTH, &length);
std::string string;
string.resize(length);
ctx.GetShaderInfoLog(handle(), length, &length, string.data());
string.resize(length - 1);
return string;
}
util::Either<program_t, std::string> program_t::link(const shader_t &vert, const shader_t &frag) {
program_t program;
program._program.el = ctx.CreateProgram();
ctx.AttachShader(program.handle(), vert.handle());
ctx.AttachShader(program.handle(), frag.handle());
// p_handle stores a copy of the program handle, since program will be moved before
// the fail guard funcion is called.
auto fg = util::fail_guard([p_handle = program.handle(), &vert, &frag]() {
ctx.DetachShader(p_handle, vert.handle());
ctx.DetachShader(p_handle, frag.handle());
});
ctx.LinkProgram(program.handle());
int status = 0;
ctx.GetProgramiv(program.handle(), GL_LINK_STATUS, &status);
if(!status) {
return program.err_str();
}
return program;
}
void program_t::bind(const buffer_t &buffer) {
ctx.UseProgram(handle());
auto i = ctx.GetUniformBlockIndex(handle(), buffer.block());
ctx.BindBufferBase(GL_UNIFORM_BUFFER, i, buffer.handle());
}
std::optional<buffer_t> program_t::uniform(const char *block, std::pair<const char *, std::string_view> *members, std::size_t count) {
auto i = ctx.GetUniformBlockIndex(handle(), block);
if(i == GL_INVALID_INDEX) {
BOOST_LOG(error) << "Couldn't find index of ["sv << block << ']';
return std::nullopt;
}
int size;
ctx.GetActiveUniformBlockiv(handle(), i, GL_UNIFORM_BLOCK_DATA_SIZE, &size);
bool error_flag = false;
util::buffer_t<GLint> offsets { count };
auto indices = (std::uint32_t *)alloca(count * sizeof(std::uint32_t));
auto names = (const char **)alloca(count * sizeof(const char *));
auto names_p = names;
std::for_each_n(members, count, [names_p](auto &member) mutable {
*names_p++ = std::get<0>(member);
});
std::fill_n(indices, count, GL_INVALID_INDEX);
ctx.GetUniformIndices(handle(), count, names, indices);
for(int x = 0; x < count; ++x) {
if(indices[x] == GL_INVALID_INDEX) {
error_flag = true;
BOOST_LOG(error) << "Couldn't find ["sv << block << '.' << members[x].first << ']';
}
}
if(error_flag) {
return std::nullopt;
}
ctx.GetActiveUniformsiv(handle(), count, indices, GL_UNIFORM_OFFSET, offsets.begin());
util::buffer_t<std::uint8_t> buffer { (std::size_t)size };
for(int x = 0; x < count; ++x) {
auto val = std::get<1>(members[x]);
std::copy_n((const std::uint8_t *)val.data(), val.size(), &buffer[offsets[x]]);
}
return buffer_t::make(std::move(offsets), block, std::string_view { (char *)buffer.begin(), buffer.size() });
}
GLuint program_t::handle() const {
return _program.el;
}
} // namespace gl
namespace gbm {
device_destroy_fn device_destroy;
create_device_fn create_device;
int init() {
static void *handle { nullptr };
static bool funcs_loaded = false;
if(funcs_loaded) return 0;
if(!handle) {
handle = dyn::handle({ "libgbm.so.1", "libgbm.so" });
if(!handle) {
return -1;
}
}
std::vector<std::tuple<GLADapiproc *, const char *>> funcs {
{ (GLADapiproc *)&device_destroy, "gbm_device_destroy" },
{ (GLADapiproc *)&create_device, "gbm_create_device" },
};
if(dyn::load(handle, funcs)) {
return -1;
}
funcs_loaded = true;
return 0;
}
} // namespace gbm
namespace egl {
constexpr auto EGL_LINUX_DMA_BUF_EXT = 0x3270;
constexpr auto EGL_LINUX_DRM_FOURCC_EXT = 0x3271;
constexpr auto EGL_DMA_BUF_PLANE0_FD_EXT = 0x3272;
constexpr auto EGL_DMA_BUF_PLANE0_OFFSET_EXT = 0x3273;
constexpr auto EGL_DMA_BUF_PLANE0_PITCH_EXT = 0x3274;
constexpr auto EGL_DMA_BUF_PLANE1_FD_EXT = 0x3275;
constexpr auto EGL_DMA_BUF_PLANE1_OFFSET_EXT = 0x3276;
constexpr auto EGL_DMA_BUF_PLANE1_PITCH_EXT = 0x3277;
constexpr auto EGL_DMA_BUF_PLANE2_FD_EXT = 0x3278;
constexpr auto EGL_DMA_BUF_PLANE2_OFFSET_EXT = 0x3279;
constexpr auto EGL_DMA_BUF_PLANE2_PITCH_EXT = 0x327A;
constexpr auto EGL_DMA_BUF_PLANE3_FD_EXT = 0x3440;
constexpr auto EGL_DMA_BUF_PLANE3_OFFSET_EXT = 0x3441;
constexpr auto EGL_DMA_BUF_PLANE3_PITCH_EXT = 0x3442;
constexpr auto EGL_DMA_BUF_PLANE0_MODIFIER_LO_EXT = 0x3443;
constexpr auto EGL_DMA_BUF_PLANE0_MODIFIER_HI_EXT = 0x3444;
constexpr auto EGL_DMA_BUF_PLANE1_MODIFIER_LO_EXT = 0x3445;
constexpr auto EGL_DMA_BUF_PLANE1_MODIFIER_HI_EXT = 0x3446;
constexpr auto EGL_DMA_BUF_PLANE2_MODIFIER_LO_EXT = 0x3447;
constexpr auto EGL_DMA_BUF_PLANE2_MODIFIER_HI_EXT = 0x3448;
constexpr auto EGL_DMA_BUF_PLANE3_MODIFIER_LO_EXT = 0x3449;
constexpr auto EGL_DMA_BUF_PLANE3_MODIFIER_HI_EXT = 0x344A;
bool fail() {
return eglGetError() != EGL_SUCCESS;
}
display_t make_display(std::variant<gbm::gbm_t::pointer, wl_display *, _XDisplay *> native_display) {
constexpr auto EGL_PLATFORM_GBM_MESA = 0x31D7;
constexpr auto EGL_PLATFORM_WAYLAND_KHR = 0x31D8;
constexpr auto EGL_PLATFORM_X11_KHR = 0x31D5;
int egl_platform;
void *native_display_p;
switch(native_display.index()) {
case 0:
egl_platform = EGL_PLATFORM_GBM_MESA;
native_display_p = std::get<0>(native_display);
break;
case 1:
egl_platform = EGL_PLATFORM_WAYLAND_KHR;
native_display_p = std::get<1>(native_display);
break;
case 2:
egl_platform = EGL_PLATFORM_X11_KHR;
native_display_p = std::get<2>(native_display);
break;
default:
BOOST_LOG(error) << "egl::make_display(): Index ["sv << native_display.index() << "] not implemented"sv;
return nullptr;
}
// native_display.left() equals native_display.right()
display_t display = eglGetPlatformDisplay(egl_platform, native_display_p, nullptr);
if(fail()) {
BOOST_LOG(error) << "Couldn't open EGL display: ["sv << util::hex(eglGetError()).to_string_view() << ']';
return nullptr;
}
int major, minor;
if(!eglInitialize(display.get(), &major, &minor)) {
BOOST_LOG(error) << "Couldn't initialize EGL display: ["sv << util::hex(eglGetError()).to_string_view() << ']';
return nullptr;
}
const char *extension_st = eglQueryString(display.get(), EGL_EXTENSIONS);
const char *version = eglQueryString(display.get(), EGL_VERSION);
const char *vendor = eglQueryString(display.get(), EGL_VENDOR);
const char *apis = eglQueryString(display.get(), EGL_CLIENT_APIS);
BOOST_LOG(debug) << "EGL: ["sv << vendor << "]: version ["sv << version << ']';
BOOST_LOG(debug) << "API's supported: ["sv << apis << ']';
const char *extensions[] {
"EGL_KHR_create_context",
"EGL_KHR_surfaceless_context",
"EGL_EXT_image_dma_buf_import",
};
for(auto ext : extensions) {
if(!std::strstr(extension_st, ext)) {
BOOST_LOG(error) << "Missing extension: ["sv << ext << ']';
return nullptr;
}
}
return display;
}
std::optional<ctx_t> make_ctx(display_t::pointer display) {
constexpr int conf_attr[] {
EGL_RENDERABLE_TYPE, EGL_OPENGL_BIT, EGL_NONE
};
int count;
EGLConfig conf;
if(!eglChooseConfig(display, conf_attr, &conf, 1, &count)) {
BOOST_LOG(error) << "Couldn't set config attributes: ["sv << util::hex(eglGetError()).to_string_view() << ']';
return std::nullopt;
}
if(!eglBindAPI(EGL_OPENGL_API)) {
BOOST_LOG(error) << "Couldn't bind API: ["sv << util::hex(eglGetError()).to_string_view() << ']';
return std::nullopt;
}
constexpr int attr[] {
EGL_CONTEXT_CLIENT_VERSION, 3, EGL_NONE
};
ctx_t ctx { display, eglCreateContext(display, conf, EGL_NO_CONTEXT, attr) };
if(fail()) {
BOOST_LOG(error) << "Couldn't create EGL context: ["sv << util::hex(eglGetError()).to_string_view() << ']';
return std::nullopt;
}
TUPLE_EL_REF(ctx_p, 1, ctx.el);
if(!eglMakeCurrent(display, EGL_NO_SURFACE, EGL_NO_SURFACE, ctx_p)) {
BOOST_LOG(error) << "Couldn't make current display"sv;
return std::nullopt;
}
if(!gladLoadGLContext(&gl::ctx, eglGetProcAddress)) {
BOOST_LOG(error) << "Couldn't load OpenGL library"sv;
return std::nullopt;
}
BOOST_LOG(debug) << "GL: vendor: "sv << gl::ctx.GetString(GL_VENDOR);
BOOST_LOG(debug) << "GL: renderer: "sv << gl::ctx.GetString(GL_RENDERER);
BOOST_LOG(debug) << "GL: version: "sv << gl::ctx.GetString(GL_VERSION);
BOOST_LOG(debug) << "GL: shader: "sv << gl::ctx.GetString(GL_SHADING_LANGUAGE_VERSION);
gl::ctx.PixelStorei(GL_UNPACK_ALIGNMENT, 1);
return ctx;
}
struct plane_attr_t {
EGLAttrib fd;
EGLAttrib offset;
EGLAttrib pitch;
EGLAttrib lo;
EGLAttrib hi;
};
inline plane_attr_t get_plane(std::uint32_t plane_indice) {
switch(plane_indice) {
case 0:
return {
EGL_DMA_BUF_PLANE0_FD_EXT,
EGL_DMA_BUF_PLANE0_OFFSET_EXT,
EGL_DMA_BUF_PLANE0_PITCH_EXT,
EGL_DMA_BUF_PLANE0_MODIFIER_LO_EXT,
EGL_DMA_BUF_PLANE0_MODIFIER_HI_EXT,
};
case 1:
return {
EGL_DMA_BUF_PLANE1_FD_EXT,
EGL_DMA_BUF_PLANE1_OFFSET_EXT,
EGL_DMA_BUF_PLANE1_PITCH_EXT,
EGL_DMA_BUF_PLANE1_MODIFIER_LO_EXT,
EGL_DMA_BUF_PLANE1_MODIFIER_HI_EXT,
};
case 2:
return {
EGL_DMA_BUF_PLANE2_FD_EXT,
EGL_DMA_BUF_PLANE2_OFFSET_EXT,
EGL_DMA_BUF_PLANE2_PITCH_EXT,
EGL_DMA_BUF_PLANE2_MODIFIER_LO_EXT,
EGL_DMA_BUF_PLANE2_MODIFIER_HI_EXT,
};
case 3:
return {
EGL_DMA_BUF_PLANE3_FD_EXT,
EGL_DMA_BUF_PLANE3_OFFSET_EXT,
EGL_DMA_BUF_PLANE3_PITCH_EXT,
EGL_DMA_BUF_PLANE3_MODIFIER_LO_EXT,
EGL_DMA_BUF_PLANE3_MODIFIER_HI_EXT,
};
}
// Avoid warning
return {};
}
std::optional<rgb_t> import_source(display_t::pointer egl_display, const surface_descriptor_t &xrgb) {
EGLAttrib attribs[47];
int atti = 0;
attribs[atti++] = EGL_WIDTH;
attribs[atti++] = xrgb.width;
attribs[atti++] = EGL_HEIGHT;
attribs[atti++] = xrgb.height;
attribs[atti++] = EGL_LINUX_DRM_FOURCC_EXT;
attribs[atti++] = xrgb.fourcc;
for(auto x = 0; x < 4; ++x) {
auto fd = xrgb.fds[x];
if(fd < 0) {
continue;
}
auto plane_attr = get_plane(x);
attribs[atti++] = plane_attr.fd;
attribs[atti++] = fd;
attribs[atti++] = plane_attr.offset;
attribs[atti++] = xrgb.offsets[x];
attribs[atti++] = plane_attr.pitch;
attribs[atti++] = xrgb.pitches[x];
if(xrgb.modifier != DRM_FORMAT_MOD_INVALID) {
attribs[atti++] = plane_attr.lo;
attribs[atti++] = xrgb.modifier & 0xFFFFFFFF;
attribs[atti++] = plane_attr.hi;
attribs[atti++] = xrgb.modifier >> 32;
}
}
attribs[atti++] = EGL_NONE;
rgb_t rgb {
egl_display,
eglCreateImage(egl_display, EGL_NO_CONTEXT, EGL_LINUX_DMA_BUF_EXT, nullptr, attribs),
gl::tex_t::make(1)
};
if(!rgb->xrgb8) {
BOOST_LOG(error) << "Couldn't import RGB Image: "sv << util::hex(eglGetError()).to_string_view();
return std::nullopt;
}
gl::ctx.BindTexture(GL_TEXTURE_2D, rgb->tex[0]);
gl::ctx.EGLImageTargetTexture2DOES(GL_TEXTURE_2D, rgb->xrgb8);
gl::ctx.BindTexture(GL_TEXTURE_2D, 0);
gl_drain_errors;
return rgb;
}
std::optional<nv12_t> import_target(display_t::pointer egl_display, std::array<file_t, nv12_img_t::num_fds> &&fds, const surface_descriptor_t &r8, const surface_descriptor_t &gr88) {
EGLAttrib img_attr_planes[2][13] {
{ EGL_LINUX_DRM_FOURCC_EXT, DRM_FORMAT_R8,
EGL_WIDTH, r8.width,
EGL_HEIGHT, r8.height,
EGL_DMA_BUF_PLANE0_FD_EXT, r8.fds[0],
EGL_DMA_BUF_PLANE0_OFFSET_EXT, r8.offsets[0],
EGL_DMA_BUF_PLANE0_PITCH_EXT, r8.pitches[0],
EGL_NONE },
{ EGL_LINUX_DRM_FOURCC_EXT, DRM_FORMAT_GR88,
EGL_WIDTH, gr88.width,
EGL_HEIGHT, gr88.height,
EGL_DMA_BUF_PLANE0_FD_EXT, r8.fds[0],
EGL_DMA_BUF_PLANE0_OFFSET_EXT, gr88.offsets[0],
EGL_DMA_BUF_PLANE0_PITCH_EXT, gr88.pitches[0],
EGL_NONE },
};
nv12_t nv12 {
egl_display,
eglCreateImage(egl_display, EGL_NO_CONTEXT, EGL_LINUX_DMA_BUF_EXT, nullptr, img_attr_planes[0]),
eglCreateImage(egl_display, EGL_NO_CONTEXT, EGL_LINUX_DMA_BUF_EXT, nullptr, img_attr_planes[1]),
gl::tex_t::make(2),
gl::frame_buf_t::make(2),
std::move(fds)
};
if(!nv12->r8 || !nv12->bg88) {
BOOST_LOG(error) << "Couldn't create KHR Image"sv;
return std::nullopt;
}
gl::ctx.BindTexture(GL_TEXTURE_2D, nv12->tex[0]);
gl::ctx.EGLImageTargetTexture2DOES(GL_TEXTURE_2D, nv12->r8);
gl::ctx.BindTexture(GL_TEXTURE_2D, nv12->tex[1]);
gl::ctx.EGLImageTargetTexture2DOES(GL_TEXTURE_2D, nv12->bg88);
nv12->buf.bind(std::begin(nv12->tex), std::end(nv12->tex));
gl_drain_errors;
return nv12;
}
void sws_t::set_colorspace(std::uint32_t colorspace, std::uint32_t color_range) {
video::color_t *color_p;
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;
}
std::string_view members[] {
util::view(color_p->color_vec_y),
util::view(color_p->color_vec_u),
util::view(color_p->color_vec_v),
util::view(color_p->range_y),
util::view(color_p->range_uv),
};
color_matrix.update(members, sizeof(members) / sizeof(decltype(members[0])));
program[0].bind(color_matrix);
program[1].bind(color_matrix);
}
std::optional<sws_t> sws_t::make(int in_width, int in_height, int out_width, int out_heigth, gl::tex_t &&tex) {
sws_t sws;
sws.serial = std::numeric_limits<std::uint64_t>::max();
// Ensure aspect ratio is maintained
auto scalar = std::fminf(out_width / (float)in_width, out_heigth / (float)in_height);
auto out_width_f = in_width * scalar;
auto out_height_f = in_height * scalar;
// result is always positive
auto offsetX_f = (out_width - out_width_f) / 2;
auto offsetY_f = (out_heigth - out_height_f) / 2;
sws.out_width = out_width_f;
sws.out_height = out_height_f;
sws.in_width = in_width;
sws.in_height = in_height;
sws.offsetX = offsetX_f;
sws.offsetY = offsetY_f;
auto width_i = 1.0f / sws.out_width;
{
const char *sources[] {
SUNSHINE_SHADERS_DIR "/ConvertUV.frag",
SUNSHINE_SHADERS_DIR "/ConvertUV.vert",
SUNSHINE_SHADERS_DIR "/ConvertY.frag",
SUNSHINE_SHADERS_DIR "/Scene.vert",
SUNSHINE_SHADERS_DIR "/Scene.frag",
};
GLenum shader_type[2] {
GL_FRAGMENT_SHADER,
GL_VERTEX_SHADER,
};
constexpr auto count = sizeof(sources) / sizeof(const char *);
util::Either<gl::shader_t, std::string> compiled_sources[count];
bool error_flag = false;
for(int x = 0; x < count; ++x) {
auto &compiled_source = compiled_sources[x];
compiled_source = gl::shader_t::compile(read_file(sources[x]), shader_type[x % 2]);
gl_drain_errors;
if(compiled_source.has_right()) {
BOOST_LOG(error) << sources[x] << ": "sv << compiled_source.right();
error_flag = true;
}
}
if(error_flag) {
return std::nullopt;
}
auto program = gl::program_t::link(compiled_sources[3].left(), compiled_sources[4].left());
if(program.has_right()) {
BOOST_LOG(error) << "GL linker: "sv << program.right();
return std::nullopt;
}
// Cursor - shader
sws.program[2] = std::move(program.left());
program = gl::program_t::link(compiled_sources[1].left(), compiled_sources[0].left());
if(program.has_right()) {
BOOST_LOG(error) << "GL linker: "sv << program.right();
return std::nullopt;
}
// UV - shader
sws.program[1] = std::move(program.left());
program = gl::program_t::link(compiled_sources[3].left(), compiled_sources[2].left());
if(program.has_right()) {
BOOST_LOG(error) << "GL linker: "sv << program.right();
return std::nullopt;
}
// Y - shader
sws.program[0] = std::move(program.left());
}
auto loc_width_i = gl::ctx.GetUniformLocation(sws.program[1].handle(), "width_i");
if(loc_width_i < 0) {
BOOST_LOG(error) << "Couldn't find uniform [width_i]"sv;
return std::nullopt;
}
gl::ctx.UseProgram(sws.program[1].handle());
gl::ctx.Uniform1fv(loc_width_i, 1, &width_i);
auto color_p = &video::colors[0];
std::pair<const char *, std::string_view> members[] {
std::make_pair("color_vec_y", util::view(color_p->color_vec_y)),
std::make_pair("color_vec_u", util::view(color_p->color_vec_u)),
std::make_pair("color_vec_v", util::view(color_p->color_vec_v)),
std::make_pair("range_y", util::view(color_p->range_y)),
std::make_pair("range_uv", util::view(color_p->range_uv)),
};
auto color_matrix = sws.program[0].uniform("ColorMatrix", members, sizeof(members) / sizeof(decltype(members[0])));
if(!color_matrix) {
return std::nullopt;
}
sws.color_matrix = std::move(*color_matrix);
sws.tex = std::move(tex);
sws.cursor_framebuffer = gl::frame_buf_t::make(1);
sws.cursor_framebuffer.bind(&sws.tex[0], &sws.tex[1]);
sws.program[0].bind(sws.color_matrix);
sws.program[1].bind(sws.color_matrix);
gl::ctx.BlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
gl_drain_errors;
return std::move(sws);
}
int sws_t::blank(gl::frame_buf_t &fb, int offsetX, int offsetY, int width, int height) {
auto f = [&]() {
std::swap(offsetX, this->offsetX);
std::swap(offsetY, this->offsetY);
std::swap(width, this->out_width);
std::swap(height, this->out_height);
};
f();
auto fg = util::fail_guard(f);
return convert(fb);
}
std::optional<sws_t> sws_t::make(int in_width, int in_height, int out_width, int out_heigth) {
auto tex = gl::tex_t::make(2);
gl::ctx.BindTexture(GL_TEXTURE_2D, tex[0]);
gl::ctx.TexStorage2D(GL_TEXTURE_2D, 1, GL_RGBA8, in_width, in_height);
return make(in_width, in_height, out_width, out_heigth, std::move(tex));
}
void sws_t::load_ram(platf::img_t &img) {
loaded_texture = tex[0];
gl::ctx.BindTexture(GL_TEXTURE_2D, loaded_texture);
gl::ctx.TexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, img.width, img.height, GL_BGRA, GL_UNSIGNED_BYTE, img.data);
}
void sws_t::load_vram(img_descriptor_t &img, int offset_x, int offset_y, int texture) {
// When only a sub-part of the image must be encoded...
const bool copy = offset_x || offset_y || img.sd.width != in_width || img.sd.height != in_height;
if(copy) {
auto framebuf = gl::frame_buf_t::make(1);
framebuf.bind(&texture, &texture + 1);
loaded_texture = tex[0];
framebuf.copy(0, loaded_texture, offset_x, offset_y, in_width, in_height);
}
else {
loaded_texture = texture;
}
if(img.data) {
GLenum attachment = GL_COLOR_ATTACHMENT0;
gl::ctx.BindFramebuffer(GL_FRAMEBUFFER, cursor_framebuffer[0]);
gl::ctx.UseProgram(program[2].handle());
// When a copy has already been made...
if(!copy) {
gl::ctx.BindTexture(GL_TEXTURE_2D, texture);
gl::ctx.DrawBuffers(1, &attachment);
gl::ctx.Viewport(0, 0, in_width, in_height);
gl::ctx.DrawArrays(GL_TRIANGLES, 0, 3);
loaded_texture = tex[0];
}
gl::ctx.BindTexture(GL_TEXTURE_2D, tex[1]);
if(serial != img.serial) {
serial = img.serial;
gl::ctx.TexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, img.width, img.height, 0, GL_BGRA, GL_UNSIGNED_BYTE, img.data);
}
gl::ctx.Enable(GL_BLEND);
gl::ctx.DrawBuffers(1, &attachment);
#ifndef NDEBUG
auto status = gl::ctx.CheckFramebufferStatus(GL_FRAMEBUFFER);
if(status != GL_FRAMEBUFFER_COMPLETE) {
BOOST_LOG(error) << "Pass Cursor: CheckFramebufferStatus() --> [0x"sv << util::hex(status).to_string_view() << ']';
return;
}
#endif
gl::ctx.Viewport(img.x, img.y, img.width, img.height);
gl::ctx.DrawArrays(GL_TRIANGLES, 0, 3);
gl::ctx.Disable(GL_BLEND);
gl::ctx.BindTexture(GL_TEXTURE_2D, 0);
gl::ctx.BindFramebuffer(GL_FRAMEBUFFER, 0);
}
}
int sws_t::convert(gl::frame_buf_t &fb) {
gl::ctx.BindTexture(GL_TEXTURE_2D, loaded_texture);
GLenum attachments[] {
GL_COLOR_ATTACHMENT0,
GL_COLOR_ATTACHMENT1
};
for(int x = 0; x < sizeof(attachments) / sizeof(decltype(attachments[0])); ++x) {
gl::ctx.BindFramebuffer(GL_FRAMEBUFFER, fb[x]);
gl::ctx.DrawBuffers(1, &attachments[x]);
#ifndef NDEBUG
auto status = gl::ctx.CheckFramebufferStatus(GL_FRAMEBUFFER);
if(status != GL_FRAMEBUFFER_COMPLETE) {
BOOST_LOG(error) << "Pass "sv << x << ": CheckFramebufferStatus() --> [0x"sv << util::hex(status).to_string_view() << ']';
return -1;
}
#endif
gl::ctx.UseProgram(program[x].handle());
gl::ctx.Viewport(offsetX / (x + 1), offsetY / (x + 1), out_width / (x + 1), out_height / (x + 1));
gl::ctx.DrawArrays(GL_TRIANGLES, 0, 3);
}
gl::ctx.BindTexture(GL_TEXTURE_2D, 0);
gl::ctx.Flush();
return 0;
}
} // namespace egl
void free_frame(AVFrame *frame) {
av_frame_free(&frame);
}

View File

@@ -0,0 +1,319 @@
#ifndef SUNSHINE_PLATFORM_LINUX_OPENGL_H
#define SUNSHINE_PLATFORM_LINUX_OPENGL_H
#include <optional>
#include <string_view>
#include <glad/egl.h>
#include <glad/gl.h>
#include "misc.h"
#include "sunshine/main.h"
#include "sunshine/platform/common.h"
#include "sunshine/utility.h"
#define SUNSHINE_STRINGIFY_HELPER(x) #x
#define SUNSHINE_STRINGIFY(x) SUNSHINE_STRINGIFY_HELPER(x)
#define gl_drain_errors_helper(x) gl::drain_errors(x)
#define gl_drain_errors gl_drain_errors_helper(__FILE__ ":" SUNSHINE_STRINGIFY(__LINE__))
extern "C" int close(int __fd);
// X11 Display
extern "C" struct _XDisplay;
struct AVFrame;
void free_frame(AVFrame *frame);
using frame_t = util::safe_ptr<AVFrame, free_frame>;
namespace gl {
extern GladGLContext ctx;
void drain_errors(const std::string_view &prefix);
class tex_t : public util::buffer_t<GLuint> {
using util::buffer_t<GLuint>::buffer_t;
public:
tex_t(tex_t &&) = default;
tex_t &operator=(tex_t &&) = default;
~tex_t();
static tex_t make(std::size_t count);
};
class frame_buf_t : public util::buffer_t<GLuint> {
using util::buffer_t<GLuint>::buffer_t;
public:
frame_buf_t(frame_buf_t &&) = default;
frame_buf_t &operator=(frame_buf_t &&) = default;
~frame_buf_t();
static frame_buf_t make(std::size_t count);
inline void bind(std::nullptr_t, std::nullptr_t) {
int x = 0;
for(auto fb : (*this)) {
ctx.BindFramebuffer(GL_FRAMEBUFFER, fb);
ctx.FramebufferTexture(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0 + x, 0, 0);
++x;
}
return;
}
template<class It>
void bind(It it_begin, It it_end) {
using namespace std::literals;
if(std::distance(it_begin, it_end) > size()) {
BOOST_LOG(warning) << "To many elements to bind"sv;
return;
}
int x = 0;
std::for_each(it_begin, it_end, [&](auto tex) {
ctx.BindFramebuffer(GL_FRAMEBUFFER, (*this)[x]);
ctx.BindTexture(GL_TEXTURE_2D, tex);
ctx.FramebufferTexture(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0 + x, tex, 0);
++x;
});
}
/**
* Copies a part of the framebuffer to texture
*/
void copy(int id, int texture, int offset_x, int offset_y, int width, int height);
};
class shader_t {
KITTY_USING_MOVE_T(shader_internal_t, GLuint, std::numeric_limits<GLuint>::max(), {
if(el != std::numeric_limits<GLuint>::max()) {
ctx.DeleteShader(el);
}
});
public:
std::string err_str();
static util::Either<shader_t, std::string> compile(const std::string_view &source, GLenum type);
GLuint handle() const;
private:
shader_internal_t _shader;
};
class buffer_t {
KITTY_USING_MOVE_T(buffer_internal_t, GLuint, std::numeric_limits<GLuint>::max(), {
if(el != std::numeric_limits<GLuint>::max()) {
ctx.DeleteBuffers(1, &el);
}
});
public:
static buffer_t make(util::buffer_t<GLint> &&offsets, const char *block, const std::string_view &data);
GLuint handle() const;
const char *block() const;
void update(const std::string_view &view, std::size_t offset = 0);
void update(std::string_view *members, std::size_t count, std::size_t offset = 0);
private:
const char *_block;
std::size_t _size;
util::buffer_t<GLint> _offsets;
buffer_internal_t _buffer;
};
class program_t {
KITTY_USING_MOVE_T(program_internal_t, GLuint, std::numeric_limits<GLuint>::max(), {
if(el != std::numeric_limits<GLuint>::max()) {
ctx.DeleteProgram(el);
}
});
public:
std::string err_str();
static util::Either<program_t, std::string> link(const shader_t &vert, const shader_t &frag);
void bind(const buffer_t &buffer);
std::optional<buffer_t> uniform(const char *block, std::pair<const char *, std::string_view> *members, std::size_t count);
GLuint handle() const;
private:
program_internal_t _program;
};
} // namespace gl
namespace gbm {
struct device;
typedef void (*device_destroy_fn)(device *gbm);
typedef device *(*create_device_fn)(int fd);
extern device_destroy_fn device_destroy;
extern create_device_fn create_device;
using gbm_t = util::dyn_safe_ptr<device, &device_destroy>;
int init();
} // namespace gbm
namespace egl {
using display_t = util::dyn_safe_ptr_v2<void, EGLBoolean, &eglTerminate>;
struct rgb_img_t {
display_t::pointer display;
EGLImage xrgb8;
gl::tex_t tex;
};
struct nv12_img_t {
display_t::pointer display;
EGLImage r8;
EGLImage bg88;
gl::tex_t tex;
gl::frame_buf_t buf;
// sizeof(va::DRMPRIMESurfaceDescriptor::objects) / sizeof(va::DRMPRIMESurfaceDescriptor::objects[0]);
static constexpr std::size_t num_fds = 4;
std::array<file_t, num_fds> fds;
};
KITTY_USING_MOVE_T(rgb_t, rgb_img_t, , {
if(el.xrgb8) {
eglDestroyImage(el.display, el.xrgb8);
}
});
KITTY_USING_MOVE_T(nv12_t, nv12_img_t, , {
if(el.r8) {
eglDestroyImage(el.display, el.r8);
}
if(el.bg88) {
eglDestroyImage(el.display, el.bg88);
}
});
KITTY_USING_MOVE_T(ctx_t, (std::tuple<display_t::pointer, EGLContext>), , {
TUPLE_2D_REF(disp, ctx, el);
if(ctx) {
eglMakeCurrent(disp, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT);
eglDestroyContext(disp, ctx);
}
});
struct surface_descriptor_t {
int width;
int height;
int fds[4];
std::uint32_t fourcc;
std::uint64_t modifier;
std::uint32_t pitches[4];
std::uint32_t offsets[4];
};
display_t make_display(std::variant<gbm::gbm_t::pointer, wl_display *, _XDisplay *> native_display);
std::optional<ctx_t> make_ctx(display_t::pointer display);
std::optional<rgb_t> import_source(
display_t::pointer egl_display,
const surface_descriptor_t &xrgb);
std::optional<nv12_t> import_target(
display_t::pointer egl_display,
std::array<file_t, nv12_img_t::num_fds> &&fds,
const surface_descriptor_t &r8, const surface_descriptor_t &gr88);
class cursor_t : public platf::img_t {
public:
int x, y;
unsigned long serial;
std::vector<std::uint8_t> buffer;
};
// Allow cursor and the underlying image to be kept together
class img_descriptor_t : public cursor_t {
public:
~img_descriptor_t() {
reset();
}
void reset() {
for(auto x = 0; x < 4; ++x) {
if(sd.fds[x] >= 0) {
close(sd.fds[x]);
sd.fds[x] = -1;
}
}
}
surface_descriptor_t sd;
// Increment sequence when new rgb_t needs to be created
std::uint64_t sequence;
};
class sws_t {
public:
static std::optional<sws_t> make(int in_width, int in_height, int out_width, int out_heigth, gl::tex_t &&tex);
static std::optional<sws_t> make(int in_width, int in_height, int out_width, int out_heigth);
// Convert the loaded image into the first two framebuffers
int convert(gl::frame_buf_t &fb);
// Make an area of the image black
int blank(gl::frame_buf_t &fb, int offsetX, int offsetY, int width, int height);
void load_ram(platf::img_t &img);
void load_vram(img_descriptor_t &img, int offset_x, int offset_y, int texture);
void set_colorspace(std::uint32_t colorspace, std::uint32_t color_range);
// The first texture is the monitor image.
// The second texture is the cursor image
gl::tex_t tex;
// The cursor image will be blended into this framebuffer
gl::frame_buf_t cursor_framebuffer;
gl::frame_buf_t copy_framebuffer;
// Y - shader, UV - shader, Cursor - shader
gl::program_t program[3];
gl::buffer_t color_matrix;
int out_width, out_height;
int in_width, in_height;
int offsetX, offsetY;
// Pointer to the texture to be converted to nv12
int loaded_texture;
// Store latest cursor for load_vram
std::uint64_t serial;
};
bool fail();
} // namespace egl
#endif

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,302 @@
#include <arpa/inet.h>
#include <dlfcn.h>
#include <fcntl.h>
#include <ifaddrs.h>
#include <pwd.h>
#include <unistd.h>
#include <fstream>
#include "graphics.h"
#include "misc.h"
#include "vaapi.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;
window_system_e window_system;
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
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 source {
enum source_e : std::size_t {
#ifdef SUNSHINE_BUILD_CUDA
NVFBC,
#endif
#ifdef SUNSHINE_BUILD_WAYLAND
WAYLAND,
#endif
#ifdef SUNSHINE_BUILD_DRM
KMS,
#endif
#ifdef SUNSHINE_BUILD_X11
X11,
#endif
MAX_FLAGS
};
} // namespace source
static std::bitset<source::MAX_FLAGS> sources;
#ifdef SUNSHINE_BUILD_CUDA
std::vector<std::string> nvfbc_display_names();
std::shared_ptr<display_t> nvfbc_display(mem_type_e hwdevice_type, const std::string &display_name, int framerate);
bool verify_nvfbc() {
return !nvfbc_display_names().empty();
}
#endif
#ifdef SUNSHINE_BUILD_WAYLAND
std::vector<std::string> wl_display_names();
std::shared_ptr<display_t> wl_display(mem_type_e hwdevice_type, const std::string &display_name, int framerate);
bool verify_wl() {
return window_system == window_system_e::WAYLAND && !wl_display_names().empty();
}
#endif
#ifdef SUNSHINE_BUILD_DRM
std::vector<std::string> kms_display_names();
std::shared_ptr<display_t> kms_display(mem_type_e hwdevice_type, const std::string &display_name, int framerate);
bool verify_kms() {
return !kms_display_names().empty();
}
#endif
#ifdef SUNSHINE_BUILD_X11
std::vector<std::string> x11_display_names();
std::shared_ptr<display_t> x11_display(mem_type_e hwdevice_type, const std::string &display_name, int framerate);
bool verify_x11() {
return window_system == window_system_e::X11 && !x11_display_names().empty();
}
#endif
std::vector<std::string> display_names(mem_type_e hwdevice_type) {
#ifdef SUNSHINE_BUILD_CUDA
// display using NvFBC only supports mem_type_e::cuda
if(sources[source::NVFBC] && hwdevice_type == mem_type_e::cuda) return nvfbc_display_names();
#endif
#ifdef SUNSHINE_BUILD_WAYLAND
if(sources[source::WAYLAND]) return wl_display_names();
#endif
#ifdef SUNSHINE_BUILD_DRM
if(sources[source::KMS]) return kms_display_names();
#endif
#ifdef SUNSHINE_BUILD_X11
if(sources[source::X11]) return x11_display_names();
#endif
return {};
}
std::shared_ptr<display_t> display(mem_type_e hwdevice_type, const std::string &display_name, int framerate) {
#ifdef SUNSHINE_BUILD_CUDA
if(sources[source::NVFBC] && hwdevice_type == mem_type_e::cuda) {
BOOST_LOG(info) << "Screencasting with NvFBC"sv;
return nvfbc_display(hwdevice_type, display_name, framerate);
}
#endif
#ifdef SUNSHINE_BUILD_WAYLAND
if(sources[source::WAYLAND]) {
BOOST_LOG(info) << "Screencasting with Wayland's protocol"sv;
return wl_display(hwdevice_type, display_name, framerate);
}
#endif
#ifdef SUNSHINE_BUILD_DRM
if(sources[source::KMS]) {
BOOST_LOG(info) << "Screencasting with KMS"sv;
return kms_display(hwdevice_type, display_name, framerate);
}
#endif
#ifdef SUNSHINE_BUILD_X11
if(sources[source::X11]) {
BOOST_LOG(info) << "Screencasting with X11"sv;
return x11_display(hwdevice_type, display_name, framerate);
}
#endif
return nullptr;
}
std::unique_ptr<deinit_t> init() {
// These are allowed to fail.
gbm::init();
va::init();
window_system = window_system_e::NONE;
#ifdef SUNSHINE_BUILD_WAYLAND
if(std::getenv("WAYLAND_DISPLAY")) {
window_system = window_system_e::WAYLAND;
}
#endif
#if defined(SUNSHINE_BUILD_X11) || defined(SUNSHINE_BUILD_CUDA)
if(std::getenv("DISPLAY") && window_system != window_system_e::WAYLAND) {
if(std::getenv("WAYLAND_DISPLAY")) {
BOOST_LOG(warning) << "Wayland detected, yet sunshine will use X11 for screencasting, screencasting will only work on XWayland applications"sv;
}
window_system = window_system_e::X11;
}
#endif
#ifdef SUNSHINE_BUILD_CUDA
if(verify_nvfbc()) {
sources[source::NVFBC] = true;
}
#endif
#ifdef SUNSHINE_BUILD_WAYLAND
if(verify_wl()) {
sources[source::WAYLAND] = true;
}
#endif
#ifdef SUNSHINE_BUILD_DRM
if(verify_kms()) {
if(window_system == window_system_e::WAYLAND) {
// On Wayland, using KMS, the cursor is unreliable.
// Hide it by default
display_cursor = false;
}
sources[source::KMS] = true;
}
#endif
#ifdef SUNSHINE_BUILD_X11
if(verify_x11()) {
sources[source::X11] = true;
}
#endif
if(sources.none()) {
return nullptr;
}
if(!gladLoaderLoadEGL(EGL_NO_DISPLAY) || !eglGetPlatformDisplay) {
BOOST_LOG(warning) << "Couldn't load EGL library"sv;
}
return std::make_unique<deinit_t>();
}
} // namespace platf

View File

@@ -0,0 +1,31 @@
#ifndef SUNSHINE_PLATFORM_MISC_H
#define SUNSHINE_PLATFORM_MISC_H
#include <unistd.h>
#include <vector>
#include "sunshine/utility.h"
KITTY_USING_MOVE_T(file_t, int, -1, {
if(el >= 0) {
close(el);
}
});
enum class window_system_e {
NONE,
X11,
WAYLAND,
};
extern window_system_e window_system;
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

View File

@@ -0,0 +1,651 @@
#include <sstream>
#include <string>
#include <fcntl.h>
extern "C" {
#include <libavcodec/avcodec.h>
}
#include "graphics.h"
#include "misc.h"
#include "sunshine/config.h"
#include "sunshine/main.h"
#include "sunshine/platform/common.h"
#include "sunshine/utility.h"
using namespace std::literals;
extern "C" struct AVBufferRef;
namespace va {
constexpr auto SURFACE_ATTRIB_MEM_TYPE_DRM_PRIME_2 = 0x40000000;
constexpr auto EXPORT_SURFACE_WRITE_ONLY = 0x0002;
constexpr auto EXPORT_SURFACE_COMPOSED_LAYERS = 0x0008;
using VADisplay = void *;
using VAStatus = int;
using VAGenericID = unsigned int;
using VASurfaceID = VAGenericID;
struct DRMPRIMESurfaceDescriptor {
// VA Pixel format fourcc of the whole surface (VA_FOURCC_*).
uint32_t fourcc;
uint32_t width;
uint32_t height;
// Number of distinct DRM objects making up the surface.
uint32_t num_objects;
struct {
// DRM PRIME file descriptor for this object.
// Needs to be closed manually
int fd;
/*
* Total size of this object (may include regions which are
* not part of the surface).
*/
uint32_t size;
// Format modifier applied to this object, not sure what that means
uint64_t drm_format_modifier;
} objects[4];
// Number of layers making up the surface.
uint32_t num_layers;
struct {
// DRM format fourcc of this layer (DRM_FOURCC_*).
uint32_t drm_format;
// Number of planes in this layer.
uint32_t num_planes;
// references objects --> DRMPRIMESurfaceDescriptor.objects[object_index[0]]
uint32_t object_index[4];
// Offset within the object of each plane.
uint32_t offset[4];
// Pitch of each plane.
uint32_t pitch[4];
} layers[4];
};
/** Currently defined profiles */
enum class profile_e {
// Profile ID used for video processing.
ProfileNone = -1,
MPEG2Simple = 0,
MPEG2Main = 1,
MPEG4Simple = 2,
MPEG4AdvancedSimple = 3,
MPEG4Main = 4,
H264Baseline = 5,
H264Main = 6,
H264High = 7,
VC1Simple = 8,
VC1Main = 9,
VC1Advanced = 10,
H263Baseline = 11,
JPEGBaseline = 12,
H264ConstrainedBaseline = 13,
VP8Version0_3 = 14,
H264MultiviewHigh = 15,
H264StereoHigh = 16,
HEVCMain = 17,
HEVCMain10 = 18,
VP9Profile0 = 19,
VP9Profile1 = 20,
VP9Profile2 = 21,
VP9Profile3 = 22,
HEVCMain12 = 23,
HEVCMain422_10 = 24,
HEVCMain422_12 = 25,
HEVCMain444 = 26,
HEVCMain444_10 = 27,
HEVCMain444_12 = 28,
HEVCSccMain = 29,
HEVCSccMain10 = 30,
HEVCSccMain444 = 31,
AV1Profile0 = 32,
AV1Profile1 = 33,
HEVCSccMain444_10 = 34,
// Profile ID used for protected video playback.
Protected = 35
};
enum class entry_e {
VLD = 1,
IZZ = 2,
IDCT = 3,
MoComp = 4,
Deblocking = 5,
EncSlice = 6, /* slice level encode */
EncPicture = 7, /* pictuer encode, JPEG, etc */
/*
* For an implementation that supports a low power/high performance variant
* for slice level encode, it can choose to expose the
* VAEntrypointEncSliceLP entrypoint. Certain encoding tools may not be
* available with this entrypoint (e.g. interlace, MBAFF) and the
* application can query the encoding configuration attributes to find
* out more details if this entrypoint is supported.
*/
EncSliceLP = 8,
VideoProc = 10, /**< Video pre/post-processing. */
/**
* \brief FEI
*
* The purpose of FEI (Flexible Encoding Infrastructure) is to allow applications to
* have more controls and trade off quality for speed with their own IPs.
* The application can optionally provide input to ENC for extra encode control
* and get the output from ENC. Application can chose to modify the ENC
* output/PAK input during encoding, but the performance impact is significant.
*
* On top of the existing buffers for normal encode, there will be
* one extra input buffer (VAEncMiscParameterFEIFrameControl) and
* three extra output buffers (VAEncFEIMVBufferType, VAEncFEIMBModeBufferType
* and VAEncFEIDistortionBufferType) for FEI entry function.
* If separate PAK is set, two extra input buffers
* (VAEncFEIMVBufferType, VAEncFEIMBModeBufferType) are needed for PAK input.
**/
FEI = 11,
/**
* \brief Stats
*
* A pre-processing function for getting some statistics and motion vectors is added,
* and some extra controls for Encode pipeline are provided. The application can
* optionally call the statistics function to get motion vectors and statistics like
* variances, distortions before calling Encode function via this entry point.
*
* Checking whether Statistics is supported can be performed with vaQueryConfigEntrypoints().
* If Statistics entry point is supported, then the list of returned entry-points will
* include #Stats. Supported pixel format, maximum resolution and statistics
* specific attributes can be obtained via normal attribute query. One input buffer
* (VAStatsStatisticsParameterBufferType) and one or two output buffers
* (VAStatsStatisticsBufferType, VAStatsStatisticsBottomFieldBufferType (for interlace only)
* and VAStatsMVBufferType) are needed for this entry point.
**/
Stats = 12,
/**
* \brief ProtectedTEEComm
*
* A function for communicating with TEE (Trusted Execution Environment).
**/
ProtectedTEEComm = 13,
/**
* \brief ProtectedContent
*
* A function for protected content to decrypt encrypted content.
**/
ProtectedContent = 14,
};
typedef VAStatus (*queryConfigEntrypoints_fn)(VADisplay dpy, profile_e profile, entry_e *entrypoint_list, int *num_entrypoints);
typedef int (*maxNumEntrypoints_fn)(VADisplay dpy);
typedef VADisplay (*getDisplayDRM_fn)(int fd);
typedef VAStatus (*terminate_fn)(VADisplay dpy);
typedef VAStatus (*initialize_fn)(VADisplay dpy, int *major_version, int *minor_version);
typedef const char *(*errorStr_fn)(VAStatus error_status);
typedef void (*VAMessageCallback)(void *user_context, const char *message);
typedef VAMessageCallback (*setErrorCallback_fn)(VADisplay dpy, VAMessageCallback callback, void *user_context);
typedef VAMessageCallback (*setInfoCallback_fn)(VADisplay dpy, VAMessageCallback callback, void *user_context);
typedef const char *(*queryVendorString_fn)(VADisplay dpy);
typedef VAStatus (*exportSurfaceHandle_fn)(
VADisplay dpy, VASurfaceID surface_id,
uint32_t mem_type, uint32_t flags,
void *descriptor);
static maxNumEntrypoints_fn maxNumEntrypoints;
static queryConfigEntrypoints_fn queryConfigEntrypoints;
static getDisplayDRM_fn getDisplayDRM;
static terminate_fn terminate;
static initialize_fn initialize;
static errorStr_fn errorStr;
static setErrorCallback_fn setErrorCallback;
static setInfoCallback_fn setInfoCallback;
static queryVendorString_fn queryVendorString;
static exportSurfaceHandle_fn exportSurfaceHandle;
using display_t = util::dyn_safe_ptr_v2<void, VAStatus, &terminate>;
int init_main_va() {
static void *handle { nullptr };
static bool funcs_loaded = false;
if(funcs_loaded) return 0;
if(!handle) {
handle = dyn::handle({ "libva.so.2", "libva.so" });
if(!handle) {
return -1;
}
}
std::vector<std::tuple<dyn::apiproc *, const char *>> funcs {
{ (dyn::apiproc *)&maxNumEntrypoints, "vaMaxNumEntrypoints" },
{ (dyn::apiproc *)&queryConfigEntrypoints, "vaQueryConfigEntrypoints" },
{ (dyn::apiproc *)&terminate, "vaTerminate" },
{ (dyn::apiproc *)&initialize, "vaInitialize" },
{ (dyn::apiproc *)&errorStr, "vaErrorStr" },
{ (dyn::apiproc *)&setErrorCallback, "vaSetErrorCallback" },
{ (dyn::apiproc *)&setInfoCallback, "vaSetInfoCallback" },
{ (dyn::apiproc *)&queryVendorString, "vaQueryVendorString" },
{ (dyn::apiproc *)&exportSurfaceHandle, "vaExportSurfaceHandle" },
};
if(dyn::load(handle, funcs)) {
return -1;
}
funcs_loaded = true;
return 0;
}
int init() {
if(init_main_va()) {
return -1;
}
static void *handle { nullptr };
static bool funcs_loaded = false;
if(funcs_loaded) return 0;
if(!handle) {
handle = dyn::handle({ "libva-drm.so.2", "libva-drm.so" });
if(!handle) {
return -1;
}
}
std::vector<std::tuple<dyn::apiproc *, const char *>> funcs {
{ (dyn::apiproc *)&getDisplayDRM, "vaGetDisplayDRM" },
};
if(dyn::load(handle, funcs)) {
return -1;
}
funcs_loaded = true;
return 0;
}
int vaapi_make_hwdevice_ctx(platf::hwdevice_t *base, AVBufferRef **hw_device_buf);
class va_t : public platf::hwdevice_t {
public:
int init(int in_width, int in_height, file_t &&render_device) {
file = std::move(render_device);
if(!va::initialize || !gbm::create_device) {
if(!va::initialize) BOOST_LOG(warning) << "libva not initialized"sv;
if(!gbm::create_device) BOOST_LOG(warning) << "libgbm not initialized"sv;
return -1;
}
this->data = (void *)vaapi_make_hwdevice_ctx;
gbm.reset(gbm::create_device(file.el));
if(!gbm) {
char string[1024];
BOOST_LOG(error) << "Couldn't create GBM device: ["sv << strerror_r(errno, string, sizeof(string)) << ']';
return -1;
}
display = egl::make_display(gbm.get());
if(!display) {
return -1;
}
auto ctx_opt = egl::make_ctx(display.get());
if(!ctx_opt) {
return -1;
}
ctx = std::move(*ctx_opt);
width = in_width;
height = in_height;
return 0;
}
int set_frame(AVFrame *frame) override {
this->hwframe.reset(frame);
this->frame = frame;
if(av_hwframe_get_buffer(frame->hw_frames_ctx, frame, 0)) {
BOOST_LOG(error) << "Couldn't get hwframe for VAAPI"sv;
return -1;
}
va::DRMPRIMESurfaceDescriptor prime;
va::VASurfaceID surface = (std::uintptr_t)frame->data[3];
auto status = va::exportSurfaceHandle(
this->va_display,
surface,
va::SURFACE_ATTRIB_MEM_TYPE_DRM_PRIME_2,
va::EXPORT_SURFACE_WRITE_ONLY | va::EXPORT_SURFACE_COMPOSED_LAYERS,
&prime);
if(status) {
BOOST_LOG(error) << "Couldn't export va surface handle: ["sv << (int)surface << "]: "sv << va::errorStr(status);
return -1;
}
// Keep track of file descriptors
std::array<file_t, egl::nv12_img_t::num_fds> fds;
for(int x = 0; x < prime.num_objects; ++x) {
fds[x] = prime.objects[x].fd;
}
auto nv12_opt = egl::import_target(
display.get(),
std::move(fds),
{ (int)prime.width,
(int)prime.height,
{ prime.objects[prime.layers[0].object_index[0]].fd, -1, -1, -1 },
0,
0,
{ prime.layers[0].pitch[0] },
{ prime.layers[0].offset[0] } },
{ (int)prime.width / 2,
(int)prime.height / 2,
{ prime.objects[prime.layers[0].object_index[1]].fd, -1, -1, -1 },
0,
0,
{ prime.layers[0].pitch[1] },
{ prime.layers[0].offset[1] } });
if(!nv12_opt) {
return -1;
}
auto sws_opt = egl::sws_t::make(width, height, frame->width, frame->height);
if(!sws_opt) {
return -1;
}
this->sws = std::move(*sws_opt);
this->nv12 = std::move(*nv12_opt);
return 0;
}
void set_colorspace(std::uint32_t colorspace, std::uint32_t color_range) override {
sws.set_colorspace(colorspace, color_range);
}
va::display_t::pointer va_display;
file_t file;
frame_t hwframe;
gbm::gbm_t gbm;
egl::display_t display;
egl::ctx_t ctx;
egl::sws_t sws;
egl::nv12_t nv12;
int width, height;
};
class va_ram_t : public va_t {
public:
int convert(platf::img_t &img) override {
sws.load_ram(img);
sws.convert(nv12->buf);
return 0;
}
};
class va_vram_t : public va_t {
public:
int convert(platf::img_t &img) override {
auto &descriptor = (egl::img_descriptor_t &)img;
if(descriptor.sequence > sequence) {
sequence = descriptor.sequence;
rgb = egl::rgb_t {};
auto rgb_opt = egl::import_source(display.get(), descriptor.sd);
if(!rgb_opt) {
return -1;
}
rgb = std::move(*rgb_opt);
}
sws.load_vram(descriptor, offset_x, offset_y, rgb->tex[0]);
sws.convert(nv12->buf);
return 0;
}
int init(int in_width, int in_height, file_t &&render_device, int offset_x, int offset_y) {
if(va_t::init(in_width, in_height, std::move(render_device))) {
return -1;
}
sequence = 0;
this->offset_x = offset_x;
this->offset_y = offset_y;
return 0;
}
std::uint64_t sequence;
egl::rgb_t rgb;
int offset_x, offset_y;
};
/**
* This is a private structure of FFmpeg, I need this to manually create
* a VAAPI hardware context
*
* xdisplay will not be used internally by FFmpeg
*/
typedef struct VAAPIDevicePriv {
union {
void *xdisplay;
int fd;
} drm;
int drm_fd;
} VAAPIDevicePriv;
/**
* VAAPI connection details.
*
* Allocated as AVHWDeviceContext.hwctx
*/
typedef struct AVVAAPIDeviceContext {
/**
* The VADisplay handle, to be filled by the user.
*/
va::VADisplay display;
/**
* Driver quirks to apply - this is filled by av_hwdevice_ctx_init(),
* with reference to a table of known drivers, unless the
* AV_VAAPI_DRIVER_QUIRK_USER_SET bit is already present. The user
* may need to refer to this field when performing any later
* operations using VAAPI with the same VADisplay.
*/
unsigned int driver_quirks;
} AVVAAPIDeviceContext;
static void __log(void *level, const char *msg) {
BOOST_LOG(*(boost::log::sources::severity_logger<int> *)level) << msg;
}
int vaapi_make_hwdevice_ctx(platf::hwdevice_t *base, AVBufferRef **hw_device_buf) {
if(!va::initialize) {
BOOST_LOG(warning) << "libva not loaded"sv;
return -1;
}
if(!va::getDisplayDRM) {
BOOST_LOG(warning) << "libva-drm not loaded"sv;
return -1;
}
auto va = (va::va_t *)base;
auto fd = dup(va->file.el);
auto *priv = (VAAPIDevicePriv *)av_mallocz(sizeof(VAAPIDevicePriv));
priv->drm_fd = fd;
priv->drm.fd = fd;
auto fg = util::fail_guard([fd, priv]() {
close(fd);
av_free(priv);
});
va::display_t display { va::getDisplayDRM(fd) };
if(!display) {
auto render_device = config::video.adapter_name.empty() ? "/dev/dri/renderD128" : config::video.adapter_name.c_str();
BOOST_LOG(error) << "Couldn't open a va display from DRM with device: "sv << render_device;
return -1;
}
va->va_display = display.get();
va::setErrorCallback(display.get(), __log, &error);
va::setErrorCallback(display.get(), __log, &info);
int major, minor;
auto status = va::initialize(display.get(), &major, &minor);
if(status) {
BOOST_LOG(error) << "Couldn't initialize va display: "sv << va::errorStr(status);
return -1;
}
BOOST_LOG(debug) << "vaapi vendor: "sv << va::queryVendorString(display.get());
*hw_device_buf = av_hwdevice_ctx_alloc(AV_HWDEVICE_TYPE_VAAPI);
auto ctx = (AVVAAPIDeviceContext *)((AVHWDeviceContext *)(*hw_device_buf)->data)->hwctx;
ctx->display = display.release();
fg.disable();
auto err = av_hwdevice_ctx_init(*hw_device_buf);
if(err) {
char err_str[AV_ERROR_MAX_STRING_SIZE] { 0 };
BOOST_LOG(error) << "Failed to create FFMpeg hardware device context: "sv << av_make_error_string(err_str, AV_ERROR_MAX_STRING_SIZE, err);
return err;
}
return 0;
}
static bool query(display_t::pointer display, profile_e profile) {
std::vector<entry_e> entrypoints;
entrypoints.resize(maxNumEntrypoints(display));
int count;
auto status = queryConfigEntrypoints(display, profile, entrypoints.data(), &count);
if(status) {
BOOST_LOG(error) << "Couldn't query entrypoints: "sv << va::errorStr(status);
return false;
}
entrypoints.resize(count);
for(auto entrypoint : entrypoints) {
if(entrypoint == entry_e::EncSlice || entrypoint == entry_e::EncSliceLP) {
return true;
}
}
return false;
}
bool validate(int fd) {
if(init()) {
return false;
}
va::display_t display { va::getDisplayDRM(fd) };
if(!display) {
char string[1024];
auto bytes = readlink(("/proc/self/fd/" + std::to_string(fd)).c_str(), string, sizeof(string));
std::string_view render_device { string, (std::size_t)bytes };
BOOST_LOG(error) << "Couldn't open a va display from DRM with device: "sv << render_device;
return false;
}
int major, minor;
auto status = initialize(display.get(), &major, &minor);
if(status) {
BOOST_LOG(error) << "Couldn't initialize va display: "sv << va::errorStr(status);
return false;
}
if(!query(display.get(), profile_e::H264Main)) {
return false;
}
if(config::video.hevc_mode > 1 && !query(display.get(), profile_e::HEVCMain)) {
return false;
}
if(config::video.hevc_mode > 2 && !query(display.get(), profile_e::HEVCMain10)) {
return false;
}
return true;
}
std::shared_ptr<platf::hwdevice_t> make_hwdevice(int width, int height, file_t &&card, int offset_x, int offset_y, bool vram) {
if(vram) {
auto egl = std::make_shared<va::va_vram_t>();
if(egl->init(width, height, std::move(card), offset_x, offset_y)) {
return nullptr;
}
return egl;
}
else {
auto egl = std::make_shared<va::va_ram_t>();
if(egl->init(width, height, std::move(card))) {
return nullptr;
}
return egl;
}
}
std::shared_ptr<platf::hwdevice_t> make_hwdevice(int width, int height, int offset_x, int offset_y, bool vram) {
auto render_device = config::video.adapter_name.empty() ? "/dev/dri/renderD128" : config::video.adapter_name.c_str();
file_t file = open(render_device, O_RDWR);
if(file.el < 0) {
char string[1024];
BOOST_LOG(error) << "Couldn't open "sv << render_device << ": " << strerror_r(errno, string, sizeof(string));
return nullptr;
}
return make_hwdevice(width, height, std::move(file), offset_x, offset_y, vram);
}
std::shared_ptr<platf::hwdevice_t> make_hwdevice(int width, int height, bool vram) {
return make_hwdevice(width, height, 0, 0, vram);
}
} // namespace va

View File

@@ -0,0 +1,27 @@
#ifndef SUNSHINE_VAAPI_H
#define SUNSHINE_VAAPI_H
#include "misc.h"
#include "sunshine/platform/common.h"
namespace egl {
struct surface_descriptor_t;
}
namespace va {
/**
* Width --> Width of the image
* Height --> Height of the image
* offset_x --> Horizontal offset of the image in the texture
* offset_y --> Vertical offset of the image in the texture
* file_t card --> The file descriptor of the render device used for encoding
*/
std::shared_ptr<platf::hwdevice_t> make_hwdevice(int width, int height, bool vram);
std::shared_ptr<platf::hwdevice_t> make_hwdevice(int width, int height, int offset_x, int offset_y, bool vram);
std::shared_ptr<platf::hwdevice_t> make_hwdevice(int width, int height, file_t &&card, int offset_x, int offset_y, bool vram);
// Ensure the render device pointed to by fd is capable of encoding h264 with the hevc_mode configured
bool validate(int fd);
int init();
} // namespace va
#endif

View File

@@ -0,0 +1,268 @@
#include <wayland-client.h>
#include <wayland-util.h>
#include <cstdlib>
#include "graphics.h"
#include "sunshine/main.h"
#include "sunshine/platform/common.h"
#include "sunshine/round_robin.h"
#include "sunshine/utility.h"
#include "wayland.h"
extern const wl_interface wl_output_interface;
using namespace std::literals;
// Disable warning for converting incompatible functions
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wpedantic"
#pragma GCC diagnostic ignored "-Wpmf-conversions"
namespace wl {
int display_t::init(const char *display_name) {
if(!display_name) {
display_name = std::getenv("WAYLAND_DISPLAY");
}
if(!display_name) {
BOOST_LOG(error) << "Environment variable WAYLAND_DISPLAY has not been defined"sv;
return -1;
}
display_internal.reset(wl_display_connect(display_name));
if(!display_internal) {
BOOST_LOG(error) << "Couldn't connect to Wayland display: "sv << display_name;
return -1;
}
BOOST_LOG(info) << "Found display ["sv << display_name << ']';
return 0;
}
void display_t::roundtrip() {
wl_display_roundtrip(display_internal.get());
}
wl_registry *display_t::registry() {
return wl_display_get_registry(display_internal.get());
}
inline monitor_t::monitor_t(wl_output *output) : output { output } {}
inline void monitor_t::xdg_name(zxdg_output_v1 *, const char *name) {
this->name = name;
BOOST_LOG(info) << "Name: "sv << this->name;
}
void monitor_t::xdg_description(zxdg_output_v1 *, const char *description) {
this->description = description;
BOOST_LOG(info) << "Found monitor: "sv << this->description;
}
void monitor_t::xdg_position(zxdg_output_v1 *, std::int32_t x, std::int32_t y) {
viewport.offset_x = x;
viewport.offset_y = y;
BOOST_LOG(info) << "Offset: "sv << x << 'x' << y;
}
void monitor_t::xdg_size(zxdg_output_v1 *, std::int32_t width, std::int32_t height) {
viewport.width = width;
viewport.height = height;
BOOST_LOG(info) << "Resolution: "sv << width << 'x' << height;
}
void monitor_t::xdg_done(zxdg_output_v1 *) {
BOOST_LOG(info) << "All info about monitor ["sv << name << "] has been send"sv;
}
void monitor_t::listen(zxdg_output_manager_v1 *output_manager) {
auto xdg_output = zxdg_output_manager_v1_get_xdg_output(output_manager, output);
#define CLASS_CALL(x, y) x = (decltype(x))&y
CLASS_CALL(listener.name, monitor_t::xdg_name);
CLASS_CALL(listener.logical_size, monitor_t::xdg_size);
CLASS_CALL(listener.logical_position, monitor_t::xdg_position);
CLASS_CALL(listener.done, monitor_t::xdg_done);
CLASS_CALL(listener.description, monitor_t::xdg_description);
#undef CLASS_CALL
zxdg_output_v1_add_listener(xdg_output, &listener, this);
}
interface_t::interface_t() noexcept
: output_manager { nullptr }, listener {
(decltype(wl_registry_listener::global))&interface_t::add_interface,
(decltype(wl_registry_listener::global_remove))&interface_t::del_interface,
} {}
void interface_t::listen(wl_registry *registry) {
wl_registry_add_listener(registry, &listener, this);
}
void interface_t::add_interface(wl_registry *registry, std::uint32_t id, const char *interface, std::uint32_t version) {
BOOST_LOG(debug) << "Available interface: "sv << interface << '(' << id << ") version "sv << version;
if(!std::strcmp(interface, wl_output_interface.name)) {
BOOST_LOG(info) << "Found interface: "sv << interface << '(' << id << ") version "sv << version;
monitors.emplace_back(
std::make_unique<monitor_t>(
(wl_output *)wl_registry_bind(registry, id, &wl_output_interface, version)));
}
else if(!std::strcmp(interface, zxdg_output_manager_v1_interface.name)) {
BOOST_LOG(info) << "Found interface: "sv << interface << '(' << id << ") version "sv << version;
output_manager = (zxdg_output_manager_v1 *)wl_registry_bind(registry, id, &zxdg_output_manager_v1_interface, version);
this->interface[XDG_OUTPUT] = true;
}
else if(!std::strcmp(interface, zwlr_export_dmabuf_manager_v1_interface.name)) {
BOOST_LOG(info) << "Found interface: "sv << interface << '(' << id << ") version "sv << version;
dmabuf_manager = (zwlr_export_dmabuf_manager_v1 *)wl_registry_bind(registry, id, &zwlr_export_dmabuf_manager_v1_interface, version);
this->interface[WLR_EXPORT_DMABUF] = true;
}
}
void interface_t::del_interface(wl_registry *registry, uint32_t id) {
BOOST_LOG(info) << "Delete: "sv << id;
}
dmabuf_t::dmabuf_t()
: status { READY }, frames {}, current_frame { &frames[0] }, listener {
(decltype(zwlr_export_dmabuf_frame_v1_listener::frame))&dmabuf_t::frame,
(decltype(zwlr_export_dmabuf_frame_v1_listener::object))&dmabuf_t::object,
(decltype(zwlr_export_dmabuf_frame_v1_listener::ready))&dmabuf_t::ready,
(decltype(zwlr_export_dmabuf_frame_v1_listener::cancel))&dmabuf_t::cancel,
} {
}
void dmabuf_t::listen(zwlr_export_dmabuf_manager_v1 *dmabuf_manager, wl_output *output, bool blend_cursor) {
auto frame = zwlr_export_dmabuf_manager_v1_capture_output(dmabuf_manager, blend_cursor, output);
zwlr_export_dmabuf_frame_v1_add_listener(frame, &listener, this);
status = WAITING;
}
dmabuf_t::~dmabuf_t() {
for(auto &frame : frames) {
frame.destroy();
}
}
void dmabuf_t::frame(
zwlr_export_dmabuf_frame_v1 *frame,
std::uint32_t width, std::uint32_t height,
std::uint32_t x, std::uint32_t y,
std::uint32_t buffer_flags, std::uint32_t flags,
std::uint32_t format,
std::uint32_t high, std::uint32_t low,
std::uint32_t obj_count) {
auto next_frame = get_next_frame();
next_frame->sd.fourcc = format;
next_frame->sd.width = width;
next_frame->sd.height = height;
next_frame->sd.modifier = (((std::uint64_t)high) << 32) | low;
}
void dmabuf_t::object(
zwlr_export_dmabuf_frame_v1 *frame,
std::uint32_t index,
std::int32_t fd,
std::uint32_t size,
std::uint32_t offset,
std::uint32_t stride,
std::uint32_t plane_index) {
auto next_frame = get_next_frame();
next_frame->sd.fds[plane_index] = fd;
next_frame->sd.pitches[plane_index] = stride;
next_frame->sd.offsets[plane_index] = offset;
}
void dmabuf_t::ready(
zwlr_export_dmabuf_frame_v1 *frame,
std::uint32_t tv_sec_hi, std::uint32_t tv_sec_lo, std::uint32_t tv_nsec) {
zwlr_export_dmabuf_frame_v1_destroy(frame);
current_frame->destroy();
current_frame = get_next_frame();
status = READY;
}
void dmabuf_t::cancel(
zwlr_export_dmabuf_frame_v1 *frame,
zwlr_export_dmabuf_frame_v1_cancel_reason reason) {
zwlr_export_dmabuf_frame_v1_destroy(frame);
auto next_frame = get_next_frame();
next_frame->destroy();
status = REINIT;
}
void frame_t::destroy() {
for(auto x = 0; x < 4; ++x) {
if(sd.fds[x] >= 0) {
close(sd.fds[x]);
sd.fds[x] = -1;
}
}
}
frame_t::frame_t() {
// File descriptors aren't open
std::fill_n(sd.fds, 4, -1);
};
std::vector<std::unique_ptr<monitor_t>> monitors(const char *display_name) {
display_t display;
if(display.init(display_name)) {
return {};
}
interface_t interface;
interface.listen(display.registry());
display.roundtrip();
if(!interface[interface_t::XDG_OUTPUT]) {
BOOST_LOG(error) << "Missing Wayland wire XDG_OUTPUT"sv;
return {};
}
for(auto &monitor : interface.monitors) {
monitor->listen(interface.output_manager);
}
display.roundtrip();
return std::move(interface.monitors);
}
static bool validate() {
display_t display;
return display.init() == 0;
}
int init() {
static bool validated = validate();
return !validated;
}
} // namespace wl
#pragma GCC diagnostic pop

View File

@@ -0,0 +1,216 @@
#ifndef SUNSHINE_WAYLAND_H
#define SUNSHINE_WAYLAND_H
#include <bitset>
#ifdef SUNSHINE_BUILD_WAYLAND
#include <wlr-export-dmabuf-unstable-v1.h>
#include <xdg-output-unstable-v1.h>
#endif
#include "graphics.h"
/**
* The classes defined in this macro block should only be used by
* cpp files whose compilation depends on SUNSHINE_BUILD_WAYLAND
*/
#ifdef SUNSHINE_BUILD_WAYLAND
namespace wl {
using display_internal_t = util::safe_ptr<wl_display, wl_display_disconnect>;
class frame_t {
public:
frame_t();
egl::surface_descriptor_t sd;
void destroy();
};
class dmabuf_t {
public:
enum status_e {
WAITING,
READY,
REINIT,
};
dmabuf_t(dmabuf_t &&) = delete;
dmabuf_t(const dmabuf_t &) = delete;
dmabuf_t &operator=(const dmabuf_t &) = delete;
dmabuf_t &operator=(dmabuf_t &&) = delete;
dmabuf_t();
void listen(zwlr_export_dmabuf_manager_v1 *dmabuf_manager, wl_output *output, bool blend_cursor = false);
~dmabuf_t();
void frame(
zwlr_export_dmabuf_frame_v1 *frame,
std::uint32_t width, std::uint32_t height,
std::uint32_t x, std::uint32_t y,
std::uint32_t buffer_flags, std::uint32_t flags,
std::uint32_t format,
std::uint32_t high, std::uint32_t low,
std::uint32_t obj_count);
void object(
zwlr_export_dmabuf_frame_v1 *frame,
std::uint32_t index,
std::int32_t fd,
std::uint32_t size,
std::uint32_t offset,
std::uint32_t stride,
std::uint32_t plane_index);
void ready(
zwlr_export_dmabuf_frame_v1 *frame,
std::uint32_t tv_sec_hi, std::uint32_t tv_sec_lo, std::uint32_t tv_nsec);
void cancel(
zwlr_export_dmabuf_frame_v1 *frame,
zwlr_export_dmabuf_frame_v1_cancel_reason reason);
inline frame_t *get_next_frame() {
return current_frame == &frames[0] ? &frames[1] : &frames[0];
}
status_e status;
std::array<frame_t, 2> frames;
frame_t *current_frame;
zwlr_export_dmabuf_frame_v1_listener listener;
};
class monitor_t {
public:
monitor_t(monitor_t &&) = delete;
monitor_t(const monitor_t &) = delete;
monitor_t &operator=(const monitor_t &) = delete;
monitor_t &operator=(monitor_t &&) = delete;
monitor_t(wl_output *output);
void xdg_name(zxdg_output_v1 *, const char *name);
void xdg_description(zxdg_output_v1 *, const char *description);
void xdg_position(zxdg_output_v1 *, std::int32_t x, std::int32_t y);
void xdg_size(zxdg_output_v1 *, std::int32_t width, std::int32_t height);
void xdg_done(zxdg_output_v1 *);
void listen(zxdg_output_manager_v1 *output_manager);
wl_output *output;
std::string name;
std::string description;
platf::touch_port_t viewport;
zxdg_output_v1_listener listener;
};
class interface_t {
struct bind_t {
std::uint32_t id;
std::uint32_t version;
};
public:
enum interface_e {
XDG_OUTPUT,
WLR_EXPORT_DMABUF,
MAX_INTERFACES,
};
interface_t(interface_t &&) = delete;
interface_t(const interface_t &) = delete;
interface_t &operator=(const interface_t &) = delete;
interface_t &operator=(interface_t &&) = delete;
interface_t() noexcept;
void listen(wl_registry *registry);
std::vector<std::unique_ptr<monitor_t>> monitors;
zwlr_export_dmabuf_manager_v1 *dmabuf_manager;
zxdg_output_manager_v1 *output_manager;
bool operator[](interface_e bit) const {
return interface[bit];
}
private:
void add_interface(wl_registry *registry, std::uint32_t id, const char *interface, std::uint32_t version);
void del_interface(wl_registry *registry, uint32_t id);
std::bitset<MAX_INTERFACES> interface;
wl_registry_listener listener;
};
class display_t {
public:
/**
* Initialize display with display_name
* If display_name == nullptr -> display_name = std::getenv("WAYLAND_DISPLAY")
*/
int init(const char *display_name = nullptr);
// Roundtrip with Wayland connection
void roundtrip();
// Get the registry associated with the display
// No need to manually free the registry
wl_registry *registry();
inline display_internal_t::pointer get() {
return display_internal.get();
}
private:
display_internal_t display_internal;
};
std::vector<std::unique_ptr<monitor_t>> monitors(const char *display_name = nullptr);
int init();
} // namespace wl
#else
struct wl_output;
struct zxdg_output_manager_v1;
namespace wl {
class monitor_t {
public:
monitor_t(monitor_t &&) = delete;
monitor_t(const monitor_t &) = delete;
monitor_t &operator=(const monitor_t &) = delete;
monitor_t &operator=(monitor_t &&) = delete;
monitor_t(wl_output *output);
void listen(zxdg_output_manager_v1 *output_manager);
wl_output *output;
std::string name;
std::string description;
platf::touch_port_t viewport;
};
inline std::vector<std::unique_ptr<monitor_t>> monitors(const char *display_name = nullptr) { return {}; }
inline int init() { return -1; }
} // namespace wl
#endif
#endif

View File

@@ -0,0 +1,369 @@
#include "sunshine/platform/common.h"
#include "sunshine/main.h"
#include "vaapi.h"
#include "wayland.h"
using namespace std::literals;
namespace wl {
static int env_width;
static int env_height;
struct img_t : public platf::img_t {
~img_t() override {
delete[] data;
data = nullptr;
}
};
class wlr_t : public platf::display_t {
public:
int init(platf::mem_type_e hwdevice_type, const std::string &display_name, int framerate) {
delay = std::chrono::nanoseconds { 1s } / framerate;
mem_type = hwdevice_type;
if(display.init()) {
return -1;
}
interface.listen(display.registry());
display.roundtrip();
if(!interface[wl::interface_t::XDG_OUTPUT]) {
BOOST_LOG(error) << "Missing Wayland wire for xdg_output"sv;
return -1;
}
if(!interface[wl::interface_t::WLR_EXPORT_DMABUF]) {
BOOST_LOG(error) << "Missing Wayland wire for wlr-export-dmabuf"sv;
return -1;
}
auto monitor = interface.monitors[0].get();
if(!display_name.empty()) {
auto streamedMonitor = util::from_view(display_name);
if(streamedMonitor >= 0 && streamedMonitor < interface.monitors.size()) {
monitor = interface.monitors[streamedMonitor].get();
}
}
monitor->listen(interface.output_manager);
display.roundtrip();
output = monitor->output;
offset_x = monitor->viewport.offset_x;
offset_y = monitor->viewport.offset_y;
width = monitor->viewport.width;
height = monitor->viewport.height;
this->env_width = ::wl::env_width;
this->env_height = ::wl::env_height;
BOOST_LOG(info) << "Selected monitor ["sv << monitor->description << "] for streaming"sv;
BOOST_LOG(debug) << "Offset: "sv << offset_x << 'x' << offset_y;
BOOST_LOG(debug) << "Resolution: "sv << width << 'x' << height;
BOOST_LOG(debug) << "Desktop Resolution: "sv << env_width << 'x' << env_height;
return 0;
}
int dummy_img(platf::img_t *img) override {
return 0;
}
inline platf::capture_e snapshot(platf::img_t *img_out_base, std::chrono::milliseconds timeout, bool cursor) {
auto to = std::chrono::steady_clock::now() + timeout;
dmabuf.listen(interface.dmabuf_manager, output, cursor);
do {
display.roundtrip();
if(to < std::chrono::steady_clock::now()) {
return platf::capture_e::timeout;
}
} while(dmabuf.status == dmabuf_t::WAITING);
auto current_frame = dmabuf.current_frame;
if(
dmabuf.status == dmabuf_t::REINIT ||
current_frame->sd.width != width ||
current_frame->sd.height != height) {
return platf::capture_e::reinit;
}
return platf::capture_e::ok;
}
platf::mem_type_e mem_type;
std::chrono::nanoseconds delay;
wl::display_t display;
interface_t interface;
dmabuf_t dmabuf;
wl_output *output;
};
class wlr_ram_t : public wlr_t {
public:
platf::capture_e capture(snapshot_cb_t &&snapshot_cb, std::shared_ptr<platf::img_t> img, bool *cursor) override {
auto next_frame = std::chrono::steady_clock::now();
while(img) {
auto now = std::chrono::steady_clock::now();
if(next_frame > now) {
std::this_thread::sleep_for((next_frame - now) / 3 * 2);
}
while(next_frame > now) {
now = std::chrono::steady_clock::now();
}
next_frame = now + delay;
auto status = snapshot(img.get(), 1000ms, *cursor);
switch(status) {
case platf::capture_e::reinit:
case platf::capture_e::error:
return status;
case platf::capture_e::timeout:
continue;
case platf::capture_e::ok:
img = snapshot_cb(img);
break;
default:
BOOST_LOG(error) << "Unrecognized capture status ["sv << (int)status << ']';
return status;
}
}
return platf::capture_e::ok;
}
platf::capture_e snapshot(platf::img_t *img_out_base, std::chrono::milliseconds timeout, bool cursor) {
auto status = wlr_t::snapshot(img_out_base, timeout, cursor);
if(status != platf::capture_e::ok) {
return status;
}
auto current_frame = dmabuf.current_frame;
auto rgb_opt = egl::import_source(egl_display.get(), current_frame->sd);
if(!rgb_opt) {
return platf::capture_e::reinit;
}
gl::ctx.BindTexture(GL_TEXTURE_2D, (*rgb_opt)->tex[0]);
gl::ctx.GetTextureSubImage((*rgb_opt)->tex[0], 0, 0, 0, 0, width, height, 1, GL_BGRA, GL_UNSIGNED_BYTE, img_out_base->height * img_out_base->row_pitch, img_out_base->data);
gl::ctx.BindTexture(GL_TEXTURE_2D, 0);
return platf::capture_e::ok;
}
int init(platf::mem_type_e hwdevice_type, const std::string &display_name, int framerate) {
if(wlr_t::init(hwdevice_type, display_name, framerate)) {
return -1;
}
egl_display = egl::make_display(display.get());
if(!egl_display) {
return -1;
}
auto ctx_opt = egl::make_ctx(egl_display.get());
if(!ctx_opt) {
return -1;
}
ctx = std::move(*ctx_opt);
return 0;
}
std::shared_ptr<platf::hwdevice_t> make_hwdevice(platf::pix_fmt_e pix_fmt) override {
if(mem_type == platf::mem_type_e::vaapi) {
return va::make_hwdevice(width, height, false);
}
return std::make_shared<platf::hwdevice_t>();
}
std::shared_ptr<platf::img_t> alloc_img() override {
auto img = std::make_shared<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;
}
egl::display_t egl_display;
egl::ctx_t ctx;
};
class wlr_vram_t : public wlr_t {
public:
platf::capture_e capture(snapshot_cb_t &&snapshot_cb, std::shared_ptr<platf::img_t> img, bool *cursor) override {
auto next_frame = std::chrono::steady_clock::now();
while(img) {
auto now = std::chrono::steady_clock::now();
if(next_frame > now) {
std::this_thread::sleep_for((next_frame - now) / 3 * 2);
}
while(next_frame > now) {
now = std::chrono::steady_clock::now();
}
next_frame = now + delay;
auto status = snapshot(img.get(), 1000ms, *cursor);
switch(status) {
case platf::capture_e::reinit:
case platf::capture_e::error:
return status;
case platf::capture_e::timeout:
continue;
case platf::capture_e::ok:
img = snapshot_cb(img);
break;
default:
BOOST_LOG(error) << "Unrecognized capture status ["sv << (int)status << ']';
return status;
}
}
return platf::capture_e::ok;
}
platf::capture_e snapshot(platf::img_t *img_out_base, std::chrono::milliseconds timeout, bool cursor) {
auto status = wlr_t::snapshot(img_out_base, timeout, cursor);
if(status != platf::capture_e::ok) {
return status;
}
auto img = (egl::img_descriptor_t *)img_out_base;
img->reset();
auto current_frame = dmabuf.current_frame;
++sequence;
img->sequence = sequence;
img->sd = current_frame->sd;
// Prevent dmabuf from closing the file descriptors.
std::fill_n(current_frame->sd.fds, 4, -1);
return platf::capture_e::ok;
}
std::shared_ptr<platf::img_t> alloc_img() override {
auto img = std::make_shared<egl::img_descriptor_t>();
img->sequence = 0;
img->serial = std::numeric_limits<decltype(img->serial)>::max();
img->data = nullptr;
// File descriptors aren't open
std::fill_n(img->sd.fds, 4, -1);
return img;
}
std::shared_ptr<platf::hwdevice_t> make_hwdevice(platf::pix_fmt_e pix_fmt) override {
if(mem_type == platf::mem_type_e::vaapi) {
return va::make_hwdevice(width, height, 0, 0, true);
}
return std::make_shared<platf::hwdevice_t>();
}
int dummy_img(platf::img_t *img) override {
return snapshot(img, 1000ms, false) != platf::capture_e::ok;
}
std::uint64_t sequence {};
};
} // namespace wl
namespace platf {
std::shared_ptr<display_t> wl_display(mem_type_e hwdevice_type, const std::string &display_name, int framerate) {
if(hwdevice_type != platf::mem_type_e::system && hwdevice_type != platf::mem_type_e::vaapi && hwdevice_type != platf::mem_type_e::cuda) {
BOOST_LOG(error) << "Could not initialize display with the given hw device type."sv;
return nullptr;
}
if(hwdevice_type == platf::mem_type_e::vaapi) {
auto wlr = std::make_shared<wl::wlr_vram_t>();
if(wlr->init(hwdevice_type, display_name, framerate)) {
return nullptr;
}
return wlr;
}
auto wlr = std::make_shared<wl::wlr_ram_t>();
if(wlr->init(hwdevice_type, display_name, framerate)) {
return nullptr;
}
return wlr;
}
std::vector<std::string> wl_display_names() {
std::vector<std::string> display_names;
wl::display_t display;
if(display.init()) {
return {};
}
wl::interface_t interface;
interface.listen(display.registry());
display.roundtrip();
if(!interface[wl::interface_t::XDG_OUTPUT]) {
BOOST_LOG(warning) << "Missing Wayland wire for xdg_output"sv;
return {};
}
if(!interface[wl::interface_t::WLR_EXPORT_DMABUF]) {
BOOST_LOG(warning) << "Missing Wayland wire for wlr-export-dmabuf"sv;
return {};
}
wl::env_width = 0;
wl::env_height = 0;
for(auto &monitor : interface.monitors) {
monitor->listen(interface.output_manager);
}
display.roundtrip();
for(int x = 0; x < interface.monitors.size(); ++x) {
auto monitor = interface.monitors[x].get();
wl::env_width = std::max(wl::env_width, (int)(monitor->viewport.offset_x + monitor->viewport.width));
wl::env_height = std::max(wl::env_height, (int)(monitor->viewport.offset_y + monitor->viewport.height));
display_names.emplace_back(std::to_string(x));
}
return display_names;
}
} // namespace platf

View File

@@ -0,0 +1,837 @@
//
// 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 "cuda.h"
#include "graphics.h"
#include "misc.h"
#include "vaapi.h"
#include "x11grab.h"
using namespace std::literals;
namespace platf {
int load_xcb();
int load_x11();
namespace x11 {
#define _FN(x, ret, args) \
typedef ret(*x##_fn) args; \
static x##_fn x
_FN(GetImage, XImage *,
(
Display * display,
Drawable d,
int x, int y,
unsigned int width, unsigned int height,
unsigned long plane_mask,
int format));
_FN(OpenDisplay, Display *, (_Xconst char *display_name));
_FN(GetWindowAttributes, Status,
(
Display * display,
Window w,
XWindowAttributes *window_attributes_return));
_FN(CloseDisplay, int, (Display * display));
_FN(Free, int, (void *data));
_FN(InitThreads, Status, (void));
namespace rr {
_FN(GetScreenResources, XRRScreenResources *, (Display * dpy, Window window));
_FN(GetOutputInfo, XRROutputInfo *, (Display * dpy, XRRScreenResources *resources, RROutput output));
_FN(GetCrtcInfo, XRRCrtcInfo *, (Display * dpy, XRRScreenResources *resources, RRCrtc crtc));
_FN(FreeScreenResources, void, (XRRScreenResources * resources));
_FN(FreeOutputInfo, void, (XRROutputInfo * outputInfo));
_FN(FreeCrtcInfo, void, (XRRCrtcInfo * crtcInfo));
int init() {
static void *handle { nullptr };
static bool funcs_loaded = false;
if(funcs_loaded) return 0;
if(!handle) {
handle = dyn::handle({ "libXrandr.so.2", "libXrandr.so" });
if(!handle) {
return -1;
}
}
std::vector<std::tuple<dyn::apiproc *, const char *>> funcs {
{ (dyn::apiproc *)&GetScreenResources, "XRRGetScreenResources" },
{ (dyn::apiproc *)&GetOutputInfo, "XRRGetOutputInfo" },
{ (dyn::apiproc *)&GetCrtcInfo, "XRRGetCrtcInfo" },
{ (dyn::apiproc *)&FreeScreenResources, "XRRFreeScreenResources" },
{ (dyn::apiproc *)&FreeOutputInfo, "XRRFreeOutputInfo" },
{ (dyn::apiproc *)&FreeCrtcInfo, "XRRFreeCrtcInfo" },
};
if(dyn::load(handle, funcs)) {
return -1;
}
funcs_loaded = true;
return 0;
}
} // namespace rr
namespace fix {
_FN(GetCursorImage, XFixesCursorImage *, (Display * dpy));
int init() {
static void *handle { nullptr };
static bool funcs_loaded = false;
if(funcs_loaded) return 0;
if(!handle) {
handle = dyn::handle({ "libXfixes.so.3", "libXfixes.so" });
if(!handle) {
return -1;
}
}
std::vector<std::tuple<dyn::apiproc *, const char *>> funcs {
{ (dyn::apiproc *)&GetCursorImage, "XFixesGetCursorImage" },
};
if(dyn::load(handle, funcs)) {
return -1;
}
funcs_loaded = true;
return 0;
}
} // namespace fix
int init() {
static void *handle { nullptr };
static bool funcs_loaded = false;
if(funcs_loaded) return 0;
if(!handle) {
handle = dyn::handle({ "libX11.so.6", "libX11.so" });
if(!handle) {
return -1;
}
}
std::vector<std::tuple<dyn::apiproc *, const char *>> funcs {
{ (dyn::apiproc *)&GetImage, "XGetImage" },
{ (dyn::apiproc *)&OpenDisplay, "XOpenDisplay" },
{ (dyn::apiproc *)&GetWindowAttributes, "XGetWindowAttributes" },
{ (dyn::apiproc *)&Free, "XFree" },
{ (dyn::apiproc *)&CloseDisplay, "XCloseDisplay" },
{ (dyn::apiproc *)&InitThreads, "XInitThreads" },
};
if(dyn::load(handle, funcs)) {
return -1;
}
funcs_loaded = true;
return 0;
}
} // namespace x11
namespace xcb {
static xcb_extension_t *shm_id;
_FN(shm_get_image_reply, xcb_shm_get_image_reply_t *,
(
xcb_connection_t * c,
xcb_shm_get_image_cookie_t cookie,
xcb_generic_error_t **e));
_FN(shm_get_image_unchecked, xcb_shm_get_image_cookie_t,
(
xcb_connection_t * c,
xcb_drawable_t drawable,
int16_t x, int16_t y,
uint16_t width, uint16_t height,
uint32_t plane_mask,
uint8_t format,
xcb_shm_seg_t shmseg,
uint32_t offset));
_FN(shm_attach, xcb_void_cookie_t,
(xcb_connection_t * c,
xcb_shm_seg_t shmseg,
uint32_t shmid,
uint8_t read_only));
_FN(get_extension_data, xcb_query_extension_reply_t *,
(xcb_connection_t * c, xcb_extension_t *ext));
_FN(get_setup, xcb_setup_t *, (xcb_connection_t * c));
_FN(disconnect, void, (xcb_connection_t * c));
_FN(connection_has_error, int, (xcb_connection_t * c));
_FN(connect, xcb_connection_t *, (const char *displayname, int *screenp));
_FN(setup_roots_iterator, xcb_screen_iterator_t, (const xcb_setup_t *R));
_FN(generate_id, std::uint32_t, (xcb_connection_t * c));
int init_shm() {
static void *handle { nullptr };
static bool funcs_loaded = false;
if(funcs_loaded) return 0;
if(!handle) {
handle = dyn::handle({ "libxcb-shm.so.0", "libxcb-shm.so" });
if(!handle) {
return -1;
}
}
std::vector<std::tuple<dyn::apiproc *, const char *>> funcs {
{ (dyn::apiproc *)&shm_id, "xcb_shm_id" },
{ (dyn::apiproc *)&shm_get_image_reply, "xcb_shm_get_image_reply" },
{ (dyn::apiproc *)&shm_get_image_unchecked, "xcb_shm_get_image_unchecked" },
{ (dyn::apiproc *)&shm_attach, "xcb_shm_attach" },
};
if(dyn::load(handle, funcs)) {
return -1;
}
funcs_loaded = true;
return 0;
}
int init() {
static void *handle { nullptr };
static bool funcs_loaded = false;
if(funcs_loaded) return 0;
if(!handle) {
handle = dyn::handle({ "libxcb.so.1", "libxcb.so" });
if(!handle) {
return -1;
}
}
std::vector<std::tuple<dyn::apiproc *, const char *>> funcs {
{ (dyn::apiproc *)&get_extension_data, "xcb_get_extension_data" },
{ (dyn::apiproc *)&get_setup, "xcb_get_setup" },
{ (dyn::apiproc *)&disconnect, "xcb_disconnect" },
{ (dyn::apiproc *)&connection_has_error, "xcb_connection_has_error" },
{ (dyn::apiproc *)&connect, "xcb_connect" },
{ (dyn::apiproc *)&setup_roots_iterator, "xcb_setup_roots_iterator" },
{ (dyn::apiproc *)&generate_id, "xcb_generate_id" },
};
if(dyn::load(handle, funcs)) {
return -1;
}
funcs_loaded = true;
return 0;
}
#undef _FN
} // namespace xcb
void freeImage(XImage *);
void freeX(XFixesCursorImage *);
using xcb_connect_t = util::dyn_safe_ptr<xcb_connection_t, &xcb::disconnect>;
using xcb_img_t = util::c_ptr<xcb_shm_get_image_reply_t>;
using ximg_t = util::safe_ptr<XImage, freeImage>;
using xcursor_t = util::safe_ptr<XFixesCursorImage, freeX>;
using crtc_info_t = util::dyn_safe_ptr<_XRRCrtcInfo, &x11::rr::FreeCrtcInfo>;
using output_info_t = util::dyn_safe_ptr<_XRROutputInfo, &x11::rr::FreeOutputInfo>;
using screen_res_t = util::dyn_safe_ptr<_XRRScreenResources, &x11::rr::FreeScreenResources>;
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;
}
};
static void blend_cursor(Display *display, img_t &img, int offsetX, int offsetY) {
xcursor_t overlay { x11::fix::GetCursorImage(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 {
std::chrono::nanoseconds delay;
x11::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 { x11::OpenDisplay(nullptr) }, xwindow {}, xattr {}, mem_type { mem_type } {
x11::InitThreads();
}
int init(const std::string &display_name, int framerate) {
if(!xdisplay) {
BOOST_LOG(error) << "Could not open X11 display"sv;
return -1;
}
delay = std::chrono::nanoseconds { 1s } / framerate;
xwindow = DefaultRootWindow(xdisplay.get());
refresh();
int streamedMonitor = -1;
if(!display_name.empty()) {
streamedMonitor = (int)util::from_view(display_name);
}
if(streamedMonitor != -1) {
BOOST_LOG(info) << "Configuring selected monitor ("sv << streamedMonitor << ") to stream"sv;
screen_res_t screenr { x11::rr::GetScreenResources(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 { x11::rr::GetOutputInfo(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;
}
if(result->crtc) {
crtc_info_t crt_info { x11::rr::GetCrtcInfo(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 {
BOOST_LOG(warning) << "Couldn't get requested display info, defaulting to recording entire virtual desktop"sv;
width = xattr.width;
height = xattr.height;
}
}
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() {
x11::GetWindowAttributes(xdisplay.get(), xwindow, &xattr); //Update xattr's
}
capture_e capture(snapshot_cb_t &&snapshot_cb, std::shared_ptr<img_t> img, bool *cursor) override {
auto next_frame = std::chrono::steady_clock::now();
while(img) {
auto now = std::chrono::steady_clock::now();
if(next_frame > now) {
std::this_thread::sleep_for((next_frame - now) / 3 * 2);
}
while(next_frame > now) {
std::this_thread::sleep_for(1ns);
now = std::chrono::steady_clock::now();
}
next_frame = now + delay;
auto status = snapshot(img.get(), 1000ms, *cursor);
switch(status) {
case platf::capture_e::reinit:
case platf::capture_e::error:
return status;
case platf::capture_e::timeout:
std::this_thread::sleep_for(1ms);
continue;
case platf::capture_e::ok:
img = snapshot_cb(img);
break;
default:
BOOST_LOG(error) << "Unrecognized capture status ["sv << (int)status << ']';
return status;
}
}
return capture_e::ok;
}
capture_e snapshot(img_t *img_out_base, std::chrono::milliseconds timeout, bool cursor) {
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 { x11::GetImage(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 va::make_hwdevice(width, height, false);
}
#ifdef SUNSHINE_BUILD_CUDA
if(mem_type == mem_type_e::cuda) {
return cuda::make_hwdevice(width, height, false);
}
#endif
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 {
x11::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 { x11::OpenDisplay(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 capture(snapshot_cb_t &&snapshot_cb, std::shared_ptr<img_t> img, bool *cursor) override {
auto next_frame = std::chrono::steady_clock::now();
while(img) {
auto now = std::chrono::steady_clock::now();
if(next_frame > now) {
std::this_thread::sleep_for((next_frame - now) / 3 * 2);
}
while(next_frame > now) {
std::this_thread::sleep_for(1ns);
now = std::chrono::steady_clock::now();
}
next_frame = now + delay;
auto status = snapshot(img.get(), 1000ms, *cursor);
switch(status) {
case platf::capture_e::reinit:
case platf::capture_e::error:
return status;
case platf::capture_e::timeout:
std::this_thread::sleep_for(1ms);
continue;
case platf::capture_e::ok:
img = snapshot_cb(img);
break;
default:
BOOST_LOG(error) << "Unrecognized capture status ["sv << (int)status << ']';
return status;
}
}
return capture_e::ok;
}
capture_e snapshot(img_t *img, std::chrono::milliseconds timeout, bool cursor) {
//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(const std::string &display_name, int framerate) {
if(x11_attr_t::init(display_name, framerate)) {
return 1;
}
shm_xdisplay.reset(x11::OpenDisplay(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> x11_display(platf::mem_type_e hwdevice_type, const std::string &display_name, int framerate) {
if(hwdevice_type != platf::mem_type_e::system && hwdevice_type != platf::mem_type_e::vaapi && hwdevice_type != platf::mem_type_e::cuda) {
BOOST_LOG(error) << "Could not initialize x11 display with the given hw device type"sv;
return nullptr;
}
if(xcb::init_shm() || xcb::init() || x11::init() || x11::rr::init() || x11::fix::init()) {
BOOST_LOG(error) << "Couldn't init x11 libraries"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(display_name, framerate);
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(display_name, framerate)) {
return nullptr;
}
return x11_disp;
}
std::vector<std::string> x11_display_names() {
if(load_x11() || load_xcb()) {
BOOST_LOG(error) << "Couldn't init x11 libraries"sv;
return {};
}
BOOST_LOG(info) << "Detecting connected monitors"sv;
x11::xdisplay_t xdisplay { x11::OpenDisplay(nullptr) };
if(!xdisplay) {
return {};
}
auto xwindow = DefaultRootWindow(xdisplay.get());
screen_res_t screenr { x11::rr::GetScreenResources(xdisplay.get(), xwindow) };
int output = screenr->noutput;
int monitor = 0;
for(int x = 0; x < output; ++x) {
output_info_t out_info { x11::rr::GetOutputInfo(xdisplay.get(), screenr.get(), screenr->outputs[x]) };
if(out_info && out_info->connection == RR_Connected) {
++monitor;
}
}
std::vector<std::string> names;
names.reserve(monitor);
for(auto x = 0; x < monitor; ++x) {
names.emplace_back(std::to_string(x));
}
return names;
}
void freeImage(XImage *p) {
XDestroyImage(p);
}
void freeX(XFixesCursorImage *p) {
x11::Free(p);
}
int load_xcb() {
// This will be called once only
static int xcb_status = xcb::init_shm() || xcb::init();
return xcb_status;
}
int load_x11() {
// This will be called once only
static int x11_status =
window_system == window_system_e::NONE ||
x11::init() || x11::rr::init() || x11::fix::init();
return x11_status;
}
namespace x11 {
std::optional<cursor_t> cursor_t::make() {
if(load_x11()) {
return std::nullopt;
}
cursor_t cursor;
cursor.ctx.reset((cursor_ctx_t::pointer)x11::OpenDisplay(nullptr));
return cursor;
}
void cursor_t::capture(egl::cursor_t &img) {
auto display = (xdisplay_t::pointer)ctx.get();
xcursor_t xcursor = fix::GetCursorImage(display);
if(img.serial != xcursor->cursor_serial) {
auto buf_size = xcursor->width * xcursor->height * sizeof(int);
if(img.buffer.size() < buf_size) {
img.buffer.resize(buf_size);
}
std::transform(xcursor->pixels, xcursor->pixels + buf_size / 4, (int *)img.buffer.data(), [](long pixel) -> int {
return pixel;
});
}
img.data = img.buffer.data();
img.width = xcursor->width;
img.height = xcursor->height;
img.x = xcursor->x - xcursor->xhot;
img.y = xcursor->y - xcursor->yhot;
img.pixel_pitch = 4;
img.row_pitch = img.pixel_pitch * img.width;
img.serial = xcursor->cursor_serial;
}
void cursor_t::blend(img_t &img, int offsetX, int offsetY) {
blend_cursor((xdisplay_t::pointer)ctx.get(), img, offsetX, offsetY);
}
xdisplay_t make_display() {
return OpenDisplay(nullptr);
}
void freeDisplay(_XDisplay *xdisplay) {
CloseDisplay(xdisplay);
}
void freeCursorCtx(cursor_ctx_t::pointer ctx) {
CloseDisplay((xdisplay_t::pointer)ctx);
}
} // namespace x11
} // namespace platf

View File

@@ -0,0 +1,60 @@
#ifndef SUNSHINE_X11_GRAB
#define SUNSHINE_X11_GRAB
#include <optional>
#include "sunshine/platform/common.h"
#include "sunshine/utility.h"
// X11 Display
extern "C" struct _XDisplay;
namespace egl {
class cursor_t;
}
namespace platf::x11 {
#ifdef SUNSHINE_BUILD_X11
struct cursor_ctx_raw_t;
void freeCursorCtx(cursor_ctx_raw_t *ctx);
void freeDisplay(_XDisplay *xdisplay);
using cursor_ctx_t = util::safe_ptr<cursor_ctx_raw_t, freeCursorCtx>;
using xdisplay_t = util::safe_ptr<_XDisplay, freeDisplay>;
class cursor_t {
public:
static std::optional<cursor_t> make();
void capture(egl::cursor_t &img);
/**
* Capture and blend the cursor into the image
*
* img <-- destination image
* offsetX, offsetY <--- Top left corner of the virtual screen
*/
void blend(img_t &img, int offsetX, int offsetY);
cursor_ctx_t ctx;
};
xdisplay_t make_display();
#else
// It's never something different from nullptr
util::safe_ptr<_XDisplay, std::default_delete<_XDisplay>>;
class cursor_t {
public:
static std::optional<cursor_t> make() { return std::nullopt; }
void capture(egl::cursor_t &) {}
void blend(img_t &, int, int) {}
};
xdisplay_t make_display() { return nullptr; }
#endif
} // namespace platf::x11
#endif

View File

@@ -22,19 +22,31 @@ 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>>;
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>>;
using vs_t = util::safe_ptr<ID3D11VertexShader, Release<ID3D11VertexShader>>;
using ps_t = util::safe_ptr<ID3D11PixelShader, Release<ID3D11PixelShader>>;
using blend_t = util::safe_ptr<ID3D11BlendState, Release<ID3D11BlendState>>;
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 raster_state_t = util::safe_ptr<ID3D11RasterizerState, Release<ID3D11RasterizerState>>;
using sampler_state_t = util::safe_ptr<ID3D11SamplerState, Release<ID3D11SamplerState>>;
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>>;
namespace video {
using device_t = util::safe_ptr<ID3D11VideoDevice, Release<ID3D11VideoDevice>>;
@@ -54,10 +66,29 @@ struct cursor_t {
bool visible;
};
struct gpu_cursor_t {
texture2d_t texture;
class gpu_cursor_t {
public:
gpu_cursor_t() : cursor_view { 0, 0, 0, 0, 0.0f, 1.0f } {};
void set_pos(LONG rel_x, LONG rel_y, bool visible) {
cursor_view.TopLeftX = rel_x;
cursor_view.TopLeftY = rel_y;
LONG width, height;
this->visible = visible;
}
void set_texture(LONG width, LONG height, texture2d_t &&texture) {
cursor_view.Width = width;
cursor_view.Height = height;
this->texture = std::move(texture);
}
texture2d_t texture;
shader_res_t input_res;
D3D11_VIEWPORT cursor_view;
bool visible;
};
class duplication_t {
@@ -74,7 +105,9 @@ public:
class display_base_t : public display_t {
public:
int init();
int init(int framerate, const std::string &display_name);
std::chrono::nanoseconds delay;
factory1_t factory;
adapter_t adapter;
@@ -100,11 +133,14 @@ public:
class display_ram_t : public display_base_t {
public:
capture_e snapshot(img_t *img, std::chrono::milliseconds timeout, bool cursor_visible) override;
capture_e capture(snapshot_cb_t &&snapshot_cb, std::shared_ptr<img_t> img, bool *cursor) override;
capture_e snapshot(img_t *img, std::chrono::milliseconds timeout, bool cursor_visible);
std::shared_ptr<img_t> alloc_img() override;
int dummy_img(img_t *img) override;
int init();
int init(int framerate, const std::string &display_name);
cursor_t cursor;
D3D11_MAPPED_SUBRESOURCE img_info;
@@ -113,15 +149,26 @@ public:
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;
capture_e capture(snapshot_cb_t &&snapshot_cb, std::shared_ptr<img_t> img, bool *cursor) override;
capture_e snapshot(img_t *img, std::chrono::milliseconds timeout, bool cursor_visible);
std::shared_ptr<img_t> alloc_img() override;
int dummy_img(img_t *img_base) override;
std::shared_ptr<platf::hwdevice_t> make_hwdevice(int width, int height, pix_fmt_e pix_fmt) override;
int init(int framerate, const std::string &display_name);
std::shared_ptr<platf::hwdevice_t> make_hwdevice(pix_fmt_e pix_fmt) override;
sampler_state_t sampler_linear;
blend_t blend_enable;
blend_t blend_disable;
ps_t scene_ps;
vs_t scene_vs;
texture2d_t src;
gpu_cursor_t cursor;
std::vector<hwdevice_t *> hwdevices;
};
} // namespace platf::dxgi

View File

@@ -4,12 +4,12 @@
#include <codecvt>
#include "display.h"
#include "misc.h"
#include "sunshine/config.h"
#include "sunshine/main.h"
#include "sunshine/platform/common.h"
#include "display.h"
namespace platf {
using namespace std::literals;
}
@@ -73,7 +73,7 @@ duplication_t::~duplication_t() {
release_frame();
}
int display_base_t::init() {
int display_base_t::init(int framerate, const std::string &display_name) {
/* Uncomment when use of IDXGIOutput5 is implemented
std::call_once(windows_cpp_once_flag, []() {
DECLARE_HANDLE(DPI_AWARENESS_CONTEXT);
@@ -91,6 +91,15 @@ int display_base_t::init() {
});
*/
// Ensure we can duplicate the current display
syncThreadDesktop();
delay = std::chrono::nanoseconds { 1s } / framerate;
// 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);
@@ -102,7 +111,7 @@ int display_base_t::init() {
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);
auto output_name = converter.from_bytes(display_name);
adapter_t::pointer adapter_p;
for(int x = 0; factory->EnumAdapters1(x, &adapter_p) != DXGI_ERROR_NOT_FOUND; ++x) {
@@ -133,6 +142,11 @@ int display_base_t::init() {
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);
}
}
@@ -195,7 +209,9 @@ int display_base_t::init() {
<< "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;
<< "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
{
@@ -417,22 +433,81 @@ const char *format_str[] = {
} // namespace platf::dxgi
namespace platf {
std::shared_ptr<display_t> display(dev_type_e hwdevice_type) {
if(hwdevice_type == dev_type_e::dxgi) {
std::shared_ptr<display_t> display(mem_type_e hwdevice_type, const std::string &display_name, int framerate) {
if(hwdevice_type == mem_type_e::dxgi) {
auto disp = std::make_shared<dxgi::display_vram_t>();
if(!disp->init()) {
if(!disp->init(framerate, display_name)) {
return disp;
}
}
else if(hwdevice_type == dev_type_e::none) {
else if(hwdevice_type == mem_type_e::system) {
auto disp = std::make_shared<dxgi::display_ram_t>();
if(!disp->init()) {
if(!disp->init(framerate, display_name)) {
return disp;
}
}
return nullptr;
}
std::vector<std::string> display_names(mem_type_e) {
std::vector<std::string> display_names;
HRESULT status;
BOOST_LOG(debug) << "Detecting monitors..."sv;
std::wstring_convert<std::codecvt_utf8_utf16<wchar_t>, wchar_t> converter;
dxgi::factory1_t factory;
status = CreateDXGIFactory1(IID_IDXGIFactory1, (void **)&factory);
if(FAILED(status)) {
BOOST_LOG(error) << "Failed to create DXGIFactory1 [0x"sv << util::hex(status).to_string_view() << ']' << std::endl;
return {};
}
dxgi::adapter_t adapter;
for(int x = 0; factory->EnumAdapters1(x, &adapter) != DXGI_ERROR_NOT_FOUND; ++x) {
DXGI_ADAPTER_DESC1 adapter_desc;
adapter->GetDesc1(&adapter_desc);
BOOST_LOG(debug)
<< std::endl
<< "====== ADAPTER ====="sv << std::endl
<< "Device Name : "sv << converter.to_bytes(adapter_desc.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
<< std::endl
<< " ====== OUTPUT ======"sv << std::endl;
dxgi::output_t::pointer output_p {};
for(int y = 0; adapter->EnumOutputs(y, &output_p) != DXGI_ERROR_NOT_FOUND; ++y) {
dxgi::output_t output { output_p };
DXGI_OUTPUT_DESC desc;
output->GetDesc(&desc);
auto device_name = converter.to_bytes(desc.DeviceName);
auto width = desc.DesktopCoordinates.right - desc.DesktopCoordinates.left;
auto height = desc.DesktopCoordinates.bottom - desc.DesktopCoordinates.top;
BOOST_LOG(debug)
<< " Output Name : "sv << device_name << std::endl
<< " AttachedToDesktop : "sv << (desc.AttachedToDesktop ? "yes"sv : "no"sv) << std::endl
<< " Resolution : "sv << width << 'x' << height << std::endl
<< std::endl;
display_names.emplace_back(std::move(device_name));
}
}
return display_names;
}
} // namespace platf

View File

@@ -165,6 +165,36 @@ void blend_cursor(const cursor_t &cursor, img_t &img) {
}
}
capture_e display_ram_t::capture(snapshot_cb_t &&snapshot_cb, std::shared_ptr<::platf::img_t> img, bool *cursor) {
auto next_frame = std::chrono::steady_clock::now();
while(img) {
auto now = std::chrono::steady_clock::now();
while(next_frame > now) {
now = std::chrono::steady_clock::now();
}
next_frame = now + delay;
auto status = snapshot(img.get(), 1000ms, *cursor);
switch(status) {
case platf::capture_e::reinit:
case platf::capture_e::error:
return status;
case platf::capture_e::timeout:
std::this_thread::sleep_for(1ms);
continue;
case platf::capture_e::ok:
img = snapshot_cb(img);
break;
default:
BOOST_LOG(error) << "Unrecognized capture status ["sv << (int)status << ']';
return status;
}
}
return capture_e::ok;
}
capture_e display_ram_t::snapshot(::platf::img_t *img_base, std::chrono::milliseconds timeout, bool cursor_visible) {
auto img = (img_t *)img_base;
@@ -263,8 +293,8 @@ int display_ram_t::dummy_img(platf::img_t *img) {
return 0;
}
int display_ram_t::init() {
if(display_base_t::init()) {
int display_ram_t::init(int framerate, const std::string &display_name) {
if(display_base_t::init(framerate, display_name)) {
return -1;
}

View File

@@ -5,65 +5,28 @@
#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"
#define SUNSHINE_SHADERS_DIR SUNSHINE_ASSETS_DIR "/shaders/directx"
namespace platf {
using namespace std::literals;
}
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>>;
using float4 = DirectX::XMFLOAT4;
using float3 = DirectX::XMFLOAT3;
using float2 = DirectX::XMFLOAT2;
struct __attribute__((__aligned__(16))) color_t {
float4 color_vec_y;
float4 color_vec_u;
float4 color_vec_v;
float2 range_y;
float2 range_uv;
};
color_t make_color_matrix(float Cr, float Cb, float U_max, float V_max, float add_Y, float add_UV, float2 range_Y, float2 range_UV) {
float Cg = 1.0f - Cr - Cb;
float Cr_i = 1.0f - Cr;
float Cb_i = 1.0f - Cb;
float shift_y = range_Y.x / 256.0f;
float shift_uv = range_UV.x / 256.0f;
float scale_y = (range_Y.y - range_Y.x) / 256.0f;
float scale_uv = (range_UV.y - range_UV.x) / 256.0f;
return {
{ Cr, Cg, Cb, add_Y },
{ -(Cr * U_max / Cb_i), -(Cg * U_max / Cb_i), U_max, add_UV },
{ V_max, -(Cg * V_max / Cr_i), -(Cb * V_max / Cr_i), add_UV },
{ scale_y, shift_y },
{ scale_uv, shift_uv },
};
static void free_frame(AVFrame *frame) {
av_frame_free(&frame);
}
color_t colors[] {
make_color_matrix(0.299f, 0.114f, 0.436f, 0.615f, 0.0625, 0.5f, { 16.0f, 235.0f }, { 16.0f, 240.0f }), // BT601 MPEG
make_color_matrix(0.299f, 0.114f, 0.5f, 0.5f, 0.0f, 0.5f, { 0.0f, 255.0f }, { 0.0f, 255.0f }), // BT601 JPEG
make_color_matrix(0.2126f, 0.0722f, 0.436f, 0.615f, 0.0625, 0.5f, { 16.0f, 235.0f }, { 16.0f, 240.0f }), //BT701 MPEG
make_color_matrix(0.2126f, 0.0722f, 0.5f, 0.5f, 0.0f, 0.5f, { 0.0f, 255.0f }, { 0.0f, 255.0f }), //BT701 JPEG
};
using frame_t = util::safe_ptr<AVFrame, free_frame>;
namespace platf::dxgi {
template<class T>
buf_t make_buffer(device_t::pointer device, const T &t) {
@@ -123,10 +86,13 @@ 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;
shader_res_t input_res;
render_target_t scene_rt;
texture2d_t texture;
~img_d3d_t() override = default;
};
@@ -249,85 +215,61 @@ blob_t compile_vertex_shader(LPCSTR file) {
return compile_shader(file, "main_vs", "vs_5_0");
}
int init_rt(device_t::pointer device, shader_res_t &shader_res, render_target_t &render_target, int width, int height, DXGI_FORMAT format, texture2d_t::pointer tex) {
D3D11_SHADER_RESOURCE_VIEW_DESC shader_resource_desc {
format,
D3D11_SRV_DIMENSION_TEXTURE2D
};
shader_resource_desc.Texture2D.MipLevels = 1;
auto status = device->CreateShaderResourceView(tex, &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
};
status = device->CreateRenderTargetView(tex, &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;
}
int init_rt(device_t::pointer device, 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;
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;
}
return init_rt(device, shader_res, render_target, width, height, format, tex.get());
}
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();
}
device_ctx_p->IASetInputLayout(input_layout.get());
_init_view_port(this->img.width, this->img.height);
device_ctx_p->OMSetRenderTargets(1, &nv12_Y_rt, nullptr);
@@ -337,7 +279,7 @@ public:
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->PSSetShaderResources(0, 1, &img.input_res);
device_ctx_p->Draw(3, 0);
// Artifacts start appearing on the rendered image if Sunshine doesn't flush
@@ -352,7 +294,7 @@ public:
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->PSSetShaderResources(0, 1, &img.input_res);
device_ctx_p->Draw(3, 0);
device_ctx_p->Flush();
@@ -362,15 +304,15 @@ public:
void set_colorspace(std::uint32_t colorspace, std::uint32_t color_range) override {
switch(colorspace) {
case 5: // SWS_CS_SMPTE170M
color_p = &colors[0];
color_p = &::video::colors[0];
break;
case 1: // SWS_CS_ITU709
color_p = &colors[2];
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 = &colors[0];
color_p = &::video::colors[0];
};
if(color_range > 1) {
@@ -388,25 +330,17 @@ public:
this->color_matrix = std::move(color_matrix);
}
int init(
std::shared_ptr<platf::display_t> display, device_t::pointer device_p, device_ctx_t::pointer device_ctx_p,
int out_width, int out_height,
pix_fmt_e pix_fmt) {
HRESULT status;
int set_frame(AVFrame *frame) {
this->hwframe.reset(frame);
this->frame = frame;
device_p->AddRef();
data = device_p;
auto device_p = (device_t::pointer)data;
this->device_ctx_p = device_ctx_p;
auto out_width = frame->width;
auto out_height = frame->height;
cursor_visible = false;
cursor_view.MinDepth = 0.0f;
cursor_view.MaxDepth = 1.0f;
platf::hwdevice_t::img = &img;
float in_width = display->width;
float in_height = display->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);
@@ -420,6 +354,87 @@ public:
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 * 4;
img.pixel_pitch = 4;
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;
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() << ']';
@@ -450,30 +465,12 @@ public:
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, in_width, in_height, DXGI_FORMAT_B8G8R8A8_UNORM)) {
return -1;
}
color_matrix = make_buffer(device_p, colors[0]);
color_matrix = make_buffer(device_p, ::video::colors[0]);
if(!color_matrix) {
BOOST_LOG(error) << "Failed to create color matrix buffer"sv;
return -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_INPUT_ELEMENT_DESC layout_desc {
"SV_Position", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 0, D3D11_INPUT_PER_VERTEX_DATA, 0
};
@@ -483,61 +480,7 @@ public:
convert_UV_vs_hlsl->GetBufferPointer(), convert_UV_vs_hlsl->GetBufferSize(),
&input_layout);
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 = pix_fmt == pix_fmt_e::nv12 ? DXGI_FORMAT_NV12 : DXGI_FORMAT_P010;
t.BindFlags = D3D11_BIND_RENDER_TARGET;
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.display = std::move(display);
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;
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;
}
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;
}
img.display = std::move(display);
// Color the background black, so that the padding for keeping the aspect ratio
// is black
@@ -558,12 +501,9 @@ public:
return -1;
}
device_ctx_p->OMSetBlendState(blend_disable.get(), nullptr, 0xFFFFFFFFu);
device_ctx_p->PSSetSamplers(0, 1, &sampler_linear);
device_ctx_p->IASetInputLayout(input_layout.get());
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;
}
@@ -572,11 +512,6 @@ public:
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:
@@ -594,73 +529,21 @@ private:
_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:
color_t *color_p;
frame_t hwframe;
blend_t blend_enable;
blend_t blend_disable;
::video::color_t *color_p;
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;
// The image referenced by hwframe
// The resulting image is stored here.
img_d3d_t img;
// Clear nv12 render target to black
@@ -675,15 +558,41 @@ public:
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::capture(snapshot_cb_t &&snapshot_cb, std::shared_ptr<::platf::img_t> img, bool *cursor) {
auto next_frame = std::chrono::steady_clock::now();
while(img) {
auto now = std::chrono::steady_clock::now();
while(next_frame > now) {
now = std::chrono::steady_clock::now();
}
next_frame = now + delay;
auto status = snapshot(img.get(), 1000ms, *cursor);
switch(status) {
case platf::capture_e::reinit:
case platf::capture_e::error:
return status;
case platf::capture_e::timeout:
std::this_thread::sleep_for(1ms);
continue;
case platf::capture_e::ok:
img = snapshot_cb(img);
break;
default:
BOOST_LOG(error) << "Unrecognized capture status ["sv << (int)status << ']';
return status;
}
}
return capture_e::ok;
}
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;
@@ -746,41 +655,121 @@ capture_e display_vram_t::snapshot(platf::img_t *img_base, std::chrono::millisec
return capture_e::error;
}
for(auto *hwdevice : hwdevices) {
if(hwdevice->set_cursor_texture(texture.get(), t.Width, t.Height)) {
return capture_e::error;
}
D3D11_SHADER_RESOURCE_VIEW_DESC desc {
DXGI_FORMAT_B8G8R8A8_UNORM,
D3D11_SRV_DIMENSION_TEXTURE2D
};
desc.Texture2D.MipLevels = 1;
// Free resources before allocating on the next line.
cursor.input_res.reset();
status = device->CreateShaderResourceView(texture.get(), &desc, &cursor.input_res);
if(FAILED(status)) {
BOOST_LOG(error) << "Failed to create cursor shader resource view [0x"sv << util::hex(status).to_string_view() << ']';
return capture_e::error;
}
cursor.texture = std::move(texture);
cursor.width = t.Width;
cursor.height = t.Height;
cursor.set_texture(t.Width, t.Height, std::move(texture));
}
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);
}
cursor.set_pos(frame_info.PointerPosition.Position.x, frame_info.PointerPosition.Position.y, frame_info.PointerPosition.Visible && cursor_visible);
}
if(frame_update_flag) {
texture2d_t src;
src.reset();
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());
device_ctx->CopyResource(img->texture.get(), src.get());
if(cursor.visible) {
D3D11_VIEWPORT view {
0.0f, 0.0f,
(float)width, (float)height,
0.0f, 1.0f
};
device_ctx->VSSetShader(scene_vs.get(), nullptr, 0);
device_ctx->PSSetShader(scene_ps.get(), nullptr, 0);
device_ctx->RSSetViewports(1, &view);
device_ctx->OMSetRenderTargets(1, &img->scene_rt, nullptr);
device_ctx->PSSetShaderResources(0, 1, &cursor.input_res);
device_ctx->OMSetBlendState(blend_enable.get(), nullptr, 0xFFFFFFFFu);
device_ctx->RSSetViewports(1, &cursor.cursor_view);
device_ctx->Draw(3, 0);
device_ctx->OMSetBlendState(blend_disable.get(), nullptr, 0xFFFFFFFFu);
}
return capture_e::ok;
}
int display_vram_t::init(int framerate, const std::string &display_name) {
if(display_base_t::init(framerate, display_name)) {
return -1;
}
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;
auto status = device->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;
}
status = device->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->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_enable = make_blend(device.get(), true);
blend_disable = make_blend(device.get(), false);
if(!blend_disable || !blend_enable) {
return -1;
}
device_ctx->OMSetBlendState(blend_disable.get(), nullptr, 0xFFFFFFFFu);
device_ctx->PSSetSamplers(0, 1, &sampler_linear);
device_ctx->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLESTRIP);
return 0;
}
std::shared_ptr<platf::img_t> display_vram_t::alloc_img() {
auto img = std::make_shared<img_d3d_t>();
img->pixel_pitch = 4;
img->row_pitch = img->pixel_pitch * width;
img->width = width;
img->height = height;
img->display = shared_from_this();
auto dummy_data = std::make_unique<std::uint8_t[]>(img->row_pitch * height);
D3D11_SUBRESOURCE_DATA data {
dummy_data.get(),
(UINT)img->row_pitch
};
std::fill_n(dummy_data.get(), img->row_pitch * height, 0);
D3D11_TEXTURE2D_DESC t {};
t.Width = width;
t.Height = height;
@@ -789,20 +778,19 @@ std::shared_ptr<platf::img_t> display_vram_t::alloc_img() {
t.SampleDesc.Count = 1;
t.Usage = D3D11_USAGE_DEFAULT;
t.Format = format;
t.BindFlags = D3D11_BIND_SHADER_RESOURCE;
t.BindFlags = D3D11_BIND_SHADER_RESOURCE | D3D11_BIND_RENDER_TARGET;
auto status = device->CreateTexture2D(&t, nullptr, &img->texture);
auto status = device->CreateTexture2D(&t, &data, &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();
if(init_rt(device.get(), img->input_res, img->scene_rt, width, height, format, img->texture.get())) {
return nullptr;
}
img->data = (std::uint8_t *)img->texture.get();
return img;
}
@@ -810,6 +798,10 @@ std::shared_ptr<platf::img_t> display_vram_t::alloc_img() {
int display_vram_t::dummy_img(platf::img_t *img_base) {
auto img = (img_d3d_t *)img_base;
if(img->texture) {
return 0;
}
img->row_pitch = width * 4;
auto dummy_data = std::make_unique<int[]>(width * height);
D3D11_SUBRESOURCE_DATA data {
@@ -835,54 +827,35 @@ int display_vram_t::dummy_img(platf::img_t *img_base) {
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;
img->texture = std::move(tex);
img->data = (std::uint8_t *)img->texture.get();
return 0;
}
std::shared_ptr<platf::hwdevice_t> display_vram_t::make_hwdevice(int width, int height, pix_fmt_e pix_fmt) {
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 hwdevice = std::make_shared<hwdevice_t>();
auto ret = hwdevice->init(
shared_from_this(),
device.get(),
device_ctx.get(),
width, height,
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() {
for(auto &color : colors) {
BOOST_LOG(debug) << "Color Matrix"sv;
BOOST_LOG(debug) << "Y ["sv << color.color_vec_y.x << ", "sv << color.color_vec_y.y << ", "sv << color.color_vec_y.z << ", "sv << color.color_vec_y.w << ']';
BOOST_LOG(debug) << "U ["sv << color.color_vec_u.x << ", "sv << color.color_vec_u.y << ", "sv << color.color_vec_u.z << ", "sv << color.color_vec_u.w << ']';
BOOST_LOG(debug) << "V ["sv << color.color_vec_v.x << ", "sv << color.color_vec_v.y << ", "sv << color.color_vec_v.z << ", "sv << color.color_vec_v.w << ']';
BOOST_LOG(debug) << "range Y ["sv << color.range_y.x << ", "sv << color.range_y.y << ']';
BOOST_LOG(debug) << "range UV ["sv << color.range_uv.x << ", "sv << color.range_uv.y << ']';
}
BOOST_LOG(info) << "Compiling shaders..."sv;
scene_vs_hlsl = compile_vertex_shader(SUNSHINE_SHADERS_DIR "/SceneVS.hlsl");
if(!scene_vs_hlsl) {

View File

@@ -1,39 +1,51 @@
#include <cmath>
#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 <cmath>
#include <ViGEm/Client.h>
#include "misc.h"
#include "sunshine/config.h"
#include "sunshine/main.h"
#include "sunshine/platform/common.h"
namespace platf {
using namespace std::literals;
using adapteraddrs_t = util::c_ptr<IP_ADAPTER_ADDRESSES>;
thread_local HDESK _lastKnownInputDesktop = nullptr;
volatile HDESK _lastKnownInputDesktop = NULL;
constexpr touch_port_t target_touch_port {
0, 0,
65535, 65535
};
HDESK pairInputDesktop();
using client_t = util::safe_ptr<_VIGEM_CLIENT_T, vigem_free>;
using target_t = util::safe_ptr<_VIGEM_TARGET_T, vigem_target_free>;
static VIGEM_TARGET_TYPE map(const std::string_view &gp) {
if(gp == "x360"sv) {
return Xbox360Wired;
}
return DualShock4Wired;
}
void CALLBACK x360_notify(
client_t::pointer client,
target_t::pointer target,
std::uint8_t largeMotor, std::uint8_t smallMotor,
std::uint8_t /* led_number */,
void *userdata);
void CALLBACK ds4_notify(
client_t::pointer client,
target_t::pointer target,
std::uint8_t largeMotor, std::uint8_t smallMotor,
DS4_LIGHTBAR_COLOR /* led_color */,
void *userdata);
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;
@@ -46,44 +58,75 @@ public:
return -1;
}
x360s.resize(MAX_GAMEPADS);
gamepads.resize(MAX_GAMEPADS);
return 0;
}
int alloc_x360(int nr) {
auto &x360 = x360s[nr];
assert(!x360);
int alloc_gamepad_interal(int nr, rumble_queue_t &rumble_queue, VIGEM_TARGET_TYPE gp_type) {
auto &[rumble, gp] = gamepads[nr];
assert(!gp);
x360.reset(vigem_target_x360_alloc());
auto status = vigem_target_add(client.get(), x360.get());
if(gp_type == Xbox360Wired) {
gp.reset(vigem_target_x360_alloc());
}
else {
gp.reset(vigem_target_ds4_alloc());
}
auto status = vigem_target_add(client.get(), gp.get());
if(!VIGEM_SUCCESS(status)) {
BOOST_LOG(error) << "Couldn't add Gamepad to ViGEm connection ["sv << util::hex(status).to_string_view() << ']';
return -1;
}
rumble = std::move(rumble_queue);
if(gp_type == Xbox360Wired) {
status = vigem_target_x360_register_notification(client.get(), gp.get(), x360_notify, this);
}
else {
status = vigem_target_ds4_register_notification(client.get(), gp.get(), ds4_notify, this);
}
if(!VIGEM_SUCCESS(status)) {
BOOST_LOG(warning) << "Couldn't register notifications for rumble support ["sv << util::hex(status).to_string_view() << ']';
}
return 0;
}
void free_target(int nr) {
auto &x360 = x360s[nr];
auto &[_, gp] = gamepads[nr];
if(x360 && vigem_target_is_attached(x360.get())) {
auto status = vigem_target_remove(client.get(), x360.get());
if(gp && vigem_target_is_attached(gp.get())) {
auto status = vigem_target_remove(client.get(), gp.get());
if(!VIGEM_SUCCESS(status)) {
BOOST_LOG(warning) << "Couldn't detach gamepad from ViGEm ["sv << util::hex(status).to_string_view() << ']';
}
}
x360.reset();
gp.reset();
}
void rumble(target_t::pointer target, std::uint8_t smallMotor, std::uint8_t largeMotor) {
for(int x = 0; x < gamepads.size(); ++x) {
auto &[rumble_queue, gp] = gamepads[x];
if(gp.get() == target) {
rumble_queue->raise(x, ((std::uint16_t)smallMotor) << 8, ((std::uint16_t)largeMotor) << 8);
return;
}
}
}
~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());
for(auto &[_, gp] : gamepads) {
if(gp && vigem_target_is_attached(gp.get())) {
auto status = vigem_target_remove(client.get(), gp.get());
if(!VIGEM_SUCCESS(status)) {
BOOST_LOG(warning) << "Couldn't detach gamepad from ViGEm ["sv << util::hex(status).to_string_view() << ']';
}
@@ -94,73 +137,37 @@ public:
}
}
std::vector<target_t> x360s;
std::vector<std::pair<rumble_queue_t, target_t>> gamepads;
client_t client;
};
std::string from_sockaddr(const sockaddr *const socket_address) {
char data[INET6_ADDRSTRLEN];
void CALLBACK x360_notify(
client_t::pointer client,
target_t::pointer target,
std::uint8_t largeMotor, std::uint8_t smallMotor,
std::uint8_t /* led_number */,
void *userdata) {
auto family = socket_address->sa_family;
if(family == AF_INET6) {
inet_ntop(AF_INET6, &((sockaddr_in6 *)socket_address)->sin6_addr, data, INET6_ADDRSTRLEN);
}
BOOST_LOG(debug)
<< "largeMotor: "sv << (int)largeMotor << std::endl
<< "smallMotor: "sv << (int)smallMotor;
if(family == AF_INET) {
inet_ntop(AF_INET, &((sockaddr_in *)socket_address)->sin_addr, data, INET_ADDRSTRLEN);
}
return std::string { data };
task_pool.push(&vigem_t::rumble, (vigem_t *)userdata, target, smallMotor, largeMotor);
}
std::pair<std::uint16_t, std::string> from_sockaddr_ex(const sockaddr *const ip_addr) {
char data[INET6_ADDRSTRLEN];
void CALLBACK ds4_notify(
client_t::pointer client,
target_t::pointer target,
std::uint8_t largeMotor, std::uint8_t smallMotor,
DS4_LIGHTBAR_COLOR /* led_color */,
void *userdata) {
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;
}
BOOST_LOG(debug)
<< "largeMotor: "sv << (int)largeMotor << std::endl
<< "smallMotor: "sv << (int)smallMotor;
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;
task_pool.push(&vigem_t::rumble, (vigem_t *)userdata, target, smallMotor, largeMotor);
}
input_t input() {
@@ -178,14 +185,15 @@ void send_input(INPUT &i) {
retry:
auto send = SendInput(1, &i, sizeof(INPUT));
if(send != 1) {
auto hDesk = pairInputDesktop();
auto hDesk = syncThreadDesktop();
if(_lastKnownInputDesktop != hDesk) {
_lastKnownInputDesktop = hDesk;
goto retry;
}
BOOST_LOG(warning) << "Couldn't send input"sv;
BOOST_LOG(error) << "Couldn't send input"sv;
}
}
void abs_mouse(input_t &input, const touch_port_t &touch_port, float x, float y) {
INPUT i {};
@@ -277,10 +285,6 @@ void scroll(input_t &input, int distance) {
}
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;
@@ -322,12 +326,12 @@ void keyboard(input_t &input, uint16_t modcode, bool release) {
send_input(i);
}
int alloc_gamepad(input_t &input, int nr) {
int alloc_gamepad(input_t &input, int nr, rumble_queue_t rumble_queue) {
if(!input) {
return 0;
}
return ((vigem_t *)input.get())->alloc_x360(nr);
return ((vigem_t *)input.get())->alloc_gamepad_interal(nr, rumble_queue, map(config::input.gamepad));
}
void free_gamepad(input_t &input, int nr) {
@@ -337,6 +341,112 @@ void free_gamepad(input_t &input, int nr) {
((vigem_t *)input.get())->free_target(nr);
}
static VIGEM_ERROR x360_update(client_t::pointer client, target_t::pointer gp, const gamepad_state_t &gamepad_state) {
auto &xusb = *(PXUSB_REPORT)&gamepad_state;
return vigem_target_x360_update(client, gp, xusb);
}
static DS4_DPAD_DIRECTIONS ds4_dpad(const gamepad_state_t &gamepad_state) {
auto flags = gamepad_state.buttonFlags;
if(flags & DPAD_UP) {
if(flags & DPAD_RIGHT) {
return DS4_BUTTON_DPAD_NORTHEAST;
}
else if(flags & DPAD_LEFT) {
return DS4_BUTTON_DPAD_NORTHWEST;
}
else {
return DS4_BUTTON_DPAD_NORTH;
}
}
else if(flags & DPAD_DOWN) {
if(flags & DPAD_RIGHT) {
return DS4_BUTTON_DPAD_SOUTHEAST;
}
else if(flags & DPAD_LEFT) {
return DS4_BUTTON_DPAD_SOUTHWEST;
}
else {
return DS4_BUTTON_DPAD_SOUTH;
}
}
else if(flags & DPAD_RIGHT) {
return DS4_BUTTON_DPAD_EAST;
}
else if(flags & DPAD_LEFT) {
return DS4_BUTTON_DPAD_WEST;
}
return DS4_BUTTON_DPAD_NONE;
}
static DS4_BUTTONS ds4_buttons(const gamepad_state_t &gamepad_state) {
int buttons {};
auto flags = gamepad_state.buttonFlags;
// clang-format off
if(flags & LEFT_STICK) buttons |= DS4_BUTTON_THUMB_LEFT;
if(flags & RIGHT_STICK) buttons |= DS4_BUTTON_THUMB_RIGHT;
if(flags & LEFT_BUTTON) buttons |= DS4_BUTTON_SHOULDER_LEFT;
if(flags & RIGHT_BUTTON) buttons |= DS4_BUTTON_SHOULDER_RIGHT;
if(flags & START) buttons |= DS4_BUTTON_OPTIONS;
if(flags & A) buttons |= DS4_BUTTON_CROSS;
if(flags & B) buttons |= DS4_BUTTON_CIRCLE;
if(flags & X) buttons |= DS4_BUTTON_SQUARE;
if(flags & Y) buttons |= DS4_BUTTON_TRIANGLE;
if(gamepad_state.lt > 0) buttons |= DS4_BUTTON_TRIGGER_LEFT;
if(gamepad_state.rt > 0) buttons |= DS4_BUTTON_TRIGGER_RIGHT;
// clang-format on
return (DS4_BUTTONS)buttons;
}
static DS4_SPECIAL_BUTTONS ds4_special_buttons(const gamepad_state_t &gamepad_state) {
int buttons {};
if(gamepad_state.buttonFlags & BACK) buttons |= DS4_SPECIAL_BUTTON_TOUCHPAD;
if(gamepad_state.buttonFlags & HOME) buttons |= DS4_SPECIAL_BUTTON_PS;
return (DS4_SPECIAL_BUTTONS)buttons;
}
static std::uint8_t to_ds4_triggerX(std::int16_t v) {
return (v + std::numeric_limits<std::uint16_t>::max() / 2 + 1) / 257;
}
static std::uint8_t to_ds4_triggerY(std::int16_t v) {
auto new_v = -((std::numeric_limits<std::uint16_t>::max() / 2 + v - 1)) / 257;
return new_v == 0 ? 0xFF : (std::uint8_t)new_v;
}
static VIGEM_ERROR ds4_update(client_t::pointer client, target_t::pointer gp, const gamepad_state_t &gamepad_state) {
DS4_REPORT report;
DS4_REPORT_INIT(&report);
DS4_SET_DPAD(&report, ds4_dpad(gamepad_state));
report.wButtons |= ds4_buttons(gamepad_state);
report.bSpecial = ds4_special_buttons(gamepad_state);
report.bTriggerL = gamepad_state.lt;
report.bTriggerR = gamepad_state.rt;
report.bThumbLX = to_ds4_triggerX(gamepad_state.lsX);
report.bThumbLY = to_ds4_triggerY(gamepad_state.lsY);
report.bThumbRX = to_ds4_triggerX(gamepad_state.rsX);
report.bThumbRY = to_ds4_triggerY(gamepad_state.rsY);
return vigem_target_ds4_update(client, gp, report);
}
void gamepad(input_t &input, int nr, const gamepad_state_t &gamepad_state) {
// If there is no gamepad support
if(!input) {
@@ -345,10 +455,17 @@ void gamepad(input_t &input, int nr, const gamepad_state_t &gamepad_state) {
auto vigem = (vigem_t *)input.get();
auto &xusb = *(PXUSB_REPORT)&gamepad_state;
auto &x360 = vigem->x360s[nr];
auto &[_, gp] = vigem->gamepads[nr];
VIGEM_ERROR status;
if(vigem_target_get_type(gp.get()) == Xbox360Wired) {
status = x360_update(vigem->client.get(), gp.get(), gamepad_state);
}
else {
status = ds4_update(vigem->client.get(), gp.get(), gamepad_state);
}
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() << ']';
@@ -357,32 +474,18 @@ void gamepad(input_t &input, int nr, const gamepad_state_t &gamepad_state) {
}
}
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;
}
std::vector<std::string_view> &supported_gamepads() {
// ds4 == ps4
static std::vector<std::string_view> gps {
"x360"sv, "ds4"sv, "ps4"sv
};
return gps;
}
} // namespace platf

View File

@@ -0,0 +1,123 @@
#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;
}
HDESK syncThreadDesktop() {
auto hDesk = OpenInputDesktop(DF_ALLOWOTHERACCOUNTHOOK, FALSE, GENERIC_ALL);
if(!hDesk) {
auto err = GetLastError();
BOOST_LOG(error) << "Failed to Open Input Desktop [0x"sv << util::hex(err).to_string_view() << ']';
return nullptr;
}
if(!SetThreadDesktop(hDesk)) {
auto err = GetLastError();
BOOST_LOG(error) << "Failed to sync desktop to thread [0x"sv << util::hex(err).to_string_view() << ']';
}
CloseDesktop(hDesk);
return hDesk;
}
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

View File

@@ -0,0 +1,13 @@
#ifndef SUNSHINE_WINDOWS_MISC_H
#define SUNSHINE_WINDOWS_MISC_H
#include <windows.h>
#include <winnt.h>
#include <string_view>
namespace platf {
void print_status(const std::string_view &prefix, HRESULT status);
HDESK syncThreadDesktop();
}
#endif

View File

@@ -0,0 +1,189 @@
#include <winsock2.h>
#include <windows.h>
#include <windns.h>
#include <winerror.h>
#include <boost/asio/ip/host_name.hpp>
#include "misc.h"
#include "sunshine/config.h"
#include "sunshine/main.h"
#include "sunshine/network.h"
#include "sunshine/nvhttp.h"
#include "sunshine/platform/common.h"
#include "sunshine/thread_safe.h"
#define _FN(x, ret, args) \
typedef ret(*x##_fn) args; \
static x##_fn x
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;
_FN(_DnsServiceFreeInstance, VOID, (_In_ PDNS_SERVICE_INSTANCE pInstance));
_FN(_DnsServiceDeRegister, DWORD, (_In_ PDNS_SERVICE_REGISTER_REQUEST pRequest, _Inout_opt_ PDNS_SERVICE_CANCEL pCancel));
_FN(_DnsServiceRegister, DWORD, (_In_ PDNS_SERVICE_REGISTER_REQUEST pRequest, _Inout_opt_ PDNS_SERVICE_CANCEL pCancel));
} /* extern "C" */
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;
}
};
int load_funcs(HMODULE handle) {
auto fg = util::fail_guard([handle]() {
FreeLibrary(handle);
});
_DnsServiceFreeInstance = (_DnsServiceFreeInstance_fn)GetProcAddress(handle, "DnsServiceFreeInstance");
_DnsServiceDeRegister = (_DnsServiceDeRegister_fn)GetProcAddress(handle, "DnsServiceDeRegister");
_DnsServiceRegister = (_DnsServiceRegister_fn)GetProcAddress(handle, "DnsServiceRegister");
if(!(_DnsServiceFreeInstance && _DnsServiceDeRegister && _DnsServiceRegister)) {
BOOST_LOG(error) << "mDNS service not available in dnsapi.dll"sv;
return -1;
}
fg.disable();
return 0;
}
std::unique_ptr<::platf::deinit_t> start() {
HMODULE handle = LoadLibrary("dnsapi.dll");
if(!handle || load_funcs(handle)) {
BOOST_LOG(error) << "Couldn't load dnsapi.dll, You'll need to add PC manually from Moonlight"sv;
return nullptr;
}
if(service(true)) {
return nullptr;
}
BOOST_LOG(info) << "Registered Sunshine Gamestream service"sv;
return std::make_unique<deinit_t>();
}
} // namespace platf::publish

View File

@@ -0,0 +1 @@
SuperDuperAmazing ICON DISCARDABLE "@SUNSHINE_ICON_PATH@"

View File

@@ -11,6 +11,7 @@
#include <boost/property_tree/json_parser.hpp>
#include <boost/property_tree/ptree.hpp>
#include <boost/filesystem.hpp>
#include "main.h"
#include "utility.h"
@@ -94,7 +95,7 @@ int proc_t::execute(int app_id) {
for(auto &cmd : proc.detached) {
BOOST_LOG(info) << "Spawning ["sv << cmd << ']';
if(proc.output.empty()) {
if(proc.output.empty() || proc.output == "null"sv) {
bp::spawn(cmd, _env, bp::std_out > bp::null, bp::std_err > bp::null, ec);
}
else {
@@ -106,15 +107,20 @@ int proc_t::execute(int app_id) {
}
}
BOOST_LOG(info) << "Executing: ["sv << proc.cmd << ']';
if(proc.cmd.empty()) {
BOOST_LOG(debug) << "Executing [Desktop]"sv;
placebo = true;
}
else if(proc.output.empty() || proc.output == "null"sv) {
_process = bp::child(_process_handle, proc.cmd, _env, bp::std_out > bp::null, bp::std_err > bp::null, ec);
}
else {
_process = bp::child(_process_handle, proc.cmd, _env, bp::std_out > _pipe.get(), bp::std_err > _pipe.get(), ec);
} else {
boost::filesystem::path working_dir = proc.working_dir.empty() ?
boost::filesystem::path(proc.cmd).parent_path() : boost::filesystem::path(proc.working_dir);
if(proc.output.empty() || proc.output == "null"sv) {
BOOST_LOG(info) << "Executing: ["sv << proc.cmd << ']';
_process = bp::child(_process_handle, proc.cmd, _env, bp::start_dir(working_dir), bp::std_out > bp::null, bp::std_err > bp::null, ec);
}
else {
BOOST_LOG(info) << "Executing: ["sv << proc.cmd << ']';
_process = bp::child(_process_handle, proc.cmd, _env, bp::start_dir(working_dir), bp::std_out > _pipe.get(), bp::std_err > _pipe.get(), ec);
}
}
if(ec) {
@@ -273,6 +279,7 @@ std::optional<proc::proc_t> parse(const std::string &file_name) {
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 working_dir = app_node.get_optional<std::string>("working-dir"s);
std::vector<proc::cmd_t> prep_cmds;
if(prep_nodes_opt) {
@@ -310,6 +317,10 @@ std::optional<proc::proc_t> parse(const std::string &file_name) {
ctx.cmd = parse_env_val(this_env, *cmd);
}
if(working_dir) {
ctx.working_dir = parse_env_val(this_env, *working_dir);
}
ctx.name = std::move(name);
ctx.prep_cmds = std::move(prep_cmds);
ctx.detached = std::move(detached);
@@ -332,6 +343,11 @@ 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);
}
}

View File

@@ -34,6 +34,7 @@ struct cmd_t {
* cmd -- Runs indefinitely until:
* No session is running and a different set of commands it to be executed
* Command exits
* working_dir -- the process working directory. This is required for some games to run properly.
* cmd_output --
* empty -- The output of the commands are appended to the output of sunshine
* "null" -- The output of the commands are discarded
@@ -52,12 +53,13 @@ struct ctx_t {
std::string name;
std::string cmd;
std::string working_dir;
std::string output;
};
class proc_t {
public:
KITTY_DEFAULT_CONSTR_THROW(proc_t)
KITTY_DEFAULT_CONSTR_MOVE_THROW(proc_t)
proc_t(
boost::process::environment &&env,

View File

@@ -2,10 +2,18 @@
// Created by loki on 2/2/20.
//
#define BOOST_BIND_GLOBAL_PLACEHOLDERS
extern "C" {
#include <moonlight-common-c/src/Rtsp.h>
}
#include <array>
#include <cctype>
#include <boost/asio.hpp>
#include <boost/bind.hpp>
#include "config.h"
#include "input.h"
#include "main.h"
@@ -22,17 +30,6 @@ 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();
}
constexpr auto RTSP_SETUP_PORT = 48010;
void free_msg(PRTSP_MESSAGE msg) {
freeMessage(msg);
@@ -42,20 +39,180 @@ void free_msg(PRTSP_MESSAGE 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 &&)>;
using cmd_func_t = std::function<void(rtsp_server_t *server, tcp::socket &, msg_t &&)>;
void print_msg(PRTSP_MESSAGE msg);
void cmd_not_found(net::host_t::pointer host, net::peer_t peer, msg_t &&req);
void cmd_not_found(tcp::socket &sock, msg_t &&req);
void respond(tcp::socket &sock, POPTION_ITEM options, int statuscode, const char *status_msg, int seqn, const std::string_view &payload);
class socket_t : public std::enable_shared_from_this<socket_t> {
public:
socket_t(boost::asio::io_service &ios, std::function<void(tcp::socket &sock, msg_t &&)> &&handle_data_fn)
: handle_data_fn { std::move(handle_data_fn) }, sock { ios } {}
void read() {
if(begin == std::end(msg_buf)) {
BOOST_LOG(error) << "RTSP: read(): Exceeded maximum rtsp packet size: "sv << msg_buf.size();
respond(sock, nullptr, 400, "BAD REQUEST", 0, {});
sock.close();
return;
}
sock.async_read_some(
boost::asio::buffer(begin, (std::size_t)(std::end(msg_buf) - begin)),
boost::bind(
&socket_t::handle_read, shared_from_this(),
boost::asio::placeholders::error,
boost::asio::placeholders::bytes_transferred));
}
void read_payload() {
if(begin == std::end(msg_buf)) {
BOOST_LOG(error) << "RTSP: read_payload(): Exceeded maximum rtsp packet size: "sv << msg_buf.size();
respond(sock, nullptr, 400, "BAD REQUEST", 0, {});
sock.close();
return;
}
sock.async_read_some(
boost::asio::buffer(begin, (std::size_t)(std::end(msg_buf) - begin)),
boost::bind(
&socket_t::handle_payload, shared_from_this(),
boost::asio::placeholders::error,
boost::asio::placeholders::bytes_transferred));
}
static void handle_payload(std::shared_ptr<socket_t> &socket, const boost::system::error_code &ec, std::size_t bytes) {
BOOST_LOG(debug) << "handle_payload(): Handle read of size: "sv << bytes << " bytes"sv;
auto sock_close = util::fail_guard([&socket]() {
boost::system::error_code ec;
socket->sock.close(ec);
if(ec) {
BOOST_LOG(error) << "RTSP: handle_payload(): Couldn't close tcp socket: "sv << ec.message();
}
});
if(ec) {
BOOST_LOG(error) << "RTSP: handle_payload(): Couldn't read from tcp socket: "sv << ec.message();
return;
}
auto end = socket->begin + bytes;
msg_t req { new msg_t::element_type {} };
if(auto status = parseRtspMessage(req.get(), socket->msg_buf.data(), (std::size_t)(end - socket->msg_buf.data()))) {
BOOST_LOG(error) << "Malformed RTSP message: ["sv << status << ']';
respond(socket->sock, nullptr, 400, "BAD REQUEST", req->sequenceNumber, {});
return;
}
sock_close.disable();
auto fg = util::fail_guard([&socket]() {
socket->read_payload();
});
auto content_lenght = 0;
for(auto option = req->options; option != nullptr; option = option->next) {
if("Content-length"sv == option->option) {
BOOST_LOG(debug) << "Found Content-Length: "sv << option->content << " bytes"sv;
// If content_length > bytes read, then we need to store current data read,
// to be appended by the next read.
std::string_view content { option->content };
auto begin = std::find_if(std::begin(content), std::end(content), [](auto ch) { return (bool)std::isdigit(ch); });
content_lenght = util::from_chars(begin, std::end(content));
break;
}
}
if(end - socket->crlf >= content_lenght) {
if(end - socket->crlf > content_lenght) {
BOOST_LOG(warning) << "(end - socket->crlf) > content_lenght -- "sv << (std::size_t)(end - socket->crlf) << " > "sv << content_lenght;
}
fg.disable();
print_msg(req.get());
socket->handle_data(std::move(req));
}
socket->begin = end;
}
static void handle_read(std::shared_ptr<socket_t> &socket, const boost::system::error_code &ec, std::size_t bytes) {
BOOST_LOG(debug) << "handle_read(): Handle read of size: "sv << bytes << " bytes"sv;
if(ec) {
BOOST_LOG(error) << "RTSP: handle_read(): Couldn't read from tcp socket: "sv << ec.message();
boost::system::error_code ec;
socket->sock.close(ec);
if(ec) {
BOOST_LOG(error) << "RTSP: handle_read(): Couldn't close tcp socket: "sv << ec.message();
}
return;
}
auto fg = util::fail_guard([&socket]() {
socket->read();
});
auto begin = std::max(socket->begin - 4, socket->begin);
auto buf_size = bytes + (begin - socket->begin);
auto end = begin + buf_size;
constexpr auto needle = "\r\n\r\n"sv;
auto it = std::search(begin, begin + buf_size, std::begin(needle), std::end(needle));
if(it == end) {
socket->begin = end;
return;
}
// Emulate read completion for payload data
socket->begin = it + needle.size();
socket->crlf = socket->begin;
buf_size = end - socket->begin;
fg.disable();
handle_payload(socket, ec, buf_size);
}
void handle_data(msg_t &&req) {
handle_data_fn(sock, std::move(req));
}
std::function<void(tcp::socket &sock, msg_t &&)> handle_data_fn;
tcp::socket sock;
std::array<char, 2048> msg_buf;
char *crlf;
char *begin = msg_buf.data();
};
class rtsp_server_t {
public:
~rtsp_server_t() {
if(_host) {
clear();
}
clear();
}
int bind(std::uint16_t port) {
int bind(std::uint16_t port, boost::system::error_code &ec) {
{
auto lg = _session_slots.lock();
@@ -63,13 +220,85 @@ public:
_slot_count = config::stream.channels;
}
_host = net::host_create(_addr, 1, port);
acceptor.open(tcp::v4(), ec);
if(ec) {
return -1;
}
return !(bool)_host;
acceptor.set_option(boost::asio::socket_base::reuse_address { true });
acceptor.bind(tcp::endpoint(tcp::v4(), port), ec);
if(ec) {
return -1;
}
acceptor.listen(4096, ec);
if(ec) {
return -1;
}
next_socket = std::make_shared<socket_t>(ios, [this](tcp::socket &sock, msg_t &&msg) {
handle_msg(sock, std::move(msg));
});
acceptor.async_accept(next_socket->sock, [this](const auto &ec) {
handle_accept(ec);
});
return 0;
}
template<class T, class X>
void iterate(std::chrono::duration<T, X> timeout) {
ios.run_one_for(timeout);
}
void handle_msg(tcp::socket &sock, msg_t &&req) {
auto func = _map_cmd_cb.find(req->message.request.command);
if(func != std::end(_map_cmd_cb)) {
func->second(this, sock, std::move(req));
}
else {
cmd_not_found(sock, std::move(req));
}
sock.shutdown(boost::asio::socket_base::shutdown_type::shutdown_both);
}
void handle_accept(const boost::system::error_code &ec) {
if(ec) {
BOOST_LOG(error) << "Couldn't accept incoming connections: "sv << ec.message();
//Stop server
clear();
return;
}
auto socket = std::move(next_socket);
socket->read();
next_socket = std::make_shared<socket_t>(ios, [this](tcp::socket &sock, msg_t &&msg) {
handle_msg(sock, std::move(msg));
});
acceptor.async_accept(next_socket->sock, [this](const auto &ec) {
handle_accept(ec);
});
}
void map(const std::string_view &type, cmd_func_t cb) {
_map_cmd_cb.emplace(type, std::move(cb));
}
void session_raise(launch_session_t launch_session) {
//FIXME: If client abandons us at this stage, _slot_count won't be raised again.
auto now = std::chrono::steady_clock::now();
// If a launch event is still pending, don't overwrite it.
if(raised_timeout > now && launch_event.peek()) {
return;
}
raised_timeout = now + 10s;
--_slot_count;
launch_event.raise(launch_session);
}
@@ -78,75 +307,17 @@ public:
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));
}
safe::event_t<launch_session_t> launch_event;
void clear(bool all = true) {
// if a launch event timed out --> Remove it.
if(raised_timeout < std::chrono::steady_clock::now()) {
auto discarded = launch_event.pop(0s);
if(discarded) {
++_slot_count;
}
}
auto lg = _session_slots.lock();
for(auto &slot : *_session_slots) {
@@ -160,10 +331,8 @@ public:
}
}
if(all) {
std::for_each(_host->peers, _host->peers + _host->peerCount, [](auto &peer) {
enet_peer_disconnect_now(&peer, 0);
});
if(all && !ios.stopped()) {
ios.stop();
}
}
@@ -188,27 +357,21 @@ public:
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;
std::chrono::steady_clock::time_point raised_timeout;
int _slot_count;
ENetAddress _addr;
net::host_t _host;
boost::asio::io_service ios;
tcp::acceptor acceptor { ios };
std::shared_ptr<socket_t> next_socket;
};
rtsp_server_t server;
rtsp_server_t server {};
void launch_session_raise(launch_session_t launch_session) {
server.session_raise(launch_session);
@@ -221,9 +384,26 @@ int session_count() {
return server.session_count();
}
void respond(net::host_t::pointer host, net::peer_t peer, msg_t &resp) {
int send(tcp::socket &sock, const std::string_view &sv) {
std::size_t bytes_send = 0;
while(bytes_send != sv.size()) {
boost::system::error_code ec;
bytes_send += sock.send(boost::asio::buffer(sv.substr(bytes_send)), 0, ec);
if(ec) {
BOOST_LOG(error) << "RTSP: Couldn't send data over tcp socket: "sv << ec.message();
return -1;
}
}
return 0;
}
void respond(tcp::socket &sock, msg_t &resp) {
auto payload = std::make_pair(resp->payload, resp->payloadLength);
// Restore response message for proper destruction
auto lg = util::fail_guard([&]() {
resp->payload = payload.first;
resp->payloadLength = payload.second;
@@ -241,57 +421,44 @@ void respond(net::host_t::pointer host, net::peer_t peer, msg_t &resp) {
<< "---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(send(sock, tmp_resp)) {
return;
}
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);
}
send(sock, std::string_view { payload.first, (std::size_t)payload.second });
}
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) {
void respond(tcp::socket &sock, 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);
respond(sock, 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_not_found(tcp::socket &sock, msg_t &&req) {
respond(sock, nullptr, 404, "NOT FOUND", req->sequenceNumber, {});
}
void cmd_option(rtsp_server_t *server, net::peer_t peer, msg_t &&req) {
void cmd_option(rtsp_server_t *server, tcp::socket &sock, 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);
auto seqn_str = std::to_string(req->sequenceNumber);
option.content = const_cast<char *>(seqn_str.c_str());
respond(server->host(), peer, &option, 200, "OK", req->sequenceNumber, {});
respond(sock, &option, 200, "OK", req->sequenceNumber, {});
}
void cmd_describe(rtsp_server_t *server, net::peer_t peer, msg_t &&req) {
void cmd_describe(rtsp_server_t *server, tcp::socket &sock, 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);
auto seqn_str = std::to_string(req->sequenceNumber);
option.content = const_cast<char *>(seqn_str.c_str());
std::stringstream ss;
@@ -326,18 +493,19 @@ void cmd_describe(rtsp_server_t *server, net::peer_t peer, msg_t &&req) {
ss << std::endl;
}
respond(server->host(), peer, &option, 200, "OK", req->sequenceNumber, ss.str());
respond(sock, &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] {};
void cmd_setup(rtsp_server_t *server, tcp::socket &sock, msg_t &&req) {
OPTION_ITEM options[3] {};
auto &seqn = options[0];
auto &session_option = options[1];
auto &port_option = options[2];
seqn.option = const_cast<char *>("CSeq");
auto seqn_str = to_string(req->sequenceNumber);
auto seqn_str = std::to_string(req->sequenceNumber);
seqn.content = const_cast<char *>(seqn_str.c_str());
std::string_view target { req->message.request.target };
@@ -345,34 +513,52 @@ void cmd_setup(rtsp_server_t *server, net::peer_t peer, msg_t &&req) {
auto end = std::find(begin, std::end(target), '/');
std::string_view type { begin, (size_t)std::distance(begin, end) };
std::uint16_t port;
if(type == "audio"sv) {
seqn.next = &session_option;
session_option.option = const_cast<char *>("Session");
session_option.content = const_cast<char *>("DEADBEEFCAFE;timeout = 90");
port = map_port(stream::AUDIO_STREAM_PORT);
}
else if(type != "video"sv && type != "control"sv) {
cmd_not_found(server->host(), peer, std::move(req));
else if(type == "video"sv) {
port = map_port(stream::VIDEO_STREAM_PORT);
}
else if(type == "control"sv) {
port = map_port(stream::CONTROL_PORT);
}
else {
cmd_not_found(sock, std::move(req));
return;
}
respond(server->host(), peer, &seqn, 200, "OK", req->sequenceNumber, {});
seqn.next = &session_option;
session_option.option = const_cast<char *>("Session");
session_option.content = const_cast<char *>("DEADBEEFCAFE;timeout = 90");
session_option.next = &port_option;
// Moonlight merely requires 'server_port=<port>'
auto port_value = "server_port=" + std::to_string(port);
port_option.option = const_cast<char *>("Transport");
port_option.content = port_value.data();
respond(sock, &seqn, 200, "OK", req->sequenceNumber, {});
}
void cmd_announce(rtsp_server_t *server, net::peer_t peer, msg_t &&req) {
void cmd_announce(rtsp_server_t *server, tcp::socket &sock, 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);
auto seqn_str = std::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, {});
respond(sock, &option, 503, "Service Unavailable", req->sequenceNumber, {});
return;
}
auto launch_session { server->launch_event.pop() };
@@ -424,6 +610,9 @@ void cmd_announce(rtsp_server_t *server, net::peer_t peer, msg_t &&req) {
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);
args.try_emplace("x-nv-general.useReliableUdp"sv, "1"sv);
args.try_emplace("x-nv-vqos[0].fec.minRequiredFecPackets"sv, "0"sv);
args.try_emplace("x-nv-general.featureFlags"sv, "135"sv);
config_t config;
@@ -436,7 +625,10 @@ void cmd_announce(rtsp_server_t *server, net::peer_t peer, msg_t &&req) {
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.controlProtocolType = util::from_view(args.at("x-nv-general.useReliableUdp"sv));
config.packetsize = util::from_view(args.at("x-nv-video[0].packetSize"sv));
config.minRequiredFecPackets = util::from_view(args.at("x-nv-vqos[0].fec.minRequiredFecPackets"sv));
config.featureFlags = util::from_view(args.at("x-nv-general.featureFlags"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));
@@ -450,14 +642,14 @@ void cmd_announce(rtsp_server_t *server, net::peer_t peer, msg_t &&req) {
}
catch(std::out_of_range &) {
respond(server->host(), peer, &option, 400, "BAD REQUEST", req->sequenceNumber, {});
respond(sock, &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, {});
respond(sock, &option, 400, "BAD REQUEST", req->sequenceNumber, {});
return;
}
@@ -467,34 +659,37 @@ void cmd_announce(rtsp_server_t *server, net::peer_t peer, msg_t &&req) {
if(!slot) {
BOOST_LOG(info) << "Ran out of slots for client from ["sv << ']';
respond(server->host(), peer, &option, 503, "Service Unavailable", req->sequenceNumber, {});
respond(sock, &option, 503, "Service Unavailable", req->sequenceNumber, {});
return;
}
if(session::start(*session, platf::from_sockaddr((sockaddr *)&peer->address.address))) {
if(session::start(*session, sock.remote_endpoint().address().to_string())) {
BOOST_LOG(error) << "Failed to start a streaming session"sv;
server->clear(slot);
respond(server->host(), peer, &option, 500, "Internal Server Error", req->sequenceNumber, {});
respond(sock, &option, 500, "Internal Server Error", req->sequenceNumber, {});
return;
}
respond(server->host(), peer, &option, 200, "OK", req->sequenceNumber, {});
respond(sock, &option, 200, "OK", req->sequenceNumber, {});
}
void cmd_play(rtsp_server_t *server, net::peer_t peer, msg_t &&req) {
void cmd_play(rtsp_server_t *server, tcp::socket &sock, 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);
auto seqn_str = std::to_string(req->sequenceNumber);
option.content = const_cast<char *>(seqn_str.c_str());
respond(server->host(), peer, &option, 200, "OK", req->sequenceNumber, {});
respond(sock, &option, 200, "OK", req->sequenceNumber, {});
}
void rtpThread(std::shared_ptr<safe::signal_t> shutdown_event) {
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);
@@ -502,8 +697,9 @@ void rtpThread(std::shared_ptr<safe::signal_t> shutdown_event) {
server.map("PLAY"sv, &cmd_play);
if(server.bind(RTSP_SETUP_PORT)) {
BOOST_LOG(fatal) << "Couldn't bind RTSP server to port ["sv << RTSP_SETUP_PORT << "], likely another process already bound to the port"sv;
boost::system::error_code ec;
if(server.bind(map_port(RTSP_SETUP_PORT), ec)) {
BOOST_LOG(fatal) << "Couldn't bind RTSP server to port ["sv << map_port(RTSP_SETUP_PORT) << "], " << ec.message();
shutdown_event->raise(true);
return;
@@ -512,7 +708,7 @@ void rtpThread(std::shared_ptr<safe::signal_t> shutdown_event) {
while(!shutdown_event->peek()) {
server.iterate(std::min(500ms, config::stream.ping_timeout));
if(broadcast_shutdown_event.peek()) {
if(broadcast_shutdown_event->peek()) {
server.clear();
}
else {

View File

@@ -11,6 +11,8 @@
#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;
@@ -21,7 +23,7 @@ struct launch_session_t {
void launch_session_raise(launch_session_t launch_session);
int session_count();
void rtpThread(std::shared_ptr<safe::signal_t> shutdown_event);
void rtpThread();
} // namespace stream

File diff suppressed because it is too large Load Diff

View File

@@ -12,11 +12,19 @@
#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;
int minRequiredFecPackets;
int featureFlags;
int controlProtocolType;
std::optional<int> gcmap;
};
@@ -35,8 +43,6 @@ void stop(session_t &session);
void join(session_t &session);
state_e state(session_t &session);
} // namespace session
extern safe::signal_t broadcast_shutdown_event;
} // namespace stream
#endif //SUNSHINE_STREAM_H

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