Compare commits

...

179 Commits

Author SHA1 Message Date
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
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
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
82 changed files with 10582 additions and 3898 deletions

3
.gitmodules vendored
View File

@@ -7,3 +7,6 @@
[submodule "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

1
.prettierrc.json Normal file
View File

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

View File

@@ -2,10 +2,17 @@ cmake_minimum_required(VERSION 3.0)
project(Sunshine)
set(CMAKE_MODULE_PATH ${CMAKE_CURRENT_SOURCE_DIR})
set(CMAKE_MODULE_PATH ${CMAKE_CURRENT_SOURCE_DIR}/cmake)
add_subdirectory(third-party/Simple-Web-Server)
set(UPNPC_BUILD_SHARED OFF CACHE BOOL "no shared libraries")
set(UPNPC_BUILD_TESTS OFF CACHE BOOL "Don't build tests for miniupnpc")
set(UPNPC_BUILD_SAMPLE OFF CACHE BOOL "Don't build samples for miniupnpc")
set(UPNPC_NO_INSTALL ON CACHE BOOL "Don't install any libraries build for miniupnpc")
add_subdirectory(third-party/miniupnp/miniupnpc)
include_directories(third-party/miniupnp)
if(WIN32)
# Ugly hack to compile with #include <qos2.h>
add_compile_definitions(
@@ -43,6 +50,8 @@ if(WIN32)
include_directories(third-party/ViGEmClient/include)
set(PLATFORM_TARGET_FILES
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
@@ -80,12 +89,9 @@ if(WIN32)
libstdc++.a
libwinpthread.a
libssp.a
Qwave
winmm
ksuser
wsock32
ws2_32
iphlpapi
d3d11 dxgi D3DCompiler
setupapi
)
@@ -96,15 +102,45 @@ 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
option(SUNSHINE_ENABLE_DRM "Enable KMS grab if available" ON)
option(SUNSHINE_ENABLE_X11 "Enable X11 grab if available" ON)
if(${SUNSHINE_ENABLE_X11})
find_package(X11)
endif()
if(${SUNSHINE_ENABLE_DRM})
find_package(LIBDRM)
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(LIBDRM_FOUND)
add_compile_definitions(SUNSHINE_BUILD_DRM)
include_directories(${LIBDRM_INCLUDE_DIRS})
list(APPEND PLATFORM_LIBRARIES ${LIBDRM_LIBRARIES})
list(APPEND PLATFORM_TARGET_FILES sunshine/platform/linux/kmsgrab.cpp)
list(APPEND SUNSHINE_DEFINITIONS EGL_NO_X11=1)
endif()
if(NOT X11_FOUND AND NOT LIBDRM_FOUND)
message(FATAL "Couldn't find either x11 or libdrm")
endif()
list(APPEND PLATFORM_TARGET_FILES
sunshine/platform/linux/publish.cpp
sunshine/platform/linux/vaapi.h
sunshine/platform/linux/vaapi.cpp
sunshine/platform/linux/graphics.h
sunshine/platform/linux/graphics.cpp
sunshine/platform/linux/misc.h
sunshine/platform/linux/misc.cpp
sunshine/platform/linux/display.cpp
sunshine/platform/linux/audio.cpp
sunshine/platform/linux/input.cpp
sunshine/platform/linux/x11grab.h
third-party/glad/src/egl.c
third-party/glad/src/gl.c
third-party/glad/include/EGL/eglplatform.h
@@ -112,27 +148,19 @@ else()
third-party/glad/include/glad/gl.h
third-party/glad/include/glad/egl.h)
set(PLATFORM_LIBRARIES
Xfixes
Xtst
xcb
xcb-shm
xcb-xfixes
Xrandr
${X11_LIBRARIES}
list(APPEND PLATFORM_LIBRARIES
dl
evdev
pulse
pulse-simple
)
set(PLATFORM_INCLUDE_DIRS
${X11_INCLUDE_DIR}
include_directories(
/usr/include/libevdev-1.0
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)
@@ -150,6 +178,8 @@ set(SUNSHINE_TARGET_FILES
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
@@ -188,6 +218,8 @@ 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}/third-party
@@ -213,10 +245,19 @@ 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
@@ -228,6 +269,8 @@ list(APPEND SUNSHINE_EXTERNAL_LIBRARIES
${PLATFORM_LIBRARIES})
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})

View File

@@ -14,28 +14,45 @@ Sunshine is a Gamestream host for Moonlight
## Linux
### Requirements:
Ubuntu 20.04:
Install the following
Install the following:
#### X11 Only
```
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 libxtst-dev libx11-dev libxrandr-dev libxfixes-dev libevdev-dev libxcb1-dev libxcb-shm0-dev libxcb-xfixes0-dev
```
#### X11 + KMS (Requires additional setup)
KMS allows Sunshine to grab the monitor with lower latency then through X11
```
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 libxtst-dev libx11-dev libxrandr-dev libxfixes-dev libevdev-dev libxcb1-dev libxcb-shm0-dev libxcb-xfixes0-dev libdrm-dev
```
### Compilation:
#### X11 Only
- `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 -DSUNSHINE_ENABLE_DRM=OFF ..`
- `make -j ${nproc}`
#### X11 + KMS
- `git clone https://github.com/loki-47-6F-64/sunshine.git --recurse-submodules`
- `cd sunshine && mkdir build && cd build`
- `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,6 +69,11 @@ 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+ep sunshine`
### Trouleshooting:
- If you get "Could not create Sunshine Gamepad: Permission Denied", ensure you are part of the group "input":
- `groups $USER`
@@ -65,13 +87,15 @@ sunshine needs access to uinput to create mouse and gamepad events:
### 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-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]
@@ -87,22 +111,18 @@ sunshine needs access to uinput to create mouse and gamepad events:
- When Moonlight request you insert the correct pin on sunshine:
- Type in the URL bar of your browser: `https://xxx.xxx.xxx.xxx:47990` where `xxx.xxx.xxx.xxx` is the IP address of your computer
- Ignore any warning given by your browser about "insecure website"
- Type in the username and password shown the first time you run Sunshine
- 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)

View File

@@ -9,7 +9,7 @@ environment:
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
- 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 libdrm-dev
- cmd: C:\msys64\usr\bin\bash -lc "pacman --needed --noconfirm -S mingw-w64-x86_64-openssl mingw-w64-x86_64-cmake mingw-w64-x86_64-toolchain mingw-w64-x86_64-opus mingw-w64-x86_64-x265 mingw-w64-x86_64-boost git yasm nasm diffutils make"
before_build:
@@ -33,6 +33,9 @@ after_build:
- 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

View File

@@ -1,255 +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 file where user credentials for the UI are stored
# By default, credentials are stored in `file_state`
# credentials_file = sunshine_state.json
# The display modes advertised by Sunshine
#
# Some versions of Moonlight, such as Moonlight-nx (Switch),
# rely on this list to ensure that the requested resolutions and fps
# are supported.
#
# fps = [10, 30, 60, 90, 120]
# resolutions = [
# 352x240,
# 480x360,
# 858x480,
# 1280x720,
# 1920x1080,
# 2560x1080,
# 3440x1440,
# 1920x1200,
# 3860x2160,
# 3840x1600,
# ]
# How long to wait in milliseconds for data from moonlight before shutting down the stream
# ping_timeout = 2000
# 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:" if running vanilla pulseaudio
# pactl info | grep Source` if running pipewire
# audio_sink = alsa_output.pci-0000_09_00.3.analog-stereo
#
# !! Windows only !!
# tools\audio-info.exe
# audio_sink = {0.0.0.00000000}.{FD47D9CC-4218-4135-9CE2-0C195C87405B}
#
# 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 :)
# 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
#################################### 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 = 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
# pactl info | grep Source` if running pipewire
# audio_sink = alsa_output.pci-0000_09_00.3.analog-stereo
#
# !! Windows only !!
# tools\audio-info.exe
# audio_sink = {0.0.0.00000000}.{FD47D9CC-4218-4135-9CE2-0C195C87405B}
#
# 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

View File

@@ -14,8 +14,11 @@
<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>
<button class="btn btn-primary" @click="editApp(i)">Edit</button>
<button class="btn btn-danger" @click="showDeleteForm(i)">
Delete
</button>
</td>
</tr>
</tbody>
@@ -26,62 +29,132 @@
<!--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>
<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>
<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>
<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>
<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>
<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>
<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">
<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>
<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>
<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 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>
<input
type="text"
class="form-control monospace"
id="appCmd"
aria-describedby="appCmdHelp"
v-model="editForm.cmd"
/>
<div id="appCmdHelp" class="form-text">
The main application, if it is not specified, a processs is started
that sleeps indefinitely
</div>
</div>
<!--buttons-->
<div class="d-flex">
<button @click="showEditForm = false" class="btn btn-secondary m-2">Cancel</button>
<button @click="showEditForm = false" class="btn btn-secondary m-2">
Cancel
</button>
<button class="btn btn-primary m-2" @click="save">Save</button>
</div>
</div>
@@ -93,30 +166,32 @@
<script>
new Vue({
el: '#app',
el: "#app",
data() {
return {
apps: [],
showEditForm: false,
editForm: null,
detachedCmd: '',
}
detachedCmd: "",
};
},
created() {
fetch("/api/apps").then(r => r.json()).then((r) => {
console.log(r);
this.apps = r.apps;
})
fetch("/api/apps")
.then((r) => r.json())
.then((r) => {
console.log(r);
this.apps = r.apps;
});
},
methods: {
newApp() {
this.editForm = {
name: '',
output: '',
name: "",
output: "",
cmd: [],
index: -1,
"prep-cmd": [],
"detached": []
detached: [],
};
this.editForm.index = -1;
this.showEditForm = true;
@@ -124,12 +199,16 @@
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", []);
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 + "?");
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();
@@ -137,18 +216,21 @@
}
},
addPrepCmd() {
this.editForm['prep-cmd'].push({
do: '',
undo: '',
this.editForm["prep-cmd"].push({
do: "",
undo: "",
});
},
save() {
fetch("/api/apps", { method: "POST", body: JSON.stringify(this.editForm) }).then((r) => {
fetch("/api/apps", {
method: "POST",
body: JSON.stringify(this.editForm),
}).then((r) => {
if (r.status == 200) document.location.reload();
});
}
}
})
},
},
});
</script>
<style>
@@ -159,4 +241,4 @@
.monospace {
font-family: monospace;
}
</style>
</style>

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,23 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Sunshine</title>
<link
href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.0/dist/css/bootstrap.min.css"
rel="stylesheet"
integrity="sha384-wEmeIV1mKuiNpC+IOBjI7aAzPcEZeedi5yW5f2yOq55WWLwNGmvvx4Um1vskeMj0"
crossorigin="anonymous"
/>
<script
src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.0/dist/js/bootstrap.bundle.min.js"
integrity="sha384-p34f1UUtsS3wqzfto5wAAmdvj+osOnFyQFpp4Ua3gs/ZVWx6oOypYoCJhGGScy+8"
crossorigin="anonymous"
></script>
<script src="https://cdn.jsdelivr.net/npm/vue@2.6.12/dist/vue.js"></script>
</head>
<body></body>
</html>

View File

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

View File

@@ -1,9 +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 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>
</div>

View File

@@ -1,97 +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>
<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="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 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>
</form>
</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: ''
}
}
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"
}
});
}
}
})
};
},
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;
}
.config-page {
padding: 1em;
border: 1px solid #dee2e6;
border-top: none;
}
.buttons {
padding: 1em 0;
}
</style>
.buttons {
padding: 1em 0;
}
</style>

View File

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

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/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)

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.7.4
Depends: libssl1.1, libavdevice58, libboost-thread1.71.0, libboost-filesystem1.71.0, libboost-log1.71.0, libpulse0, libopus0, libxcb-shm0, libxcb-xfixes0
Version: 0.10.2
Depends: libssl1.1, libavdevice58, libboost-thread1.67.0 | libboost-thread1.71.0, libboost-filesystem1.67.0 | libboost-filesystem1.71.0, libboost-log1.67.0 | libboost-log1.71.0, libpulse0, libopus0, libxcb-shm0, libxcb-xfixes0, libxtst6, libevdev2, libdrm2
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
@@ -55,6 +68,16 @@ 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"
@@ -65,21 +88,28 @@ 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+ep /usr/bin/sunshine"
$path_to_setcap cap_sys_admin+ep /usr/bin/sunshine
fi
EOF
cat << 'EOF' > $RULES/85-sunshine-rules.rules
KERNEL=="uinput", GROUP="input", MODE="0660"
EOF
mkdir -p $ASSETS/shaders
cp sunshine $BIN/sunshine
cp @CMAKE_CURRENT_SOURCE_DIR@/assets/apps_linux.json $ASSETS/apps_linux.json
cp @CMAKE_CURRENT_SOURCE_DIR@/assets/sunshine.conf $ASSETS/sunshine.conf
cp @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

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,11 +87,14 @@ void encodeThread(packet_queue_t packets, sample_queue_t samples, config_t confi
OPUS_APPLICATION_AUDIO,
nullptr) };
opus_multistream_encoder_ctl(opus.get(), OPUS_SET_VBR(0));
// 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 { 1024 }; // 1KB
buffer_t packet { 1400 }; // 1KB
int bytes = opus_multistream_encode(opus.get(), sample->data(), frame_size, std::begin(packet), packet.size());
if(bytes < 0) {
@@ -99,12 +104,22 @@ void encodeThread(packet_queue_t packets, sample_queue_t samples, config_t confi
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])];
@@ -115,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;
}
@@ -148,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();
@@ -208,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

View File

@@ -1,10 +1,12 @@
extern "C" {
#include <cbs/cbs_h264.h>
#include <cbs/cbs_h265.h>
#include <cbs/h264_levels.h>
#include <cbs/video_levels.h>
#include <libavcodec/avcodec.h>
#include <libavutil/pixdesc.h>
}
#include "cbs.h"
#include "main.h"
#include "utility.h"
@@ -46,19 +48,16 @@ public:
}
};
util::buffer_t<std::uint8_t> write(std::uint8_t nal, void *uh, AVCodecID codec_id) {
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 NAL unit SPS: "sv << av_make_error_string(err_str, AV_ERROR_MAX_STRING_SIZE, err);
BOOST_LOG(error) << "Could not insert NAL unit SPS: "sv << av_make_error_string(err_str, AV_ERROR_MAX_STRING_SIZE, err);
return {};
}
cbs::ctx_t cbs_ctx;
ff_cbs_init(&cbs_ctx, codec_id, nullptr);
err = ff_cbs_write_fragment_data(cbs_ctx.get(), &frag);
if(err < 0) {
char err_str[AV_ERROR_MAX_STRING_SIZE] { 0 };
@@ -74,6 +73,13 @@ util::buffer_t<std::uint8_t> write(std::uint8_t nal, void *uh, AVCodecID codec_i
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 {};
@@ -162,9 +168,83 @@ util::buffer_t<std::uint8_t> make_sps_h264(const AVCodecContext *ctx) {
return write(sps.nal_unit_header.nal_unit_type, (void *)&sps.nal_unit_header, AV_CODEC_ID_H264);
}
util::buffer_t<std::uint8_t> read_sps(const AVPacket *packet, int codec_id) {
hevc_t make_sps_hevc(const AVCodecContext *avctx, const AVPacket *packet) {
cbs::ctx_t ctx;
if(ff_cbs_init(&ctx, (AVCodecID)codec_id, nullptr)) {
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 {};
}
@@ -178,24 +258,15 @@ util::buffer_t<std::uint8_t> read_sps(const AVPacket *packet, int codec_id) {
return {};
}
if(codec_id == AV_CODEC_ID_H264) {
auto h264 = (H264RawNALUnitHeader *)((CodedBitstreamH264Context *)ctx->priv_data)->active_sps;
return write(h264->nal_unit_type, (void *)h264, AV_CODEC_ID_H264);
}
auto hevc = (H264RawNALUnitHeader *)((CodedBitstreamH265Context *)ctx->priv_data)->active_sps;
return write(hevc->nal_unit_type, (void *)hevc, AV_CODEC_ID_H265);
auto h264 = (H264RawNALUnitHeader *)((CodedBitstreamH264Context *)ctx->priv_data)->active_sps;
return write(h264->nal_unit_type, (void *)h264, AV_CODEC_ID_H264);
}
util::buffer_t<std::uint8_t> make_sps(const AVCodecContext *ctx, int format) {
switch(format) {
case 0:
return make_sps_h264(ctx);
}
BOOST_LOG(warning) << "make_sps: video format ["sv << format << "] not supported"sv;
return {};
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) {
@@ -206,7 +277,7 @@ bool validate_sps(const AVPacket *packet, int codec_id) {
cbs::frag_t frag;
int err = ff_cbs_read_packet(ctx.get(), &frag, &*packet);
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);

View File

@@ -5,10 +5,25 @@
struct AVPacket;
struct AVCodecContext;
namespace cbs {
util::buffer_t<std::uint8_t> read_sps(const AVPacket *packet, int codec_id);
util::buffer_t<std::uint8_t> make_sps(const AVCodecContext *ctx, int video_format);
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

View File

@@ -20,7 +20,7 @@ using namespace std::literals;
#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 {
namespace nv {
@@ -147,7 +147,6 @@ int coder_from_view(const std::string_view &coder) {
} // namespace amd
video_t video {
0, // crf
28, // qp
0, // hevc_mode
@@ -176,16 +175,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,
@@ -209,9 +210,19 @@ 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 {
@@ -221,8 +232,9 @@ sunshine_t sunshine {
{}, // Username
{}, // Password
{}, // Password Salt
SUNSHINE_ASSETS_DIR "/sunshine.conf", // config file
{} // cmd args
SUNSHINE_CONFIG_DIR "/sunshine.conf", // config file
{}, // cmd args
47989,
};
bool endline(char ch) {
@@ -392,8 +404,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);
}
@@ -405,8 +429,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);
}
@@ -446,9 +482,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);
@@ -456,7 +495,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) {
@@ -535,7 +574,42 @@ 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 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);
}
}
@@ -549,8 +623,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;
@@ -564,11 +641,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 });
@@ -604,6 +684,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() });
@@ -614,7 +695,18 @@ void apply_config(std::unordered_map<std::string, std::string> &&vars) {
int_between_f(vars, "channels", stream.channels, { 1, std::numeric_limits<int>::max() });
path_f(vars, "file_apps", stream.file_apps);
int_between_f(vars, "fec_percentage", stream.fec_percentage, { 1, 100 });
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);
@@ -636,8 +728,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) {
@@ -721,11 +826,22 @@ 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));
}
}
}
if(!fs::exists(sunshine.config_file)) {
fs::copy_file(SUNSHINE_DEFAULT_DIR "/sunshine.conf", sunshine.config_file);
}
auto vars = parse_config(read_file(sunshine.config_file.c_str()));
for(auto &[name, value] : cmd_vars) {

View File

@@ -11,8 +11,7 @@
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;
@@ -59,6 +58,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
@@ -73,16 +73,22 @@ 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, // 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
};
}
@@ -103,6 +109,8 @@ struct sunshine_t {
int argc;
char **argv;
} cmd;
std::uint16_t port;
};
extern video_t video;

View File

@@ -15,7 +15,7 @@
#include <boost/asio/ssl/context.hpp>
#include <Simple-Web-Server/crypto.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"
@@ -30,10 +30,9 @@
#include "utility.h"
#include "uuid.h"
namespace confighttp {
using namespace std::literals;
constexpr auto PORT_HTTPS = 47990;
namespace confighttp {
namespace fs = std::filesystem;
namespace pt = boost::property_tree;
@@ -67,23 +66,38 @@ void print_req(const req_https_t &request) {
void send_unauthorized(resp_https_t response, req_https_t request) {
auto address = request->remote_endpoint_address();
BOOST_LOG(info) << '[' << address << "] -- denied"sv;
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_pin_allowed) {
BOOST_LOG(info) << '[' << address << "] -- denied"sv;
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);
});
@@ -186,6 +200,17 @@ void getPasswordPage(resp_https_t response, req_https_t request) {
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 getApps(resp_https_t response, req_https_t request) {
if(!authenticate(response, request)) return;
@@ -372,7 +397,7 @@ void saveConfig(resp_https_t response, req_https_t request) {
}
void savePassword(resp_https_t response, req_https_t request) {
if(!authenticate(response, request)) return;
if(!config::sunshine.username.empty() && !authenticate(response, request)) return;
print_req(request);
@@ -391,27 +416,31 @@ void savePassword(resp_https_t response, req_https_t request) {
try {
//TODO: Input Validation
pt::read_json(ss, inputTree);
auto username = inputTree.get<std::string>("currentUsername");
auto username = inputTree.count("currentUsername") > 0 ? inputTree.get<std::string>("currentUsername") : "";
auto newUsername = inputTree.get<std::string>("newUsername");
auto password = inputTree.get<std::string>("currentPassword");
auto newPassword = inputTree.get<std::string>("newPassword");
auto confirmPassword = inputTree.get<std::string>("confirmNewPassword");
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;
auto hash = util::hex(crypto::hash(password + config::sunshine.salt)).to_string();
if(username == config::sunshine.username && hash == config::sunshine.password) {
if(newPassword != confirmPassword) {
outputTree.put("status", false);
outputTree.put("error", "Password Mismatch");
}
http::save_user_creds(config::sunshine.credentials_file, newUsername, newPassword);
http::reload_user_creds(config::sunshine.credentials_file);
outputTree.put("status", true);
}
else {
if(newUsername.length() == 0){
outputTree.put("status", false);
outputTree.put("error", "Invalid Current Credentials");
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) {
@@ -452,7 +481,11 @@ void savePin(resp_https_t response, req_https_t request) {
}
}
void start(std::shared_ptr<safe::signal_t> shutdown_event) {
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);
@@ -464,6 +497,7 @@ void start(std::shared_ptr<safe::signal_t> shutdown_event) {
server.resource["^/clients$"]["GET"] = getClientsPage;
server.resource["^/config$"]["GET"] = getConfigPage;
server.resource["^/password$"]["GET"] = getPasswordPage;
server.resource["^/welcome$"]["GET"] = getWelcomePage;
server.resource["^/api/pin"]["POST"] = savePin;
server.resource["^/api/apps$"]["GET"] = getApps;
server.resource["^/api/apps$"]["POST"] = saveApp;
@@ -473,14 +507,14 @@ void start(std::shared_ptr<safe::signal_t> shutdown_event) {
server.resource["^/api/apps/([0-9]+)$"]["DELETE"] = deleteApp;
server.config.reuse_address = true;
server.config.address = "0.0.0.0"s;
server.config.port = PORT_HTTPS;
server.config.port = port_https;
try {
server.bind();
BOOST_LOG(info) << "Configuration UI available at [https://localhost:"sv << PORT_HTTPS << "]";
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();
BOOST_LOG(fatal) << "Couldn't bind http server to ports ["sv << port_https << "]: "sv << err.what();
shutdown_event->raise(true);
return;
@@ -495,7 +529,7 @@ void start(std::shared_ptr<safe::signal_t> shutdown_event) {
return;
}
BOOST_LOG(fatal) << "Couldn't start Configuration HTTP server to ports ["sv << PORT_HTTPS << ", "sv << PORT_HTTPS << "]: "sv << err.what();
BOOST_LOG(fatal) << "Couldn't start Configuration HTTPS server to port ["sv << port_https << "]: "sv << err.what();
shutdown_event->raise(true);
return;
}
@@ -509,4 +543,4 @@ void start(std::shared_ptr<safe::signal_t> shutdown_event) {
tcp.join();
}
} // namespace confighttp
} // namespace confighttp

View File

@@ -14,7 +14,8 @@
namespace confighttp {
void start(std::shared_ptr<safe::signal_t> shutdown_event);
}
constexpr auto PORT_HTTPS = 1;
void start();
} // namespace confighttp
#endif //SUNSHINE_CONFIGHTTP_H

View File

@@ -4,6 +4,7 @@
#include "crypto.h"
#include <openssl/pem.h>
namespace crypto {
using big_num_t = util::safe_ptr<BIGNUM, BN_free>;
//using rsa_t = util::safe_ptr<RSA, RSA_free>;
@@ -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;

View File

@@ -66,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

View File

@@ -35,10 +35,13 @@ 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);
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;
@@ -51,15 +54,11 @@ int init() {
return -1;
}
}
if(!user_creds_exist(config::sunshine.credentials_file)) {
if(save_user_creds(config::sunshine.credentials_file, "sunshine"s, crypto::rand_alphabet(16), true)) {
return -1;
}
if(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";
}
if(reload_user_creds(config::sunshine.credentials_file)) {
return -1;
}
return 0;
}
@@ -89,12 +88,6 @@ int save_user_creds(const std::string &file, const std::string &username, const
}
BOOST_LOG(info) << "New credentials have been created"sv;
if(run_our_mouth) {
BOOST_LOG(info) << "Username: "sv << username;
BOOST_LOG(info) << "Password: "sv << password;
}
return 0;
}

View File

@@ -14,5 +14,6 @@ int save_user_creds(
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,103 @@ 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;
}
/**
* 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 print(PNV_REL_MOUSE_MOVE_PACKET packet) {
BOOST_LOG(debug)
@@ -142,7 +236,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 +283,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 +309,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 +338,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);
@@ -294,43 +399,45 @@ void passthrough(std::shared_ptr<input_t> &input, PNV_MOUSE_BUTTON_PACKET packet
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, 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;
}
short map_keycode(short keycode) {
keycode &= 0x00FF;
switch(keycode) {
case 0x10:
return 0xA0;
case 0x11:
return 0xA2;
case 0x12:
return 0xA4;
auto it = config::input.keybindings.find(keycode);
if(it != std::end(config::input.keybindings)) {
return it->second;
}
return keycode;
}
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 {
@@ -345,16 +452,15 @@ void passthrough(std::shared_ptr<input_t> &input, PNV_KEYBOARD_PACKET packet) {
pressed = !release;
platf::keyboard(platf_input, map_keycode(packet->keyCode), release);
update_shortcutFlags(&input->shortcutFlags, 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, platf::rumble_queue_t rumble_queue) {
auto xorGamepadMask = old_state ^ new_state;
if(!xorGamepadMask) {
return 0;
@@ -380,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, std::move(rumble_queue))) {
free_id(gamepadMask, id);
// allocating a gamepad failed: solution: ignore gamepads
// The implementations of platf::alloc_gamepad already has logging
@@ -396,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;
}
@@ -422,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,
@@ -532,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
@@ -551,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

@@ -19,9 +19,11 @@
#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"
@@ -30,6 +32,8 @@ extern "C" {
#include <rs.h>
}
safe::mail_t mail::man;
using namespace std::literals;
namespace bl = boost::log;
@@ -41,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;
@@ -57,13 +61,16 @@ void print_help(const char *name) {
<< "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
<< " --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;
<< " | 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 {
@@ -108,6 +115,25 @@ std::map<std::string_view, std::function<int(const char *name, int argc, char **
};
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;
}
@@ -159,6 +185,10 @@ 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);
@@ -178,10 +208,34 @@ int main(int argc, char *argv[]) {
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);
});
on_signal(SIGTERM, [&force_shutdown, shutdown_event]() {
BOOST_LOG(info) << "Terminate 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_event->raise(true);
});
@@ -193,6 +247,7 @@ int main(int argc, char *argv[]) {
}
reed_solomon_init();
auto input_deinit_guard = input::init();
if(video::init()) {
return 2;
}
@@ -200,11 +255,25 @@ int main(int argc, char *argv[]) {
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 };
std::thread configThread { confighttp::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();
@@ -243,4 +312,8 @@ int write_file(const char *path, const std::string_view &contents) {
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;
@@ -24,4 +28,30 @@ 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

@@ -30,13 +30,11 @@
#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;
@@ -134,7 +132,7 @@ void save_state() {
void load_state() {
if(!fs::exists(config::nvhttp.file_state)) {
BOOST_LOG(info) << "DOENST EXIST"sv;
BOOST_LOG(info) << "File "sv << config::nvhttp.file_state << " doesn't exist"sv;
http::unique_id = util::uuid_t::generate().string();
return;
}
@@ -192,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;
@@ -213,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);
@@ -224,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);
@@ -244,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);
@@ -369,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>
@@ -379,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);
@@ -402,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;
@@ -417,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;
}
}
@@ -437,11 +441,6 @@ 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());
}
bool pin(std::string pin) {
@@ -478,10 +477,12 @@ 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) << '[' << address << "] -- denied"sv;
BOOST_LOG(info) << "/pin: ["sv << address << "] -- denied"sv;
response->write(SimpleWeb::StatusCode::client_error_forbidden);
@@ -522,6 +523,8 @@ 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", 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());
@@ -574,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) {
@@ -586,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();
@@ -629,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) {
@@ -675,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);
}
@@ -687,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,
@@ -720,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);
}
@@ -732,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,
@@ -756,9 +766,14 @@ 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;
}
void start(std::shared_ptr<safe::signal_t> shutdown_event) {
void start() {
auto shutdown_event = mail::man->event<bool>(mail::shutdown);
auto port_http = map_port(PORT_HTTP);
auto port_https = map_port(PORT_HTTPS);
bool clean_slate = config::sunshine.flags[config::flag::FRESH_STATE];
@@ -782,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];
@@ -808,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>;
@@ -837,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>;
@@ -846,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;
@@ -869,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;
}

View File

@@ -6,13 +6,13 @@
#define SUNSHINE_NVHTTP_H
#include "thread_safe.h"
#include <Simple-Web-Server/server_http.hpp>
#include <Simple-Web-Server/server_https.hpp>
#include <functional>
#include <string>
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);
} // namespace nvhttp

View File

@@ -7,9 +7,11 @@
#include <bitset>
#include <filesystem>
#include <functional>
#include <mutex>
#include <string>
#include "sunshine/thread_safe.h"
#include "sunshine/utility.h"
struct sockaddr;
@@ -34,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,
@@ -75,6 +89,7 @@ enum class mem_type_e {
system,
vaapi,
dxgi,
cuda,
unknown
};
@@ -105,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 {
@@ -137,10 +147,6 @@ public:
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;
};
@@ -188,9 +194,33 @@ 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;
@@ -202,6 +232,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;
};
@@ -236,7 +267,20 @@ std::string from_sockaddr(const sockaddr *const);
std::pair<std::uint16_t, std::string> from_sockaddr_ex(const sockaddr *const);
std::unique_ptr<audio_control_t> audio_control();
std::shared_ptr<display_t> display(mem_type_e hwdevice_type);
/**
* 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
std::vector<std::string> display_names();
input_t input();
void move_mouse(input_t &input, int deltaX, int deltaY);
@@ -246,10 +290,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

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

View File

@@ -0,0 +1,707 @@
#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 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 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;
}
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;
bool fail() {
return eglGetError() != EGL_SUCCESS;
}
display_t make_display(gbm::gbm_t::pointer gbm) {
constexpr auto EGL_PLATFORM_GBM_MESA = 0x31D7;
display_t display = eglGetPlatformDisplay(EGL_PLATFORM_GBM_MESA, gbm, 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",
"EGL_KHR_image_pixmap"
};
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;
}
std::optional<rgb_t> import_source(display_t::pointer egl_display, const surface_descriptor_t &xrgb) {
EGLAttrib img_attr_planes[13] {
EGL_LINUX_DRM_FOURCC_EXT, DRM_FORMAT_XRGB8888,
EGL_WIDTH, xrgb.width,
EGL_HEIGHT, xrgb.height,
EGL_DMA_BUF_PLANE0_FD_EXT, xrgb.fd,
EGL_DMA_BUF_PLANE0_OFFSET_EXT, xrgb.offset,
EGL_DMA_BUF_PLANE0_PITCH_EXT, xrgb.pitch,
EGL_NONE
};
rgb_t rgb {
egl_display,
eglCreateImage(egl_display, EGL_NO_CONTEXT, EGL_LINUX_DMA_BUF_EXT, nullptr, img_attr_planes),
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.fd,
EGL_DMA_BUF_PLANE0_OFFSET_EXT, r8.offset,
EGL_DMA_BUF_PLANE0_PITCH_EXT, r8.pitch,
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.fd,
EGL_DMA_BUF_PLANE0_OFFSET_EXT, gr88.offset,
EGL_DMA_BUF_PLANE0_PITCH_EXT, gr88.pitch,
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);
}
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) {
gl::ctx.BindTexture(GL_TEXTURE_2D, tex[0]);
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(cursor_t &img, int offset_x, int offset_y, int framebuffer) {
gl::ctx.BindFramebuffer(GL_FRAMEBUFFER, framebuffer);
gl::ctx.ReadBuffer(GL_COLOR_ATTACHMENT0);
gl::ctx.BindTexture(GL_TEXTURE_2D, tex[0]);
gl::ctx.CopyTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, offset_x, offset_y, in_width, in_height);
if(img.data) {
gl::ctx.BindTexture(GL_TEXTURE_2D, tex[1]);
if(serial != img.serial) {
serial = img.serial;
gl::ctx.TexStorage2D(GL_TEXTURE_2D, 1, GL_RGBA8, img.width, img.height);
gl::ctx.TexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, img.width, img.height, GL_BGRA, GL_UNSIGNED_BYTE, img.data);
}
gl::ctx.Enable(GL_BLEND);
GLenum attachment = GL_COLOR_ATTACHMENT0;
gl::ctx.BindFramebuffer(GL_FRAMEBUFFER, cursor_framebuffer[0]);
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.UseProgram(program[2].handle());
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);
}
int sws_t::convert(nv12_t &nv12) {
gl::ctx.BindTexture(GL_TEXTURE_2D, tex[0]);
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, nv12->buf[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,267 @@
#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(x) #x
#define gl_drain_errors_helper(x) gl::drain_errors("line " SUNSHINE_STRINGIFY(x))
#define gl_drain_errors gl_drain_errors_helper(__LINE__)
extern "C" int close(int __fd);
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);
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;
});
}
};
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 fd;
int width;
int height;
int offset;
int pitch;
};
display_t make_display(gbm::gbm_t::pointer gbm);
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;
};
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);
int convert(nv12_t &nv12);
void load_ram(platf::img_t &img);
void load_vram(cursor_t &img, int offset_x, int offset_y, int framebuffer);
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;
// 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;
// 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

View File

@@ -0,0 +1,745 @@
#include <drm_fourcc.h>
#include <errno.h>
#include <fcntl.h>
#include <unistd.h>
#include <xf86drm.h>
#include <xf86drmMode.h>
#include <filesystem>
#include "sunshine/main.h"
#include "sunshine/platform/common.h"
#include "sunshine/round_robin.h"
#include "sunshine/utility.h"
// Cursor rendering support through x11
#include "graphics.h"
#include "vaapi.h"
#include "x11grab.h"
using namespace std::literals;
namespace fs = std::filesystem;
namespace platf {
namespace kms {
using plane_res_t = util::safe_ptr<drmModePlaneRes, drmModeFreePlaneResources>;
using plane_t = util::safe_ptr<drmModePlane, drmModeFreePlane>;
using fb_t = util::safe_ptr<drmModeFB, drmModeFreeFB>;
using crtc_t = util::safe_ptr<drmModeCrtc, drmModeFreeCrtc>;
using obj_prop_t = util::safe_ptr<drmModeObjectProperties, drmModeFreeObjectProperties>;
using prop_t = util::safe_ptr<drmModePropertyRes, drmModeFreeProperty>;
static int env_width;
static int env_height;
std::string_view plane_type(std::uint64_t val) {
switch(val) {
case DRM_PLANE_TYPE_OVERLAY:
return "DRM_PLANE_TYPE_OVERLAY"sv;
case DRM_PLANE_TYPE_PRIMARY:
return "DRM_PLANE_TYPE_PRIMARY"sv;
case DRM_PLANE_TYPE_CURSOR:
return "DRM_PLANE_TYPE_CURSOR"sv;
}
return "UNKNOWN"sv;
}
class plane_it_t : public util::it_wrap_t<plane_t::element_type, plane_it_t> {
public:
plane_it_t(int fd, std::uint32_t *plane_p, std::uint32_t *end)
: fd { fd }, plane_p { plane_p }, end { end } {
inc();
}
plane_it_t(int fd, std::uint32_t *end)
: fd { fd }, plane_p { end }, end { end } {}
void inc() {
this->plane.reset();
for(; plane_p != end; ++plane_p) {
plane_t plane = drmModeGetPlane(fd, *plane_p);
if(!plane) {
BOOST_LOG(error) << "Couldn't get drm plane ["sv << (end - plane_p) << "]: "sv << strerror(errno);
continue;
}
// If this plane is unused
if(plane->fb_id) {
this->plane = util::make_shared<plane_t>(plane.release());
// One last increment
++plane_p;
break;
}
}
}
bool eq(const plane_it_t &other) const {
return plane_p == other.plane_p;
}
plane_t::pointer get() {
return plane.get();
}
int fd;
std::uint32_t *plane_p;
std::uint32_t *end;
util::shared_t<plane_t> plane;
};
class card_t {
public:
int init(const char *path) {
fd.el = open(path, O_RDWR);
if(fd.el < 0) {
BOOST_LOG(error) << "Couldn't open: "sv << path << ": "sv << strerror(errno);
return -1;
}
if(drmSetClientCap(fd.el, DRM_CLIENT_CAP_UNIVERSAL_PLANES, 1)) {
BOOST_LOG(error) << "Couldn't expose some/all drm planes for card: "sv << path;
return -1;
}
if(drmSetClientCap(fd.el, DRM_CLIENT_CAP_ATOMIC, 1)) {
BOOST_LOG(warning) << "Couldn't expose some properties for card: "sv << path;
}
plane_res.reset(drmModeGetPlaneResources(fd.el));
if(!plane_res) {
BOOST_LOG(error) << "Couldn't get drm plane resources"sv;
return -1;
}
return 0;
}
fb_t fb(plane_t::pointer plane) {
return drmModeGetFB(fd.el, plane->fb_id);
}
crtc_t crtc(std::uint32_t id) {
return drmModeGetCrtc(fd.el, id);
}
file_t handleFD(std::uint32_t handle) {
file_t fb_fd;
auto status = drmPrimeHandleToFD(fd.el, handle, 0 /* flags */, &fb_fd.el);
if(status) {
return {};
}
return fb_fd;
}
std::vector<std::pair<prop_t, std::uint64_t>> props(std::uint32_t id, std::uint32_t type) {
obj_prop_t obj_prop = drmModeObjectGetProperties(fd.el, id, type);
std::vector<std::pair<prop_t, std::uint64_t>> props;
props.reserve(obj_prop->count_props);
for(auto x = 0; x < obj_prop->count_props; ++x) {
props.emplace_back(drmModeGetProperty(fd.el, obj_prop->props[x]), obj_prop->prop_values[x]);
}
return props;
}
std::vector<std::pair<prop_t, std::uint64_t>> plane_props(std::uint32_t id) {
return props(id, DRM_MODE_OBJECT_PLANE);
}
std::vector<std::pair<prop_t, std::uint64_t>> crtc_props(std::uint32_t id) {
return props(id, DRM_MODE_OBJECT_CRTC);
}
std::vector<std::pair<prop_t, std::uint64_t>> connector_props(std::uint32_t id) {
return props(id, DRM_MODE_OBJECT_CONNECTOR);
}
plane_t
operator[](std::uint32_t index) {
return drmModeGetPlane(fd.el, plane_res->planes[index]);
}
std::uint32_t count() {
return plane_res->count_planes;
}
plane_it_t begin() const {
return plane_it_t { fd.el, plane_res->planes, plane_res->planes + plane_res->count_planes };
}
plane_it_t end() const {
return plane_it_t { fd.el, plane_res->planes + plane_res->count_planes };
}
file_t fd;
plane_res_t plane_res;
};
struct kms_img_t : public img_t {
~kms_img_t() override {
delete[] data;
data = nullptr;
}
};
void print(plane_t::pointer plane, fb_t::pointer fb, crtc_t::pointer crtc) {
if(crtc) {
BOOST_LOG(debug) << "crtc("sv << crtc->x << ", "sv << crtc->y << ')';
BOOST_LOG(debug) << "crtc("sv << crtc->width << ", "sv << crtc->height << ')';
BOOST_LOG(debug) << "plane->possible_crtcs == "sv << plane->possible_crtcs;
}
BOOST_LOG(debug)
<< "x("sv << plane->x
<< ") y("sv << plane->y
<< ") crtc_x("sv << plane->crtc_x
<< ") crtc_y("sv << plane->crtc_y
<< ") crtc_id("sv << plane->crtc_id
<< ')';
BOOST_LOG(debug)
<< "Resolution: "sv << fb->width << 'x' << fb->height
<< ": Pitch: "sv << fb->pitch
<< ": bpp: "sv << fb->bpp
<< ": depth: "sv << fb->depth;
std::stringstream ss;
ss << "Format ["sv;
std::for_each_n(plane->formats, plane->count_formats - 1, [&ss](auto format) {
ss << util::view(format) << ", "sv;
});
ss << util::view(plane->formats[plane->count_formats - 1]) << ']';
BOOST_LOG(debug) << ss.str();
}
class display_t : public platf::display_t {
public:
display_t(mem_type_e mem_type) : platf::display_t(), mem_type { mem_type } {}
~display_t() {
while(!thread_pool.cancel(loop_id))
;
}
mem_type_e mem_type;
std::chrono::nanoseconds delay;
// Done on a seperate thread to prevent additional latency to capture code
// This code detects if the framebuffer has been removed from KMS
void task_loop() {
capture_e capture = capture_e::reinit;
std::uint32_t framebuffer_count = 0;
auto end = std::end(card);
for(auto plane = std::begin(card); plane != end; ++plane) {
if(++framebuffer_count != framebuffer_index) {
continue;
}
auto fb = card.fb(plane.get());
if(!fb) {
BOOST_LOG(error) << "Couldn't get drm fb for plane ["sv << plane->fb_id << "]: "sv << strerror(errno);
capture = capture_e::error;
}
auto crct = card.crtc(plane->crtc_id);
bool different =
fb->width != img_width ||
fb->height != img_height ||
fb->pitch != pitch ||
crct->x != offset_x ||
crct->y != offset_y;
if(!different) {
capture = capture_e::ok;
break;
}
}
this->status = capture;
loop_id = thread_pool.pushDelayed(&display_t::task_loop, 2s, this).task_id;
}
int init(const std::string &display_name, int framerate) {
delay = std::chrono::nanoseconds { 1s } / framerate;
int monitor_index = util::from_view(display_name);
int monitor = 0;
fs::path card_dir { "/dev/dri"sv };
for(auto &entry : fs::directory_iterator { card_dir }) {
auto file = entry.path().filename();
auto filestring = file.generic_u8string();
if(std::string_view { filestring }.substr(0, 4) != "card"sv) {
continue;
}
kms::card_t card;
if(card.init(entry.path().c_str())) {
return {};
}
std::uint32_t framebuffer_index = 0;
auto end = std::end(card);
for(auto plane = std::begin(card); plane != end; ++plane) {
++framebuffer_index;
bool cursor = false;
auto props = card.plane_props(plane->plane_id);
for(auto &[prop, val] : props) {
if(prop->name == "type"sv) {
BOOST_LOG(verbose) << prop->name << "::"sv << kms::plane_type(val);
if(val == DRM_PLANE_TYPE_CURSOR) {
// Don't count as a monitor when it is a cursor
cursor = true;
break;
}
}
}
if(cursor) {
continue;
}
if(monitor != monitor_index) {
++monitor;
continue;
}
auto fb = card.fb(plane.get());
if(!fb) {
BOOST_LOG(error) << "Couldn't get drm fb for plane ["sv << plane->fb_id << "]: "sv << strerror(errno);
return -1;
}
if(!fb->handle) {
BOOST_LOG(error)
<< "Couldn't get handle for DRM Framebuffer ["sv << plane->fb_id << "]: Possibly not permitted: do [sudo setcap cap_sys_admin+ep sunshine]"sv;
return -1;
}
fb_fd = card.handleFD(fb->handle);
if(fb_fd.el < 0) {
BOOST_LOG(error) << "Couldn't get primary file descriptor for Framebuffer ["sv << fb->fb_id << "]: "sv << strerror(errno);
continue;
}
BOOST_LOG(info) << "Found monitor for DRM screencasting"sv;
auto crct = card.crtc(plane->crtc_id);
kms::print(plane.get(), fb.get(), crct.get());
img_width = fb->width;
img_height = fb->height;
width = crct->width;
height = crct->height;
pitch = fb->pitch;
this->env_width = ::platf::kms::env_width;
this->env_height = ::platf::kms::env_height;
offset_x = crct->x;
offset_y = crct->y;
this->card = std::move(card);
this->framebuffer_index = framebuffer_index;
goto break_loop;
}
}
// Neatly break from nested for loop
break_loop:
if(monitor != monitor_index) {
BOOST_LOG(error) << "Couldn't find monitor ["sv << monitor_index << ']';
return -1;
}
cursor_opt = x11::cursor_t::make();
status = capture_e::ok;
thread_pool.start(1);
loop_id = thread_pool.pushDelayed(&display_t::task_loop, 2s, this).task_id;
return 0;
}
// When the framebuffer is reinitialized, this id can no longer be found
std::uint32_t framebuffer_index;
capture_e status;
int img_width, img_height;
int pitch;
card_t card;
file_t fb_fd;
std::optional<x11::cursor_t> cursor_opt;
util::TaskPool::task_id_t loop_id;
util::ThreadPool thread_pool;
};
class display_ram_t : public display_t {
public:
display_ram_t(mem_type_e mem_type) : display_t(mem_type) {}
int init(const std::string &display_name, int framerate) {
if(!gbm::create_device) {
BOOST_LOG(warning) << "libgbm not initialized"sv;
return -1;
}
if(display_t::init(display_name, framerate)) {
return -1;
}
gbm.reset(gbm::create_device(card.fd.el));
if(!gbm) {
BOOST_LOG(error) << "Couldn't create GBM device: ["sv << util::hex(eglGetError()).to_string_view() << ']';
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);
auto rgb_opt = egl::import_source(display.get(),
{
fb_fd.el,
img_width,
img_height,
0,
pitch,
});
if(!rgb_opt) {
return -1;
}
rgb = std::move(*rgb_opt);
return 0;
}
capture_e capture(snapshot_cb_t &&snapshot_cb, std::shared_ptr<img_t> img, bool *cursor) {
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:
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;
}
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);
}
return std::make_shared<hwdevice_t>();
}
capture_e snapshot(img_t *img_out_base, std::chrono::milliseconds timeout, bool cursor) {
gl::ctx.BindTexture(GL_TEXTURE_2D, rgb->tex[0]);
gl::ctx.GetTextureSubImage(rgb->tex[0], 0, offset_x, offset_y, 0, width, height, 1, GL_BGRA, GL_UNSIGNED_BYTE, img_out_base->height * img_out_base->row_pitch, img_out_base->data);
if(cursor_opt && cursor) {
cursor_opt->blend(*img_out_base, offset_x, offset_y);
}
return status;
}
std::shared_ptr<img_t> alloc_img() override {
auto img = std::make_shared<kms_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 {
snapshot(img, 1s, false);
return 0;
}
gbm::gbm_t gbm;
egl::display_t display;
egl::ctx_t ctx;
egl::rgb_t rgb;
};
class display_vram_t : public display_t {
public:
display_vram_t(mem_type_e mem_type) : display_t(mem_type) {}
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, dup(card.fd.el), offset_x, offset_y,
{
fb_fd.el,
img_width,
img_height,
0,
pitch,
});
}
BOOST_LOG(error) << "Unsupported pixel format for egl::display_vram_t: "sv << platf::from_pix_fmt(pix_fmt);
return nullptr;
}
std::shared_ptr<img_t> alloc_img() override {
auto img = std::make_shared<egl::cursor_t>();
img->serial = std::numeric_limits<decltype(img->serial)>::max();
img->data = nullptr;
img->pixel_pitch = 4;
return img;
}
int dummy_img(platf::img_t *img) override {
return 0;
}
capture_e capture(snapshot_cb_t &&snapshot_cb, std::shared_ptr<img_t> img, bool *cursor) {
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:
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) {
if(!cursor || !cursor_opt) {
img_out_base->data = nullptr;
return capture_e::ok;
}
auto img = (egl::cursor_t *)img_out_base;
cursor_opt->capture(*img);
img->x -= offset_x;
img->y -= offset_y;
return status;
}
int init(const std::string &display_name, int framerate) {
if(display_t::init(display_name, framerate)) {
return -1;
}
if(!va::validate(card.fd.el)) {
BOOST_LOG(warning) << "Monitor "sv << display_name << " doesn't support hardware encoding. Reverting back to GPU -> RAM -> GPU"sv;
return -1;
}
return 0;
}
};
} // namespace kms
std::shared_ptr<display_t> kms_display(mem_type_e hwdevice_type, const std::string &display_name, int framerate) {
if(hwdevice_type == mem_type_e::vaapi) {
auto disp = std::make_shared<kms::display_vram_t>(hwdevice_type);
if(!disp->init(display_name, framerate)) {
return disp;
}
// In the case of failure, attempt the old method for VAAPI
}
auto disp = std::make_shared<kms::display_ram_t>(hwdevice_type);
if(disp->init(display_name, framerate)) {
return nullptr;
}
return disp;
}
// A list of names of displays accepted as display_name
std::vector<std::string> kms_display_names() {
kms::env_width = 0;
kms::env_height = 0;
int count = 0;
if(!gbm::create_device) {
BOOST_LOG(warning) << "libgbm not initialized"sv;
return {};
}
std::vector<std::string> display_names;
fs::path card_dir { "/dev/dri"sv };
for(auto &entry : fs::directory_iterator { card_dir }) {
auto file = entry.path().filename();
auto filestring = file.generic_u8string();
if(std::string_view { filestring }.substr(0, 4) != "card"sv) {
continue;
}
kms::card_t card;
if(card.init(entry.path().c_str())) {
return {};
}
auto end = std::end(card);
for(auto plane = std::begin(card); plane != end; ++plane) {
auto fb = card.fb(plane.get());
if(!fb) {
BOOST_LOG(error) << "Couldn't get drm fb for plane ["sv << plane->fb_id << "]: "sv << strerror(errno);
continue;
}
if(!fb->handle) {
BOOST_LOG(error)
<< "Couldn't get handle for DRM Framebuffer ["sv << plane->fb_id << "]: Possibly not permitted: do [sudo setcap cap_sys_admin+ep sunshine]"sv;
break;
}
bool cursor = false;
{
BOOST_LOG(verbose) << "PLANE INFO ["sv << count << ']';
auto props = card.plane_props(plane->plane_id);
for(auto &[prop, val] : props) {
if(prop->name == "type"sv) {
BOOST_LOG(verbose) << prop->name << "::"sv << kms::plane_type(val);
if(val == DRM_PLANE_TYPE_CURSOR) {
cursor = true;
}
}
else {
BOOST_LOG(verbose) << prop->name << "::"sv << val;
}
}
}
{
BOOST_LOG(verbose) << "CRTC INFO"sv;
auto props = card.crtc_props(plane->crtc_id);
for(auto &[prop, val] : props) {
BOOST_LOG(verbose) << prop->name << "::"sv << val;
}
}
// This appears to return the offset of the monitor
auto crtc = card.crtc(plane->crtc_id);
if(!crtc) {
BOOST_LOG(error) << "Couldn't get crtc info: "sv << strerror(errno);
return {};
}
kms::env_width = std::max(kms::env_width, (int)(crtc->x + crtc->width));
kms::env_height = std::max(kms::env_height, (int)(crtc->y + crtc->height));
kms::print(plane.get(), fb.get(), crtc.get());
if(!cursor) {
display_names.emplace_back(std::to_string(count++));
}
}
}
return display_names;
}
} // namespace platf

View File

@@ -1,17 +1,69 @@
#include "sunshine/platform/common.h"
#include <fstream>
#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;
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>;
@@ -85,4 +137,94 @@ std::string get_mac_address(const std::string_view &address) {
BOOST_LOG(warning) << "Unable to find MAC address for "sv << address;
return "00:00:00:00:00:00"s;
}
enum class source_e {
#ifdef SUNSHINE_BUILD_DRM
KMS,
#endif
#ifdef SUNSHINE_BUILD_X11
X11,
#endif
};
static source_e source;
#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 !x11_display_names().empty();
}
#endif
std::vector<std::string> display_names() {
switch(source) {
#ifdef SUNSHINE_BUILD_DRM
case source_e::KMS:
return kms_display_names();
#endif
#ifdef SUNSHINE_BUILD_X11
case source_e::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) {
switch(source) {
#ifdef SUNSHINE_BUILD_DRM
case source_e::KMS:
return kms_display(hwdevice_type, display_name, framerate);
#endif
#ifdef SUNSHINE_BUILD_X11
case source_e::X11:
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();
#ifdef SUNSHINE_BUILD_DRM
if(verify_kms()) {
BOOST_LOG(info) << "Using KMS for screencasting"sv;
source = source_e::KMS;
goto found_source;
}
#endif
#ifdef SUNSHINE_BUILD_X11
if(verify_x11()) {
BOOST_LOG(info) << "Using X11 for screencasting"sv;
source = source_e::X11;
goto found_source;
}
#endif
// Did not find a source
return nullptr;
// Normally, I would simply use if-else statements to achieve this result,
// but due to the macro's, (*spits on ground*), it would be too messy
found_source:
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,23 @@
#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);
}
});
namespace dyn {
typedef void (*apiproc)(void);
int load(void *handle, const std::vector<std::tuple<apiproc *, const char *>> &funcs, bool strict = true);
void *handle(const std::vector<const char *> &libs);
} // namespace dyn
#endif

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -1,8 +1,19 @@
#ifndef SUNSHINE_DISPLAY_H
#define SUNSHINE_DISPLAY_H
#ifndef SUNSHINE_VAAPI_H
#define SUNSHINE_VAAPI_H
#include "misc.h"
#include "sunshine/platform/common.h"
namespace platf::egl {
std::shared_ptr<hwdevice_t> make_hwdevice(int width, int height);
} // namespace platf::egl
namespace egl {
struct surface_descriptor_t;
}
namespace va {
std::shared_ptr<platf::hwdevice_t> make_hwdevice(int width, int height);
std::shared_ptr<platf::hwdevice_t> make_hwdevice(int width, int height, file_t &&card, int offset_x, int offset_y, const egl::surface_descriptor_t &sd);
// Ensure the render device pointed to by fd is capable of encoding h264
bool validate(int fd);
int init();
} // namespace va
#endif

View File

@@ -0,0 +1,812 @@
//
// 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 "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 xdisplay_t = util::dyn_safe_ptr_v2<Display, int, &x11::CloseDisplay>;
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;
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;
}
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 {
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) {
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);
}
return std::make_shared<hwdevice_t>();
}
int dummy_img(img_t *img) override {
snapshot(img, 0s, true);
return 0;
}
};
struct shm_attr_t : public x11_attr_t {
xdisplay_t shm_xdisplay; // Prevent race condition with x11_attr_t::xdisplay
xcb_connect_t xcb;
xcb_screen_t *display;
std::uint32_t seg;
shm_id_t shm_id;
shm_data_t data;
util::TaskPool::task_id_t refresh_task_id;
void delayed_refresh() {
refresh();
refresh_task_id = task_pool.pushDelayed(&shm_attr_t::delayed_refresh, 2s, this).task_id;
}
shm_attr_t(mem_type_e mem_type) : x11_attr_t(mem_type), shm_xdisplay { 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) {
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 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;
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 = 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);
}
void freeCursorCtx(cursor_ctx_t::pointer ctx) {
x11::CloseDisplay((xdisplay_t::pointer)ctx);
}
} // namespace x11
} // namespace platf

View File

@@ -0,0 +1,48 @@
#ifndef SUNSHINE_X11_GRAB
#define SUNSHINE_X11_GRAB
#include <optional>
#include "sunshine/platform/common.h"
#include "sunshine/utility.h"
namespace egl {
class cursor_t;
}
namespace platf::x11 {
#ifdef SUNSHINE_BUILD_X11
struct cursor_ctx_raw_t;
void freeCursorCtx(cursor_ctx_raw_t *ctx);
using cursor_ctx_t = util::safe_ptr<cursor_ctx_raw_t, freeCursorCtx>;
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;
};
#else
class cursor_t {
public:
static std::optional<cursor_t> make() { return std::nullopt; }
void capture(egl::cursor_t &) {}
void blend(img_t &, int, int) {}
};
#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;
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(mem_type_e hwdevice_type) {
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 == 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() {
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

@@ -27,18 +27,6 @@ static void free_frame(AVFrame *frame) {
using frame_t = util::safe_ptr<AVFrame, free_frame>;
namespace platf::dxgi {
using input_layout_t = util::safe_ptr<ID3D11InputLayout, Release<ID3D11InputLayout>>;
using render_target_t = util::safe_ptr<ID3D11RenderTargetView, Release<ID3D11RenderTargetView>>;
using shader_res_t = util::safe_ptr<ID3D11ShaderResourceView, Release<ID3D11ShaderResourceView>>;
using buf_t = util::safe_ptr<ID3D11Buffer, Release<ID3D11Buffer>>;
using blend_t = util::safe_ptr<ID3D11BlendState, Release<ID3D11BlendState>>;
using raster_state_t = util::safe_ptr<ID3D11RasterizerState, Release<ID3D11RasterizerState>>;
using sampler_state_t = util::safe_ptr<ID3D11SamplerState, Release<ID3D11SamplerState>>;
using vs_t = util::safe_ptr<ID3D11VertexShader, Release<ID3D11VertexShader>>;
using ps_t = util::safe_ptr<ID3D11PixelShader, Release<ID3D11PixelShader>>;
using blob_t = util::safe_ptr<ID3DBlob, Release<ID3DBlob>>;
using depth_stencil_state_t = util::safe_ptr<ID3D11DepthStencilState, Release<ID3D11DepthStencilState>>;
using depth_stencil_view_t = util::safe_ptr<ID3D11DepthStencilView, Release<ID3D11DepthStencilView>>;
template<class T>
buf_t make_buffer(device_t::pointer device, const T &t) {
@@ -98,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;
};
@@ -224,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);
@@ -312,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
@@ -327,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();
@@ -406,8 +373,8 @@ public:
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;
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);
@@ -429,7 +396,8 @@ public:
}
nv12_rt_desc.Format = DXGI_FORMAT_R8G8_UNORM;
status = device_p->CreateRenderTargetView(img.texture.get(), &nv12_rt_desc, &nv12_UV_rt);
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;
@@ -466,10 +434,6 @@ public:
this->device_ctx_p = device_ctx_p;
cursor_visible = false;
cursor_view.MinDepth = 0.0f;
cursor_view.MaxDepth = 1.0f;
format = (pix_fmt == pix_fmt_e::nv12 ? DXGI_FORMAT_NV12 : DXGI_FORMAT_P010);
status = device_p->CreateVertexShader(scene_vs_hlsl->GetBufferPointer(), scene_vs_hlsl->GetBufferSize(), nullptr, &scene_vs);
if(status) {
@@ -501,17 +465,6 @@ 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, display->width, display->height, DXGI_FORMAT_B8G8R8A8_UNORM)) {
return -1;
}
color_matrix = make_buffer(device_p, ::video::colors[0]);
if(!color_matrix) {
BOOST_LOG(error) << "Failed to create color matrix buffer"sv;
@@ -529,21 +482,6 @@ public:
img.display = std::move(display);
D3D11_SAMPLER_DESC sampler_desc {};
sampler_desc.Filter = D3D11_FILTER_MIN_MAG_MIP_LINEAR;
sampler_desc.AddressU = D3D11_TEXTURE_ADDRESS_CLAMP;
sampler_desc.AddressV = D3D11_TEXTURE_ADDRESS_CLAMP;
sampler_desc.AddressW = D3D11_TEXTURE_ADDRESS_WRAP;
sampler_desc.ComparisonFunc = D3D11_COMPARISON_NEVER;
sampler_desc.MinLOD = 0;
sampler_desc.MaxLOD = D3D11_FLOAT32_MAX;
status = device_p->CreateSamplerState(&sampler_desc, &sampler_linear);
if(FAILED(status)) {
BOOST_LOG(error) << "Failed to create point sampler state [0x"sv << util::hex(status).to_string_view() << ']';
return -1;
}
// Color the background black, so that the padding for keeping the aspect ratio
// is black
if(img.display->dummy_img(&back_img)) {
@@ -563,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;
}
@@ -577,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:
@@ -599,75 +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:
frame_t hwframe;
::video::color_t *color_p;
blend_t blend_enable;
blend_t blend_disable;
buf_t info_scene;
buf_t color_matrix;
sampler_state_t sampler_linear;
input_layout_t input_layout;
render_target_t nv12_Y_rt;
render_target_t nv12_UV_rt;
render_target_t scene_rt;
shader_res_t scene_sr;
// The image referenced by hwframe
// The resulting image is stored here.
img_d3d_t img;
// Clear nv12 render target to black
@@ -682,17 +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;
@@ -755,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;
@@ -798,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;
}
@@ -819,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 {
@@ -844,11 +827,8 @@ 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;
}
@@ -860,7 +840,7 @@ std::shared_ptr<platf::hwdevice_t> display_vram_t::make_hwdevice(pix_fmt_e pix_f
return nullptr;
}
auto hwdevice = std::make_shared<hwdevice_t>(&hwdevices);
auto hwdevice = std::make_shared<hwdevice_t>();
auto ret = hwdevice->init(
shared_from_this(),
@@ -872,12 +852,6 @@ std::shared_ptr<platf::hwdevice_t> display_vram_t::make_hwdevice(pix_fmt_e pix_f
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;
}

View File

@@ -4,25 +4,48 @@
#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;
volatile HDESK _lastKnownInputDesktop = NULL;
thread_local HDESK _lastKnownInputDesktop = nullptr;
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;
@@ -35,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() << ']';
}
@@ -83,10 +137,39 @@ public:
}
}
std::vector<target_t> x360s;
std::vector<std::pair<rumble_queue_t, target_t>> gamepads;
client_t client;
};
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) {
BOOST_LOG(debug)
<< "largeMotor: "sv << (int)largeMotor << std::endl
<< "smallMotor: "sv << (int)smallMotor;
task_pool.push(&vigem_t::rumble, (vigem_t *)userdata, target, smallMotor, largeMotor);
}
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) {
BOOST_LOG(debug)
<< "largeMotor: "sv << (int)largeMotor << std::endl
<< "smallMotor: "sv << (int)smallMotor;
task_pool.push(&vigem_t::rumble, (vigem_t *)userdata, target, smallMotor, largeMotor);
}
input_t input() {
input_t result { new vigem_t {} };
@@ -102,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 {};
@@ -201,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;
@@ -246,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) {
@@ -261,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) {
@@ -269,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() << ']';
@@ -281,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

@@ -87,4 +87,37 @@ std::string get_mac_address(const std::string_view &address) {
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

@@ -94,7 +94,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,14 +106,16 @@ 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) {
BOOST_LOG(info) << "Executing: ["sv << proc.cmd << ']';
_process = bp::child(_process_handle, proc.cmd, _env, 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::std_out > _pipe.get(), bp::std_err > _pipe.get(), ec);
}

View File

@@ -57,7 +57,7 @@ struct ctx_t {
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

View File

@@ -8,6 +8,7 @@
#include <atomic>
#include <condition_variable>
#include <functional>
#include <map>
#include <mutex>
#include <vector>
@@ -97,8 +98,6 @@ public:
}
bool peek() {
std::lock_guard lg { _lock };
return _continue && (bool)_status;
}
@@ -222,7 +221,7 @@ class queue_t {
public:
using status_t = util::optional_t<T>;
queue_t(std::uint32_t max_elements) : _max_elements { max_elements } {}
queue_t(std::uint32_t max_elements = 32) : _max_elements { max_elements } {}
template<class... Args>
void raise(Args &&...args) {
@@ -242,8 +241,6 @@ public:
}
bool peek() {
std::lock_guard lg { _lock };
return _continue && !_queue.empty();
}
@@ -426,6 +423,89 @@ auto make_shared(F_Construct &&fc, F_Destruct &&fd) {
}
using signal_t = event_t<bool>;
class mail_raw_t;
using mail_t = std::shared_ptr<mail_raw_t>;
void cleanup(mail_raw_t *);
template<class T>
class post_t : public T {
public:
template<class... Args>
post_t(mail_t mail, Args &&...args) : T(std::forward<Args>(args)...), mail { std::move(mail) } {}
mail_t mail;
~post_t() {
cleanup(mail.get());
}
};
template<class T>
inline auto lock(const std::weak_ptr<void> &wp) {
return std::reinterpret_pointer_cast<typename T::element_type>(wp.lock());
}
class mail_raw_t : public std::enable_shared_from_this<mail_raw_t> {
public:
template<class T>
using event_t = std::shared_ptr<post_t<event_t<T>>>;
template<class T>
using queue_t = std::shared_ptr<post_t<queue_t<T>>>;
template<class T>
event_t<T> event(const std::string_view &id) {
std::lock_guard lg { mutex };
auto it = id_to_post.find(id);
if(it != std::end(id_to_post)) {
return lock<event_t<T>>(it->second);
}
auto post = std::make_shared<typename event_t<T>::element_type>(shared_from_this());
id_to_post.emplace(std::pair<std::string, std::weak_ptr<void>> { std::string { id }, post });
return post;
}
template<class T>
queue_t<T> queue(const std::string_view &id) {
std::lock_guard lg { mutex };
auto it = id_to_post.find(id);
if(it != std::end(id_to_post)) {
return lock<queue_t<T>>(it->second);
}
auto post = std::make_shared<typename queue_t<T>::element_type>(shared_from_this(), 32);
id_to_post.emplace(std::pair<std::string, std::weak_ptr<void>> { std::string { id }, post });
return post;
}
void cleanup() {
std::lock_guard lg { mutex };
for(auto it = std::begin(id_to_post); it != std::end(id_to_post); ++it) {
auto &weak = it->second;
if(weak.expired()) {
id_to_post.erase(it);
return;
}
}
}
std::mutex mutex;
std::map<std::string, std::weak_ptr<void>, std::less<>> id_to_post;
};
inline void cleanup(mail_raw_t *mail) {
mail->cleanup();
}
} // namespace safe
#endif //SUNSHINE_THREAD_SAFE_H

184
sunshine/upnp.cpp Normal file
View File

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

10
sunshine/upnp.h Normal file
View File

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

View File

@@ -56,16 +56,21 @@ struct argument_type<T(U)> { typedef U type; };
x &operator=(x &&) noexcept = default; \
x();
#define KITTY_DEFAULT_CONSTR(x) \
#define KITTY_DEFAULT_CONSTR_MOVE(x) \
x(x &&) noexcept = default; \
x &operator=(x &&) noexcept = default; \
x() = default;
#define KITTY_DEFAULT_CONSTR_THROW(x) \
x(x &&) = default; \
x &operator=(x &&) = default; \
#define KITTY_DEFAULT_CONSTR_MOVE_THROW(x) \
x(x &&) = default; \
x &operator=(x &&) = default; \
x() = default;
#define KITTY_DEFAULT_CONSTR(x) \
KITTY_DEFAULT_CONSTR_MOVE(x) \
x(const x &) noexcept = default; \
x &operator=(const x &) = default;
#define TUPLE_2D(a, b, expr) \
decltype(expr) a##_##b = expr; \
auto &a = std::get<0>(a##_##b); \
@@ -263,11 +268,12 @@ std::string hex_vec(C &&c, bool rev = false) {
}
template<class T>
std::optional<T> from_hex(const std::string_view &hex, bool rev = false) {
T from_hex(const std::string_view &hex, bool rev = false) {
std::uint8_t buf[sizeof(T)];
static char constexpr shift_bit = 'a' - 'A';
auto is_convertable = [](char ch) -> bool {
auto is_convertable = [](char ch) -> bool {
if(isdigit(ch)) {
return true;
}
@@ -282,9 +288,7 @@ std::optional<T> from_hex(const std::string_view &hex, bool rev = false) {
};
auto buf_size = std::count_if(std::begin(hex), std::end(hex), is_convertable) / 2;
if(buf_size != sizeof(T)) {
return std::nullopt;
}
auto padding = sizeof(T) - buf_size;
const char *data = hex.data() + hex.size() - 1;
@@ -296,7 +300,9 @@ std::optional<T> from_hex(const std::string_view &hex, bool rev = false) {
return (std::uint8_t)(ch | (char)32) - 'a' + (char)10;
};
for(auto &el : buf) {
std::fill_n(buf + buf_size, padding, 0);
std::for_each_n(buf, buf_size, [&](auto &el) {
while(!is_convertable(*data)) { --data; }
std::uint8_t ch_r = convert(*data--);
@@ -304,7 +310,7 @@ std::optional<T> from_hex(const std::string_view &hex, bool rev = false) {
std::uint8_t ch_l = convert(*data--);
el = (ch_l << 4) | ch_r;
}
});
if(rev) {
std::reverse(std::begin(buf), std::end(buf));
@@ -383,6 +389,10 @@ auto enm(T &val) -> std::underlying_type_t<T> & {
}
inline std::int64_t from_chars(const char *begin, const char *end) {
if(begin == end) {
return 0;
}
std::int64_t res {};
std::int64_t mul = 1;
while(begin != --end) {
@@ -586,6 +596,14 @@ bool operator!=(std::nullptr_t, const uniq_ptr<T, D> &y) {
return (bool)y;
}
template<class P>
using shared_t = std::shared_ptr<typename P::element_type>;
template<class P, class T>
shared_t<P> make_shared(T *pointer) {
return shared_t<P>(reinterpret_cast<typename P::pointer>(pointer), typename P::deleter_type());
}
template<class T>
class wrap_ptr {
public:
@@ -881,7 +899,7 @@ struct endian_helper<T, std::enable_if_t<
static inline T big(T x) {
if(!x) return x;
if constexpr(endianness<T>::big) {
if constexpr(endianness<T>::little) {
auto *data = reinterpret_cast<uint8_t *>(&*x);
std::reverse(data, data + sizeof(*x));

File diff suppressed because it is too large Load Diff

View File

@@ -42,18 +42,21 @@ struct packet_raw_t : public AVPacket {
av_packet_unref(this);
}
struct {
struct replace_t {
std::string_view old;
std::string_view replacement;
} sps;
std::string_view _new;
KITTY_DEFAULT_CONSTR_MOVE(replace_t)
replace_t(std::string_view old, std::string_view _new) noexcept : old { std::move(old) }, _new { std::move(_new) } {}
};
std::vector<replace_t> *replacements;
void *channel_data;
};
using packet_t = std::unique_ptr<packet_raw_t>;
using packet_queue_t = std::shared_ptr<safe::queue_t<packet_t>>;
using idr_event_t = std::shared_ptr<safe::event_t<std::pair<int64_t, int64_t>>>;
using img_event_t = std::shared_ptr<safe::event_t<std::shared_ptr<platf::img_t>>>;
using packet_t = std::unique_ptr<packet_raw_t>;
struct config_t {
int width;
@@ -82,9 +85,7 @@ struct __attribute__((__aligned__(16))) color_t {
extern color_t colors[4];
void capture(
safe::signal_t *shutdown_event,
packet_queue_t packets,
idr_event_t idr_events,
safe::mail_t mail,
config_t config,
void *channel_data);

View File

@@ -18,7 +18,7 @@ include/cbs/h2645_parse.h
include/cbs/h264.h
include/cbs/hevc.h
include/cbs/sei.h
include/cbs/h264_levels.h
include/cbs/video_levels.h
cbs.c
cbs_h2645.c
@@ -28,7 +28,7 @@ cbs_mpeg2.c
cbs_jpeg.c
cbs_sei.c
h2645_parse.c
h264_levels.c
video_levels.c
bytestream.h
cbs_internal.h

View File

@@ -1,121 +0,0 @@
/*
* This file is part of FFmpeg.
*
* FFmpeg is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* FFmpeg is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with FFmpeg; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
#include <libavcodec/avcodec.h>
#include "include/cbs/h264_levels.h"
// H.264 table A-1.
static const H264LevelDescriptor h264_levels[] = {
// Name MaxMBPS MaxBR MinCR
// | level_idc | MaxFS | MaxCPB | MaxMvsPer2Mb
// | | cs3f | | MaxDpbMbs | | MaxVmvR | |
{ "1", 10, 0, 1485, 99, 396, 64, 175, 64, 2, 0 },
{ "1b", 11, 1, 1485, 99, 396, 128, 350, 64, 2, 0 },
{ "1b", 9, 0, 1485, 99, 396, 128, 350, 64, 2, 0 },
{ "1.1", 11, 0, 3000, 396, 900, 192, 500, 128, 2, 0 },
{ "1.2", 12, 0, 6000, 396, 2376, 384, 1000, 128, 2, 0 },
{ "1.3", 13, 0, 11880, 396, 2376, 768, 2000, 128, 2, 0 },
{ "2", 20, 0, 11880, 396, 2376, 2000, 2000, 128, 2, 0 },
{ "2.1", 21, 0, 19800, 792, 4752, 4000, 4000, 256, 2, 0 },
{ "2.2", 22, 0, 20250, 1620, 8100, 4000, 4000, 256, 2, 0 },
{ "3", 30, 0, 40500, 1620, 8100, 10000, 10000, 256, 2, 32 },
{ "3.1", 31, 0, 108000, 3600, 18000, 14000, 14000, 512, 4, 16 },
{ "3.2", 32, 0, 216000, 5120, 20480, 20000, 20000, 512, 4, 16 },
{ "4", 40, 0, 245760, 8192, 32768, 20000, 25000, 512, 4, 16 },
{ "4.1", 41, 0, 245760, 8192, 32768, 50000, 62500, 512, 2, 16 },
{ "4.2", 42, 0, 522240, 8704, 34816, 50000, 62500, 512, 2, 16 },
{ "5", 50, 0, 589824, 22080, 110400, 135000, 135000, 512, 2, 16 },
{ "5.1", 51, 0, 983040, 36864, 184320, 240000, 240000, 512, 2, 16 },
{ "5.2", 52, 0, 2073600, 36864, 184320, 240000, 240000, 512, 2, 16 },
{ "6", 60, 0, 4177920, 139264, 696320, 240000, 240000, 8192, 2, 16 },
{ "6.1", 61, 0, 8355840, 139264, 696320, 480000, 480000, 8192, 2, 16 },
{ "6.2", 62, 0, 16711680, 139264, 696320, 800000, 800000, 8192, 2, 16 },
};
// H.264 table A-2 plus values from A-1.
static const struct {
int profile_idc;
int cpb_br_vcl_factor;
int cpb_br_nal_factor;
} h264_br_factors[] = {
{ 66, 1000, 1200 },
{ 77, 1000, 1200 },
{ 88, 1000, 1200 },
{ 100, 1250, 1500 },
{ 110, 3000, 3600 },
{ 122, 4000, 4800 },
{ 244, 4000, 4800 },
{ 44, 4000, 4800 },
};
// We are only ever interested in the NAL bitrate factor.
static int h264_get_br_factor(int profile_idc) {
int i;
for(i = 0; i < FF_ARRAY_ELEMS(h264_br_factors); i++) {
if(h264_br_factors[i].profile_idc == profile_idc)
return h264_br_factors[i].cpb_br_nal_factor;
}
// Default to the non-high profile value if not specified.
return 1200;
}
const H264LevelDescriptor *ff_h264_guess_level(int profile_idc,
int64_t bitrate,
int framerate,
int width, int height,
int max_dec_frame_buffering) {
int width_mbs = (width + 15) / 16;
int height_mbs = (height + 15) / 16;
int no_cs3f = !(profile_idc == 66 ||
profile_idc == 77 ||
profile_idc == 88);
int i;
for(i = 0; i < FF_ARRAY_ELEMS(h264_levels); i++) {
const H264LevelDescriptor *level = &h264_levels[i];
if(level->constraint_set3_flag && no_cs3f)
continue;
if(bitrate > (int64_t)level->max_br * h264_get_br_factor(profile_idc))
continue;
if(width_mbs * height_mbs > level->max_fs)
continue;
if(width_mbs * width_mbs > 8 * level->max_fs)
continue;
if(height_mbs * height_mbs > 8 * level->max_fs)
continue;
if(width_mbs && height_mbs) {
int max_dpb_frames =
FFMIN(level->max_dpb_mbs / (width_mbs * height_mbs), 16);
if(max_dec_frame_buffering > max_dpb_frames)
continue;
if(framerate > (level->max_mbps / (width_mbs * height_mbs)))
continue;
}
return level;
}
// No usable levels found - frame is too big or bitrate is too high.
return NULL;
}

View File

@@ -22,6 +22,53 @@
#include <stdint.h>
#include "cbs_h265.h"
typedef struct H265LevelDescriptor {
const char *name;
uint8_t level_idc;
// Table A.6.
uint32_t max_luma_ps;
uint32_t max_cpb_main;
uint32_t max_cpb_high;
uint16_t max_slice_segments_per_picture;
uint8_t max_tile_rows;
uint8_t max_tile_cols;
// Table A.7.
uint32_t max_luma_sr;
uint32_t max_br_main;
uint32_t max_br_high;
uint8_t min_cr_base_main;
uint8_t min_cr_base_high;
} H265LevelDescriptor;
typedef struct H265ProfileDescriptor {
const char *name;
uint8_t profile_idc;
uint8_t high_throughput;
// Tables A.2, A.3 and A.5.
uint8_t max_14bit;
uint8_t max_12bit;
uint8_t max_10bit;
uint8_t max_8bit;
uint8_t max_422chroma;
uint8_t max_420chroma;
uint8_t max_monochrome;
uint8_t intra;
uint8_t one_picture_only;
uint8_t lower_bit_rate;
// Table A.8.
uint16_t cpb_vcl_factor;
uint16_t cpb_nal_factor;
float format_capability_factor;
float min_cr_scale_factor;
uint8_t max_dpb_pic_buf;
} H265ProfileDescriptor;
typedef struct H264LevelDescriptor {
const char *name;
uint8_t level_idc;
@@ -36,6 +83,20 @@ typedef struct H264LevelDescriptor {
uint8_t max_mvs_per_2mb;
} H264LevelDescriptor;
const H265ProfileDescriptor *ff_h265_get_profile(const H265RawProfileTierLevel *ptl);
/**
* Guess the level of a stream from some parameters.
*
* Unknown parameters may be zero, in which case they are ignored.
*/
const H265LevelDescriptor *ff_h265_guess_level(const H265RawProfileTierLevel *ptl,
int64_t bitrate,
int width, int height,
int slice_segments,
int tile_rows, int tile_cols,
int max_dec_pic_buffering);
/**
* Guess the level of a stream from some parameters.
*

349
third-party/cbs/video_levels.c vendored Normal file
View File

@@ -0,0 +1,349 @@
/*
* This file is part of FFmpeg.
*
* FFmpeg is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* FFmpeg is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with FFmpeg; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
#include <libavcodec/avcodec.h>
#include "include/cbs/video_levels.h"
// H.264 table A-1.
static const H264LevelDescriptor h264_levels[] = {
// Name MaxMBPS MaxBR MinCR
// | level_idc | MaxFS | MaxCPB | MaxMvsPer2Mb
// | | cs3f | | MaxDpbMbs | | MaxVmvR | |
{ "1", 10, 0, 1485, 99, 396, 64, 175, 64, 2, 0 },
{ "1b", 11, 1, 1485, 99, 396, 128, 350, 64, 2, 0 },
{ "1b", 9, 0, 1485, 99, 396, 128, 350, 64, 2, 0 },
{ "1.1", 11, 0, 3000, 396, 900, 192, 500, 128, 2, 0 },
{ "1.2", 12, 0, 6000, 396, 2376, 384, 1000, 128, 2, 0 },
{ "1.3", 13, 0, 11880, 396, 2376, 768, 2000, 128, 2, 0 },
{ "2", 20, 0, 11880, 396, 2376, 2000, 2000, 128, 2, 0 },
{ "2.1", 21, 0, 19800, 792, 4752, 4000, 4000, 256, 2, 0 },
{ "2.2", 22, 0, 20250, 1620, 8100, 4000, 4000, 256, 2, 0 },
{ "3", 30, 0, 40500, 1620, 8100, 10000, 10000, 256, 2, 32 },
{ "3.1", 31, 0, 108000, 3600, 18000, 14000, 14000, 512, 4, 16 },
{ "3.2", 32, 0, 216000, 5120, 20480, 20000, 20000, 512, 4, 16 },
{ "4", 40, 0, 245760, 8192, 32768, 20000, 25000, 512, 4, 16 },
{ "4.1", 41, 0, 245760, 8192, 32768, 50000, 62500, 512, 2, 16 },
{ "4.2", 42, 0, 522240, 8704, 34816, 50000, 62500, 512, 2, 16 },
{ "5", 50, 0, 589824, 22080, 110400, 135000, 135000, 512, 2, 16 },
{ "5.1", 51, 0, 983040, 36864, 184320, 240000, 240000, 512, 2, 16 },
{ "5.2", 52, 0, 2073600, 36864, 184320, 240000, 240000, 512, 2, 16 },
{ "6", 60, 0, 4177920, 139264, 696320, 240000, 240000, 8192, 2, 16 },
{ "6.1", 61, 0, 8355840, 139264, 696320, 480000, 480000, 8192, 2, 16 },
{ "6.2", 62, 0, 16711680, 139264, 696320, 800000, 800000, 8192, 2, 16 },
};
// H.264 table A-2 plus values from A-1.
static const struct {
int profile_idc;
int cpb_br_vcl_factor;
int cpb_br_nal_factor;
} h264_br_factors[] = {
{ 66, 1000, 1200 },
{ 77, 1000, 1200 },
{ 88, 1000, 1200 },
{ 100, 1250, 1500 },
{ 110, 3000, 3600 },
{ 122, 4000, 4800 },
{ 244, 4000, 4800 },
{ 44, 4000, 4800 },
};
// We are only ever interested in the NAL bitrate factor.
static int h264_get_br_factor(int profile_idc) {
int i;
for(i = 0; i < FF_ARRAY_ELEMS(h264_br_factors); i++) {
if(h264_br_factors[i].profile_idc == profile_idc)
return h264_br_factors[i].cpb_br_nal_factor;
}
// Default to the non-high profile value if not specified.
return 1200;
}
const H264LevelDescriptor *ff_h264_guess_level(int profile_idc,
int64_t bitrate,
int framerate,
int width, int height,
int max_dec_frame_buffering) {
int width_mbs = (width + 15) / 16;
int height_mbs = (height + 15) / 16;
int no_cs3f = !(profile_idc == 66 ||
profile_idc == 77 ||
profile_idc == 88);
int i;
for(i = 0; i < FF_ARRAY_ELEMS(h264_levels); i++) {
const H264LevelDescriptor *level = &h264_levels[i];
if(level->constraint_set3_flag && no_cs3f)
continue;
if(bitrate > (int64_t)level->max_br * h264_get_br_factor(profile_idc))
continue;
if(width_mbs * height_mbs > level->max_fs)
continue;
if(width_mbs * width_mbs > 8 * level->max_fs)
continue;
if(height_mbs * height_mbs > 8 * level->max_fs)
continue;
if(width_mbs && height_mbs) {
int max_dpb_frames =
FFMIN(level->max_dpb_mbs / (width_mbs * height_mbs), 16);
if(max_dec_frame_buffering > max_dpb_frames)
continue;
if(framerate > (level->max_mbps / (width_mbs * height_mbs)))
continue;
}
return level;
}
// No usable levels found - frame is too big or bitrate is too high.
return NULL;
}
static const H265LevelDescriptor h265_levels[] = {
// Name CpbFactor-Main MaxSliceSegmentsPerPicture
// | level_idc | CpbFactor-High MaxLumaSr BrFactor-High
// | | MaxLumaPs | | | MaxTileRows | BrFactor-Main | MinCr-Main
// | | | | | | | MaxTileCols | | | MinCr-High
{ "1", 30, 36864, 350, 0, 16, 1, 1, 552960, 128, 0, 2, 2 },
{ "2", 60, 122880, 1500, 0, 16, 1, 1, 3686400, 1500, 0, 2, 2 },
{ "2.1", 63, 245760, 3000, 0, 20, 1, 1, 7372800, 3000, 0, 2, 2 },
{ "3", 90, 552960, 6000, 0, 30, 2, 2, 16588800, 6000, 0, 2, 2 },
{ "3.1", 93, 983040, 10000, 0, 40, 3, 3, 33177600, 10000, 0, 2, 2 },
{ "4", 120, 2228224, 12000, 30000, 75, 5, 5, 66846720, 12000, 30000, 4, 4 },
{ "4.1", 123, 2228224, 20000, 50000, 75, 5, 5, 133693440, 20000, 50000, 4, 4 },
{ "5", 150, 8912896, 25000, 100000, 200, 11, 10, 267386880, 25000, 100000, 6, 4 },
{ "5.1", 153, 8912896, 40000, 160000, 200, 11, 10, 534773760, 40000, 160000, 8, 4 },
{ "5.2", 156, 8912896, 60000, 240000, 200, 11, 10, 1069547520, 60000, 240000, 8, 4 },
{ "6", 180, 35651584, 60000, 240000, 600, 22, 20, 1069547520, 60000, 240000, 8, 4 },
{ "6.1", 183, 35651584, 120000, 480000, 600, 22, 20, 2139095040, 120000, 480000, 8, 4 },
{ "6.2", 186, 35651584, 240000, 800000, 600, 22, 20, 4278190080, 240000, 800000, 6, 4 },
};
static const H265ProfileDescriptor h265_profiles[] = {
// profile_idc 8bit one-picture
// HT-profile | 422chroma | lower-bit-rate
// | 14bit | | 420chroma | | CpbVclFactor MinCrScaleFactor
// | | 12bit | | | monochrome| | CpbNalFactor | maxDpbPicBuf
// | | | 10bit | | | intra | | | FormatCapabilityFactor
{ "Monochrome", // | | | | | | | | | | |
4, 0, 2, 1, 1, 1, 1, 1, 1, 0, 0, 1, 667, 733, 1.000, 1.0, 6 },
{ "Monochrome 10",
4, 0, 2, 1, 1, 0, 1, 1, 1, 0, 0, 1, 833, 917, 1.250, 1.0, 6 },
{ "Monochrome 12",
4, 0, 2, 1, 0, 0, 1, 1, 1, 0, 0, 1, 1000, 1100, 1.500, 1.0, 6 },
{ "Monochrome 16",
4, 0, 2, 0, 0, 0, 1, 1, 1, 0, 0, 1, 1333, 1467, 2.000, 1.0, 6 },
{ "Main",
1, 0, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1000, 1100, 1.500, 1.0, 6 },
{ "Screen-Extended Main",
9, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 1000, 1100, 1.500, 1.0, 7 },
{ "Main 10",
2, 0, 2, 2, 2, 2, 2, 2, 2, 2, 0, 2, 1000, 1100, 1.875, 1.0, 6 },
{ "Screen-Extended Main 10",
9, 0, 1, 1, 1, 0, 1, 1, 0, 0, 0, 1, 1000, 1100, 1.875, 1.0, 7 },
{ "Main 12",
4, 0, 2, 1, 0, 0, 1, 1, 0, 0, 0, 1, 1500, 1650, 2.250, 1.0, 6 },
{ "Main Still Picture",
3, 0, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1000, 1100, 1.500, 1.0, 6 },
{ "Main 10 Still Picture",
2, 0, 2, 2, 2, 2, 2, 2, 2, 2, 1, 2, 1000, 1100, 1.875, 1.0, 6 },
{ "Main 4:2:2 10",
4, 0, 2, 1, 1, 0, 1, 0, 0, 0, 0, 1, 1667, 1833, 2.500, 0.5, 6 },
{ "Main 4:2:2 12",
4, 0, 2, 1, 0, 0, 1, 0, 0, 0, 0, 1, 2000, 2200, 3.000, 0.5, 6 },
{ "Main 4:4:4",
4, 0, 2, 1, 1, 1, 0, 0, 0, 0, 0, 1, 2000, 2200, 3.000, 0.5, 6 },
{ "High Throughput 4:4:4",
5, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 1, 2000, 2200, 3.000, 0.5, 6 },
{ "Screen-Extended Main 4:4:4",
9, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 1, 2000, 2200, 3.000, 0.5, 7 },
{ "Screen-Extended High Throughput 4:4:4",
9, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 1, 2000, 2200, 3.000, 0.5, 7 },
{ "Main 4:4:4 10",
4, 0, 2, 1, 1, 0, 0, 0, 0, 0, 0, 1, 2500, 2750, 3.750, 0.5, 6 },
{ "High Throughput 4:4:4 10",
5, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 1, 2500, 2750, 3.750, 0.5, 6 },
{ "Screen-Extended Main 4:4:4 10",
9, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 1, 2500, 2750, 3.750, 0.5, 7 },
{ "Screen-Extended High Throughput 4:4:4 10",
9, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 1, 2500, 2750, 3.750, 0.5, 7 },
{ "Main 4:4:4 12",
4, 0, 2, 1, 0, 0, 0, 0, 0, 0, 0, 1, 3000, 3300, 4.500, 0.5, 6 },
{ "High Throughput 4:4:4 14",
5, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 3500, 3850, 5.250, 0.5, 6 },
{ "Screen-Extended High Throughput 4:4:4 14",
9, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 3500, 3850, 5.250, 0.5, 7 },
{ "Main Intra",
4, 0, 2, 1, 1, 1, 1, 1, 0, 1, 0, 2, 1000, 1100, 1.500, 1.0, 6 },
{ "Main 10 Intra",
4, 0, 2, 1, 1, 0, 1, 1, 0, 1, 0, 2, 1000, 1100, 1.875, 1.0, 6 },
{ "Main 12 Intra",
4, 0, 2, 1, 0, 0, 1, 1, 0, 1, 0, 2, 1500, 1650, 2.250, 1.0, 6 },
{ "Main 4:2:2 10 Intra",
4, 0, 2, 1, 1, 0, 1, 0, 0, 1, 0, 2, 1667, 1833, 2.500, 0.5, 6 },
{ "Main 4:2:2 12 Intra",
4, 0, 2, 1, 0, 0, 1, 0, 0, 1, 0, 2, 2000, 2200, 3.000, 0.5, 6 },
{ "Main 4:4:4 Intra",
4, 0, 2, 1, 1, 1, 0, 0, 0, 1, 0, 2, 2000, 2200, 3.000, 0.5, 6 },
{ "Main 4:4:4 10 Intra",
4, 0, 2, 1, 1, 0, 0, 0, 0, 1, 0, 2, 2500, 2750, 3.750, 0.5, 6 },
{ "Main 4:4:4 12 Intra",
4, 0, 2, 1, 0, 0, 0, 0, 0, 1, 0, 2, 3000, 3300, 4.500, 0.5, 6 },
{ "Main 4:4:4 16 Intra",
4, 0, 2, 0, 0, 0, 0, 0, 0, 1, 0, 2, 4000, 4400, 6.000, 0.5, 6 },
{ "Main 4:4:4 Still Picture",
4, 0, 2, 1, 1, 1, 0, 0, 0, 1, 1, 2, 2000, 2200, 3.000, 0.5, 6 },
{ "Main 4:4:4 16 Still Picture",
4, 0, 2, 0, 0, 0, 0, 0, 0, 1, 1, 2, 4000, 4400, 6.000, 0.5, 6 },
{ "High Throughput 4:4:4 16 Intra",
5, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 2, 4000, 4400, 6.000, 0.5, 6 },
};
const H265ProfileDescriptor *ff_h265_get_profile(const H265RawProfileTierLevel *ptl) {
int i;
if(ptl->general_profile_space)
return NULL;
for(i = 0; i < FF_ARRAY_ELEMS(h265_profiles); i++) {
const H265ProfileDescriptor *profile = &h265_profiles[i];
if(ptl->general_profile_idc &&
ptl->general_profile_idc != profile->profile_idc)
continue;
if(!ptl->general_profile_compatibility_flag[profile->profile_idc])
continue;
#define check_flag(name) \
if(profile->name < 2) { \
if(profile->name != ptl->general_##name##_constraint_flag) \
continue; \
}
check_flag(max_14bit);
check_flag(max_12bit);
check_flag(max_10bit);
check_flag(max_8bit);
check_flag(max_422chroma);
check_flag(max_420chroma);
check_flag(max_monochrome);
check_flag(intra);
check_flag(one_picture_only);
check_flag(lower_bit_rate);
#undef check_flag
return profile;
}
return NULL;
}
const H265LevelDescriptor *ff_h265_guess_level(const H265RawProfileTierLevel *ptl,
int64_t bitrate,
int width, int height,
int slice_segments,
int tile_rows, int tile_cols,
int max_dec_pic_buffering) {
const H265ProfileDescriptor *profile;
int pic_size, tier_flag, lbr_flag, hbr_factor;
int i;
if(ptl)
profile = ff_h265_get_profile(ptl);
else
profile = NULL;
if(!profile) {
// Default to using multiplication factors for Main profile.
profile = &h265_profiles[4];
}
pic_size = width * height;
if(ptl) {
tier_flag = ptl->general_tier_flag;
lbr_flag = ptl->general_lower_bit_rate_constraint_flag;
}
else {
tier_flag = 0;
lbr_flag = profile->lower_bit_rate > 0;
}
if(profile->profile_idc == 1 || profile->profile_idc == 2) {
hbr_factor = 1;
}
else if(profile->high_throughput) {
if(profile->intra)
hbr_factor = 24 - 12 * lbr_flag;
else
hbr_factor = 6;
}
else {
hbr_factor = 2 - lbr_flag;
}
for(i = 0; i < FF_ARRAY_ELEMS(h265_levels); i++) {
const H265LevelDescriptor *level = &h265_levels[i];
int max_br, max_dpb_size;
if(tier_flag && !level->max_br_high)
continue;
if(pic_size > level->max_luma_ps)
continue;
if(width * width > 8 * level->max_luma_ps)
continue;
if(height * height > 8 * level->max_luma_ps)
continue;
if(slice_segments > level->max_slice_segments_per_picture)
continue;
if(tile_rows > level->max_tile_rows)
continue;
if(tile_cols > level->max_tile_cols)
continue;
if(tier_flag)
max_br = level->max_br_high;
else
max_br = level->max_br_main;
if(!max_br)
continue;
if(bitrate > (int64_t)profile->cpb_nal_factor * hbr_factor * max_br)
continue;
if(pic_size <= (level->max_luma_ps >> 2))
max_dpb_size = FFMIN(4 * profile->max_dpb_pic_buf, 16);
else if(pic_size <= (level->max_luma_ps >> 1))
max_dpb_size = FFMIN(2 * profile->max_dpb_pic_buf, 16);
else if(pic_size <= (3 * level->max_luma_ps >> 2))
max_dpb_size = FFMIN(4 * profile->max_dpb_pic_buf / 3, 16);
else
max_dpb_size = profile->max_dpb_pic_buf;
if(max_dec_pic_buffering > max_dpb_size)
continue;
return level;
}
return NULL;
}

1
third-party/miniupnp vendored Submodule

Submodule third-party/miniupnp added at 6f848ae082

View File

@@ -19,3 +19,11 @@ target_link_libraries(audio-info
ksuser
${PLATFORM_LIBRARIES})
target_compile_options(audio-info PRIVATE ${SUNSHINE_COMPILE_OPTIONS})
add_executable(sunshinesvc sunshinesvc.cpp)
set_target_properties(sunshinesvc PROPERTIES CXX_STANDARD 17)
target_link_libraries(sunshinesvc
${CMAKE_THREAD_LIBS_INIT}
wtsapi32
${PLATFORM_LIBRARIES})
target_compile_options(sunshinesvc PRIVATE ${SUNSHINE_COMPILE_OPTIONS})

24
tools/install-service.bat Normal file
View File

@@ -0,0 +1,24 @@
@echo off
set SERVICE_NAME=sunshinesvc
set SERVICE_BIN="%~dp0\tools\sunshinesvc.exe"
set SERVICE_START_TYPE=auto
rem Check if sunshinesvc already exists
sc qc %SERVICE_NAME% > nul 2>&1
if %ERRORLEVEL%==0 (
rem Stop the existing service if running
net stop %SERVICE_NAME%
rem Reconfigure the existing service
set SC_CMD=config
) else (
rem Create a new service
set SC_CMD=create
)
rem Run the sc command to create/reconfigure the service
sc %SC_CMD% %SERVICE_NAME% binPath= %SERVICE_BIN% start= %SERVICE_START_TYPE%
rem Start the new service
net start %SERVICE_NAME%

187
tools/sunshinesvc.cpp Normal file
View File

@@ -0,0 +1,187 @@
#define WIN32_LEAN_AND_MEAN
#include <Windows.h>
#include <wtsapi32.h>
SERVICE_STATUS_HANDLE service_status_handle;
SERVICE_STATUS service_status;
HANDLE stop_event;
#define SERVICE_NAME "SunshineSvc"
DWORD WINAPI HandlerEx(DWORD dwControl, DWORD dwEventType, LPVOID lpEventData, LPVOID lpContext) {
switch(dwControl) {
case SERVICE_CONTROL_INTERROGATE:
return NO_ERROR;
case SERVICE_CONTROL_STOP:
// Let SCM know we're stopping in up to 30 seconds
service_status.dwCurrentState = SERVICE_STOP_PENDING;
service_status.dwControlsAccepted = 0;
service_status.dwWaitHint = 30 * 1000;
SetServiceStatus(service_status_handle, &service_status);
// Trigger ServiceMain() to start cleanup
SetEvent(stop_event);
return NO_ERROR;
default:
return NO_ERROR;
}
}
HANDLE DuplicateTokenForConsoleSession() {
auto console_session_id = WTSGetActiveConsoleSessionId();
if(console_session_id == 0xFFFFFFFF) {
// No console session yet
return NULL;
}
HANDLE current_token;
if(!OpenProcessToken(GetCurrentProcess(), TOKEN_DUPLICATE, &current_token)) {
return NULL;
}
// Duplicate our own LocalSystem token
HANDLE new_token;
if(!DuplicateTokenEx(current_token, TOKEN_ALL_ACCESS, NULL, SecurityImpersonation, TokenPrimary, &new_token)) {
CloseHandle(current_token);
return NULL;
}
CloseHandle(current_token);
// Change the duplicated token to the console session ID
if(!SetTokenInformation(new_token, TokenSessionId, &console_session_id, sizeof(console_session_id))) {
CloseHandle(new_token);
return NULL;
}
return new_token;
}
HANDLE OpenLogFileHandle() {
WCHAR log_file_name[MAX_PATH];
// Create sunshine.log in the Temp folder (usually %SYSTEMROOT%\Temp)
GetTempPathW(_countof(log_file_name), log_file_name);
wcscat_s(log_file_name, L"sunshine.log");
// The file handle must be inheritable for our child process to use it
SECURITY_ATTRIBUTES security_attributes = { sizeof(security_attributes), NULL, TRUE };
// Overwrite the old sunshine.log
return CreateFileW(log_file_name,
GENERIC_WRITE,
FILE_SHARE_READ,
&security_attributes,
CREATE_ALWAYS,
0,
NULL);
}
VOID WINAPI ServiceMain(DWORD dwArgc, LPTSTR *lpszArgv) {
stop_event = CreateEventA(NULL, TRUE, FALSE, NULL);
if(stop_event == NULL) {
return;
}
service_status_handle = RegisterServiceCtrlHandlerEx(SERVICE_NAME, HandlerEx, NULL);
if(service_status_handle == NULL) {
return;
}
auto log_file_handle = OpenLogFileHandle();
if (log_file_handle == INVALID_HANDLE_VALUE) {
return;
}
// Tell SCM we're running
service_status.dwServiceType = SERVICE_WIN32_OWN_PROCESS;
service_status.dwServiceSpecificExitCode = 0;
service_status.dwWin32ExitCode = NO_ERROR;
service_status.dwWaitHint = 0;
service_status.dwControlsAccepted = SERVICE_ACCEPT_STOP;
service_status.dwCheckPoint = 0;
service_status.dwCurrentState = SERVICE_RUNNING;
SetServiceStatus(service_status_handle, &service_status);
// Loop every 3 seconds until the stop event is set or Sunshine.exe is running
while(WaitForSingleObject(stop_event, 3000) != WAIT_OBJECT_0) {
auto console_token = DuplicateTokenForConsoleSession();
if(console_token == NULL) {
continue;
}
STARTUPINFOW startup_info = {};
startup_info.cb = sizeof(startup_info);
startup_info.lpDesktop = (LPWSTR)L"winsta0\\default";
startup_info.dwFlags = STARTF_USESTDHANDLES;
startup_info.hStdInput = INVALID_HANDLE_VALUE;
startup_info.hStdOutput = log_file_handle;
startup_info.hStdError = log_file_handle;
PROCESS_INFORMATION process_info;
if(!CreateProcessAsUserW(console_token,
L"Sunshine.exe",
NULL,
NULL,
NULL,
TRUE,
ABOVE_NORMAL_PRIORITY_CLASS | CREATE_UNICODE_ENVIRONMENT | CREATE_NO_WINDOW,
NULL,
NULL,
&startup_info,
&process_info)) {
CloseHandle(console_token);
continue;
}
// Close handles that are no longer needed
CloseHandle(console_token);
CloseHandle(process_info.hThread);
// Wait for either the stop event to be set or Sunshine.exe to terminate
const HANDLE wait_objects[] = { stop_event, process_info.hProcess };
switch(WaitForMultipleObjects(_countof(wait_objects), wait_objects, FALSE, INFINITE)) {
case WAIT_OBJECT_0:
// The service is shutting down, so terminate Sunshine.exe.
// TODO: Send a graceful exit request and only terminate forcefully as a last resort.
TerminateProcess(process_info.hProcess, ERROR_PROCESS_ABORTED);
break;
case WAIT_OBJECT_0 + 1:
// Sunshine terminated itself.
break;
}
CloseHandle(process_info.hProcess);
}
// Let SCM know we've stopped
service_status.dwCurrentState = SERVICE_STOPPED;
SetServiceStatus(service_status_handle, &service_status);
}
int main(int argc, char* argv[])
{
static const SERVICE_TABLE_ENTRY service_table[] = {
{ (LPSTR)SERVICE_NAME, ServiceMain },
{ NULL, NULL }
};
// By default, services have their current directory set to %SYSTEMROOT%\System32.
// We want to use the directory where Sunshine.exe is located instead of system32.
// This requires stripping off 2 path components: the file name and the last folder
WCHAR module_path[MAX_PATH];
GetModuleFileNameW(NULL, module_path, _countof(module_path));
for(auto i = 0; i < 2; i++) {
auto last_sep = wcsrchr(module_path, '\\');
if(last_sep) {
*last_sep = 0;
}
}
SetCurrentDirectoryW(module_path);
// Trigger our ServiceMain()
return StartServiceCtrlDispatcher(service_table);
}

View File

@@ -0,0 +1,7 @@
@echo off
set SERVICE_NAME=sunshinesvc
net stop %SERVICE_NAME%
sc delete %SERVICE_NAME%