mirror of
https://github.com/LizardByte/Sunshine.git
synced 2025-08-10 00:52:16 +00:00
Compare commits
257 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6749b64ed5 | ||
|
|
0c43b83598 | ||
|
|
c47e3cb020 | ||
|
|
f3b07efaf3 | ||
|
|
02360f7aef | ||
|
|
c21301a423 | ||
|
|
64a6c1419b | ||
|
|
6f95d360b5 | ||
|
|
71cef4b700 | ||
|
|
56bb4ca865 | ||
|
|
fa0ff91d23 | ||
|
|
13caabaec8 | ||
|
|
8f2e00f31b | ||
|
|
78244a48d2 | ||
|
|
8be7c563a9 | ||
|
|
3fe134d8a6 | ||
|
|
f4c145136c | ||
|
|
ee10a795bd | ||
|
|
8e32c8e6f4 | ||
|
|
30f7742f51 | ||
|
|
55ce46e6ed | ||
|
|
9e03cdda42 | ||
|
|
76f34be395 | ||
|
|
ee89b211de | ||
|
|
0c5daa44a1 | ||
|
|
5495c8367a | ||
|
|
988bf33b40 | ||
|
|
e53f65c305 | ||
|
|
1afd1b7c94 | ||
|
|
33fbd5f431 | ||
|
|
17d3fcf0d0 | ||
|
|
cf77b301bc | ||
|
|
9d52174d6b | ||
|
|
e2874b40c1 | ||
|
|
914f329eea | ||
|
|
877c739f1b | ||
|
|
61b195e9f4 | ||
|
|
c53564dd31 | ||
|
|
9eb4eadda6 | ||
|
|
44ebc4846b | ||
|
|
3a3d4928f9 | ||
|
|
84c55d6efc | ||
|
|
7f636a25a8 | ||
|
|
825a706139 | ||
|
|
4df57a722e | ||
|
|
45f9ce3a20 | ||
|
|
f2863cceb7 | ||
|
|
6b9ed7fcb3 | ||
|
|
f33a587218 | ||
|
|
bdb9ed9001 | ||
|
|
41911a26fb | ||
|
|
0942f2ee7f | ||
|
|
a05a3b355c | ||
|
|
ff1722bbd1 | ||
|
|
dc281119af | ||
|
|
b848db8f2b | ||
|
|
bc261fddf2 | ||
|
|
bb251d5046 | ||
|
|
ec9481392a | ||
|
|
8961b0462e | ||
|
|
ffb80c5fc3 | ||
|
|
4835366a0c | ||
|
|
c09855f703 | ||
|
|
5761b05f3b | ||
|
|
ff1ea1a63e | ||
|
|
0ea6363172 | ||
|
|
fe5375f17b | ||
|
|
fd43ebc2bf | ||
|
|
113e7a52d4 | ||
|
|
9f6b4ed93b | ||
|
|
7aff15f743 | ||
|
|
1c4435312e | ||
|
|
dcb32eaaf7 | ||
|
|
ba07fd510e | ||
|
|
2fb5f8a7d0 | ||
|
|
b119121e10 | ||
|
|
cb101eca16 | ||
|
|
29674b8ed0 | ||
|
|
c6a0eeef3f | ||
|
|
825efb512a | ||
|
|
cd870bdcdd | ||
|
|
0868d898f6 | ||
|
|
66a78f0d40 | ||
|
|
2b04e1428c | ||
|
|
57f444357d | ||
|
|
fe08c241ec | ||
|
|
3d8a99f541 | ||
|
|
03236a50e5 | ||
|
|
a6c1649493 | ||
|
|
33a330fd6c | ||
|
|
3901e404a9 | ||
|
|
381e8bcfaa | ||
|
|
1050978246 | ||
|
|
9e48e58221 | ||
|
|
5d313b509e | ||
|
|
022b2202f6 | ||
|
|
2e9a1cfbba | ||
|
|
27a1144217 | ||
|
|
1d84c8f9ce | ||
|
|
04421d84a3 | ||
|
|
92cd8648fa | ||
|
|
41cc9a3e80 | ||
|
|
b97c902d10 | ||
|
|
25309f21ee | ||
|
|
1ba8da9780 | ||
|
|
66bc335d3f | ||
|
|
e834f375fb | ||
|
|
9c41972b65 | ||
|
|
41c30e9cfd | ||
|
|
fd8cbf0c7d | ||
|
|
4fe90dcbd6 | ||
|
|
377b086882 | ||
|
|
ade2ef3a15 | ||
|
|
6f428eb316 | ||
|
|
2970ad662c | ||
|
|
1dfe49e765 | ||
|
|
7a93a72710 | ||
|
|
661c8260e5 | ||
|
|
13c2da07e8 | ||
|
|
71b214ca43 | ||
|
|
513942c888 | ||
|
|
67df04e0a2 | ||
|
|
7b45f0d899 | ||
|
|
0f661e467e | ||
|
|
0232d8027c | ||
|
|
3a0377851d | ||
|
|
a93bad4cf3 | ||
|
|
88c3828ad3 | ||
|
|
c19853f03f | ||
|
|
1b7e103ef6 | ||
|
|
900d59b3ac | ||
|
|
37a9256587 | ||
|
|
127b5501d9 | ||
|
|
a081a9f5c4 | ||
|
|
fe8c2ceab9 | ||
|
|
1be5b4787f | ||
|
|
bb88d6f828 | ||
|
|
a7030641f3 | ||
|
|
208de3dae9 | ||
|
|
e81db118d5 | ||
|
|
488d8e5fc2 | ||
|
|
0049b36471 | ||
|
|
66b4b70023 | ||
|
|
438ae6a761 | ||
|
|
0cfb440cf6 | ||
|
|
a613280bbd | ||
|
|
6112c81db7 | ||
|
|
3f26a4fd21 | ||
|
|
bb5b003dd5 | ||
|
|
99777c8e82 | ||
|
|
4daaa1f089 | ||
|
|
0828cc3f83 | ||
|
|
790835f6c6 | ||
|
|
fe3784454a | ||
|
|
b336bf2fcb | ||
|
|
87be37293e | ||
|
|
7abcfc0390 | ||
|
|
020e2069cb | ||
|
|
83dd07469e | ||
|
|
0a883ab651 | ||
|
|
7b39b93bb2 | ||
|
|
30496c79ab | ||
|
|
415dec37ad | ||
|
|
fbbe396416 | ||
|
|
c6da7d31d0 | ||
|
|
1f9c6945ec | ||
|
|
4094aec2eb | ||
|
|
ff7a5aa1ea | ||
|
|
bfe59f6cf2 | ||
|
|
d8b4c66d7e | ||
|
|
6f92c42ad8 | ||
|
|
1b1d514d10 | ||
|
|
af1135d455 | ||
|
|
e10c9a1fa1 | ||
|
|
1a233ca4aa | ||
|
|
6878dcbf37 | ||
|
|
8d735e5611 | ||
|
|
1862662b3a | ||
|
|
1102ac9f3b | ||
|
|
12115a8a75 | ||
|
|
7ee59669da | ||
|
|
22418cb613 | ||
|
|
4bccec1c39 | ||
|
|
8b1a288ef3 | ||
|
|
fa489531b0 | ||
|
|
2e52402e27 | ||
|
|
519f7a8bf1 | ||
|
|
17e9b803db | ||
|
|
48b4dbd81c | ||
|
|
70bf11ec27 | ||
|
|
8ea9d34729 | ||
|
|
8c4519a2d0 | ||
|
|
ea266b979f | ||
|
|
2f978b3159 | ||
|
|
d81ba12aa8 | ||
|
|
dd13131fe6 | ||
|
|
5a4055f313 | ||
|
|
87f3ab0181 | ||
|
|
c7d6e959e0 | ||
|
|
0b1a69a067 | ||
|
|
525e8b3c6d | ||
|
|
ad7f93c3cb | ||
|
|
c7a72553c4 | ||
|
|
679f74e53c | ||
|
|
7edaa0cce0 | ||
|
|
c21038af88 | ||
|
|
65f44cc885 | ||
|
|
ceb784c648 | ||
|
|
8e3df43caf | ||
|
|
afbca0f6cd | ||
|
|
f4e99a1bd6 | ||
|
|
ed6b919a4d | ||
|
|
2d2fd77bc1 | ||
|
|
15f347c69c | ||
|
|
9fdaf2117f | ||
|
|
631be974ee | ||
|
|
bd3d1d6988 | ||
|
|
f2636b163e | ||
|
|
df5daa045a | ||
|
|
4de547228c | ||
|
|
3d595ce927 | ||
|
|
456d33cf77 | ||
|
|
3ceb9b37a0 | ||
|
|
94181fd047 | ||
|
|
55705af922 | ||
|
|
a9423574fe | ||
|
|
34b1ef8779 | ||
|
|
68a7cece49 | ||
|
|
ad87463784 | ||
|
|
ad34fef228 | ||
|
|
25de180d6e | ||
|
|
c0116e0cdd | ||
|
|
e571b29868 | ||
|
|
5bbca8f517 | ||
|
|
5502df5512 | ||
|
|
87779b0ec8 | ||
|
|
92f51622cc | ||
|
|
e0721aa104 | ||
|
|
b4f1ef1127 | ||
|
|
1362abc70d | ||
|
|
1d6c6046a2 | ||
|
|
41f4b71c99 | ||
|
|
38ec7c22fb | ||
|
|
02e842b066 | ||
|
|
2ae7d2e363 | ||
|
|
f170e736c5 | ||
|
|
da246d6417 | ||
|
|
bb95d6ab52 | ||
|
|
11d447d7eb | ||
|
|
f56e7fc50d | ||
|
|
5d606bf567 | ||
|
|
4b216d6676 | ||
|
|
834f7b9063 | ||
|
|
5cd0fd76bf | ||
|
|
753f57c71b | ||
|
|
c71d2739b1 | ||
|
|
b10c971374 |
67
.clang-format
Normal file
67
.clang-format
Normal file
@@ -0,0 +1,67 @@
|
||||
# Generated from CLion C/C++ Code Style settings
|
||||
BasedOnStyle: LLVM
|
||||
AccessModifierOffset: -2
|
||||
AlignAfterOpenBracket: DontAlign
|
||||
AlignConsecutiveAssignments: true
|
||||
AlignOperands: Align
|
||||
AllowAllArgumentsOnNextLine: false
|
||||
AllowAllConstructorInitializersOnNextLine: false
|
||||
AllowAllParametersOfDeclarationOnNextLine: false
|
||||
AllowShortBlocksOnASingleLine: Always
|
||||
AllowShortCaseLabelsOnASingleLine: false
|
||||
AllowShortFunctionsOnASingleLine: All
|
||||
AllowShortIfStatementsOnASingleLine: Always
|
||||
AllowShortLambdasOnASingleLine: All
|
||||
AllowShortLoopsOnASingleLine: true
|
||||
AlwaysBreakAfterReturnType: None
|
||||
AlwaysBreakTemplateDeclarations: Yes
|
||||
BreakBeforeBraces: Custom
|
||||
BraceWrapping:
|
||||
AfterCaseLabel: false
|
||||
AfterClass: false
|
||||
AfterControlStatement: Never
|
||||
AfterEnum: false
|
||||
AfterFunction: false
|
||||
AfterNamespace: false
|
||||
AfterUnion: false
|
||||
BeforeCatch: true
|
||||
BeforeElse: true
|
||||
IndentBraces: false
|
||||
SplitEmptyFunction: false
|
||||
SplitEmptyRecord: true
|
||||
BreakBeforeBinaryOperators: None
|
||||
BreakBeforeTernaryOperators: false
|
||||
BreakConstructorInitializers: BeforeColon
|
||||
BreakInheritanceList: BeforeColon
|
||||
ColumnLimit: 0
|
||||
CompactNamespaces: false
|
||||
ContinuationIndentWidth: 2
|
||||
IndentCaseLabels: false
|
||||
IndentPPDirectives: None
|
||||
IndentWidth: 2
|
||||
KeepEmptyLinesAtTheStartOfBlocks: true
|
||||
MaxEmptyLinesToKeep: 2
|
||||
NamespaceIndentation: None
|
||||
ObjCSpaceAfterProperty: false
|
||||
ObjCSpaceBeforeProtocolList: true
|
||||
PointerAlignment: Right
|
||||
ReflowComments: false
|
||||
SpaceAfterCStyleCast: false
|
||||
SpaceAfterLogicalNot: false
|
||||
SpaceAfterTemplateKeyword: false
|
||||
SpaceBeforeAssignmentOperators: true
|
||||
SpaceBeforeCpp11BracedList: true
|
||||
SpaceBeforeCtorInitializerColon: true
|
||||
SpaceBeforeInheritanceColon: true
|
||||
SpaceBeforeParens: Never
|
||||
SpaceBeforeRangeBasedForLoopColon: true
|
||||
SpaceInEmptyParentheses: false
|
||||
SpacesBeforeTrailingComments: 1
|
||||
SpacesInAngles: false
|
||||
SpacesInCStyleCastParentheses: false
|
||||
SpacesInContainerLiterals: false
|
||||
SpacesInParentheses: false
|
||||
SpacesInSquareBrackets: false
|
||||
TabWidth: 2
|
||||
Cpp11BracedListStyle: false
|
||||
UseTab: Never
|
||||
8
.gitignore
vendored
8
.gitignore
vendored
@@ -1,8 +1,10 @@
|
||||
build
|
||||
cmake-build-*
|
||||
cmake-build*
|
||||
.DS_Store
|
||||
|
||||
.vscode
|
||||
.vs
|
||||
*.swp
|
||||
*.kdev4
|
||||
|
||||
.idea
|
||||
.cache
|
||||
.idea
|
||||
9
.gitmodules
vendored
9
.gitmodules
vendored
@@ -1,12 +1,9 @@
|
||||
[submodule "moonlight-common-c"]
|
||||
path = moonlight-common-c
|
||||
path = third-party/moonlight-common-c
|
||||
url = https://github.com/moonlight-stream/moonlight-common-c.git
|
||||
[submodule "Simple-Web-Server"]
|
||||
path = Simple-Web-Server
|
||||
path = third-party/Simple-Web-Server
|
||||
url = https://github.com/loki-47-6F-64/Simple-Web-Server.git
|
||||
[submodule "ViGEmClient"]
|
||||
path = ViGEmClient
|
||||
path = third-party/ViGEmClient
|
||||
url = https://github.com/ViGEm/ViGEmClient
|
||||
[submodule "pre-compiled"]
|
||||
path = pre-compiled
|
||||
url = https://bitbucket.org/Loki-47-6F-64/pre-compiled.git
|
||||
|
||||
183
CMakeLists.txt
183
CMakeLists.txt
@@ -1,98 +1,126 @@
|
||||
cmake_minimum_required(VERSION 2.8)
|
||||
cmake_minimum_required(VERSION 3.0)
|
||||
|
||||
project(Sunshine)
|
||||
|
||||
set(CMAKE_MODULE_PATH ${CMAKE_CURRENT_SOURCE_DIR})
|
||||
|
||||
# On MSYS2, building a stand-alone binary that links with ffmpeg is not possible,
|
||||
# Therefore, ffmpeg, libx264 and libx265 must be build from source
|
||||
add_subdirectory(third-party/Simple-Web-Server)
|
||||
|
||||
if(WIN32)
|
||||
option(SUNSHINE_STANDALONE "Compile stand-alone binary of Sunshine" OFF)
|
||||
if(SUNSHINE_STANDALONE)
|
||||
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -static")
|
||||
|
||||
if(NOT DEFINED SUNSHINE_PREPARED_BINARIES)
|
||||
set(SUNSHINE_PREPARED_BINARIES "${CMAKE_CURRENT_SOURCE_DIR}/pre-compiled/windows")
|
||||
endif()
|
||||
list(PREPEND PLATFORM_LIBRARIES
|
||||
C:/msys64/mingw64/lib/gcc/x86_64-w64-mingw32/${CMAKE_CXX_COMPILER_VERSION}/libstdc++.a
|
||||
C:/msys64/mingw64/x86_64-w64-mingw32/lib/libwinpthread.a
|
||||
)
|
||||
|
||||
set(FFMPEG_INCLUDE_DIRS
|
||||
${SUNSHINE_PREPARED_BINARIES}/include)
|
||||
set(FFMPEG_LIBRARIES
|
||||
${SUNSHINE_PREPARED_BINARIES}/lib/libavcodec.a
|
||||
${SUNSHINE_PREPARED_BINARIES}/lib/libavdevice.a
|
||||
${SUNSHINE_PREPARED_BINARIES}/lib/libavfilter.a
|
||||
${SUNSHINE_PREPARED_BINARIES}/lib/libavformat.a
|
||||
${SUNSHINE_PREPARED_BINARIES}/lib/libavutil.a
|
||||
${SUNSHINE_PREPARED_BINARIES}/lib/libpostproc.a
|
||||
${SUNSHINE_PREPARED_BINARIES}/lib/libswresample.a
|
||||
${SUNSHINE_PREPARED_BINARIES}/lib/libswscale.a
|
||||
${SUNSHINE_PREPARED_BINARIES}/lib/libx264.a
|
||||
${SUNSHINE_PREPARED_BINARIES}/lib/libx265.a
|
||||
z lzma bcrypt C:/msys64/mingw64/lib/libiconv.a)
|
||||
endif()
|
||||
else()
|
||||
set(SUNSHINE_STANDALONE OFF)
|
||||
# Ugly hack to compile with #include <qos2.h>
|
||||
add_compile_definitions(
|
||||
QOS_FLOWID=UINT32
|
||||
PQOS_FLOWID=UINT32*
|
||||
QOS_NON_ADAPTIVE_FLOW=2)
|
||||
endif()
|
||||
|
||||
add_subdirectory(Simple-Web-Server)
|
||||
add_subdirectory(moonlight-common-c/enet)
|
||||
add_subdirectory(third-party/moonlight-common-c/enet)
|
||||
|
||||
find_package(Threads REQUIRED)
|
||||
find_package(OpenSSL REQUIRED)
|
||||
|
||||
if(NOT SUNSHINE_STANDALONE)
|
||||
find_package(FFmpeg REQUIRED)
|
||||
endif()
|
||||
|
||||
list(APPEND SUNSHINE_COMPILE_OPTIONS -fPIC -Wall -Wno-missing-braces -Wno-maybe-uninitialized -Wno-sign-compare)
|
||||
|
||||
if(WIN32)
|
||||
file(
|
||||
DOWNLOAD "https://github.com/TheElixZammuto/sunshine-prebuilt/releases/download/1.0.0/pre-compiled.zip" "${CMAKE_CURRENT_BINARY_DIR}/pre-compiled.zip"
|
||||
TIMEOUT 60
|
||||
EXPECTED_HASH SHA256=5d59986bd7f619eaaf82b2dd56b5127b747c9cbe8db61e3b898ff6b485298ed6)
|
||||
|
||||
file(ARCHIVE_EXTRACT
|
||||
INPUT "${CMAKE_CURRENT_BINARY_DIR}/pre-compiled.zip"
|
||||
DESTINATION ${CMAKE_CURRENT_BINARY_DIR}/pre-compiled)
|
||||
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -static")
|
||||
|
||||
if(NOT DEFINED SUNSHINE_PREPARED_BINARIES)
|
||||
set(SUNSHINE_PREPARED_BINARIES "${CMAKE_CURRENT_BINARY_DIR}/pre-compiled/windows")
|
||||
endif()
|
||||
|
||||
add_compile_definitions(SUNSHINE_PLATFORM="windows")
|
||||
add_subdirectory(tools) #This is temporary, only tools for Windows are needed, for now
|
||||
|
||||
list(APPEND SUNSHINE_DEFINITIONS APPS_JSON="apps_windows.json")
|
||||
include_directories(
|
||||
ViGEmClient/include)
|
||||
|
||||
include_directories(third-party/ViGEmClient/include)
|
||||
|
||||
set(PLATFORM_TARGET_FILES
|
||||
sunshine/platform/windows.cpp
|
||||
sunshine/platform/windows_dxgi.cpp
|
||||
sunshine/platform/windows_wasapi.cpp
|
||||
ViGEmClient/src/ViGEmClient.cpp
|
||||
ViGEmClient/include/ViGEm/Client.h
|
||||
ViGEmClient/include/ViGEm/Common.h
|
||||
ViGEmClient/include/ViGEm/Util.h
|
||||
ViGEmClient/include/ViGEm/km/BusShared.h)
|
||||
sunshine/platform/windows/misc.cpp
|
||||
sunshine/platform/windows/input.cpp
|
||||
sunshine/platform/windows/display.h
|
||||
sunshine/platform/windows/display_base.cpp
|
||||
sunshine/platform/windows/display_vram.cpp
|
||||
sunshine/platform/windows/display_ram.cpp
|
||||
sunshine/platform/windows/audio.cpp
|
||||
third-party/ViGEmClient/src/ViGEmClient.cpp
|
||||
third-party/ViGEmClient/include/ViGEm/Client.h
|
||||
third-party/ViGEmClient/include/ViGEm/Common.h
|
||||
third-party/ViGEmClient/include/ViGEm/Util.h
|
||||
third-party/ViGEmClient/include/ViGEm/km/BusShared.h)
|
||||
|
||||
set(OPENSSL_LIBRARIES
|
||||
libssl.a
|
||||
libcrypto.a)
|
||||
|
||||
set(FFMPEG_INCLUDE_DIRS
|
||||
${SUNSHINE_PREPARED_BINARIES}/include)
|
||||
set(FFMPEG_LIBRARIES
|
||||
${SUNSHINE_PREPARED_BINARIES}/lib/libavcodec.a
|
||||
${SUNSHINE_PREPARED_BINARIES}/lib/libavdevice.a
|
||||
${SUNSHINE_PREPARED_BINARIES}/lib/libavfilter.a
|
||||
${SUNSHINE_PREPARED_BINARIES}/lib/libavformat.a
|
||||
${SUNSHINE_PREPARED_BINARIES}/lib/libavutil.a
|
||||
${SUNSHINE_PREPARED_BINARIES}/lib/libpostproc.a
|
||||
${SUNSHINE_PREPARED_BINARIES}/lib/libswresample.a
|
||||
${SUNSHINE_PREPARED_BINARIES}/lib/libswscale.a
|
||||
${SUNSHINE_PREPARED_BINARIES}/lib/libx264.a
|
||||
${SUNSHINE_PREPARED_BINARIES}/lib/libx265.a
|
||||
${SUNSHINE_PREPARED_BINARIES}/lib/libhdr10plus.a
|
||||
z lzma bcrypt libiconv.a)
|
||||
|
||||
list(PREPEND PLATFORM_LIBRARIES
|
||||
libstdc++.a
|
||||
libwinpthread.a
|
||||
libssp.a
|
||||
Qwave
|
||||
winmm
|
||||
ksuser
|
||||
wsock32
|
||||
ws2_32
|
||||
iphlpapi
|
||||
windowsapp
|
||||
d3d11 dxgi
|
||||
d3d11 dxgi D3DCompiler
|
||||
setupapi
|
||||
)
|
||||
|
||||
set_source_files_properties(ViGEmClient/src/ViGEmClient.cpp PROPERTIES COMPILE_DEFINITIONS "UNICODE=1;ERROR_INVALID_DEVICE_OBJECT_PARAMETER=650")
|
||||
set_source_files_properties(ViGEmClient/src/ViGEmClient.cpp PROPERTIES COMPILE_FLAGS "-Wno-unknown-pragmas -Wno-misleading-indentation -Wno-class-memaccess")
|
||||
set_source_files_properties(third-party/ViGEmClient/src/ViGEmClient.cpp PROPERTIES COMPILE_DEFINITIONS "UNICODE=1;ERROR_INVALID_DEVICE_OBJECT_PARAMETER=650")
|
||||
set_source_files_properties(third-party/ViGEmClient/src/ViGEmClient.cpp PROPERTIES COMPILE_FLAGS "-Wno-unknown-pragmas -Wno-misleading-indentation -Wno-class-memaccess")
|
||||
else()
|
||||
add_compile_definitions(SUNSHINE_PLATFORM="linux")
|
||||
list(APPEND SUNSHINE_DEFINITIONS APPS_JSON="apps_linux.json")
|
||||
|
||||
find_package(X11 REQUIRED)
|
||||
find_package(FFmpeg REQUIRED)
|
||||
set(PLATFORM_TARGET_FILES
|
||||
sunshine/platform/linux.cpp
|
||||
sunshine/platform/linux_evdev.cpp)
|
||||
|
||||
sunshine/platform/linux/vaapi.h
|
||||
sunshine/platform/linux/vaapi.cpp
|
||||
sunshine/platform/linux/misc.cpp
|
||||
sunshine/platform/linux/display.cpp
|
||||
sunshine/platform/linux/audio.cpp
|
||||
sunshine/platform/linux/input.cpp
|
||||
third-party/glad/src/egl.c
|
||||
third-party/glad/src/gl.c
|
||||
third-party/glad/include/EGL/eglplatform.h
|
||||
third-party/glad/include/KHR/khrplatform.h
|
||||
third-party/glad/include/glad/gl.h
|
||||
third-party/glad/include/glad/egl.h)
|
||||
|
||||
set(PLATFORM_LIBRARIES
|
||||
Xfixes
|
||||
Xtst
|
||||
xcb
|
||||
xcb-shm
|
||||
xcb-xfixes
|
||||
Xrandr
|
||||
${X11_LIBRARIES}
|
||||
dl
|
||||
evdev
|
||||
pulse
|
||||
pulse-simple
|
||||
@@ -100,7 +128,8 @@ else()
|
||||
|
||||
set(PLATFORM_INCLUDE_DIRS
|
||||
${X11_INCLUDE_DIR}
|
||||
/usr/include/libevdev-1.0)
|
||||
/usr/include/libevdev-1.0
|
||||
third-party/glad/include)
|
||||
|
||||
if(NOT DEFINED SUNSHINE_EXECUTABLE_PATH)
|
||||
set(SUNSHINE_EXECUTABLE_PATH "${CMAKE_CURRENT_BINARY_DIR}/sunshine")
|
||||
@@ -109,16 +138,19 @@ else()
|
||||
configure_file(sunshine.service.in sunshine.service @ONLY)
|
||||
endif()
|
||||
|
||||
add_subdirectory(third-party/cbs)
|
||||
|
||||
set(Boost_USE_STATIC_LIBS ON)
|
||||
find_package(Boost COMPONENTS log filesystem REQUIRED)
|
||||
|
||||
set(SUNSHINE_TARGET_FILES
|
||||
moonlight-common-c/reedsolomon/rs.c
|
||||
moonlight-common-c/reedsolomon/rs.h
|
||||
moonlight-common-c/src/Input.h
|
||||
moonlight-common-c/src/Rtsp.h
|
||||
moonlight-common-c/src/RtspParser.c
|
||||
moonlight-common-c/src/Video.h
|
||||
third-party/moonlight-common-c/reedsolomon/rs.c
|
||||
third-party/moonlight-common-c/reedsolomon/rs.h
|
||||
third-party/moonlight-common-c/src/Input.h
|
||||
third-party/moonlight-common-c/src/Rtsp.h
|
||||
third-party/moonlight-common-c/src/RtspParser.c
|
||||
third-party/moonlight-common-c/src/Video.h
|
||||
sunshine/cbs.cpp
|
||||
sunshine/utility.h
|
||||
sunshine/uuid.h
|
||||
sunshine/config.h
|
||||
@@ -129,11 +161,16 @@ set(SUNSHINE_TARGET_FILES
|
||||
sunshine/crypto.h
|
||||
sunshine/nvhttp.cpp
|
||||
sunshine/nvhttp.h
|
||||
sunshine/httpcommon.cpp
|
||||
sunshine/httpcommon.h
|
||||
sunshine/confighttp.cpp
|
||||
sunshine/confighttp.h
|
||||
sunshine/rtsp.cpp
|
||||
sunshine/rtsp.h
|
||||
sunshine/stream.cpp
|
||||
sunshine/stream.h
|
||||
sunshine/video.cpp
|
||||
sunshine/video.h
|
||||
sunshine/thread_safe.h
|
||||
sunshine/input.cpp
|
||||
sunshine/input.h
|
||||
sunshine/audio.cpp
|
||||
@@ -146,13 +183,17 @@ set(SUNSHINE_TARGET_FILES
|
||||
sunshine/move_by_copy.h
|
||||
sunshine/task_pool.h
|
||||
sunshine/thread_pool.h
|
||||
sunshine/thread_safe.h
|
||||
sunshine/sync.h
|
||||
sunshine/round_robin.h
|
||||
${PLATFORM_TARGET_FILES})
|
||||
|
||||
include_directories(
|
||||
${CMAKE_CURRENT_SOURCE_DIR}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/Simple-Web-Server
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/moonlight-common-c/enet/include
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/moonlight-common-c/reedsolomon
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/third-party
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/third-party/cbs/include
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/third-party/moonlight-common-c/enet/include
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/third-party/moonlight-common-c/reedsolomon
|
||||
${FFMPEG_INCLUDE_DIRS}
|
||||
${PLATFORM_INCLUDE_DIRS}
|
||||
)
|
||||
@@ -172,13 +213,11 @@ if(NOT SUNSHINE_ASSETS_DIR)
|
||||
set(SUNSHINE_ASSETS_DIR "${CMAKE_CURRENT_SOURCE_DIR}/assets")
|
||||
endif()
|
||||
|
||||
if(SUNSHINE_STANDALONE)
|
||||
set(OPENSSL_LIBRARIES
|
||||
C:/msys64/mingw64/lib/libssl.a
|
||||
C:/msys64/mingw64/lib/libcrypto.a)
|
||||
endif()
|
||||
list(APPEND CBS_EXTERNAL_LIBRARIES
|
||||
cbs)
|
||||
|
||||
list(APPEND SUNSHINE_EXTERNAL_LIBRARIES
|
||||
${CBS_EXTERNAL_LIBRARIES}
|
||||
${CMAKE_THREAD_LIBS_INIT}
|
||||
stdc++fs
|
||||
enet
|
||||
|
||||
674
LICENSE
Normal file
674
LICENSE
Normal file
@@ -0,0 +1,674 @@
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
Version 3, 29 June 2007
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
Preamble
|
||||
|
||||
The GNU General Public License is a free, copyleft license for
|
||||
software and other kinds of works.
|
||||
|
||||
The licenses for most software and other practical works are designed
|
||||
to take away your freedom to share and change the works. By contrast,
|
||||
the GNU General Public License is intended to guarantee your freedom to
|
||||
share and change all versions of a program--to make sure it remains free
|
||||
software for all its users. We, the Free Software Foundation, use the
|
||||
GNU General Public License for most of our software; it applies also to
|
||||
any other work released this way by its authors. You can apply it to
|
||||
your programs, too.
|
||||
|
||||
When we speak of free software, we are referring to freedom, not
|
||||
price. Our General Public Licenses are designed to make sure that you
|
||||
have the freedom to distribute copies of free software (and charge for
|
||||
them if you wish), that you receive source code or can get it if you
|
||||
want it, that you can change the software or use pieces of it in new
|
||||
free programs, and that you know you can do these things.
|
||||
|
||||
To protect your rights, we need to prevent others from denying you
|
||||
these rights or asking you to surrender the rights. Therefore, you have
|
||||
certain responsibilities if you distribute copies of the software, or if
|
||||
you modify it: responsibilities to respect the freedom of others.
|
||||
|
||||
For example, if you distribute copies of such a program, whether
|
||||
gratis or for a fee, you must pass on to the recipients the same
|
||||
freedoms that you received. You must make sure that they, too, receive
|
||||
or can get the source code. And you must show them these terms so they
|
||||
know their rights.
|
||||
|
||||
Developers that use the GNU GPL protect your rights with two steps:
|
||||
(1) assert copyright on the software, and (2) offer you this License
|
||||
giving you legal permission to copy, distribute and/or modify it.
|
||||
|
||||
For the developers' and authors' protection, the GPL clearly explains
|
||||
that there is no warranty for this free software. For both users' and
|
||||
authors' sake, the GPL requires that modified versions be marked as
|
||||
changed, so that their problems will not be attributed erroneously to
|
||||
authors of previous versions.
|
||||
|
||||
Some devices are designed to deny users access to install or run
|
||||
modified versions of the software inside them, although the manufacturer
|
||||
can do so. This is fundamentally incompatible with the aim of
|
||||
protecting users' freedom to change the software. The systematic
|
||||
pattern of such abuse occurs in the area of products for individuals to
|
||||
use, which is precisely where it is most unacceptable. Therefore, we
|
||||
have designed this version of the GPL to prohibit the practice for those
|
||||
products. If such problems arise substantially in other domains, we
|
||||
stand ready to extend this provision to those domains in future versions
|
||||
of the GPL, as needed to protect the freedom of users.
|
||||
|
||||
Finally, every program is threatened constantly by software patents.
|
||||
States should not allow patents to restrict development and use of
|
||||
software on general-purpose computers, but in those that do, we wish to
|
||||
avoid the special danger that patents applied to a free program could
|
||||
make it effectively proprietary. To prevent this, the GPL assures that
|
||||
patents cannot be used to render the program non-free.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
|
||||
TERMS AND CONDITIONS
|
||||
|
||||
0. Definitions.
|
||||
|
||||
"This License" refers to version 3 of the GNU General Public License.
|
||||
|
||||
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||
works, such as semiconductor masks.
|
||||
|
||||
"The Program" refers to any copyrightable work licensed under this
|
||||
License. Each licensee is addressed as "you". "Licensees" and
|
||||
"recipients" may be individuals or organizations.
|
||||
|
||||
To "modify" a work means to copy from or adapt all or part of the work
|
||||
in a fashion requiring copyright permission, other than the making of an
|
||||
exact copy. The resulting work is called a "modified version" of the
|
||||
earlier work or a work "based on" the earlier work.
|
||||
|
||||
A "covered work" means either the unmodified Program or a work based
|
||||
on the Program.
|
||||
|
||||
To "propagate" a work means to do anything with it that, without
|
||||
permission, would make you directly or secondarily liable for
|
||||
infringement under applicable copyright law, except executing it on a
|
||||
computer or modifying a private copy. Propagation includes copying,
|
||||
distribution (with or without modification), making available to the
|
||||
public, and in some countries other activities as well.
|
||||
|
||||
To "convey" a work means any kind of propagation that enables other
|
||||
parties to make or receive copies. Mere interaction with a user through
|
||||
a computer network, with no transfer of a copy, is not conveying.
|
||||
|
||||
An interactive user interface displays "Appropriate Legal Notices"
|
||||
to the extent that it includes a convenient and prominently visible
|
||||
feature that (1) displays an appropriate copyright notice, and (2)
|
||||
tells the user that there is no warranty for the work (except to the
|
||||
extent that warranties are provided), that licensees may convey the
|
||||
work under this License, and how to view a copy of this License. If
|
||||
the interface presents a list of user commands or options, such as a
|
||||
menu, a prominent item in the list meets this criterion.
|
||||
|
||||
1. Source Code.
|
||||
|
||||
The "source code" for a work means the preferred form of the work
|
||||
for making modifications to it. "Object code" means any non-source
|
||||
form of a work.
|
||||
|
||||
A "Standard Interface" means an interface that either is an official
|
||||
standard defined by a recognized standards body, or, in the case of
|
||||
interfaces specified for a particular programming language, one that
|
||||
is widely used among developers working in that language.
|
||||
|
||||
The "System Libraries" of an executable work include anything, other
|
||||
than the work as a whole, that (a) is included in the normal form of
|
||||
packaging a Major Component, but which is not part of that Major
|
||||
Component, and (b) serves only to enable use of the work with that
|
||||
Major Component, or to implement a Standard Interface for which an
|
||||
implementation is available to the public in source code form. A
|
||||
"Major Component", in this context, means a major essential component
|
||||
(kernel, window system, and so on) of the specific operating system
|
||||
(if any) on which the executable work runs, or a compiler used to
|
||||
produce the work, or an object code interpreter used to run it.
|
||||
|
||||
The "Corresponding Source" for a work in object code form means all
|
||||
the source code needed to generate, install, and (for an executable
|
||||
work) run the object code and to modify the work, including scripts to
|
||||
control those activities. However, it does not include the work's
|
||||
System Libraries, or general-purpose tools or generally available free
|
||||
programs which are used unmodified in performing those activities but
|
||||
which are not part of the work. For example, Corresponding Source
|
||||
includes interface definition files associated with source files for
|
||||
the work, and the source code for shared libraries and dynamically
|
||||
linked subprograms that the work is specifically designed to require,
|
||||
such as by intimate data communication or control flow between those
|
||||
subprograms and other parts of the work.
|
||||
|
||||
The Corresponding Source need not include anything that users
|
||||
can regenerate automatically from other parts of the Corresponding
|
||||
Source.
|
||||
|
||||
The Corresponding Source for a work in source code form is that
|
||||
same work.
|
||||
|
||||
2. Basic Permissions.
|
||||
|
||||
All rights granted under this License are granted for the term of
|
||||
copyright on the Program, and are irrevocable provided the stated
|
||||
conditions are met. This License explicitly affirms your unlimited
|
||||
permission to run the unmodified Program. The output from running a
|
||||
covered work is covered by this License only if the output, given its
|
||||
content, constitutes a covered work. This License acknowledges your
|
||||
rights of fair use or other equivalent, as provided by copyright law.
|
||||
|
||||
You may make, run and propagate covered works that you do not
|
||||
convey, without conditions so long as your license otherwise remains
|
||||
in force. You may convey covered works to others for the sole purpose
|
||||
of having them make modifications exclusively for you, or provide you
|
||||
with facilities for running those works, provided that you comply with
|
||||
the terms of this License in conveying all material for which you do
|
||||
not control copyright. Those thus making or running the covered works
|
||||
for you must do so exclusively on your behalf, under your direction
|
||||
and control, on terms that prohibit them from making any copies of
|
||||
your copyrighted material outside their relationship with you.
|
||||
|
||||
Conveying under any other circumstances is permitted solely under
|
||||
the conditions stated below. Sublicensing is not allowed; section 10
|
||||
makes it unnecessary.
|
||||
|
||||
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
||||
|
||||
No covered work shall be deemed part of an effective technological
|
||||
measure under any applicable law fulfilling obligations under article
|
||||
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
||||
similar laws prohibiting or restricting circumvention of such
|
||||
measures.
|
||||
|
||||
When you convey a covered work, you waive any legal power to forbid
|
||||
circumvention of technological measures to the extent such circumvention
|
||||
is effected by exercising rights under this License with respect to
|
||||
the covered work, and you disclaim any intention to limit operation or
|
||||
modification of the work as a means of enforcing, against the work's
|
||||
users, your or third parties' legal rights to forbid circumvention of
|
||||
technological measures.
|
||||
|
||||
4. Conveying Verbatim Copies.
|
||||
|
||||
You may convey verbatim copies of the Program's source code as you
|
||||
receive it, in any medium, provided that you conspicuously and
|
||||
appropriately publish on each copy an appropriate copyright notice;
|
||||
keep intact all notices stating that this License and any
|
||||
non-permissive terms added in accord with section 7 apply to the code;
|
||||
keep intact all notices of the absence of any warranty; and give all
|
||||
recipients a copy of this License along with the Program.
|
||||
|
||||
You may charge any price or no price for each copy that you convey,
|
||||
and you may offer support or warranty protection for a fee.
|
||||
|
||||
5. Conveying Modified Source Versions.
|
||||
|
||||
You may convey a work based on the Program, or the modifications to
|
||||
produce it from the Program, in the form of source code under the
|
||||
terms of section 4, provided that you also meet all of these conditions:
|
||||
|
||||
a) The work must carry prominent notices stating that you modified
|
||||
it, and giving a relevant date.
|
||||
|
||||
b) The work must carry prominent notices stating that it is
|
||||
released under this License and any conditions added under section
|
||||
7. This requirement modifies the requirement in section 4 to
|
||||
"keep intact all notices".
|
||||
|
||||
c) You must license the entire work, as a whole, under this
|
||||
License to anyone who comes into possession of a copy. This
|
||||
License will therefore apply, along with any applicable section 7
|
||||
additional terms, to the whole of the work, and all its parts,
|
||||
regardless of how they are packaged. This License gives no
|
||||
permission to license the work in any other way, but it does not
|
||||
invalidate such permission if you have separately received it.
|
||||
|
||||
d) If the work has interactive user interfaces, each must display
|
||||
Appropriate Legal Notices; however, if the Program has interactive
|
||||
interfaces that do not display Appropriate Legal Notices, your
|
||||
work need not make them do so.
|
||||
|
||||
A compilation of a covered work with other separate and independent
|
||||
works, which are not by their nature extensions of the covered work,
|
||||
and which are not combined with it such as to form a larger program,
|
||||
in or on a volume of a storage or distribution medium, is called an
|
||||
"aggregate" if the compilation and its resulting copyright are not
|
||||
used to limit the access or legal rights of the compilation's users
|
||||
beyond what the individual works permit. Inclusion of a covered work
|
||||
in an aggregate does not cause this License to apply to the other
|
||||
parts of the aggregate.
|
||||
|
||||
6. Conveying Non-Source Forms.
|
||||
|
||||
You may convey a covered work in object code form under the terms
|
||||
of sections 4 and 5, provided that you also convey the
|
||||
machine-readable Corresponding Source under the terms of this License,
|
||||
in one of these ways:
|
||||
|
||||
a) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by the
|
||||
Corresponding Source fixed on a durable physical medium
|
||||
customarily used for software interchange.
|
||||
|
||||
b) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by a
|
||||
written offer, valid for at least three years and valid for as
|
||||
long as you offer spare parts or customer support for that product
|
||||
model, to give anyone who possesses the object code either (1) a
|
||||
copy of the Corresponding Source for all the software in the
|
||||
product that is covered by this License, on a durable physical
|
||||
medium customarily used for software interchange, for a price no
|
||||
more than your reasonable cost of physically performing this
|
||||
conveying of source, or (2) access to copy the
|
||||
Corresponding Source from a network server at no charge.
|
||||
|
||||
c) Convey individual copies of the object code with a copy of the
|
||||
written offer to provide the Corresponding Source. This
|
||||
alternative is allowed only occasionally and noncommercially, and
|
||||
only if you received the object code with such an offer, in accord
|
||||
with subsection 6b.
|
||||
|
||||
d) Convey the object code by offering access from a designated
|
||||
place (gratis or for a charge), and offer equivalent access to the
|
||||
Corresponding Source in the same way through the same place at no
|
||||
further charge. You need not require recipients to copy the
|
||||
Corresponding Source along with the object code. If the place to
|
||||
copy the object code is a network server, the Corresponding Source
|
||||
may be on a different server (operated by you or a third party)
|
||||
that supports equivalent copying facilities, provided you maintain
|
||||
clear directions next to the object code saying where to find the
|
||||
Corresponding Source. Regardless of what server hosts the
|
||||
Corresponding Source, you remain obligated to ensure that it is
|
||||
available for as long as needed to satisfy these requirements.
|
||||
|
||||
e) Convey the object code using peer-to-peer transmission, provided
|
||||
you inform other peers where the object code and Corresponding
|
||||
Source of the work are being offered to the general public at no
|
||||
charge under subsection 6d.
|
||||
|
||||
A separable portion of the object code, whose source code is excluded
|
||||
from the Corresponding Source as a System Library, need not be
|
||||
included in conveying the object code work.
|
||||
|
||||
A "User Product" is either (1) a "consumer product", which means any
|
||||
tangible personal property which is normally used for personal, family,
|
||||
or household purposes, or (2) anything designed or sold for incorporation
|
||||
into a dwelling. In determining whether a product is a consumer product,
|
||||
doubtful cases shall be resolved in favor of coverage. For a particular
|
||||
product received by a particular user, "normally used" refers to a
|
||||
typical or common use of that class of product, regardless of the status
|
||||
of the particular user or of the way in which the particular user
|
||||
actually uses, or expects or is expected to use, the product. A product
|
||||
is a consumer product regardless of whether the product has substantial
|
||||
commercial, industrial or non-consumer uses, unless such uses represent
|
||||
the only significant mode of use of the product.
|
||||
|
||||
"Installation Information" for a User Product means any methods,
|
||||
procedures, authorization keys, or other information required to install
|
||||
and execute modified versions of a covered work in that User Product from
|
||||
a modified version of its Corresponding Source. The information must
|
||||
suffice to ensure that the continued functioning of the modified object
|
||||
code is in no case prevented or interfered with solely because
|
||||
modification has been made.
|
||||
|
||||
If you convey an object code work under this section in, or with, or
|
||||
specifically for use in, a User Product, and the conveying occurs as
|
||||
part of a transaction in which the right of possession and use of the
|
||||
User Product is transferred to the recipient in perpetuity or for a
|
||||
fixed term (regardless of how the transaction is characterized), the
|
||||
Corresponding Source conveyed under this section must be accompanied
|
||||
by the Installation Information. But this requirement does not apply
|
||||
if neither you nor any third party retains the ability to install
|
||||
modified object code on the User Product (for example, the work has
|
||||
been installed in ROM).
|
||||
|
||||
The requirement to provide Installation Information does not include a
|
||||
requirement to continue to provide support service, warranty, or updates
|
||||
for a work that has been modified or installed by the recipient, or for
|
||||
the User Product in which it has been modified or installed. Access to a
|
||||
network may be denied when the modification itself materially and
|
||||
adversely affects the operation of the network or violates the rules and
|
||||
protocols for communication across the network.
|
||||
|
||||
Corresponding Source conveyed, and Installation Information provided,
|
||||
in accord with this section must be in a format that is publicly
|
||||
documented (and with an implementation available to the public in
|
||||
source code form), and must require no special password or key for
|
||||
unpacking, reading or copying.
|
||||
|
||||
7. Additional Terms.
|
||||
|
||||
"Additional permissions" are terms that supplement the terms of this
|
||||
License by making exceptions from one or more of its conditions.
|
||||
Additional permissions that are applicable to the entire Program shall
|
||||
be treated as though they were included in this License, to the extent
|
||||
that they are valid under applicable law. If additional permissions
|
||||
apply only to part of the Program, that part may be used separately
|
||||
under those permissions, but the entire Program remains governed by
|
||||
this License without regard to the additional permissions.
|
||||
|
||||
When you convey a copy of a covered work, you may at your option
|
||||
remove any additional permissions from that copy, or from any part of
|
||||
it. (Additional permissions may be written to require their own
|
||||
removal in certain cases when you modify the work.) You may place
|
||||
additional permissions on material, added by you to a covered work,
|
||||
for which you have or can give appropriate copyright permission.
|
||||
|
||||
Notwithstanding any other provision of this License, for material you
|
||||
add to a covered work, you may (if authorized by the copyright holders of
|
||||
that material) supplement the terms of this License with terms:
|
||||
|
||||
a) Disclaiming warranty or limiting liability differently from the
|
||||
terms of sections 15 and 16 of this License; or
|
||||
|
||||
b) Requiring preservation of specified reasonable legal notices or
|
||||
author attributions in that material or in the Appropriate Legal
|
||||
Notices displayed by works containing it; or
|
||||
|
||||
c) Prohibiting misrepresentation of the origin of that material, or
|
||||
requiring that modified versions of such material be marked in
|
||||
reasonable ways as different from the original version; or
|
||||
|
||||
d) Limiting the use for publicity purposes of names of licensors or
|
||||
authors of the material; or
|
||||
|
||||
e) Declining to grant rights under trademark law for use of some
|
||||
trade names, trademarks, or service marks; or
|
||||
|
||||
f) Requiring indemnification of licensors and authors of that
|
||||
material by anyone who conveys the material (or modified versions of
|
||||
it) with contractual assumptions of liability to the recipient, for
|
||||
any liability that these contractual assumptions directly impose on
|
||||
those licensors and authors.
|
||||
|
||||
All other non-permissive additional terms are considered "further
|
||||
restrictions" within the meaning of section 10. If the Program as you
|
||||
received it, or any part of it, contains a notice stating that it is
|
||||
governed by this License along with a term that is a further
|
||||
restriction, you may remove that term. If a license document contains
|
||||
a further restriction but permits relicensing or conveying under this
|
||||
License, you may add to a covered work material governed by the terms
|
||||
of that license document, provided that the further restriction does
|
||||
not survive such relicensing or conveying.
|
||||
|
||||
If you add terms to a covered work in accord with this section, you
|
||||
must place, in the relevant source files, a statement of the
|
||||
additional terms that apply to those files, or a notice indicating
|
||||
where to find the applicable terms.
|
||||
|
||||
Additional terms, permissive or non-permissive, may be stated in the
|
||||
form of a separately written license, or stated as exceptions;
|
||||
the above requirements apply either way.
|
||||
|
||||
8. Termination.
|
||||
|
||||
You may not propagate or modify a covered work except as expressly
|
||||
provided under this License. Any attempt otherwise to propagate or
|
||||
modify it is void, and will automatically terminate your rights under
|
||||
this License (including any patent licenses granted under the third
|
||||
paragraph of section 11).
|
||||
|
||||
However, if you cease all violation of this License, then your
|
||||
license from a particular copyright holder is reinstated (a)
|
||||
provisionally, unless and until the copyright holder explicitly and
|
||||
finally terminates your license, and (b) permanently, if the copyright
|
||||
holder fails to notify you of the violation by some reasonable means
|
||||
prior to 60 days after the cessation.
|
||||
|
||||
Moreover, your license from a particular copyright holder is
|
||||
reinstated permanently if the copyright holder notifies you of the
|
||||
violation by some reasonable means, this is the first time you have
|
||||
received notice of violation of this License (for any work) from that
|
||||
copyright holder, and you cure the violation prior to 30 days after
|
||||
your receipt of the notice.
|
||||
|
||||
Termination of your rights under this section does not terminate the
|
||||
licenses of parties who have received copies or rights from you under
|
||||
this License. If your rights have been terminated and not permanently
|
||||
reinstated, you do not qualify to receive new licenses for the same
|
||||
material under section 10.
|
||||
|
||||
9. Acceptance Not Required for Having Copies.
|
||||
|
||||
You are not required to accept this License in order to receive or
|
||||
run a copy of the Program. Ancillary propagation of a covered work
|
||||
occurring solely as a consequence of using peer-to-peer transmission
|
||||
to receive a copy likewise does not require acceptance. However,
|
||||
nothing other than this License grants you permission to propagate or
|
||||
modify any covered work. These actions infringe copyright if you do
|
||||
not accept this License. Therefore, by modifying or propagating a
|
||||
covered work, you indicate your acceptance of this License to do so.
|
||||
|
||||
10. Automatic Licensing of Downstream Recipients.
|
||||
|
||||
Each time you convey a covered work, the recipient automatically
|
||||
receives a license from the original licensors, to run, modify and
|
||||
propagate that work, subject to this License. You are not responsible
|
||||
for enforcing compliance by third parties with this License.
|
||||
|
||||
An "entity transaction" is a transaction transferring control of an
|
||||
organization, or substantially all assets of one, or subdividing an
|
||||
organization, or merging organizations. If propagation of a covered
|
||||
work results from an entity transaction, each party to that
|
||||
transaction who receives a copy of the work also receives whatever
|
||||
licenses to the work the party's predecessor in interest had or could
|
||||
give under the previous paragraph, plus a right to possession of the
|
||||
Corresponding Source of the work from the predecessor in interest, if
|
||||
the predecessor has it or can get it with reasonable efforts.
|
||||
|
||||
You may not impose any further restrictions on the exercise of the
|
||||
rights granted or affirmed under this License. For example, you may
|
||||
not impose a license fee, royalty, or other charge for exercise of
|
||||
rights granted under this License, and you may not initiate litigation
|
||||
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
||||
any patent claim is infringed by making, using, selling, offering for
|
||||
sale, or importing the Program or any portion of it.
|
||||
|
||||
11. Patents.
|
||||
|
||||
A "contributor" is a copyright holder who authorizes use under this
|
||||
License of the Program or a work on which the Program is based. The
|
||||
work thus licensed is called the contributor's "contributor version".
|
||||
|
||||
A contributor's "essential patent claims" are all patent claims
|
||||
owned or controlled by the contributor, whether already acquired or
|
||||
hereafter acquired, that would be infringed by some manner, permitted
|
||||
by this License, of making, using, or selling its contributor version,
|
||||
but do not include claims that would be infringed only as a
|
||||
consequence of further modification of the contributor version. For
|
||||
purposes of this definition, "control" includes the right to grant
|
||||
patent sublicenses in a manner consistent with the requirements of
|
||||
this License.
|
||||
|
||||
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
||||
patent license under the contributor's essential patent claims, to
|
||||
make, use, sell, offer for sale, import and otherwise run, modify and
|
||||
propagate the contents of its contributor version.
|
||||
|
||||
In the following three paragraphs, a "patent license" is any express
|
||||
agreement or commitment, however denominated, not to enforce a patent
|
||||
(such as an express permission to practice a patent or covenant not to
|
||||
sue for patent infringement). To "grant" such a patent license to a
|
||||
party means to make such an agreement or commitment not to enforce a
|
||||
patent against the party.
|
||||
|
||||
If you convey a covered work, knowingly relying on a patent license,
|
||||
and the Corresponding Source of the work is not available for anyone
|
||||
to copy, free of charge and under the terms of this License, through a
|
||||
publicly available network server or other readily accessible means,
|
||||
then you must either (1) cause the Corresponding Source to be so
|
||||
available, or (2) arrange to deprive yourself of the benefit of the
|
||||
patent license for this particular work, or (3) arrange, in a manner
|
||||
consistent with the requirements of this License, to extend the patent
|
||||
license to downstream recipients. "Knowingly relying" means you have
|
||||
actual knowledge that, but for the patent license, your conveying the
|
||||
covered work in a country, or your recipient's use of the covered work
|
||||
in a country, would infringe one or more identifiable patents in that
|
||||
country that you have reason to believe are valid.
|
||||
|
||||
If, pursuant to or in connection with a single transaction or
|
||||
arrangement, you convey, or propagate by procuring conveyance of, a
|
||||
covered work, and grant a patent license to some of the parties
|
||||
receiving the covered work authorizing them to use, propagate, modify
|
||||
or convey a specific copy of the covered work, then the patent license
|
||||
you grant is automatically extended to all recipients of the covered
|
||||
work and works based on it.
|
||||
|
||||
A patent license is "discriminatory" if it does not include within
|
||||
the scope of its coverage, prohibits the exercise of, or is
|
||||
conditioned on the non-exercise of one or more of the rights that are
|
||||
specifically granted under this License. You may not convey a covered
|
||||
work if you are a party to an arrangement with a third party that is
|
||||
in the business of distributing software, under which you make payment
|
||||
to the third party based on the extent of your activity of conveying
|
||||
the work, and under which the third party grants, to any of the
|
||||
parties who would receive the covered work from you, a discriminatory
|
||||
patent license (a) in connection with copies of the covered work
|
||||
conveyed by you (or copies made from those copies), or (b) primarily
|
||||
for and in connection with specific products or compilations that
|
||||
contain the covered work, unless you entered into that arrangement,
|
||||
or that patent license was granted, prior to 28 March 2007.
|
||||
|
||||
Nothing in this License shall be construed as excluding or limiting
|
||||
any implied license or other defenses to infringement that may
|
||||
otherwise be available to you under applicable patent law.
|
||||
|
||||
12. No Surrender of Others' Freedom.
|
||||
|
||||
If conditions are imposed on you (whether by court order, agreement or
|
||||
otherwise) that contradict the conditions of this License, they do not
|
||||
excuse you from the conditions of this License. If you cannot convey a
|
||||
covered work so as to satisfy simultaneously your obligations under this
|
||||
License and any other pertinent obligations, then as a consequence you may
|
||||
not convey it at all. For example, if you agree to terms that obligate you
|
||||
to collect a royalty for further conveying from those to whom you convey
|
||||
the Program, the only way you could satisfy both those terms and this
|
||||
License would be to refrain entirely from conveying the Program.
|
||||
|
||||
13. Use with the GNU Affero General Public License.
|
||||
|
||||
Notwithstanding any other provision of this License, you have
|
||||
permission to link or combine any covered work with a work licensed
|
||||
under version 3 of the GNU Affero General Public License into a single
|
||||
combined work, and to convey the resulting work. The terms of this
|
||||
License will continue to apply to the part which is the covered work,
|
||||
but the special requirements of the GNU Affero General Public License,
|
||||
section 13, concerning interaction through a network will apply to the
|
||||
combination as such.
|
||||
|
||||
14. Revised Versions of this License.
|
||||
|
||||
The Free Software Foundation may publish revised and/or new versions of
|
||||
the GNU General Public License from time to time. Such new versions will
|
||||
be similar in spirit to the present version, but may differ in detail to
|
||||
address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the
|
||||
Program specifies that a certain numbered version of the GNU General
|
||||
Public License "or any later version" applies to it, you have the
|
||||
option of following the terms and conditions either of that numbered
|
||||
version or of any later version published by the Free Software
|
||||
Foundation. If the Program does not specify a version number of the
|
||||
GNU General Public License, you may choose any version ever published
|
||||
by the Free Software Foundation.
|
||||
|
||||
If the Program specifies that a proxy can decide which future
|
||||
versions of the GNU General Public License can be used, that proxy's
|
||||
public statement of acceptance of a version permanently authorizes you
|
||||
to choose that version for the Program.
|
||||
|
||||
Later license versions may give you additional or different
|
||||
permissions. However, no additional obligations are imposed on any
|
||||
author or copyright holder as a result of your choosing to follow a
|
||||
later version.
|
||||
|
||||
15. Disclaimer of Warranty.
|
||||
|
||||
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
||||
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
||||
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
||||
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
||||
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
||||
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
||||
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||
|
||||
16. Limitation of Liability.
|
||||
|
||||
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
||||
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
||||
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
||||
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
||||
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
||||
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
||||
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
||||
SUCH DAMAGES.
|
||||
|
||||
17. Interpretation of Sections 15 and 16.
|
||||
|
||||
If the disclaimer of warranty and limitation of liability provided
|
||||
above cannot be given local legal effect according to their terms,
|
||||
reviewing courts shall apply local law that most closely approximates
|
||||
an absolute waiver of all civil liability in connection with the
|
||||
Program, unless a warranty or assumption of liability accompanies a
|
||||
copy of the Program in return for a fee.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
How to Apply These Terms to Your New Programs
|
||||
|
||||
If you develop a new program, and you want it to be of the greatest
|
||||
possible use to the public, the best way to achieve this is to make it
|
||||
free software which everyone can redistribute and change under these terms.
|
||||
|
||||
To do so, attach the following notices to the program. It is safest
|
||||
to attach them to the start of each source file to most effectively
|
||||
state the exclusion of warranty; and each file should have at least
|
||||
the "copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
<one line to give the program's name and a brief idea of what it does.>
|
||||
Copyright (C) <year> <name of author>
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If the program does terminal interaction, make it output a short
|
||||
notice like this when it starts in an interactive mode:
|
||||
|
||||
<program> Copyright (C) <year> <name of author>
|
||||
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
||||
This is free software, and you are welcome to redistribute it
|
||||
under certain conditions; type `show c' for details.
|
||||
|
||||
The hypothetical commands `show w' and `show c' should show the appropriate
|
||||
parts of the General Public License. Of course, your program's commands
|
||||
might be different; for a GUI interface, you would use an "about box".
|
||||
|
||||
You should also get your employer (if you work as a programmer) or school,
|
||||
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||
For more information on this, and how to apply and follow the GNU GPL, see
|
||||
<https://www.gnu.org/licenses/>.
|
||||
|
||||
The GNU General Public License does not permit incorporating your program
|
||||
into proprietary programs. If your program is a subroutine library, you
|
||||
may consider it more useful to permit linking proprietary applications with
|
||||
the library. If this is what you want to do, use the GNU Lesser General
|
||||
Public License instead of this License. But first, please read
|
||||
<https://www.gnu.org/licenses/why-not-lgpl.html>.
|
||||
199
README.md
Normal file
199
README.md
Normal file
@@ -0,0 +1,199 @@
|
||||
# Introduction
|
||||
Sunshine is a Gamestream host for Moonlight
|
||||
|
||||
[](https://ci.appveyor.com/project/loki-47-6F-64/sunshine/branch/master)
|
||||
[](https://github.com/Loki-47-6F-64/sunshine/releases)
|
||||
|
||||
- [Building](README.md#building)
|
||||
- [Credits](README.md#credits)
|
||||
|
||||
# Building
|
||||
- [Linux](README.md#linux)
|
||||
- [Windows](README.md#windows-10)
|
||||
|
||||
## Linux
|
||||
|
||||
### Requirements:
|
||||
Ubuntu 20.04:
|
||||
Install the following
|
||||
```
|
||||
sudo apt install cmake libssl-dev libavdevice-dev libboost-thread-dev libboost-filesystem-dev libboost-log-dev libpulse-dev libopus-dev libxtst-dev libx11-dev libxrandr-dev libxfixes-dev libevdev-dev libxcb1-dev libxcb-shm0-dev libxcb-xfixes0-dev
|
||||
```
|
||||
|
||||
### Compilation:
|
||||
- `git clone https://github.com/loki-47-6F-64/sunshine.git --recurse-submodules`
|
||||
- `cd sunshine && mkdir build && cd build`
|
||||
- `cmake ..`
|
||||
- `make -j ${nproc}`
|
||||
|
||||
|
||||
### Setup:
|
||||
sunshine needs access to uinput to create mouse and gamepad events:
|
||||
- Add user to group 'input':
|
||||
`usermod -a -G input $USER`
|
||||
- Create udev rules:
|
||||
- Run the following command:
|
||||
`nano /etc/udev/rules.d/85-sunshine-input.rules`
|
||||
- Input the following contents:
|
||||
`KERNEL=="uinput", GROUP="input", mode="0660"`
|
||||
- Save the file and exit
|
||||
1. `CTRL+X` to start exit
|
||||
2. `Y` to save modifications
|
||||
- `assets/sunshine.conf` is an example configuration file. Modify it as you see fit, then use it by running:
|
||||
`sunshine path/to/sunshine.conf`
|
||||
- Configure autostart service
|
||||
`path/to/build/dir/sunshine.service` is used to start sunshine in the background. To use it, do the following:
|
||||
1. Copy it to the users systemd, `cp sunshine.service ~/.config/systemd/user/`
|
||||
2. Starting
|
||||
- Onetime:
|
||||
`systemctl --user start sunshine`
|
||||
- Always on boot:
|
||||
`systemctl --user enable sunshine`
|
||||
|
||||
- `assets/apps.json` is an [example](README.md#application-list) of a list of applications that are started just before running a stream
|
||||
|
||||
### Trouleshooting:
|
||||
- If you get "Could not create Sunshine Gamepad: Permission Denied", ensure you are part of the group "input":
|
||||
- `groups $USER`
|
||||
|
||||
- If Sunshine sends audio from the microphone instead of the speaker, try the following steps:
|
||||
1. `$ pacmd list-sources | grep "name:"` or `$ pactl info | grep Source` if running pipewire.
|
||||
2. Copy the name to the configuration option "audio_sink"
|
||||
3. restart sunshine
|
||||
|
||||
## Windows 10
|
||||
|
||||
### Requirements:
|
||||
|
||||
mingw-w64-x86_64-openssl mingw-w64-x86_64-cmake mingw-w64-x86_64-toolchain mingw-w64-x86_64-opus mingw-w64-x86_64-x265 mingw-w64-x86_64-boost git yasm nasm diffutils make
|
||||
|
||||
### Compilation:
|
||||
- `git clone https://github.com/loki-47-6F-64/sunshine.git --recursive`
|
||||
- `cd sunshine && mkdir build && cd build`
|
||||
- `cmake -G"Unix Makefiles" ..`
|
||||
- `make`
|
||||
|
||||
### Setup:
|
||||
- **OPTIONAL** Gamepad support: Download and run 'ViGEmBus_Setup_1.16.116.exe' from [https://github.com/ViGEm/ViGEmBus/releases]
|
||||
|
||||
|
||||
|
||||
# Common
|
||||
|
||||
## Usage:
|
||||
- run "sunshine path/to/sunshine.conf"
|
||||
- If running for the first time, make sure to note the username and password Sunshine showed to you, since you **cannot get back later**!
|
||||
- In Moonlight: Add PC manually
|
||||
- When Moonlight request you insert the correct pin on sunshine:
|
||||
- Type in the URL bar of your browser: `https://xxx.xxx.xxx.xxx:47990` where `xxx.xxx.xxx.xxx` is the IP address of your computer
|
||||
- Ignore any warning given by your browser about "insecure website"
|
||||
- Type in the username and password shown the first time you run Sunshine
|
||||
- Go to "PIN" in the Header
|
||||
- Type in your PIN and press Enter, you should get a Success Message
|
||||
- Click on one of the Applications listed
|
||||
- Have fun :)
|
||||
|
||||
|
||||
## Note:
|
||||
- The Windows key is not passed through by Moonlight, therefore Sunshine maps Right-Alt key to the Windows key
|
||||
- If you set Video Bitrate to 0.5Mb/s:
|
||||
- Sunshine will use CRF or QP to controll the quality of the stream. (See example configuration file for more details)
|
||||
- This is less CPU intensive and it has lower average bandwith requirements compared to manually setting bitrate to acceptable quality
|
||||
- However, it has higher peak bitrates, forcing Sunshine to drop entire frames when streaming 1080P due to their size.
|
||||
- When this happens, the video portion of the stream appears to be frozen.
|
||||
- This is rare enough that using this for the desktop environment is tolerable (in my opinion), however for gaming not so much.
|
||||
|
||||
|
||||
## Credits:
|
||||
- [Simple-Web-Server](https://gitlab.com/eidheim/Simple-Web-Server)
|
||||
- [Moonlight](https://github.com/moonlight-stream)
|
||||
- [Looking-Glass](https://github.com/gnif/LookingGlass) (For showing me how to properly capture frames on Windows, saving me a lot of time :)
|
||||
- [Eretik](http://eretik.omegahg.com/) (For creating PolicyConfig.h, allowing me to change the default audio device on Windows programmatically)
|
||||
|
||||
## Application List:
|
||||
**Note:** You can change the Application List in the "Apps" section of the User Interface `https://xxx.xxx.xxx.xxx:47990/`
|
||||
- You can use Environment variables in place of values
|
||||
- $(HOME) will be replaced by the value of $HOME
|
||||
- $$ will be replaced by $ --> $$(HOME) will be replaced by $(HOME)
|
||||
- env: Adds or overwrites Environment variables for the commands/applications run by Sunshine.
|
||||
- "Variable name":"Variable value"
|
||||
- apps: The list of applications
|
||||
- Example:
|
||||
```json
|
||||
{
|
||||
"name":"An App",
|
||||
"cmd":"command to open app",
|
||||
"prep-cmd":[
|
||||
{
|
||||
"do":"some-command",
|
||||
"undo":"undo-that-command"
|
||||
}
|
||||
],
|
||||
"detached":[
|
||||
"some-command",
|
||||
"another-command"
|
||||
]
|
||||
}
|
||||
```
|
||||
- name: Self explanatory
|
||||
- output <optional>: The file where the output of the command is stored
|
||||
- If it is not specified, the output is ignored
|
||||
- detached: A list of commands to be run and forgotten about
|
||||
- prep-cmd: A list of commands to be run before/after the application
|
||||
- If any of the prep-commands fail, starting the application is aborted
|
||||
- do: Run before the application
|
||||
- If it fails, all 'undo' commands of the previously succeeded 'do' commands are run
|
||||
- undo <optional>: Run after the application has terminated
|
||||
- This should not fail considering it is supposed to undo the 'do' commands.
|
||||
- If it fails, Sunshine is terminated
|
||||
- cmd <optional>: The main application
|
||||
- If not specified, a processs is started that sleeps indefinitely
|
||||
|
||||
1. When an application is started, if there is an application already running, it will be terminated.
|
||||
2. When the application has been shutdown, the stream shuts down as well.
|
||||
3. In addition to the apps listed, one app "Desktop" is hardcoded into Sunshine. It does not start an application, instead it simply starts a stream.
|
||||
|
||||
Linux
|
||||
```json
|
||||
{
|
||||
"env":{
|
||||
"DISPLAY":":0",
|
||||
"DRI_PRIME":"1",
|
||||
"XAUTHORITY":"$(HOME)/.Xauthority",
|
||||
"PATH":"$(PATH):$(HOME)/.local/bin"
|
||||
},
|
||||
"apps":[
|
||||
{
|
||||
"name":"Low Res Desktop",
|
||||
"prep-cmd":[
|
||||
{ "do":"xrandr --output HDMI-1 --mode 1920x1080", "undo":"xrandr --output HDMI-1 --mode 1920x1200" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"name":"Steam BigPicture",
|
||||
|
||||
"output":"steam.txt",
|
||||
"cmd":"steam -bigpicture",
|
||||
"prep-cmd":[]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
Windows
|
||||
```json
|
||||
{
|
||||
"env":{
|
||||
"PATH":"$(PATH);C:\\Program Files (x86)\\Steam"
|
||||
},
|
||||
"apps":[
|
||||
{
|
||||
"name":"Steam BigPicture",
|
||||
|
||||
"output":"steam.txt",
|
||||
"prep-cmd":[
|
||||
{"do":"steam \"steam://open/bigpicture\""}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
107
README.txt
107
README.txt
@@ -1,107 +0,0 @@
|
||||
######### Linux ##############
|
||||
|
||||
Requirements:
|
||||
Ubuntu 19.10: cmake libssl-dev libavdevice-dev libboost-thread-dev libboost-filesystem-dev libboost-log-dev libpulse-dev libopus-dev libxtst-dev libx11-dev libxfixes-dev libevdev-dev libxcb1-dev libxcb-shm0-dev libxcb-xfixes0-dev
|
||||
|
||||
Compilation:
|
||||
* git clone <repository> --recurse-submodules
|
||||
* mkdir build && cd build
|
||||
* cmake ..
|
||||
* make
|
||||
|
||||
|
||||
Setup:
|
||||
* sunshine needs access to uinput to create mouse and gamepad events:
|
||||
* Add user to group 'input': "usermod -a -G input username
|
||||
* Create a file: "/etc/udev/rules.d/85-sunshine-input.rules"
|
||||
* The contents of the file is as follows:
|
||||
KERNEL=="uinput", GROUP="input", mode="0660"
|
||||
* assets/sunshine.conf is an example configuration file. Modify it as you see fit and use it by running: "sunshine path/to/sunshine.conf"
|
||||
* path/to/build/dir/sunshine.service is used to start sunshine in the background:
|
||||
* cp sunshine.service $HOME/.config/systemd/user/
|
||||
* Modify $HOME/.config/systemd/user/sunshine.conf to point to the sunshine executable
|
||||
* systemctl --user start sunshine
|
||||
|
||||
* assets/apps.json is an example of a list of applications that are started just before running a stream:
|
||||
* See below for a detailed explanation
|
||||
|
||||
Trouleshooting:
|
||||
* If you get "Could not create Sunshine Gamepad: Permission Denied", ensure you are part of the group "input":
|
||||
* groups
|
||||
* If Sunshine sends audio from the microphone instead of the speaker, try the following steps:
|
||||
* pacmd list-sources | grep "name:"
|
||||
* Copy the name to the configuration option "audio_sink"
|
||||
* restart sunshine
|
||||
|
||||
|
||||
|
||||
######### Windows 10 ############
|
||||
|
||||
Requirements:
|
||||
MSYS2 : mingw-w64-x86_64-openssl mingw-w64-x86_64-cmake mingw-w64-x86_64-toolchain mingw-w64-x86_64-ffmpeg mingw-w64-x86_64-boost
|
||||
|
||||
Compilation:
|
||||
* git clone <repository> --recurse-submodules
|
||||
* mkdir build && cd build
|
||||
* cmake -G"Unix Makefiles" ..
|
||||
* make
|
||||
|
||||
Setup:
|
||||
* <optional> Gamepad support: Download and run 'ViGEmBus_Setup_1.16.116.exe' from [https://github.com/ViGEm/ViGEmBus/releases]
|
||||
|
||||
|
||||
|
||||
|
||||
######### Common #############
|
||||
|
||||
Usage:
|
||||
* run "sunshine path/to/sunshine.conf"
|
||||
* In Moonlight: Add PC manually
|
||||
* When Moonlight request you insert the correct pin on sunshine:
|
||||
wget xxx.xxx.xxx.xxx:47989/pin/xxxx -- where the first few x's are substituted by the ip of Sunshine and the final 4 x'es are substituted by the pin
|
||||
or
|
||||
Type in the URL bar of your browser: xxx.xxx.xxx.xxx:47989/pin/xxxx -- where the first few x's are substituted by the ip of the final 4 x'es are subsituted by the pin
|
||||
* Click on one of the Applications listed
|
||||
* Have fun :)
|
||||
|
||||
|
||||
Note:
|
||||
* The Windows key is not passed through by Moonlight, therefore Sunshine maps Right-Alt key to the Windows key
|
||||
* If you set Video Bitrate to 0.5Mb/s:
|
||||
* Sunshine will use CRF or QP to controll the quality of the stream. (See example configuration file for more details)
|
||||
* This is less CPU intensive and it has lower average bandwith requirements compared to manually setting bitrate to acceptable quality
|
||||
* However, it has higher peak bitrates, forcing Sunshine to drop entire frames when streaming 1080P due to their size.
|
||||
* When this happens, the video portion of the stream appears to be frozen.
|
||||
* This is rare enough that using this for the desktop environment is tolerable (in my opinion), however for gaming not so much.
|
||||
|
||||
|
||||
Credits:
|
||||
* Simple-Web-Server [https://gitlab.com/eidheim/Simple-Web-Server]
|
||||
* Moonlight [https://github.com/moonlight-stream]
|
||||
* Looking-Glass [https://github.com/gnif/LookingGlass] (For showing me how to properly capture frames on Windows, saving me a lot of time :)
|
||||
|
||||
|
||||
|
||||
Application List:
|
||||
* You can use Environment variables in place of values
|
||||
* $(HOME) will be replaced by the value of $HOME
|
||||
* $$ will be replaced by $ --> $$(HOME) will be replaced by $(HOME)
|
||||
* env: Adds or overwrites Environment variables for the commands/applications run by Sunshine.
|
||||
* "Variable name":"Variable value"
|
||||
* apps: The list of applications
|
||||
* name: Self explanatory
|
||||
* output <optional>: The file where the output of the command is stored
|
||||
* If it is not specified, the output is ignored
|
||||
* prep-cmd: A list of commands to be run before/after the application
|
||||
* If any of the prep-commands fail, starting the application is aborted
|
||||
* do: Run before the application
|
||||
* If it fails, all 'undo' commands of the previously succeeded 'do' commands are run
|
||||
* undo <optional>: Run after the application has terminated
|
||||
* This should not fail considering it is supposed to undo the 'do' commands.
|
||||
* If it fails, Sunshine is terminated
|
||||
* cmd <optional>: The main application
|
||||
* If not specified, a processs is started that sleeps indefinitely
|
||||
|
||||
When an application is started, if there is an application already running, it will be terminated.
|
||||
When the application has been shutdown, the stream shuts down as well.
|
||||
In addition to the apps listed, one app "Desktop" is hardcoded into Sunshine. It does not start an application, instead it simply starts a stream.
|
||||
20
appveyor.yml
20
appveyor.yml
@@ -1,5 +1,5 @@
|
||||
image:
|
||||
- Ubuntu
|
||||
- Ubuntu2004
|
||||
- Visual Studio 2019
|
||||
|
||||
environment:
|
||||
@@ -8,18 +8,8 @@ environment:
|
||||
- BUILD_TYPE: Release
|
||||
|
||||
install:
|
||||
- sh: sudo add-apt-repository ppa:hnakamur/icu
|
||||
- sh: sudo add-apt-repository ppa:hnakamur/boost
|
||||
|
||||
- sh: sudo add-apt-repository ppa:savoury1/build-tools
|
||||
- sh: sudo add-apt-repository ppa:savoury1/backports
|
||||
- sh: sudo add-apt-repository ppa:savoury1/graphics
|
||||
- sh: sudo add-apt-repository ppa:savoury1/multimedia
|
||||
- sh: sudo add-apt-repository ppa:savoury1/ffmpeg4
|
||||
|
||||
- sh: sudo apt update
|
||||
- sh: sudo apt install -y fakeroot cmake libssl-dev libavdevice-dev libboost-thread1.67-dev libboost-filesystem1.67-dev libboost-log1.67-dev libpulse-dev libopus-dev libxtst-dev libx11-dev libxfixes-dev libevdev-dev libxcb1-dev libxcb-shm0-dev libxcb-xfixes0-dev
|
||||
- sh: sudo update-alternatives --set gcc /usr/bin/gcc-8
|
||||
- sh: sudo apt update --ignore-missing
|
||||
- sh: sudo apt install -y build-essential fakeroot gcc-10 g++-10 cmake libssl-dev libavdevice-dev libboost-thread-dev libboost-filesystem-dev libboost-log-dev libpulse-dev libopus-dev libxtst-dev libx11-dev libxrandr-dev libxfixes-dev libevdev-dev libxcb1-dev libxcb-shm0-dev libxcb-xfixes0-dev
|
||||
- cmd: C:\msys64\usr\bin\bash -lc "pacman --needed --noconfirm -S mingw-w64-x86_64-openssl mingw-w64-x86_64-cmake mingw-w64-x86_64-toolchain mingw-w64-x86_64-opus mingw-w64-x86_64-x265 mingw-w64-x86_64-boost git yasm nasm diffutils make"
|
||||
|
||||
before_build:
|
||||
@@ -30,8 +20,8 @@ before_build:
|
||||
build_script:
|
||||
- cmd: set OLDPATH=%PATH%
|
||||
- cmd: set PATH=C:\msys64\mingw64\bin
|
||||
- sh: cmake -DCMAKE_BUILD_TYPE=$BUILD_TYPE -DSUNSHINE_EXECUTABLE_PATH=sunshine -DSUNSHINE_ASSETS_DIR=/etc/sunshine ..
|
||||
- cmd: cmake -DCMAKE_BUILD_TYPE=$BUILD_TYPE -DOPENSSL_ROOT_DIR=C:\OpenSSL-v111-Win64 -DSUNSHINE_STANDALONE=ON -DSUNSHINE_ASSETS_DIR=assets -G "MinGW Makefiles" ..
|
||||
- sh: cmake -DCMAKE_C_COMPILER=gcc-10 -DCMAKE_CXX_COMPILER=g++-10 -DCMAKE_BUILD_TYPE=$BUILD_TYPE -DSUNSHINE_EXECUTABLE_PATH=sunshine -DSUNSHINE_ASSETS_DIR=/etc/sunshine ..
|
||||
- cmd: cmake -DCMAKE_BUILD_TYPE=$BUILD_TYPE -DSUNSHINE_ASSETS_DIR=assets -G "MinGW Makefiles" ..
|
||||
- sh: make -j$(nproc)
|
||||
- cmd: mingw32-make -j2
|
||||
- cmd: set PATH=%OLDPATH%
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
{
|
||||
"env":{
|
||||
"DISPLAY":":0",
|
||||
"DRI_PRIME":"1",
|
||||
"XAUTHORITY":"$(HOME)/.Xauthority",
|
||||
"PATH":"$(PATH):$(HOME)/.local/bin"
|
||||
},
|
||||
"apps":[
|
||||
@@ -16,8 +13,7 @@
|
||||
"name":"Steam BigPicture",
|
||||
|
||||
"output":"steam.txt",
|
||||
"cmd":"steam -bigpicture",
|
||||
"prep-cmd":[]
|
||||
"detached":["setsid steam steam://open/bigpicture"]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -7,9 +7,7 @@
|
||||
"name":"Steam BigPicture",
|
||||
|
||||
"output":"steam.txt",
|
||||
"prep-cmd":[
|
||||
{"do":"steam \"steam://open/bigpicture\""}
|
||||
]
|
||||
"detached":["steam steam://open/bigpicture"]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
33
assets/shaders/directx/ConvertUVPS.hlsl
Normal file
33
assets/shaders/directx/ConvertUVPS.hlsl
Normal file
@@ -0,0 +1,33 @@
|
||||
Texture2D image : register(t0);
|
||||
|
||||
SamplerState def_sampler : register(s0);
|
||||
|
||||
struct FragTexWide {
|
||||
float3 uuv : TEXCOORD0;
|
||||
};
|
||||
|
||||
cbuffer ColorMatrix : register(b0) {
|
||||
float4 color_vec_y;
|
||||
float4 color_vec_u;
|
||||
float4 color_vec_v;
|
||||
float2 range_y;
|
||||
float2 range_uv;
|
||||
};
|
||||
|
||||
//--------------------------------------------------------------------------------------
|
||||
// Pixel Shader
|
||||
//--------------------------------------------------------------------------------------
|
||||
float2 main_ps(FragTexWide input) : SV_Target
|
||||
{
|
||||
float3 rgb_left = image.Sample(def_sampler, input.uuv.xz).rgb;
|
||||
float3 rgb_right = image.Sample(def_sampler, input.uuv.yz).rgb;
|
||||
float3 rgb = (rgb_left + rgb_right) * 0.5;
|
||||
|
||||
float u = dot(color_vec_u.xyz, rgb) + color_vec_u.w;
|
||||
float v = dot(color_vec_v.xyz, rgb) + color_vec_v.w;
|
||||
|
||||
u = u * range_uv.x + range_uv.y;
|
||||
v = v * range_uv.x + range_uv.y;
|
||||
|
||||
return float2(u, v * 224.0f/256.0f + 0.0625);
|
||||
}
|
||||
29
assets/shaders/directx/ConvertUVVS.hlsl
Normal file
29
assets/shaders/directx/ConvertUVVS.hlsl
Normal file
@@ -0,0 +1,29 @@
|
||||
struct VertTexPosWide {
|
||||
float3 uuv : TEXCOORD;
|
||||
float4 pos : SV_POSITION;
|
||||
};
|
||||
|
||||
cbuffer info : register(b0) {
|
||||
float width_i;
|
||||
};
|
||||
|
||||
//--------------------------------------------------------------------------------------
|
||||
// Vertex Shader
|
||||
//--------------------------------------------------------------------------------------
|
||||
VertTexPosWide main_vs(uint vI : SV_VERTEXID)
|
||||
{
|
||||
float idHigh = float(vI >> 1);
|
||||
float idLow = float(vI & uint(1));
|
||||
|
||||
float x = idHigh * 4.0 - 1.0;
|
||||
float y = idLow * 4.0 - 1.0;
|
||||
|
||||
float u_right = idHigh * 2.0;
|
||||
float u_left = u_right - width_i;
|
||||
float v = 1.0 - idLow * 2.0;
|
||||
|
||||
VertTexPosWide vert_out;
|
||||
vert_out.uuv = float3(u_left, u_right, v);
|
||||
vert_out.pos = float4(x, y, 0.0, 1.0);
|
||||
return vert_out;
|
||||
}
|
||||
25
assets/shaders/directx/ConvertYPS.hlsl
Normal file
25
assets/shaders/directx/ConvertYPS.hlsl
Normal file
@@ -0,0 +1,25 @@
|
||||
Texture2D image : register(t0);
|
||||
|
||||
SamplerState def_sampler : register(s0);
|
||||
|
||||
cbuffer ColorMatrix : register(b0) {
|
||||
float4 color_vec_y;
|
||||
float4 color_vec_u;
|
||||
float4 color_vec_v;
|
||||
float2 range_y;
|
||||
float2 range_uv;
|
||||
};
|
||||
|
||||
struct PS_INPUT
|
||||
{
|
||||
float4 pos : SV_POSITION;
|
||||
float2 tex : TEXCOORD;
|
||||
};
|
||||
|
||||
float main_ps(PS_INPUT frag_in) : SV_Target
|
||||
{
|
||||
float3 rgb = image.Sample(def_sampler, frag_in.tex, 0).rgb;
|
||||
float y = dot(color_vec_y.xyz, rgb);
|
||||
|
||||
return y * range_y.x + range_y.y;
|
||||
}
|
||||
14
assets/shaders/directx/ScenePS.hlsl
Normal file
14
assets/shaders/directx/ScenePS.hlsl
Normal file
@@ -0,0 +1,14 @@
|
||||
Texture2D image : register(t0);
|
||||
|
||||
SamplerState def_sampler : register(s0);
|
||||
|
||||
struct PS_INPUT
|
||||
{
|
||||
float4 pos : SV_POSITION;
|
||||
float2 tex : TEXCOORD;
|
||||
};
|
||||
|
||||
float4 main_ps(PS_INPUT frag_in) : SV_Target
|
||||
{
|
||||
return image.Sample(def_sampler, frag_in.tex, 0);
|
||||
}
|
||||
22
assets/shaders/directx/SceneVS.hlsl
Normal file
22
assets/shaders/directx/SceneVS.hlsl
Normal file
@@ -0,0 +1,22 @@
|
||||
struct PS_INPUT
|
||||
{
|
||||
float4 pos : SV_POSITION;
|
||||
float2 tex : TEXCOORD;
|
||||
};
|
||||
|
||||
PS_INPUT main_vs(uint vI : SV_VERTEXID)
|
||||
{
|
||||
float idHigh = float(vI >> 1);
|
||||
float idLow = float(vI & uint(1));
|
||||
|
||||
float x = idHigh * 4.0 - 1.0;
|
||||
float y = idLow * 4.0 - 1.0;
|
||||
|
||||
float u = idHigh * 2.0;
|
||||
float v = 1.0 - idLow * 2.0;
|
||||
|
||||
PS_INPUT vert_out;
|
||||
vert_out.pos = float4(x, y, 0.0, 1.0);
|
||||
vert_out.tex = float2(u, v);
|
||||
return vert_out;
|
||||
}
|
||||
35
assets/shaders/opengl/ConvertUV.frag
Normal file
35
assets/shaders/opengl/ConvertUV.frag
Normal file
@@ -0,0 +1,35 @@
|
||||
#version 300 es
|
||||
|
||||
#ifdef GL_ES
|
||||
precision lowp float;
|
||||
#endif
|
||||
|
||||
uniform sampler2D image;
|
||||
|
||||
layout(shared) uniform ColorMatrix {
|
||||
vec4 color_vec_y;
|
||||
vec4 color_vec_u;
|
||||
vec4 color_vec_v;
|
||||
vec2 range_y;
|
||||
vec2 range_uv;
|
||||
};
|
||||
|
||||
in vec3 uuv;
|
||||
layout(location = 0) out vec2 color;
|
||||
|
||||
//--------------------------------------------------------------------------------------
|
||||
// Pixel Shader
|
||||
//--------------------------------------------------------------------------------------
|
||||
void main() {
|
||||
vec3 rgb_left = texture(image, uuv.xz).rgb;
|
||||
vec3 rgb_right = texture(image, uuv.yz).rgb;
|
||||
vec3 rgb = (rgb_left + rgb_right) * 0.5;
|
||||
|
||||
float u = dot(color_vec_u.xyz, rgb) + color_vec_u.w;
|
||||
float v = dot(color_vec_v.xyz, rgb) + color_vec_v.w;
|
||||
|
||||
u = u * range_uv.x + range_uv.y;
|
||||
v = v * range_uv.x + range_uv.y;
|
||||
|
||||
color = vec2(u, v * 224.0f / 256.0f + 0.0625);
|
||||
}
|
||||
27
assets/shaders/opengl/ConvertUV.vert
Normal file
27
assets/shaders/opengl/ConvertUV.vert
Normal file
@@ -0,0 +1,27 @@
|
||||
#version 300 es
|
||||
|
||||
#ifdef GL_ES
|
||||
precision mediump float;
|
||||
#endif
|
||||
|
||||
uniform float width_i;
|
||||
|
||||
out vec3 uuv;
|
||||
//--------------------------------------------------------------------------------------
|
||||
// Vertex Shader
|
||||
//--------------------------------------------------------------------------------------
|
||||
void main()
|
||||
{
|
||||
float idHigh = float(gl_VertexID >> 1);
|
||||
float idLow = float(gl_VertexID & int(1));
|
||||
|
||||
float x = idHigh * 4.0 - 1.0;
|
||||
float y = idLow * 4.0 - 1.0;
|
||||
|
||||
float u_right = idHigh * 2.0;
|
||||
float u_left = u_right - width_i;
|
||||
float v = idLow * 2.0;
|
||||
|
||||
uuv = vec3(u_left, u_right, v);
|
||||
gl_Position = vec4(x, y, 0.0, 1.0);
|
||||
}
|
||||
26
assets/shaders/opengl/ConvertY.frag
Normal file
26
assets/shaders/opengl/ConvertY.frag
Normal file
@@ -0,0 +1,26 @@
|
||||
#version 300 es
|
||||
|
||||
#ifdef GL_ES
|
||||
precision lowp float;
|
||||
#endif
|
||||
|
||||
uniform sampler2D image;
|
||||
|
||||
layout(shared) uniform ColorMatrix {
|
||||
vec4 color_vec_y;
|
||||
vec4 color_vec_u;
|
||||
vec4 color_vec_v;
|
||||
vec2 range_y;
|
||||
vec2 range_uv;
|
||||
};
|
||||
|
||||
in vec2 tex;
|
||||
layout(location = 0) out float color;
|
||||
|
||||
void main()
|
||||
{
|
||||
vec3 rgb = texture(image, tex).rgb;
|
||||
float y = dot(color_vec_y.xyz, rgb);
|
||||
|
||||
color = y * range_y.x + range_y.y;
|
||||
}
|
||||
14
assets/shaders/opengl/Scene.frag
Normal file
14
assets/shaders/opengl/Scene.frag
Normal file
@@ -0,0 +1,14 @@
|
||||
#version 300 es
|
||||
|
||||
#ifdef GL_ES
|
||||
precision lowp float;
|
||||
#endif
|
||||
|
||||
uniform sampler2D image;
|
||||
|
||||
in vec2 tex;
|
||||
layout(location = 0) out vec4 color;
|
||||
void main()
|
||||
{
|
||||
color = texture(image, tex);
|
||||
}
|
||||
22
assets/shaders/opengl/Scene.vert
Normal file
22
assets/shaders/opengl/Scene.vert
Normal file
@@ -0,0 +1,22 @@
|
||||
#version 300 es
|
||||
|
||||
#ifdef GL_ES
|
||||
precision mediump float;
|
||||
#endif
|
||||
|
||||
out vec2 tex;
|
||||
|
||||
void main()
|
||||
{
|
||||
float idHigh = float(gl_VertexID >> 1);
|
||||
float idLow = float(gl_VertexID & int(1));
|
||||
|
||||
float x = idHigh * 4.0 - 1.0;
|
||||
float y = idLow * 4.0 - 1.0;
|
||||
|
||||
float u = idHigh * 2.0;
|
||||
float v = idLow * 2.0;
|
||||
|
||||
gl_Position = vec4(x, y, 0.0, 1.0);
|
||||
tex = vec2(u, v);
|
||||
}
|
||||
@@ -37,6 +37,30 @@
|
||||
# The file where current state of Sunshine is stored
|
||||
# file_state = sunshine_state.json
|
||||
|
||||
# The file where user credentials for the UI are stored
|
||||
# By default, credentials are stored in `file_state`
|
||||
# credentials_file = sunshine_state.json
|
||||
|
||||
# The display modes advertised by Sunshine
|
||||
#
|
||||
# Some versions of Moonlight, such as Moonlight-nx (Switch),
|
||||
# rely on this list to ensure that the requested resolutions and fps
|
||||
# are supported.
|
||||
#
|
||||
# fps = [10, 30, 60, 90, 120]
|
||||
# resolutions = [
|
||||
# 352x240,
|
||||
# 480x360,
|
||||
# 858x480,
|
||||
# 1280x720,
|
||||
# 1920x1080,
|
||||
# 2560x1080,
|
||||
# 3440x1440,
|
||||
# 1920x1200,
|
||||
# 3860x2160,
|
||||
# 3840x1600,
|
||||
# ]
|
||||
|
||||
# How long to wait in milliseconds for data from moonlight before shutting down the stream
|
||||
# ping_timeout = 2000
|
||||
|
||||
@@ -50,6 +74,14 @@
|
||||
# The value must be greater than 0 and lower than or equal to 100
|
||||
# fec_percentage = 10
|
||||
|
||||
# When multicasting, it could be usefull to have different configurations for each connected Client.
|
||||
# For example:
|
||||
# Clients connected through WAN and LAN have different bitrate contstraints.
|
||||
# Decoders may require different settings for color
|
||||
#
|
||||
# Unlike simply broadcasting to multiple Client, this will generate distinct video streams.
|
||||
# Note, CPU usage increases for each distinct video stream generated
|
||||
# channels = 1
|
||||
|
||||
# The back/select button on the controller
|
||||
# On the Shield, the home and powerbutton are not passed to Moonlight
|
||||
@@ -57,18 +89,31 @@
|
||||
# If back_button_timeout < 0, then the Home/Guide button will not be emulated
|
||||
# back_button_timeout = 2000
|
||||
|
||||
# !! Windows only !!
|
||||
# Control how fast keys will repeat themselves
|
||||
# The initial delay in milliseconds before repeating keys
|
||||
# key_repeat_delay = 500
|
||||
#
|
||||
# How often keys repeat every second
|
||||
# This configurable option supports decimals
|
||||
# key_repeat_frequency = 24.9
|
||||
|
||||
# The name of the audio sink used for Audio Loopback
|
||||
# If you do not specify this variable, pulseaudio will select the default monitor device.
|
||||
#
|
||||
# You can find the name of the audio sink using the following command:
|
||||
# !! Linux only !!
|
||||
# pacmd list-sources | grep "name:"
|
||||
# audio_sink = alsa_output.pci-0000_09_00.3.analog-stereo.monitor
|
||||
# pacmd list-sinks | grep "name:" if running vanilla pulseaudio
|
||||
# pactl info | grep Source` if running pipewire
|
||||
# audio_sink = alsa_output.pci-0000_09_00.3.analog-stereo
|
||||
#
|
||||
# !! Windows only !!
|
||||
# tools\audio-info.exe
|
||||
# audio_sink = {0.0.0.00000000}.{FD47D9CC-4218-4135-9CE2-0C195C87405B}
|
||||
# audio_sink = {0.0.0.00000000}.{FD47D9CC-4218-4135-9CE2-0C195C87405B}
|
||||
#
|
||||
# The virtual sink, is the audio device that's virtual (Like Steam Streaming Speakers), it allows Sunshine
|
||||
# to stream audio, while muting the speakers.
|
||||
# virtual_sink = {0.0.0.00000000}.{8edba70c-1125-467c-b89c-15da389bc1d4}
|
||||
|
||||
# !! Windows only !!
|
||||
# You can select the video card you want to stream:
|
||||
@@ -77,6 +122,13 @@
|
||||
# adapter_name = Radeon RX 580 Series
|
||||
# output_name = \\.\DISPLAY1
|
||||
|
||||
# !! Linux only !!
|
||||
# Set the display number to stream.
|
||||
# You can find them by the following command:
|
||||
# xrandr --listmonitors
|
||||
# Example output: "0: +HDMI-1 1920/518x1200/324+0+0 HDMI-1"
|
||||
# ^ <-- You need this.
|
||||
# output_name = 0
|
||||
|
||||
###############################################
|
||||
# FFmpeg software encoding parameters
|
||||
@@ -97,15 +149,107 @@
|
||||
# Increasing the value slightly reduces encoding efficiency, but the tradeoff is usually
|
||||
# worth it to gain the use of more CPU cores for encoding. The ideal value is the lowest
|
||||
# value that can reliably encode at your desired streaming settings on your hardware.
|
||||
# min_threads = 2
|
||||
# min_threads = 1
|
||||
|
||||
# Allows the client to request HEVC Main or HEVC Main10 video streams.
|
||||
# HEVC is more CPU-intensive to encode, so enabling this may reduce performance.
|
||||
# If set to 0 (default), Sunshine will not advertise support for HEVC
|
||||
# If set to 1, Sunshine will advertise support for HEVC Main profile
|
||||
# If set to 2, Sunshine will advertise support for HEVC Main and Main10 (HDR) profiles
|
||||
# HEVC is more CPU-intensive to encode, so enabling this may reduce performance when using software encoding.
|
||||
# If set to 0 (default), Sunshine will specify support for HEVC based on encoder
|
||||
# If set to 1, Sunshine will not advertise support for HEVC
|
||||
# If set to 2, Sunshine will advertise support for HEVC Main profile
|
||||
# If set to 3, Sunshine will advertise support for HEVC Main and Main10 (HDR) profiles
|
||||
# hevc_mode = 2
|
||||
|
||||
# Force a specific encoder, otherwise Sunshine will use the first encoder that is available
|
||||
# supported encoders:
|
||||
# nvenc
|
||||
# amdvce # NOTE: alpha stage. The cursor is not yet displayed
|
||||
# software
|
||||
#
|
||||
# encoder = nvenc
|
||||
##################################### Software #####################################
|
||||
# See x264 --fullhelp for the different presets
|
||||
# preset = superfast
|
||||
# tune = zerolatency
|
||||
# sw_preset = superfast
|
||||
# sw_tune = zerolatency
|
||||
#
|
||||
|
||||
##################################### NVENC #####################################
|
||||
###### presets ###########
|
||||
# default
|
||||
# hp -- high performance
|
||||
# hq -- high quality
|
||||
# slow -- hq 2 passes
|
||||
# medium -- hq 1 pass
|
||||
# fast -- hp 1 pass
|
||||
# bd
|
||||
# ll -- low latency
|
||||
# llhq
|
||||
# llhp
|
||||
# lossless
|
||||
# losslesshp
|
||||
##########################
|
||||
# nv_preset = llhq
|
||||
#
|
||||
####### rate control #####
|
||||
# auto -- let ffmpeg decide rate control
|
||||
# constqp -- constant QP mode
|
||||
# vbr -- variable bitrate
|
||||
# cbr -- constant bitrate
|
||||
# cbr_hq -- cbr high quality
|
||||
# cbr_ld_hq -- cbr low delay high quality
|
||||
# vbr_hq -- vbr high quality
|
||||
##########################
|
||||
# nv_rc = auto
|
||||
|
||||
###### h264 entropy ######
|
||||
# auto -- let ffmpeg nvenc decide the entropy encoding
|
||||
# cabac
|
||||
# cavlc
|
||||
##########################
|
||||
# nv_coder = auto
|
||||
|
||||
##################################### AMD #####################################
|
||||
###### presets ###########
|
||||
# default
|
||||
# speed
|
||||
# balanced
|
||||
##########################
|
||||
# amd_preset = balanced
|
||||
#
|
||||
####### rate control #####
|
||||
# auto -- let ffmpeg decide rate control
|
||||
# constqp -- constant QP mode
|
||||
# vbr_latency -- Latency Constrained Variable Bitrate
|
||||
# vbr_peak -- Peak Contrained Variable Bitrate
|
||||
# cbr -- constant bitrate
|
||||
##########################
|
||||
# amd_rc = auto
|
||||
|
||||
###### h264 entropy ######
|
||||
# auto -- let ffmpeg nvenc decide the entropy encoding
|
||||
# cabac
|
||||
# cavlc
|
||||
##########################
|
||||
# amd_coder = auto
|
||||
|
||||
#################################### VAAPI ###################################
|
||||
####### adapter ##########
|
||||
# Unlike with `amdvce` and `nvenc`, it doesn't matter if video encoding is done
|
||||
# on a different GPU.
|
||||
# Run the following commands:
|
||||
# 1. ls /dev/dri/renderD*
|
||||
# to find all devices capable of VAAPI
|
||||
# 2. vainfo --display drm --device /dev/dri/renderD129 | grep -E "((VAProfileH264High|VAProfileHEVCMain|VAProfileHEVCMain10).*VAEntrypointEncSlice)|Driver version"
|
||||
# Lists the name and capabilities of the device.
|
||||
# To be supported by Sunshine, it needs to have at the very minimum:
|
||||
# VAProfileH264High : VAEntrypointEncSlice
|
||||
# adapter_name = /dev/dri/renderD128
|
||||
|
||||
##############################################
|
||||
# Some configurable parameters, are merely toggles for specific features
|
||||
# The first occurrence turns it on, the second occurence turns it off, the third occurence turns it on again, etc, etc
|
||||
# Here, you change the default state of any flag
|
||||
#
|
||||
# To set the initial state of flags -0 and -1 to on, set the following flags:
|
||||
# flags = 01
|
||||
#
|
||||
# See: sunshine --help for all options under the header: flags
|
||||
|
||||
162
assets/web/apps.html
Normal file
162
assets/web/apps.html
Normal file
@@ -0,0 +1,162 @@
|
||||
<div id="app" class="container">
|
||||
<div class="my-4">
|
||||
<h1>Applications</h1>
|
||||
<div>Applications are refreshed only when Client is restarted</div>
|
||||
</div>
|
||||
<div class="card p-4">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">Name</th>
|
||||
<th scope="col">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="(app,i) in apps" :key="i">
|
||||
<td>{{app.name}}</td>
|
||||
<td><button class="btn btn-primary" @click="editApp(i)">Edit</button>
|
||||
<button class="btn btn-danger" @click="showDeleteForm(i)">Delete</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="edit-form card mt-2" v-if="showEditForm">
|
||||
<div class="p-4">
|
||||
<!--name-->
|
||||
<div class="mb-3">
|
||||
<label for="appName" class="form-label">Application Name</label>
|
||||
<input type="text" class="form-control" id="appName" aria-describedby="appNameHelp" v-model="editForm.name">
|
||||
<div id="appNameHelp" class="form-text">Application Name, as shown on Moonlight</div>
|
||||
</div>
|
||||
<!--output-->
|
||||
<div class="mb-3">
|
||||
<label for="appOutput" class="form-label">Output</label>
|
||||
<input type="text" class="form-control monospace" id="appOutput" aria-describedby="appOutputHelp"
|
||||
v-model="editForm.output">
|
||||
<div id="appOutputHelp" class="form-text">The file where the output of the command is stored, if it is not
|
||||
specified, the output is ignored</div>
|
||||
</div>
|
||||
<!--prep-cmd-->
|
||||
<div class="mb-3 d-flex flex-column">
|
||||
<label for="appName" class="form-label">Command Preparations</label>
|
||||
<div class="form-text">A list of commands to be run before/after the application. <br> If any of the
|
||||
prep-commands fail, starting the application is aborted</div>
|
||||
<table v-if="editForm['prep-cmd'].length > 0">
|
||||
<thead>
|
||||
<th class="precmd-head">Do</th>
|
||||
<th class="precmd-head">Undo</th>
|
||||
<th style="width: 48px;"></th>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="(c,i) in editForm['prep-cmd']">
|
||||
<td><input type="text" class="form-control monospace" v-model="c.do"></td>
|
||||
<td><input type="text" class="form-control monospace" v-model="c.undo"></td>
|
||||
<td><button class="btn btn-danger" @click="editForm['prep-cmd'].splice(i,1)">×</button></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<button class="mt-2 btn btn-success" style="margin: 0 auto;" @click="addPrepCmd">+ Add</button>
|
||||
</div>
|
||||
<!--detatched-->
|
||||
<div class="mb-3">
|
||||
<label for="appName" class="form-label">Detached Commands</label>
|
||||
<div v-for="(c,i) in editForm.detached" class="d-flex justify-content-between my-2">
|
||||
<pre>{{c}}</pre>
|
||||
<button class="btn btn-danger mx-2" @click="editForm.detached.splice(i,1)">×</button>
|
||||
</div>
|
||||
<div class="d-flex justify-content-between">
|
||||
<input type="text" class="form-control monospace" v-model="detachedCmd">
|
||||
<button class="btn btn-success mx-2" @click="editForm.detached.push(detachedCmd);detachedCmd = '';">+</button>
|
||||
</div>
|
||||
<div class="form-text">A list of commands to be run and forgotten about</div>
|
||||
</div>
|
||||
<!--command-->
|
||||
<div class="mb-3">
|
||||
<label for="appCmd" class="form-label">Command</label>
|
||||
<input type="text" class="form-control monospace" id="appCmd" aria-describedby="appCmdHelp"
|
||||
v-model="editForm.cmd">
|
||||
<div id="appCmdHelp" class="form-text">The main application, if it is not specified, a processs is started that
|
||||
sleeps indefinitely</div>
|
||||
</div>
|
||||
<!--buttons-->
|
||||
<div class="d-flex">
|
||||
<button @click="showEditForm = false" class="btn btn-secondary m-2">Cancel</button>
|
||||
<button class="btn btn-primary m-2" @click="save">Save</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-2" v-else>
|
||||
<button class="btn btn-primary" @click="newApp">+ Add New</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
new Vue({
|
||||
el: '#app',
|
||||
data() {
|
||||
return {
|
||||
apps: [],
|
||||
showEditForm: false,
|
||||
editForm: null,
|
||||
detachedCmd: '',
|
||||
}
|
||||
},
|
||||
created() {
|
||||
fetch("/api/apps").then(r => r.json()).then((r) => {
|
||||
console.log(r);
|
||||
this.apps = r.apps;
|
||||
})
|
||||
},
|
||||
methods: {
|
||||
newApp() {
|
||||
this.editForm = {
|
||||
name: '',
|
||||
output: '',
|
||||
cmd: [],
|
||||
index: -1,
|
||||
"prep-cmd": [],
|
||||
"detached": []
|
||||
};
|
||||
this.editForm.index = -1;
|
||||
this.showEditForm = true;
|
||||
},
|
||||
editApp(id) {
|
||||
this.editForm = JSON.parse(JSON.stringify(this.apps[id]));
|
||||
this.$set(this.editForm, "index", id);
|
||||
if (this.editForm["prep-cmd"] === undefined) this.$set(this.editForm, "prep-cmd", []);
|
||||
if (this.editForm["detached"] === undefined) this.$set(this.editForm, "detached", []);
|
||||
this.showEditForm = true;
|
||||
},
|
||||
showDeleteForm(id) {
|
||||
let resp = confirm("Are you sure to delete " + this.apps[id].name + "?");
|
||||
if (resp) {
|
||||
fetch("/api/apps/" + id, { method: "DELETE" }).then((r) => {
|
||||
if (r.status == 200) document.location.reload();
|
||||
});
|
||||
}
|
||||
},
|
||||
addPrepCmd() {
|
||||
this.editForm['prep-cmd'].push({
|
||||
do: '',
|
||||
undo: '',
|
||||
});
|
||||
},
|
||||
save() {
|
||||
fetch("/api/apps", { method: "POST", body: JSON.stringify(this.editForm) }).then((r) => {
|
||||
if (r.status == 200) document.location.reload();
|
||||
});
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.precmd-head {
|
||||
width: 200px;
|
||||
}
|
||||
|
||||
.monospace {
|
||||
font-family: monospace;
|
||||
}
|
||||
</style>
|
||||
3
assets/web/clients.html
Normal file
3
assets/web/clients.html
Normal file
@@ -0,0 +1,3 @@
|
||||
<div id="content" class="container">
|
||||
<h1>Clients</h1>
|
||||
</div>
|
||||
560
assets/web/config.html
Normal file
560
assets/web/config.html
Normal file
@@ -0,0 +1,560 @@
|
||||
<div id="app" class="container">
|
||||
<h1 class="my-4">Configuration</h1>
|
||||
<div class="form" v-if="config">
|
||||
<!--Header-->
|
||||
<ul class="nav nav-tabs">
|
||||
<li class="nav-item" v-for="tab in tabs" :key="tab.id">
|
||||
<a class="nav-link" :class="{'active': tab.id === currentTab}" href="#"
|
||||
@click="currentTab = tab.id">{{tab.name}}</a>
|
||||
</li>
|
||||
</ul>
|
||||
<!--General Tab-->
|
||||
<div v-if="currentTab === 'general'" class="config-page">
|
||||
<!--Sunshine Name-->
|
||||
<div class="mb-3">
|
||||
<label for="sunshine_name" class="form-label">Sunshine Name</label>
|
||||
<input type="text" class="form-control" id="sunshine_name" placeholder="Sunshine"
|
||||
v-model="config.sunshine_name">
|
||||
<div class="form-text">The name displayed by Moonlight. If not specified, the PC's hostname is used
|
||||
</div>
|
||||
</div>
|
||||
<!--Log Level-->
|
||||
<div class="mb-3">
|
||||
<label for="min_log_level" class="form-label">Log Level</label>
|
||||
<select id="min_log_level" class="form-select" v-model="config.min_log_level">
|
||||
<option :value="0">Verbose</option>
|
||||
<option :value="1">Debug</option>
|
||||
<option :value="2">Info</option>
|
||||
<option :value="3">Warning</option>
|
||||
<option :value="4">Error</option>
|
||||
<option :value="5">Fatal</option>
|
||||
<option :value="6">None</option>
|
||||
</select>
|
||||
<div class="form-text">The minimum log level printed to standard out</div>
|
||||
</div>
|
||||
<!--Origin PIN Allowed-->
|
||||
<div class="mb-3">
|
||||
<label for="origin_pin_allowed" class="form-label">Origin PIN Allowed</label>
|
||||
<select id="origin_pin_allowed" class="form-select" v-model="config.origin_pin_allowed">
|
||||
<option value="pc">Only localhost may access /pin and Web UI</option>
|
||||
<option value="lan">Only those in LAN may access /pin and Web UI</option>
|
||||
<option value="wan">Anyone may access /pin and Web UI</option>
|
||||
</select>
|
||||
<div class="form-text">The origin of the remote endpoint address that is not denied for HTTP method /pin
|
||||
</div>
|
||||
</div>
|
||||
<!--External IP-->
|
||||
<div class="mb-3">
|
||||
<label for="external_ip" class="form-label">External IP</label>
|
||||
<input type="text" class="form-control" id="external_ip" placeholder="123.456.789.12"
|
||||
v-model="config.external_ip">
|
||||
<div class="form-text">If no external IP address is given, the local IP address is used</div>
|
||||
</div>
|
||||
<!--Ping Timeout-->
|
||||
<div class="mb-3">
|
||||
<label for="ping_timeout" class="form-label">Ping Timeout</label>
|
||||
<input type="text" class="form-control" id="ping_timeout" placeholder="2000"
|
||||
v-model="config.ping_timeout">
|
||||
<div class="form-text">How long to wait in milliseconds for data from moonlight before shutting down the
|
||||
stream</div>
|
||||
</div>
|
||||
<!--Advertised FPS and Resolutions-->
|
||||
<div class="mb-3">
|
||||
<label for="ping_timeout" class="form-label">Advertised Resolutions and FPS</label>
|
||||
<div class="resolutions-container">
|
||||
<label>Resolutions</label>
|
||||
<div class="resolutions d-flex flex-wrap">
|
||||
<div class="p-2 ms-item m-2 d-flex justify-content-between" v-for="(r,i) in resolutions"
|
||||
:key="r">
|
||||
<span class="px-2">{{r}}</span>
|
||||
<span style="cursor: pointer;" @click="resolutions.splice(i,1)">×</span>
|
||||
</div>
|
||||
<form @submit.prevent="resolutions.push(resIn);resIn = '';" class="d-flex align-items-center">
|
||||
<input type="text" v-model="resIn" required pattern="[0-9]+x[0-9]+"
|
||||
style="border-top-right-radius: 0;border-bottom-right-radius: 0;" class="form-control">
|
||||
<button style="border-top-left-radius: 0;border-bottom-left-radius: 0;"
|
||||
class="btn btn-success">+</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="fps-container">
|
||||
<label>FPS</label>
|
||||
<div class="fps d-flex flex-wrap">
|
||||
<div class="p-2 ms-item m-2 d-flex justify-content-between" v-for="(f,i) in fps" :key="f">
|
||||
<span class="px-2">{{f}}</span>
|
||||
<span style="cursor: pointer;" @click="fps.splice(i,1)">×</span>
|
||||
</div>
|
||||
<form @submit.prevent="fps.push(fpsIn);fpsIn = '';" class="d-flex align-items-center">
|
||||
<input type="text" v-model="fpsIn" required pattern="[0-9]+"
|
||||
style="width: 6ch;border-top-right-radius: 0;border-bottom-right-radius: 0;"
|
||||
class="form-control">
|
||||
<button style="border-top-left-radius: 0;border-bottom-left-radius: 0;"
|
||||
class="btn btn-success">+</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-text">
|
||||
The display modes advertised by Sunshine<br>
|
||||
Some versions of Moonlight, such as Moonlight-nx (Switch),
|
||||
rely on this list to ensure that the requested resolutions and fps
|
||||
are supported.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!--Files Tab-->
|
||||
<div v-if="currentTab === 'files'" class="config-page">
|
||||
<!--Private Key-->
|
||||
<div class="mb-3">
|
||||
<label for="pkey" class="form-label">Private Key</label>
|
||||
<input type="text" class="form-control" id="pkey" placeholder="/dir/pkey.pem" v-model="config.pkey">
|
||||
<div class="form-text">The private key must be 2048 bits</div>
|
||||
</div>
|
||||
<!--Cert-->
|
||||
<div class="mb-3">
|
||||
<label for="cert" class="form-label">Cert</label>
|
||||
<input type="text" class="form-control" id="cert" placeholder="/dir/cert.pem" v-model="config.cert">
|
||||
<div class="form-text">The certificate must be signed with a 2048 bit key</div>
|
||||
</div>
|
||||
|
||||
<!--State File-->
|
||||
<div class="mb-3">
|
||||
<label for="file_state" class="form-label">State File</label>
|
||||
<input type="text" class="form-control" id="file_state" placeholder="sunshine_state.json"
|
||||
v-model="config.file_state">
|
||||
<div class="form-text">The file where current state of Sunshine is stored</div>
|
||||
</div>
|
||||
<!--Apps File-->
|
||||
<div class="mb-3">
|
||||
<label for="file_apps" class="form-label">Apps File</label>
|
||||
<input type="text" class="form-control" id="file_apps" placeholder="apps.json"
|
||||
v-model="config.file_apps">
|
||||
<div class="form-text">The file where current apps of Sunshine are stored</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="currentTab === 'input'" class="config-page">
|
||||
<!--Back Button Timeout-->
|
||||
<div class="mb-3">
|
||||
<label for="back_button_timeout" class="form-label">Back Button Timeout</label>
|
||||
<input type="text" class="form-control" id="back_button_timeout" placeholder="2000"
|
||||
v-model="config.back_button_timeout">
|
||||
<div class="form-text">
|
||||
The back/select button on the controller.<br>
|
||||
On the Shield, the home and powerbutton are not passed to Moonlight.<br>
|
||||
If, after the timeout, the back button is still pressed down, Home/Guide button press is
|
||||
emulated.<br>
|
||||
If back_button_timeout < 0, then the Home/Guide button will not be emulated<br>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Key Repeat Delay-->
|
||||
<div class="mb-3" v-if="platform === 'windows'">
|
||||
<label for="key_repeat_delay" class="form-label">Key Repeat Delay</label>
|
||||
<input type="text" class="form-control" id="key_repeat_delay" placeholder="500"
|
||||
v-model="config.key_repeat_delay">
|
||||
<div class="form-text">
|
||||
Control how fast keys will repeat themselves<br>
|
||||
The initial delay in milliseconds before repeating keys
|
||||
</div>
|
||||
</div>
|
||||
<!-- Key Repeat Frequency-->
|
||||
<div class="mb-3" v-if="platform === 'windows'">
|
||||
<label for="key_repeat_frequency" class="form-label">Key Repeat Frequency</label>
|
||||
<input type="text" class="form-control" id="key_repeat_frequency" placeholder="24.9"
|
||||
v-model="config.key_repeat_frequency">
|
||||
<div class="form-text">
|
||||
How often keys repeat every second<br>
|
||||
This configurable option supports decimals
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!--Files Tab-->
|
||||
<div v-if="currentTab === 'av'" class="config-page">
|
||||
<!--Audio Sink-->
|
||||
<div class="mb-3" v-if="platform === 'windows'">
|
||||
<label for="audio_sink" class="form-label">Audio Sink</label>
|
||||
<input type="text" class="form-control" id="audio_sink"
|
||||
placeholder="{0.0.0.00000000}.{FD47D9CC-4218-4135-9CE2-0C195C87405B}" v-model="config.audio_sink">
|
||||
<div class="form-text">
|
||||
The name of the audio sink used for Audio Loopback<br>
|
||||
You can find the name of the audio sink using the following command:<br>
|
||||
<pre>tools\audio-info.exe</pre>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3" v-if="platform === 'linux'">
|
||||
<label for="audio_sink" class="form-label">Audio Sink</label>
|
||||
<input type="text" class="form-control" id="audio_sink"
|
||||
placeholder="alsa_output.pci-0000_09_00.3.analog-stereo" v-model="config.audio_sink">
|
||||
<div class="form-text">
|
||||
The name of the audio sink used for Audio Loopback<br>
|
||||
If you do not specify this variable, pulseaudio will select the default monitor device.<br>
|
||||
<br>
|
||||
You can find the name of the audio sink using either command:<br>
|
||||
<pre>pacmd list-sinks | grep "name:"</pre>
|
||||
<pre>pactl info | grep Source</pre><br>
|
||||
</div>
|
||||
</div>
|
||||
<!--Virtual Sink-->
|
||||
<div class="mb-3" v-if="platform === 'windows'">
|
||||
<label for="virtual_sink" class="form-label">Virtual Sink</label>
|
||||
<input type="text" class="form-control" id="virtual_sink"
|
||||
placeholder="{0.0.0.00000000}.{8edba70c-1125-467c-b89c-15da389bc1d4}" v-model="config.virtual_sink">
|
||||
<div class="form-text">
|
||||
The virtual sink, is the audio device that's virtual (Like Steam Streaming Speakers), it allows
|
||||
Sunshine
|
||||
to stream audio, while muting the speakers.
|
||||
</div>
|
||||
</div>
|
||||
<!--Adapter Name -->
|
||||
<div class="mb-3" v-if="platform === 'windows'">
|
||||
<label for="adapter_name" class="form-label">Adapter Name</label>
|
||||
<input type="text" class="form-control" id="adapter_name" placeholder="Radeon RX 580 Series"
|
||||
v-model="config.adapter_name">
|
||||
<div class="form-text" v-if="platform === 'windows'">
|
||||
You can select the video card you want to stream:<br>
|
||||
The appropriate values can be found using the following command:<br>
|
||||
<pre>tools\dxgi-info.exe</pre>
|
||||
</div>
|
||||
</div>
|
||||
<!--Output Name -->
|
||||
<div class="mb-3" class="config-page" v-if="platform === 'windows'">
|
||||
<label for="output_name" class="form-label">Output Name</label>
|
||||
<input type="text" class="form-control" id="output_name" placeholder="\\.\DISPLAY1"
|
||||
v-model="config.output_name">
|
||||
<div class="form-text">
|
||||
You can select the video card you want to stream:<br>
|
||||
The appropriate values can be found using the following command:<br>
|
||||
tools\dxgi-info.exe<br><br>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3" class="config-page" v-if="platform === 'linux'">
|
||||
<label for="output_name" class="form-label">Monitor number</label>
|
||||
<input type="text" class="form-control" id="output_name" placeholder="0" v-model="config.output_name">
|
||||
<div class="form-text">
|
||||
xrandr --listmonitors<br>
|
||||
Example output:
|
||||
<pre> 0: +HDMI-1 1920/518x1200/324+0+0 HDMI-1</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="currentTab === 'advanced'" class="config-page">
|
||||
<!--Constant Rate Factor-->
|
||||
<div class="mb-3">
|
||||
<label for="crf" class="form-label">Constant Rate Factor</label>
|
||||
<input type="number" min="0" max="52" class="form-control" id="crf" placeholder="0"
|
||||
v-model="config.crf">
|
||||
<div class="form-text">
|
||||
Constant Rate Factor. Between 1 and 52. It allows QP to go up during motion and down with still
|
||||
image,
|
||||
resulting in constant perceived quality<br>
|
||||
Higher value means more compression, but less quality<br>
|
||||
If crf == 0, then use QP directly instead
|
||||
</div>
|
||||
</div>
|
||||
<!-- Quantization Parameter -->
|
||||
<div class="mb-3">
|
||||
<label for="qp" class="form-label">Quantitization Parameter</label>
|
||||
<input type="number" class="form-control" id="qp" placeholder="28" v-model="config.qp">
|
||||
<div class="form-text">
|
||||
Quantitization Parameter<br>
|
||||
Higher value means more compression, but less quality<br>
|
||||
If crf != 0, then this parameter is ignored
|
||||
</div>
|
||||
</div>
|
||||
<!-- Min Threads -->
|
||||
<div class="mb-3">
|
||||
<label for="min_threads" class="form-label">Minimum number of threads used by ffmpeg to encode the
|
||||
video.</label>
|
||||
<input type="number" min="1" class="form-control" id="min_threads" placeholder="1"
|
||||
v-model="config.min_threads">
|
||||
<div class="form-text">
|
||||
Minimum number of threads used by ffmpeg to encode the video.<br>
|
||||
Increasing the value slightly reduces encoding efficiency, but the tradeoff is usually<br>
|
||||
worth it to gain the use of more CPU cores for encoding. The ideal value is the lowest<br>
|
||||
value that can reliably encode at your desired streaming settings on your hardware.
|
||||
</div>
|
||||
</div>
|
||||
<!--HEVC Suppport -->
|
||||
<div class="mb-3">
|
||||
<label for="hevc_mode" class="form-label">HEVC Support</label>
|
||||
<select id="hevc_mode" class="form-select" v-model="config.hevc_mode">
|
||||
<option value="0">Sunshine will specify support for HEVC based on encoder</option>
|
||||
<option value="1">Sunshine will not advertise support for HEVC</option>
|
||||
<option value="2">Sunshine will advertise support for HEVC Main profile</option>
|
||||
<option value="3">Sunshine will advertise support for HEVC Main and Main10 (HDR) profiles
|
||||
</option>
|
||||
</select>
|
||||
<div class="form-text">
|
||||
Allows the client to request HEVC Main or HEVC Main10 video streams.<br>
|
||||
HEVC is more CPU-intensive to encode, so enabling this may reduce performance when using
|
||||
software
|
||||
encoding.
|
||||
</div>
|
||||
</div>
|
||||
<!--Encoder -->
|
||||
<div class="mb-3">
|
||||
<label for="encoder" class="form-label">Force a Specific Encoder</label>
|
||||
<select id="encoder" class="form-select" v-model="config.encoder">
|
||||
<option :value="''">Autodetect</option>
|
||||
<option value="nvenc">nVidia NVENC</option>
|
||||
<option value="amdvce">AMD AMF/VCE</option>
|
||||
<option value="vaapi">VA-API</option>
|
||||
<option value="software">Software</option>
|
||||
</select>
|
||||
<div class="form-text">
|
||||
Force a specific encoder, otherwise Sunshine will use the first encoder that is available
|
||||
</div>
|
||||
</div>
|
||||
<!--FEC Percentage-->
|
||||
<div class="mb-3">
|
||||
<label for="fec_percentage" class="form-label">FEC Percentage</label>
|
||||
<input type="text" class="form-control" id="fec_percentage" placeholder="10"
|
||||
v-model="config.fec_percentage">
|
||||
<div class="form-text">
|
||||
How much error correcting packets must be send for every video.<br>
|
||||
This is just some random number, don't know the optimal value.<br>
|
||||
The higher fec_percentage, the lower space for the actual data to send per frame there is
|
||||
</div>
|
||||
</div>
|
||||
<!--Channels-->
|
||||
<div class="mb-3">
|
||||
<label for="channels" class="form-label">Channels</label>
|
||||
<input type="text" class="form-control" id="channels" placeholder="1" v-model="config.channels">
|
||||
<div class="form-text">
|
||||
When multicasting, it could be useful to have different configurations for each connected
|
||||
Client.
|
||||
For example:
|
||||
<ul>
|
||||
<li>Clients connected through WAN and LAN have different bitrate contstraints.</li>
|
||||
<li>Decoders may require different settings for color</li>
|
||||
</ul>
|
||||
Unlike simply broadcasting to multiple Client, this will generate distinct video streams.<br>
|
||||
Note, CPU usage increases for each distinct video stream generated
|
||||
</div>
|
||||
</div>
|
||||
<!--Credentials File-->
|
||||
<div class="mb-3">
|
||||
<label for="credentials_file" class="form-label">Web Manager Credentials File</label>
|
||||
<input type="text" class="form-control" id="credentials_file" placeholder="sunshine_state.json"
|
||||
v-model="config.credentials_file">
|
||||
<div class="form-text">
|
||||
Store Username/Password seperately from Sunshine's state file.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!--Software Settings-->
|
||||
<div v-if="currentTab === 'sw'" class="config-page">
|
||||
<div class="mb-3">
|
||||
<label for="sw_preset" class="form-label">SW Presets</label>
|
||||
<input class="form-control" id="sw_preset" placeholder="superfast" v-model="config.sw_preset">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="sw_tune" class="form-label">SW Tune</label>
|
||||
<input class="form-control" id="sw_tune" placeholder="zerolatency" v-model="config.sw_tune">
|
||||
</div>
|
||||
</div>
|
||||
<!--Nvidia Encoder Settings-->
|
||||
<div v-if="currentTab === 'nv'" class="config-page">
|
||||
<!--NVENC SETTINGS-->
|
||||
<div class="mb-3">
|
||||
<label for="nv_preset" class="form-label">NVEnc Preset</label>
|
||||
<select id="nv_preset" class="form-select" v-model="config.nv_preset">
|
||||
<option value="default">Default</option>
|
||||
<option value="hp">High Performance</option>
|
||||
<option value="hq">High Quality</option>
|
||||
<option value="slow">Slow - hq 2 passes</option>
|
||||
<option value="medium">medium -- hq 1 pass</option>
|
||||
<option value="fast">fast -- hp 1 pass</option>
|
||||
<option value="bd">bd</option>
|
||||
<option value="ll">ll -- low latency</option>
|
||||
<option value="llhq">llhq</option>
|
||||
<option value="llhp">llhp</option>
|
||||
<option value="lossless">lossless</option>
|
||||
<option value="losslesshp">losslesshp</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="nv_rc" class="form-label">NVEnc Rate Control</label>
|
||||
<select id="nv_rc" class="form-select" v-model="config.nv_rc">
|
||||
<option value="auto">auto -- let ffmpeg decide rate control</option>
|
||||
<option value="constqp">constqp -- constant QP mode</option>
|
||||
<option value="vbr">vbr -- variable bitrate</option>
|
||||
<option value="cbr">cbr -- constant bitrate</option>
|
||||
<option value="cbr_hq">cbr_hq -- cbr high quality</option>
|
||||
<option value="cbr_ld_hq">cbr_ld_hq -- cbr low delay high quality</option>
|
||||
<option value="vbr_hq">vbr_hq -- vbr high quality</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="nv_coder" class="form-label">NVEnc Coder</label>
|
||||
<select id="nv_coder" class="form-select" v-model="config.nv_coder">
|
||||
<option value="auto">auto</option>
|
||||
<option value="cabac">cabac</option>
|
||||
<option value="cavlc">cavlc</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<!--AMD Encoder Settings-->
|
||||
<div v-if="currentTab === 'amd'" class="config-page">
|
||||
<!--Presets-->
|
||||
<div class="mb-3">
|
||||
<label for="amd_quality" class="form-label">AMD AMF Quality</label>
|
||||
<select id="amd_quality" class="form-select" v-model="config.amd_quality">
|
||||
<option value="default">Default</option>
|
||||
<option value="speed">Speed</option>
|
||||
<option value="balanced">Balanced</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="amd_rc" class="form-label">AMD AMF Rate Control</label>
|
||||
<select id="amd_rc" class="form-select">
|
||||
<option value="auto">auto -- let ffmpeg decide rate control</option>
|
||||
<option value="constqp">constqp -- constant QP mode</option>
|
||||
<option value="vbr_latency">vbr_latency -- Latency Constrained Variable Bitrate</option>
|
||||
<option value="vbr_peak">vbr_peak -- Peak Contrained Variable Bitrate</option>
|
||||
<option value="cbr">cbr -- constant bitrate</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="amd_coder" class="form-label">AMD AMF Rate Control</label>
|
||||
<select id="amd_coder" class="form-select" v-model="config.amd_coder">
|
||||
<option value="auto">auto</option>
|
||||
<option value="cabac">cabac</option>
|
||||
<option value="cavlc">cavlc</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="currentTab === 'va-api'" class="config-page">
|
||||
<input class="form-control" id="adapter_name" placeholder="/dev/dri/renderD128"
|
||||
v-model="config.adapter_name">
|
||||
</div>
|
||||
</div>
|
||||
<div class="alert alert-success my-4" v-if="success"><b>Success!</b> Restart Sunshine to apply changes</div>
|
||||
<div class="mb-3 buttons">
|
||||
<button class="btn btn-primary" @click="save">Save</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
new Vue({
|
||||
el: '#app',
|
||||
data() {
|
||||
return {
|
||||
platform: '',
|
||||
success: false,
|
||||
config: null,
|
||||
fps: [],
|
||||
resolutions: [],
|
||||
currentTab: 'general',
|
||||
resIn: '',
|
||||
fpsIn: '',
|
||||
tabs: [{
|
||||
id: 'general',
|
||||
name: "General"
|
||||
},
|
||||
{
|
||||
id: 'files',
|
||||
name: "Files"
|
||||
},
|
||||
{
|
||||
id: 'input',
|
||||
name: "Input"
|
||||
},
|
||||
{
|
||||
id: 'av',
|
||||
name: "Audio/Video"
|
||||
},
|
||||
{
|
||||
id: 'advanced',
|
||||
name: "Advanced"
|
||||
},
|
||||
{
|
||||
id: "sw",
|
||||
name: "Software Encoder"
|
||||
},
|
||||
{
|
||||
id: "nv",
|
||||
name: "NVENC Encoder"
|
||||
},
|
||||
{
|
||||
id: "amd",
|
||||
name: "AMF Encoder"
|
||||
},
|
||||
{
|
||||
id: "va-api",
|
||||
name: "VA-API encoder"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
created() {
|
||||
fetch("/api/config").then(r => r.json()).then((r) => {
|
||||
this.config = r;
|
||||
this.platform = this.config.platform;
|
||||
|
||||
var app = document.getElementById("app");
|
||||
if (this.platform == "windows") {
|
||||
this.tabs = this.tabs.filter(el => {
|
||||
return el.id !== "va-api";
|
||||
});
|
||||
}
|
||||
if (this.platform == "linux") {
|
||||
this.tabs = this.tabs.filter(el => {
|
||||
return el.id !== "nv" && el.id !== "amd";
|
||||
});
|
||||
}
|
||||
|
||||
delete this.config.status;
|
||||
delete this.config.platform;
|
||||
//Populate default values if not present in config
|
||||
this.config.min_log_level = this.config.min_log_level || 2;
|
||||
this.config.origin_pin_allowed = this.config.origin_pin_allowed || "lan";
|
||||
this.config.hevc_mode = this.config.hevc_mode || 0;
|
||||
this.config.encoder = this.config.encoder || '';
|
||||
this.config.nv_preset = this.config.nv_preset || 'default';
|
||||
this.config.nv_rc = this.config.nv_rc || 'auto';
|
||||
this.config.nv_coder = this.config.nv_coder || 'auto';
|
||||
this.config.amd_quality = this.config.amd_quality || 'default';
|
||||
this.config.amd_rc = this.config.amd_rc || 'auto';
|
||||
this.config.fps = this.config.fps || '[10, 30, 60, 90, 120]';
|
||||
this.config.resolutions = this.config.resolutions || '[352x240,480x360,858x480,1280x720,1920x1080,2560x1080,3440x1440,1920x1200,3860x2160,3840x1600]';
|
||||
this.fps = JSON.parse(this.config.fps);
|
||||
//Resolutions should be fixed because are not valid JSON
|
||||
let res = this.config.resolutions.substring(1, this.config.resolutions.length - 1);
|
||||
let resolutions = [];
|
||||
res.split(",").forEach(r => resolutions.push(r.trim()));
|
||||
this.resolutions = resolutions;
|
||||
})
|
||||
},
|
||||
methods: {
|
||||
save() {
|
||||
this.success = false;
|
||||
let nl = this.config === 'windows' ? "\r\n" : "\n";
|
||||
this.config.resolutions = "[" + nl + " " + this.resolutions.join("," + nl + " ") + nl + "]";
|
||||
this.config.fps = JSON.stringify(this.fps);
|
||||
fetch("/api/config", {
|
||||
method: "POST",
|
||||
body: JSON.stringify(this.config)
|
||||
}).then((r) => {
|
||||
if (r.status == 200) this.success = true;
|
||||
});
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.config-page {
|
||||
padding: 1em;
|
||||
border: 1px solid #dee2e6;
|
||||
border-top: none;
|
||||
}
|
||||
|
||||
.buttons {
|
||||
padding: 1em 0;
|
||||
}
|
||||
|
||||
.ms-item {
|
||||
background-color: #CCC;
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
}
|
||||
</style>
|
||||
45
assets/web/header.html
Normal file
45
assets/web/header.html
Normal file
@@ -0,0 +1,45 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Sunshine</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.0/dist/css/bootstrap.min.css" rel="stylesheet"
|
||||
integrity="sha384-wEmeIV1mKuiNpC+IOBjI7aAzPcEZeedi5yW5f2yOq55WWLwNGmvvx4Um1vskeMj0" crossorigin="anonymous">
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.0/dist/js/bootstrap.bundle.min.js"
|
||||
integrity="sha384-p34f1UUtsS3wqzfto5wAAmdvj+osOnFyQFpp4Ua3gs/ZVWx6oOypYoCJhGGScy+8" crossorigin="anonymous">
|
||||
</script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/vue@2.6.12/dist/vue.js"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<nav class="navbar navbar-expand-lg navbar-light" style="background-color: #ffc400;">
|
||||
<div class="container-fluid">
|
||||
<span class="navbar-brand">Sunshine</span>
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse"
|
||||
data-bs-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false"
|
||||
aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<div class="collapse navbar-collapse" id="navbarSupportedContent">
|
||||
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/">Home</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/pin">PIN</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/apps">Applications</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/config">Configuration</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/password">Change Password</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
||||
9
assets/web/index.html
Normal file
9
assets/web/index.html
Normal file
@@ -0,0 +1,9 @@
|
||||
<div id="content" class="container">
|
||||
<div class="row">
|
||||
<div class="col-md-6 py-4" style="margin: 0 auto;">
|
||||
<h1>Hello, Sunshine!</h1>
|
||||
<p>Sunshine is a Gamestream host for Moonlight</p>
|
||||
<a href="https://github.com/loki-47-6F-64/sunshine">Official GitHub Repository</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
97
assets/web/password.html
Normal file
97
assets/web/password.html
Normal file
@@ -0,0 +1,97 @@
|
||||
<div id="app" class="container">
|
||||
<h1 class="my-4">Password Change</h1>
|
||||
<form @submit.prevent="save">
|
||||
<div class="card d-flex p-4 flex-row">
|
||||
<div class="col-md-6 px-4">
|
||||
<h4>Current Credentials</h4>
|
||||
<div class="mb-3">
|
||||
<label for="currentUsername" class="form-label">Username</label>
|
||||
<input required type="text" class="form-control" id="currentUsername" v-model="passwordData.currentUsername">
|
||||
<div class="form-text"> </div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="currentPassword" class="form-label">Password</label>
|
||||
<input autocomplete="current-password" type="password" class="form-control" id="currentPassword" v-model="passwordData.currentPassword">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6 px-4">
|
||||
<h4>New Credentials</h4>
|
||||
<div class="mb-3">
|
||||
<label for="newUsername" class="form-label">New Username</label>
|
||||
<input type="text" class="form-control" id="newUsername" v-model="passwordData.newUsername">
|
||||
<div class="form-text">If not specified, the username will not change
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="newPassword" class="form-label">Password</label>
|
||||
<input autocomplete="new-password" required type="password" class="form-control" id="newPassword" v-model="passwordData.newPassword">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="confirmNewPassword" class="form-label">Confirm Password</label>
|
||||
<input autocomplete="new-password" required type="password" class="form-control" id="confirmNewPassword" v-model="passwordData.confirmNewPassword">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="alert alert-danger" v-if="error"><b>Error: </b>{{error}}</div>
|
||||
<div class="alert alert-success" v-if="success"><b>Success! </b>This page will reload soon, your browser will ask you for the new credentials</div>
|
||||
<div class="mb-3 buttons">
|
||||
<button class="btn btn-primary">Save</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
new Vue({
|
||||
el: '#app',
|
||||
data() {
|
||||
return {
|
||||
error: null,
|
||||
success: false,
|
||||
passwordData: {
|
||||
currentUsername: '',
|
||||
currentPassword: '',
|
||||
newUsername: '',
|
||||
newPassword: '',
|
||||
confirmNewPassword: ''
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
save() {
|
||||
this.error = null;
|
||||
fetch("/api/password", {
|
||||
method: "POST",
|
||||
body: JSON.stringify(this.passwordData)
|
||||
}).then((r) => {
|
||||
if (r.status == 200){
|
||||
r.json().then((rj) => {
|
||||
if(rj.status.toString() === "true"){
|
||||
this.success = true;
|
||||
setTimeout(()=>{
|
||||
document.location.reload();
|
||||
},5000);
|
||||
} else {
|
||||
this.error = rj.error;
|
||||
}
|
||||
})
|
||||
}
|
||||
else {
|
||||
this.error = "Internal Server Error"
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.config-page {
|
||||
padding: 1em;
|
||||
border: 1px solid #dee2e6;
|
||||
border-top: none;
|
||||
}
|
||||
|
||||
.buttons {
|
||||
padding: 1em 0;
|
||||
}
|
||||
</style>
|
||||
31
assets/web/pin.html
Normal file
31
assets/web/pin.html
Normal file
@@ -0,0 +1,31 @@
|
||||
|
||||
<div id="content" class="container">
|
||||
<h1 class="my-4">PIN Pairing</h1>
|
||||
<form action="" class="form d-flex flex-column align-items-center" id="form">
|
||||
<div class="card flex-column d-flex p-4 mb-4">
|
||||
<input type="number" placeholder="PIN" id="pin-input" class="form-control my-4">
|
||||
<button class="btn btn-primary">Send</button>
|
||||
</div>
|
||||
<div class="alert alert-warning">
|
||||
<b>Warning!</b> Make sure you have access to the client you are pairing with.<br>
|
||||
This software can give total control to your computer, so be careful!
|
||||
</div>
|
||||
<div id="status"></div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.querySelector("#form").addEventListener("submit", (e) => {
|
||||
e.preventDefault();
|
||||
let pin = document.querySelector("#pin-input").value;
|
||||
document.querySelector("#status").innerHTML = "";
|
||||
let b = JSON.stringify({pin: pin});
|
||||
fetch("/api/pin",{method: "POST",body: b}).then((response) => response.json()).then((response)=>{
|
||||
if(response.status){
|
||||
document.querySelector("#status").innerHTML = `<div class="alert alert-success" role="alert">Success! Please check Moonlight to continue</div>`;
|
||||
} else {
|
||||
document.querySelector("#status").innerHTML = `<div class="alert alert-danger" role="alert">PIN does not match, please check if it's typed correctly</div>`;
|
||||
}
|
||||
})
|
||||
})
|
||||
</script>
|
||||
23
gen-deb.in
23
gen-deb.in
@@ -35,8 +35,8 @@ Package: sunshine
|
||||
Architecture: amd64
|
||||
Maintainer: @loki
|
||||
Priority: optional
|
||||
Version: 0.1.1
|
||||
Depends: libssl | libavdevice | libboost-thread (>= 1.67) | libboost-filesystem (>= 1.67) | libboost-log (>= 1.67) | libpulse | libopus | libxtst | libx11 | libxfixes | libevdev | libxcb1 | libxcb-shm0 | libxcb-xfixes0
|
||||
Version: 0.7.3
|
||||
Depends: libssl1.1, libavdevice58, libboost-thread1.71.0, libboost-filesystem1.71.0, libboost-log1.71.0, libpulse0, libopus0, libxcb-shm0, libxcb-xfixes0
|
||||
Description: Gamestream host for Moonlight
|
||||
EOF
|
||||
|
||||
@@ -55,22 +55,35 @@ else
|
||||
echo "Warning: /etc/group not found"
|
||||
fi
|
||||
|
||||
# Prevent necessity of rebooting system
|
||||
chmod 0660 /dev/uinput
|
||||
chown root:$GROUP_INPUT /dev/uinput
|
||||
# Update permissions on config files for Web Manager
|
||||
if [ -f /etc/sunshine/apps_linux.json ]; then
|
||||
echo "chmod 666 /etc/sunshine/apps_linux.json"
|
||||
chmod 666 /etc/sunshine/apps_linux.json
|
||||
fi
|
||||
|
||||
if [ -f /etc/sunshine/sunshine.conf ]; then
|
||||
echo "chmod 666 /etc/sunshine/sunshine.conf"
|
||||
chmod 666 /etc/sunshine/sunshine.conf
|
||||
fi
|
||||
EOF
|
||||
|
||||
cat << 'EOF' > $RULES/85-sunshine-rules.rules
|
||||
KERNEL=="uinput", GROUP="input", MODE="0660"
|
||||
EOF
|
||||
|
||||
mkdir -p $ASSETS/shaders
|
||||
|
||||
cp sunshine $BIN/sunshine
|
||||
cp @CMAKE_CURRENT_SOURCE_DIR@/assets/apps_linux.json $ASSETS/apps_linux.json
|
||||
cp @CMAKE_CURRENT_SOURCE_DIR@/assets/sunshine.conf $ASSETS/sunshine.conf
|
||||
cp -r @CMAKE_CURRENT_SOURCE_DIR@/assets/web $ASSETS/web
|
||||
cp -r @CMAKE_CURRENT_SOURCE_DIR@/assets/shaders/opengl $ASSETS/shaders/opengl
|
||||
|
||||
chmod 755 $DEBIAN/postinst
|
||||
chmod 755 $BIN/sunshine
|
||||
chmod 644 $RULES/85-sunshine-rules.rules
|
||||
chmod 666 $ASSETS/apps_linux.json
|
||||
chmod 666 $ASSETS/sunshine.conf
|
||||
|
||||
cd package-deb
|
||||
if fakeroot dpkg-deb --build sunshine; then
|
||||
|
||||
Submodule moonlight-common-c deleted from cfeb0ffd90
Submodule pre-compiled deleted from 51f776dbd4
@@ -2,12 +2,7 @@
|
||||
Description=Sunshine Gamestream Server for Moonlight
|
||||
|
||||
[Service]
|
||||
WorkingDirectory=/home/%u
|
||||
Environment="DISPLAY=:0"
|
||||
Type=simple
|
||||
# wait for Xorg
|
||||
ExecStartPre=/bin/sh -c 'while ! pgrep Xorg; do sleep 2; done'
|
||||
ExecStart=@SUNSHINE_EXECUTABLE_PATH@
|
||||
|
||||
[Install]
|
||||
WantedBy=default.target
|
||||
WantedBy=graphical-session.target
|
||||
|
||||
@@ -4,129 +4,237 @@
|
||||
|
||||
#include "platform/common.h"
|
||||
|
||||
#include "utility.h"
|
||||
#include "thread_safe.h"
|
||||
#include "audio.h"
|
||||
#include "config.h"
|
||||
#include "main.h"
|
||||
#include "thread_safe.h"
|
||||
#include "utility.h"
|
||||
|
||||
namespace audio {
|
||||
using namespace std::literals;
|
||||
using opus_t = util::safe_ptr<OpusMSEncoder, opus_multistream_encoder_destroy>;
|
||||
using opus_t = util::safe_ptr<OpusMSEncoder, opus_multistream_encoder_destroy>;
|
||||
using sample_queue_t = std::shared_ptr<safe::queue_t<std::vector<std::int16_t>>>;
|
||||
|
||||
struct opus_stream_config_t {
|
||||
std::int32_t sampleRate;
|
||||
int channelCount;
|
||||
int streams;
|
||||
int coupledStreams;
|
||||
const std::uint8_t *mapping;
|
||||
struct audio_ctx_t {
|
||||
// We want to change the sink for the first stream only
|
||||
std::unique_ptr<std::atomic_bool> sink_flag;
|
||||
|
||||
std::unique_ptr<platf::audio_control_t> control;
|
||||
|
||||
bool restore_sink;
|
||||
platf::sink_t sink;
|
||||
};
|
||||
|
||||
constexpr std::uint8_t map_stereo[] { 0, 1 };
|
||||
constexpr std::uint8_t map_surround51[] {0, 4, 1, 5, 2, 3};
|
||||
constexpr std::uint8_t map_high_surround51[] {0, 1, 2, 3, 4, 5};
|
||||
static int start_audio_control(audio_ctx_t &ctx);
|
||||
static void stop_audio_control(audio_ctx_t &);
|
||||
|
||||
int map_stream(int channels, bool quality);
|
||||
|
||||
constexpr auto SAMPLE_RATE = 48000;
|
||||
static opus_stream_config_t stereo = {
|
||||
|
||||
opus_stream_config_t stream_configs[MAX_STREAM_CONFIG] {
|
||||
{
|
||||
SAMPLE_RATE,
|
||||
2,
|
||||
1,
|
||||
1,
|
||||
map_stereo
|
||||
};
|
||||
|
||||
static opus_stream_config_t Surround51 = {
|
||||
platf::speaker::map_stereo,
|
||||
},
|
||||
{
|
||||
SAMPLE_RATE,
|
||||
6,
|
||||
4,
|
||||
2,
|
||||
map_surround51
|
||||
};
|
||||
|
||||
static opus_stream_config_t HighSurround51 = {
|
||||
platf::speaker::map_surround51,
|
||||
},
|
||||
{
|
||||
SAMPLE_RATE,
|
||||
6,
|
||||
6,
|
||||
0,
|
||||
map_high_surround51
|
||||
platf::speaker::map_surround51,
|
||||
},
|
||||
{
|
||||
SAMPLE_RATE,
|
||||
8,
|
||||
5,
|
||||
3,
|
||||
platf::speaker::map_surround71,
|
||||
},
|
||||
{
|
||||
SAMPLE_RATE,
|
||||
8,
|
||||
8,
|
||||
0,
|
||||
platf::speaker::map_surround71,
|
||||
},
|
||||
};
|
||||
|
||||
void encodeThread(std::shared_ptr<safe::queue_t<packet_t>> packets, sample_queue_t samples, config_t config) {
|
||||
auto control_shared = safe::make_shared<audio_ctx_t>(start_audio_control, stop_audio_control);
|
||||
|
||||
void encodeThread(packet_queue_t packets, sample_queue_t samples, config_t config, void *channel_data) {
|
||||
//FIXME: Pick correct opus_stream_config_t based on config.channels
|
||||
auto stream = &stereo;
|
||||
opus_t opus { opus_multistream_encoder_create(
|
||||
stream->sampleRate,
|
||||
stream->channelCount,
|
||||
stream->streams,
|
||||
stream->coupledStreams,
|
||||
stream->mapping,
|
||||
OPUS_APPLICATION_AUDIO,
|
||||
nullptr)
|
||||
};
|
||||
auto stream = &stream_configs[map_stream(config.channels, config.flags[config_t::HIGH_QUALITY])];
|
||||
|
||||
opus_t opus { opus_multistream_encoder_create(
|
||||
stream->sampleRate,
|
||||
stream->channelCount,
|
||||
stream->streams,
|
||||
stream->coupledStreams,
|
||||
stream->mapping,
|
||||
OPUS_APPLICATION_AUDIO,
|
||||
nullptr) };
|
||||
|
||||
opus_multistream_encoder_ctl(opus.get(), OPUS_SET_VBR(0));
|
||||
|
||||
auto frame_size = config.packetDuration * stream->sampleRate / 1000;
|
||||
while(auto sample = samples->pop()) {
|
||||
packet_t packet { 16*1024 }; // 16KB
|
||||
packet_t packet { 1024 }; // 1KB
|
||||
|
||||
int bytes = opus_multistream_encode(opus.get(), sample->data(), frame_size, std::begin(packet), packet.size());
|
||||
if(bytes < 0) {
|
||||
BOOST_LOG(error) << opus_strerror(bytes);
|
||||
BOOST_LOG(error) << "Couldn't encode audio: "sv << opus_strerror(bytes);
|
||||
packets->stop();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
packet.fake_resize(bytes);
|
||||
packets->raise(std::move(packet));
|
||||
packets->raise(channel_data, std::move(packet));
|
||||
}
|
||||
}
|
||||
|
||||
void capture(std::shared_ptr<safe::queue_t<packet_t>> packets, config_t config) {
|
||||
auto samples = std::make_shared<sample_queue_t::element_type>();
|
||||
std::thread thread { encodeThread, packets, samples, config };
|
||||
|
||||
auto fg = util::fail_guard([&]() {
|
||||
packets->stop();
|
||||
samples->stop();
|
||||
thread.join();
|
||||
});
|
||||
|
||||
void capture(safe::signal_t *shutdown_event, packet_queue_t packets, config_t config, void *channel_data) {
|
||||
//FIXME: Pick correct opus_stream_config_t based on config.channels
|
||||
auto stream = &stereo;
|
||||
auto stream = &stream_configs[map_stream(config.channels, config.flags[config_t::HIGH_QUALITY])];
|
||||
|
||||
auto mic = platf::microphone(stream->sampleRate);
|
||||
if(!mic) {
|
||||
BOOST_LOG(error) << "Couldn't create audio input"sv ;
|
||||
auto ref = control_shared.ref();
|
||||
if(!ref) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto &control = ref->control;
|
||||
if(!control) {
|
||||
BOOST_LOG(error) << "Couldn't create audio control"sv;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
auto frame_size = config.packetDuration * stream->sampleRate / 1000;
|
||||
std::string *sink =
|
||||
config::audio.sink.empty() ? &ref->sink.host : &config::audio.sink;
|
||||
if(ref->sink.null) {
|
||||
auto &null = *ref->sink.null;
|
||||
switch(stream->channelCount) {
|
||||
case 2:
|
||||
sink = &null.stereo;
|
||||
break;
|
||||
case 6:
|
||||
sink = &null.surround51;
|
||||
break;
|
||||
case 8:
|
||||
sink = &null.surround71;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Only the first to start a session may change the default sink
|
||||
if(!ref->sink_flag->exchange(true, std::memory_order_acquire)) {
|
||||
ref->restore_sink = !config.flags[config_t::HOST_AUDIO];
|
||||
|
||||
// If the client requests audio on the host, don't change the default sink
|
||||
if(!config.flags[config_t::HOST_AUDIO] && control->set_sink(*sink)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
auto samples = std::make_shared<sample_queue_t::element_type>(30);
|
||||
std::thread thread { encodeThread, packets, samples, config, channel_data };
|
||||
|
||||
auto fg = util::fail_guard([&]() {
|
||||
samples->stop();
|
||||
thread.join();
|
||||
|
||||
shutdown_event->view();
|
||||
});
|
||||
|
||||
auto frame_size = config.packetDuration * stream->sampleRate / 1000;
|
||||
int samples_per_frame = frame_size * stream->channelCount;
|
||||
|
||||
while(packets->running()) {
|
||||
auto mic = control->microphone(stream->mapping, stream->channelCount, stream->sampleRate, frame_size);
|
||||
if(!mic) {
|
||||
BOOST_LOG(error) << "Couldn't create audio input"sv;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
while(!shutdown_event->peek()) {
|
||||
std::vector<std::int16_t> sample_buffer;
|
||||
sample_buffer.resize(samples_per_frame);
|
||||
|
||||
auto status = mic->sample(sample_buffer);
|
||||
switch(status) {
|
||||
case platf::capture_e::ok:
|
||||
break;
|
||||
case platf::capture_e::timeout:
|
||||
continue;
|
||||
case platf::capture_e::reinit:
|
||||
mic.reset();
|
||||
mic = platf::microphone(stream->sampleRate);
|
||||
if(!mic) {
|
||||
BOOST_LOG(error) << "Couldn't re-initialize audio input"sv ;
|
||||
case platf::capture_e::ok:
|
||||
break;
|
||||
case platf::capture_e::timeout:
|
||||
continue;
|
||||
case platf::capture_e::reinit:
|
||||
mic.reset();
|
||||
mic = control->microphone(stream->mapping, stream->channelCount, stream->sampleRate, frame_size);
|
||||
if(!mic) {
|
||||
BOOST_LOG(error) << "Couldn't re-initialize audio input"sv;
|
||||
|
||||
return;
|
||||
}
|
||||
return;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
return;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
|
||||
samples->raise(std::move(sample_buffer));
|
||||
}
|
||||
}
|
||||
|
||||
int map_stream(int channels, bool quality) {
|
||||
int shift = quality ? 1 : 0;
|
||||
switch(channels) {
|
||||
case 2:
|
||||
return STEREO;
|
||||
case 6:
|
||||
return SURROUND51 + shift;
|
||||
case 8:
|
||||
return SURROUND71 + shift;
|
||||
}
|
||||
return STEREO;
|
||||
}
|
||||
|
||||
int start_audio_control(audio_ctx_t &ctx) {
|
||||
ctx.sink_flag = std::make_unique<std::atomic_bool>(false);
|
||||
|
||||
if(!(ctx.control = platf::audio_control())) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
auto sink = ctx.control->sink_info();
|
||||
if(!sink) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
// The default sink has not been replaced yet.
|
||||
ctx.restore_sink = false;
|
||||
|
||||
ctx.sink = std::move(*sink);
|
||||
return 0;
|
||||
}
|
||||
void stop_audio_control(audio_ctx_t &ctx) {
|
||||
// restore audio-sink if applicable
|
||||
if(!ctx.restore_sink) {
|
||||
return;
|
||||
}
|
||||
|
||||
const std::string &sink = config::audio.sink.empty() ? ctx.sink.host : config::audio.sink;
|
||||
if(!sink.empty()) {
|
||||
// Best effort, it's allowed to fail
|
||||
ctx.control->set_sink(sink);
|
||||
}
|
||||
}
|
||||
} // namespace audio
|
||||
|
||||
@@ -1,18 +1,45 @@
|
||||
#ifndef SUNSHINE_AUDIO_H
|
||||
#define SUNSHINE_AUDIO_H
|
||||
|
||||
#include "utility.h"
|
||||
#include "thread_safe.h"
|
||||
#include "utility.h"
|
||||
namespace audio {
|
||||
enum stream_config_e : int {
|
||||
STEREO,
|
||||
SURROUND51,
|
||||
HIGH_SURROUND51,
|
||||
SURROUND71,
|
||||
HIGH_SURROUND71,
|
||||
MAX_STREAM_CONFIG
|
||||
};
|
||||
|
||||
struct opus_stream_config_t {
|
||||
std::int32_t sampleRate;
|
||||
int channelCount;
|
||||
int streams;
|
||||
int coupledStreams;
|
||||
const std::uint8_t *mapping;
|
||||
};
|
||||
|
||||
extern opus_stream_config_t stream_configs[MAX_STREAM_CONFIG];
|
||||
|
||||
struct config_t {
|
||||
enum flags_e : int {
|
||||
HIGH_QUALITY,
|
||||
HOST_AUDIO,
|
||||
MAX_FLAGS
|
||||
};
|
||||
|
||||
int packetDuration;
|
||||
int channels;
|
||||
int mask;
|
||||
|
||||
std::bitset<MAX_FLAGS> flags;
|
||||
};
|
||||
|
||||
using packet_t = util::buffer_t<std::uint8_t>;
|
||||
using packet_queue_t = std::shared_ptr<safe::queue_t<packet_t>>;
|
||||
void capture(std::shared_ptr<safe::queue_t<packet_t>> packets, config_t config);
|
||||
}
|
||||
using 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);
|
||||
} // namespace audio
|
||||
|
||||
#endif
|
||||
|
||||
229
sunshine/cbs.cpp
Normal file
229
sunshine/cbs.cpp
Normal file
@@ -0,0 +1,229 @@
|
||||
extern "C" {
|
||||
#include <cbs/cbs_h264.h>
|
||||
#include <cbs/cbs_h265.h>
|
||||
#include <cbs/h264_levels.h>
|
||||
#include <libavcodec/avcodec.h>
|
||||
}
|
||||
|
||||
#include "main.h"
|
||||
#include "utility.h"
|
||||
|
||||
using namespace std::literals;
|
||||
namespace cbs {
|
||||
void close(CodedBitstreamContext *c) {
|
||||
ff_cbs_close(&c);
|
||||
}
|
||||
|
||||
using ctx_t = util::safe_ptr<CodedBitstreamContext, close>;
|
||||
|
||||
class frag_t : public CodedBitstreamFragment {
|
||||
public:
|
||||
frag_t(frag_t &&o) {
|
||||
std::copy((std::uint8_t *)&o, (std::uint8_t *)(&o + 1), (std::uint8_t *)this);
|
||||
|
||||
o.data = nullptr;
|
||||
o.units = nullptr;
|
||||
};
|
||||
|
||||
frag_t() {
|
||||
std::fill_n((std::uint8_t *)this, sizeof(*this), 0);
|
||||
}
|
||||
|
||||
frag_t &operator=(frag_t &&o) {
|
||||
std::copy((std::uint8_t *)&o, (std::uint8_t *)(&o + 1), (std::uint8_t *)this);
|
||||
|
||||
o.data = nullptr;
|
||||
o.units = nullptr;
|
||||
|
||||
return *this;
|
||||
};
|
||||
|
||||
|
||||
~frag_t() {
|
||||
if(data || units) {
|
||||
ff_cbs_fragment_free(this);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
util::buffer_t<std::uint8_t> write(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);
|
||||
|
||||
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 };
|
||||
BOOST_LOG(error) << "Could not write fragment data: "sv << av_make_error_string(err_str, AV_ERROR_MAX_STRING_SIZE, err);
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
// frag.data_size * 8 - frag.data_bit_padding == bits in fragment
|
||||
util::buffer_t<std::uint8_t> data { frag.data_size };
|
||||
std::copy_n(frag.data, frag.data_size, std::begin(data));
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
util::buffer_t<std::uint8_t> make_sps_h264(const AVCodecContext *ctx) {
|
||||
H264RawSPS sps {};
|
||||
|
||||
/* b_per_p == ctx->max_b_frames for h264 */
|
||||
/* desired_b_depth == avoption("b_depth") == 1 */
|
||||
/* max_b_depth == std::min(av_log2(ctx->b_per_p) + 1, desired_b_depth) ==> 1 */
|
||||
auto max_b_depth = 1;
|
||||
auto dpb_frame = ctx->gop_size == 1 ? 0 : 1 + max_b_depth;
|
||||
auto mb_width = (FFALIGN(ctx->width, 16) / 16) * 16;
|
||||
auto mb_height = (FFALIGN(ctx->height, 16) / 16) * 16;
|
||||
|
||||
|
||||
sps.nal_unit_header.nal_ref_idc = 3;
|
||||
sps.nal_unit_header.nal_unit_type = H264_NAL_SPS;
|
||||
|
||||
sps.profile_idc = FF_PROFILE_H264_HIGH & 0xFF;
|
||||
|
||||
sps.constraint_set1_flag = 1;
|
||||
|
||||
if(ctx->level != FF_LEVEL_UNKNOWN) {
|
||||
sps.level_idc = ctx->level;
|
||||
}
|
||||
else {
|
||||
auto framerate = ctx->framerate;
|
||||
|
||||
auto level = ff_h264_guess_level(
|
||||
sps.profile_idc,
|
||||
ctx->bit_rate,
|
||||
framerate.num / framerate.den,
|
||||
mb_width,
|
||||
mb_height,
|
||||
dpb_frame);
|
||||
|
||||
if(!level) {
|
||||
BOOST_LOG(error) << "Could not guess h264 level"sv;
|
||||
|
||||
return {};
|
||||
}
|
||||
sps.level_idc = level->level_idc;
|
||||
}
|
||||
|
||||
sps.seq_parameter_set_id = 0;
|
||||
sps.chroma_format_idc = 1;
|
||||
|
||||
sps.log2_max_frame_num_minus4 = 3; //4;
|
||||
sps.pic_order_cnt_type = 0;
|
||||
sps.log2_max_pic_order_cnt_lsb_minus4 = 0; //4;
|
||||
|
||||
sps.max_num_ref_frames = dpb_frame;
|
||||
|
||||
sps.pic_width_in_mbs_minus1 = mb_width / 16 - 1;
|
||||
sps.pic_height_in_map_units_minus1 = mb_height / 16 - 1;
|
||||
|
||||
sps.frame_mbs_only_flag = 1;
|
||||
sps.direct_8x8_inference_flag = 1;
|
||||
|
||||
if(ctx->width != mb_width || ctx->height != mb_height) {
|
||||
sps.frame_cropping_flag = 1;
|
||||
sps.frame_crop_left_offset = 0;
|
||||
sps.frame_crop_top_offset = 0;
|
||||
sps.frame_crop_right_offset = (mb_width - ctx->width) / 2;
|
||||
sps.frame_crop_bottom_offset = (mb_height - ctx->height) / 2;
|
||||
}
|
||||
|
||||
sps.vui_parameters_present_flag = 1;
|
||||
|
||||
auto &vui = sps.vui;
|
||||
|
||||
vui.video_format = 5;
|
||||
vui.colour_description_present_flag = 1;
|
||||
vui.video_signal_type_present_flag = 1;
|
||||
vui.video_full_range_flag = ctx->color_range == AVCOL_RANGE_JPEG;
|
||||
vui.colour_primaries = ctx->color_primaries;
|
||||
vui.transfer_characteristics = ctx->color_trc;
|
||||
vui.matrix_coefficients = ctx->colorspace;
|
||||
|
||||
vui.low_delay_hrd_flag = 1 - vui.fixed_frame_rate_flag;
|
||||
|
||||
vui.bitstream_restriction_flag = 1;
|
||||
vui.motion_vectors_over_pic_boundaries_flag = 1;
|
||||
vui.log2_max_mv_length_horizontal = 15;
|
||||
vui.log2_max_mv_length_vertical = 15;
|
||||
vui.max_num_reorder_frames = max_b_depth;
|
||||
vui.max_dec_frame_buffering = max_b_depth + 1;
|
||||
|
||||
return write(sps.nal_unit_header.nal_unit_type, (void *)&sps.nal_unit_header, AV_CODEC_ID_H264);
|
||||
}
|
||||
|
||||
util::buffer_t<std::uint8_t> read_sps(const AVPacket *packet, int codec_id) {
|
||||
cbs::ctx_t ctx;
|
||||
if(ff_cbs_init(&ctx, (AVCodecID)codec_id, 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 {};
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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 {};
|
||||
}
|
||||
|
||||
bool validate_sps(const AVPacket *packet, int codec_id) {
|
||||
cbs::ctx_t ctx;
|
||||
if(ff_cbs_init(&ctx, (AVCodecID)codec_id, nullptr)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
cbs::frag_t frag;
|
||||
|
||||
int err = ff_cbs_read_packet(ctx.get(), &frag, &*packet);
|
||||
if(err < 0) {
|
||||
char err_str[AV_ERROR_MAX_STRING_SIZE] { 0 };
|
||||
BOOST_LOG(error) << "Couldn't read packet: "sv << av_make_error_string(err_str, AV_ERROR_MAX_STRING_SIZE, err);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
if(codec_id == AV_CODEC_ID_H264) {
|
||||
auto h264 = (CodedBitstreamH264Context *)ctx->priv_data;
|
||||
|
||||
if(!h264->active_sps->vui_parameters_present_flag) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return ((CodedBitstreamH265Context *)ctx->priv_data)->active_sps->vui_parameters_present_flag;
|
||||
}
|
||||
} // namespace cbs
|
||||
19
sunshine/cbs.h
Normal file
19
sunshine/cbs.h
Normal file
@@ -0,0 +1,19 @@
|
||||
#ifndef SUNSHINE_CBS_H
|
||||
#define SUNSHINE_CBS_H
|
||||
|
||||
#include "utility.h"
|
||||
|
||||
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);
|
||||
|
||||
/**
|
||||
* Check if SPS->VUI is present
|
||||
*/
|
||||
bool validate_sps(const AVPacket *packet, int codec_id);
|
||||
} // namespace cbs
|
||||
|
||||
#endif
|
||||
@@ -1,12 +1,20 @@
|
||||
#include <algorithm>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
#include <functional>
|
||||
#include <iostream>
|
||||
#include <unordered_map>
|
||||
|
||||
#include <boost/asio.hpp>
|
||||
|
||||
#include "utility.h"
|
||||
#include "config.h"
|
||||
#include "main.h"
|
||||
#include "utility.h"
|
||||
|
||||
#include "platform/common.h"
|
||||
|
||||
namespace fs = std::filesystem;
|
||||
using namespace std::literals;
|
||||
|
||||
#define CA_DIR "credentials"
|
||||
#define PRIVATE_KEY_FILE CA_DIR "/cakey.pem"
|
||||
@@ -14,18 +22,155 @@
|
||||
|
||||
#define APPS_JSON_PATH SUNSHINE_ASSETS_DIR "/" APPS_JSON
|
||||
namespace config {
|
||||
using namespace std::literals;
|
||||
|
||||
namespace nv {
|
||||
enum preset_e : int {
|
||||
_default = 0,
|
||||
slow,
|
||||
medium,
|
||||
fast,
|
||||
hp,
|
||||
hq,
|
||||
bd,
|
||||
ll_default,
|
||||
llhq,
|
||||
llhp,
|
||||
lossless_default, // lossless presets must be the last ones
|
||||
lossless_hp,
|
||||
};
|
||||
|
||||
enum rc_e : int {
|
||||
constqp = 0x0, /**< Constant QP mode */
|
||||
vbr = 0x1, /**< Variable bitrate mode */
|
||||
cbr = 0x2, /**< Constant bitrate mode */
|
||||
cbr_ld_hq = 0x8, /**< low-delay CBR, high quality */
|
||||
cbr_hq = 0x10, /**< CBR, high quality (slower) */
|
||||
vbr_hq = 0x20 /**< VBR, high quality (slower) */
|
||||
};
|
||||
|
||||
enum coder_e : int {
|
||||
_auto = 0,
|
||||
cabac,
|
||||
cavlc
|
||||
};
|
||||
|
||||
std::optional<preset_e> preset_from_view(const std::string_view &preset) {
|
||||
#define _CONVERT_(x) \
|
||||
if(preset == #x##sv) return x
|
||||
_CONVERT_(slow);
|
||||
_CONVERT_(medium);
|
||||
_CONVERT_(fast);
|
||||
_CONVERT_(hp);
|
||||
_CONVERT_(bd);
|
||||
_CONVERT_(ll_default);
|
||||
_CONVERT_(llhq);
|
||||
_CONVERT_(llhp);
|
||||
_CONVERT_(lossless_default);
|
||||
_CONVERT_(lossless_hp);
|
||||
if(preset == "default"sv) return _default;
|
||||
#undef _CONVERT_
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
std::optional<rc_e> rc_from_view(const std::string_view &rc) {
|
||||
#define _CONVERT_(x) \
|
||||
if(rc == #x##sv) return x
|
||||
_CONVERT_(constqp);
|
||||
_CONVERT_(vbr);
|
||||
_CONVERT_(cbr);
|
||||
_CONVERT_(cbr_hq);
|
||||
_CONVERT_(vbr_hq);
|
||||
_CONVERT_(cbr_ld_hq);
|
||||
#undef _CONVERT_
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
int coder_from_view(const std::string_view &coder) {
|
||||
if(coder == "auto"sv) return _auto;
|
||||
if(coder == "cabac"sv || coder == "ac"sv) return cabac;
|
||||
if(coder == "cavlc"sv || coder == "vlc"sv) return cavlc;
|
||||
|
||||
return -1;
|
||||
}
|
||||
} // namespace nv
|
||||
|
||||
namespace amd {
|
||||
enum quality_e : int {
|
||||
_default = 0,
|
||||
speed,
|
||||
balanced,
|
||||
//quality2,
|
||||
};
|
||||
|
||||
enum rc_e : int {
|
||||
constqp, /**< Constant QP mode */
|
||||
vbr_latency, /**< Latency Constrained Variable Bitrate */
|
||||
vbr_peak, /**< Peak Contrained Variable Bitrate */
|
||||
cbr, /**< Constant bitrate mode */
|
||||
};
|
||||
|
||||
enum coder_e : int {
|
||||
_auto = 0,
|
||||
cabac,
|
||||
cavlc
|
||||
};
|
||||
|
||||
std::optional<quality_e> quality_from_view(const std::string_view &quality) {
|
||||
#define _CONVERT_(x) \
|
||||
if(quality == #x##sv) return x
|
||||
_CONVERT_(speed);
|
||||
_CONVERT_(balanced);
|
||||
//_CONVERT_(quality2);
|
||||
if(quality == "default"sv) return _default;
|
||||
#undef _CONVERT_
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
std::optional<rc_e> rc_from_view(const std::string_view &rc) {
|
||||
#define _CONVERT_(x) \
|
||||
if(rc == #x##sv) return x
|
||||
_CONVERT_(constqp);
|
||||
_CONVERT_(vbr_latency);
|
||||
_CONVERT_(vbr_peak);
|
||||
_CONVERT_(cbr);
|
||||
#undef _CONVERT_
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
int coder_from_view(const std::string_view &coder) {
|
||||
if(coder == "auto"sv) return _auto;
|
||||
if(coder == "cabac"sv || coder == "ac"sv) return cabac;
|
||||
if(coder == "cavlc"sv || coder == "vlc"sv) return cavlc;
|
||||
|
||||
return -1;
|
||||
}
|
||||
} // namespace amd
|
||||
|
||||
video_t video {
|
||||
0, // crf
|
||||
0, // crf
|
||||
28, // qp
|
||||
|
||||
2, // min_threads
|
||||
|
||||
0, // hevc_mode
|
||||
"superfast"s, // preset
|
||||
"zerolatency"s, // tune
|
||||
|
||||
1, // min_threads
|
||||
{
|
||||
"superfast"s, // preset
|
||||
"zerolatency"s, // tune
|
||||
}, // software
|
||||
|
||||
{
|
||||
nv::llhq,
|
||||
std::nullopt,
|
||||
-1 }, // nv
|
||||
|
||||
{
|
||||
amd::balanced,
|
||||
std::nullopt,
|
||||
-1 }, // amd
|
||||
|
||||
{}, // encoder
|
||||
{}, // adapter_name
|
||||
{} // output_name
|
||||
{}, // output_name
|
||||
};
|
||||
|
||||
audio_t audio {};
|
||||
@@ -35,7 +180,8 @@ stream_t stream {
|
||||
|
||||
APPS_JSON_PATH,
|
||||
|
||||
10 // fecPercentage
|
||||
10, // fecPercentage
|
||||
1 // channels
|
||||
};
|
||||
|
||||
nvhttp_t nvhttp {
|
||||
@@ -44,52 +190,136 @@ nvhttp_t nvhttp {
|
||||
CERTIFICATE_FILE,
|
||||
|
||||
boost::asio::ip::host_name(), // sunshine_name,
|
||||
"sunshine_state.json"s // file_state
|
||||
"sunshine_state.json"s, // file_state
|
||||
{}, // external_ip
|
||||
{
|
||||
"352x240"s,
|
||||
"480x360"s,
|
||||
"858x480"s,
|
||||
"1280x720"s,
|
||||
"1920x1080"s,
|
||||
"2560x1080"s,
|
||||
"3440x1440"s
|
||||
"1920x1200"s,
|
||||
"3860x2160"s,
|
||||
"3840x1600"s,
|
||||
}, // supported resolutions
|
||||
|
||||
{ 10, 30, 60, 90, 120 }, // supported fps
|
||||
};
|
||||
|
||||
input_t input {
|
||||
2s
|
||||
2s, // back_button_timeout
|
||||
500ms, // key_repeat_delay
|
||||
std::chrono::duration<double> { 1 / 24.9 } // key_repeat_period
|
||||
};
|
||||
|
||||
sunshine_t sunshine {
|
||||
2 // min_log_level
|
||||
2, // min_log_level
|
||||
0, // flags
|
||||
{}, // User file
|
||||
{}, // Username
|
||||
{}, // Password
|
||||
{}, // Password Salt
|
||||
SUNSHINE_ASSETS_DIR "/sunshine.conf", // config file
|
||||
{} // cmd args
|
||||
};
|
||||
|
||||
bool whitespace(char ch) {
|
||||
bool endline(char ch) {
|
||||
return ch == '\r' || ch == '\n';
|
||||
}
|
||||
|
||||
bool space_tab(char ch) {
|
||||
return ch == ' ' || ch == '\t';
|
||||
}
|
||||
|
||||
std::string to_string(const char *begin, const char *end) {
|
||||
return { begin, (std::size_t)(end - begin) };
|
||||
bool whitespace(char ch) {
|
||||
return space_tab(ch) || endline(ch);
|
||||
}
|
||||
|
||||
std::optional<std::pair<std::string, std::string>> parse_line(std::string_view::const_iterator begin, std::string_view::const_iterator end) {
|
||||
begin = std::find_if(begin, end, std::not_fn(whitespace));
|
||||
end = std::find(begin, end, '#');
|
||||
end = std::find_if(std::make_reverse_iterator(end), std::make_reverse_iterator(begin), std::not_fn(whitespace)).base();
|
||||
std::string to_string(const char *begin, const char *end) {
|
||||
std::string result;
|
||||
|
||||
auto eq = std::find(begin, end, '=');
|
||||
if(eq == end || eq == begin) {
|
||||
return std::nullopt;
|
||||
KITTY_WHILE_LOOP(auto pos = begin, pos != end, {
|
||||
auto comment = std::find(pos, end, '#');
|
||||
auto endl = std::find_if(comment, end, endline);
|
||||
|
||||
result.append(pos, comment);
|
||||
|
||||
pos = endl;
|
||||
})
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
template<class It>
|
||||
It skip_list(It skipper, It end) {
|
||||
int stack = 1;
|
||||
while(skipper != end && stack) {
|
||||
if(*skipper == '[') {
|
||||
++stack;
|
||||
}
|
||||
if(*skipper == ']') {
|
||||
--stack;
|
||||
}
|
||||
|
||||
++skipper;
|
||||
}
|
||||
|
||||
auto end_name = std::find_if(std::make_reverse_iterator(eq - 1), std::make_reverse_iterator(begin), std::not_fn(whitespace)).base();
|
||||
auto begin_val = std::find_if(eq + 1, end, std::not_fn(whitespace));
|
||||
|
||||
return std::pair { to_string(begin, end_name), to_string(begin_val, end) };
|
||||
return skipper;
|
||||
}
|
||||
|
||||
std::unordered_map<std::string, std::string> parse_config(std::string_view file_content) {
|
||||
std::pair<
|
||||
std::string_view::const_iterator,
|
||||
std::optional<std::pair<std::string, std::string>>>
|
||||
parse_option(std::string_view::const_iterator begin, std::string_view::const_iterator end) {
|
||||
begin = std::find_if_not(begin, end, whitespace);
|
||||
auto endl = std::find_if(begin, end, endline);
|
||||
auto endc = std::find(begin, endl, '#');
|
||||
endc = std::find_if(std::make_reverse_iterator(endc), std::make_reverse_iterator(begin), std::not_fn(whitespace)).base();
|
||||
|
||||
auto eq = std::find(begin, endc, '=');
|
||||
if(eq == endc || eq == begin) {
|
||||
return std::make_pair(endl, std::nullopt);
|
||||
}
|
||||
|
||||
auto end_name = std::find_if_not(std::make_reverse_iterator(eq), std::make_reverse_iterator(begin), space_tab).base();
|
||||
auto begin_val = std::find_if_not(eq + 1, endc, space_tab);
|
||||
|
||||
if(begin_val == endl) {
|
||||
return std::make_pair(endl, std::nullopt);
|
||||
}
|
||||
|
||||
// Lists might contain newlines
|
||||
if(*begin_val == '[') {
|
||||
endl = skip_list(begin_val + 1, end);
|
||||
if(endl == end) {
|
||||
std::cout << "Warning: Config option ["sv << to_string(begin, end_name) << "] Missing ']'"sv;
|
||||
|
||||
return std::make_pair(endl, std::nullopt);
|
||||
}
|
||||
}
|
||||
|
||||
return std::make_pair(
|
||||
endl,
|
||||
std::make_pair(to_string(begin, end_name), to_string(begin_val, endl)));
|
||||
}
|
||||
|
||||
std::unordered_map<std::string, std::string> parse_config(const std::string_view &file_content) {
|
||||
std::unordered_map<std::string, std::string> vars;
|
||||
|
||||
auto pos = std::begin(file_content);
|
||||
auto end = std::end(file_content);
|
||||
|
||||
while(pos < end) {
|
||||
auto newline = std::find_if(pos, end, [](auto ch) { return ch == '\n' || ch == '\r'; });
|
||||
auto var = parse_line(pos, newline);
|
||||
// auto newline = std::find_if(pos, end, [](auto ch) { return ch == '\n' || ch == '\r'; });
|
||||
TUPLE_2D(endl, var, parse_option(pos, end));
|
||||
|
||||
pos = endl;
|
||||
if(pos != end) {
|
||||
pos += (*pos == '\r') ? 2 : 1;
|
||||
}
|
||||
|
||||
pos = (*newline == '\r') ? newline + 2 : newline + 1;
|
||||
if(!var) {
|
||||
continue;
|
||||
}
|
||||
@@ -123,6 +353,38 @@ void string_restricted_f(std::unordered_map<std::string, std::string> &vars, con
|
||||
}
|
||||
}
|
||||
|
||||
void path_f(std::unordered_map<std::string, std::string> &vars, const std::string &name, fs::path &input) {
|
||||
// appdata needs to be retrieved once only
|
||||
static auto appdata = platf::appdata();
|
||||
|
||||
std::string temp;
|
||||
string_f(vars, name, temp);
|
||||
|
||||
if(!temp.empty()) {
|
||||
input = temp;
|
||||
}
|
||||
|
||||
if(input.is_relative()) {
|
||||
input = appdata / input;
|
||||
}
|
||||
|
||||
auto dir = input;
|
||||
dir.remove_filename();
|
||||
|
||||
// Ensure the directories exists
|
||||
if(!fs::exists(dir)) {
|
||||
fs::create_directories(dir);
|
||||
}
|
||||
}
|
||||
|
||||
void path_f(std::unordered_map<std::string, std::string> &vars, const std::string &name, std::string &input) {
|
||||
fs::path temp = input;
|
||||
|
||||
path_f(vars, name, temp);
|
||||
|
||||
input = temp.string();
|
||||
}
|
||||
|
||||
void int_f(std::unordered_map<std::string, std::string> &vars, const std::string &name, int &input) {
|
||||
auto it = vars.find(name);
|
||||
|
||||
@@ -131,11 +393,42 @@ void int_f(std::unordered_map<std::string, std::string> &vars, const std::string
|
||||
}
|
||||
|
||||
auto &val = it->second;
|
||||
input = util::from_chars(&val[0], &val[0] + val.size());
|
||||
input = util::from_chars(&val[0], &val[0] + val.size());
|
||||
|
||||
vars.erase(it);
|
||||
}
|
||||
|
||||
void int_f(std::unordered_map<std::string, std::string> &vars, const std::string &name, std::optional<int> &input) {
|
||||
auto it = vars.find(name);
|
||||
|
||||
if(it == std::end(vars)) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto &val = it->second;
|
||||
input = util::from_chars(&val[0], &val[0] + val.size());
|
||||
|
||||
vars.erase(it);
|
||||
}
|
||||
|
||||
template<class F>
|
||||
void int_f(std::unordered_map<std::string, std::string> &vars, const std::string &name, int &input, F &&f) {
|
||||
std::string tmp;
|
||||
string_f(vars, name, tmp);
|
||||
if(!tmp.empty()) {
|
||||
input = f(tmp);
|
||||
}
|
||||
}
|
||||
|
||||
template<class F>
|
||||
void int_f(std::unordered_map<std::string, std::string> &vars, const std::string &name, std::optional<int> &input, F &&f) {
|
||||
std::string tmp;
|
||||
string_f(vars, name, tmp);
|
||||
if(!tmp.empty()) {
|
||||
input = f(tmp);
|
||||
}
|
||||
}
|
||||
|
||||
void int_between_f(std::unordered_map<std::string, std::string> &vars, const std::string &name, int &input, const std::pair<int, int> &range) {
|
||||
int temp = input;
|
||||
|
||||
@@ -147,15 +440,130 @@ void int_between_f(std::unordered_map<std::string, std::string> &vars, const std
|
||||
}
|
||||
}
|
||||
|
||||
void parse_file(const char *file) {
|
||||
std::ifstream in(file);
|
||||
bool to_bool(std::string &boolean) {
|
||||
std::for_each(std::begin(boolean), std::end(boolean), [](char ch) { return (char)std::tolower(ch); });
|
||||
|
||||
auto vars = parse_config(std::string {
|
||||
// Quick and dirty
|
||||
std::istreambuf_iterator<char>(in),
|
||||
std::istreambuf_iterator<char>()
|
||||
});
|
||||
return boolean == "true"sv ||
|
||||
boolean == "yes"sv ||
|
||||
boolean == "enable"sv ||
|
||||
(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) {
|
||||
std::string tmp;
|
||||
string_f(vars, name, tmp);
|
||||
|
||||
if(tmp.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
input = to_bool(tmp) ? 1 : 0;
|
||||
}
|
||||
|
||||
void double_f(std::unordered_map<std::string, std::string> &vars, const std::string &name, double &input) {
|
||||
std::string tmp;
|
||||
string_f(vars, name, tmp);
|
||||
|
||||
if(tmp.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
char *c_str_p;
|
||||
auto val = std::strtod(tmp.c_str(), &c_str_p);
|
||||
|
||||
if(c_str_p == tmp.c_str()) {
|
||||
return;
|
||||
}
|
||||
|
||||
input = val;
|
||||
}
|
||||
|
||||
void double_between_f(std::unordered_map<std::string, std::string> &vars, const std::string &name, double &input, const std::pair<double, double> &range) {
|
||||
double temp = input;
|
||||
|
||||
double_f(vars, name, temp);
|
||||
|
||||
TUPLE_2D_REF(lower, upper, range);
|
||||
if(temp >= lower && temp <= upper) {
|
||||
input = temp;
|
||||
}
|
||||
}
|
||||
|
||||
void list_string_f(std::unordered_map<std::string, std::string> &vars, const std::string &name, std::vector<std::string> &input) {
|
||||
std::string string;
|
||||
string_f(vars, name, string);
|
||||
|
||||
if(string.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
input.clear();
|
||||
|
||||
auto begin = std::cbegin(string);
|
||||
if(*begin == '[') {
|
||||
++begin;
|
||||
}
|
||||
|
||||
begin = std::find_if_not(begin, std::cend(string), whitespace);
|
||||
if(begin == std::cend(string)) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto pos = begin;
|
||||
while(pos < std::cend(string)) {
|
||||
if(*pos == '[') {
|
||||
pos = skip_list(pos + 1, std::cend(string)) + 1;
|
||||
}
|
||||
else if(*pos == ']') {
|
||||
break;
|
||||
}
|
||||
else if(*pos == ',') {
|
||||
input.emplace_back(begin, pos);
|
||||
pos = begin = std::find_if_not(pos + 1, std::cend(string), whitespace);
|
||||
}
|
||||
else {
|
||||
++pos;
|
||||
}
|
||||
}
|
||||
|
||||
if(pos != begin) {
|
||||
input.emplace_back(begin, pos);
|
||||
}
|
||||
}
|
||||
|
||||
void list_int_f(std::unordered_map<std::string, std::string> &vars, const std::string &name, std::vector<int> &input) {
|
||||
std::vector<std::string> list;
|
||||
list_string_f(vars, name, list);
|
||||
|
||||
for(auto &el : list) {
|
||||
input.emplace_back(util::from_view(el));
|
||||
}
|
||||
}
|
||||
|
||||
int apply_flags(const char *line) {
|
||||
int ret = 0;
|
||||
while(*line != '\0') {
|
||||
switch(*line) {
|
||||
case '0':
|
||||
config::sunshine.flags[config::flag::PIN_STDIN].flip();
|
||||
break;
|
||||
case '1':
|
||||
config::sunshine.flags[config::flag::FRESH_STATE].flip();
|
||||
break;
|
||||
case 'p':
|
||||
config::sunshine.flags[config::flag::CONST_PIN].flip();
|
||||
break;
|
||||
default:
|
||||
std::cout << "Warning: Unrecognized flag: ["sv << *line << ']' << std::endl;
|
||||
ret = -1;
|
||||
}
|
||||
|
||||
++line;
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
void apply_config(std::unordered_map<std::string, std::string> &&vars) {
|
||||
for(auto &[name, val] : vars) {
|
||||
std::cout << "["sv << name << "] -- ["sv << val << ']' << std::endl;
|
||||
}
|
||||
@@ -163,47 +571,73 @@ void parse_file(const char *file) {
|
||||
int_f(vars, "crf", video.crf);
|
||||
int_f(vars, "qp", video.qp);
|
||||
int_f(vars, "min_threads", video.min_threads);
|
||||
int_between_f(vars, "hevc_mode", video.hevc_mode, {
|
||||
0, 2
|
||||
});
|
||||
string_f(vars, "preset", video.preset);
|
||||
string_f(vars, "tune", video.tune);
|
||||
int_between_f(vars, "hevc_mode", video.hevc_mode, { 0, 3 });
|
||||
string_f(vars, "sw_preset", video.sw.preset);
|
||||
string_f(vars, "sw_tune", video.sw.tune);
|
||||
int_f(vars, "nv_preset", video.nv.preset, nv::preset_from_view);
|
||||
int_f(vars, "nv_rc", video.nv.rc, nv::rc_from_view);
|
||||
int_f(vars, "nv_coder", video.nv.coder, nv::coder_from_view);
|
||||
|
||||
int_f(vars, "amd_quality", video.amd.quality, amd::quality_from_view);
|
||||
int_f(vars, "amd_rc", video.amd.rc, amd::rc_from_view);
|
||||
int_f(vars, "amd_coder", video.amd.coder, amd::coder_from_view);
|
||||
|
||||
string_f(vars, "encoder", video.encoder);
|
||||
string_f(vars, "adapter_name", video.adapter_name);
|
||||
string_f(vars, "output_name", video.output_name);
|
||||
|
||||
string_f(vars, "pkey", nvhttp.pkey);
|
||||
string_f(vars, "cert", nvhttp.cert);
|
||||
path_f(vars, "pkey", nvhttp.pkey);
|
||||
path_f(vars, "cert", nvhttp.cert);
|
||||
string_f(vars, "sunshine_name", nvhttp.sunshine_name);
|
||||
string_f(vars, "file_state", nvhttp.file_state);
|
||||
|
||||
path_f(vars, "file_state", nvhttp.file_state);
|
||||
|
||||
// Must be run after "file_state"
|
||||
config::sunshine.credentials_file = config::nvhttp.file_state;
|
||||
path_f(vars, "credentials_file", config::sunshine.credentials_file);
|
||||
|
||||
string_f(vars, "external_ip", nvhttp.external_ip);
|
||||
list_string_f(vars, "resolutions"s, nvhttp.resolutions);
|
||||
list_int_f(vars, "fps"s, nvhttp.fps);
|
||||
|
||||
string_f(vars, "audio_sink", audio.sink);
|
||||
string_f(vars, "virtual_sink", audio.virtual_sink);
|
||||
|
||||
string_restricted_f(vars, "origin_pin_allowed", nvhttp.origin_pin_allowed, {
|
||||
"pc"sv, "lan"sv, "wan"sv
|
||||
});
|
||||
string_restricted_f(vars, "origin_pin_allowed", nvhttp.origin_pin_allowed, { "pc"sv, "lan"sv, "wan"sv });
|
||||
|
||||
int to = -1;
|
||||
int_f(vars, "ping_timeout", to);
|
||||
if(to > 0) {
|
||||
int_between_f(vars, "ping_timeout", to, { -1, std::numeric_limits<int>::max() });
|
||||
if(to != -1) {
|
||||
stream.ping_timeout = std::chrono::milliseconds(to);
|
||||
}
|
||||
string_f(vars, "file_apps", stream.file_apps);
|
||||
int_between_f(vars, "fec_percentage", stream.fec_percentage, {
|
||||
1, 100
|
||||
});
|
||||
|
||||
int_between_f(vars, "channels", stream.channels, { 1, std::numeric_limits<int>::max() });
|
||||
|
||||
path_f(vars, "file_apps", stream.file_apps);
|
||||
int_between_f(vars, "fec_percentage", stream.fec_percentage, { 1, 100 });
|
||||
|
||||
to = std::numeric_limits<int>::min();
|
||||
int_f(vars, "back_button_timeout", to);
|
||||
|
||||
if(to > std::numeric_limits<int>::min()) {
|
||||
input.back_button_timeout = std::chrono::milliseconds {to };
|
||||
input.back_button_timeout = std::chrono::milliseconds { to };
|
||||
}
|
||||
|
||||
double repeat_frequency { 0 };
|
||||
double_between_f(vars, "key_repeat_frequency", repeat_frequency, { 0, std::numeric_limits<double>::max() });
|
||||
|
||||
if(repeat_frequency > 0) {
|
||||
config::input.key_repeat_period = std::chrono::duration<double> { 1 / repeat_frequency };
|
||||
}
|
||||
|
||||
to = -1;
|
||||
int_f(vars, "key_repeat_delay", to);
|
||||
if(to >= 0) {
|
||||
input.key_repeat_delay = std::chrono::milliseconds { to };
|
||||
}
|
||||
|
||||
std::string log_level_string;
|
||||
string_restricted_f(vars, "min_log_level", log_level_string, {
|
||||
"verbose"sv, "debug"sv, "info"sv, "warning"sv, "error"sv, "fatal"sv, "none"sv
|
||||
});
|
||||
string_restricted_f(vars, "min_log_level", log_level_string, { "verbose"sv, "debug"sv, "info"sv, "warning"sv, "error"sv, "fatal"sv, "none"sv });
|
||||
|
||||
if(!log_level_string.empty()) {
|
||||
if(log_level_string == "verbose"sv) {
|
||||
@@ -227,14 +661,79 @@ void parse_file(const char *file) {
|
||||
else if(log_level_string == "none"sv) {
|
||||
sunshine.min_log_level = 6;
|
||||
}
|
||||
else {
|
||||
// accept digit directly
|
||||
auto val = log_level_string[0];
|
||||
if(val >= '0' && val < '7') {
|
||||
sunshine.min_log_level = val - '0';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
auto it = vars.find("flags"s);
|
||||
if(it != std::end(vars)) {
|
||||
apply_flags(it->second.c_str());
|
||||
|
||||
vars.erase(it);
|
||||
}
|
||||
|
||||
if(sunshine.min_log_level <= 3) {
|
||||
for(auto &[var,_] : vars) {
|
||||
for(auto &[var, _] : vars) {
|
||||
std::cout << "Warning: Unrecognized configurable option ["sv << var << ']' << std::endl;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int parse(int argc, char *argv[]) {
|
||||
std::unordered_map<std::string, std::string> cmd_vars;
|
||||
|
||||
for(auto x = 1; x < argc; ++x) {
|
||||
auto line = argv[x];
|
||||
|
||||
if(line == "--help"sv) {
|
||||
print_help(*argv);
|
||||
return 1;
|
||||
}
|
||||
else if(*line == '-') {
|
||||
if(*(line + 1) == '-') {
|
||||
sunshine.cmd.name = line + 2;
|
||||
sunshine.cmd.argc = argc - x - 1;
|
||||
sunshine.cmd.argv = argv + x + 1;
|
||||
|
||||
break;
|
||||
}
|
||||
if(apply_flags(line + 1)) {
|
||||
print_help(*argv);
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
else {
|
||||
auto line_end = line + strlen(line);
|
||||
|
||||
auto pos = std::find(line, line_end, '=');
|
||||
if(pos == line_end) {
|
||||
sunshine.config_file = line;
|
||||
}
|
||||
else {
|
||||
TUPLE_EL(var, 1, parse_option(line, line_end));
|
||||
if(!var) {
|
||||
print_help(*argv);
|
||||
return -1;
|
||||
}
|
||||
|
||||
cmd_vars.emplace(std::move(*var));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
auto vars = parse_config(read_file(sunshine.config_file.c_str()));
|
||||
|
||||
for(auto &[name, value] : cmd_vars) {
|
||||
vars.insert_or_assign(std::move(name), std::move(value));
|
||||
}
|
||||
|
||||
apply_config(std::move(vars));
|
||||
|
||||
return 0;
|
||||
}
|
||||
} // namespace config
|
||||
|
||||
@@ -1,27 +1,47 @@
|
||||
#ifndef SUNSHINE_CONFIG_H
|
||||
#define SUNSHINE_CONFIG_H
|
||||
|
||||
#include <bitset>
|
||||
#include <chrono>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
namespace config {
|
||||
struct video_t {
|
||||
// ffmpeg params
|
||||
int crf; // higher == more compression and less quality
|
||||
int qp; // higher == more compression and less quality, ignored if crf != 0
|
||||
|
||||
int min_threads; // Minimum number of threads/slices for CPU encoding
|
||||
int qp; // higher == more compression and less quality, ignored if crf != 0
|
||||
|
||||
int hevc_mode;
|
||||
std::string preset;
|
||||
std::string tune;
|
||||
|
||||
int min_threads; // Minimum number of threads/slices for CPU encoding
|
||||
struct {
|
||||
std::string preset;
|
||||
std::string tune;
|
||||
} sw;
|
||||
|
||||
struct {
|
||||
std::optional<int> preset;
|
||||
std::optional<int> rc;
|
||||
int coder;
|
||||
} nv;
|
||||
|
||||
struct {
|
||||
std::optional<int> quality;
|
||||
std::optional<int> rc;
|
||||
int coder;
|
||||
} amd;
|
||||
|
||||
std::string encoder;
|
||||
std::string adapter_name;
|
||||
std::string output_name;
|
||||
};
|
||||
|
||||
struct audio_t {
|
||||
std::string sink;
|
||||
std::string virtual_sink;
|
||||
};
|
||||
|
||||
struct stream_t {
|
||||
@@ -30,6 +50,9 @@ struct stream_t {
|
||||
std::string file_apps;
|
||||
|
||||
int fec_percentage;
|
||||
|
||||
// max unique instances of video and audio streams
|
||||
int channels;
|
||||
};
|
||||
|
||||
struct nvhttp_t {
|
||||
@@ -45,14 +68,41 @@ struct nvhttp_t {
|
||||
std::string file_state;
|
||||
|
||||
std::string external_ip;
|
||||
std::vector<std::string> resolutions;
|
||||
std::vector<int> fps;
|
||||
};
|
||||
|
||||
struct input_t {
|
||||
std::chrono::milliseconds back_button_timeout;
|
||||
std::chrono::milliseconds key_repeat_delay;
|
||||
std::chrono::duration<double> key_repeat_period;
|
||||
};
|
||||
|
||||
namespace flag {
|
||||
enum flag_e : std::size_t {
|
||||
PIN_STDIN = 0, // Read PIN from stdin instead of http
|
||||
FRESH_STATE, // Do not load or save state
|
||||
CONST_PIN, // Use "universal" pin
|
||||
FLAG_SIZE
|
||||
};
|
||||
}
|
||||
|
||||
struct sunshine_t {
|
||||
int min_log_level;
|
||||
std::bitset<flag::FLAG_SIZE> flags;
|
||||
std::string credentials_file;
|
||||
|
||||
std::string username;
|
||||
std::string password;
|
||||
std::string salt;
|
||||
|
||||
std::string config_file;
|
||||
|
||||
struct cmd_t {
|
||||
std::string name;
|
||||
int argc;
|
||||
char **argv;
|
||||
} cmd;
|
||||
};
|
||||
|
||||
extern video_t video;
|
||||
@@ -62,7 +112,7 @@ extern nvhttp_t nvhttp;
|
||||
extern input_t input;
|
||||
extern sunshine_t sunshine;
|
||||
|
||||
void parse_file(const char *file);
|
||||
}
|
||||
|
||||
int parse(int argc, char *argv[]);
|
||||
std::unordered_map<std::string, std::string> parse_config(const std::string_view &file_content);
|
||||
} // namespace config
|
||||
#endif
|
||||
|
||||
512
sunshine/confighttp.cpp
Normal file
512
sunshine/confighttp.cpp
Normal file
@@ -0,0 +1,512 @@
|
||||
//
|
||||
// Created by TheElixZammuto on 2021-05-09.
|
||||
// TODO: Authentication, better handling of routes common to nvhttp, cleanup
|
||||
|
||||
#define BOOST_BIND_GLOBAL_PLACEHOLDERS
|
||||
|
||||
#include "process.h"
|
||||
|
||||
#include <filesystem>
|
||||
|
||||
#include <boost/property_tree/json_parser.hpp>
|
||||
#include <boost/property_tree/ptree.hpp>
|
||||
#include <boost/property_tree/xml_parser.hpp>
|
||||
|
||||
#include <boost/asio/ssl/context.hpp>
|
||||
|
||||
#include <Simple-Web-Server/crypto.hpp>
|
||||
#include <Simple-Web-Server/server_http.hpp>
|
||||
#include <boost/asio/ssl/context_base.hpp>
|
||||
|
||||
#include "config.h"
|
||||
#include "confighttp.h"
|
||||
#include "crypto.h"
|
||||
#include "httpcommon.h"
|
||||
#include "main.h"
|
||||
#include "network.h"
|
||||
#include "nvhttp.h"
|
||||
#include "platform/common.h"
|
||||
#include "rtsp.h"
|
||||
#include "utility.h"
|
||||
#include "uuid.h"
|
||||
|
||||
namespace confighttp {
|
||||
using namespace std::literals;
|
||||
constexpr auto PORT_HTTPS = 47990;
|
||||
|
||||
namespace fs = std::filesystem;
|
||||
namespace pt = boost::property_tree;
|
||||
|
||||
using https_server_t = SimpleWeb::Server<SimpleWeb::HTTPS>;
|
||||
|
||||
using args_t = SimpleWeb::CaseInsensitiveMultimap;
|
||||
using resp_https_t = std::shared_ptr<typename SimpleWeb::ServerBase<SimpleWeb::HTTPS>::Response>;
|
||||
using req_https_t = std::shared_ptr<typename SimpleWeb::ServerBase<SimpleWeb::HTTPS>::Request>;
|
||||
|
||||
enum class op_e {
|
||||
ADD,
|
||||
REMOVE
|
||||
};
|
||||
|
||||
void print_req(const req_https_t &request) {
|
||||
BOOST_LOG(debug) << "METHOD :: "sv << request->method;
|
||||
BOOST_LOG(debug) << "DESTINATION :: "sv << request->path;
|
||||
|
||||
for(auto &[name, val] : request->header) {
|
||||
BOOST_LOG(debug) << name << " -- " << val;
|
||||
}
|
||||
|
||||
BOOST_LOG(debug) << " [--] "sv;
|
||||
|
||||
for(auto &[name, val] : request->parse_query_string()) {
|
||||
BOOST_LOG(debug) << name << " -- " << val;
|
||||
}
|
||||
|
||||
BOOST_LOG(debug) << " [--] "sv;
|
||||
}
|
||||
|
||||
void send_unauthorized(resp_https_t response, req_https_t request) {
|
||||
auto address = request->remote_endpoint_address();
|
||||
BOOST_LOG(info) << '[' << address << "] -- denied"sv;
|
||||
const SimpleWeb::CaseInsensitiveMultimap headers {
|
||||
{ "WWW-Authenticate", R"(Basic realm="Sunshine Gamestream Host", charset="UTF-8")" }
|
||||
};
|
||||
response->write(SimpleWeb::StatusCode::client_error_unauthorized, headers);
|
||||
}
|
||||
|
||||
bool authenticate(resp_https_t response, req_https_t request) {
|
||||
auto address = request->remote_endpoint_address();
|
||||
auto ip_type = net::from_address(address);
|
||||
|
||||
if(ip_type > http::origin_pin_allowed) {
|
||||
BOOST_LOG(info) << '[' << address << "] -- denied"sv;
|
||||
response->write(SimpleWeb::StatusCode::client_error_forbidden);
|
||||
return false;
|
||||
}
|
||||
|
||||
auto fg = util::fail_guard([&]() {
|
||||
send_unauthorized(response, request);
|
||||
});
|
||||
|
||||
auto auth = request->header.find("authorization");
|
||||
if(auth == request->header.end()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
auto &rawAuth = auth->second;
|
||||
auto authData = SimpleWeb::Crypto::Base64::decode(rawAuth.substr("Basic "sv.length()));
|
||||
|
||||
int index = authData.find(':');
|
||||
if(index >= authData.size() - 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
auto username = authData.substr(0, index);
|
||||
auto password = authData.substr(index + 1);
|
||||
auto hash = util::hex(crypto::hash(password + config::sunshine.salt)).to_string();
|
||||
|
||||
if(username != config::sunshine.username || hash != config::sunshine.password) {
|
||||
return false;
|
||||
}
|
||||
|
||||
fg.disable();
|
||||
return true;
|
||||
}
|
||||
|
||||
void not_found(resp_https_t response, req_https_t request) {
|
||||
pt::ptree tree;
|
||||
tree.put("root.<xmlattr>.status_code", 404);
|
||||
|
||||
std::ostringstream data;
|
||||
|
||||
pt::write_xml(data, tree);
|
||||
response->write(data.str());
|
||||
|
||||
*response << "HTTP/1.1 404 NOT FOUND\r\n"
|
||||
<< data.str();
|
||||
}
|
||||
|
||||
void getIndexPage(resp_https_t response, req_https_t request) {
|
||||
if(!authenticate(response, request)) return;
|
||||
|
||||
print_req(request);
|
||||
|
||||
std::string header = read_file(WEB_DIR "header.html");
|
||||
std::string content = read_file(WEB_DIR "index.html");
|
||||
response->write(header + content);
|
||||
}
|
||||
|
||||
void getPinPage(resp_https_t response, req_https_t request) {
|
||||
if(!authenticate(response, request)) return;
|
||||
|
||||
print_req(request);
|
||||
|
||||
std::string header = read_file(WEB_DIR "header.html");
|
||||
std::string content = read_file(WEB_DIR "pin.html");
|
||||
response->write(header + content);
|
||||
}
|
||||
|
||||
void getAppsPage(resp_https_t response, req_https_t request) {
|
||||
if(!authenticate(response, request)) return;
|
||||
|
||||
print_req(request);
|
||||
|
||||
std::string header = read_file(WEB_DIR "header.html");
|
||||
std::string content = read_file(WEB_DIR "apps.html");
|
||||
response->write(header + content);
|
||||
}
|
||||
|
||||
void getClientsPage(resp_https_t response, req_https_t request) {
|
||||
if(!authenticate(response, request)) return;
|
||||
|
||||
print_req(request);
|
||||
|
||||
std::string header = read_file(WEB_DIR "header.html");
|
||||
std::string content = read_file(WEB_DIR "clients.html");
|
||||
response->write(header + content);
|
||||
}
|
||||
|
||||
void getConfigPage(resp_https_t response, req_https_t request) {
|
||||
if(!authenticate(response, request)) return;
|
||||
|
||||
print_req(request);
|
||||
|
||||
std::string header = read_file(WEB_DIR "header.html");
|
||||
std::string content = read_file(WEB_DIR "config.html");
|
||||
response->write(header + content);
|
||||
}
|
||||
|
||||
void getPasswordPage(resp_https_t response, req_https_t request) {
|
||||
if(!authenticate(response, request)) return;
|
||||
|
||||
print_req(request);
|
||||
|
||||
std::string header = read_file(WEB_DIR "header.html");
|
||||
std::string content = read_file(WEB_DIR "password.html");
|
||||
response->write(header + content);
|
||||
}
|
||||
|
||||
void getApps(resp_https_t response, req_https_t request) {
|
||||
if(!authenticate(response, request)) return;
|
||||
|
||||
print_req(request);
|
||||
|
||||
std::string content = read_file(config::stream.file_apps.c_str());
|
||||
response->write(content);
|
||||
}
|
||||
|
||||
void saveApp(resp_https_t response, req_https_t request) {
|
||||
if(!authenticate(response, request)) return;
|
||||
|
||||
print_req(request);
|
||||
|
||||
std::stringstream ss;
|
||||
ss << request->content.rdbuf();
|
||||
|
||||
pt::ptree outputTree;
|
||||
auto g = util::fail_guard([&]() {
|
||||
std::ostringstream data;
|
||||
|
||||
pt::write_json(data, outputTree);
|
||||
response->write(data.str());
|
||||
});
|
||||
|
||||
pt::ptree inputTree, fileTree;
|
||||
|
||||
BOOST_LOG(fatal) << config::stream.file_apps;
|
||||
try {
|
||||
//TODO: Input Validation
|
||||
pt::read_json(ss, inputTree);
|
||||
pt::read_json(config::stream.file_apps, fileTree);
|
||||
|
||||
if(inputTree.get_child("prep-cmd").empty()) {
|
||||
inputTree.erase("prep-cmd");
|
||||
}
|
||||
|
||||
if(inputTree.get_child("detached").empty()) {
|
||||
inputTree.erase("detached");
|
||||
}
|
||||
|
||||
auto &apps_node = fileTree.get_child("apps"s);
|
||||
int index = inputTree.get<int>("index");
|
||||
|
||||
inputTree.erase("index");
|
||||
|
||||
if(index == -1) {
|
||||
apps_node.push_back(std::make_pair("", inputTree));
|
||||
}
|
||||
else {
|
||||
//Unfortuantely Boost PT does not allow to directly edit the array, copy should do the trick
|
||||
pt::ptree newApps;
|
||||
int i = 0;
|
||||
for(const auto &kv : apps_node) {
|
||||
if(i == index) {
|
||||
newApps.push_back(std::make_pair("", inputTree));
|
||||
}
|
||||
else {
|
||||
newApps.push_back(std::make_pair("", kv.second));
|
||||
}
|
||||
i++;
|
||||
}
|
||||
fileTree.erase("apps");
|
||||
fileTree.push_back(std::make_pair("apps", newApps));
|
||||
}
|
||||
pt::write_json(config::stream.file_apps, fileTree);
|
||||
}
|
||||
catch(std::exception &e) {
|
||||
BOOST_LOG(warning) << "SaveApp: "sv << e.what();
|
||||
|
||||
outputTree.put("status", "false");
|
||||
outputTree.put("error", "Invalid Input JSON");
|
||||
return;
|
||||
}
|
||||
|
||||
outputTree.put("status", "true");
|
||||
proc::refresh(config::stream.file_apps);
|
||||
}
|
||||
|
||||
void deleteApp(resp_https_t response, req_https_t request) {
|
||||
if(!authenticate(response, request)) return;
|
||||
|
||||
print_req(request);
|
||||
|
||||
pt::ptree outputTree;
|
||||
auto g = util::fail_guard([&]() {
|
||||
std::ostringstream data;
|
||||
|
||||
pt::write_json(data, outputTree);
|
||||
response->write(data.str());
|
||||
});
|
||||
pt::ptree fileTree;
|
||||
try {
|
||||
pt::read_json(config::stream.file_apps, fileTree);
|
||||
auto &apps_node = fileTree.get_child("apps"s);
|
||||
int index = stoi(request->path_match[1]);
|
||||
|
||||
if(index < 0) {
|
||||
outputTree.put("status", "false");
|
||||
outputTree.put("error", "Invalid Index");
|
||||
return;
|
||||
}
|
||||
else {
|
||||
//Unfortuantely Boost PT does not allow to directly edit the array, copy should do the trick
|
||||
pt::ptree newApps;
|
||||
int i = 0;
|
||||
for(const auto &kv : apps_node) {
|
||||
if(i++ != index) {
|
||||
newApps.push_back(std::make_pair("", kv.second));
|
||||
}
|
||||
}
|
||||
fileTree.erase("apps");
|
||||
fileTree.push_back(std::make_pair("apps", newApps));
|
||||
}
|
||||
pt::write_json(config::stream.file_apps, fileTree);
|
||||
}
|
||||
catch(std::exception &e) {
|
||||
BOOST_LOG(warning) << "DeleteApp: "sv << e.what();
|
||||
outputTree.put("status", "false");
|
||||
outputTree.put("error", "Invalid File JSON");
|
||||
return;
|
||||
}
|
||||
|
||||
outputTree.put("status", "true");
|
||||
proc::refresh(config::stream.file_apps);
|
||||
}
|
||||
|
||||
void getConfig(resp_https_t response, req_https_t request) {
|
||||
if(!authenticate(response, request)) return;
|
||||
|
||||
print_req(request);
|
||||
|
||||
pt::ptree outputTree;
|
||||
auto g = util::fail_guard([&]() {
|
||||
std::ostringstream data;
|
||||
|
||||
pt::write_json(data, outputTree);
|
||||
response->write(data.str());
|
||||
});
|
||||
|
||||
outputTree.put("status", "true");
|
||||
outputTree.put("platform", SUNSHINE_PLATFORM);
|
||||
|
||||
auto vars = config::parse_config(read_file(config::sunshine.config_file.c_str()));
|
||||
|
||||
for(auto &[name, value] : vars) {
|
||||
outputTree.put(std::move(name), std::move(value));
|
||||
}
|
||||
}
|
||||
|
||||
void saveConfig(resp_https_t response, req_https_t request) {
|
||||
if(!authenticate(response, request)) return;
|
||||
|
||||
print_req(request);
|
||||
|
||||
std::stringstream ss;
|
||||
std::stringstream configStream;
|
||||
ss << request->content.rdbuf();
|
||||
pt::ptree outputTree;
|
||||
auto g = util::fail_guard([&]() {
|
||||
std::ostringstream data;
|
||||
|
||||
pt::write_json(data, outputTree);
|
||||
response->write(data.str());
|
||||
});
|
||||
pt::ptree inputTree;
|
||||
try {
|
||||
//TODO: Input Validation
|
||||
pt::read_json(ss, inputTree);
|
||||
for(const auto &kv : inputTree) {
|
||||
std::string value = inputTree.get<std::string>(kv.first);
|
||||
if(value.length() == 0 || value.compare("null") == 0) continue;
|
||||
|
||||
configStream << kv.first << " = " << value << std::endl;
|
||||
}
|
||||
write_file(config::sunshine.config_file.c_str(), configStream.str());
|
||||
}
|
||||
catch(std::exception &e) {
|
||||
BOOST_LOG(warning) << "SaveConfig: "sv << e.what();
|
||||
outputTree.put("status", "false");
|
||||
outputTree.put("error", e.what());
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void savePassword(resp_https_t response, req_https_t request) {
|
||||
if(!authenticate(response, request)) return;
|
||||
|
||||
print_req(request);
|
||||
|
||||
std::stringstream ss;
|
||||
std::stringstream configStream;
|
||||
ss << request->content.rdbuf();
|
||||
|
||||
pt::ptree inputTree, outputTree;
|
||||
|
||||
auto g = util::fail_guard([&]() {
|
||||
std::ostringstream data;
|
||||
pt::write_json(data, outputTree);
|
||||
response->write(data.str());
|
||||
});
|
||||
|
||||
try {
|
||||
//TODO: Input Validation
|
||||
pt::read_json(ss, inputTree);
|
||||
auto username = inputTree.get<std::string>("currentUsername");
|
||||
auto newUsername = inputTree.get<std::string>("newUsername");
|
||||
auto password = inputTree.get<std::string>("currentPassword");
|
||||
auto newPassword = inputTree.get<std::string>("newPassword");
|
||||
auto confirmPassword = inputTree.get<std::string>("confirmNewPassword");
|
||||
if(newUsername.length() == 0) newUsername = username;
|
||||
|
||||
auto hash = util::hex(crypto::hash(password + config::sunshine.salt)).to_string();
|
||||
if(username == config::sunshine.username && hash == config::sunshine.password) {
|
||||
if(newPassword != confirmPassword) {
|
||||
outputTree.put("status", false);
|
||||
outputTree.put("error", "Password Mismatch");
|
||||
}
|
||||
|
||||
http::save_user_creds(config::sunshine.credentials_file, newUsername, newPassword);
|
||||
http::reload_user_creds(config::sunshine.credentials_file);
|
||||
outputTree.put("status", true);
|
||||
}
|
||||
else {
|
||||
outputTree.put("status", false);
|
||||
outputTree.put("error", "Invalid Current Credentials");
|
||||
}
|
||||
}
|
||||
catch(std::exception &e) {
|
||||
BOOST_LOG(warning) << "SavePassword: "sv << e.what();
|
||||
outputTree.put("status", false);
|
||||
outputTree.put("error", e.what());
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void savePin(resp_https_t response, req_https_t request) {
|
||||
if(!authenticate(response, request)) return;
|
||||
|
||||
print_req(request);
|
||||
|
||||
std::stringstream ss;
|
||||
ss << request->content.rdbuf();
|
||||
|
||||
pt::ptree inputTree, outputTree;
|
||||
|
||||
auto g = util::fail_guard([&]() {
|
||||
std::ostringstream data;
|
||||
pt::write_json(data, outputTree);
|
||||
response->write(data.str());
|
||||
});
|
||||
|
||||
try {
|
||||
//TODO: Input Validation
|
||||
pt::read_json(ss, inputTree);
|
||||
std::string pin = inputTree.get<std::string>("pin");
|
||||
outputTree.put("status", nvhttp::pin(pin));
|
||||
}
|
||||
catch(std::exception &e) {
|
||||
BOOST_LOG(warning) << "SavePin: "sv << e.what();
|
||||
outputTree.put("status", false);
|
||||
outputTree.put("error", e.what());
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void start(std::shared_ptr<safe::signal_t> shutdown_event) {
|
||||
auto ctx = std::make_shared<boost::asio::ssl::context>(boost::asio::ssl::context::tls);
|
||||
ctx->use_certificate_chain_file(config::nvhttp.cert);
|
||||
ctx->use_private_key_file(config::nvhttp.pkey, boost::asio::ssl::context::pem);
|
||||
https_server_t server { ctx, 0 };
|
||||
server.default_resource = not_found;
|
||||
server.resource["^/$"]["GET"] = getIndexPage;
|
||||
server.resource["^/pin$"]["GET"] = getPinPage;
|
||||
server.resource["^/apps$"]["GET"] = getAppsPage;
|
||||
server.resource["^/clients$"]["GET"] = getClientsPage;
|
||||
server.resource["^/config$"]["GET"] = getConfigPage;
|
||||
server.resource["^/password$"]["GET"] = getPasswordPage;
|
||||
server.resource["^/api/pin"]["POST"] = savePin;
|
||||
server.resource["^/api/apps$"]["GET"] = getApps;
|
||||
server.resource["^/api/apps$"]["POST"] = saveApp;
|
||||
server.resource["^/api/config$"]["GET"] = getConfig;
|
||||
server.resource["^/api/config$"]["POST"] = saveConfig;
|
||||
server.resource["^/api/password$"]["POST"] = savePassword;
|
||||
server.resource["^/api/apps/([0-9]+)$"]["DELETE"] = deleteApp;
|
||||
server.config.reuse_address = true;
|
||||
server.config.address = "0.0.0.0"s;
|
||||
server.config.port = PORT_HTTPS;
|
||||
|
||||
try {
|
||||
server.bind();
|
||||
BOOST_LOG(info) << "Configuration UI available at [https://localhost:"sv << PORT_HTTPS << "]";
|
||||
}
|
||||
catch(boost::system::system_error &err) {
|
||||
BOOST_LOG(fatal) << "Couldn't bind http server to ports ["sv << PORT_HTTPS << "]: "sv << err.what();
|
||||
|
||||
shutdown_event->raise(true);
|
||||
return;
|
||||
}
|
||||
auto accept_and_run = [&](auto *server) {
|
||||
try {
|
||||
server->accept_and_run();
|
||||
}
|
||||
catch(boost::system::system_error &err) {
|
||||
// It's possible the exception gets thrown after calling server->stop() from a different thread
|
||||
if(shutdown_event->peek()) {
|
||||
return;
|
||||
}
|
||||
|
||||
BOOST_LOG(fatal) << "Couldn't start Configuration HTTP server to ports ["sv << PORT_HTTPS << ", "sv << PORT_HTTPS << "]: "sv << err.what();
|
||||
shutdown_event->raise(true);
|
||||
return;
|
||||
}
|
||||
};
|
||||
std::thread tcp { accept_and_run, &server };
|
||||
|
||||
// Wait for any event
|
||||
shutdown_event->view();
|
||||
|
||||
server.stop();
|
||||
|
||||
tcp.join();
|
||||
}
|
||||
} // namespace confighttp
|
||||
20
sunshine/confighttp.h
Normal file
20
sunshine/confighttp.h
Normal file
@@ -0,0 +1,20 @@
|
||||
//
|
||||
// Created by loki on 6/3/19.
|
||||
//
|
||||
|
||||
#ifndef SUNSHINE_CONFIGHTTP_H
|
||||
#define SUNSHINE_CONFIGHTTP_H
|
||||
|
||||
#include <functional>
|
||||
#include <string>
|
||||
|
||||
#include "thread_safe.h"
|
||||
|
||||
#define WEB_DIR SUNSHINE_ASSETS_DIR "/web/"
|
||||
|
||||
|
||||
namespace confighttp {
|
||||
void start(std::shared_ptr<safe::signal_t> shutdown_event);
|
||||
}
|
||||
|
||||
#endif //SUNSHINE_CONFIGHTTP_H
|
||||
@@ -2,14 +2,14 @@
|
||||
// Created by loki on 5/31/19.
|
||||
//
|
||||
|
||||
#include <openssl/pem.h>
|
||||
#include "crypto.h"
|
||||
#include <openssl/pem.h>
|
||||
namespace crypto {
|
||||
using big_num_t = util::safe_ptr<BIGNUM, BN_free>;
|
||||
//using rsa_t = util::safe_ptr<RSA, RSA_free>;
|
||||
using asn1_string_t = util::safe_ptr<ASN1_STRING, ASN1_STRING_free>;
|
||||
|
||||
cert_chain_t::cert_chain_t() : _certs {}, _cert_ctx {X509_STORE_CTX_new() } {}
|
||||
cert_chain_t::cert_chain_t() : _certs {}, _cert_ctx { X509_STORE_CTX_new() } {}
|
||||
void cert_chain_t::add(x509_t &&cert) {
|
||||
x509_store_t x509_store { X509_STORE_new() };
|
||||
|
||||
@@ -26,7 +26,7 @@ void cert_chain_t::add(x509_t &&cert) {
|
||||
*/
|
||||
const char *cert_chain_t::verify(x509_t::element_type *cert) {
|
||||
int err_code = 0;
|
||||
for(auto &[_,x509_store] : _certs) {
|
||||
for(auto &[_, x509_store] : _certs) {
|
||||
auto fg = util::fail_guard([this]() {
|
||||
X509_STORE_CTX_cleanup(_cert_ctx.get());
|
||||
});
|
||||
@@ -36,7 +36,7 @@ const char *cert_chain_t::verify(x509_t::element_type *cert) {
|
||||
|
||||
auto err = X509_verify_cert(_cert_ctx.get());
|
||||
|
||||
if (err == 1) {
|
||||
if(err == 1) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
@@ -46,7 +46,7 @@ const char *cert_chain_t::verify(x509_t::element_type *cert) {
|
||||
if(err_code == X509_V_ERR_UNABLE_TO_GET_ISSUER_CERT_LOCALLY) {
|
||||
return nullptr;
|
||||
}
|
||||
if (err_code != X509_V_ERR_DEPTH_ZERO_SELF_SIGNED_CERT && err_code != X509_V_ERR_INVALID_CA) {
|
||||
if(err_code != X509_V_ERR_DEPTH_ZERO_SELF_SIGNED_CERT && err_code != X509_V_ERR_INVALID_CA) {
|
||||
return X509_verify_cert_error_string(err_code);
|
||||
}
|
||||
}
|
||||
@@ -63,7 +63,7 @@ int cipher_t::decrypt(const std::string_view &cipher, std::vector<std::uint8_t>
|
||||
});
|
||||
|
||||
// Gen 7 servers use 128-bit AES ECB
|
||||
if (EVP_DecryptInit_ex(ctx.get(), EVP_aes_128_ecb(), nullptr, key.data(), nullptr) != 1) {
|
||||
if(EVP_DecryptInit_ex(ctx.get(), EVP_aes_128_ecb(), nullptr, key.data(), nullptr) != 1) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
@@ -72,11 +72,11 @@ int cipher_t::decrypt(const std::string_view &cipher, std::vector<std::uint8_t>
|
||||
plaintext.resize((cipher.size() + 15) / 16 * 16);
|
||||
auto size = (int)plaintext.size();
|
||||
// Encrypt into the caller's buffer, leaving room for the auth tag to be prepended
|
||||
if (EVP_DecryptUpdate(ctx.get(), plaintext.data(), &size, (const std::uint8_t*)cipher.data(), cipher.size()) != 1) {
|
||||
if(EVP_DecryptUpdate(ctx.get(), plaintext.data(), &size, (const std::uint8_t *)cipher.data(), cipher.size()) != 1) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (EVP_DecryptFinal_ex(ctx.get(), plaintext.data(), &len) != 1) {
|
||||
if(EVP_DecryptFinal_ex(ctx.get(), plaintext.data(), &len) != 1) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
@@ -85,7 +85,7 @@ int cipher_t::decrypt(const std::string_view &cipher, std::vector<std::uint8_t>
|
||||
}
|
||||
|
||||
int cipher_t::decrypt_gcm(aes_t &iv, const std::string_view &tagged_cipher,
|
||||
std::vector<std::uint8_t> &plaintext) {
|
||||
std::vector<std::uint8_t> &plaintext) {
|
||||
auto cipher = tagged_cipher.substr(16);
|
||||
auto tag = tagged_cipher.substr(0, 16);
|
||||
|
||||
@@ -93,15 +93,15 @@ int cipher_t::decrypt_gcm(aes_t &iv, const std::string_view &tagged_cipher,
|
||||
EVP_CIPHER_CTX_reset(ctx.get());
|
||||
});
|
||||
|
||||
if (EVP_DecryptInit_ex(ctx.get(), EVP_aes_128_gcm(), nullptr, nullptr, nullptr) != 1) {
|
||||
if(EVP_DecryptInit_ex(ctx.get(), EVP_aes_128_gcm(), nullptr, nullptr, nullptr) != 1) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (EVP_CIPHER_CTX_ctrl(ctx.get(), EVP_CTRL_GCM_SET_IVLEN, iv.size(), nullptr) != 1) {
|
||||
if(EVP_CIPHER_CTX_ctrl(ctx.get(), EVP_CTRL_GCM_SET_IVLEN, iv.size(), nullptr) != 1) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (EVP_DecryptInit_ex(ctx.get(), nullptr, nullptr, key.data(), iv.data()) != 1) {
|
||||
if(EVP_DecryptInit_ex(ctx.get(), nullptr, nullptr, key.data(), iv.data()) != 1) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
@@ -109,16 +109,16 @@ int cipher_t::decrypt_gcm(aes_t &iv, const std::string_view &tagged_cipher,
|
||||
plaintext.resize((cipher.size() + 15) / 16 * 16);
|
||||
|
||||
int size;
|
||||
if (EVP_DecryptUpdate(ctx.get(), plaintext.data(), &size, (const std::uint8_t*)cipher.data(), cipher.size()) != 1) {
|
||||
if(EVP_DecryptUpdate(ctx.get(), plaintext.data(), &size, (const std::uint8_t *)cipher.data(), cipher.size()) != 1) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (EVP_CIPHER_CTX_ctrl(ctx.get(), EVP_CTRL_GCM_SET_TAG, tag.size(), const_cast<char*>(tag.data())) != 1) {
|
||||
if(EVP_CIPHER_CTX_ctrl(ctx.get(), EVP_CTRL_GCM_SET_TAG, tag.size(), const_cast<char *>(tag.data())) != 1) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
int len = size;
|
||||
if (EVP_DecryptFinal_ex(ctx.get(), plaintext.data() + size, &len) != 1) {
|
||||
if(EVP_DecryptFinal_ex(ctx.get(), plaintext.data() + size, &len) != 1) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
@@ -134,7 +134,7 @@ int cipher_t::encrypt(const std::string_view &plaintext, std::vector<std::uint8_
|
||||
});
|
||||
|
||||
// Gen 7 servers use 128-bit AES ECB
|
||||
if (EVP_EncryptInit_ex(ctx.get(), EVP_aes_128_ecb(), nullptr, key.data(), nullptr) != 1) {
|
||||
if(EVP_EncryptInit_ex(ctx.get(), EVP_aes_128_ecb(), nullptr, key.data(), nullptr) != 1) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
@@ -143,11 +143,11 @@ int cipher_t::encrypt(const std::string_view &plaintext, std::vector<std::uint8_
|
||||
cipher.resize((plaintext.size() + 15) / 16 * 16);
|
||||
auto size = (int)cipher.size();
|
||||
// Encrypt into the caller's buffer
|
||||
if (EVP_EncryptUpdate(ctx.get(), cipher.data(), &size, (const std::uint8_t*)plaintext.data(), plaintext.size()) != 1) {
|
||||
if(EVP_EncryptUpdate(ctx.get(), cipher.data(), &size, (const std::uint8_t *)plaintext.data(), plaintext.size()) != 1) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (EVP_EncryptFinal_ex(ctx.get(), cipher.data() + size, &len) != 1) {
|
||||
if(EVP_EncryptFinal_ex(ctx.get(), cipher.data() + size, &len) != 1) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
@@ -187,10 +187,10 @@ x509_t x509(const std::string_view &x) {
|
||||
|
||||
BIO_write(io.get(), x.data(), x.size());
|
||||
|
||||
X509 *p = nullptr;
|
||||
x509_t p;
|
||||
PEM_read_bio_X509(io.get(), &p, nullptr, nullptr);
|
||||
|
||||
return x509_t { p };
|
||||
return p;
|
||||
}
|
||||
|
||||
pkey_t pkey(const std::string_view &k) {
|
||||
@@ -198,10 +198,10 @@ pkey_t pkey(const std::string_view &k) {
|
||||
|
||||
BIO_write(io.get(), k.data(), k.size());
|
||||
|
||||
EVP_PKEY *p = nullptr;
|
||||
pkey_t p = nullptr;
|
||||
PEM_read_bio_PrivateKey(io.get(), &p, nullptr, nullptr);
|
||||
|
||||
return pkey_t { p };
|
||||
return p;
|
||||
}
|
||||
|
||||
std::string pem(x509_t &x509) {
|
||||
@@ -230,14 +230,14 @@ std::string_view signature(const x509_t &x) {
|
||||
const ASN1_BIT_STRING *asn1 = nullptr;
|
||||
X509_get0_signature(&asn1, nullptr, x.get());
|
||||
|
||||
return { (const char*)asn1->data, (std::size_t)asn1->length };
|
||||
return { (const char *)asn1->data, (std::size_t)asn1->length };
|
||||
}
|
||||
|
||||
std::string rand(std::size_t bytes) {
|
||||
std::string r;
|
||||
r.resize(bytes);
|
||||
|
||||
RAND_bytes((uint8_t*)r.data(), r.size());
|
||||
RAND_bytes((uint8_t *)r.data(), r.size());
|
||||
|
||||
return r;
|
||||
}
|
||||
@@ -297,8 +297,8 @@ creds_t gen_creds(const std::string_view &cn, std::uint32_t key_bits) {
|
||||
X509_set_pubkey(x509.get(), pkey.get());
|
||||
|
||||
auto name = X509_get_subject_name(x509.get());
|
||||
X509_NAME_add_entry_by_txt(name,"CN", MBSTRING_ASC,
|
||||
(const std::uint8_t*)cn.data(), cn.size(),
|
||||
X509_NAME_add_entry_by_txt(name, "CN", MBSTRING_ASC,
|
||||
(const std::uint8_t *)cn.data(), cn.size(),
|
||||
-1, 0);
|
||||
|
||||
X509_set_issuer_name(x509.get(), name);
|
||||
@@ -324,7 +324,7 @@ bool verify(const x509_t &x509, const std::string_view &data, const std::string_
|
||||
return false;
|
||||
}
|
||||
|
||||
if(EVP_DigestVerifyFinal(ctx.get(), (const uint8_t*)signature.data(), signature.size()) != 1) {
|
||||
if(EVP_DigestVerifyFinal(ctx.get(), (const uint8_t *)signature.data(), signature.size()) != 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -338,4 +338,14 @@ bool verify256(const x509_t &x509, const std::string_view &data, const std::stri
|
||||
void md_ctx_destroy(EVP_MD_CTX *ctx) {
|
||||
EVP_MD_CTX_destroy(ctx);
|
||||
}
|
||||
|
||||
std::string rand_alphabet(std::size_t bytes, const std::string_view &alphabet) {
|
||||
auto value = rand(bytes);
|
||||
|
||||
for(std::size_t i = 0; i != value.size(); ++i) {
|
||||
value[i] = alphabet[value[i] % alphabet.length()];
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
} // namespace crypto
|
||||
@@ -5,12 +5,11 @@
|
||||
#ifndef SUNSHINE_CRYPTO_H
|
||||
#define SUNSHINE_CRYPTO_H
|
||||
|
||||
#include <cassert>
|
||||
#include <array>
|
||||
#include <openssl/evp.h>
|
||||
#include <openssl/rand.h>
|
||||
#include <openssl/sha.h>
|
||||
#include <openssl/x509.h>
|
||||
#include <openssl/rand.h>
|
||||
|
||||
#include "utility.h"
|
||||
|
||||
@@ -25,16 +24,17 @@ void md_ctx_destroy(EVP_MD_CTX *);
|
||||
|
||||
using sha256_t = std::array<std::uint8_t, SHA256_DIGEST_LENGTH>;
|
||||
|
||||
using aes_t = std::array<std::uint8_t, 16>;
|
||||
using x509_t = util::safe_ptr<X509, X509_free>;
|
||||
using x509_store_t = util::safe_ptr<X509_STORE, X509_STORE_free>;
|
||||
using aes_t = std::array<std::uint8_t, 16>;
|
||||
using x509_t = util::safe_ptr<X509, X509_free>;
|
||||
using x509_store_t = util::safe_ptr<X509_STORE, X509_STORE_free>;
|
||||
using x509_store_ctx_t = util::safe_ptr<X509_STORE_CTX, X509_STORE_CTX_free>;
|
||||
using cipher_ctx_t = util::safe_ptr<EVP_CIPHER_CTX, EVP_CIPHER_CTX_free>;
|
||||
using md_ctx_t = util::safe_ptr<EVP_MD_CTX, md_ctx_destroy>;
|
||||
using bio_t = util::safe_ptr<BIO, BIO_free_all>;
|
||||
using pkey_t = util::safe_ptr<EVP_PKEY, EVP_PKEY_free>;
|
||||
using cipher_ctx_t = util::safe_ptr<EVP_CIPHER_CTX, EVP_CIPHER_CTX_free>;
|
||||
using md_ctx_t = util::safe_ptr<EVP_MD_CTX, md_ctx_destroy>;
|
||||
using bio_t = util::safe_ptr<BIO, BIO_free_all>;
|
||||
using pkey_t = util::safe_ptr<EVP_PKEY, EVP_PKEY_free>;
|
||||
|
||||
sha256_t hash(const std::string_view &plaintext);
|
||||
|
||||
aes_t gen_aes_key(const std::array<uint8_t, 16> &salt, const std::string_view &pin);
|
||||
|
||||
x509_t x509(const std::string_view &x);
|
||||
@@ -50,6 +50,8 @@ creds_t gen_creds(const std::string_view &cn, std::uint32_t key_bits);
|
||||
std::string_view signature(const x509_t &x);
|
||||
|
||||
std::string rand(std::size_t bytes);
|
||||
std::string rand_alphabet(std::size_t bytes,
|
||||
const std::string_view &alphabet = std::string_view { "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!%&()=-" });
|
||||
|
||||
class cert_chain_t {
|
||||
public:
|
||||
@@ -58,6 +60,7 @@ public:
|
||||
void add(x509_t &&cert);
|
||||
|
||||
const char *verify(x509_t::element_type *cert);
|
||||
|
||||
private:
|
||||
std::vector<std::pair<x509_t, x509_store_t>> _certs;
|
||||
x509_store_ctx_t _cert_ctx;
|
||||
@@ -66,13 +69,14 @@ private:
|
||||
class cipher_t {
|
||||
public:
|
||||
cipher_t(const aes_t &key);
|
||||
cipher_t(cipher_t&&) noexcept = default;
|
||||
cipher_t &operator=(cipher_t&&) noexcept = default;
|
||||
cipher_t(cipher_t &&) noexcept = default;
|
||||
cipher_t &operator=(cipher_t &&) noexcept = default;
|
||||
|
||||
int encrypt(const std::string_view &plaintext, std::vector<std::uint8_t> &cipher);
|
||||
|
||||
int decrypt_gcm(aes_t &iv, const std::string_view &cipher, std::vector<std::uint8_t> &plaintext);
|
||||
int decrypt(const std::string_view &cipher, std::vector<std::uint8_t> &plaintext);
|
||||
|
||||
private:
|
||||
cipher_ctx_t ctx;
|
||||
aes_t key;
|
||||
@@ -80,6 +84,6 @@ private:
|
||||
public:
|
||||
bool padding;
|
||||
};
|
||||
}
|
||||
} // namespace crypto
|
||||
|
||||
#endif //SUNSHINE_CRYPTO_H
|
||||
|
||||
189
sunshine/httpcommon.cpp
Normal file
189
sunshine/httpcommon.cpp
Normal file
@@ -0,0 +1,189 @@
|
||||
#define BOOST_BIND_GLOBAL_PLACEHOLDERS
|
||||
|
||||
#include "process.h"
|
||||
|
||||
#include <filesystem>
|
||||
|
||||
#include <boost/property_tree/json_parser.hpp>
|
||||
#include <boost/property_tree/ptree.hpp>
|
||||
#include <boost/property_tree/xml_parser.hpp>
|
||||
|
||||
#include <boost/asio/ssl/context.hpp>
|
||||
|
||||
#include <Simple-Web-Server/server_http.hpp>
|
||||
#include <Simple-Web-Server/server_https.hpp>
|
||||
#include <boost/asio/ssl/context_base.hpp>
|
||||
|
||||
#include "config.h"
|
||||
#include "crypto.h"
|
||||
#include "httpcommon.h"
|
||||
#include "main.h"
|
||||
#include "network.h"
|
||||
#include "nvhttp.h"
|
||||
#include "platform/common.h"
|
||||
#include "rtsp.h"
|
||||
#include "utility.h"
|
||||
#include "uuid.h"
|
||||
|
||||
namespace http {
|
||||
using namespace std::literals;
|
||||
namespace fs = std::filesystem;
|
||||
namespace pt = boost::property_tree;
|
||||
|
||||
int reload_user_creds(const std::string &file);
|
||||
bool user_creds_exist(const std::string &file);
|
||||
|
||||
std::string unique_id;
|
||||
net::net_e origin_pin_allowed;
|
||||
|
||||
int init() {
|
||||
bool clean_slate = config::sunshine.flags[config::flag::FRESH_STATE];
|
||||
origin_pin_allowed = net::from_enum_string(config::nvhttp.origin_pin_allowed);
|
||||
if(clean_slate) {
|
||||
unique_id = util::uuid_t::generate().string();
|
||||
auto dir = std::filesystem::temp_directory_path() / "Sushine"sv;
|
||||
config::nvhttp.cert = (dir / ("cert-"s + unique_id)).string();
|
||||
config::nvhttp.pkey = (dir / ("pkey-"s + unique_id)).string();
|
||||
}
|
||||
|
||||
if(!fs::exists(config::nvhttp.pkey) || !fs::exists(config::nvhttp.cert)) {
|
||||
if(create_creds(config::nvhttp.pkey, config::nvhttp.cert)) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
if(!user_creds_exist(config::sunshine.credentials_file)) {
|
||||
if(save_user_creds(config::sunshine.credentials_file, "sunshine"s, crypto::rand_alphabet(16), true)) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
if(reload_user_creds(config::sunshine.credentials_file)) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int save_user_creds(const std::string &file, const std::string &username, const std::string &password, bool run_our_mouth) {
|
||||
pt::ptree outputTree;
|
||||
|
||||
if(fs::exists(file)) {
|
||||
try {
|
||||
pt::read_json(file, outputTree);
|
||||
}
|
||||
catch(std::exception &e) {
|
||||
BOOST_LOG(error) << "Couldn't read user credentials: "sv << e.what();
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
auto salt = crypto::rand_alphabet(16);
|
||||
outputTree.put("username", username);
|
||||
outputTree.put("salt", salt);
|
||||
outputTree.put("password", util::hex(crypto::hash(password + salt)).to_string());
|
||||
try {
|
||||
pt::write_json(file, outputTree);
|
||||
}
|
||||
catch(std::exception &e) {
|
||||
BOOST_LOG(error) << "generating user credentials: "sv << e.what();
|
||||
return -1;
|
||||
}
|
||||
|
||||
BOOST_LOG(info) << "New credentials have been created"sv;
|
||||
|
||||
if(run_our_mouth) {
|
||||
BOOST_LOG(info) << "Username: "sv << username;
|
||||
BOOST_LOG(info) << "Password: "sv << password;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
bool user_creds_exist(const std::string &file) {
|
||||
if(!fs::exists(file)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
pt::ptree inputTree;
|
||||
try {
|
||||
pt::read_json(file, inputTree);
|
||||
return inputTree.find("username") != inputTree.not_found() &&
|
||||
inputTree.find("password") != inputTree.not_found() &&
|
||||
inputTree.find("salt") != inputTree.not_found();
|
||||
}
|
||||
catch(std::exception &e) {
|
||||
BOOST_LOG(error) << "validating user credentials: "sv << e.what();
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
int reload_user_creds(const std::string &file) {
|
||||
pt::ptree inputTree;
|
||||
try {
|
||||
pt::read_json(file, inputTree);
|
||||
config::sunshine.username = inputTree.get<std::string>("username");
|
||||
config::sunshine.password = inputTree.get<std::string>("password");
|
||||
config::sunshine.salt = inputTree.get<std::string>("salt");
|
||||
}
|
||||
catch(std::exception &e) {
|
||||
BOOST_LOG(error) << "loading user credentials: "sv << e.what();
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int create_creds(const std::string &pkey, const std::string &cert) {
|
||||
fs::path pkey_path = pkey;
|
||||
fs::path cert_path = cert;
|
||||
|
||||
auto creds = crypto::gen_creds("Sunshine Gamestream Host"sv, 2048);
|
||||
|
||||
auto pkey_dir = pkey_path;
|
||||
auto cert_dir = cert_path;
|
||||
pkey_dir.remove_filename();
|
||||
cert_dir.remove_filename();
|
||||
|
||||
std::error_code err_code {};
|
||||
fs::create_directories(pkey_dir, err_code);
|
||||
if(err_code) {
|
||||
BOOST_LOG(error) << "Couldn't create directory ["sv << pkey_dir << "] :"sv << err_code.message();
|
||||
return -1;
|
||||
}
|
||||
|
||||
fs::create_directories(cert_dir, err_code);
|
||||
if(err_code) {
|
||||
BOOST_LOG(error) << "Couldn't create directory ["sv << cert_dir << "] :"sv << err_code.message();
|
||||
return -1;
|
||||
}
|
||||
|
||||
if(write_file(pkey.c_str(), creds.pkey)) {
|
||||
BOOST_LOG(error) << "Couldn't open ["sv << config::nvhttp.pkey << ']';
|
||||
return -1;
|
||||
}
|
||||
|
||||
if(write_file(cert.c_str(), creds.x509)) {
|
||||
BOOST_LOG(error) << "Couldn't open ["sv << config::nvhttp.cert << ']';
|
||||
return -1;
|
||||
}
|
||||
|
||||
fs::permissions(pkey_path,
|
||||
fs::perms::owner_read | fs::perms::owner_write,
|
||||
fs::perm_options::replace, err_code);
|
||||
|
||||
if(err_code) {
|
||||
BOOST_LOG(error) << "Couldn't change permissions of ["sv << config::nvhttp.pkey << "] :"sv << err_code.message();
|
||||
return -1;
|
||||
}
|
||||
|
||||
fs::permissions(cert_path,
|
||||
fs::perms::owner_read | fs::perms::group_read | fs::perms::others_read | fs::perms::owner_write,
|
||||
fs::perm_options::replace, err_code);
|
||||
|
||||
if(err_code) {
|
||||
BOOST_LOG(error) << "Couldn't change permissions of ["sv << config::nvhttp.cert << "] :"sv << err_code.message();
|
||||
return -1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
} // namespace http
|
||||
18
sunshine/httpcommon.h
Normal file
18
sunshine/httpcommon.h
Normal file
@@ -0,0 +1,18 @@
|
||||
#include "network.h"
|
||||
#include "thread_safe.h"
|
||||
|
||||
namespace http {
|
||||
|
||||
int init();
|
||||
int create_creds(const std::string &pkey, const std::string &cert);
|
||||
int save_user_creds(
|
||||
const std::string &file,
|
||||
const std::string &username,
|
||||
const std::string &password,
|
||||
bool run_our_mouth = false);
|
||||
|
||||
int reload_user_creds(const std::string &file);
|
||||
extern std::string unique_id;
|
||||
extern net::net_e origin_pin_allowed;
|
||||
|
||||
} // namespace http
|
||||
@@ -2,26 +2,119 @@
|
||||
// Created by loki on 6/20/19.
|
||||
//
|
||||
|
||||
// define uint32_t for <moonlight-common-c/src/Input.h>
|
||||
#include <cstdint>
|
||||
extern "C" {
|
||||
#include <moonlight-common-c/src/Input.h>
|
||||
}
|
||||
|
||||
#include <cstring>
|
||||
#include <bitset>
|
||||
|
||||
#include "main.h"
|
||||
#include "config.h"
|
||||
#include "input.h"
|
||||
#include "main.h"
|
||||
#include "platform/common.h"
|
||||
#include "thread_pool.h"
|
||||
#include "utility.h"
|
||||
|
||||
namespace input {
|
||||
|
||||
constexpr auto MAX_GAMEPADS = std::min((std::size_t)platf::MAX_GAMEPADS, sizeof(std::int16_t) * 8);
|
||||
#define DISABLE_LEFT_BUTTON_DELAY ((util::ThreadPool::task_id_t)0x01)
|
||||
#define ENABLE_LEFT_BUTTON_DELAY nullptr
|
||||
|
||||
enum class button_state_e {
|
||||
NONE,
|
||||
DOWN,
|
||||
UP
|
||||
};
|
||||
|
||||
template<std::size_t N>
|
||||
int alloc_id(std::bitset<N> &gamepad_mask) {
|
||||
for(int x = 0; x < gamepad_mask.size(); ++x) {
|
||||
if(!gamepad_mask[x]) {
|
||||
gamepad_mask[x] = true;
|
||||
return x;
|
||||
}
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
template<std::size_t N>
|
||||
void free_id(std::bitset<N> &gamepad_mask, int id) {
|
||||
gamepad_mask[id] = false;
|
||||
}
|
||||
|
||||
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 std::unordered_map<short, bool> key_press {};
|
||||
static std::array<std::uint8_t, 5> mouse_press {};
|
||||
|
||||
static platf::input_t platf_input;
|
||||
static std::bitset<platf::MAX_GAMEPADS> gamepadMask {};
|
||||
|
||||
void free_gamepad(platf::input_t &platf_input, int id) {
|
||||
platf::gamepad(platf_input, id, platf::gamepad_state_t {});
|
||||
platf::free_gamepad(platf_input, id);
|
||||
|
||||
free_id(gamepadMask, id);
|
||||
}
|
||||
struct gamepad_t {
|
||||
gamepad_t() : gamepad_state {}, back_timeout_id {}, id { -1 }, back_button_state { button_state_e::NONE } {}
|
||||
~gamepad_t() {
|
||||
if(id >= 0) {
|
||||
task_pool.push([id = this->id]() {
|
||||
free_gamepad(platf_input, id);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
platf::gamepad_state_t gamepad_state;
|
||||
|
||||
util::ThreadPool::task_id_t back_timeout_id;
|
||||
|
||||
int id;
|
||||
|
||||
// When emulating the HOME button, we may need to artificially release the back button.
|
||||
// Afterwards, the gamepad state on sunshine won't match the state on Moonlight.
|
||||
// To prevent Sunshine from sending erronious input data to the active application,
|
||||
// Sunshine forces the button to be in a specific state until the gamepad state matches that of
|
||||
// Moonlight once more.
|
||||
button_state_e back_button_state;
|
||||
};
|
||||
|
||||
struct input_t {
|
||||
input_t() : active_gamepad_state {}, gamepads(MAX_GAMEPADS), mouse_left_button_timeout {} {}
|
||||
|
||||
std::uint16_t active_gamepad_state;
|
||||
std::vector<gamepad_t> gamepads;
|
||||
|
||||
util::ThreadPool::task_id_t mouse_left_button_timeout;
|
||||
};
|
||||
|
||||
using namespace std::literals;
|
||||
|
||||
void print(PNV_MOUSE_MOVE_PACKET packet) {
|
||||
void print(PNV_REL_MOUSE_MOVE_PACKET packet) {
|
||||
BOOST_LOG(debug)
|
||||
<< "--begin mouse move packet--"sv << std::endl
|
||||
<< "--begin relative mouse move packet--"sv << std::endl
|
||||
<< "deltaX ["sv << util::endian::big(packet->deltaX) << ']' << std::endl
|
||||
<< "deltaY ["sv << util::endian::big(packet->deltaY) << ']' << std::endl
|
||||
<< "--end mouse move packet--"sv;
|
||||
<< "--end relative mouse move packet--"sv;
|
||||
}
|
||||
|
||||
void print(PNV_ABS_MOUSE_MOVE_PACKET packet) {
|
||||
BOOST_LOG(debug)
|
||||
<< "--begin absolute mouse move packet--"sv << std::endl
|
||||
<< "x ["sv << util::endian::big(packet->x) << ']' << std::endl
|
||||
<< "y ["sv << util::endian::big(packet->y) << ']' << std::endl
|
||||
<< "width ["sv << util::endian::big(packet->width) << ']' << std::endl
|
||||
<< "height ["sv << util::endian::big(packet->height) << ']' << std::endl
|
||||
<< "--end absolute mouse move packet--"sv;
|
||||
}
|
||||
|
||||
void print(PNV_MOUSE_BUTTON_PACKET packet) {
|
||||
@@ -65,80 +158,274 @@ void print(PNV_MULTI_CONTROLLER_PACKET packet) {
|
||||
|
||||
constexpr int PACKET_TYPE_SCROLL_OR_KEYBOARD = PACKET_TYPE_SCROLL;
|
||||
void print(void *input) {
|
||||
int input_type = util::endian::big(*(int*)input);
|
||||
int input_type = util::endian::big(*(int *)input);
|
||||
|
||||
switch(input_type) {
|
||||
case PACKET_TYPE_MOUSE_MOVE:
|
||||
print((PNV_MOUSE_MOVE_PACKET)input);
|
||||
break;
|
||||
case PACKET_TYPE_MOUSE_BUTTON:
|
||||
print((PNV_MOUSE_BUTTON_PACKET)input);
|
||||
break;
|
||||
case PACKET_TYPE_SCROLL_OR_KEYBOARD:
|
||||
{
|
||||
char *tmp_input = (char*)input + 4;
|
||||
if(tmp_input[0] == 0x0A) {
|
||||
print((PNV_SCROLL_PACKET)input);
|
||||
}
|
||||
else {
|
||||
print((PNV_KEYBOARD_PACKET)input);
|
||||
}
|
||||
|
||||
break;
|
||||
case PACKET_TYPE_REL_MOUSE_MOVE:
|
||||
print((PNV_REL_MOUSE_MOVE_PACKET)input);
|
||||
break;
|
||||
case PACKET_TYPE_ABS_MOUSE_MOVE:
|
||||
print((PNV_ABS_MOUSE_MOVE_PACKET)input);
|
||||
break;
|
||||
case PACKET_TYPE_MOUSE_BUTTON:
|
||||
print((PNV_MOUSE_BUTTON_PACKET)input);
|
||||
break;
|
||||
case PACKET_TYPE_SCROLL_OR_KEYBOARD: {
|
||||
char *tmp_input = (char *)input + 4;
|
||||
if(tmp_input[0] == 0x0A) {
|
||||
print((PNV_SCROLL_PACKET)input);
|
||||
}
|
||||
case PACKET_TYPE_MULTI_CONTROLLER:
|
||||
print((PNV_MULTI_CONTROLLER_PACKET)input);
|
||||
break;
|
||||
else {
|
||||
print((PNV_KEYBOARD_PACKET)input);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
case PACKET_TYPE_MULTI_CONTROLLER:
|
||||
print((PNV_MULTI_CONTROLLER_PACKET)input);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void passthrough(platf::input_t &input, PNV_MOUSE_MOVE_PACKET packet) {
|
||||
void passthrough(std::shared_ptr<input_t> &input, PNV_REL_MOUSE_MOVE_PACKET packet) {
|
||||
display_cursor = true;
|
||||
|
||||
platf::move_mouse(input, util::endian::big(packet->deltaX), util::endian::big(packet->deltaY));
|
||||
input->mouse_left_button_timeout = DISABLE_LEFT_BUTTON_DELAY;
|
||||
platf::move_mouse(platf_input, util::endian::big(packet->deltaX), util::endian::big(packet->deltaY));
|
||||
}
|
||||
|
||||
void passthrough(std::shared_ptr<input_t> &input, PNV_ABS_MOUSE_MOVE_PACKET packet) {
|
||||
display_cursor = true;
|
||||
|
||||
if(input->mouse_left_button_timeout == DISABLE_LEFT_BUTTON_DELAY) {
|
||||
input->mouse_left_button_timeout = ENABLE_LEFT_BUTTON_DELAY;
|
||||
}
|
||||
|
||||
if(touch_port_event->peek()) {
|
||||
touch_port = *touch_port_event->pop();
|
||||
}
|
||||
|
||||
float x = util::endian::big(packet->x);
|
||||
float y = util::endian::big(packet->y);
|
||||
|
||||
// Prevent divide by zero
|
||||
// Don't expect it to happen, but just in case
|
||||
if(!packet->width || !packet->height) {
|
||||
BOOST_LOG(warning) << "Moonlight passed invalid dimensions"sv;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
float width = util::endian::big(packet->width);
|
||||
float height = util::endian::big(packet->height);
|
||||
|
||||
auto scale_x = (float)touch_port.width / width;
|
||||
auto scale_y = (float)touch_port.height / height;
|
||||
|
||||
platf::abs_mouse(platf_input, touch_port, x * scale_x, y * scale_y);
|
||||
}
|
||||
|
||||
void passthrough(std::shared_ptr<input_t> &input, PNV_MOUSE_BUTTON_PACKET packet) {
|
||||
auto constexpr BUTTON_RELEASED = 0x09;
|
||||
|
||||
auto constexpr BUTTON_LEFT = 0x01;
|
||||
auto constexpr BUTTON_RIGHT = 0x03;
|
||||
|
||||
display_cursor = true;
|
||||
|
||||
auto button = util::endian::big(packet->button);
|
||||
if(button > 0 && button < input->mouse_press.size()) {
|
||||
input->mouse_press[button] = packet->action != BUTTON_RELEASED;
|
||||
}
|
||||
auto release = packet->action == BUTTON_RELEASED;
|
||||
|
||||
platf::button_mouse(input->input, button, packet->action == BUTTON_RELEASED);
|
||||
auto button = util::endian::big(packet->button);
|
||||
if(button > 0 && button < mouse_press.size()) {
|
||||
if(mouse_press[button] != release) {
|
||||
// button state is already what we want
|
||||
return;
|
||||
}
|
||||
|
||||
mouse_press[button] = !release;
|
||||
}
|
||||
///////////////////////////////////
|
||||
/*/
|
||||
* When Moonlight sends mouse input through absolute coordinates,
|
||||
* it's possible that BUTTON_RIGHT is pressed down immediately after releasing BUTTON_LEFT.
|
||||
* As a result, Sunshine will left click on hyperlinks in the browser before right clicking
|
||||
*
|
||||
* This can be solved by delaying BUTTON_LEFT, however, any delay on input is undesirable during gaming
|
||||
* As a compromise, Sunshine will only put delays on BUTTON_LEFT when
|
||||
* absolute mouse coordinates have been send.
|
||||
*
|
||||
* Try to make sure BUTTON_RIGHT gets called before BUTTON_LEFT is released.
|
||||
*
|
||||
* input->mouse_left_button_timeout can only be nullptr
|
||||
* when the last mouse coordinates were absolute
|
||||
/*/
|
||||
if(button == BUTTON_LEFT && release && !input->mouse_left_button_timeout) {
|
||||
auto f = [=]() {
|
||||
auto left_released = mouse_press[BUTTON_LEFT];
|
||||
if(left_released) {
|
||||
// Already released left button
|
||||
return;
|
||||
}
|
||||
platf::button_mouse(platf_input, BUTTON_LEFT, release);
|
||||
|
||||
mouse_press[BUTTON_LEFT] = false;
|
||||
input->mouse_left_button_timeout = nullptr;
|
||||
};
|
||||
|
||||
input->mouse_left_button_timeout = task_pool.pushDelayed(std::move(f), 10ms).task_id;
|
||||
|
||||
return;
|
||||
}
|
||||
if(
|
||||
button == BUTTON_RIGHT && !release &&
|
||||
input->mouse_left_button_timeout > DISABLE_LEFT_BUTTON_DELAY) {
|
||||
platf::button_mouse(platf_input, BUTTON_RIGHT, false);
|
||||
platf::button_mouse(platf_input, BUTTON_RIGHT, true);
|
||||
|
||||
mouse_press[BUTTON_RIGHT] = false;
|
||||
|
||||
return;
|
||||
}
|
||||
///////////////////////////////////
|
||||
|
||||
platf::button_mouse(platf_input, button, release);
|
||||
}
|
||||
|
||||
void repeat_key(short key_code) {
|
||||
// If key no longer pressed, stop repeating
|
||||
if(!key_press[key_code]) {
|
||||
task_id = nullptr;
|
||||
return;
|
||||
}
|
||||
|
||||
platf::keyboard(platf_input, key_code & 0x00FF, false);
|
||||
|
||||
task_id = task_pool.pushDelayed(repeat_key, config::input.key_repeat_period, key_code).task_id;
|
||||
}
|
||||
|
||||
short map_keycode(short keycode) {
|
||||
keycode &= 0x00FF;
|
||||
|
||||
switch(keycode) {
|
||||
case 0x10:
|
||||
return 0xA0;
|
||||
case 0x11:
|
||||
return 0xA2;
|
||||
case 0x12:
|
||||
return 0xA4;
|
||||
}
|
||||
|
||||
return keycode;
|
||||
}
|
||||
void passthrough(std::shared_ptr<input_t> &input, PNV_KEYBOARD_PACKET packet) {
|
||||
auto constexpr BUTTON_RELEASED = 0x04;
|
||||
|
||||
input->key_press[packet->keyCode] = packet->keyAction != BUTTON_RELEASED;
|
||||
platf::keyboard(input->input, packet->keyCode & 0x00FF, packet->keyAction == BUTTON_RELEASED);
|
||||
auto release = packet->keyAction == BUTTON_RELEASED;
|
||||
|
||||
auto &pressed = key_press[packet->keyCode];
|
||||
if(!pressed) {
|
||||
if(!release) {
|
||||
if(task_id) {
|
||||
task_pool.cancel(task_id);
|
||||
}
|
||||
|
||||
if(config::input.key_repeat_delay.count() > 0) {
|
||||
task_id = task_pool.pushDelayed(repeat_key, config::input.key_repeat_delay, packet->keyCode).task_id;
|
||||
}
|
||||
}
|
||||
else {
|
||||
// Already released
|
||||
return;
|
||||
}
|
||||
}
|
||||
else if(!release) {
|
||||
// Already pressed down key
|
||||
return;
|
||||
}
|
||||
|
||||
pressed = !release;
|
||||
|
||||
platf::keyboard(platf_input, map_keycode(packet->keyCode), release);
|
||||
}
|
||||
|
||||
void passthrough(platf::input_t &input, PNV_SCROLL_PACKET packet) {
|
||||
void passthrough(PNV_SCROLL_PACKET packet) {
|
||||
display_cursor = true;
|
||||
|
||||
platf::scroll(input, util::endian::big(packet->scrollAmt1));
|
||||
platf::scroll(platf_input, util::endian::big(packet->scrollAmt1));
|
||||
}
|
||||
|
||||
int updateGamepads(std::vector<gamepad_t> &gamepads, std::int16_t old_state, std::int16_t new_state) {
|
||||
auto xorGamepadMask = old_state ^ new_state;
|
||||
if(!xorGamepadMask) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
for(int x = 0; x < sizeof(std::int16_t) * 8; ++x) {
|
||||
if((xorGamepadMask >> x) & 1) {
|
||||
auto &gamepad = gamepads[x];
|
||||
|
||||
if((old_state >> x) & 1) {
|
||||
if(gamepad.id < 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
free_gamepad(platf_input, gamepad.id);
|
||||
gamepad.id = -1;
|
||||
}
|
||||
else {
|
||||
auto id = alloc_id(gamepadMask);
|
||||
|
||||
if(id < 0) {
|
||||
// Out of gamepads
|
||||
return -1;
|
||||
}
|
||||
|
||||
if(platf::alloc_gamepad(platf_input, id)) {
|
||||
free_id(gamepadMask, id);
|
||||
// allocating a gamepad failed: solution: ignore gamepads
|
||||
// The implementations of platf::alloc_gamepad already has logging
|
||||
return -1;
|
||||
}
|
||||
|
||||
gamepad.id = id;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
void passthrough(std::shared_ptr<input_t> &input, PNV_MULTI_CONTROLLER_PACKET packet) {
|
||||
if(updateGamepads(input->gamepads, input->active_gamepad_state, packet->activeGamepadMask)) {
|
||||
return;
|
||||
}
|
||||
|
||||
input->active_gamepad_state = packet->activeGamepadMask;
|
||||
|
||||
if(packet->controllerNumber < 0 || packet->controllerNumber >= input->gamepads.size()) {
|
||||
BOOST_LOG(warning) << "ControllerNumber out of range ["sv << packet->controllerNumber << ']';
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if(!((input->active_gamepad_state >> packet->controllerNumber) & 1)) {
|
||||
BOOST_LOG(warning) << "ControllerNumber ["sv << packet->controllerNumber << "] not allocated"sv;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
auto &gamepad = input->gamepads[packet->controllerNumber];
|
||||
|
||||
// If this gamepad has not been initialized, ignore it.
|
||||
// This could happen when platf::alloc_gamepad fails
|
||||
if(gamepad.id < 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
display_cursor = false;
|
||||
|
||||
std::uint16_t bf;
|
||||
std::memcpy(&bf, &packet->buttonFlags, sizeof(std::uint16_t));
|
||||
|
||||
platf::gamepad_state_t gamepad_state{
|
||||
std::uint16_t bf = packet->buttonFlags;
|
||||
platf::gamepad_state_t gamepad_state {
|
||||
bf,
|
||||
packet->leftTrigger,
|
||||
packet->rightTrigger,
|
||||
@@ -150,31 +437,30 @@ void passthrough(std::shared_ptr<input_t> &input, PNV_MULTI_CONTROLLER_PACKET pa
|
||||
|
||||
auto bf_new = gamepad_state.buttonFlags;
|
||||
switch(gamepad.back_button_state) {
|
||||
case button_state_e::UP:
|
||||
if(!(platf::BACK & bf_new)) {
|
||||
gamepad.back_button_state = button_state_e::NONE;
|
||||
}
|
||||
gamepad_state.buttonFlags &= ~platf::BACK;
|
||||
break;
|
||||
case button_state_e::DOWN:
|
||||
if(platf::BACK & bf_new) {
|
||||
gamepad.back_button_state = button_state_e::NONE;
|
||||
}
|
||||
gamepad_state.buttonFlags |= platf::BACK;
|
||||
break;
|
||||
case button_state_e::NONE:
|
||||
break;
|
||||
case button_state_e::UP:
|
||||
if(!(platf::BACK & bf_new)) {
|
||||
gamepad.back_button_state = button_state_e::NONE;
|
||||
}
|
||||
gamepad_state.buttonFlags &= ~platf::BACK;
|
||||
break;
|
||||
case button_state_e::DOWN:
|
||||
if(platf::BACK & bf_new) {
|
||||
gamepad.back_button_state = button_state_e::NONE;
|
||||
}
|
||||
gamepad_state.buttonFlags |= platf::BACK;
|
||||
break;
|
||||
case button_state_e::NONE:
|
||||
break;
|
||||
}
|
||||
|
||||
bf = gamepad_state.buttonFlags ^ gamepad.gamepad_state.buttonFlags;
|
||||
bf = gamepad_state.buttonFlags ^ gamepad.gamepad_state.buttonFlags;
|
||||
bf_new = gamepad_state.buttonFlags;
|
||||
|
||||
if (platf::BACK & bf) {
|
||||
if (platf::BACK & bf_new) {
|
||||
|
||||
if(platf::BACK & bf) {
|
||||
if(platf::BACK & bf_new) {
|
||||
// Don't emulate home button if timeout < 0
|
||||
if(config::input.back_button_timeout >= 0ms) {
|
||||
gamepad.back_timeout_id = task_pool.pushDelayed([input, controller=packet->controllerNumber]() {
|
||||
auto f = [input, controller = packet->controllerNumber]() {
|
||||
auto &gamepad = input->gamepads[controller];
|
||||
|
||||
auto &state = gamepad.gamepad_state;
|
||||
@@ -182,27 +468,29 @@ void passthrough(std::shared_ptr<input_t> &input, PNV_MULTI_CONTROLLER_PACKET pa
|
||||
// Force the back button up
|
||||
gamepad.back_button_state = button_state_e::UP;
|
||||
state.buttonFlags &= ~platf::BACK;
|
||||
platf::gamepad(input->input, controller, state);
|
||||
platf::gamepad(platf_input, gamepad.id, state);
|
||||
|
||||
// Press Home button
|
||||
state.buttonFlags |= platf::HOME;
|
||||
platf::gamepad(input->input, controller, state);
|
||||
platf::gamepad(platf_input, gamepad.id, state);
|
||||
|
||||
// Release Home button
|
||||
state.buttonFlags &= ~platf::HOME;
|
||||
platf::gamepad(input->input, controller, state);
|
||||
platf::gamepad(platf_input, gamepad.id, state);
|
||||
|
||||
gamepad.back_timeout_id = nullptr;
|
||||
}, config::input.back_button_timeout).task_id;
|
||||
};
|
||||
|
||||
gamepad.back_timeout_id = task_pool.pushDelayed(std::move(f), config::input.back_button_timeout).task_id;
|
||||
}
|
||||
}
|
||||
else if (gamepad.back_timeout_id) {
|
||||
else if(gamepad.back_timeout_id) {
|
||||
task_pool.cancel(gamepad.back_timeout_id);
|
||||
gamepad.back_timeout_id = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
platf::gamepad(input->input, packet->controllerNumber, gamepad_state);
|
||||
platf::gamepad(platf_input, gamepad.id, gamepad_state);
|
||||
|
||||
gamepad.gamepad_state = gamepad_state;
|
||||
}
|
||||
@@ -210,60 +498,33 @@ void passthrough(std::shared_ptr<input_t> &input, PNV_MULTI_CONTROLLER_PACKET pa
|
||||
void passthrough_helper(std::shared_ptr<input_t> input, std::vector<std::uint8_t> &&input_data) {
|
||||
void *payload = input_data.data();
|
||||
|
||||
int input_type = util::endian::big(*(int*)payload);
|
||||
int input_type = util::endian::big(*(int *)payload);
|
||||
|
||||
switch(input_type) {
|
||||
case PACKET_TYPE_MOUSE_MOVE:
|
||||
passthrough(input->input, (PNV_MOUSE_MOVE_PACKET)payload);
|
||||
break;
|
||||
case PACKET_TYPE_MOUSE_BUTTON:
|
||||
passthrough(input, (PNV_MOUSE_BUTTON_PACKET)payload);
|
||||
break;
|
||||
case PACKET_TYPE_SCROLL_OR_KEYBOARD:
|
||||
{
|
||||
char *tmp_input = (char*)payload + 4;
|
||||
if(tmp_input[0] == 0x0A) {
|
||||
passthrough(input->input, (PNV_SCROLL_PACKET)payload);
|
||||
}
|
||||
else {
|
||||
passthrough(input, (PNV_KEYBOARD_PACKET)payload);
|
||||
}
|
||||
|
||||
break;
|
||||
case PACKET_TYPE_REL_MOUSE_MOVE:
|
||||
passthrough(input, (PNV_REL_MOUSE_MOVE_PACKET)payload);
|
||||
break;
|
||||
case PACKET_TYPE_ABS_MOUSE_MOVE:
|
||||
passthrough(input, (PNV_ABS_MOUSE_MOVE_PACKET)payload);
|
||||
break;
|
||||
case PACKET_TYPE_MOUSE_BUTTON:
|
||||
passthrough(input, (PNV_MOUSE_BUTTON_PACKET)payload);
|
||||
break;
|
||||
case PACKET_TYPE_SCROLL_OR_KEYBOARD: {
|
||||
char *tmp_input = (char *)payload + 4;
|
||||
if(tmp_input[0] == 0x0A) {
|
||||
passthrough((PNV_SCROLL_PACKET)payload);
|
||||
}
|
||||
case PACKET_TYPE_MULTI_CONTROLLER:
|
||||
passthrough(input, (PNV_MULTI_CONTROLLER_PACKET)payload);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void reset_helper(std::shared_ptr<input_t> input) {
|
||||
for(auto &[key_press, key_down] : input->key_press) {
|
||||
if(key_down) {
|
||||
key_down = false;
|
||||
platf::keyboard(input->input, key_press & 0x00FF, true);
|
||||
else {
|
||||
passthrough(input, (PNV_KEYBOARD_PACKET)payload);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
auto &mouse_press = input->mouse_press;
|
||||
for(int x = 0; x < mouse_press.size(); ++x) {
|
||||
if(mouse_press[x]) {
|
||||
mouse_press[x] = false;
|
||||
|
||||
platf::button_mouse(input->input, x + 1, true);
|
||||
}
|
||||
case PACKET_TYPE_MULTI_CONTROLLER:
|
||||
passthrough(input, (PNV_MULTI_CONTROLLER_PACKET)payload);
|
||||
break;
|
||||
}
|
||||
|
||||
NV_MULTI_CONTROLLER_PACKET fake_packet;
|
||||
fake_packet.buttonFlags = 0;
|
||||
fake_packet.leftStickX = 0;
|
||||
fake_packet.leftStickY = 0;
|
||||
fake_packet.rightStickX = 0;
|
||||
fake_packet.rightStickY = 0;
|
||||
fake_packet.leftTrigger = 0;
|
||||
fake_packet.rightTrigger = 0;
|
||||
|
||||
passthrough(input, &fake_packet);
|
||||
}
|
||||
|
||||
void passthrough(std::shared_ptr<input_t> &input, std::vector<std::uint8_t> &&input_data) {
|
||||
@@ -271,9 +532,40 @@ void passthrough(std::shared_ptr<input_t> &input, std::vector<std::uint8_t> &&in
|
||||
}
|
||||
|
||||
void reset(std::shared_ptr<input_t> &input) {
|
||||
task_pool.push(reset_helper, input);
|
||||
task_pool.cancel(task_id);
|
||||
task_pool.cancel(input->mouse_left_button_timeout);
|
||||
|
||||
// Ensure input is synchronous, by using the task_pool
|
||||
task_pool.push([]() {
|
||||
for(int x = 0; x < mouse_press.size(); ++x) {
|
||||
if(mouse_press[x]) {
|
||||
platf::button_mouse(platf_input, x, true);
|
||||
mouse_press[x] = false;
|
||||
}
|
||||
}
|
||||
|
||||
for(auto &kp : key_press) {
|
||||
platf::keyboard(platf_input, kp.first & 0x00FF, true);
|
||||
key_press[kp.first] = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
input_t::input_t() : mouse_press {}, input { platf::input() }, gamepads(platf::MAX_GAMEPADS) {}
|
||||
gamepad_t::gamepad_t() : gamepad_state {}, back_timeout_id {}, back_button_state { button_state_e::NONE } {}
|
||||
void init() {
|
||||
touch_port_event = std::make_unique<touch_port_event_t::element_type>();
|
||||
platf_input = platf::input();
|
||||
}
|
||||
|
||||
std::shared_ptr<input_t> alloc() {
|
||||
auto input = std::make_shared<input_t>();
|
||||
|
||||
// Workaround to ensure new frames will be captured when a client connects
|
||||
task_pool.pushDelayed([]() {
|
||||
platf::move_mouse(platf_input, 1, 1);
|
||||
platf::move_mouse(platf_input, -1, -1);
|
||||
},
|
||||
100ms);
|
||||
|
||||
return input;
|
||||
}
|
||||
} // namespace input
|
||||
|
||||
@@ -6,43 +6,22 @@
|
||||
#define SUNSHINE_INPUT_H
|
||||
|
||||
#include "platform/common.h"
|
||||
#include "thread_pool.h"
|
||||
|
||||
#include "thread_safe.h"
|
||||
namespace input {
|
||||
enum class button_state_e {
|
||||
NONE,
|
||||
DOWN,
|
||||
UP
|
||||
};
|
||||
|
||||
struct gamepad_t {
|
||||
gamepad_t();
|
||||
platf::gamepad_state_t gamepad_state;
|
||||
|
||||
util::ThreadPool::task_id_t back_timeout_id;
|
||||
|
||||
|
||||
// When emulating the HOME button, we may need to artificially release the back button.
|
||||
// Afterwards, the gamepad state on sunshine won't match the state on Moonlight.
|
||||
// To prevent Sunshine from sending erronious input data to the active application,
|
||||
// Sunshine forces the button to be in a specific state until the gamepad state matches that of
|
||||
// Moonlight once more.
|
||||
button_state_e back_button_state;
|
||||
};
|
||||
struct input_t {
|
||||
input_t();
|
||||
|
||||
std::unordered_map<short, bool> key_press;
|
||||
std::array<std::uint8_t, 5> mouse_press;
|
||||
|
||||
platf::input_t input;
|
||||
|
||||
std::vector<gamepad_t> gamepads;
|
||||
};
|
||||
struct input_t;
|
||||
|
||||
void print(void *input);
|
||||
void passthrough(std::shared_ptr<input_t> &input, std::vector<std::uint8_t> &&input_data);
|
||||
void reset(std::shared_ptr<input_t> &input);
|
||||
}
|
||||
void passthrough(std::shared_ptr<input_t> &input, std::vector<std::uint8_t> &&input_data);
|
||||
|
||||
|
||||
void init();
|
||||
|
||||
std::shared_ptr<input_t> alloc();
|
||||
|
||||
using touch_port_event_t = std::unique_ptr<safe::event_t<platf::touch_port_t>>;
|
||||
extern touch_port_event_t touch_port_event;
|
||||
} // namespace input
|
||||
|
||||
#endif //SUNSHINE_INPUT_H
|
||||
|
||||
@@ -4,23 +4,29 @@
|
||||
|
||||
#include "process.h"
|
||||
|
||||
#include <thread>
|
||||
#include <filesystem>
|
||||
#include <iostream>
|
||||
#include <csignal>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
#include <thread>
|
||||
|
||||
#include <boost/log/attributes/clock.hpp>
|
||||
#include <boost/log/common.hpp>
|
||||
#include <boost/log/sinks.hpp>
|
||||
#include <boost/log/expressions.hpp>
|
||||
#include <boost/log/sinks.hpp>
|
||||
#include <boost/log/sources/severity_logger.hpp>
|
||||
|
||||
#include "nvhttp.h"
|
||||
#include "stream.h"
|
||||
#include "config.h"
|
||||
#include "confighttp.h"
|
||||
#include "httpcommon.h"
|
||||
#include "nvhttp.h"
|
||||
#include "rtsp.h"
|
||||
#include "thread_pool.h"
|
||||
#include "video.h"
|
||||
|
||||
#include "platform/common.h"
|
||||
extern "C" {
|
||||
#include <libavutil/log.h>
|
||||
#include <rs.h>
|
||||
}
|
||||
|
||||
@@ -46,6 +52,27 @@ struct NoDelete {
|
||||
|
||||
BOOST_LOG_ATTRIBUTE_KEYWORD(severity, "Severity", int)
|
||||
|
||||
void print_help(const char *name) {
|
||||
std::cout
|
||||
<< "Usage: "sv << name << " [options] [/path/to/configuration_file] [--cmd]"sv << std::endl
|
||||
<< " Any configurable option can be overwritten with: \"name=value\""sv << std::endl
|
||||
<< std::endl
|
||||
<< " --help | print help"sv << std::endl
|
||||
<< " --creds username password | set user credentials for the Web manager" << std::endl
|
||||
<< std::endl
|
||||
<< " flags"sv << std::endl
|
||||
<< " -0 | Read PIN from stdin"sv << std::endl
|
||||
<< " -1 | Do not load previously saved state and do retain any state after shutdown"sv << std::endl
|
||||
<< " | Effectively starting as if for the first time without overwriting any pairings with your devices"sv << std::endl;
|
||||
}
|
||||
|
||||
namespace help {
|
||||
int entry(const char *name, int argc, char *argv[]) {
|
||||
print_help(name);
|
||||
return 0;
|
||||
}
|
||||
} // namespace help
|
||||
|
||||
void log_flush() {
|
||||
sink->flush();
|
||||
}
|
||||
@@ -62,17 +89,34 @@ void on_signal(int sig, FN &&fn) {
|
||||
std::signal(sig, on_signal_forwarder);
|
||||
}
|
||||
|
||||
int main(int argc, char *argv[]) {
|
||||
const char *config_file = SUNSHINE_ASSETS_DIR "/sunshine.conf";
|
||||
if(argc > 1) {
|
||||
config_file = argv[1];
|
||||
namespace gen_creds {
|
||||
int entry(const char *name, int argc, char *argv[]) {
|
||||
if(argc < 2 || argv[0] == "help"sv || argv[1] == "help"sv) {
|
||||
print_help(name);
|
||||
return 0;
|
||||
}
|
||||
|
||||
if(!std::filesystem::exists(config_file)) {
|
||||
std::cout << "Warning: Couldn't find configuration file ["sv << config_file << ']' << std::endl;
|
||||
http::save_user_creds(config::sunshine.credentials_file, argv[0], argv[1]);
|
||||
|
||||
return 0;
|
||||
}
|
||||
} // namespace gen_creds
|
||||
|
||||
std::map<std::string_view, std::function<int(const char *name, int argc, char **argv)>> cmd_to_func {
|
||||
{ "creds"sv, gen_creds::entry },
|
||||
{ "help"sv, help::entry }
|
||||
};
|
||||
|
||||
int main(int argc, char *argv[]) {
|
||||
if(config::parse(argc, argv)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if(config::sunshine.min_log_level >= 1) {
|
||||
av_log_set_level(AV_LOG_QUIET);
|
||||
}
|
||||
else {
|
||||
config::parse_file(config_file);
|
||||
av_log_set_level(AV_LOG_DEBUG);
|
||||
}
|
||||
|
||||
sink = boost::make_shared<text_sink>();
|
||||
@@ -81,37 +125,59 @@ int main(int argc, char *argv[]) {
|
||||
sink->locked_backend()->add_stream(stream);
|
||||
sink->set_filter(severity >= config::sunshine.min_log_level);
|
||||
|
||||
sink->set_formatter([severity="Severity"s](const bl::record_view &view, bl::formatting_ostream &os) {
|
||||
sink->set_formatter([message = "Message"s, severity = "Severity"s](const bl::record_view &view, bl::formatting_ostream &os) {
|
||||
constexpr int DATE_BUFFER_SIZE = 21 + 2 + 1; // Full string plus ": \0"
|
||||
|
||||
auto log_level = view.attribute_values()[severity].extract<int>().get();
|
||||
|
||||
std::string_view log_type;
|
||||
switch(log_level) {
|
||||
case 0:
|
||||
log_type = "Verbose: "sv;
|
||||
break;
|
||||
case 1:
|
||||
log_type = "Debug: "sv;
|
||||
break;
|
||||
case 2:
|
||||
log_type = "Info: "sv;
|
||||
break;
|
||||
case 3:
|
||||
log_type = "Warning: "sv;
|
||||
break;
|
||||
case 4:
|
||||
log_type = "Error: "sv;
|
||||
break;
|
||||
case 5:
|
||||
log_type = "Fatal: "sv;
|
||||
break;
|
||||
case 0:
|
||||
log_type = "Verbose: "sv;
|
||||
break;
|
||||
case 1:
|
||||
log_type = "Debug: "sv;
|
||||
break;
|
||||
case 2:
|
||||
log_type = "Info: "sv;
|
||||
break;
|
||||
case 3:
|
||||
log_type = "Warning: "sv;
|
||||
break;
|
||||
case 4:
|
||||
log_type = "Error: "sv;
|
||||
break;
|
||||
case 5:
|
||||
log_type = "Fatal: "sv;
|
||||
break;
|
||||
};
|
||||
|
||||
os << log_type << view.attribute_values()["Message"].extract<std::string>();
|
||||
char _date[DATE_BUFFER_SIZE];
|
||||
std::time_t t = std::time(nullptr);
|
||||
strftime(_date, DATE_BUFFER_SIZE, "[%Y:%m:%d:%H:%M:%S]: ", std::localtime(&t));
|
||||
|
||||
os << _date << log_type << view.attribute_values()[message].extract<std::string>();
|
||||
});
|
||||
|
||||
bl::core::get()->add_sink(sink);
|
||||
auto fg = util::fail_guard(log_flush);
|
||||
|
||||
if(!config::sunshine.cmd.name.empty()) {
|
||||
auto fn = cmd_to_func.find(config::sunshine.cmd.name);
|
||||
if(fn == std::end(cmd_to_func)) {
|
||||
BOOST_LOG(fatal) << "Unknown command: "sv << config::sunshine.cmd.name;
|
||||
|
||||
BOOST_LOG(info) << "Possible commands:"sv;
|
||||
for(auto &[key, _] : cmd_to_func) {
|
||||
BOOST_LOG(info) << '\t' << key;
|
||||
}
|
||||
|
||||
return 7;
|
||||
}
|
||||
|
||||
return fn->second(argv[0], config::sunshine.cmd.argc, config::sunshine.cmd.argv);
|
||||
}
|
||||
|
||||
// Create signal handler after logging has been initialized
|
||||
auto shutdown_event = std::make_shared<safe::event_t<bool>>();
|
||||
on_signal(SIGINT, [shutdown_event]() {
|
||||
@@ -119,21 +185,62 @@ int main(int argc, char *argv[]) {
|
||||
shutdown_event->raise(true);
|
||||
});
|
||||
|
||||
auto proc_opt = proc::parse(config::stream.file_apps);
|
||||
if(!proc_opt) {
|
||||
return 7;
|
||||
proc::refresh(config::stream.file_apps);
|
||||
|
||||
auto deinit_guard = platf::init();
|
||||
if(!deinit_guard) {
|
||||
return 4;
|
||||
}
|
||||
|
||||
proc::proc = std::move(*proc_opt);
|
||||
|
||||
reed_solomon_init();
|
||||
if(video::init()) {
|
||||
return 2;
|
||||
}
|
||||
if(http::init()) {
|
||||
return 3;
|
||||
}
|
||||
|
||||
task_pool.start(1);
|
||||
|
||||
std::thread httpThread { nvhttp::start, shutdown_event };
|
||||
std::thread configThread { confighttp::start, shutdown_event };
|
||||
stream::rtpThread(shutdown_event);
|
||||
|
||||
httpThread.join();
|
||||
configThread.join();
|
||||
|
||||
task_pool.stop();
|
||||
task_pool.join();
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
std::string read_file(const char *path) {
|
||||
if(!std::filesystem::exists(path)) {
|
||||
return {};
|
||||
}
|
||||
|
||||
std::ifstream in(path);
|
||||
|
||||
std::string input;
|
||||
std::string base64_cert;
|
||||
|
||||
while(!in.eof()) {
|
||||
std::getline(in, input);
|
||||
base64_cert += input + '\n';
|
||||
}
|
||||
|
||||
return base64_cert;
|
||||
}
|
||||
|
||||
int write_file(const char *path, const std::string_view &contents) {
|
||||
std::ofstream out(path);
|
||||
|
||||
if(!out.is_open()) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
out << contents;
|
||||
|
||||
return 0;
|
||||
}
|
||||
@@ -5,8 +5,8 @@
|
||||
#ifndef SUNSHINE_MAIN_H
|
||||
#define SUNSHINE_MAIN_H
|
||||
|
||||
#include <boost/log/common.hpp>
|
||||
#include "thread_pool.h"
|
||||
#include <boost/log/common.hpp>
|
||||
|
||||
extern util::ThreadPool task_pool;
|
||||
extern bool display_cursor;
|
||||
@@ -19,4 +19,9 @@ extern boost::log::sources::severity_logger<int> error;
|
||||
extern boost::log::sources::severity_logger<int> fatal;
|
||||
|
||||
void log_flush();
|
||||
|
||||
void print_help(const char *name);
|
||||
|
||||
std::string read_file(const char *path);
|
||||
int write_file(const char *path, const std::string_view &contents);
|
||||
#endif //SUNSHINE_MAIN_H
|
||||
|
||||
@@ -11,23 +11,24 @@ template<class T>
|
||||
class MoveByCopy {
|
||||
public:
|
||||
typedef T move_type;
|
||||
|
||||
private:
|
||||
move_type _to_move;
|
||||
public:
|
||||
|
||||
explicit MoveByCopy(move_type &&to_move) : _to_move(std::move(to_move)) { }
|
||||
public:
|
||||
explicit MoveByCopy(move_type &&to_move) : _to_move(std::move(to_move)) {}
|
||||
|
||||
MoveByCopy(MoveByCopy &&other) = default;
|
||||
|
||||
|
||||
MoveByCopy(const MoveByCopy &other) {
|
||||
*this = other;
|
||||
}
|
||||
|
||||
MoveByCopy& operator=(MoveByCopy &&other) = default;
|
||||
|
||||
MoveByCopy& operator=(const MoveByCopy &other) {
|
||||
this->_to_move = std::move(const_cast<MoveByCopy&>(other)._to_move);
|
||||
|
||||
MoveByCopy &operator=(MoveByCopy &&other) = default;
|
||||
|
||||
MoveByCopy &operator=(const MoveByCopy &other) {
|
||||
this->_to_move = std::move(const_cast<MoveByCopy &>(other)._to_move);
|
||||
|
||||
return *this;
|
||||
}
|
||||
|
||||
@@ -44,7 +45,7 @@ MoveByCopy<T> cmove(T &movable) {
|
||||
// Do NOT use this unless you are absolutely certain the object to be moved is no longer used by the caller
|
||||
template<class T>
|
||||
MoveByCopy<T> const_cmove(const T &movable) {
|
||||
return MoveByCopy<T>(std::move(const_cast<T&>(movable)));
|
||||
}
|
||||
return MoveByCopy<T>(std::move(const_cast<T &>(movable)));
|
||||
}
|
||||
} // namespace util
|
||||
#endif
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
// Created by loki on 12/27/19.
|
||||
//
|
||||
|
||||
#include <algorithm>
|
||||
#include "network.h"
|
||||
#include "utility.h"
|
||||
#include <algorithm>
|
||||
|
||||
namespace net {
|
||||
using namespace std::literals;
|
||||
@@ -21,17 +21,17 @@ std::vector<std::tuple<std::uint32_t, std::uint32_t>> lan_ips {
|
||||
};
|
||||
|
||||
std::uint32_t ip(const std::string_view &ip_str) {
|
||||
auto begin = std::begin(ip_str);
|
||||
auto end = std::end(ip_str);
|
||||
auto begin = std::begin(ip_str);
|
||||
auto end = std::end(ip_str);
|
||||
auto temp_end = std::find(begin, end, '.');
|
||||
|
||||
std::uint32_t ip = 0;
|
||||
auto shift = 24;
|
||||
auto shift = 24;
|
||||
while(temp_end != end) {
|
||||
ip += (util::from_chars(begin, temp_end) << shift);
|
||||
shift -= 8;
|
||||
|
||||
begin = temp_end + 1;
|
||||
begin = temp_end + 1;
|
||||
temp_end = std::find(begin, end, '.');
|
||||
}
|
||||
|
||||
@@ -43,7 +43,7 @@ std::uint32_t ip(const std::string_view &ip_str) {
|
||||
// In the format "xxx.xxx.xxx.xxx/x"
|
||||
std::pair<std::uint32_t, std::uint32_t> ip_block(const std::string_view &ip_str) {
|
||||
auto begin = std::begin(ip_str);
|
||||
auto end = std::find(begin, std::end(ip_str), '/');
|
||||
auto end = std::find(begin, std::end(ip_str), '/');
|
||||
|
||||
auto addr = ip({ begin, (std::size_t)(end - begin) });
|
||||
|
||||
@@ -82,15 +82,34 @@ net_e from_address(const std::string_view &view) {
|
||||
|
||||
std::string_view to_enum_string(net_e net) {
|
||||
switch(net) {
|
||||
case PC:
|
||||
return "pc"sv;
|
||||
case LAN:
|
||||
return "lan"sv;
|
||||
case WAN:
|
||||
return "wan"sv;
|
||||
case PC:
|
||||
return "pc"sv;
|
||||
case LAN:
|
||||
return "lan"sv;
|
||||
case WAN:
|
||||
return "wan"sv;
|
||||
}
|
||||
|
||||
// avoid warning
|
||||
return "wan"sv;
|
||||
}
|
||||
}
|
||||
|
||||
host_t host_create(ENetAddress &addr, std::size_t peers, std::uint16_t port) {
|
||||
enet_address_set_host(&addr, "0.0.0.0");
|
||||
enet_address_set_port(&addr, port);
|
||||
|
||||
return host_t { enet_host_create(AF_INET, &addr, peers, 1, 0, 0) };
|
||||
}
|
||||
|
||||
void free_host(ENetHost *host) {
|
||||
std::for_each(host->peers, host->peers + host->peerCount, [](ENetPeer &peer_ref) {
|
||||
ENetPeer *peer = &peer_ref;
|
||||
|
||||
if(peer) {
|
||||
enet_peer_disconnect_now(peer, 0);
|
||||
}
|
||||
});
|
||||
|
||||
enet_host_destroy(host);
|
||||
}
|
||||
} // namespace net
|
||||
@@ -6,7 +6,18 @@
|
||||
#define SUNSHINE_NETWORK_H
|
||||
|
||||
#include <tuple>
|
||||
|
||||
#include <enet/enet.h>
|
||||
|
||||
#include "utility.h"
|
||||
|
||||
namespace net {
|
||||
void free_host(ENetHost *host);
|
||||
|
||||
using host_t = util::safe_ptr<ENetHost, free_host>;
|
||||
using peer_t = ENetPeer *;
|
||||
using packet_t = util::safe_ptr<ENetPacket, enet_packet_destroy>;
|
||||
|
||||
enum net_e : int {
|
||||
PC,
|
||||
LAN,
|
||||
@@ -17,6 +28,8 @@ net_e from_enum_string(const std::string_view &view);
|
||||
std::string_view to_enum_string(net_e net);
|
||||
|
||||
net_e from_address(const std::string_view &view);
|
||||
}
|
||||
|
||||
host_t host_create(ENetAddress &addr, std::size_t peers, std::uint16_t port);
|
||||
} // namespace net
|
||||
|
||||
#endif //SUNSHINE_NETWORK_H
|
||||
|
||||
@@ -2,13 +2,15 @@
|
||||
// Created by loki on 6/3/19.
|
||||
//
|
||||
|
||||
#define BOOST_BIND_GLOBAL_PLACEHOLDERS
|
||||
|
||||
#include "process.h"
|
||||
|
||||
#include <filesystem>
|
||||
|
||||
#include <boost/property_tree/json_parser.hpp>
|
||||
#include <boost/property_tree/ptree.hpp>
|
||||
#include <boost/property_tree/xml_parser.hpp>
|
||||
#include <boost/property_tree/json_parser.hpp>
|
||||
|
||||
#include <boost/asio/ssl/context.hpp>
|
||||
|
||||
@@ -16,15 +18,17 @@
|
||||
#include <Simple-Web-Server/server_https.hpp>
|
||||
#include <boost/asio/ssl/context_base.hpp>
|
||||
|
||||
|
||||
#include "config.h"
|
||||
#include "utility.h"
|
||||
#include "stream.h"
|
||||
#include "crypto.h"
|
||||
#include "httpcommon.h"
|
||||
#include "main.h"
|
||||
#include "network.h"
|
||||
#include "nvhttp.h"
|
||||
#include "platform/common.h"
|
||||
#include "network.h"
|
||||
#include "rtsp.h"
|
||||
#include "utility.h"
|
||||
#include "uuid.h"
|
||||
#include "main.h"
|
||||
|
||||
|
||||
namespace nvhttp {
|
||||
using namespace std::literals;
|
||||
@@ -32,14 +36,11 @@ constexpr auto PORT_HTTP = 47989;
|
||||
constexpr auto PORT_HTTPS = 47984;
|
||||
|
||||
constexpr auto VERSION = "7.1.400.0";
|
||||
constexpr auto GFE_VERSION = "2.0.0.1";
|
||||
constexpr auto GFE_VERSION = "3.12.0.1";
|
||||
|
||||
namespace fs = std::filesystem;
|
||||
namespace pt = boost::property_tree;
|
||||
|
||||
std::string read_file(const char *path);
|
||||
int write_file(const char *path, const std::string_view &contents);
|
||||
|
||||
using https_server_t = SimpleWeb::Server<SimpleWeb::HTTPS>;
|
||||
using http_server_t = SimpleWeb::Server<SimpleWeb::HTTP>;
|
||||
|
||||
@@ -68,8 +69,8 @@ struct pair_session_t {
|
||||
struct {
|
||||
util::Either<
|
||||
std::shared_ptr<typename SimpleWeb::ServerBase<SimpleWeb::HTTP>::Response>,
|
||||
std::shared_ptr<typename SimpleWeb::ServerBase<SimpleWeb::HTTPS>::Response>
|
||||
> response;
|
||||
std::shared_ptr<typename SimpleWeb::ServerBase<SimpleWeb::HTTPS>::Response>>
|
||||
response;
|
||||
std::string salt;
|
||||
} async_insert_pin;
|
||||
};
|
||||
@@ -77,14 +78,12 @@ struct pair_session_t {
|
||||
// uniqueID, session
|
||||
std::unordered_map<std::string, pair_session_t> map_id_sess;
|
||||
std::unordered_map<std::string, client_t> map_id_client;
|
||||
std::string unique_id;
|
||||
net::net_e origin_pin_allowed;
|
||||
|
||||
using args_t = SimpleWeb::CaseInsensitiveMultimap;
|
||||
using args_t = SimpleWeb::CaseInsensitiveMultimap;
|
||||
using resp_https_t = std::shared_ptr<typename SimpleWeb::ServerBase<SimpleWeb::HTTPS>::Response>;
|
||||
using req_https_t = std::shared_ptr<typename SimpleWeb::ServerBase<SimpleWeb::HTTPS>::Request>;
|
||||
using resp_http_t = std::shared_ptr<typename SimpleWeb::ServerBase<SimpleWeb::HTTP>::Response>;
|
||||
using req_http_t = std::shared_ptr<typename SimpleWeb::ServerBase<SimpleWeb::HTTP>::Request>;
|
||||
using req_https_t = std::shared_ptr<typename SimpleWeb::ServerBase<SimpleWeb::HTTPS>::Request>;
|
||||
using resp_http_t = std::shared_ptr<typename SimpleWeb::ServerBase<SimpleWeb::HTTP>::Response>;
|
||||
using req_http_t = std::shared_ptr<typename SimpleWeb::ServerBase<SimpleWeb::HTTP>::Request>;
|
||||
|
||||
enum class op_e {
|
||||
ADD,
|
||||
@@ -94,9 +93,21 @@ enum class op_e {
|
||||
void save_state() {
|
||||
pt::ptree root;
|
||||
|
||||
root.put("root.uniqueid", unique_id);
|
||||
if(fs::exists(config::nvhttp.file_state)) {
|
||||
try {
|
||||
pt::read_json(config::nvhttp.file_state, root);
|
||||
}
|
||||
catch(std::exception &e) {
|
||||
BOOST_LOG(error) << "Couldn't read "sv << config::nvhttp.file_state << ": "sv << e.what();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
root.erase("root"s);
|
||||
|
||||
root.put("root.uniqueid", http::unique_id);
|
||||
auto &nodes = root.add_child("root.devices", pt::ptree {});
|
||||
for(auto &[_,client] : map_id_client) {
|
||||
for(auto &[_, client] : map_id_client) {
|
||||
pt::ptree node;
|
||||
|
||||
node.put("uniqueid"s, client.uniqueID);
|
||||
@@ -112,31 +123,44 @@ void save_state() {
|
||||
nodes.push_back(std::make_pair(""s, node));
|
||||
}
|
||||
|
||||
pt::write_json(config::nvhttp.file_state, root);
|
||||
try {
|
||||
pt::write_json(config::nvhttp.file_state, root);
|
||||
}
|
||||
catch(std::exception &e) {
|
||||
BOOST_LOG(error) << "Couldn't write "sv << config::nvhttp.file_state << ": "sv << e.what();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void load_state() {
|
||||
auto file_state = fs::current_path() / config::nvhttp.file_state;
|
||||
|
||||
if(!fs::exists(file_state)) {
|
||||
unique_id = util::uuid_t::generate().string();
|
||||
if(!fs::exists(config::nvhttp.file_state)) {
|
||||
BOOST_LOG(info) << "DOENST EXIST"sv;
|
||||
http::unique_id = util::uuid_t::generate().string();
|
||||
return;
|
||||
}
|
||||
|
||||
pt::ptree root;
|
||||
try {
|
||||
pt::read_json(config::nvhttp.file_state, root);
|
||||
} catch (std::exception &e) {
|
||||
BOOST_LOG(warning) << e.what();
|
||||
}
|
||||
catch(std::exception &e) {
|
||||
BOOST_LOG(error) << "Couldn't read "sv << config::nvhttp.file_state << ": "sv << e.what();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
unique_id = root.get<std::string>("root.uniqueid");
|
||||
auto unique_id_p = root.get_optional<std::string>("root.uniqueid");
|
||||
if(!unique_id_p) {
|
||||
// This file doesn't contain moonlight credentials
|
||||
http::unique_id = util::uuid_t::generate().string();
|
||||
return;
|
||||
}
|
||||
http::unique_id = std::move(*unique_id_p);
|
||||
|
||||
auto device_nodes = root.get_child("root.devices");
|
||||
|
||||
for(auto &[_,device_node] : device_nodes) {
|
||||
auto uniqID = device_node.get<std::string>("uniqueid");
|
||||
for(auto &[_, device_node] : device_nodes) {
|
||||
auto uniqID = device_node.get<std::string>("uniqueid");
|
||||
auto &client = map_id_client.emplace(uniqID, client_t {}).first->second;
|
||||
|
||||
client.uniqueID = uniqID;
|
||||
@@ -149,25 +173,47 @@ void load_state() {
|
||||
|
||||
void update_id_client(const std::string &uniqueID, std::string &&cert, op_e op) {
|
||||
switch(op) {
|
||||
case op_e::ADD:
|
||||
{
|
||||
auto &client = map_id_client[uniqueID];
|
||||
client.certs.emplace_back(std::move(cert));
|
||||
client.uniqueID = uniqueID;
|
||||
}
|
||||
break;
|
||||
case op_e::REMOVE:
|
||||
map_id_client.erase(uniqueID);
|
||||
break;
|
||||
case op_e::ADD: {
|
||||
auto &client = map_id_client[uniqueID];
|
||||
client.certs.emplace_back(std::move(cert));
|
||||
client.uniqueID = uniqueID;
|
||||
} break;
|
||||
case op_e::REMOVE:
|
||||
map_id_client.erase(uniqueID);
|
||||
break;
|
||||
}
|
||||
|
||||
save_state();
|
||||
if(!config::sunshine.flags[config::flag::FRESH_STATE]) {
|
||||
save_state();
|
||||
}
|
||||
}
|
||||
|
||||
stream::launch_session_t make_launch_session(bool host_audio, const args_t &args) {
|
||||
stream::launch_session_t launch_session;
|
||||
|
||||
launch_session.host_audio = host_audio;
|
||||
launch_session.gcm_key = *util::from_hex<crypto::aes_t>(args.at("rikey"s), true);
|
||||
uint32_t prepend_iv = util::endian::big<uint32_t>(util::from_view(args.at("rikeyid"s)));
|
||||
auto prepend_iv_p = (uint8_t *)&prepend_iv;
|
||||
|
||||
auto next = std::copy(prepend_iv_p, prepend_iv_p + sizeof(prepend_iv), std::begin(launch_session.iv));
|
||||
std::fill(next, std::end(launch_session.iv), 0);
|
||||
|
||||
return launch_session;
|
||||
}
|
||||
|
||||
void getservercert(pair_session_t &sess, pt::ptree &tree, const std::string &pin) {
|
||||
auto salt = util::from_hex<std::array<uint8_t, 16>>(sess.async_insert_pin.salt, true);
|
||||
if(sess.async_insert_pin.salt.size() < 32) {
|
||||
tree.put("root.paired", 0);
|
||||
tree.put("root.<xmlattr>.status_code", 400);
|
||||
return;
|
||||
}
|
||||
|
||||
auto key = crypto::gen_aes_key(*salt, pin);
|
||||
std::string_view salt_view { sess.async_insert_pin.salt.data(), 32 };
|
||||
|
||||
auto salt = util::from_hex<std::array<uint8_t, 16>>(salt_view, true);
|
||||
|
||||
auto key = crypto::gen_aes_key(*salt, pin);
|
||||
sess.cipher_key = std::make_unique<crypto::aes_t>(key);
|
||||
|
||||
tree.put("root.paired", 1);
|
||||
@@ -186,7 +232,7 @@ void serverchallengeresp(pair_session_t &sess, pt::ptree &tree, const args_t &ar
|
||||
sess.clienthash = std::move(decrypted);
|
||||
|
||||
auto serversecret = sess.serversecret;
|
||||
auto sign = crypto::sign256(crypto::pkey(conf_intern.pkey), serversecret);
|
||||
auto sign = crypto::sign256(crypto::pkey(conf_intern.pkey), serversecret);
|
||||
|
||||
serversecret.insert(std::end(serversecret), std::begin(sign), std::end(sign));
|
||||
|
||||
@@ -204,14 +250,14 @@ void clientchallenge(pair_session_t &sess, pt::ptree &tree, const args_t &args)
|
||||
std::vector<uint8_t> decrypted;
|
||||
cipher.decrypt(challenge, decrypted);
|
||||
|
||||
auto x509 = crypto::x509(conf_intern.servercert);
|
||||
auto sign = crypto::signature(x509);
|
||||
auto x509 = crypto::x509(conf_intern.servercert);
|
||||
auto sign = crypto::signature(x509);
|
||||
auto serversecret = crypto::rand(16);
|
||||
|
||||
decrypted.insert(std::end(decrypted), std::begin(sign), std::end(sign));
|
||||
decrypted.insert(std::end(decrypted), std::begin(serversecret), std::end(serversecret));
|
||||
|
||||
auto hash = crypto::hash({ (char*)decrypted.data(), decrypted.size() });
|
||||
auto hash = crypto::hash({ (char *)decrypted.data(), decrypted.size() });
|
||||
auto serverchallenge = crypto::rand(16);
|
||||
|
||||
std::string plaintext;
|
||||
@@ -241,7 +287,7 @@ void clientpairingsecret(std::shared_ptr<safe::queue_t<crypto::x509_t>> &add_cer
|
||||
|
||||
assert((secret.size() + sign.size()) == pairingsecret.size());
|
||||
|
||||
auto x509 = crypto::x509(client.cert);
|
||||
auto x509 = crypto::x509(client.cert);
|
||||
auto x509_sign = crypto::signature(x509);
|
||||
|
||||
std::string data;
|
||||
@@ -295,8 +341,8 @@ template<class T>
|
||||
void print_req(std::shared_ptr<typename SimpleWeb::ServerBase<T>::Request> request) {
|
||||
BOOST_LOG(debug) << "TUNNEL :: "sv << tunnel<T>::to_string;
|
||||
|
||||
BOOST_LOG(debug) << "METHOD :: "sv << request->method;
|
||||
BOOST_LOG(debug) << "DESTINATION :: "sv << request->path;
|
||||
BOOST_LOG(debug) << "METHOD :: "sv << request->method;
|
||||
BOOST_LOG(debug) << "DESTINATION :: "sv << request->path;
|
||||
|
||||
for(auto &[name, val] : request->header) {
|
||||
BOOST_LOG(debug) << name << " -- " << val;
|
||||
@@ -323,33 +369,56 @@ void not_found(std::shared_ptr<typename SimpleWeb::ServerBase<T>::Response> resp
|
||||
pt::write_xml(data, tree);
|
||||
response->write(data.str());
|
||||
|
||||
*response << "HTTP/1.1 404 NOT FOUND\r\n" << data.str();
|
||||
*response << "HTTP/1.1 404 NOT FOUND\r\n"
|
||||
<< data.str();
|
||||
}
|
||||
|
||||
template<class T>
|
||||
void pair(std::shared_ptr<safe::queue_t<crypto::x509_t>> &add_cert, std::shared_ptr<typename SimpleWeb::ServerBase<T>::Response> response, std::shared_ptr<typename SimpleWeb::ServerBase<T>::Request> request) {
|
||||
print_req<T>(request);
|
||||
|
||||
pt::ptree tree;
|
||||
|
||||
auto args = request->parse_query_string();
|
||||
if(args.find("uniqueid"s) == std::end(args)) {
|
||||
tree.put("root.<xmlattr>.status_code", 400);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
auto uniqID { std::move(args.at("uniqueid"s)) };
|
||||
auto sess_it = map_id_sess.find(uniqID);
|
||||
|
||||
pt::ptree tree;
|
||||
|
||||
args_t::const_iterator it;
|
||||
if(it = args.find("phrase"); it != std::end(args)) {
|
||||
if(it->second == "getservercert"sv) {
|
||||
pair_session_t sess;
|
||||
|
||||
|
||||
sess.client.uniqueID = std::move(uniqID);
|
||||
sess.client.cert = util::from_hex_vec(args.at("clientcert"s), true);
|
||||
sess.client.cert = util::from_hex_vec(args.at("clientcert"s), true);
|
||||
|
||||
BOOST_LOG(debug) << sess.client.cert;
|
||||
auto ptr = map_id_sess.emplace(sess.client.uniqueID, std::move(sess)).first;
|
||||
|
||||
ptr->second.async_insert_pin.salt = std::move(args.at("salt"s));
|
||||
ptr->second.async_insert_pin.response = std::move(response);
|
||||
return;
|
||||
|
||||
if(config::sunshine.flags[config::flag::CONST_PIN]) {
|
||||
std::string pin("6174");
|
||||
getservercert(ptr->second, tree, pin);
|
||||
}
|
||||
else if(config::sunshine.flags[config::flag::PIN_STDIN]) {
|
||||
std::string pin;
|
||||
|
||||
std::cout << "Please insert pin: "sv;
|
||||
std::getline(std::cin, pin);
|
||||
|
||||
getservercert(ptr->second, tree, pin);
|
||||
}
|
||||
else {
|
||||
ptr->second.async_insert_pin.response = std::move(response);
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
else if(it->second == "pairchallenge"sv) {
|
||||
tree.put("root.paired", 1);
|
||||
@@ -375,30 +444,14 @@ void pair(std::shared_ptr<safe::queue_t<crypto::x509_t>> &add_cert, std::shared_
|
||||
response->write(data.str());
|
||||
}
|
||||
|
||||
template<class T>
|
||||
void pin(std::shared_ptr<typename SimpleWeb::ServerBase<T>::Response> response, std::shared_ptr<typename SimpleWeb::ServerBase<T>::Request> request) {
|
||||
print_req<T>(request);
|
||||
|
||||
auto address = request->remote_endpoint_address();
|
||||
auto ip_type = net::from_address(address);
|
||||
if(ip_type > origin_pin_allowed) {
|
||||
BOOST_LOG(info) << '[' << address << "] -- denied"sv;
|
||||
|
||||
response->write(SimpleWeb::StatusCode::client_error_forbidden);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
bool pin(std::string pin) {
|
||||
pt::ptree tree;
|
||||
|
||||
if(map_id_sess.empty()) {
|
||||
response->write(SimpleWeb::StatusCode::client_error_im_a_teapot);
|
||||
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
auto &sess = std::begin(map_id_sess)->second;
|
||||
getservercert(sess, tree, request->path_match[1]);
|
||||
getservercert(sess, tree, pin);
|
||||
|
||||
// response to the request for pin
|
||||
std::ostringstream data;
|
||||
@@ -408,19 +461,40 @@ void pin(std::shared_ptr<typename SimpleWeb::ServerBase<T>::Response> response,
|
||||
if(async_response.has_left() && async_response.left()) {
|
||||
async_response.left()->write(data.str());
|
||||
}
|
||||
else if(async_response.has_right() && async_response.right()){
|
||||
else if(async_response.has_right() && async_response.right()) {
|
||||
async_response.right()->write(data.str());
|
||||
}
|
||||
else {
|
||||
response->write(SimpleWeb::StatusCode::client_error_im_a_teapot);
|
||||
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
// reset async_response
|
||||
async_response = std::decay_t<decltype(async_response.left())>();
|
||||
// response to the current request
|
||||
response->write(SimpleWeb::StatusCode::success_ok);
|
||||
return true;
|
||||
}
|
||||
|
||||
template<class T>
|
||||
void pin(std::shared_ptr<typename SimpleWeb::ServerBase<T>::Response> response, std::shared_ptr<typename SimpleWeb::ServerBase<T>::Request> request) {
|
||||
print_req<T>(request);
|
||||
|
||||
auto address = request->remote_endpoint_address();
|
||||
auto ip_type = net::from_address(address);
|
||||
if(ip_type > http::origin_pin_allowed) {
|
||||
BOOST_LOG(info) << '[' << address << "] -- denied"sv;
|
||||
|
||||
response->write(SimpleWeb::StatusCode::client_error_forbidden);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
bool pinResponse = pin(request->path_match[1]);
|
||||
if(pinResponse) {
|
||||
response->write(SimpleWeb::StatusCode::success_ok);
|
||||
}
|
||||
else {
|
||||
response->write(SimpleWeb::StatusCode::client_error_im_a_teapot);
|
||||
}
|
||||
}
|
||||
|
||||
template<class T>
|
||||
@@ -428,13 +502,13 @@ void serverinfo(std::shared_ptr<typename SimpleWeb::ServerBase<T>::Response> res
|
||||
print_req<T>(request);
|
||||
|
||||
int pair_status = 0;
|
||||
if constexpr (std::is_same_v<SimpleWeb::HTTPS, T>) {
|
||||
auto args = request->parse_query_string();
|
||||
if constexpr(std::is_same_v<SimpleWeb::HTTPS, T>) {
|
||||
auto args = request->parse_query_string();
|
||||
auto clientID = args.find("uniqueid"s);
|
||||
|
||||
|
||||
if(clientID != std::end(args)) {
|
||||
if (auto it = map_id_client.find(clientID->second); it != std::end(map_id_client)) {
|
||||
if(auto it = map_id_client.find(clientID->second); it != std::end(map_id_client)) {
|
||||
pair_status = 1;
|
||||
}
|
||||
}
|
||||
@@ -447,15 +521,15 @@ void serverinfo(std::shared_ptr<typename SimpleWeb::ServerBase<T>::Response> res
|
||||
|
||||
tree.put("root.appversion", VERSION);
|
||||
tree.put("root.GfeVersion", GFE_VERSION);
|
||||
tree.put("root.uniqueid", unique_id);
|
||||
tree.put("root.uniqueid", http::unique_id);
|
||||
tree.put("root.mac", platf::get_mac_address(request->local_endpoint_address()));
|
||||
tree.put("root.MaxLumaPixelsHEVC", config::video.hevc_mode > 0 ? "1869449984" : "0");
|
||||
tree.put("root.MaxLumaPixelsHEVC", config::video.hevc_mode > 1 ? "1869449984" : "0");
|
||||
tree.put("root.LocalIP", request->local_endpoint_address());
|
||||
|
||||
if(config::video.hevc_mode == 2) {
|
||||
if(config::video.hevc_mode == 3) {
|
||||
tree.put("root.ServerCodecModeSupport", "3843");
|
||||
}
|
||||
else if(config::video.hevc_mode == 1) {
|
||||
else if(config::video.hevc_mode == 2) {
|
||||
tree.put("root.ServerCodecModeSupport", "259");
|
||||
}
|
||||
else {
|
||||
@@ -466,10 +540,35 @@ void serverinfo(std::shared_ptr<typename SimpleWeb::ServerBase<T>::Response> res
|
||||
tree.put("root.ExternalIP", config::nvhttp.external_ip);
|
||||
}
|
||||
|
||||
pt::ptree display_nodes;
|
||||
for(auto &resolution : config::nvhttp.resolutions) {
|
||||
auto pred = [](auto ch) { return ch == ' ' || ch == '\t' || ch == 'x'; };
|
||||
|
||||
auto middle = std::find_if(std::begin(resolution), std::end(resolution), pred);
|
||||
if(middle == std::end(resolution)) {
|
||||
BOOST_LOG(warning) << resolution << " is not in the proper format for a resolution: WIDTHxHEIGHT"sv;
|
||||
continue;
|
||||
}
|
||||
|
||||
auto width = util::from_chars(&*std::begin(resolution), &*middle);
|
||||
auto height = util::from_chars(&*(middle + 1), &*std::end(resolution));
|
||||
for(auto fps : config::nvhttp.fps) {
|
||||
pt::ptree display_node;
|
||||
display_node.put("Width", width);
|
||||
display_node.put("Height", height);
|
||||
display_node.put("RefreshRate", fps);
|
||||
|
||||
display_nodes.add_child("DisplayMode", display_node);
|
||||
}
|
||||
}
|
||||
|
||||
if(!config::nvhttp.resolutions.empty()) {
|
||||
tree.add_child("root.SupportedDisplayMode", display_nodes);
|
||||
}
|
||||
auto current_appid = proc::proc.running();
|
||||
tree.put("root.PairStatus", pair_status);
|
||||
tree.put("root.currentgame", current_appid >= 0 ? current_appid + 2 : 0);
|
||||
tree.put("root.state", "_SERVER_BUSY");
|
||||
tree.put("root.currentgame", current_appid >= 0 ? current_appid + 1 : 0);
|
||||
tree.put("root.state", current_appid >= 0 ? "SUNSHINE_SERVER_BUSY" : "SUNSHINE_SERVER_FREE");
|
||||
|
||||
std::ostringstream data;
|
||||
|
||||
@@ -480,9 +579,6 @@ void serverinfo(std::shared_ptr<typename SimpleWeb::ServerBase<T>::Response> res
|
||||
void applist(resp_https_t response, req_https_t request) {
|
||||
print_req<SimpleWeb::HTTPS>(request);
|
||||
|
||||
auto args = request->parse_query_string();
|
||||
auto clientID = args.at("uniqueid"s);
|
||||
|
||||
pt::ptree tree;
|
||||
|
||||
auto g = util::fail_guard([&]() {
|
||||
@@ -492,6 +588,15 @@ void applist(resp_https_t response, req_https_t request) {
|
||||
response->write(data.str());
|
||||
});
|
||||
|
||||
auto args = request->parse_query_string();
|
||||
if(args.find("uniqueid"s) == std::end(args)) {
|
||||
tree.put("root.<xmlattr>.status_code", 400);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
auto clientID = args.at("uniqueid"s);
|
||||
|
||||
auto client = map_id_client.find(clientID);
|
||||
if(client == std::end(map_id_client)) {
|
||||
tree.put("root.<xmlattr>.status_code", 501);
|
||||
@@ -501,28 +606,21 @@ void applist(resp_https_t response, req_https_t request) {
|
||||
|
||||
auto &apps = tree.add_child("root", pt::ptree {});
|
||||
|
||||
pt::ptree desktop;
|
||||
|
||||
apps.put("<xmlattr>.status_code", 200);
|
||||
desktop.put("IsHdrSupported"s, config::video.hevc_mode == 2 ? 1 : 0);
|
||||
desktop.put("AppTitle"s, "Desktop");
|
||||
desktop.put("ID"s, 1);
|
||||
|
||||
int x = 2;
|
||||
int x = 0;
|
||||
for(auto &proc : proc::proc.get_apps()) {
|
||||
pt::ptree app;
|
||||
|
||||
app.put("IsHdrSupported"s, config::video.hevc_mode == 2 ? 1 : 0);
|
||||
app.put("IsHdrSupported"s, config::video.hevc_mode == 3 ? 1 : 0);
|
||||
app.put("AppTitle"s, proc.name);
|
||||
app.put("ID"s, x++);
|
||||
app.put("ID"s, ++x);
|
||||
|
||||
apps.push_back(std::make_pair("App", std::move(app)));
|
||||
}
|
||||
|
||||
apps.push_back(std::make_pair("App", desktop));
|
||||
}
|
||||
|
||||
void launch(resp_https_t response, req_https_t request) {
|
||||
void launch(bool &host_audio, resp_https_t response, req_https_t request) {
|
||||
print_req<SimpleWeb::HTTPS>(request);
|
||||
|
||||
pt::ptree tree;
|
||||
@@ -533,20 +631,37 @@ void launch(resp_https_t response, req_https_t request) {
|
||||
response->write(data.str());
|
||||
});
|
||||
|
||||
auto args = request->parse_query_string();
|
||||
auto appid = util::from_view(args.at("appid")) -2;
|
||||
|
||||
stream::launch_session_t launch_session;
|
||||
|
||||
if(stream::session_state != stream::state_e::STOPPED) {
|
||||
if(stream::session_count() == config::stream.channels) {
|
||||
tree.put("root.resume", 0);
|
||||
tree.put("root.<xmlattr>.status_code", 503);
|
||||
tree.put("root.gamesession", 0);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
auto args = request->parse_query_string();
|
||||
if(
|
||||
args.find("rikey"s) == std::end(args) ||
|
||||
args.find("rikeyid"s) == std::end(args) ||
|
||||
args.find("localAudioPlayMode"s) == std::end(args) ||
|
||||
args.find("appid"s) == std::end(args)) {
|
||||
|
||||
tree.put("root.resume", 0);
|
||||
tree.put("root.<xmlattr>.status_code", 400);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
auto appid = util::from_view(args.at("appid")) - 1;
|
||||
|
||||
auto current_appid = proc::proc.running();
|
||||
if(appid >= 0 && appid != current_appid) {
|
||||
if(current_appid != -1) {
|
||||
tree.put("root.resume", 0);
|
||||
tree.put("root.<xmlattr>.status_code", 400);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if(appid >= 0) {
|
||||
auto err = proc::proc.execute(appid);
|
||||
if(err) {
|
||||
tree.put("root.<xmlattr>.status_code", err);
|
||||
@@ -554,36 +669,16 @@ void launch(resp_https_t response, req_https_t request) {
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
current_appid = appid;
|
||||
}
|
||||
|
||||
// Needed to determine if session must be closed when no process is running in proc::proc
|
||||
launch_session.has_process = current_appid >= 0;
|
||||
|
||||
auto clientID = args.at("uniqueid"s);
|
||||
launch_session.gcm_key = *util::from_hex<crypto::aes_t>(args.at("rikey"s), true);
|
||||
uint32_t prepend_iv = util::endian::big<uint32_t>(util::from_view(args.at("rikeyid"s)));
|
||||
auto prepend_iv_p = (uint8_t*)&prepend_iv;
|
||||
|
||||
auto next = std::copy(prepend_iv_p, prepend_iv_p + sizeof(prepend_iv), std::begin(launch_session.iv));
|
||||
std::fill(next, std::end(launch_session.iv), 0);
|
||||
|
||||
stream::launch_event.raise(launch_session);
|
||||
|
||||
/*
|
||||
bool sops = args.at("sops"s) == "1";
|
||||
std::optional<int> gcmap { std::nullopt };
|
||||
if(auto it = args.find("gcmap"s); it != std::end(args)) {
|
||||
gcmap = std::stoi(it->second);
|
||||
}
|
||||
*/
|
||||
host_audio = util::from_view(args.at("localAudioPlayMode"));
|
||||
stream::launch_session_raise(make_launch_session(host_audio, args));
|
||||
|
||||
tree.put("root.<xmlattr>.status_code", 200);
|
||||
tree.put("root.gamesession", 1);
|
||||
}
|
||||
|
||||
void resume(resp_https_t response, req_https_t request) {
|
||||
void resume(bool &host_audio, resp_https_t response, req_https_t request) {
|
||||
print_req<SimpleWeb::HTTPS>(request);
|
||||
|
||||
pt::ptree tree;
|
||||
@@ -594,28 +689,35 @@ void resume(resp_https_t response, req_https_t request) {
|
||||
response->write(data.str());
|
||||
});
|
||||
|
||||
auto current_appid = proc::proc.running();
|
||||
if(current_appid == -1 || stream::session_state != stream::state_e::STOPPED) {
|
||||
// It is possible that due a race condition that this if-statement gives a false negative,
|
||||
// that is automatically resolved in rtsp_server_t
|
||||
if(stream::session_count() == config::stream.channels) {
|
||||
tree.put("root.resume", 0);
|
||||
tree.put("root.<xmlattr>.status_code", 503);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
stream::launch_session_t launch_session;
|
||||
// Needed to determine if session must be closed when no process is running in proc::proc
|
||||
launch_session.has_process = current_appid >= 0;
|
||||
auto current_appid = proc::proc.running();
|
||||
if(current_appid == -1) {
|
||||
tree.put("root.resume", 0);
|
||||
tree.put("root.<xmlattr>.status_code", 503);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
auto args = request->parse_query_string();
|
||||
auto clientID = args.at("uniqueid"s);
|
||||
launch_session.gcm_key = *util::from_hex<crypto::aes_t>(args.at("rikey"s), true);
|
||||
uint32_t prepend_iv = util::endian::big<uint32_t>(util::from_view(args.at("rikeyid"s)));
|
||||
auto prepend_iv_p = (uint8_t*)&prepend_iv;
|
||||
if(
|
||||
args.find("rikey"s) == std::end(args) ||
|
||||
args.find("rikeyid"s) == std::end(args)) {
|
||||
|
||||
auto next = std::copy(prepend_iv_p, prepend_iv_p + sizeof(prepend_iv), std::begin(launch_session.iv));
|
||||
std::fill(next, std::end(launch_session.iv), 0);
|
||||
tree.put("root.resume", 0);
|
||||
tree.put("root.<xmlattr>.status_code", 400);
|
||||
|
||||
stream::launch_event.raise(launch_session);
|
||||
return;
|
||||
}
|
||||
|
||||
stream::launch_session_raise(make_launch_session(host_audio, args));
|
||||
|
||||
tree.put("root.<xmlattr>.status_code", 200);
|
||||
tree.put("root.resume", 1);
|
||||
@@ -632,24 +734,21 @@ void cancel(resp_https_t response, req_https_t request) {
|
||||
response->write(data.str());
|
||||
});
|
||||
|
||||
if(proc::proc.running() == -1) {
|
||||
tree.put("root.cancel", 1);
|
||||
tree.put("root.<xmlattr>.status_code", 200);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if(stream::session_state != stream::state_e::STOPPED) {
|
||||
// It is possible that due a race condition that this if-statement gives a false positive,
|
||||
// the client should try again
|
||||
if(stream::session_count() != 0) {
|
||||
tree.put("root.resume", 0);
|
||||
tree.put("root.<xmlattr>.status_code", 503);
|
||||
tree.put("root.cancel", 0);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
proc::proc.terminate();
|
||||
|
||||
tree.put("root.cancel", 1);
|
||||
tree.put("root.<xmlattr>.status_code", 200);
|
||||
|
||||
if(proc::proc.running() != -1) {
|
||||
proc::proc.terminate();
|
||||
}
|
||||
}
|
||||
|
||||
void appasset(resp_https_t response, req_https_t request) {
|
||||
@@ -659,73 +758,15 @@ void appasset(resp_https_t response, req_https_t request) {
|
||||
response->write(SimpleWeb::StatusCode::success_ok, in);
|
||||
}
|
||||
|
||||
int create_creds(const std::string &pkey, const std::string &cert) {
|
||||
fs::path pkey_path = pkey;
|
||||
fs::path cert_path = cert;
|
||||
void start(std::shared_ptr<safe::signal_t> shutdown_event) {
|
||||
|
||||
auto creds = crypto::gen_creds("Sunshine Gamestream Host"sv, 2048);
|
||||
bool clean_slate = config::sunshine.flags[config::flag::FRESH_STATE];
|
||||
|
||||
auto pkey_dir = pkey_path;
|
||||
auto cert_dir = cert_path;
|
||||
pkey_dir.remove_filename();
|
||||
cert_dir.remove_filename();
|
||||
|
||||
std::error_code err_code{};
|
||||
fs::create_directories(pkey_dir, err_code);
|
||||
if (err_code) {
|
||||
BOOST_LOG(fatal) << "Couldn't create directory ["sv << pkey_dir << "] :"sv << err_code.message();
|
||||
return -1;
|
||||
if(!clean_slate) {
|
||||
load_state();
|
||||
}
|
||||
|
||||
fs::create_directories(cert_dir, err_code);
|
||||
if (err_code) {
|
||||
BOOST_LOG(fatal) << "Couldn't create directory ["sv << cert_dir << "] :"sv << err_code.message();
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (write_file(pkey.c_str(), creds.pkey)) {
|
||||
BOOST_LOG(fatal) << "Couldn't open ["sv << config::nvhttp.pkey << ']';
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (write_file(cert.c_str(), creds.x509)) {
|
||||
BOOST_LOG(fatal) << "Couldn't open ["sv << config::nvhttp.cert << ']';
|
||||
return -1;
|
||||
}
|
||||
|
||||
fs::permissions(pkey_path,
|
||||
fs::perms::owner_read | fs::perms::owner_write,
|
||||
fs::perm_options::replace, err_code);
|
||||
|
||||
if (err_code) {
|
||||
BOOST_LOG(fatal) << "Couldn't change permissions of ["sv << config::nvhttp.pkey << "] :"sv << err_code.message();
|
||||
return -1;
|
||||
}
|
||||
|
||||
fs::permissions(cert_path,
|
||||
fs::perms::owner_read | fs::perms::group_read | fs::perms::others_read | fs::perms::owner_write,
|
||||
fs::perm_options::replace, err_code);
|
||||
|
||||
if (err_code) {
|
||||
BOOST_LOG(fatal) << "Couldn't change permissions of ["sv << config::nvhttp.cert << "] :"sv << err_code.message();
|
||||
return -1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
void start(std::shared_ptr<safe::event_t<bool>> shutdown_event) {
|
||||
if(!fs::exists(config::nvhttp.pkey) || !fs::exists(config::nvhttp.cert)) {
|
||||
if(create_creds(config::nvhttp.pkey, config::nvhttp.cert)) {
|
||||
shutdown_event->raise(true);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
origin_pin_allowed = net::from_enum_string(config::nvhttp.origin_pin_allowed);
|
||||
load_state();
|
||||
|
||||
conf_intern.pkey = read_file(config::nvhttp.pkey.c_str());
|
||||
conf_intern.pkey = read_file(config::nvhttp.pkey.c_str());
|
||||
conf_intern.servercert = read_file(config::nvhttp.cert.c_str());
|
||||
|
||||
auto ctx = std::make_shared<boost::asio::ssl::context>(boost::asio::ssl::context::tls);
|
||||
@@ -733,13 +774,13 @@ void start(std::shared_ptr<safe::event_t<bool>> shutdown_event) {
|
||||
ctx->use_private_key_file(config::nvhttp.pkey, boost::asio::ssl::context::pem);
|
||||
|
||||
crypto::cert_chain_t cert_chain;
|
||||
for(auto &[_,client] : map_id_client) {
|
||||
for(auto &[_, client] : map_id_client) {
|
||||
for(auto &cert : client.certs) {
|
||||
cert_chain.add(crypto::x509(cert));
|
||||
}
|
||||
}
|
||||
|
||||
auto add_cert = std::make_shared<safe::queue_t<crypto::x509_t>>();
|
||||
auto add_cert = std::make_shared<safe::queue_t<crypto::x509_t>>(30);
|
||||
|
||||
// Ugly hack for verifying certificates, see crypto::cert_chain_t::verify() for details
|
||||
ctx->set_verify_callback([&cert_chain, add_cert](int verified, boost::asio::ssl::verify_context &ctx) {
|
||||
@@ -777,35 +818,64 @@ void start(std::shared_ptr<safe::event_t<bool>> shutdown_event) {
|
||||
return 1;
|
||||
});
|
||||
|
||||
// /resume doesn't get the parameter "localAudioPlayMode"
|
||||
// /launch will store it in host_audio
|
||||
bool host_audio {};
|
||||
|
||||
https_server_t https_server { ctx, boost::asio::ssl::verify_peer | boost::asio::ssl::verify_fail_if_no_peer_cert | boost::asio::ssl::verify_client_once };
|
||||
http_server_t http_server;
|
||||
|
||||
https_server.default_resource = not_found<SimpleWeb::HTTPS>;
|
||||
https_server.resource["^/serverinfo$"]["GET"] = serverinfo<SimpleWeb::HTTPS>;
|
||||
https_server.resource["^/pair$"]["GET"] = [&add_cert](auto resp, auto req) { pair<SimpleWeb::HTTPS>(add_cert, resp, req); };
|
||||
https_server.resource["^/applist$"]["GET"] = applist;
|
||||
https_server.resource["^/appasset$"]["GET"] = appasset;
|
||||
https_server.resource["^/launch$"]["GET"] = launch;
|
||||
https_server.default_resource = not_found<SimpleWeb::HTTPS>;
|
||||
https_server.resource["^/serverinfo$"]["GET"] = serverinfo<SimpleWeb::HTTPS>;
|
||||
https_server.resource["^/pair$"]["GET"] = [&add_cert](auto resp, auto req) { pair<SimpleWeb::HTTPS>(add_cert, resp, req); };
|
||||
https_server.resource["^/applist$"]["GET"] = applist;
|
||||
https_server.resource["^/appasset$"]["GET"] = appasset;
|
||||
https_server.resource["^/launch$"]["GET"] = [&host_audio](auto resp, auto req) { launch(host_audio, resp, req); };
|
||||
https_server.resource["^/pin/([0-9]+)$"]["GET"] = pin<SimpleWeb::HTTPS>;
|
||||
https_server.resource["^/resume$"]["GET"] = resume;
|
||||
https_server.resource["^/cancel$"]["GET"] = cancel;
|
||||
https_server.resource["^/resume$"]["GET"] = [&host_audio](auto resp, auto req) { resume(host_audio, resp, req); };
|
||||
https_server.resource["^/cancel$"]["GET"] = cancel;
|
||||
|
||||
https_server.config.reuse_address = true;
|
||||
https_server.config.address = "0.0.0.0"s;
|
||||
https_server.config.port = PORT_HTTPS;
|
||||
https_server.config.address = "0.0.0.0"s;
|
||||
https_server.config.port = PORT_HTTPS;
|
||||
|
||||
http_server.default_resource = not_found<SimpleWeb::HTTP>;
|
||||
http_server.resource["^/serverinfo$"]["GET"] = serverinfo<SimpleWeb::HTTP>;
|
||||
http_server.resource["^/pair$"]["GET"] = [&add_cert](auto resp, auto req) { pair<SimpleWeb::HTTP>(add_cert, resp, req); };
|
||||
http_server.default_resource = not_found<SimpleWeb::HTTP>;
|
||||
http_server.resource["^/serverinfo$"]["GET"] = serverinfo<SimpleWeb::HTTP>;
|
||||
http_server.resource["^/pair$"]["GET"] = [&add_cert](auto resp, auto req) { pair<SimpleWeb::HTTP>(add_cert, resp, req); };
|
||||
http_server.resource["^/pin/([0-9]+)$"]["GET"] = pin<SimpleWeb::HTTP>;
|
||||
|
||||
http_server.config.reuse_address = true;
|
||||
http_server.config.address = "0.0.0.0"s;
|
||||
http_server.config.port = PORT_HTTP;
|
||||
http_server.config.address = "0.0.0.0"s;
|
||||
http_server.config.port = PORT_HTTP;
|
||||
|
||||
std::thread ssl { &https_server_t::start, &https_server };
|
||||
std::thread tcp { &http_server_t::start, &http_server };
|
||||
try {
|
||||
https_server.bind();
|
||||
http_server.bind();
|
||||
}
|
||||
catch(boost::system::system_error &err) {
|
||||
BOOST_LOG(fatal) << "Couldn't bind http server to ports ["sv << PORT_HTTPS << ", "sv << PORT_HTTP << "]: "sv << err.what();
|
||||
|
||||
shutdown_event->raise(true);
|
||||
return;
|
||||
}
|
||||
|
||||
auto accept_and_run = [&](auto *http_server) {
|
||||
try {
|
||||
http_server->accept_and_run();
|
||||
}
|
||||
catch(boost::system::system_error &err) {
|
||||
// It's possible the exception gets thrown after calling http_server->stop() from a different thread
|
||||
if(shutdown_event->peek()) {
|
||||
return;
|
||||
}
|
||||
|
||||
BOOST_LOG(fatal) << "Couldn't start http server to ports ["sv << PORT_HTTPS << ", "sv << PORT_HTTP << "]: "sv << err.what();
|
||||
shutdown_event->raise(true);
|
||||
return;
|
||||
}
|
||||
};
|
||||
std::thread ssl { accept_and_run, &https_server };
|
||||
std::thread tcp { accept_and_run, &http_server };
|
||||
|
||||
// Wait for any event
|
||||
shutdown_event->view();
|
||||
@@ -816,31 +886,4 @@ void start(std::shared_ptr<safe::event_t<bool>> shutdown_event) {
|
||||
ssl.join();
|
||||
tcp.join();
|
||||
}
|
||||
|
||||
int write_file(const char *path, const std::string_view &contents) {
|
||||
std::ofstream out(path);
|
||||
|
||||
if(!out.is_open()) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
out << contents;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
std::string read_file(const char *path) {
|
||||
std::ifstream in(path);
|
||||
|
||||
std::string input;
|
||||
std::string base64_cert;
|
||||
|
||||
//FIXME: Being unable to read file could result in infinite loop
|
||||
while(!in.eof()) {
|
||||
std::getline(in, input);
|
||||
base64_cert += input + '\n';
|
||||
}
|
||||
|
||||
return base64_cert;
|
||||
}
|
||||
}
|
||||
} // namespace nvhttp
|
||||
|
||||
@@ -5,17 +5,15 @@
|
||||
#ifndef SUNSHINE_NVHTTP_H
|
||||
#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>
|
||||
|
||||
#include "thread_safe.h"
|
||||
|
||||
#define CA_DIR SUNSHINE_ASSETS_DIR "/demoCA"
|
||||
#define PRIVATE_KEY_FILE CA_DIR "/cakey.pem"
|
||||
#define CERTIFICATE_FILE CA_DIR "/cacert.pem"
|
||||
|
||||
namespace nvhttp {
|
||||
void start(std::shared_ptr<safe::event_t<bool>> shutdown_event);
|
||||
}
|
||||
void start(std::shared_ptr<safe::signal_t> shutdown_event);
|
||||
bool pin(std::string pin);
|
||||
} // namespace nvhttp
|
||||
|
||||
#endif //SUNSHINE_NVHTTP_H
|
||||
|
||||
@@ -5,11 +5,18 @@
|
||||
#ifndef SUNSHINE_COMMON_H
|
||||
#define SUNSHINE_COMMON_H
|
||||
|
||||
#include <bitset>
|
||||
#include <filesystem>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
|
||||
#include "sunshine/utility.h"
|
||||
|
||||
struct sockaddr;
|
||||
struct AVFrame;
|
||||
|
||||
namespace platf {
|
||||
constexpr auto MAX_GAMEPADS = 2;
|
||||
constexpr auto MAX_GAMEPADS = 32;
|
||||
|
||||
constexpr std::uint16_t DPAD_UP = 0x0001;
|
||||
constexpr std::uint16_t DPAD_DOWN = 0x0002;
|
||||
@@ -27,6 +34,86 @@ constexpr std::uint16_t B = 0x2000;
|
||||
constexpr std::uint16_t X = 0x4000;
|
||||
constexpr std::uint16_t Y = 0x8000;
|
||||
|
||||
namespace speaker {
|
||||
enum speaker_e {
|
||||
FRONT_LEFT,
|
||||
FRONT_RIGHT,
|
||||
FRONT_CENTER,
|
||||
LOW_FREQUENCY,
|
||||
BACK_LEFT,
|
||||
BACK_RIGHT,
|
||||
SIDE_LEFT,
|
||||
SIDE_RIGHT,
|
||||
MAX_SPEAKERS,
|
||||
};
|
||||
|
||||
constexpr std::uint8_t map_stereo[] {
|
||||
FRONT_LEFT, FRONT_RIGHT
|
||||
};
|
||||
constexpr std::uint8_t map_surround51[] {
|
||||
FRONT_LEFT,
|
||||
FRONT_RIGHT,
|
||||
FRONT_CENTER,
|
||||
LOW_FREQUENCY,
|
||||
BACK_LEFT,
|
||||
BACK_RIGHT,
|
||||
};
|
||||
constexpr std::uint8_t map_surround71[] {
|
||||
FRONT_LEFT,
|
||||
FRONT_RIGHT,
|
||||
FRONT_CENTER,
|
||||
LOW_FREQUENCY,
|
||||
LOW_FREQUENCY,
|
||||
BACK_LEFT,
|
||||
BACK_RIGHT,
|
||||
SIDE_LEFT,
|
||||
SIDE_RIGHT,
|
||||
};
|
||||
} // namespace speaker
|
||||
|
||||
enum class mem_type_e {
|
||||
system,
|
||||
vaapi,
|
||||
dxgi,
|
||||
unknown
|
||||
};
|
||||
|
||||
enum class pix_fmt_e {
|
||||
yuv420p,
|
||||
yuv420p10,
|
||||
nv12,
|
||||
p010,
|
||||
unknown
|
||||
};
|
||||
|
||||
inline std::string_view from_pix_fmt(pix_fmt_e pix_fmt) {
|
||||
using namespace std::literals;
|
||||
#define _CONVERT(x) \
|
||||
case pix_fmt_e::x: \
|
||||
return #x##sv
|
||||
switch(pix_fmt) {
|
||||
_CONVERT(yuv420p);
|
||||
_CONVERT(yuv420p10);
|
||||
_CONVERT(nv12);
|
||||
_CONVERT(p010);
|
||||
_CONVERT(unknown);
|
||||
}
|
||||
#undef _CONVERT
|
||||
|
||||
return "unknown"sv;
|
||||
}
|
||||
|
||||
// Dimensions for touchscreen input
|
||||
struct touch_port_t {
|
||||
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 } {};
|
||||
};
|
||||
|
||||
struct gamepad_state_t {
|
||||
std::uint16_t buttonFlags;
|
||||
std::uint8_t lt;
|
||||
@@ -37,21 +124,61 @@ struct gamepad_state_t {
|
||||
std::int16_t rsY;
|
||||
};
|
||||
|
||||
class deinit_t {
|
||||
public:
|
||||
virtual ~deinit_t() = default;
|
||||
};
|
||||
|
||||
struct img_t {
|
||||
public:
|
||||
std::uint8_t *data {};
|
||||
std::int32_t width {};
|
||||
std::uint8_t *data {};
|
||||
std::int32_t width {};
|
||||
std::int32_t height {};
|
||||
std::int32_t pixel_pitch {};
|
||||
std::int32_t row_pitch {};
|
||||
|
||||
img_t() = default;
|
||||
img_t(const img_t&) = delete;
|
||||
img_t(img_t&&) = delete;
|
||||
img_t() = default;
|
||||
img_t(const img_t &) = delete;
|
||||
img_t(img_t &&) = delete;
|
||||
|
||||
virtual ~img_t() = default;
|
||||
};
|
||||
|
||||
struct sink_t {
|
||||
// Play on host PC
|
||||
std::string host;
|
||||
|
||||
// On Windows, it is not possible to create a virtual sink
|
||||
// Therefore, it is optional
|
||||
struct null_t {
|
||||
std::string stereo;
|
||||
std::string surround51;
|
||||
std::string surround71;
|
||||
};
|
||||
std::optional<null_t> null;
|
||||
};
|
||||
|
||||
struct hwdevice_t {
|
||||
void *data {};
|
||||
AVFrame *frame {};
|
||||
|
||||
virtual int convert(platf::img_t &img) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* implementations must take ownership of 'frame'
|
||||
*/
|
||||
virtual int set_frame(AVFrame *frame) {
|
||||
std::abort(); // ^ This function must never be called
|
||||
return -1;
|
||||
};
|
||||
|
||||
virtual void set_colorspace(std::uint32_t colorspace, std::uint32_t color_range) {};
|
||||
|
||||
virtual ~hwdevice_t() = default;
|
||||
};
|
||||
|
||||
enum class capture_e : int {
|
||||
ok,
|
||||
reinit,
|
||||
@@ -61,10 +188,22 @@ enum class capture_e : int {
|
||||
|
||||
class display_t {
|
||||
public:
|
||||
virtual capture_e snapshot(img_t *img, bool cursor) = 0;
|
||||
virtual std::unique_ptr<img_t> alloc_img() = 0;
|
||||
display_t() noexcept : offset_x { 0 }, offset_y { 0 } {}
|
||||
virtual capture_e snapshot(img_t *img, std::chrono::milliseconds timeout, bool cursor) = 0;
|
||||
virtual std::shared_ptr<img_t> alloc_img() = 0;
|
||||
|
||||
virtual int dummy_img(img_t *img) = 0;
|
||||
|
||||
virtual std::shared_ptr<hwdevice_t> make_hwdevice(pix_fmt_e pix_fmt) {
|
||||
return std::make_shared<hwdevice_t>();
|
||||
}
|
||||
|
||||
virtual ~display_t() = default;
|
||||
|
||||
// Offsets for when streaming a specific monitor. By default, they are 0.
|
||||
int offset_x, offset_y;
|
||||
|
||||
int width, height;
|
||||
};
|
||||
|
||||
class mic_t {
|
||||
@@ -74,22 +213,43 @@ public:
|
||||
virtual ~mic_t() = default;
|
||||
};
|
||||
|
||||
class audio_control_t {
|
||||
public:
|
||||
virtual int set_sink(const std::string &sink) = 0;
|
||||
|
||||
void freeInput(void*);
|
||||
virtual std::unique_ptr<mic_t> microphone(const std::uint8_t *mapping, int channels, std::uint32_t sample_rate, std::uint32_t frame_size) = 0;
|
||||
|
||||
virtual std::optional<sink_t> sink_info() = 0;
|
||||
|
||||
virtual ~audio_control_t() = default;
|
||||
};
|
||||
|
||||
void freeInput(void *);
|
||||
|
||||
using input_t = util::safe_ptr<void, freeInput>;
|
||||
|
||||
std::filesystem::path appdata();
|
||||
|
||||
std::string get_mac_address(const std::string_view &address);
|
||||
|
||||
std::unique_ptr<mic_t> microphone(std::uint32_t sample_rate);
|
||||
std::shared_ptr<display_t> display();
|
||||
std::string from_sockaddr(const sockaddr *const);
|
||||
std::pair<std::uint16_t, std::string> from_sockaddr_ex(const sockaddr *const);
|
||||
|
||||
std::unique_ptr<audio_control_t> audio_control();
|
||||
std::shared_ptr<display_t> display(mem_type_e hwdevice_type);
|
||||
|
||||
input_t input();
|
||||
void move_mouse(input_t &input, int deltaX, int deltaY);
|
||||
void abs_mouse(input_t &input, const touch_port_t &touch_port, float x, float y);
|
||||
void button_mouse(input_t &input, int button, bool release);
|
||||
void scroll(input_t &input, int distance);
|
||||
void keyboard(input_t &input, uint16_t modcode, bool release);
|
||||
void gamepad(input_t &input, int nr, const gamepad_state_t &gamepad_state);
|
||||
}
|
||||
|
||||
int alloc_gamepad(input_t &input, int nr);
|
||||
void free_gamepad(input_t &input, int nr);
|
||||
|
||||
[[nodiscard]] std::unique_ptr<deinit_t> init();
|
||||
} // namespace platf
|
||||
|
||||
#endif //SUNSHINE_COMMON_H
|
||||
|
||||
@@ -1,416 +0,0 @@
|
||||
//
|
||||
// Created by loki on 6/21/19.
|
||||
//
|
||||
|
||||
#include "common.h"
|
||||
#include "../main.h"
|
||||
|
||||
#include <fstream>
|
||||
|
||||
#include <arpa/inet.h>
|
||||
#include <ifaddrs.h>
|
||||
#include <net/if.h>
|
||||
|
||||
#include <X11/X.h>
|
||||
#include <X11/Xlib.h>
|
||||
#include <X11/Xutil.h>
|
||||
#include <X11/extensions/Xfixes.h>
|
||||
#include <xcb/shm.h>
|
||||
#include <xcb/xfixes.h>
|
||||
#include <sys/ipc.h>
|
||||
#include <sys/shm.h>
|
||||
|
||||
#include <pulse/simple.h>
|
||||
#include <pulse/error.h>
|
||||
|
||||
#include <bitset>
|
||||
#include <sunshine/task_pool.h>
|
||||
#include <sunshine/config.h>
|
||||
|
||||
namespace platf {
|
||||
using namespace std::literals;
|
||||
|
||||
void freeImage(XImage *);
|
||||
void freeX(XFixesCursorImage *);
|
||||
|
||||
using ifaddr_t = util::safe_ptr<ifaddrs, freeifaddrs>;
|
||||
using xcb_connect_t = util::safe_ptr<xcb_connection_t, xcb_disconnect>;
|
||||
using xcb_img_t = util::c_ptr<xcb_shm_get_image_reply_t>;
|
||||
using xcb_cursor_img = util::c_ptr<xcb_xfixes_get_cursor_image_reply_t>;
|
||||
|
||||
using xdisplay_t = util::safe_ptr_v2<Display, int, XCloseDisplay>;
|
||||
using ximg_t = util::safe_ptr<XImage, freeImage>;
|
||||
using xcursor_t = util::safe_ptr<XFixesCursorImage, freeX>;
|
||||
|
||||
class shm_id_t {
|
||||
public:
|
||||
shm_id_t() : id { -1 } {}
|
||||
shm_id_t(int id) : id {id } {}
|
||||
shm_id_t(shm_id_t &&other) noexcept : id(other.id) {
|
||||
other.id = -1;
|
||||
}
|
||||
|
||||
~shm_id_t() {
|
||||
if(id != -1) {
|
||||
shmctl(id, IPC_RMID, nullptr);
|
||||
id = -1;
|
||||
}
|
||||
}
|
||||
int id;
|
||||
};
|
||||
|
||||
class shm_data_t {
|
||||
public:
|
||||
shm_data_t() : data {(void*)-1 } {}
|
||||
shm_data_t(void *data) : data {data } {}
|
||||
|
||||
shm_data_t(shm_data_t &&other) noexcept : data(other.data) {
|
||||
other.data = (void*)-1;
|
||||
}
|
||||
|
||||
~shm_data_t() {
|
||||
if((std::uintptr_t)data != -1) {
|
||||
shmdt(data);
|
||||
data = (void*)-1;
|
||||
}
|
||||
}
|
||||
|
||||
void *data;
|
||||
};
|
||||
|
||||
struct x11_img_t : public img_t {
|
||||
ximg_t img;
|
||||
};
|
||||
|
||||
struct shm_img_t : public img_t {
|
||||
~shm_img_t() override {
|
||||
delete[] data;
|
||||
data = nullptr;
|
||||
}
|
||||
};
|
||||
|
||||
void blend_cursor(Display *display, img_t &img) {
|
||||
xcursor_t overlay { XFixesGetCursorImage(display) };
|
||||
|
||||
if(!overlay) {
|
||||
BOOST_LOG(error) << "Couldn't get cursor from XFixesGetCursorImage"sv;
|
||||
return;
|
||||
}
|
||||
|
||||
overlay->x -= overlay->xhot;
|
||||
overlay->y -= overlay->yhot;
|
||||
|
||||
overlay->x = std::max((short)0, overlay->x);
|
||||
overlay->y = std::max((short)0, overlay->y);
|
||||
|
||||
auto pixels = (int*)img.data;
|
||||
|
||||
auto screen_height = img.height;
|
||||
auto screen_width = img.width;
|
||||
|
||||
auto delta_height = std::min<uint16_t>(overlay->height, std::max(0, screen_height - overlay->y));
|
||||
auto delta_width = std::min<uint16_t>(overlay->width, std::max(0, screen_width - overlay->x));
|
||||
for(auto y = 0; y < delta_height; ++y) {
|
||||
|
||||
auto overlay_begin = &overlay->pixels[y * overlay->width];
|
||||
auto overlay_end = &overlay->pixels[y * overlay->width + delta_width];
|
||||
|
||||
auto pixels_begin = &pixels[(y + overlay->y) * (img.row_pitch / img.pixel_pitch) + overlay->x];
|
||||
std::for_each(overlay_begin, overlay_end, [&](long pixel) {
|
||||
int *pixel_p = (int*)&pixel;
|
||||
|
||||
auto colors_in = (uint8_t*)pixels_begin;
|
||||
|
||||
auto alpha = (*(uint*)pixel_p) >> 24u;
|
||||
if(alpha == 255) {
|
||||
*pixels_begin = *pixel_p;
|
||||
}
|
||||
else {
|
||||
auto colors_out = (uint8_t*)pixel_p;
|
||||
colors_in[0] = colors_out[0] + (colors_in[0] * (255 - alpha) + 255/2) / 255;
|
||||
colors_in[1] = colors_out[1] + (colors_in[1] * (255 - alpha) + 255/2) / 255;
|
||||
colors_in[2] = colors_out[2] + (colors_in[2] * (255 - alpha) + 255/2) / 255;
|
||||
}
|
||||
++pixels_begin;
|
||||
});
|
||||
}
|
||||
}
|
||||
struct x11_attr_t : public display_t {
|
||||
x11_attr_t() : xdisplay {XOpenDisplay(nullptr) }, xwindow { }, xattr {} {
|
||||
if(!xdisplay) {
|
||||
BOOST_LOG(fatal) << "Could not open x11 display"sv;
|
||||
log_flush();
|
||||
std::abort();
|
||||
}
|
||||
|
||||
xwindow = DefaultRootWindow(xdisplay.get());
|
||||
|
||||
refresh();
|
||||
}
|
||||
|
||||
void refresh() {
|
||||
XGetWindowAttributes(xdisplay.get(), xwindow, &xattr);
|
||||
}
|
||||
|
||||
capture_e snapshot(img_t *img_out_base, bool cursor) override {
|
||||
refresh();
|
||||
XImage *img { XGetImage(
|
||||
xdisplay.get(),
|
||||
xwindow,
|
||||
0, 0,
|
||||
xattr.width, xattr.height,
|
||||
AllPlanes, ZPixmap)
|
||||
};
|
||||
|
||||
auto img_out = (x11_img_t*)img_out_base;
|
||||
img_out->width = img->width;
|
||||
img_out->height = img->height;
|
||||
img_out->data = (uint8_t*)img->data;
|
||||
img_out->row_pitch = img->bytes_per_line;
|
||||
img_out->pixel_pitch = img->bits_per_pixel / 8;
|
||||
img_out->img.reset(img);
|
||||
|
||||
if(cursor) {
|
||||
blend_cursor(xdisplay.get(), *img_out_base);
|
||||
}
|
||||
|
||||
return capture_e::ok;
|
||||
}
|
||||
|
||||
std::unique_ptr<img_t> alloc_img() override {
|
||||
return std::make_unique<x11_img_t>();
|
||||
}
|
||||
|
||||
xdisplay_t xdisplay;
|
||||
Window xwindow;
|
||||
XWindowAttributes xattr;
|
||||
};
|
||||
|
||||
struct shm_attr_t : public x11_attr_t {
|
||||
xdisplay_t shm_xdisplay; // Prevent race condition with x11_attr_t::xdisplay
|
||||
xcb_connect_t xcb;
|
||||
xcb_screen_t *display;
|
||||
std::uint32_t seg;
|
||||
|
||||
shm_id_t shm_id;
|
||||
|
||||
shm_data_t data;
|
||||
|
||||
util::TaskPool::task_id_t refresh_task_id;
|
||||
void delayed_refresh() {
|
||||
refresh();
|
||||
|
||||
refresh_task_id = task_pool.pushDelayed(&shm_attr_t::delayed_refresh, 2s, this).task_id;
|
||||
}
|
||||
|
||||
shm_attr_t() : x11_attr_t(), shm_xdisplay {XOpenDisplay(nullptr) } {
|
||||
refresh_task_id = task_pool.pushDelayed(&shm_attr_t::delayed_refresh, 2s, this).task_id;
|
||||
}
|
||||
|
||||
~shm_attr_t() override {
|
||||
while(!task_pool.cancel(refresh_task_id));
|
||||
}
|
||||
|
||||
capture_e snapshot(img_t *img, bool cursor) override {
|
||||
if(display->width_in_pixels != xattr.width || display->height_in_pixels != xattr.height) {
|
||||
return capture_e::reinit;
|
||||
}
|
||||
|
||||
auto img_cookie = xcb_shm_get_image_unchecked(
|
||||
xcb.get(),
|
||||
display->root,
|
||||
0, 0,
|
||||
display->width_in_pixels, display->height_in_pixels,
|
||||
~0,
|
||||
XCB_IMAGE_FORMAT_Z_PIXMAP,
|
||||
seg,
|
||||
0
|
||||
);
|
||||
|
||||
xcb_img_t img_reply { xcb_shm_get_image_reply(xcb.get(), img_cookie, nullptr) };
|
||||
if(!img_reply) {
|
||||
BOOST_LOG(error) << "Could not get image reply"sv;
|
||||
return capture_e::reinit;
|
||||
}
|
||||
|
||||
if(img->width != display->width_in_pixels || img->height != display->height_in_pixels) {
|
||||
delete[] img->data;
|
||||
|
||||
img->data = new std::uint8_t[frame_size()];
|
||||
img->width = display->width_in_pixels;
|
||||
img->height = display->height_in_pixels;
|
||||
img->pixel_pitch = 4;
|
||||
img->row_pitch = img->width * img->pixel_pitch;
|
||||
}
|
||||
|
||||
std::copy_n((std::uint8_t*)data.data, frame_size(), img->data);
|
||||
|
||||
if(cursor) {
|
||||
blend_cursor(shm_xdisplay.get(), *img);
|
||||
}
|
||||
|
||||
return capture_e::ok;
|
||||
}
|
||||
|
||||
std::unique_ptr<img_t> alloc_img() override {
|
||||
return std::make_unique<shm_img_t>();
|
||||
}
|
||||
|
||||
int init() {
|
||||
shm_xdisplay.reset(XOpenDisplay(nullptr));
|
||||
xcb.reset(xcb_connect(nullptr, nullptr));
|
||||
if(xcb_connection_has_error(xcb.get())) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if(!xcb_get_extension_data(xcb.get(), &xcb_shm_id)->present) {
|
||||
BOOST_LOG(error) << "Missing SHM extension"sv;
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
auto iter = xcb_setup_roots_iterator(xcb_get_setup(xcb.get()));
|
||||
display = iter.data;
|
||||
seg = xcb_generate_id(xcb.get());
|
||||
|
||||
shm_id.id = shmget(IPC_PRIVATE, frame_size(), IPC_CREAT | 0777);
|
||||
if(shm_id.id == -1) {
|
||||
BOOST_LOG(error) << "shmget failed"sv;
|
||||
return -1;
|
||||
}
|
||||
|
||||
xcb_shm_attach(xcb.get(), seg, shm_id.id, false);
|
||||
data.data = shmat(shm_id.id, nullptr, 0);
|
||||
|
||||
if ((uintptr_t)data.data == -1) {
|
||||
BOOST_LOG(error) << "shmat failed"sv;
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
std::uint32_t frame_size() {
|
||||
return display->height_in_pixels * display->width_in_pixels * 4;
|
||||
}
|
||||
};
|
||||
|
||||
struct mic_attr_t : public mic_t {
|
||||
pa_sample_spec ss;
|
||||
util::safe_ptr<pa_simple, pa_simple_free> mic;
|
||||
|
||||
explicit mic_attr_t(const pa_sample_spec& ss) : ss(ss), mic {} {}
|
||||
capture_e sample(std::vector<std::int16_t> &sample_buf) override {
|
||||
auto sample_size = sample_buf.size();
|
||||
|
||||
auto buf = sample_buf.data();
|
||||
int status;
|
||||
if(pa_simple_read(mic.get(), buf, sample_size * 2, &status)) {
|
||||
BOOST_LOG(error) << "pa_simple_read() failed: "sv << pa_strerror(status);
|
||||
|
||||
return capture_e::error;
|
||||
}
|
||||
|
||||
return capture_e::ok;
|
||||
}
|
||||
};
|
||||
|
||||
std::unique_ptr<display_t> shm_display() {
|
||||
auto shm = std::make_unique<shm_attr_t>();
|
||||
|
||||
if(shm->init()) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
return shm;
|
||||
}
|
||||
|
||||
std::shared_ptr<display_t> display() {
|
||||
auto shm_disp = shm_display();
|
||||
|
||||
if(!shm_disp) {
|
||||
return std::unique_ptr<display_t> { new x11_attr_t {} };
|
||||
}
|
||||
|
||||
return shm_disp;
|
||||
}
|
||||
|
||||
std::unique_ptr<mic_t> microphone(std::uint32_t sample_rate) {
|
||||
std::unique_ptr<mic_attr_t> mic {
|
||||
new mic_attr_t {
|
||||
{ PA_SAMPLE_S16LE, sample_rate, 2 }
|
||||
}
|
||||
};
|
||||
|
||||
int status;
|
||||
|
||||
const char *audio_sink = nullptr;
|
||||
if(!config::audio.sink.empty()) {
|
||||
audio_sink = config::audio.sink.c_str();
|
||||
}
|
||||
else {
|
||||
audio_sink = "@DEFAULT_MONITOR@";
|
||||
}
|
||||
|
||||
mic->mic.reset(
|
||||
pa_simple_new(nullptr, "sunshine", pa_stream_direction_t::PA_STREAM_RECORD, audio_sink, "sunshine_record", &mic->ss, nullptr, nullptr, &status)
|
||||
);
|
||||
|
||||
if(!mic->mic) {
|
||||
auto err_str = pa_strerror(status);
|
||||
BOOST_LOG(error) << "pa_simple_new() failed: "sv << err_str;
|
||||
|
||||
log_flush();
|
||||
std::abort();
|
||||
}
|
||||
|
||||
return mic;
|
||||
}
|
||||
|
||||
ifaddr_t get_ifaddrs() {
|
||||
ifaddrs *p { nullptr };
|
||||
|
||||
getifaddrs(&p);
|
||||
|
||||
return ifaddr_t { p };
|
||||
}
|
||||
|
||||
std::string from_sockaddr(const sockaddr *const ip_addr) {
|
||||
char data[INET6_ADDRSTRLEN];
|
||||
|
||||
auto family = ip_addr->sa_family;
|
||||
if(family == AF_INET6) {
|
||||
inet_ntop(AF_INET6, &((sockaddr_in6*)ip_addr)->sin6_addr, data, INET6_ADDRSTRLEN);
|
||||
}
|
||||
|
||||
if(family == AF_INET) {
|
||||
inet_ntop(AF_INET, &((sockaddr_in*)ip_addr)->sin_addr, data, INET_ADDRSTRLEN);
|
||||
}
|
||||
|
||||
return std::string { data };
|
||||
}
|
||||
|
||||
std::string get_mac_address(const std::string_view &address) {
|
||||
auto ifaddrs = get_ifaddrs();
|
||||
for(auto pos = ifaddrs.get(); pos != nullptr; pos = pos->ifa_next) {
|
||||
if(pos->ifa_addr && address == from_sockaddr(pos->ifa_addr)) {
|
||||
std::ifstream mac_file("/sys/class/net/"s + pos->ifa_name + "/address");
|
||||
if(mac_file.good()) {
|
||||
std::string mac_address;
|
||||
std::getline(mac_file, mac_address);
|
||||
return mac_address;
|
||||
}
|
||||
}
|
||||
}
|
||||
BOOST_LOG(warning) << "Unable to find MAC address for "sv << address;
|
||||
return "00:00:00:00:00:00"s;
|
||||
}
|
||||
|
||||
void freeImage(XImage *p) {
|
||||
XDestroyImage(p);
|
||||
}
|
||||
void freeX(XFixesCursorImage *p) {
|
||||
XFree(p);
|
||||
}
|
||||
}
|
||||
437
sunshine/platform/linux/audio.cpp
Normal file
437
sunshine/platform/linux/audio.cpp
Normal file
@@ -0,0 +1,437 @@
|
||||
//
|
||||
// Created by loki on 5/16/21.
|
||||
//
|
||||
#include <bitset>
|
||||
#include <sstream>
|
||||
|
||||
#include <boost/regex.hpp>
|
||||
|
||||
#include <pulse/error.h>
|
||||
#include <pulse/pulseaudio.h>
|
||||
#include <pulse/simple.h>
|
||||
|
||||
#include "sunshine/platform/common.h"
|
||||
|
||||
#include "sunshine/config.h"
|
||||
#include "sunshine/main.h"
|
||||
#include "sunshine/thread_safe.h"
|
||||
|
||||
namespace platf {
|
||||
using namespace std::literals;
|
||||
|
||||
constexpr pa_channel_position_t position_mapping[] {
|
||||
PA_CHANNEL_POSITION_FRONT_LEFT,
|
||||
PA_CHANNEL_POSITION_FRONT_RIGHT,
|
||||
PA_CHANNEL_POSITION_FRONT_CENTER,
|
||||
PA_CHANNEL_POSITION_LFE,
|
||||
PA_CHANNEL_POSITION_REAR_LEFT,
|
||||
PA_CHANNEL_POSITION_REAR_RIGHT,
|
||||
PA_CHANNEL_POSITION_SIDE_LEFT,
|
||||
PA_CHANNEL_POSITION_SIDE_RIGHT,
|
||||
};
|
||||
|
||||
std::string to_string(const char *name, const std::uint8_t *mapping, int channels) {
|
||||
std::stringstream ss;
|
||||
|
||||
ss << "rate=48000 sink_name="sv << name << " format=s16le channels="sv << channels << " channel_map="sv;
|
||||
std::for_each_n(mapping, channels - 1, [&ss](std::uint8_t pos) {
|
||||
ss << pa_channel_position_to_string(position_mapping[pos]) << ',';
|
||||
});
|
||||
|
||||
ss << pa_channel_position_to_string(position_mapping[mapping[channels - 1]]);
|
||||
|
||||
ss << " sink_properties=device.description="sv << name;
|
||||
auto result = ss.str();
|
||||
|
||||
BOOST_LOG(debug) << "null-sink args: "sv << result;
|
||||
return result;
|
||||
}
|
||||
|
||||
struct mic_attr_t : public mic_t {
|
||||
util::safe_ptr<pa_simple, pa_simple_free> mic;
|
||||
|
||||
capture_e sample(std::vector<std::int16_t> &sample_buf) override {
|
||||
auto sample_size = sample_buf.size();
|
||||
|
||||
auto buf = sample_buf.data();
|
||||
int status;
|
||||
if(pa_simple_read(mic.get(), buf, sample_size * 2, &status)) {
|
||||
BOOST_LOG(error) << "pa_simple_read() failed: "sv << pa_strerror(status);
|
||||
|
||||
return capture_e::error;
|
||||
}
|
||||
|
||||
return capture_e::ok;
|
||||
}
|
||||
};
|
||||
|
||||
std::unique_ptr<mic_t> microphone(const std::uint8_t *mapping, int channels, std::uint32_t sample_rate) {
|
||||
auto mic = std::make_unique<mic_attr_t>();
|
||||
|
||||
pa_sample_spec ss { PA_SAMPLE_S16LE, sample_rate, (std::uint8_t)channels };
|
||||
pa_channel_map pa_map;
|
||||
|
||||
pa_map.channels = channels;
|
||||
std::for_each_n(pa_map.map, pa_map.channels, [mapping](auto &channel) mutable {
|
||||
channel = position_mapping[*mapping++];
|
||||
});
|
||||
|
||||
int status;
|
||||
|
||||
const char *audio_sink = "@DEFAULT_MONITOR@";
|
||||
if(!config::audio.sink.empty()) {
|
||||
audio_sink = config::audio.sink.c_str();
|
||||
}
|
||||
|
||||
mic->mic.reset(
|
||||
pa_simple_new(nullptr, "sunshine",
|
||||
pa_stream_direction_t::PA_STREAM_RECORD, audio_sink,
|
||||
"sunshine-record", &ss, &pa_map, nullptr, &status));
|
||||
|
||||
if(!mic->mic) {
|
||||
auto err_str = pa_strerror(status);
|
||||
BOOST_LOG(error) << "pa_simple_new() failed: "sv << err_str;
|
||||
|
||||
log_flush();
|
||||
std::abort();
|
||||
}
|
||||
|
||||
return mic;
|
||||
}
|
||||
|
||||
namespace pa {
|
||||
template<bool B, class T>
|
||||
struct add_const_helper;
|
||||
|
||||
template<class T>
|
||||
struct add_const_helper<true, T> {
|
||||
using type = const std::remove_pointer_t<T> *;
|
||||
};
|
||||
|
||||
template<class T>
|
||||
struct add_const_helper<false, T> {
|
||||
using type = const T *;
|
||||
};
|
||||
|
||||
template<class T>
|
||||
using add_const_t = typename add_const_helper<std::is_pointer_v<T>, T>::type;
|
||||
|
||||
template<class T>
|
||||
void pa_free(T *p) {
|
||||
pa_xfree(p);
|
||||
}
|
||||
using ctx_t = util::safe_ptr<pa_context, pa_context_unref>;
|
||||
using loop_t = util::safe_ptr<pa_mainloop, pa_mainloop_free>;
|
||||
using op_t = util::safe_ptr<pa_operation, pa_operation_unref>;
|
||||
using string_t = util::safe_ptr<char, pa_free<char>>;
|
||||
|
||||
template<class T>
|
||||
using cb_t = std::function<void(ctx_t::pointer, add_const_t<T> i, int eol)>;
|
||||
|
||||
template<class T>
|
||||
void cb(ctx_t::pointer ctx, add_const_t<T> i, int eol, void *userdata) {
|
||||
auto &f = *(cb_t<T> *)userdata;
|
||||
|
||||
// For some reason, pulseaudio calls this callback after disconnecting
|
||||
if(i && eol) {
|
||||
return;
|
||||
}
|
||||
|
||||
f(ctx, i, eol);
|
||||
}
|
||||
|
||||
void cb_i(ctx_t::pointer ctx, std::uint32_t i, void *userdata) {
|
||||
auto alarm = (safe::alarm_raw_t<int> *)userdata;
|
||||
|
||||
alarm->ring(i);
|
||||
}
|
||||
|
||||
void ctx_state_cb(ctx_t::pointer ctx, void *userdata) {
|
||||
auto &f = *(std::function<void(ctx_t::pointer)> *)userdata;
|
||||
|
||||
f(ctx);
|
||||
}
|
||||
|
||||
void success_cb(ctx_t::pointer ctx, int status, void *userdata) {
|
||||
assert(userdata != nullptr);
|
||||
|
||||
auto alarm = (safe::alarm_raw_t<int> *)userdata;
|
||||
alarm->ring(status ? 0 : 1);
|
||||
}
|
||||
|
||||
class server_t : public audio_control_t {
|
||||
enum ctx_event_e : int {
|
||||
ready,
|
||||
terminated,
|
||||
failed
|
||||
};
|
||||
|
||||
public:
|
||||
loop_t loop;
|
||||
ctx_t ctx;
|
||||
|
||||
struct {
|
||||
std::uint32_t stereo = PA_INVALID_INDEX;
|
||||
std::uint32_t surround51 = PA_INVALID_INDEX;
|
||||
std::uint32_t surround71 = PA_INVALID_INDEX;
|
||||
} index;
|
||||
|
||||
std::unique_ptr<safe::event_t<ctx_event_e>> events;
|
||||
std::unique_ptr<std::function<void(ctx_t::pointer)>> events_cb;
|
||||
|
||||
std::thread worker;
|
||||
int init() {
|
||||
events = std::make_unique<safe::event_t<ctx_event_e>>();
|
||||
loop.reset(pa_mainloop_new());
|
||||
ctx.reset(pa_context_new(pa_mainloop_get_api(loop.get()), "sunshine"));
|
||||
|
||||
events_cb = std::make_unique<std::function<void(ctx_t::pointer)>>([this](ctx_t::pointer ctx) {
|
||||
switch(pa_context_get_state(ctx)) {
|
||||
case PA_CONTEXT_READY:
|
||||
events->raise(ready);
|
||||
break;
|
||||
case PA_CONTEXT_TERMINATED:
|
||||
BOOST_LOG(debug) << "Pulseadio context terminated"sv;
|
||||
events->raise(terminated);
|
||||
break;
|
||||
case PA_CONTEXT_FAILED:
|
||||
BOOST_LOG(debug) << "Pulseadio context failed"sv;
|
||||
events->raise(failed);
|
||||
break;
|
||||
case PA_CONTEXT_CONNECTING:
|
||||
BOOST_LOG(debug) << "Connecting to pulseaudio"sv;
|
||||
case PA_CONTEXT_UNCONNECTED:
|
||||
case PA_CONTEXT_AUTHORIZING:
|
||||
case PA_CONTEXT_SETTING_NAME:
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
pa_context_set_state_callback(ctx.get(), ctx_state_cb, events_cb.get());
|
||||
|
||||
auto status = pa_context_connect(ctx.get(), nullptr, PA_CONTEXT_NOFLAGS, nullptr);
|
||||
if(status) {
|
||||
BOOST_LOG(error) << "Couldn't connect to pulseaudio: "sv << pa_strerror(status);
|
||||
return -1;
|
||||
}
|
||||
|
||||
worker = std::thread {
|
||||
[](loop_t::pointer loop) {
|
||||
int retval;
|
||||
auto status = pa_mainloop_run(loop, &retval);
|
||||
|
||||
if(status < 0) {
|
||||
BOOST_LOG(fatal) << "Couldn't run pulseaudio main loop"sv;
|
||||
|
||||
log_flush();
|
||||
std::abort();
|
||||
}
|
||||
},
|
||||
loop.get()
|
||||
};
|
||||
|
||||
auto event = events->pop();
|
||||
if(event == failed) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int load_null(const char *name, const std::uint8_t *channel_mapping, int channels) {
|
||||
auto alarm = safe::make_alarm<int>();
|
||||
|
||||
op_t op {
|
||||
pa_context_load_module(
|
||||
ctx.get(),
|
||||
"module-null-sink",
|
||||
to_string(name, channel_mapping, channels).c_str(),
|
||||
cb_i,
|
||||
alarm.get()),
|
||||
};
|
||||
|
||||
alarm->wait();
|
||||
return *alarm->status();
|
||||
}
|
||||
|
||||
int unload_null(std::uint32_t i) {
|
||||
if(i == PA_INVALID_INDEX) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
auto alarm = safe::make_alarm<int>();
|
||||
|
||||
op_t op {
|
||||
pa_context_unload_module(ctx.get(), i, success_cb, alarm.get())
|
||||
};
|
||||
|
||||
alarm->wait();
|
||||
|
||||
if(*alarm->status()) {
|
||||
BOOST_LOG(error) << "Couldn't unload null-sink with index ["sv << i << "]: "sv << pa_strerror(pa_context_errno(ctx.get()));
|
||||
return -1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
std::optional<sink_t> sink_info() override {
|
||||
constexpr auto stereo = "sink-sunshine-stereo";
|
||||
constexpr auto surround51 = "sink-sunshine-surround51";
|
||||
constexpr auto surround71 = "sink-sunshine-surround71";
|
||||
|
||||
auto alarm = safe::make_alarm<int>();
|
||||
|
||||
sink_t sink;
|
||||
|
||||
// If hardware sink with more channels found, set that as host
|
||||
int channels = 0;
|
||||
// Count of all virtual sinks that are created by us
|
||||
int nullcount = 0;
|
||||
|
||||
cb_t<pa_sink_info *> f = [&](ctx_t::pointer ctx, const pa_sink_info *sink_info, int eol) {
|
||||
if(!sink_info) {
|
||||
if(!eol) {
|
||||
BOOST_LOG(error) << "Couldn't get pulseaudio sink info: "sv << pa_strerror(pa_context_errno(ctx));
|
||||
|
||||
alarm->ring(-1);
|
||||
}
|
||||
|
||||
alarm->ring(0);
|
||||
return;
|
||||
}
|
||||
|
||||
if(sink_info->active_port != nullptr) {
|
||||
sink.host = sink_info->name;
|
||||
channels = sink_info->channel_map.channels;
|
||||
}
|
||||
|
||||
// Ensure Sunshine won't create a sink that already exists.
|
||||
if(!std::strcmp(sink_info->name, stereo)) {
|
||||
index.stereo = sink_info->owner_module;
|
||||
|
||||
++nullcount;
|
||||
}
|
||||
else if(!std::strcmp(sink_info->name, surround51)) {
|
||||
index.surround51 = sink_info->owner_module;
|
||||
|
||||
++nullcount;
|
||||
}
|
||||
else if(!std::strcmp(sink_info->name, surround71)) {
|
||||
index.surround71 = sink_info->owner_module;
|
||||
|
||||
++nullcount;
|
||||
}
|
||||
};
|
||||
|
||||
op_t op { pa_context_get_sink_info_list(ctx.get(), cb<pa_sink_info *>, &f) };
|
||||
|
||||
if(!op) {
|
||||
BOOST_LOG(error) << "Couldn't create card info operation: "sv << pa_strerror(pa_context_errno(ctx.get()));
|
||||
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
alarm->wait();
|
||||
|
||||
if(*alarm->status()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
if(!channels) {
|
||||
BOOST_LOG(warning) << "Couldn't find an active sink"sv;
|
||||
}
|
||||
|
||||
if(index.stereo == PA_INVALID_INDEX) {
|
||||
index.stereo = load_null(stereo, speaker::map_stereo, sizeof(speaker::map_stereo));
|
||||
if(index.stereo == PA_INVALID_INDEX) {
|
||||
BOOST_LOG(warning) << "Couldn't create virtual sink for stereo: "sv << pa_strerror(pa_context_errno(ctx.get()));
|
||||
}
|
||||
else {
|
||||
++nullcount;
|
||||
}
|
||||
}
|
||||
|
||||
if(index.surround51 == PA_INVALID_INDEX) {
|
||||
index.surround51 = load_null(surround51, speaker::map_surround51, sizeof(speaker::map_surround51));
|
||||
if(index.surround51 == PA_INVALID_INDEX) {
|
||||
BOOST_LOG(warning) << "Couldn't create virtual sink for surround-51: "sv << pa_strerror(pa_context_errno(ctx.get()));
|
||||
}
|
||||
else {
|
||||
++nullcount;
|
||||
}
|
||||
}
|
||||
|
||||
if(index.surround71 == PA_INVALID_INDEX) {
|
||||
index.surround71 = load_null(surround71, speaker::map_surround71, sizeof(speaker::map_surround71));
|
||||
if(index.surround71 == PA_INVALID_INDEX) {
|
||||
BOOST_LOG(warning) << "Couldn't create virtual sink for surround-71: "sv << pa_strerror(pa_context_errno(ctx.get()));
|
||||
}
|
||||
else {
|
||||
++nullcount;
|
||||
}
|
||||
}
|
||||
|
||||
if(nullcount == 3) {
|
||||
sink.null = std::make_optional(sink_t::null_t { stereo, surround51, surround71 });
|
||||
}
|
||||
|
||||
return std::make_optional(std::move(sink));
|
||||
}
|
||||
|
||||
std::unique_ptr<mic_t> microphone(const std::uint8_t *mapping, int channels, std::uint32_t sample_rate, std::uint32_t frame_size) override {
|
||||
return ::platf::microphone(mapping, channels, sample_rate);
|
||||
}
|
||||
|
||||
int set_sink(const std::string &sink) override {
|
||||
auto alarm = safe::make_alarm<int>();
|
||||
|
||||
op_t op {
|
||||
pa_context_set_default_sink(
|
||||
ctx.get(), sink.c_str(), success_cb, alarm.get()),
|
||||
};
|
||||
|
||||
if(!op) {
|
||||
BOOST_LOG(error) << "Couldn't create set default-sink operation: "sv << pa_strerror(pa_context_errno(ctx.get()));
|
||||
return -1;
|
||||
}
|
||||
|
||||
alarm->wait();
|
||||
if(*alarm->status()) {
|
||||
BOOST_LOG(error) << "Couldn't set default-sink ["sv << sink << "]: "sv << pa_strerror(pa_context_errno(ctx.get()));
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
~server_t() override {
|
||||
unload_null(index.stereo);
|
||||
unload_null(index.surround51);
|
||||
unload_null(index.surround71);
|
||||
|
||||
if(worker.joinable()) {
|
||||
pa_context_disconnect(ctx.get());
|
||||
|
||||
KITTY_WHILE_LOOP(auto event = events->pop(), event != terminated && event != failed, {
|
||||
event = events->pop();
|
||||
})
|
||||
|
||||
pa_mainloop_quit(loop.get(), 0);
|
||||
worker.join();
|
||||
}
|
||||
}
|
||||
};
|
||||
} // namespace pa
|
||||
|
||||
std::unique_ptr<audio_control_t> audio_control() {
|
||||
auto audio = std::make_unique<pa::server_t>();
|
||||
|
||||
if(audio->init()) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
return audio;
|
||||
}
|
||||
} // namespace platf
|
||||
408
sunshine/platform/linux/display.cpp
Normal file
408
sunshine/platform/linux/display.cpp
Normal file
@@ -0,0 +1,408 @@
|
||||
//
|
||||
// Created by loki on 6/21/19.
|
||||
//
|
||||
|
||||
#include "sunshine/platform/common.h"
|
||||
|
||||
#include <fstream>
|
||||
|
||||
#include <X11/X.h>
|
||||
#include <X11/Xlib.h>
|
||||
#include <X11/Xutil.h>
|
||||
#include <X11/extensions/Xfixes.h>
|
||||
#include <X11/extensions/Xrandr.h>
|
||||
#include <sys/ipc.h>
|
||||
#include <sys/shm.h>
|
||||
#include <xcb/shm.h>
|
||||
#include <xcb/xfixes.h>
|
||||
|
||||
#include "sunshine/config.h"
|
||||
#include "sunshine/main.h"
|
||||
#include "sunshine/task_pool.h"
|
||||
|
||||
#include "vaapi.h"
|
||||
|
||||
using namespace std::literals;
|
||||
|
||||
namespace platf {
|
||||
|
||||
void freeImage(XImage *);
|
||||
void freeX(XFixesCursorImage *);
|
||||
|
||||
using xcb_connect_t = util::safe_ptr<xcb_connection_t, xcb_disconnect>;
|
||||
using xcb_img_t = util::c_ptr<xcb_shm_get_image_reply_t>;
|
||||
|
||||
using xdisplay_t = util::safe_ptr_v2<Display, int, XCloseDisplay>;
|
||||
using ximg_t = util::safe_ptr<XImage, freeImage>;
|
||||
using xcursor_t = util::safe_ptr<XFixesCursorImage, freeX>;
|
||||
|
||||
using crtc_info_t = util::safe_ptr<_XRRCrtcInfo, XRRFreeCrtcInfo>;
|
||||
using output_info_t = util::safe_ptr<_XRROutputInfo, XRRFreeOutputInfo>;
|
||||
using screen_res_t = util::safe_ptr<_XRRScreenResources, XRRFreeScreenResources>;
|
||||
|
||||
class shm_id_t {
|
||||
public:
|
||||
shm_id_t() : id { -1 } {}
|
||||
shm_id_t(int id) : id { id } {}
|
||||
shm_id_t(shm_id_t &&other) noexcept : id(other.id) {
|
||||
other.id = -1;
|
||||
}
|
||||
|
||||
~shm_id_t() {
|
||||
if(id != -1) {
|
||||
shmctl(id, IPC_RMID, nullptr);
|
||||
id = -1;
|
||||
}
|
||||
}
|
||||
int id;
|
||||
};
|
||||
|
||||
class shm_data_t {
|
||||
public:
|
||||
shm_data_t() : data { (void *)-1 } {}
|
||||
shm_data_t(void *data) : data { data } {}
|
||||
|
||||
shm_data_t(shm_data_t &&other) noexcept : data(other.data) {
|
||||
other.data = (void *)-1;
|
||||
}
|
||||
|
||||
~shm_data_t() {
|
||||
if((std::uintptr_t)data != -1) {
|
||||
shmdt(data);
|
||||
}
|
||||
}
|
||||
|
||||
void *data;
|
||||
};
|
||||
|
||||
struct x11_img_t : public img_t {
|
||||
ximg_t img;
|
||||
};
|
||||
|
||||
struct shm_img_t : public img_t {
|
||||
~shm_img_t() override {
|
||||
delete[] data;
|
||||
data = nullptr;
|
||||
}
|
||||
};
|
||||
|
||||
void blend_cursor(Display *display, img_t &img, int offsetX, int offsetY) {
|
||||
xcursor_t overlay { XFixesGetCursorImage(display) };
|
||||
|
||||
if(!overlay) {
|
||||
BOOST_LOG(error) << "Couldn't get cursor from XFixesGetCursorImage"sv;
|
||||
return;
|
||||
}
|
||||
|
||||
overlay->x -= overlay->xhot;
|
||||
overlay->y -= overlay->yhot;
|
||||
|
||||
overlay->x -= offsetX;
|
||||
overlay->y -= offsetY;
|
||||
|
||||
overlay->x = std::max((short)0, overlay->x);
|
||||
overlay->y = std::max((short)0, overlay->y);
|
||||
|
||||
auto pixels = (int *)img.data;
|
||||
|
||||
auto screen_height = img.height;
|
||||
auto screen_width = img.width;
|
||||
|
||||
auto delta_height = std::min<uint16_t>(overlay->height, std::max(0, screen_height - overlay->y));
|
||||
auto delta_width = std::min<uint16_t>(overlay->width, std::max(0, screen_width - overlay->x));
|
||||
for(auto y = 0; y < delta_height; ++y) {
|
||||
auto overlay_begin = &overlay->pixels[y * overlay->width];
|
||||
auto overlay_end = &overlay->pixels[y * overlay->width + delta_width];
|
||||
|
||||
auto pixels_begin = &pixels[(y + overlay->y) * (img.row_pitch / img.pixel_pitch) + overlay->x];
|
||||
|
||||
std::for_each(overlay_begin, overlay_end, [&](long pixel) {
|
||||
int *pixel_p = (int *)&pixel;
|
||||
|
||||
auto colors_in = (uint8_t *)pixels_begin;
|
||||
|
||||
auto alpha = (*(uint *)pixel_p) >> 24u;
|
||||
if(alpha == 255) {
|
||||
*pixels_begin = *pixel_p;
|
||||
}
|
||||
else {
|
||||
auto colors_out = (uint8_t *)pixel_p;
|
||||
colors_in[0] = colors_out[0] + (colors_in[0] * (255 - alpha) + 255 / 2) / 255;
|
||||
colors_in[1] = colors_out[1] + (colors_in[1] * (255 - alpha) + 255 / 2) / 255;
|
||||
colors_in[2] = colors_out[2] + (colors_in[2] * (255 - alpha) + 255 / 2) / 255;
|
||||
}
|
||||
++pixels_begin;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
struct x11_attr_t : public display_t {
|
||||
xdisplay_t xdisplay;
|
||||
Window xwindow;
|
||||
XWindowAttributes xattr;
|
||||
|
||||
mem_type_e mem_type;
|
||||
|
||||
/*
|
||||
* Last X (NOT the streamed monitor!) size.
|
||||
* This way we can trigger reinitialization if the dimensions changed while streaming
|
||||
*/
|
||||
int 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
|
||||
@@ -1,16 +1,17 @@
|
||||
#include <libevdev/libevdev.h>
|
||||
#include <libevdev/libevdev-uinput.h>
|
||||
#include <libevdev/libevdev.h>
|
||||
|
||||
#include <X11/X.h>
|
||||
#include <X11/Xlib.h>
|
||||
#include <X11/Xutil.h>
|
||||
#include <X11/extensions/XTest.h>
|
||||
|
||||
#include <cmath>
|
||||
#include <cstring>
|
||||
#include <filesystem>
|
||||
|
||||
#include "common.h"
|
||||
#include "sunshine/main.h"
|
||||
#include "sunshine/platform/common.h"
|
||||
#include "sunshine/utility.h"
|
||||
|
||||
// Support older versions
|
||||
@@ -24,13 +25,27 @@
|
||||
|
||||
namespace platf {
|
||||
using namespace std::literals;
|
||||
using evdev_t = util::safe_ptr<libevdev, libevdev_free>;
|
||||
using evdev_t = util::safe_ptr<libevdev, libevdev_free>;
|
||||
using uinput_t = util::safe_ptr<libevdev_uinput, libevdev_uinput_destroy>;
|
||||
|
||||
using keyboard_t = util::safe_ptr_v2<Display, int, XCloseDisplay>;
|
||||
|
||||
constexpr touch_port_t target_touch_port {
|
||||
0, 0,
|
||||
19200, 12000
|
||||
};
|
||||
|
||||
struct input_raw_t {
|
||||
public:
|
||||
void clear_touchscreen() {
|
||||
std::filesystem::path touch_path { "sunshine_touchscreen"sv };
|
||||
|
||||
if(std::filesystem::is_symlink(touch_path)) {
|
||||
std::filesystem::remove(touch_path);
|
||||
}
|
||||
|
||||
touch_input.reset();
|
||||
}
|
||||
void clear_mouse() {
|
||||
std::filesystem::path mouse_path { "sunshine_mouse"sv };
|
||||
|
||||
@@ -51,13 +66,11 @@ public:
|
||||
std::filesystem::remove(gamepad_path);
|
||||
}
|
||||
|
||||
gamepads[nr].first.reset();
|
||||
gamepads[nr] = std::make_pair(uinput_t {}, gamepad_state_t {});
|
||||
}
|
||||
|
||||
int create_mouse() {
|
||||
libevdev_uinput *buf;
|
||||
int err = libevdev_uinput_create_from_device(mouse_dev.get(), LIBEVDEV_UINPUT_OPEN_MANAGED, &buf);
|
||||
mouse_input.reset(buf);
|
||||
int err = libevdev_uinput_create_from_device(mouse_dev.get(), LIBEVDEV_UINPUT_OPEN_MANAGED, &mouse_input);
|
||||
|
||||
if(err) {
|
||||
BOOST_LOG(error) << "Could not create Sunshine Mouse: "sv << strerror(-err);
|
||||
@@ -69,13 +82,24 @@ public:
|
||||
return 0;
|
||||
}
|
||||
|
||||
int create_gamepad(int nr) {
|
||||
int create_touchscreen() {
|
||||
int err = libevdev_uinput_create_from_device(touch_dev.get(), LIBEVDEV_UINPUT_OPEN_MANAGED, &touch_input);
|
||||
|
||||
if(err) {
|
||||
BOOST_LOG(error) << "Could not create Sunshine Touchscreen: "sv << strerror(-err);
|
||||
return -1;
|
||||
}
|
||||
|
||||
std::filesystem::create_symlink(libevdev_uinput_get_devnode(touch_input.get()), "sunshine_touchscreen"sv);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int alloc_gamepad(int nr) {
|
||||
TUPLE_2D_REF(input, gamepad_state, gamepads[nr]);
|
||||
|
||||
libevdev_uinput *buf;
|
||||
int err = libevdev_uinput_create_from_device(gamepad_dev.get(), LIBEVDEV_UINPUT_OPEN_MANAGED, &buf);
|
||||
int err = libevdev_uinput_create_from_device(gamepad_dev.get(), LIBEVDEV_UINPUT_OPEN_MANAGED, &input);
|
||||
|
||||
input.reset(buf);
|
||||
gamepad_state = gamepad_state_t {};
|
||||
|
||||
if(err) {
|
||||
@@ -86,12 +110,17 @@ public:
|
||||
std::stringstream ss;
|
||||
ss << "sunshine_gamepad_"sv << nr;
|
||||
std::filesystem::path gamepad_path { ss.str() };
|
||||
std::filesystem::create_symlink(libevdev_uinput_get_devnode(input.get()), gamepad_path);
|
||||
|
||||
if(std::filesystem::is_symlink(gamepad_path)) {
|
||||
std::filesystem::remove(gamepad_path);
|
||||
}
|
||||
|
||||
std::filesystem::create_symlink(libevdev_uinput_get_devnode(input.get()), gamepad_path);
|
||||
return 0;
|
||||
}
|
||||
|
||||
void clear() {
|
||||
clear_touchscreen();
|
||||
clear_mouse();
|
||||
for(int x = 0; x < gamepads.size(); ++x) {
|
||||
clear_gamepad(x);
|
||||
@@ -102,18 +131,33 @@ public:
|
||||
clear();
|
||||
}
|
||||
|
||||
evdev_t gamepad_dev;
|
||||
|
||||
std::vector<std::pair<uinput_t, gamepad_state_t>> gamepads;
|
||||
|
||||
evdev_t mouse_dev;
|
||||
uinput_t mouse_input;
|
||||
uinput_t touch_input;
|
||||
|
||||
evdev_t gamepad_dev;
|
||||
evdev_t touch_dev;
|
||||
evdev_t mouse_dev;
|
||||
|
||||
keyboard_t keyboard;
|
||||
};
|
||||
|
||||
void abs_mouse(input_t &input, const touch_port_t &touch_port, float x, float y) {
|
||||
auto touchscreen = ((input_raw_t *)input.get())->touch_input.get();
|
||||
|
||||
auto scaled_x = (int)std::lround((x + touch_port.offset_x) * ((float)target_touch_port.width / (float)touch_port.width));
|
||||
auto scaled_y = (int)std::lround((y + touch_port.offset_y) * ((float)target_touch_port.height / (float)touch_port.height));
|
||||
|
||||
libevdev_uinput_write_event(touchscreen, EV_ABS, ABS_X, scaled_x);
|
||||
libevdev_uinput_write_event(touchscreen, EV_ABS, ABS_Y, scaled_y);
|
||||
libevdev_uinput_write_event(touchscreen, EV_KEY, BTN_TOOL_FINGER, 1);
|
||||
libevdev_uinput_write_event(touchscreen, EV_KEY, BTN_TOOL_FINGER, 0);
|
||||
|
||||
libevdev_uinput_write_event(touchscreen, EV_SYN, SYN_REPORT, 0);
|
||||
}
|
||||
|
||||
void move_mouse(input_t &input, int deltaX, int deltaY) {
|
||||
auto mouse = ((input_raw_t*)input.get())->mouse_input.get();
|
||||
auto mouse = ((input_raw_t *)input.get())->mouse_input.get();
|
||||
|
||||
if(deltaX) {
|
||||
libevdev_uinput_write_event(mouse, EV_REL, REL_X, deltaX);
|
||||
@@ -132,26 +176,26 @@ void button_mouse(input_t &input, int button, bool release) {
|
||||
|
||||
if(button == 1) {
|
||||
btn_type = BTN_LEFT;
|
||||
scan = 90001;
|
||||
scan = 90001;
|
||||
}
|
||||
else if(button == 2) {
|
||||
btn_type = BTN_MIDDLE;
|
||||
scan = 90003;
|
||||
scan = 90003;
|
||||
}
|
||||
else if(button == 3) {
|
||||
btn_type = BTN_RIGHT;
|
||||
scan = 90002;
|
||||
scan = 90002;
|
||||
}
|
||||
else if(button == 4) {
|
||||
btn_type = BTN_SIDE;
|
||||
scan = 90004;
|
||||
scan = 90004;
|
||||
}
|
||||
else {
|
||||
btn_type = BTN_EXTRA;
|
||||
scan = 90005;
|
||||
scan = 90005;
|
||||
}
|
||||
|
||||
auto mouse = ((input_raw_t*)input.get())->mouse_input.get();
|
||||
auto mouse = ((input_raw_t *)input.get())->mouse_input.get();
|
||||
libevdev_uinput_write_event(mouse, EV_MSC, MSC_SCAN, scan);
|
||||
libevdev_uinput_write_event(mouse, EV_KEY, btn_type, release ? 0 : 1);
|
||||
libevdev_uinput_write_event(mouse, EV_SYN, SYN_REPORT, 0);
|
||||
@@ -160,7 +204,7 @@ void button_mouse(input_t &input, int button, bool release) {
|
||||
void scroll(input_t &input, int high_res_distance) {
|
||||
int distance = high_res_distance / 120;
|
||||
|
||||
auto mouse = ((input_raw_t*)input.get())->mouse_input.get();
|
||||
auto mouse = ((input_raw_t *)input.get())->mouse_input.get();
|
||||
libevdev_uinput_write_event(mouse, EV_REL, REL_WHEEL, distance);
|
||||
libevdev_uinput_write_event(mouse, EV_REL, REL_WHEEL_HI_RES, high_res_distance);
|
||||
libevdev_uinput_write_event(mouse, EV_SYN, SYN_REPORT, 0);
|
||||
@@ -168,7 +212,7 @@ void scroll(input_t &input, int high_res_distance) {
|
||||
|
||||
uint16_t keysym(uint16_t modcode) {
|
||||
constexpr auto VK_NUMPAD = 0x60;
|
||||
constexpr auto VK_F1 = 0x70;
|
||||
constexpr auto VK_F1 = 0x70;
|
||||
|
||||
if(modcode >= VK_NUMPAD && modcode < VK_NUMPAD + 10) {
|
||||
return XK_KP_0 + (modcode - VK_NUMPAD);
|
||||
@@ -180,108 +224,108 @@ uint16_t keysym(uint16_t modcode) {
|
||||
|
||||
|
||||
switch(modcode) {
|
||||
case 0x08:
|
||||
return XK_BackSpace;
|
||||
case 0x09:
|
||||
return XK_Tab;
|
||||
case 0x0D:
|
||||
return XK_Return;
|
||||
case 0x13:
|
||||
return XK_Pause;
|
||||
case 0x14:
|
||||
return XK_Caps_Lock;
|
||||
case 0x1B:
|
||||
return XK_Escape;
|
||||
case 0x21:
|
||||
return XK_Page_Up;
|
||||
case 0x22:
|
||||
return XK_Page_Down;
|
||||
case 0x23:
|
||||
return XK_End;
|
||||
case 0x24:
|
||||
return XK_Home;
|
||||
case 0x25:
|
||||
return XK_Left;
|
||||
case 0x26:
|
||||
return XK_Up;
|
||||
case 0x27:
|
||||
return XK_Right;
|
||||
case 0x28:
|
||||
return XK_Down;
|
||||
case 0x29:
|
||||
return XK_Select;
|
||||
case 0x2B:
|
||||
return XK_Execute;
|
||||
case 0x2C:
|
||||
return XK_Print;
|
||||
case 0x2D:
|
||||
return XK_Insert;
|
||||
case 0x2E:
|
||||
return XK_Delete;
|
||||
case 0x2F:
|
||||
return XK_Help;
|
||||
case 0x6A:
|
||||
return XK_KP_Multiply;
|
||||
case 0x6B:
|
||||
return XK_KP_Add;
|
||||
case 0x6C:
|
||||
return XK_KP_Separator;
|
||||
case 0x6D:
|
||||
return XK_KP_Subtract;
|
||||
case 0x6E:
|
||||
return XK_KP_Decimal;
|
||||
case 0x6F:
|
||||
return XK_KP_Divide;
|
||||
case 0x90:
|
||||
return XK_Num_Lock;
|
||||
case 0x91:
|
||||
return XK_Scroll_Lock;
|
||||
case 0xA0:
|
||||
return XK_Shift_L;
|
||||
case 0xA1:
|
||||
return XK_Shift_R;
|
||||
case 0xA2:
|
||||
return XK_Control_L;
|
||||
case 0xA3:
|
||||
return XK_Control_R;
|
||||
case 0xA4:
|
||||
return XK_Alt_L;
|
||||
case 0xA5: /* return XK_Alt_R; */
|
||||
return XK_Super_L;
|
||||
case 0x5B:
|
||||
return XK_Super_L;
|
||||
case 0x5C:
|
||||
return XK_Super_R;
|
||||
case 0xBA:
|
||||
return XK_semicolon;
|
||||
case 0xBB:
|
||||
return XK_equal;
|
||||
case 0xBC:
|
||||
return XK_comma;
|
||||
case 0xBD:
|
||||
return XK_minus;
|
||||
case 0xBE:
|
||||
return XK_period;
|
||||
case 0xBF:
|
||||
return XK_slash;
|
||||
case 0xC0:
|
||||
return XK_grave;
|
||||
case 0xDB:
|
||||
return XK_bracketleft;
|
||||
case 0xDC:
|
||||
return XK_backslash;
|
||||
case 0xDD:
|
||||
return XK_bracketright;
|
||||
case 0xDE:
|
||||
return XK_apostrophe;
|
||||
case 0x08:
|
||||
return XK_BackSpace;
|
||||
case 0x09:
|
||||
return XK_Tab;
|
||||
case 0x0D:
|
||||
return XK_Return;
|
||||
case 0x13:
|
||||
return XK_Pause;
|
||||
case 0x14:
|
||||
return XK_Caps_Lock;
|
||||
case 0x1B:
|
||||
return XK_Escape;
|
||||
case 0x21:
|
||||
return XK_Page_Up;
|
||||
case 0x22:
|
||||
return XK_Page_Down;
|
||||
case 0x23:
|
||||
return XK_End;
|
||||
case 0x24:
|
||||
return XK_Home;
|
||||
case 0x25:
|
||||
return XK_Left;
|
||||
case 0x26:
|
||||
return XK_Up;
|
||||
case 0x27:
|
||||
return XK_Right;
|
||||
case 0x28:
|
||||
return XK_Down;
|
||||
case 0x29:
|
||||
return XK_Select;
|
||||
case 0x2B:
|
||||
return XK_Execute;
|
||||
case 0x2C:
|
||||
return XK_Print;
|
||||
case 0x2D:
|
||||
return XK_Insert;
|
||||
case 0x2E:
|
||||
return XK_Delete;
|
||||
case 0x2F:
|
||||
return XK_Help;
|
||||
case 0x6A:
|
||||
return XK_KP_Multiply;
|
||||
case 0x6B:
|
||||
return XK_KP_Add;
|
||||
case 0x6C:
|
||||
return XK_KP_Separator;
|
||||
case 0x6D:
|
||||
return XK_KP_Subtract;
|
||||
case 0x6E:
|
||||
return XK_KP_Decimal;
|
||||
case 0x6F:
|
||||
return XK_KP_Divide;
|
||||
case 0x90:
|
||||
return XK_Num_Lock;
|
||||
case 0x91:
|
||||
return XK_Scroll_Lock;
|
||||
case 0xA0:
|
||||
return XK_Shift_L;
|
||||
case 0xA1:
|
||||
return XK_Shift_R;
|
||||
case 0xA2:
|
||||
return XK_Control_L;
|
||||
case 0xA3:
|
||||
return XK_Control_R;
|
||||
case 0xA4:
|
||||
return XK_Alt_L;
|
||||
case 0xA5: /* return XK_Alt_R; */
|
||||
return XK_Super_L;
|
||||
case 0x5B:
|
||||
return XK_Super_L;
|
||||
case 0x5C:
|
||||
return XK_Super_R;
|
||||
case 0xBA:
|
||||
return XK_semicolon;
|
||||
case 0xBB:
|
||||
return XK_equal;
|
||||
case 0xBC:
|
||||
return XK_comma;
|
||||
case 0xBD:
|
||||
return XK_minus;
|
||||
case 0xBE:
|
||||
return XK_period;
|
||||
case 0xBF:
|
||||
return XK_slash;
|
||||
case 0xC0:
|
||||
return XK_grave;
|
||||
case 0xDB:
|
||||
return XK_bracketleft;
|
||||
case 0xDC:
|
||||
return XK_backslash;
|
||||
case 0xDD:
|
||||
return XK_bracketright;
|
||||
case 0xDE:
|
||||
return XK_apostrophe;
|
||||
}
|
||||
|
||||
return modcode;
|
||||
}
|
||||
|
||||
void keyboard(input_t &input, uint16_t modcode, bool release) {
|
||||
auto &keyboard = ((input_raw_t*)input.get())->keyboard;
|
||||
KeyCode kc = XKeysymToKeycode(keyboard.get(), keysym(modcode));
|
||||
auto &keyboard = ((input_raw_t *)input.get())->keyboard;
|
||||
KeyCode kc = XKeysymToKeycode(keyboard.get(), keysym(modcode));
|
||||
|
||||
if(!kc) {
|
||||
return;
|
||||
@@ -293,11 +337,19 @@ void keyboard(input_t &input, uint16_t modcode, bool release) {
|
||||
XFlush(keyboard.get());
|
||||
}
|
||||
|
||||
int alloc_gamepad(input_t &input, int nr) {
|
||||
return ((input_raw_t *)input.get())->alloc_gamepad(nr);
|
||||
}
|
||||
|
||||
void free_gamepad(input_t &input, int nr) {
|
||||
((input_raw_t *)input.get())->clear_gamepad(nr);
|
||||
}
|
||||
|
||||
void gamepad(input_t &input, int nr, const gamepad_state_t &gamepad_state) {
|
||||
TUPLE_2D_REF(uinput, gamepad_state_old, ((input_raw_t*)input.get())->gamepads[nr]);
|
||||
TUPLE_2D_REF(uinput, gamepad_state_old, ((input_raw_t *)input.get())->gamepads[nr]);
|
||||
|
||||
|
||||
auto bf = gamepad_state.buttonFlags ^ gamepad_state_old.buttonFlags;
|
||||
auto bf = gamepad_state.buttonFlags ^ gamepad_state_old.buttonFlags;
|
||||
auto bf_new = gamepad_state.buttonFlags;
|
||||
|
||||
if(bf) {
|
||||
@@ -314,17 +366,17 @@ void gamepad(input_t &input, int nr, const gamepad_state_t &gamepad_state) {
|
||||
libevdev_uinput_write_event(uinput.get(), EV_ABS, ABS_HAT0X, button_state);
|
||||
}
|
||||
|
||||
if(START & bf) libevdev_uinput_write_event(uinput.get(), EV_KEY, BTN_START, bf_new & START ? 1 : 0);
|
||||
if(BACK & bf) libevdev_uinput_write_event(uinput.get(), EV_KEY, BTN_SELECT, bf_new & BACK ? 1 : 0);
|
||||
if(LEFT_STICK & bf) libevdev_uinput_write_event(uinput.get(), EV_KEY, BTN_THUMBL, bf_new & LEFT_STICK ? 1 : 0);
|
||||
if(RIGHT_STICK & bf) libevdev_uinput_write_event(uinput.get(), EV_KEY, BTN_THUMBR, bf_new & RIGHT_STICK ? 1 : 0);
|
||||
if(LEFT_BUTTON & bf) libevdev_uinput_write_event(uinput.get(), EV_KEY, BTN_TL, bf_new & LEFT_BUTTON ? 1 : 0);
|
||||
if(RIGHT_BUTTON & bf) libevdev_uinput_write_event(uinput.get(), EV_KEY, BTN_TR, bf_new & RIGHT_BUTTON ? 1 : 0);
|
||||
if(HOME & bf) libevdev_uinput_write_event(uinput.get(), EV_KEY, BTN_MODE, bf_new & HOME ? 1 : 0);
|
||||
if(A & bf) libevdev_uinput_write_event(uinput.get(), EV_KEY, BTN_SOUTH, bf_new & A ? 1 : 0);
|
||||
if(B & bf) libevdev_uinput_write_event(uinput.get(), EV_KEY, BTN_EAST, bf_new & B ? 1 : 0);
|
||||
if(X & bf) libevdev_uinput_write_event(uinput.get(), EV_KEY, BTN_NORTH, bf_new & X ? 1 : 0);
|
||||
if(Y & bf) libevdev_uinput_write_event(uinput.get(), EV_KEY, BTN_WEST, bf_new & Y ? 1 : 0);
|
||||
if(START & bf) libevdev_uinput_write_event(uinput.get(), EV_KEY, BTN_START, bf_new & START ? 1 : 0);
|
||||
if(BACK & bf) libevdev_uinput_write_event(uinput.get(), EV_KEY, BTN_SELECT, bf_new & BACK ? 1 : 0);
|
||||
if(LEFT_STICK & bf) libevdev_uinput_write_event(uinput.get(), EV_KEY, BTN_THUMBL, bf_new & LEFT_STICK ? 1 : 0);
|
||||
if(RIGHT_STICK & bf) libevdev_uinput_write_event(uinput.get(), EV_KEY, BTN_THUMBR, bf_new & RIGHT_STICK ? 1 : 0);
|
||||
if(LEFT_BUTTON & bf) libevdev_uinput_write_event(uinput.get(), EV_KEY, BTN_TL, bf_new & LEFT_BUTTON ? 1 : 0);
|
||||
if(RIGHT_BUTTON & bf) libevdev_uinput_write_event(uinput.get(), EV_KEY, BTN_TR, bf_new & RIGHT_BUTTON ? 1 : 0);
|
||||
if(HOME & bf) libevdev_uinput_write_event(uinput.get(), EV_KEY, BTN_MODE, bf_new & HOME ? 1 : 0);
|
||||
if(A & bf) libevdev_uinput_write_event(uinput.get(), EV_KEY, BTN_SOUTH, bf_new & A ? 1 : 0);
|
||||
if(B & bf) libevdev_uinput_write_event(uinput.get(), EV_KEY, BTN_EAST, bf_new & B ? 1 : 0);
|
||||
if(X & bf) libevdev_uinput_write_event(uinput.get(), EV_KEY, BTN_NORTH, bf_new & X ? 1 : 0);
|
||||
if(Y & bf) libevdev_uinput_write_event(uinput.get(), EV_KEY, BTN_WEST, bf_new & Y ? 1 : 0);
|
||||
}
|
||||
|
||||
if(gamepad_state_old.lt != gamepad_state.lt) {
|
||||
@@ -356,7 +408,7 @@ void gamepad(input_t &input, int nr, const gamepad_state_t &gamepad_state) {
|
||||
}
|
||||
|
||||
evdev_t mouse() {
|
||||
evdev_t dev { libevdev_new() };
|
||||
evdev_t dev { libevdev_new() };
|
||||
|
||||
libevdev_set_uniq(dev.get(), "Sunshine Mouse");
|
||||
libevdev_set_id_product(dev.get(), 0x4038);
|
||||
@@ -397,6 +449,48 @@ evdev_t mouse() {
|
||||
return dev;
|
||||
}
|
||||
|
||||
evdev_t touchscreen() {
|
||||
evdev_t dev { libevdev_new() };
|
||||
|
||||
libevdev_set_uniq(dev.get(), "Sunshine Touch");
|
||||
libevdev_set_id_product(dev.get(), 0xDEAD);
|
||||
libevdev_set_id_vendor(dev.get(), 0xBEEF);
|
||||
libevdev_set_id_bustype(dev.get(), 0x3);
|
||||
libevdev_set_id_version(dev.get(), 0x111);
|
||||
libevdev_set_name(dev.get(), "Touchscreen passthrough");
|
||||
|
||||
libevdev_enable_property(dev.get(), INPUT_PROP_DIRECT);
|
||||
|
||||
|
||||
libevdev_enable_event_type(dev.get(), EV_KEY);
|
||||
libevdev_enable_event_code(dev.get(), EV_KEY, BTN_TOUCH, nullptr);
|
||||
libevdev_enable_event_code(dev.get(), EV_KEY, BTN_TOOL_PEN, nullptr); // Needed to be enabled for BTN_TOOL_FINGER to work.
|
||||
libevdev_enable_event_code(dev.get(), EV_KEY, BTN_TOOL_FINGER, nullptr);
|
||||
|
||||
input_absinfo absx {
|
||||
0,
|
||||
0,
|
||||
target_touch_port.width,
|
||||
1,
|
||||
0,
|
||||
28
|
||||
};
|
||||
|
||||
input_absinfo absy {
|
||||
0,
|
||||
0,
|
||||
target_touch_port.height,
|
||||
1,
|
||||
0,
|
||||
28
|
||||
};
|
||||
libevdev_enable_event_type(dev.get(), EV_ABS);
|
||||
libevdev_enable_event_code(dev.get(), EV_ABS, ABS_X, &absx);
|
||||
libevdev_enable_event_code(dev.get(), EV_ABS, ABS_Y, &absy);
|
||||
|
||||
return dev;
|
||||
}
|
||||
|
||||
evdev_t x360() {
|
||||
evdev_t dev { libevdev_new() };
|
||||
|
||||
@@ -459,9 +553,8 @@ evdev_t x360() {
|
||||
|
||||
input_t input() {
|
||||
input_t result { new input_raw_t() };
|
||||
auto &gp = *(input_raw_t*)result.get();
|
||||
auto &gp = *(input_raw_t *)result.get();
|
||||
|
||||
gp.gamepads.resize(MAX_GAMEPADS);
|
||||
gp.keyboard.reset(XOpenDisplay(nullptr));
|
||||
|
||||
// If we do not have a keyboard, gamepad or mouse, no input is possible and we should abort
|
||||
@@ -471,19 +564,15 @@ input_t input() {
|
||||
std::abort();
|
||||
}
|
||||
|
||||
gp.gamepads.resize(MAX_GAMEPADS);
|
||||
|
||||
// Ensure starting from clean slate
|
||||
gp.clear();
|
||||
gp.mouse_dev = mouse();
|
||||
gp.touch_dev = touchscreen();
|
||||
gp.mouse_dev = mouse();
|
||||
gp.gamepad_dev = x360();
|
||||
|
||||
for(int x = 0; x < gp.gamepads.size(); ++x) {
|
||||
if(gp.create_gamepad(x)) {
|
||||
log_flush();
|
||||
std::abort();
|
||||
}
|
||||
}
|
||||
|
||||
if(gp.create_mouse()) {
|
||||
if(gp.create_mouse() || gp.create_touchscreen()) {
|
||||
log_flush();
|
||||
std::abort();
|
||||
}
|
||||
@@ -492,7 +581,7 @@ input_t input() {
|
||||
}
|
||||
|
||||
void freeInput(void *p) {
|
||||
auto *input = (input_raw_t*)p;
|
||||
auto *input = (input_raw_t *)p;
|
||||
delete input;
|
||||
}
|
||||
}
|
||||
} // namespace platf
|
||||
88
sunshine/platform/linux/misc.cpp
Normal file
88
sunshine/platform/linux/misc.cpp
Normal file
@@ -0,0 +1,88 @@
|
||||
#include "sunshine/platform/common.h"
|
||||
|
||||
#include <fstream>
|
||||
|
||||
#include <arpa/inet.h>
|
||||
#include <ifaddrs.h>
|
||||
#include <pwd.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include "sunshine/main.h"
|
||||
|
||||
using namespace std::literals;
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
namespace platf {
|
||||
using ifaddr_t = util::safe_ptr<ifaddrs, freeifaddrs>;
|
||||
|
||||
ifaddr_t get_ifaddrs() {
|
||||
ifaddrs *p { nullptr };
|
||||
|
||||
getifaddrs(&p);
|
||||
|
||||
return ifaddr_t { p };
|
||||
}
|
||||
|
||||
fs::path appdata() {
|
||||
const char *homedir;
|
||||
if((homedir = getenv("HOME")) == nullptr) {
|
||||
homedir = getpwuid(geteuid())->pw_dir;
|
||||
}
|
||||
|
||||
return fs::path { homedir } / ".config/sunshine"sv;
|
||||
}
|
||||
|
||||
std::string from_sockaddr(const sockaddr *const ip_addr) {
|
||||
char data[INET6_ADDRSTRLEN];
|
||||
|
||||
auto family = ip_addr->sa_family;
|
||||
if(family == AF_INET6) {
|
||||
inet_ntop(AF_INET6, &((sockaddr_in6 *)ip_addr)->sin6_addr, data,
|
||||
INET6_ADDRSTRLEN);
|
||||
}
|
||||
|
||||
if(family == AF_INET) {
|
||||
inet_ntop(AF_INET, &((sockaddr_in *)ip_addr)->sin_addr, data,
|
||||
INET_ADDRSTRLEN);
|
||||
}
|
||||
|
||||
return std::string { data };
|
||||
}
|
||||
|
||||
std::pair<std::uint16_t, std::string> from_sockaddr_ex(const sockaddr *const ip_addr) {
|
||||
char data[INET6_ADDRSTRLEN];
|
||||
|
||||
auto family = ip_addr->sa_family;
|
||||
std::uint16_t port;
|
||||
if(family == AF_INET6) {
|
||||
inet_ntop(AF_INET6, &((sockaddr_in6 *)ip_addr)->sin6_addr, data,
|
||||
INET6_ADDRSTRLEN);
|
||||
port = ((sockaddr_in6 *)ip_addr)->sin6_port;
|
||||
}
|
||||
|
||||
if(family == AF_INET) {
|
||||
inet_ntop(AF_INET, &((sockaddr_in *)ip_addr)->sin_addr, data,
|
||||
INET_ADDRSTRLEN);
|
||||
port = ((sockaddr_in *)ip_addr)->sin_port;
|
||||
}
|
||||
|
||||
return { port, std::string { data } };
|
||||
}
|
||||
|
||||
std::string get_mac_address(const std::string_view &address) {
|
||||
auto ifaddrs = get_ifaddrs();
|
||||
for(auto pos = ifaddrs.get(); pos != nullptr; pos = pos->ifa_next) {
|
||||
if(pos->ifa_addr && address == from_sockaddr(pos->ifa_addr)) {
|
||||
std::ifstream mac_file("/sys/class/net/"s + pos->ifa_name + "/address");
|
||||
if(mac_file.good()) {
|
||||
std::string mac_address;
|
||||
std::getline(mac_file, mac_address);
|
||||
return mac_address;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
BOOST_LOG(warning) << "Unable to find MAC address for "sv << address;
|
||||
return "00:00:00:00:00:00"s;
|
||||
}
|
||||
} // namespace platf
|
||||
1137
sunshine/platform/linux/vaapi.cpp
Normal file
1137
sunshine/platform/linux/vaapi.cpp
Normal file
File diff suppressed because it is too large
Load Diff
8
sunshine/platform/linux/vaapi.h
Normal file
8
sunshine/platform/linux/vaapi.h
Normal file
@@ -0,0 +1,8 @@
|
||||
#ifndef SUNSHINE_DISPLAY_H
|
||||
#define SUNSHINE_DISPLAY_H
|
||||
|
||||
#include "sunshine/platform/common.h"
|
||||
namespace platf::egl {
|
||||
std::shared_ptr<hwdevice_t> make_hwdevice(int width, int height);
|
||||
} // namespace platf::egl
|
||||
#endif
|
||||
@@ -1,291 +0,0 @@
|
||||
#include <thread>
|
||||
#include <sstream>
|
||||
#include <iomanip>
|
||||
|
||||
#include <Ws2tcpip.h>
|
||||
#include <Winsock2.h>
|
||||
#include <windows.h>
|
||||
#include <winuser.h>
|
||||
#include <iphlpapi.h>
|
||||
|
||||
#include <ViGEm/Client.h>
|
||||
|
||||
#include "sunshine/main.h"
|
||||
#include "common.h"
|
||||
|
||||
namespace platf {
|
||||
using namespace std::literals;
|
||||
|
||||
using adapteraddrs_t = util::c_ptr<IP_ADAPTER_ADDRESSES>;
|
||||
|
||||
class vigem_t {
|
||||
public:
|
||||
using client_t = util::safe_ptr<_VIGEM_CLIENT_T, vigem_free>;
|
||||
using target_t = util::safe_ptr<_VIGEM_TARGET_T, vigem_target_free>;
|
||||
|
||||
int init() {
|
||||
VIGEM_ERROR status;
|
||||
|
||||
client.reset(vigem_alloc());
|
||||
|
||||
status = vigem_connect(client.get());
|
||||
if(!VIGEM_SUCCESS(status)) {
|
||||
BOOST_LOG(warning) << "Couldn't setup connection to ViGEm for gamepad support ["sv << util::hex(status).to_string_view() << ']';
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
x360s.resize(MAX_GAMEPADS);
|
||||
for(auto &x360 : x360s) {
|
||||
x360.reset(vigem_target_x360_alloc());
|
||||
|
||||
status = vigem_target_add(client.get(), x360.get());
|
||||
if(!VIGEM_SUCCESS(status)) {
|
||||
BOOST_LOG(error) << "Couldn't add Gamepad to ViGEm connection ["sv << util::hex(status).to_string_view() << ']';
|
||||
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
~vigem_t() {
|
||||
if(client) {
|
||||
for(auto &x360 : x360s) {
|
||||
if(x360 && vigem_target_is_attached(x360.get())) {
|
||||
auto status = vigem_target_remove(client.get(), x360.get());
|
||||
if(!VIGEM_SUCCESS(status)) {
|
||||
BOOST_LOG(warning) << "Couldn't detach gamepad from ViGEm ["sv << util::hex(status).to_string_view() << ']';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
vigem_disconnect(client.get());
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<target_t> x360s;
|
||||
client_t client;
|
||||
};
|
||||
|
||||
std::string from_socket_address(const SOCKET_ADDRESS &socket_address) {
|
||||
char data[INET6_ADDRSTRLEN];
|
||||
|
||||
auto family = socket_address.lpSockaddr->sa_family;
|
||||
if(family == AF_INET6) {
|
||||
inet_ntop(AF_INET6, &((sockaddr_in6*)socket_address.lpSockaddr)->sin6_addr, data, INET6_ADDRSTRLEN);
|
||||
}
|
||||
|
||||
if(family == AF_INET) {
|
||||
inet_ntop(AF_INET, &((sockaddr_in*)socket_address.lpSockaddr)->sin_addr, data, INET_ADDRSTRLEN);
|
||||
}
|
||||
|
||||
return std::string { data };
|
||||
}
|
||||
|
||||
adapteraddrs_t get_adapteraddrs() {
|
||||
adapteraddrs_t info { nullptr };
|
||||
ULONG size = 0;
|
||||
|
||||
while(GetAdaptersAddresses(AF_UNSPEC, 0, nullptr, info.get(), &size) == ERROR_BUFFER_OVERFLOW) {
|
||||
info.reset((PIP_ADAPTER_ADDRESSES)malloc(size));
|
||||
}
|
||||
|
||||
return info;
|
||||
}
|
||||
|
||||
std::string get_mac_address(const std::string_view &address) {
|
||||
adapteraddrs_t info = get_adapteraddrs();
|
||||
for(auto adapter_pos = info.get(); adapter_pos != nullptr; adapter_pos = adapter_pos->Next) {
|
||||
for(auto addr_pos = adapter_pos->FirstUnicastAddress; addr_pos != nullptr; addr_pos = addr_pos->Next) {
|
||||
if(adapter_pos->PhysicalAddressLength != 0 && address == from_socket_address(addr_pos->Address)) {
|
||||
std::stringstream mac_addr;
|
||||
mac_addr << std::hex;
|
||||
for(int i = 0; i < adapter_pos->PhysicalAddressLength; i++) {
|
||||
if(i > 0) {
|
||||
mac_addr << ':';
|
||||
}
|
||||
mac_addr << std::setw(2) << std::setfill('0') << (int)adapter_pos->PhysicalAddress[i];
|
||||
}
|
||||
return mac_addr.str();
|
||||
}
|
||||
}
|
||||
}
|
||||
BOOST_LOG(warning) << "Unable to find MAC address for "sv << address;
|
||||
return "00:00:00:00:00:00"s;
|
||||
}
|
||||
|
||||
input_t input() {
|
||||
input_t result { new vigem_t {} };
|
||||
|
||||
auto vigem = (vigem_t*)result.get();
|
||||
if(vigem->init()) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
void move_mouse(input_t &input, int deltaX, int deltaY) {
|
||||
INPUT i {};
|
||||
|
||||
i.type = INPUT_MOUSE;
|
||||
auto &mi = i.mi;
|
||||
|
||||
mi.dwFlags = MOUSEEVENTF_MOVE;
|
||||
mi.dx = deltaX;
|
||||
mi.dy = deltaY;
|
||||
|
||||
auto send = SendInput(1, &i, sizeof(INPUT));
|
||||
if(send != 1) {
|
||||
BOOST_LOG(warning) << "Couldn't send mouse movement input"sv;
|
||||
}
|
||||
}
|
||||
|
||||
void button_mouse(input_t &input, int button, bool release) {
|
||||
constexpr SHORT KEY_STATE_DOWN = 0x8000;
|
||||
|
||||
INPUT i {};
|
||||
|
||||
i.type = INPUT_MOUSE;
|
||||
auto &mi = i.mi;
|
||||
|
||||
int mouse_button;
|
||||
if(button == 1) {
|
||||
mi.dwFlags = release ? MOUSEEVENTF_LEFTUP : MOUSEEVENTF_LEFTDOWN;
|
||||
mouse_button = VK_LBUTTON;
|
||||
}
|
||||
else if(button == 2) {
|
||||
mi.dwFlags = release ? MOUSEEVENTF_MIDDLEUP : MOUSEEVENTF_MIDDLEDOWN;
|
||||
mouse_button = VK_MBUTTON;
|
||||
}
|
||||
else if(button == 3) {
|
||||
mi.dwFlags = release ? MOUSEEVENTF_RIGHTUP : MOUSEEVENTF_RIGHTDOWN;
|
||||
mouse_button = VK_RBUTTON;
|
||||
}
|
||||
else if(button == 4) {
|
||||
mi.dwFlags = release ? MOUSEEVENTF_XUP : MOUSEEVENTF_XDOWN;
|
||||
mi.mouseData = XBUTTON1;
|
||||
mouse_button = VK_XBUTTON1;
|
||||
}
|
||||
else {
|
||||
mi.dwFlags = release ? MOUSEEVENTF_XUP : MOUSEEVENTF_XDOWN;
|
||||
mi.mouseData = XBUTTON2;
|
||||
mouse_button = VK_XBUTTON2;
|
||||
}
|
||||
|
||||
auto key_state = GetAsyncKeyState(mouse_button);
|
||||
bool key_state_down = (key_state & KEY_STATE_DOWN) != 0;
|
||||
if(key_state_down != release) {
|
||||
BOOST_LOG(warning) << "Button state of mouse_button ["sv << button << "] does not match the desired state"sv;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
auto send = SendInput(1, &i, sizeof(INPUT));
|
||||
if(send != 1) {
|
||||
BOOST_LOG(warning) << "Couldn't send mouse button input"sv;
|
||||
}
|
||||
}
|
||||
|
||||
void scroll(input_t &input, int distance) {
|
||||
INPUT i {};
|
||||
|
||||
i.type = INPUT_MOUSE;
|
||||
auto &mi = i.mi;
|
||||
|
||||
mi.dwFlags = MOUSEEVENTF_WHEEL;
|
||||
mi.mouseData = distance;
|
||||
|
||||
auto send = SendInput(1, &i, sizeof(INPUT));
|
||||
if(send != 1) {
|
||||
BOOST_LOG(warning) << "Couldn't send moue movement input"sv;
|
||||
}
|
||||
}
|
||||
|
||||
void keyboard(input_t &input, uint16_t modcode, bool release) {
|
||||
constexpr SHORT KEY_STATE_DOWN = 0x8000;
|
||||
|
||||
if(modcode == VK_RMENU) {
|
||||
modcode = VK_LWIN;
|
||||
}
|
||||
|
||||
auto key_state = GetAsyncKeyState(modcode);
|
||||
bool key_state_down = (key_state & KEY_STATE_DOWN) != 0;
|
||||
if(key_state_down != release) {
|
||||
BOOST_LOG(warning) << "Key state of vkey ["sv << util::hex(modcode).to_string_view() << "] does not match the desired state ["sv << (release ? "on]"sv : "off]"sv);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
INPUT i {};
|
||||
i.type = INPUT_KEYBOARD;
|
||||
auto &ki = i.ki;
|
||||
|
||||
// For some reason, MapVirtualKey(VK_LWIN, MAPVK_VK_TO_VSC) doesn't seem to work :/
|
||||
if(modcode != VK_LWIN && modcode != VK_RWIN && modcode != VK_PAUSE) {
|
||||
ki.wScan = MapVirtualKey(modcode, MAPVK_VK_TO_VSC);
|
||||
ki.dwFlags = KEYEVENTF_SCANCODE;
|
||||
}
|
||||
else {
|
||||
ki.wVk = modcode;
|
||||
}
|
||||
|
||||
// https://docs.microsoft.com/en-us/windows/win32/inputdev/about-keyboard-input#keystroke-message-flags
|
||||
switch(modcode) {
|
||||
case VK_RMENU:
|
||||
case VK_RCONTROL:
|
||||
case VK_INSERT:
|
||||
case VK_DELETE:
|
||||
case VK_HOME:
|
||||
case VK_END:
|
||||
case VK_PRIOR:
|
||||
case VK_NEXT:
|
||||
case VK_UP:
|
||||
case VK_DOWN:
|
||||
case VK_LEFT:
|
||||
case VK_RIGHT:
|
||||
case VK_DIVIDE:
|
||||
ki.dwFlags |= KEYEVENTF_EXTENDEDKEY;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
if(release) {
|
||||
ki.dwFlags |= KEYEVENTF_KEYUP;
|
||||
}
|
||||
|
||||
auto send = SendInput(1, &i, sizeof(INPUT));
|
||||
if(send != 1) {
|
||||
BOOST_LOG(warning) << "Couldn't send moue movement input"sv;
|
||||
}
|
||||
}
|
||||
|
||||
void gamepad(input_t &input, int nr, const gamepad_state_t &gamepad_state) {
|
||||
// If there is no gamepad support
|
||||
if(!input) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto vigem = (vigem_t*)input.get();
|
||||
|
||||
auto &xusb = *(PXUSB_REPORT)&gamepad_state;
|
||||
auto &x360 = vigem->x360s[nr];
|
||||
|
||||
auto status = vigem_target_x360_update(vigem->client.get(), x360.get(), xusb);
|
||||
if(!VIGEM_SUCCESS(status)) {
|
||||
BOOST_LOG(fatal) << "Couldn't send gamepad input to ViGEm ["sv << util::hex(status).to_string_view() << ']';
|
||||
|
||||
log_flush();
|
||||
std::abort();
|
||||
}
|
||||
}
|
||||
|
||||
void freeInput(void *p) {
|
||||
auto vigem = (vigem_t*)p;
|
||||
|
||||
delete vigem;
|
||||
}
|
||||
}
|
||||
164
sunshine/platform/windows/PolicyConfig.h
Normal file
164
sunshine/platform/windows/PolicyConfig.h
Normal file
@@ -0,0 +1,164 @@
|
||||
// ----------------------------------------------------------------------------
|
||||
// PolicyConfig.h
|
||||
// Undocumented COM-interface IPolicyConfig.
|
||||
// Use for set default audio render endpoint
|
||||
// @author EreTIk
|
||||
// http://eretik.omegahg.com/
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
|
||||
#pragma once
|
||||
|
||||
#ifdef __MINGW32__
|
||||
#undef DEFINE_GUID
|
||||
#ifdef __cplusplus
|
||||
#define DEFINE_GUID(name, l, w1, w2, b1, b2, b3, b4, b5, b6, b7, b8) EXTERN_C const GUID DECLSPEC_SELECTANY name = { l, w1, w2, { b1, b2, b3, b4, b5, b6, b7, b8 } }
|
||||
#else
|
||||
#define DEFINE_GUID(name, l, w1, w2, b1, b2, b3, b4, b5, b6, b7, b8) const GUID DECLSPEC_SELECTANY name = { l, w1, w2, { b1, b2, b3, b4, b5, b6, b7, b8 } }
|
||||
#endif
|
||||
|
||||
DEFINE_GUID(IID_IPolicyConfig, 0xf8679f50, 0x850a, 0x41cf, 0x9c, 0x72, 0x43, 0x0f, 0x29, 0x02, 0x90, 0xc8);
|
||||
DEFINE_GUID(CLSID_CPolicyConfigClient, 0x870af99c, 0x171d, 0x4f9e, 0xaf, 0x0d, 0xe6, 0x3d, 0xf4, 0x0c, 0x2b, 0xc9);
|
||||
|
||||
#endif
|
||||
|
||||
interface DECLSPEC_UUID("f8679f50-850a-41cf-9c72-430f290290c8") IPolicyConfig;
|
||||
class DECLSPEC_UUID("870af99c-171d-4f9e-af0d-e63df40c2bc9") CPolicyConfigClient;
|
||||
// ----------------------------------------------------------------------------
|
||||
// class CPolicyConfigClient
|
||||
// {870af99c-171d-4f9e-af0d-e63df40c2bc9}
|
||||
//
|
||||
// interface IPolicyConfig
|
||||
// {f8679f50-850a-41cf-9c72-430f290290c8}
|
||||
//
|
||||
// Query interface:
|
||||
// CComPtr<IPolicyConfig> PolicyConfig;
|
||||
// PolicyConfig.CoCreateInstance(__uuidof(CPolicyConfigClient));
|
||||
//
|
||||
// @compatible: Windows 7 and Later
|
||||
// ----------------------------------------------------------------------------
|
||||
interface IPolicyConfig : public IUnknown {
|
||||
public:
|
||||
virtual HRESULT GetMixFormat(
|
||||
PCWSTR,
|
||||
WAVEFORMATEX **);
|
||||
|
||||
virtual HRESULT STDMETHODCALLTYPE GetDeviceFormat(
|
||||
PCWSTR,
|
||||
INT,
|
||||
WAVEFORMATEX **);
|
||||
|
||||
virtual HRESULT STDMETHODCALLTYPE ResetDeviceFormat(
|
||||
PCWSTR);
|
||||
|
||||
virtual HRESULT STDMETHODCALLTYPE SetDeviceFormat(
|
||||
PCWSTR,
|
||||
WAVEFORMATEX *,
|
||||
WAVEFORMATEX *);
|
||||
|
||||
virtual HRESULT STDMETHODCALLTYPE GetProcessingPeriod(
|
||||
PCWSTR,
|
||||
INT,
|
||||
PINT64,
|
||||
PINT64);
|
||||
|
||||
virtual HRESULT STDMETHODCALLTYPE SetProcessingPeriod(
|
||||
PCWSTR,
|
||||
PINT64);
|
||||
|
||||
virtual HRESULT STDMETHODCALLTYPE GetShareMode(
|
||||
PCWSTR,
|
||||
struct DeviceShareMode *);
|
||||
|
||||
virtual HRESULT STDMETHODCALLTYPE SetShareMode(
|
||||
PCWSTR,
|
||||
struct DeviceShareMode *);
|
||||
|
||||
virtual HRESULT STDMETHODCALLTYPE GetPropertyValue(
|
||||
PCWSTR,
|
||||
const PROPERTYKEY &,
|
||||
PROPVARIANT *);
|
||||
|
||||
virtual HRESULT STDMETHODCALLTYPE SetPropertyValue(
|
||||
PCWSTR,
|
||||
const PROPERTYKEY &,
|
||||
PROPVARIANT *);
|
||||
|
||||
virtual HRESULT STDMETHODCALLTYPE SetDefaultEndpoint(
|
||||
PCWSTR wszDeviceId,
|
||||
ERole eRole);
|
||||
|
||||
virtual HRESULT STDMETHODCALLTYPE SetEndpointVisibility(
|
||||
PCWSTR,
|
||||
INT);
|
||||
};
|
||||
|
||||
interface DECLSPEC_UUID("568b9108-44bf-40b4-9006-86afe5b5a620") IPolicyConfigVista;
|
||||
class DECLSPEC_UUID("294935CE-F637-4E7C-A41B-AB255460B862") CPolicyConfigVistaClient;
|
||||
// ----------------------------------------------------------------------------
|
||||
// class CPolicyConfigVistaClient
|
||||
// {294935CE-F637-4E7C-A41B-AB255460B862}
|
||||
//
|
||||
// interface IPolicyConfigVista
|
||||
// {568b9108-44bf-40b4-9006-86afe5b5a620}
|
||||
//
|
||||
// Query interface:
|
||||
// CComPtr<IPolicyConfigVista> PolicyConfig;
|
||||
// PolicyConfig.CoCreateInstance(__uuidof(CPolicyConfigVistaClient));
|
||||
//
|
||||
// @compatible: Windows Vista and Later
|
||||
// ----------------------------------------------------------------------------
|
||||
interface IPolicyConfigVista : public IUnknown {
|
||||
public:
|
||||
virtual HRESULT GetMixFormat(
|
||||
PCWSTR,
|
||||
WAVEFORMATEX **); // not available on Windows 7, use method from IPolicyConfig
|
||||
|
||||
virtual HRESULT STDMETHODCALLTYPE GetDeviceFormat(
|
||||
PCWSTR,
|
||||
INT,
|
||||
WAVEFORMATEX **);
|
||||
|
||||
virtual HRESULT STDMETHODCALLTYPE SetDeviceFormat(
|
||||
PCWSTR,
|
||||
WAVEFORMATEX *,
|
||||
WAVEFORMATEX *);
|
||||
|
||||
virtual HRESULT STDMETHODCALLTYPE GetProcessingPeriod(
|
||||
PCWSTR,
|
||||
INT,
|
||||
PINT64,
|
||||
PINT64); // not available on Windows 7, use method from IPolicyConfig
|
||||
|
||||
virtual HRESULT STDMETHODCALLTYPE SetProcessingPeriod(
|
||||
PCWSTR,
|
||||
PINT64); // not available on Windows 7, use method from IPolicyConfig
|
||||
|
||||
virtual HRESULT STDMETHODCALLTYPE GetShareMode(
|
||||
PCWSTR,
|
||||
struct DeviceShareMode *); // not available on Windows 7, use method from IPolicyConfig
|
||||
|
||||
virtual HRESULT STDMETHODCALLTYPE SetShareMode(
|
||||
PCWSTR,
|
||||
struct DeviceShareMode *); // not available on Windows 7, use method from IPolicyConfig
|
||||
|
||||
virtual HRESULT STDMETHODCALLTYPE GetPropertyValue(
|
||||
PCWSTR,
|
||||
const PROPERTYKEY &,
|
||||
PROPVARIANT *);
|
||||
|
||||
virtual HRESULT STDMETHODCALLTYPE SetPropertyValue(
|
||||
PCWSTR,
|
||||
const PROPERTYKEY &,
|
||||
PROPVARIANT *);
|
||||
|
||||
virtual HRESULT STDMETHODCALLTYPE SetDefaultEndpoint(
|
||||
PCWSTR wszDeviceId,
|
||||
ERole eRole);
|
||||
|
||||
virtual HRESULT STDMETHODCALLTYPE SetEndpointVisibility(
|
||||
PCWSTR,
|
||||
INT); // not available on Windows 7, use method from IPolicyConfig
|
||||
};
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
988
sunshine/platform/windows/audio.cpp
Normal file
988
sunshine/platform/windows/audio.cpp
Normal file
@@ -0,0 +1,988 @@
|
||||
//
|
||||
// Created by loki on 1/12/20.
|
||||
//
|
||||
|
||||
#include <audioclient.h>
|
||||
#include <mmdeviceapi.h>
|
||||
#include <roapi.h>
|
||||
|
||||
#include <codecvt>
|
||||
|
||||
#include <synchapi.h>
|
||||
|
||||
#define INITGUID
|
||||
#include <propkeydef.h>
|
||||
#undef INITGUID
|
||||
|
||||
#include "sunshine/config.h"
|
||||
#include "sunshine/main.h"
|
||||
#include "sunshine/platform/common.h"
|
||||
|
||||
// Must be the last included file
|
||||
// clang-format off
|
||||
#include "PolicyConfig.h"
|
||||
// clang-format on
|
||||
|
||||
DEFINE_PROPERTYKEY(PKEY_Device_DeviceDesc, 0xa45c254e, 0xdf1c, 0x4efd, 0x80, 0x20, 0x67, 0xd1, 0x46, 0xa8, 0x50, 0xe0, 2); // DEVPROP_TYPE_STRING
|
||||
DEFINE_PROPERTYKEY(PKEY_Device_FriendlyName, 0xa45c254e, 0xdf1c, 0x4efd, 0x80, 0x20, 0x67, 0xd1, 0x46, 0xa8, 0x50, 0xe0, 14); // DEVPROP_TYPE_STRING
|
||||
DEFINE_PROPERTYKEY(PKEY_DeviceInterface_FriendlyName, 0x026e516e, 0xb814, 0x414b, 0x83, 0xcd, 0x85, 0x6d, 0x6f, 0xef, 0x48, 0x22, 2);
|
||||
|
||||
const CLSID CLSID_MMDeviceEnumerator = __uuidof(MMDeviceEnumerator);
|
||||
const IID IID_IMMDeviceEnumerator = __uuidof(IMMDeviceEnumerator);
|
||||
const IID IID_IAudioClient = __uuidof(IAudioClient);
|
||||
const IID IID_IAudioCaptureClient = __uuidof(IAudioCaptureClient);
|
||||
|
||||
using namespace std::literals;
|
||||
namespace platf::audio {
|
||||
constexpr auto SAMPLE_RATE = 48000;
|
||||
|
||||
template<class T>
|
||||
void Release(T *p) {
|
||||
p->Release();
|
||||
}
|
||||
|
||||
template<class T>
|
||||
void co_task_free(T *p) {
|
||||
CoTaskMemFree((LPVOID)p);
|
||||
}
|
||||
|
||||
using device_enum_t = util::safe_ptr<IMMDeviceEnumerator, Release<IMMDeviceEnumerator>>;
|
||||
using device_t = util::safe_ptr<IMMDevice, Release<IMMDevice>>;
|
||||
using collection_t = util::safe_ptr<IMMDeviceCollection, Release<IMMDeviceCollection>>;
|
||||
using audio_client_t = util::safe_ptr<IAudioClient, Release<IAudioClient>>;
|
||||
using audio_capture_t = util::safe_ptr<IAudioCaptureClient, Release<IAudioCaptureClient>>;
|
||||
using wave_format_t = util::safe_ptr<WAVEFORMATEX, co_task_free<WAVEFORMATEX>>;
|
||||
using wstring_t = util::safe_ptr<WCHAR, co_task_free<WCHAR>>;
|
||||
using handle_t = util::safe_ptr_v2<void, BOOL, CloseHandle>;
|
||||
using policy_t = util::safe_ptr<IPolicyConfig, Release<IPolicyConfig>>;
|
||||
using prop_t = util::safe_ptr<IPropertyStore, Release<IPropertyStore>>;
|
||||
|
||||
class co_init_t : public deinit_t {
|
||||
public:
|
||||
co_init_t() {
|
||||
CoInitializeEx(nullptr, COINIT_MULTITHREADED | COINIT_SPEED_OVER_MEMORY);
|
||||
}
|
||||
|
||||
~co_init_t() override {
|
||||
CoUninitialize();
|
||||
}
|
||||
};
|
||||
|
||||
class prop_var_t {
|
||||
public:
|
||||
prop_var_t() {
|
||||
PropVariantInit(&prop);
|
||||
}
|
||||
|
||||
~prop_var_t() {
|
||||
PropVariantClear(&prop);
|
||||
}
|
||||
|
||||
PROPVARIANT prop;
|
||||
};
|
||||
|
||||
class audio_pipe_t {
|
||||
public:
|
||||
static constexpr auto stereo = 2;
|
||||
static constexpr auto channels51 = 6;
|
||||
static constexpr auto channels71 = 8;
|
||||
|
||||
using samples_t = std::vector<std::int16_t>;
|
||||
using buf_t = util::buffer_t<std::int16_t>;
|
||||
|
||||
virtual void to_stereo(samples_t &out, const buf_t &in) = 0;
|
||||
virtual void to_51(samples_t &out, const buf_t &in) = 0;
|
||||
virtual void to_71(samples_t &out, const buf_t &in) = 0;
|
||||
};
|
||||
|
||||
class mono_t : public audio_pipe_t {
|
||||
public:
|
||||
void to_stereo(samples_t &out, const buf_t &in) override {
|
||||
auto sample_in_pos = std::begin(in);
|
||||
auto sample_end = std::begin(out) + out.size();
|
||||
|
||||
for(auto sample_out_p = std::begin(out); sample_out_p != sample_end;) {
|
||||
*sample_out_p++ = *sample_in_pos * 7 / 10;
|
||||
*sample_out_p++ = *sample_in_pos++ * 7 / 10;
|
||||
}
|
||||
}
|
||||
|
||||
void to_51(samples_t &out, const buf_t &in) override {
|
||||
using namespace speaker;
|
||||
|
||||
auto sample_in_pos = std::begin(in);
|
||||
auto sample_end = std::begin(out) + out.size();
|
||||
|
||||
for(auto sample_out_p = std::begin(out); sample_out_p != sample_end; sample_out_p += channels51) {
|
||||
int left = *sample_in_pos++;
|
||||
|
||||
auto fl = (left * 7 / 10);
|
||||
|
||||
sample_out_p[FRONT_LEFT] = fl;
|
||||
sample_out_p[FRONT_RIGHT] = fl;
|
||||
sample_out_p[FRONT_CENTER] = fl * 6;
|
||||
sample_out_p[LOW_FREQUENCY] = fl / 10;
|
||||
sample_out_p[BACK_LEFT] = left * 4 / 10;
|
||||
sample_out_p[BACK_RIGHT] = left * 4 / 10;
|
||||
}
|
||||
}
|
||||
|
||||
void to_71(samples_t &out, const buf_t &in) override {
|
||||
using namespace speaker;
|
||||
|
||||
auto sample_in_pos = std::begin(in);
|
||||
auto sample_end = std::begin(out) + out.size();
|
||||
|
||||
for(auto sample_out_p = std::begin(out); sample_out_p != sample_end; sample_out_p += channels71) {
|
||||
int left = *sample_in_pos++;
|
||||
|
||||
auto fl = (left * 7 / 10);
|
||||
|
||||
sample_out_p[FRONT_LEFT] = fl;
|
||||
sample_out_p[FRONT_RIGHT] = fl;
|
||||
sample_out_p[FRONT_CENTER] = fl * 6;
|
||||
sample_out_p[LOW_FREQUENCY] = fl / 10;
|
||||
sample_out_p[BACK_LEFT] = left * 4 / 10;
|
||||
sample_out_p[BACK_RIGHT] = left * 4 / 10;
|
||||
sample_out_p[SIDE_LEFT] = left * 5 / 10;
|
||||
sample_out_p[SIDE_RIGHT] = left * 5 / 10;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
class stereo_t : public audio_pipe_t {
|
||||
public:
|
||||
void to_stereo(samples_t &out, const buf_t &in) override {
|
||||
std::copy_n(std::begin(in), out.size(), std::begin(out));
|
||||
}
|
||||
|
||||
void to_51(samples_t &out, const buf_t &in) override {
|
||||
using namespace speaker;
|
||||
|
||||
auto sample_in_pos = std::begin(in);
|
||||
auto sample_end = std::begin(out) + out.size();
|
||||
|
||||
for(auto sample_out_p = std::begin(out); sample_out_p != sample_end; sample_out_p += channels51) {
|
||||
int left = sample_in_pos[speaker::FRONT_LEFT];
|
||||
int right = sample_in_pos[speaker::FRONT_RIGHT];
|
||||
|
||||
sample_in_pos += 2;
|
||||
|
||||
auto fl = (left * 7 / 10);
|
||||
auto fr = (right * 7 / 10);
|
||||
|
||||
auto mix = (fl + fr) / 2;
|
||||
|
||||
sample_out_p[FRONT_LEFT] = fl;
|
||||
sample_out_p[FRONT_RIGHT] = fr;
|
||||
sample_out_p[FRONT_CENTER] = mix;
|
||||
sample_out_p[LOW_FREQUENCY] = mix / 2;
|
||||
sample_out_p[BACK_LEFT] = left * 4 / 10;
|
||||
sample_out_p[BACK_RIGHT] = right * 4 / 10;
|
||||
}
|
||||
}
|
||||
|
||||
void to_71(samples_t &out, const buf_t &in) override {
|
||||
using namespace speaker;
|
||||
|
||||
auto sample_in_pos = std::begin(in);
|
||||
auto sample_end = std::begin(out) + out.size();
|
||||
|
||||
for(auto sample_out_p = std::begin(out); sample_out_p != sample_end; sample_out_p += channels71) {
|
||||
int left = sample_in_pos[speaker::FRONT_LEFT];
|
||||
int right = sample_in_pos[speaker::FRONT_RIGHT];
|
||||
|
||||
sample_in_pos += 2;
|
||||
|
||||
auto fl = (left * 7 / 10);
|
||||
auto fr = (right * 7 / 10);
|
||||
|
||||
auto mix = (fl + fr) / 2;
|
||||
|
||||
sample_out_p[FRONT_LEFT] = fl;
|
||||
sample_out_p[FRONT_RIGHT] = fr;
|
||||
sample_out_p[FRONT_CENTER] = mix;
|
||||
sample_out_p[LOW_FREQUENCY] = mix / 2;
|
||||
sample_out_p[BACK_LEFT] = left * 4 / 10;
|
||||
sample_out_p[BACK_RIGHT] = right * 4 / 10;
|
||||
sample_out_p[SIDE_LEFT] = left * 5 / 10;
|
||||
sample_out_p[SIDE_RIGHT] = right * 5 / 10;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
class surr51_t : public audio_pipe_t {
|
||||
public:
|
||||
void to_stereo(samples_t &out, const buf_t &in) {
|
||||
using namespace speaker;
|
||||
|
||||
auto sample_in_pos = std::begin(in);
|
||||
auto sample_end = std::begin(out) + out.size();
|
||||
|
||||
for(auto sample_out_p = std::begin(out); sample_out_p != sample_end; sample_out_p += stereo) {
|
||||
int left {}, right {};
|
||||
|
||||
left += sample_in_pos[FRONT_LEFT];
|
||||
left += sample_in_pos[FRONT_CENTER] * 9 / 10;
|
||||
left += sample_in_pos[LOW_FREQUENCY] * 3 / 10;
|
||||
left += sample_in_pos[BACK_LEFT] * 7 / 10;
|
||||
left += sample_in_pos[BACK_RIGHT] * 3 / 10;
|
||||
|
||||
right += sample_in_pos[FRONT_RIGHT];
|
||||
right += sample_in_pos[FRONT_CENTER] * 9 / 10;
|
||||
right += sample_in_pos[LOW_FREQUENCY] * 3 / 10;
|
||||
right += sample_in_pos[BACK_LEFT] * 3 / 10;
|
||||
right += sample_in_pos[BACK_RIGHT] * 7 / 10;
|
||||
|
||||
sample_out_p[0] = left;
|
||||
sample_out_p[1] = right;
|
||||
|
||||
sample_in_pos += channels51;
|
||||
}
|
||||
}
|
||||
|
||||
void to_51(samples_t &out, const buf_t &in) override {
|
||||
std::copy_n(std::begin(in), out.size(), std::begin(out));
|
||||
}
|
||||
|
||||
void to_71(samples_t &out, const buf_t &in) override {
|
||||
using namespace speaker;
|
||||
|
||||
auto sample_in_pos = std::begin(in);
|
||||
auto sample_end = std::begin(out) + out.size();
|
||||
|
||||
for(auto sample_out_p = std::begin(out); sample_out_p != sample_end; sample_out_p += channels71) {
|
||||
int fl = sample_in_pos[FRONT_LEFT];
|
||||
int fr = sample_in_pos[FRONT_RIGHT];
|
||||
int bl = sample_in_pos[BACK_LEFT];
|
||||
int br = sample_in_pos[BACK_RIGHT];
|
||||
|
||||
auto mix_l = (fl + bl) / 2;
|
||||
auto mix_r = (bl + br) / 2;
|
||||
|
||||
sample_out_p[FRONT_LEFT] = fl;
|
||||
sample_out_p[FRONT_RIGHT] = fr;
|
||||
sample_out_p[FRONT_CENTER] = sample_in_pos[FRONT_CENTER];
|
||||
sample_out_p[LOW_FREQUENCY] = sample_in_pos[LOW_FREQUENCY];
|
||||
sample_out_p[BACK_LEFT] = bl;
|
||||
sample_out_p[BACK_RIGHT] = br;
|
||||
sample_out_p[SIDE_LEFT] = mix_l;
|
||||
sample_out_p[SIDE_RIGHT] = mix_r;
|
||||
|
||||
sample_in_pos += channels51;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
class surr71_t : public audio_pipe_t {
|
||||
public:
|
||||
void to_stereo(samples_t &out, const buf_t &in) {
|
||||
using namespace speaker;
|
||||
|
||||
auto sample_in_pos = std::begin(in);
|
||||
auto sample_end = std::begin(out) + out.size();
|
||||
|
||||
for(auto sample_out_p = std::begin(out); sample_out_p != sample_end; sample_out_p += stereo) {
|
||||
int left {}, right {};
|
||||
|
||||
left += sample_in_pos[FRONT_LEFT];
|
||||
left += sample_in_pos[FRONT_CENTER] * 9 / 10;
|
||||
left += sample_in_pos[LOW_FREQUENCY] * 3 / 10;
|
||||
left += sample_in_pos[BACK_LEFT] * 7 / 10;
|
||||
left += sample_in_pos[BACK_RIGHT] * 3 / 10;
|
||||
left += sample_in_pos[SIDE_LEFT];
|
||||
|
||||
right += sample_in_pos[FRONT_RIGHT];
|
||||
right += sample_in_pos[FRONT_CENTER] * 9 / 10;
|
||||
right += sample_in_pos[LOW_FREQUENCY] * 3 / 10;
|
||||
right += sample_in_pos[BACK_LEFT] * 3 / 10;
|
||||
right += sample_in_pos[BACK_RIGHT] * 7 / 10;
|
||||
right += sample_in_pos[SIDE_RIGHT];
|
||||
|
||||
sample_out_p[0] = left;
|
||||
sample_out_p[1] = right;
|
||||
|
||||
sample_in_pos += channels71;
|
||||
}
|
||||
}
|
||||
|
||||
void to_51(samples_t &out, const buf_t &in) override {
|
||||
using namespace speaker;
|
||||
|
||||
auto sample_in_pos = std::begin(in);
|
||||
auto sample_end = std::begin(out) + out.size();
|
||||
|
||||
for(auto sample_out_p = std::begin(out); sample_out_p != sample_end; sample_out_p += channels51) {
|
||||
auto sl = (int)sample_out_p[SIDE_LEFT] * 3 / 10;
|
||||
auto sr = (int)sample_out_p[SIDE_RIGHT] * 3 / 10;
|
||||
|
||||
sample_out_p[FRONT_LEFT] = sample_in_pos[FRONT_LEFT] + sl;
|
||||
sample_out_p[FRONT_RIGHT] = sample_in_pos[FRONT_RIGHT] + sr;
|
||||
sample_out_p[FRONT_CENTER] = sample_in_pos[FRONT_CENTER];
|
||||
sample_out_p[LOW_FREQUENCY] = sample_in_pos[LOW_FREQUENCY];
|
||||
sample_out_p[BACK_LEFT] = sample_in_pos[BACK_LEFT] + sl;
|
||||
sample_out_p[BACK_RIGHT] = sample_in_pos[BACK_RIGHT] + sr;
|
||||
|
||||
sample_in_pos += channels71;
|
||||
}
|
||||
}
|
||||
|
||||
void to_71(samples_t &out, const buf_t &in) override {
|
||||
std::copy_n(std::begin(in), out.size(), std::begin(out));
|
||||
}
|
||||
};
|
||||
|
||||
static std::wstring_convert<std::codecvt_utf8_utf16<wchar_t>, wchar_t> converter;
|
||||
struct format_t {
|
||||
enum type_e : int {
|
||||
none,
|
||||
mono,
|
||||
stereo,
|
||||
surr51,
|
||||
surr71,
|
||||
} type;
|
||||
|
||||
std::string_view name;
|
||||
int channels;
|
||||
int channel_mask;
|
||||
} formats[] {
|
||||
{
|
||||
format_t::mono,
|
||||
"Mono"sv,
|
||||
1,
|
||||
SPEAKER_FRONT_CENTER,
|
||||
},
|
||||
{
|
||||
format_t::stereo,
|
||||
"Stereo"sv,
|
||||
2,
|
||||
SPEAKER_FRONT_LEFT | SPEAKER_FRONT_RIGHT,
|
||||
},
|
||||
{
|
||||
format_t::surr51,
|
||||
"Surround 5.1"sv,
|
||||
6,
|
||||
SPEAKER_FRONT_LEFT |
|
||||
SPEAKER_FRONT_RIGHT |
|
||||
SPEAKER_FRONT_CENTER |
|
||||
SPEAKER_LOW_FREQUENCY |
|
||||
SPEAKER_BACK_LEFT |
|
||||
SPEAKER_BACK_RIGHT,
|
||||
},
|
||||
{
|
||||
format_t::surr71,
|
||||
"Surround 7.1"sv,
|
||||
8,
|
||||
SPEAKER_FRONT_LEFT |
|
||||
SPEAKER_FRONT_RIGHT |
|
||||
SPEAKER_FRONT_CENTER |
|
||||
SPEAKER_LOW_FREQUENCY |
|
||||
SPEAKER_BACK_LEFT |
|
||||
SPEAKER_BACK_RIGHT |
|
||||
SPEAKER_SIDE_LEFT |
|
||||
SPEAKER_SIDE_RIGHT,
|
||||
},
|
||||
};
|
||||
|
||||
static format_t surround_51_side_speakers {
|
||||
format_t::surr51,
|
||||
"Surround 5.1"sv,
|
||||
6,
|
||||
SPEAKER_FRONT_LEFT |
|
||||
SPEAKER_FRONT_RIGHT |
|
||||
SPEAKER_FRONT_CENTER |
|
||||
SPEAKER_LOW_FREQUENCY |
|
||||
SPEAKER_SIDE_LEFT |
|
||||
SPEAKER_SIDE_RIGHT,
|
||||
};
|
||||
|
||||
void set_wave_format(audio::wave_format_t &wave_format, const format_t &format) {
|
||||
wave_format->nChannels = format.channels;
|
||||
wave_format->nBlockAlign = wave_format->nChannels * wave_format->wBitsPerSample / 8;
|
||||
wave_format->nAvgBytesPerSec = wave_format->nSamplesPerSec * wave_format->nBlockAlign;
|
||||
|
||||
if(wave_format->wFormatTag == WAVE_FORMAT_EXTENSIBLE) {
|
||||
((PWAVEFORMATEXTENSIBLE)wave_format.get())->dwChannelMask = format.channel_mask;
|
||||
}
|
||||
}
|
||||
|
||||
int init_wave_format(audio::wave_format_t &wave_format, DWORD sample_rate) {
|
||||
wave_format->wBitsPerSample = 16;
|
||||
wave_format->nSamplesPerSec = sample_rate;
|
||||
switch(wave_format->wFormatTag) {
|
||||
case WAVE_FORMAT_PCM:
|
||||
break;
|
||||
case WAVE_FORMAT_IEEE_FLOAT:
|
||||
break;
|
||||
case WAVE_FORMAT_EXTENSIBLE: {
|
||||
auto wave_ex = (PWAVEFORMATEXTENSIBLE)wave_format.get();
|
||||
if(IsEqualGUID(KSDATAFORMAT_SUBTYPE_IEEE_FLOAT, wave_ex->SubFormat)) {
|
||||
wave_ex->Samples.wValidBitsPerSample = 16;
|
||||
wave_ex->SubFormat = KSDATAFORMAT_SUBTYPE_PCM;
|
||||
break;
|
||||
}
|
||||
|
||||
BOOST_LOG(error) << "Unsupported Sub Format for WAVE_FORMAT_EXTENSIBLE: [0x"sv << util::hex(wave_ex->SubFormat).to_string_view() << ']';
|
||||
}
|
||||
default:
|
||||
BOOST_LOG(error) << "Unsupported Wave Format: [0x"sv << util::hex(wave_format->wFormatTag).to_string_view() << ']';
|
||||
return -1;
|
||||
};
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
audio_client_t make_audio_client(device_t &device, const format_t &format, int sample_rate) {
|
||||
audio_client_t audio_client;
|
||||
auto status = device->Activate(
|
||||
IID_IAudioClient,
|
||||
CLSCTX_ALL,
|
||||
nullptr,
|
||||
(void **)&audio_client);
|
||||
|
||||
if(FAILED(status)) {
|
||||
BOOST_LOG(error) << "Couldn't activate Device: [0x"sv << util::hex(status).to_string_view() << ']';
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
wave_format_t wave_format;
|
||||
status = audio_client->GetMixFormat(&wave_format);
|
||||
if(FAILED(status)) {
|
||||
BOOST_LOG(error) << "Couldn't acquire Wave Format [0x"sv << util::hex(status).to_string_view() << ']';
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
if(init_wave_format(wave_format, sample_rate)) {
|
||||
return nullptr;
|
||||
}
|
||||
set_wave_format(wave_format, format);
|
||||
|
||||
status = audio_client->Initialize(
|
||||
AUDCLNT_SHAREMODE_SHARED,
|
||||
AUDCLNT_STREAMFLAGS_LOOPBACK | AUDCLNT_STREAMFLAGS_EVENTCALLBACK,
|
||||
0, 0,
|
||||
wave_format.get(),
|
||||
nullptr);
|
||||
|
||||
if(status) {
|
||||
BOOST_LOG(debug) << "Couldn't initialize audio client for ["sv << format.name << "]: [0x"sv << util::hex(status).to_string_view() << ']';
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
return audio_client;
|
||||
}
|
||||
|
||||
const wchar_t *no_null(const wchar_t *str) {
|
||||
return str ? str : L"Unknown";
|
||||
}
|
||||
|
||||
format_t::type_e validate_device(device_t &device, int sample_rate) {
|
||||
for(const auto &format : formats) {
|
||||
// Ensure WaveFromat is compatible
|
||||
auto audio_client = make_audio_client(device, format, sample_rate);
|
||||
|
||||
BOOST_LOG(debug) << format.name << ": "sv << (!audio_client ? "unsupported"sv : "supported"sv);
|
||||
|
||||
if(audio_client) {
|
||||
return format.type;
|
||||
}
|
||||
}
|
||||
|
||||
return format_t::none;
|
||||
}
|
||||
|
||||
device_t default_device(device_enum_t &device_enum) {
|
||||
device_t device;
|
||||
HRESULT status;
|
||||
status = device_enum->GetDefaultAudioEndpoint(
|
||||
eRender,
|
||||
eConsole,
|
||||
&device);
|
||||
|
||||
|
||||
if(FAILED(status)) {
|
||||
BOOST_LOG(error) << "Couldn't create audio Device [0x"sv << util::hex(status).to_string_view() << ']';
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
return device;
|
||||
}
|
||||
|
||||
class mic_wasapi_t : public mic_t {
|
||||
public:
|
||||
capture_e sample(std::vector<std::int16_t> &sample_out) override {
|
||||
auto sample_size = sample_out.size() / channels_out * channels_in;
|
||||
while(sample_buf_pos - std::begin(sample_buf) < sample_size) {
|
||||
//FIXME: Use IAudioClient3 instead of IAudioClient, that would allows for adjusting the latency of the audio samples
|
||||
auto capture_result = _fill_buffer();
|
||||
|
||||
if(capture_result != capture_e::ok) {
|
||||
return capture_result;
|
||||
}
|
||||
}
|
||||
|
||||
switch(channels_out) {
|
||||
case 2:
|
||||
pipe->to_stereo(sample_out, sample_buf);
|
||||
break;
|
||||
case 6:
|
||||
pipe->to_51(sample_out, sample_buf);
|
||||
break;
|
||||
case 8:
|
||||
pipe->to_71(sample_out, sample_buf);
|
||||
break;
|
||||
default:
|
||||
BOOST_LOG(error) << "converting to ["sv << channels_out << "] channels is not supported"sv;
|
||||
return capture_e::error;
|
||||
}
|
||||
|
||||
// The excess samples should be in front of the queue
|
||||
std::move(&sample_buf[sample_size], sample_buf_pos, std::begin(sample_buf));
|
||||
sample_buf_pos -= sample_size;
|
||||
|
||||
return capture_e::ok;
|
||||
}
|
||||
|
||||
|
||||
int init(std::uint32_t sample_rate, std::uint32_t frame_size, std::uint32_t channels_out) {
|
||||
audio_event.reset(CreateEventA(nullptr, FALSE, FALSE, nullptr));
|
||||
if(!audio_event) {
|
||||
BOOST_LOG(error) << "Couldn't create Event handle"sv;
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
HRESULT status;
|
||||
|
||||
status = CoCreateInstance(
|
||||
CLSID_MMDeviceEnumerator,
|
||||
nullptr,
|
||||
CLSCTX_ALL,
|
||||
IID_IMMDeviceEnumerator,
|
||||
(void **)&device_enum);
|
||||
|
||||
if(FAILED(status)) {
|
||||
BOOST_LOG(error) << "Couldn't create Device Enumerator [0x"sv << util::hex(status).to_string_view() << ']';
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
auto device = default_device(device_enum);
|
||||
if(!device) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
for(auto &format : formats) {
|
||||
BOOST_LOG(debug) << "Trying audio format ["sv << format.name << ']';
|
||||
audio_client = make_audio_client(device, format, sample_rate);
|
||||
|
||||
if(audio_client) {
|
||||
BOOST_LOG(debug) << "Found audio format ["sv << format.name << ']';
|
||||
channels_in = format.channels;
|
||||
this->channels_out = channels_out;
|
||||
|
||||
switch(channels_in) {
|
||||
case 1:
|
||||
pipe = std::make_unique<mono_t>();
|
||||
break;
|
||||
case 2:
|
||||
pipe = std::make_unique<stereo_t>();
|
||||
break;
|
||||
case 6:
|
||||
pipe = std::make_unique<surr51_t>();
|
||||
break;
|
||||
case 8:
|
||||
pipe = std::make_unique<surr71_t>();
|
||||
break;
|
||||
default:
|
||||
BOOST_LOG(error) << "converting from ["sv << channels_in << "] channels is not supported"sv;
|
||||
return -1;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if(!audio_client) {
|
||||
BOOST_LOG(error) << "Couldn't find supported format for audio"sv;
|
||||
return -1;
|
||||
}
|
||||
|
||||
REFERENCE_TIME default_latency;
|
||||
audio_client->GetDevicePeriod(&default_latency, nullptr);
|
||||
default_latency_ms = default_latency / 1000;
|
||||
|
||||
std::uint32_t frames;
|
||||
status = audio_client->GetBufferSize(&frames);
|
||||
if(FAILED(status)) {
|
||||
BOOST_LOG(error) << "Couldn't acquire the number of audio frames [0x"sv << util::hex(status).to_string_view() << ']';
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
// *2 --> needs to fit double
|
||||
sample_buf = util::buffer_t<std::int16_t> { std::max(frames, frame_size) * 2 * channels_in };
|
||||
sample_buf_pos = std::begin(sample_buf);
|
||||
|
||||
status = audio_client->GetService(IID_IAudioCaptureClient, (void **)&audio_capture);
|
||||
if(FAILED(status)) {
|
||||
BOOST_LOG(error) << "Couldn't initialize audio capture client [0x"sv << util::hex(status).to_string_view() << ']';
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
status = audio_client->SetEventHandle(audio_event.get());
|
||||
if(FAILED(status)) {
|
||||
BOOST_LOG(error) << "Couldn't set event handle [0x"sv << util::hex(status).to_string_view() << ']';
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
status = audio_client->Start();
|
||||
if(FAILED(status)) {
|
||||
BOOST_LOG(error) << "Couldn't start recording [0x"sv << util::hex(status).to_string_view() << ']';
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
~mic_wasapi_t() override {
|
||||
if(audio_client) {
|
||||
audio_client->Stop();
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
capture_e _fill_buffer() {
|
||||
HRESULT status;
|
||||
|
||||
// Total number of samples
|
||||
struct sample_aligned_t {
|
||||
std::uint32_t uninitialized;
|
||||
std::int16_t *samples;
|
||||
} sample_aligned;
|
||||
|
||||
// number of samples / number of channels
|
||||
struct block_aligned_t {
|
||||
std::uint32_t audio_sample_size;
|
||||
} block_aligned;
|
||||
|
||||
status = WaitForSingleObjectEx(audio_event.get(), default_latency_ms, FALSE);
|
||||
switch(status) {
|
||||
case WAIT_OBJECT_0:
|
||||
break;
|
||||
case WAIT_TIMEOUT:
|
||||
return capture_e::timeout;
|
||||
default:
|
||||
BOOST_LOG(error) << "Couldn't wait for audio event: [0x"sv << util::hex(status).to_string_view() << ']';
|
||||
return capture_e::error;
|
||||
}
|
||||
|
||||
std::uint32_t packet_size {};
|
||||
for(
|
||||
status = audio_capture->GetNextPacketSize(&packet_size);
|
||||
SUCCEEDED(status) && packet_size > 0;
|
||||
status = audio_capture->GetNextPacketSize(&packet_size)) {
|
||||
DWORD buffer_flags;
|
||||
status = audio_capture->GetBuffer(
|
||||
(BYTE **)&sample_aligned.samples,
|
||||
&block_aligned.audio_sample_size,
|
||||
&buffer_flags,
|
||||
nullptr, nullptr);
|
||||
|
||||
switch(status) {
|
||||
case S_OK:
|
||||
break;
|
||||
case AUDCLNT_E_DEVICE_INVALIDATED:
|
||||
return capture_e::reinit;
|
||||
default:
|
||||
BOOST_LOG(error) << "Couldn't capture audio [0x"sv << util::hex(status).to_string_view() << ']';
|
||||
return capture_e::error;
|
||||
}
|
||||
|
||||
sample_aligned.uninitialized = std::end(sample_buf) - sample_buf_pos;
|
||||
auto n = std::min(sample_aligned.uninitialized, block_aligned.audio_sample_size * channels_in);
|
||||
|
||||
if(buffer_flags & AUDCLNT_BUFFERFLAGS_SILENT) {
|
||||
std::fill_n(sample_buf_pos, n, 0);
|
||||
}
|
||||
else {
|
||||
std::copy_n(sample_aligned.samples, n, sample_buf_pos);
|
||||
}
|
||||
|
||||
sample_buf_pos += n;
|
||||
|
||||
audio_capture->ReleaseBuffer(block_aligned.audio_sample_size);
|
||||
}
|
||||
|
||||
if(status == AUDCLNT_E_DEVICE_INVALIDATED) {
|
||||
return capture_e::reinit;
|
||||
}
|
||||
|
||||
if(FAILED(status)) {
|
||||
return capture_e::error;
|
||||
}
|
||||
|
||||
return capture_e::ok;
|
||||
}
|
||||
|
||||
public:
|
||||
handle_t audio_event;
|
||||
|
||||
device_enum_t device_enum;
|
||||
device_t device;
|
||||
audio_client_t audio_client;
|
||||
audio_capture_t audio_capture;
|
||||
|
||||
REFERENCE_TIME default_latency_ms;
|
||||
|
||||
util::buffer_t<std::int16_t> sample_buf;
|
||||
std::int16_t *sample_buf_pos;
|
||||
|
||||
// out --> our audio output
|
||||
int channels_out;
|
||||
// in --> our wasapi input
|
||||
int channels_in;
|
||||
|
||||
std::unique_ptr<audio_pipe_t> pipe;
|
||||
};
|
||||
|
||||
class audio_control_t : public ::platf::audio_control_t {
|
||||
public:
|
||||
std::optional<sink_t> sink_info() override {
|
||||
auto virtual_adapter_name = L"Steam Streaming Speakers"sv;
|
||||
|
||||
sink_t sink;
|
||||
|
||||
audio::device_enum_t device_enum;
|
||||
auto status = CoCreateInstance(
|
||||
CLSID_MMDeviceEnumerator,
|
||||
nullptr,
|
||||
CLSCTX_ALL,
|
||||
IID_IMMDeviceEnumerator,
|
||||
(void **)&device_enum);
|
||||
|
||||
if(FAILED(status)) {
|
||||
BOOST_LOG(error) << "Couldn't create Device Enumerator: [0x"sv << util::hex(status).to_string_view() << ']';
|
||||
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
auto device = default_device(device_enum);
|
||||
if(!device) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
audio::wstring_t wstring;
|
||||
device->GetId(&wstring);
|
||||
|
||||
sink.host = converter.to_bytes(wstring.get());
|
||||
|
||||
collection_t collection;
|
||||
status = device_enum->EnumAudioEndpoints(eRender, DEVICE_STATE_ACTIVE, &collection);
|
||||
if(FAILED(status)) {
|
||||
BOOST_LOG(error) << "Couldn't enumerate: [0x"sv << util::hex(status).to_string_view() << ']';
|
||||
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
UINT count;
|
||||
collection->GetCount(&count);
|
||||
|
||||
std::string virtual_device_id = config::audio.virtual_sink;
|
||||
for(auto x = 0; x < count; ++x) {
|
||||
audio::device_t device;
|
||||
collection->Item(x, &device);
|
||||
|
||||
auto type = validate_device(device, SAMPLE_RATE);
|
||||
if(type == format_t::none) {
|
||||
continue;
|
||||
}
|
||||
|
||||
audio::wstring_t wstring;
|
||||
device->GetId(&wstring);
|
||||
|
||||
audio::prop_t prop;
|
||||
device->OpenPropertyStore(STGM_READ, &prop);
|
||||
|
||||
prop_var_t adapter_friendly_name;
|
||||
prop_var_t device_friendly_name;
|
||||
prop_var_t device_desc;
|
||||
|
||||
prop->GetValue(PKEY_Device_FriendlyName, &device_friendly_name.prop);
|
||||
prop->GetValue(PKEY_DeviceInterface_FriendlyName, &adapter_friendly_name.prop);
|
||||
prop->GetValue(PKEY_Device_DeviceDesc, &device_desc.prop);
|
||||
|
||||
auto adapter_name = no_null((LPWSTR)adapter_friendly_name.prop.pszVal);
|
||||
BOOST_LOG(verbose)
|
||||
<< L"===== Device ====="sv << std::endl
|
||||
<< L"Device ID : "sv << wstring.get() << std::endl
|
||||
<< L"Device name : "sv << no_null((LPWSTR)device_friendly_name.prop.pszVal) << std::endl
|
||||
<< L"Adapter name : "sv << adapter_name << std::endl
|
||||
<< L"Device description : "sv << no_null((LPWSTR)device_desc.prop.pszVal) << std::endl
|
||||
<< std::endl;
|
||||
|
||||
if(virtual_device_id.empty() && adapter_name == virtual_adapter_name) {
|
||||
virtual_device_id = converter.to_bytes(wstring.get());
|
||||
}
|
||||
}
|
||||
|
||||
if(!virtual_device_id.empty()) {
|
||||
sink.null = std::make_optional(sink_t::null_t {
|
||||
"virtual-"s.append(formats[format_t::stereo - 1].name) + virtual_device_id,
|
||||
"virtual-"s.append(formats[format_t::surr51 - 1].name) + virtual_device_id,
|
||||
"virtual-"s.append(formats[format_t::surr71 - 1].name) + virtual_device_id,
|
||||
});
|
||||
}
|
||||
|
||||
return sink;
|
||||
}
|
||||
|
||||
std::unique_ptr<mic_t> microphone(const std::uint8_t *mapping, int channels, std::uint32_t sample_rate, std::uint32_t frame_size) override {
|
||||
auto mic = std::make_unique<mic_wasapi_t>();
|
||||
|
||||
if(mic->init(sample_rate, frame_size, channels)) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
return mic;
|
||||
}
|
||||
|
||||
/**
|
||||
* If the requested sink is a virtual sink, meaning no speakers attached to
|
||||
* the host, then we can seamlessly set the format to stereo and surround sound.
|
||||
*
|
||||
* Any virtual sink detected will be prefixed by:
|
||||
* virtual-(format name)
|
||||
* If it doesn't contain that prefix, then the format will not be changed
|
||||
*/
|
||||
std::optional<std::wstring> set_format(const std::string &sink) {
|
||||
std::string_view sv { sink.c_str(), sink.size() };
|
||||
|
||||
format_t::type_e type = format_t::none;
|
||||
// sink format:
|
||||
// [virtual-(format name)]device_id
|
||||
auto prefix = "virtual-"sv;
|
||||
if(sv.find(prefix) == 0) {
|
||||
sv = sv.substr(prefix.size(), sv.size() - prefix.size());
|
||||
|
||||
for(auto &format : formats) {
|
||||
auto &name = format.name;
|
||||
if(sv.find(name) == 0) {
|
||||
type = format.type;
|
||||
sv = sv.substr(name.size(), sv.size() - name.size());
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
auto wstring_device_id = converter.from_bytes(sv.data());
|
||||
|
||||
if(type == format_t::none) {
|
||||
// wstring_device_id does not contain virtual-(format name)
|
||||
// It's a simple deviceId, just pass it back
|
||||
return std::make_optional(std::move(wstring_device_id));
|
||||
}
|
||||
|
||||
wave_format_t wave_format;
|
||||
auto status = policy->GetMixFormat(wstring_device_id.c_str(), &wave_format);
|
||||
if(FAILED(status)) {
|
||||
BOOST_LOG(error) << "Couldn't acquire Wave Format [0x"sv << util::hex(status).to_string_view() << ']';
|
||||
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
if(init_wave_format(wave_format, SAMPLE_RATE)) {
|
||||
return std::nullopt;
|
||||
}
|
||||
set_wave_format(wave_format, formats[(int)type - 1]);
|
||||
|
||||
WAVEFORMATEXTENSIBLE p {};
|
||||
status = policy->SetDeviceFormat(wstring_device_id.c_str(), wave_format.get(), (WAVEFORMATEX *)&p);
|
||||
|
||||
// Surround 5.1 might contain side-{left, right} instead of speaker in the back
|
||||
// Try again with different speaker mask.
|
||||
if(status == 0x88890008 && type == format_t::surr51) {
|
||||
set_wave_format(wave_format, surround_51_side_speakers);
|
||||
status = policy->SetDeviceFormat(wstring_device_id.c_str(), wave_format.get(), (WAVEFORMATEX *)&p);
|
||||
}
|
||||
if(FAILED(status)) {
|
||||
BOOST_LOG(error) << "Couldn't set Wave Format [0x"sv << util::hex(status).to_string_view() << ']';
|
||||
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
return std::make_optional(std::move(wstring_device_id));
|
||||
}
|
||||
|
||||
int set_sink(const std::string &sink) override {
|
||||
auto wstring_device_id = set_format(sink);
|
||||
if(!wstring_device_id) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
int failure {};
|
||||
for(int x = 0; x < (int)ERole_enum_count; ++x) {
|
||||
auto status = policy->SetDefaultEndpoint(wstring_device_id->c_str(), (ERole)x);
|
||||
if(status) {
|
||||
BOOST_LOG(warning) << "Couldn't set ["sv << sink << "] to role ["sv << x << ']';
|
||||
|
||||
++failure;
|
||||
}
|
||||
}
|
||||
|
||||
return failure;
|
||||
}
|
||||
|
||||
int init() {
|
||||
auto status = CoCreateInstance(
|
||||
CLSID_CPolicyConfigClient,
|
||||
nullptr,
|
||||
CLSCTX_ALL,
|
||||
IID_IPolicyConfig,
|
||||
(void **)&policy);
|
||||
|
||||
if(FAILED(status)) {
|
||||
BOOST_LOG(error) << "Couldn't create audio policy config: [0x"sv << util::hex(status).to_string_view() << ']';
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
~audio_control_t() override {}
|
||||
|
||||
policy_t policy;
|
||||
};
|
||||
} // namespace platf::audio
|
||||
|
||||
namespace platf {
|
||||
|
||||
// It's not big enough to justify it's own source file :/
|
||||
namespace dxgi {
|
||||
int init();
|
||||
}
|
||||
|
||||
std::unique_ptr<audio_control_t> audio_control() {
|
||||
auto control = std::make_unique<audio::audio_control_t>();
|
||||
|
||||
if(control->init()) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
return control;
|
||||
}
|
||||
|
||||
std::unique_ptr<deinit_t> init() {
|
||||
if(dxgi::init()) {
|
||||
return nullptr;
|
||||
}
|
||||
return std::make_unique<platf::audio::co_init_t>();
|
||||
}
|
||||
} // namespace platf
|
||||
128
sunshine/platform/windows/display.h
Normal file
128
sunshine/platform/windows/display.h
Normal file
@@ -0,0 +1,128 @@
|
||||
//
|
||||
// Created by loki on 4/23/20.
|
||||
//
|
||||
|
||||
#ifndef SUNSHINE_DISPLAY_H
|
||||
#define SUNSHINE_DISPLAY_H
|
||||
|
||||
#include <d3d11.h>
|
||||
#include <d3d11_4.h>
|
||||
#include <d3dcommon.h>
|
||||
#include <dxgi.h>
|
||||
#include <dxgi1_2.h>
|
||||
|
||||
#include "sunshine/platform/common.h"
|
||||
#include "sunshine/utility.h"
|
||||
|
||||
namespace platf::dxgi {
|
||||
extern const char *format_str[];
|
||||
|
||||
template<class T>
|
||||
void Release(T *dxgi) {
|
||||
dxgi->Release();
|
||||
}
|
||||
|
||||
using factory1_t = util::safe_ptr<IDXGIFactory1, Release<IDXGIFactory1>>;
|
||||
using dxgi_t = util::safe_ptr<IDXGIDevice, Release<IDXGIDevice>>;
|
||||
using dxgi1_t = util::safe_ptr<IDXGIDevice1, Release<IDXGIDevice1>>;
|
||||
using device_t = util::safe_ptr<ID3D11Device, Release<ID3D11Device>>;
|
||||
using device_ctx_t = util::safe_ptr<ID3D11DeviceContext, Release<ID3D11DeviceContext>>;
|
||||
using adapter_t = util::safe_ptr<IDXGIAdapter1, Release<IDXGIAdapter1>>;
|
||||
using output_t = util::safe_ptr<IDXGIOutput, Release<IDXGIOutput>>;
|
||||
using output1_t = util::safe_ptr<IDXGIOutput1, Release<IDXGIOutput1>>;
|
||||
using dup_t = util::safe_ptr<IDXGIOutputDuplication, Release<IDXGIOutputDuplication>>;
|
||||
using texture2d_t = util::safe_ptr<ID3D11Texture2D, Release<ID3D11Texture2D>>;
|
||||
using texture1d_t = util::safe_ptr<ID3D11Texture1D, Release<ID3D11Texture1D>>;
|
||||
using resource_t = util::safe_ptr<IDXGIResource, Release<IDXGIResource>>;
|
||||
using multithread_t = util::safe_ptr<ID3D11Multithread, Release<ID3D11Multithread>>;
|
||||
|
||||
namespace video {
|
||||
using device_t = util::safe_ptr<ID3D11VideoDevice, Release<ID3D11VideoDevice>>;
|
||||
using ctx_t = util::safe_ptr<ID3D11VideoContext, Release<ID3D11VideoContext>>;
|
||||
using processor_t = util::safe_ptr<ID3D11VideoProcessor, Release<ID3D11VideoProcessor>>;
|
||||
using processor_out_t = util::safe_ptr<ID3D11VideoProcessorOutputView, Release<ID3D11VideoProcessorOutputView>>;
|
||||
using processor_in_t = util::safe_ptr<ID3D11VideoProcessorInputView, Release<ID3D11VideoProcessorInputView>>;
|
||||
using processor_enum_t = util::safe_ptr<ID3D11VideoProcessorEnumerator, Release<ID3D11VideoProcessorEnumerator>>;
|
||||
} // namespace video
|
||||
|
||||
class hwdevice_t;
|
||||
struct cursor_t {
|
||||
std::vector<std::uint8_t> img_data;
|
||||
|
||||
DXGI_OUTDUPL_POINTER_SHAPE_INFO shape_info;
|
||||
int x, y;
|
||||
bool visible;
|
||||
};
|
||||
|
||||
struct gpu_cursor_t {
|
||||
texture2d_t texture;
|
||||
|
||||
LONG width, height;
|
||||
};
|
||||
|
||||
class duplication_t {
|
||||
public:
|
||||
dup_t dup;
|
||||
bool has_frame {};
|
||||
|
||||
capture_e next_frame(DXGI_OUTDUPL_FRAME_INFO &frame_info, std::chrono::milliseconds timeout, resource_t::pointer *res_p);
|
||||
capture_e reset(dup_t::pointer dup_p = dup_t::pointer());
|
||||
capture_e release_frame();
|
||||
|
||||
~duplication_t();
|
||||
};
|
||||
|
||||
class display_base_t : public display_t {
|
||||
public:
|
||||
int init();
|
||||
|
||||
factory1_t factory;
|
||||
adapter_t adapter;
|
||||
output_t output;
|
||||
device_t device;
|
||||
device_ctx_t device_ctx;
|
||||
duplication_t dup;
|
||||
|
||||
DXGI_FORMAT format;
|
||||
D3D_FEATURE_LEVEL feature_level;
|
||||
|
||||
typedef enum _D3DKMT_SCHEDULINGPRIORITYCLASS {
|
||||
D3DKMT_SCHEDULINGPRIORITYCLASS_IDLE,
|
||||
D3DKMT_SCHEDULINGPRIORITYCLASS_BELOW_NORMAL,
|
||||
D3DKMT_SCHEDULINGPRIORITYCLASS_NORMAL,
|
||||
D3DKMT_SCHEDULINGPRIORITYCLASS_ABOVE_NORMAL,
|
||||
D3DKMT_SCHEDULINGPRIORITYCLASS_HIGH,
|
||||
D3DKMT_SCHEDULINGPRIORITYCLASS_REALTIME
|
||||
} D3DKMT_SCHEDULINGPRIORITYCLASS;
|
||||
|
||||
typedef NTSTATUS WINAPI (*PD3DKMTSetProcessSchedulingPriorityClass)(HANDLE, D3DKMT_SCHEDULINGPRIORITYCLASS);
|
||||
};
|
||||
|
||||
class display_ram_t : public display_base_t {
|
||||
public:
|
||||
capture_e snapshot(img_t *img, std::chrono::milliseconds timeout, bool cursor_visible) override;
|
||||
std::shared_ptr<img_t> alloc_img() override;
|
||||
int dummy_img(img_t *img) override;
|
||||
|
||||
int init();
|
||||
|
||||
cursor_t cursor;
|
||||
D3D11_MAPPED_SUBRESOURCE img_info;
|
||||
texture2d_t texture;
|
||||
};
|
||||
|
||||
class display_vram_t : public display_base_t, public std::enable_shared_from_this<display_vram_t> {
|
||||
public:
|
||||
capture_e snapshot(img_t *img, std::chrono::milliseconds timeout, bool cursor_visible) override;
|
||||
|
||||
std::shared_ptr<img_t> alloc_img() override;
|
||||
int dummy_img(img_t *img_base) override;
|
||||
|
||||
std::shared_ptr<platf::hwdevice_t> make_hwdevice(pix_fmt_e pix_fmt) override;
|
||||
|
||||
gpu_cursor_t cursor;
|
||||
std::vector<hwdevice_t *> hwdevices;
|
||||
};
|
||||
} // namespace platf::dxgi
|
||||
|
||||
#endif
|
||||
438
sunshine/platform/windows/display_base.cpp
Normal file
438
sunshine/platform/windows/display_base.cpp
Normal file
@@ -0,0 +1,438 @@
|
||||
//
|
||||
// Created by loki on 1/12/20.
|
||||
//
|
||||
|
||||
#include <codecvt>
|
||||
|
||||
#include "sunshine/config.h"
|
||||
#include "sunshine/main.h"
|
||||
#include "sunshine/platform/common.h"
|
||||
|
||||
#include "display.h"
|
||||
|
||||
namespace platf {
|
||||
using namespace std::literals;
|
||||
}
|
||||
namespace platf::dxgi {
|
||||
capture_e duplication_t::next_frame(DXGI_OUTDUPL_FRAME_INFO &frame_info, std::chrono::milliseconds timeout, resource_t::pointer *res_p) {
|
||||
auto capture_status = release_frame();
|
||||
if(capture_status != capture_e::ok) {
|
||||
return capture_status;
|
||||
}
|
||||
|
||||
auto status = dup->AcquireNextFrame(timeout.count(), &frame_info, res_p);
|
||||
|
||||
switch(status) {
|
||||
case S_OK:
|
||||
has_frame = true;
|
||||
return capture_e::ok;
|
||||
case DXGI_ERROR_WAIT_TIMEOUT:
|
||||
return capture_e::timeout;
|
||||
case WAIT_ABANDONED:
|
||||
case DXGI_ERROR_ACCESS_LOST:
|
||||
case DXGI_ERROR_ACCESS_DENIED:
|
||||
return capture_e::reinit;
|
||||
default:
|
||||
BOOST_LOG(error) << "Couldn't acquire next frame [0x"sv << util::hex(status).to_string_view();
|
||||
return capture_e::error;
|
||||
}
|
||||
}
|
||||
|
||||
capture_e duplication_t::reset(dup_t::pointer dup_p) {
|
||||
auto capture_status = release_frame();
|
||||
|
||||
dup.reset(dup_p);
|
||||
|
||||
return capture_status;
|
||||
}
|
||||
|
||||
capture_e duplication_t::release_frame() {
|
||||
if(!has_frame) {
|
||||
return capture_e::ok;
|
||||
}
|
||||
|
||||
auto status = dup->ReleaseFrame();
|
||||
switch(status) {
|
||||
case S_OK:
|
||||
has_frame = false;
|
||||
return capture_e::ok;
|
||||
case DXGI_ERROR_WAIT_TIMEOUT:
|
||||
return capture_e::timeout;
|
||||
case WAIT_ABANDONED:
|
||||
case DXGI_ERROR_ACCESS_LOST:
|
||||
case DXGI_ERROR_ACCESS_DENIED:
|
||||
has_frame = false;
|
||||
return capture_e::reinit;
|
||||
default:
|
||||
BOOST_LOG(error) << "Couldn't release frame [0x"sv << util::hex(status).to_string_view();
|
||||
return capture_e::error;
|
||||
}
|
||||
}
|
||||
|
||||
duplication_t::~duplication_t() {
|
||||
release_frame();
|
||||
}
|
||||
|
||||
int display_base_t::init() {
|
||||
/* Uncomment when use of IDXGIOutput5 is implemented
|
||||
std::call_once(windows_cpp_once_flag, []() {
|
||||
DECLARE_HANDLE(DPI_AWARENESS_CONTEXT);
|
||||
const auto DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2 = ((DPI_AWARENESS_CONTEXT)-4);
|
||||
|
||||
typedef BOOL (*User32_SetProcessDpiAwarenessContext)(DPI_AWARENESS_CONTEXT value);
|
||||
|
||||
auto user32 = LoadLibraryA("user32.dll");
|
||||
auto f = (User32_SetProcessDpiAwarenessContext)GetProcAddress(user32, "SetProcessDpiAwarenessContext");
|
||||
if(f) {
|
||||
f(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2);
|
||||
}
|
||||
|
||||
FreeLibrary(user32);
|
||||
});
|
||||
*/
|
||||
|
||||
HRESULT status;
|
||||
|
||||
status = CreateDXGIFactory1(IID_IDXGIFactory1, (void **)&factory);
|
||||
if(FAILED(status)) {
|
||||
BOOST_LOG(error) << "Failed to create DXGIFactory1 [0x"sv << util::hex(status).to_string_view() << ']';
|
||||
return -1;
|
||||
}
|
||||
|
||||
std::wstring_convert<std::codecvt_utf8_utf16<wchar_t>, wchar_t> converter;
|
||||
|
||||
auto adapter_name = converter.from_bytes(config::video.adapter_name);
|
||||
auto output_name = converter.from_bytes(config::video.output_name);
|
||||
|
||||
adapter_t::pointer adapter_p;
|
||||
for(int x = 0; factory->EnumAdapters1(x, &adapter_p) != DXGI_ERROR_NOT_FOUND; ++x) {
|
||||
dxgi::adapter_t adapter_tmp { adapter_p };
|
||||
|
||||
DXGI_ADAPTER_DESC1 adapter_desc;
|
||||
adapter_tmp->GetDesc1(&adapter_desc);
|
||||
|
||||
if(!adapter_name.empty() && adapter_desc.Description != adapter_name) {
|
||||
continue;
|
||||
}
|
||||
|
||||
dxgi::output_t::pointer output_p;
|
||||
for(int y = 0; adapter_tmp->EnumOutputs(y, &output_p) != DXGI_ERROR_NOT_FOUND; ++y) {
|
||||
dxgi::output_t output_tmp { output_p };
|
||||
|
||||
DXGI_OUTPUT_DESC desc;
|
||||
output_tmp->GetDesc(&desc);
|
||||
|
||||
if(!output_name.empty() && desc.DeviceName != output_name) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if(desc.AttachedToDesktop) {
|
||||
output = std::move(output_tmp);
|
||||
|
||||
offset_x = desc.DesktopCoordinates.left;
|
||||
offset_y = desc.DesktopCoordinates.top;
|
||||
width = desc.DesktopCoordinates.right - offset_x;
|
||||
height = desc.DesktopCoordinates.bottom - offset_y;
|
||||
}
|
||||
}
|
||||
|
||||
if(output) {
|
||||
adapter = std::move(adapter_tmp);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if(!output) {
|
||||
BOOST_LOG(error) << "Failed to locate an output device"sv;
|
||||
return -1;
|
||||
}
|
||||
|
||||
D3D_FEATURE_LEVEL featureLevels[] {
|
||||
D3D_FEATURE_LEVEL_11_1,
|
||||
D3D_FEATURE_LEVEL_11_0,
|
||||
D3D_FEATURE_LEVEL_10_1,
|
||||
D3D_FEATURE_LEVEL_10_0,
|
||||
D3D_FEATURE_LEVEL_9_3,
|
||||
D3D_FEATURE_LEVEL_9_2,
|
||||
D3D_FEATURE_LEVEL_9_1
|
||||
};
|
||||
|
||||
status = adapter->QueryInterface(IID_IDXGIAdapter, (void **)&adapter_p);
|
||||
if(FAILED(status)) {
|
||||
BOOST_LOG(error) << "Failed to query IDXGIAdapter interface"sv;
|
||||
return -1;
|
||||
}
|
||||
|
||||
status = D3D11CreateDevice(
|
||||
adapter_p,
|
||||
D3D_DRIVER_TYPE_UNKNOWN,
|
||||
nullptr,
|
||||
D3D11_CREATE_DEVICE_VIDEO_SUPPORT,
|
||||
featureLevels, sizeof(featureLevels) / sizeof(D3D_FEATURE_LEVEL),
|
||||
D3D11_SDK_VERSION,
|
||||
&device,
|
||||
&feature_level,
|
||||
&device_ctx);
|
||||
|
||||
adapter_p->Release();
|
||||
|
||||
if(FAILED(status)) {
|
||||
BOOST_LOG(error) << "Failed to create D3D11 device [0x"sv << util::hex(status).to_string_view() << ']';
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
DXGI_ADAPTER_DESC adapter_desc;
|
||||
adapter->GetDesc(&adapter_desc);
|
||||
|
||||
auto description = converter.to_bytes(adapter_desc.Description);
|
||||
BOOST_LOG(info)
|
||||
<< std::endl
|
||||
<< "Device Description : " << description << std::endl
|
||||
<< "Device Vendor ID : 0x"sv << util::hex(adapter_desc.VendorId).to_string_view() << std::endl
|
||||
<< "Device Device ID : 0x"sv << util::hex(adapter_desc.DeviceId).to_string_view() << std::endl
|
||||
<< "Device Video Mem : "sv << adapter_desc.DedicatedVideoMemory / 1048576 << " MiB"sv << std::endl
|
||||
<< "Device Sys Mem : "sv << adapter_desc.DedicatedSystemMemory / 1048576 << " MiB"sv << std::endl
|
||||
<< "Share Sys Mem : "sv << adapter_desc.SharedSystemMemory / 1048576 << " MiB"sv << std::endl
|
||||
<< "Feature Level : 0x"sv << util::hex(feature_level).to_string_view() << std::endl
|
||||
<< "Capture size : "sv << width << 'x' << height;
|
||||
|
||||
// Bump up thread priority
|
||||
{
|
||||
const DWORD flags = TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY;
|
||||
TOKEN_PRIVILEGES tp;
|
||||
HANDLE token;
|
||||
LUID val;
|
||||
|
||||
if(OpenProcessToken(GetCurrentProcess(), flags, &token) &&
|
||||
!!LookupPrivilegeValue(NULL, SE_INC_BASE_PRIORITY_NAME, &val)) {
|
||||
tp.PrivilegeCount = 1;
|
||||
tp.Privileges[0].Luid = val;
|
||||
tp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;
|
||||
|
||||
if(!AdjustTokenPrivileges(token, false, &tp, sizeof(tp), NULL, NULL)) {
|
||||
BOOST_LOG(warning) << "Could not set privilege to increase GPU priority";
|
||||
}
|
||||
}
|
||||
|
||||
CloseHandle(token);
|
||||
|
||||
HMODULE gdi32 = GetModuleHandleA("GDI32");
|
||||
if(gdi32) {
|
||||
PD3DKMTSetProcessSchedulingPriorityClass fn =
|
||||
(PD3DKMTSetProcessSchedulingPriorityClass)GetProcAddress(gdi32, "D3DKMTSetProcessSchedulingPriorityClass");
|
||||
if(fn) {
|
||||
status = fn(GetCurrentProcess(), D3DKMT_SCHEDULINGPRIORITYCLASS_REALTIME);
|
||||
if(FAILED(status)) {
|
||||
BOOST_LOG(warning) << "Failed to set realtime GPU priority. Please run application as administrator for optimal performance.";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dxgi::dxgi_t dxgi;
|
||||
status = device->QueryInterface(IID_IDXGIDevice, (void **)&dxgi);
|
||||
if(FAILED(status)) {
|
||||
BOOST_LOG(warning) << "Failed to query DXGI interface from device [0x"sv << util::hex(status).to_string_view() << ']';
|
||||
return -1;
|
||||
}
|
||||
|
||||
dxgi->SetGPUThreadPriority(7);
|
||||
}
|
||||
|
||||
// Try to reduce latency
|
||||
{
|
||||
dxgi::dxgi1_t dxgi {};
|
||||
status = device->QueryInterface(IID_IDXGIDevice, (void **)&dxgi);
|
||||
if(FAILED(status)) {
|
||||
BOOST_LOG(error) << "Failed to query DXGI interface from device [0x"sv << util::hex(status).to_string_view() << ']';
|
||||
return -1;
|
||||
}
|
||||
|
||||
status = dxgi->SetMaximumFrameLatency(1);
|
||||
if(FAILED(status)) {
|
||||
BOOST_LOG(warning) << "Failed to set maximum frame latency [0x"sv << util::hex(status).to_string_view() << ']';
|
||||
}
|
||||
}
|
||||
|
||||
//FIXME: Duplicate output on RX580 in combination with DOOM (2016) --> BSOD
|
||||
//TODO: Use IDXGIOutput5 for improved performance
|
||||
{
|
||||
dxgi::output1_t output1 {};
|
||||
status = output->QueryInterface(IID_IDXGIOutput1, (void **)&output1);
|
||||
if(FAILED(status)) {
|
||||
BOOST_LOG(error) << "Failed to query IDXGIOutput1 from the output"sv;
|
||||
return -1;
|
||||
}
|
||||
|
||||
// We try this twice, in case we still get an error on reinitialization
|
||||
for(int x = 0; x < 2; ++x) {
|
||||
status = output1->DuplicateOutput((IUnknown *)device.get(), &dup.dup);
|
||||
if(SUCCEEDED(status)) {
|
||||
break;
|
||||
}
|
||||
std::this_thread::sleep_for(200ms);
|
||||
}
|
||||
|
||||
if(FAILED(status)) {
|
||||
BOOST_LOG(error) << "DuplicateOutput Failed [0x"sv << util::hex(status).to_string_view() << ']';
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
DXGI_OUTDUPL_DESC dup_desc;
|
||||
dup.dup->GetDesc(&dup_desc);
|
||||
|
||||
format = dup_desc.ModeDesc.Format;
|
||||
|
||||
BOOST_LOG(debug) << "Source format ["sv << format_str[dup_desc.ModeDesc.Format] << ']';
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
const char *format_str[] = {
|
||||
"DXGI_FORMAT_UNKNOWN",
|
||||
"DXGI_FORMAT_R32G32B32A32_TYPELESS",
|
||||
"DXGI_FORMAT_R32G32B32A32_FLOAT",
|
||||
"DXGI_FORMAT_R32G32B32A32_UINT",
|
||||
"DXGI_FORMAT_R32G32B32A32_SINT",
|
||||
"DXGI_FORMAT_R32G32B32_TYPELESS",
|
||||
"DXGI_FORMAT_R32G32B32_FLOAT",
|
||||
"DXGI_FORMAT_R32G32B32_UINT",
|
||||
"DXGI_FORMAT_R32G32B32_SINT",
|
||||
"DXGI_FORMAT_R16G16B16A16_TYPELESS",
|
||||
"DXGI_FORMAT_R16G16B16A16_FLOAT",
|
||||
"DXGI_FORMAT_R16G16B16A16_UNORM",
|
||||
"DXGI_FORMAT_R16G16B16A16_UINT",
|
||||
"DXGI_FORMAT_R16G16B16A16_SNORM",
|
||||
"DXGI_FORMAT_R16G16B16A16_SINT",
|
||||
"DXGI_FORMAT_R32G32_TYPELESS",
|
||||
"DXGI_FORMAT_R32G32_FLOAT",
|
||||
"DXGI_FORMAT_R32G32_UINT",
|
||||
"DXGI_FORMAT_R32G32_SINT",
|
||||
"DXGI_FORMAT_R32G8X24_TYPELESS",
|
||||
"DXGI_FORMAT_D32_FLOAT_S8X24_UINT",
|
||||
"DXGI_FORMAT_R32_FLOAT_X8X24_TYPELESS",
|
||||
"DXGI_FORMAT_X32_TYPELESS_G8X24_UINT",
|
||||
"DXGI_FORMAT_R10G10B10A2_TYPELESS",
|
||||
"DXGI_FORMAT_R10G10B10A2_UNORM",
|
||||
"DXGI_FORMAT_R10G10B10A2_UINT",
|
||||
"DXGI_FORMAT_R11G11B10_FLOAT",
|
||||
"DXGI_FORMAT_R8G8B8A8_TYPELESS",
|
||||
"DXGI_FORMAT_R8G8B8A8_UNORM",
|
||||
"DXGI_FORMAT_R8G8B8A8_UNORM_SRGB",
|
||||
"DXGI_FORMAT_R8G8B8A8_UINT",
|
||||
"DXGI_FORMAT_R8G8B8A8_SNORM",
|
||||
"DXGI_FORMAT_R8G8B8A8_SINT",
|
||||
"DXGI_FORMAT_R16G16_TYPELESS",
|
||||
"DXGI_FORMAT_R16G16_FLOAT",
|
||||
"DXGI_FORMAT_R16G16_UNORM",
|
||||
"DXGI_FORMAT_R16G16_UINT",
|
||||
"DXGI_FORMAT_R16G16_SNORM",
|
||||
"DXGI_FORMAT_R16G16_SINT",
|
||||
"DXGI_FORMAT_R32_TYPELESS",
|
||||
"DXGI_FORMAT_D32_FLOAT",
|
||||
"DXGI_FORMAT_R32_FLOAT",
|
||||
"DXGI_FORMAT_R32_UINT",
|
||||
"DXGI_FORMAT_R32_SINT",
|
||||
"DXGI_FORMAT_R24G8_TYPELESS",
|
||||
"DXGI_FORMAT_D24_UNORM_S8_UINT",
|
||||
"DXGI_FORMAT_R24_UNORM_X8_TYPELESS",
|
||||
"DXGI_FORMAT_X24_TYPELESS_G8_UINT",
|
||||
"DXGI_FORMAT_R8G8_TYPELESS",
|
||||
"DXGI_FORMAT_R8G8_UNORM",
|
||||
"DXGI_FORMAT_R8G8_UINT",
|
||||
"DXGI_FORMAT_R8G8_SNORM",
|
||||
"DXGI_FORMAT_R8G8_SINT",
|
||||
"DXGI_FORMAT_R16_TYPELESS",
|
||||
"DXGI_FORMAT_R16_FLOAT",
|
||||
"DXGI_FORMAT_D16_UNORM",
|
||||
"DXGI_FORMAT_R16_UNORM",
|
||||
"DXGI_FORMAT_R16_UINT",
|
||||
"DXGI_FORMAT_R16_SNORM",
|
||||
"DXGI_FORMAT_R16_SINT",
|
||||
"DXGI_FORMAT_R8_TYPELESS",
|
||||
"DXGI_FORMAT_R8_UNORM",
|
||||
"DXGI_FORMAT_R8_UINT",
|
||||
"DXGI_FORMAT_R8_SNORM",
|
||||
"DXGI_FORMAT_R8_SINT",
|
||||
"DXGI_FORMAT_A8_UNORM",
|
||||
"DXGI_FORMAT_R1_UNORM",
|
||||
"DXGI_FORMAT_R9G9B9E5_SHAREDEXP",
|
||||
"DXGI_FORMAT_R8G8_B8G8_UNORM",
|
||||
"DXGI_FORMAT_G8R8_G8B8_UNORM",
|
||||
"DXGI_FORMAT_BC1_TYPELESS",
|
||||
"DXGI_FORMAT_BC1_UNORM",
|
||||
"DXGI_FORMAT_BC1_UNORM_SRGB",
|
||||
"DXGI_FORMAT_BC2_TYPELESS",
|
||||
"DXGI_FORMAT_BC2_UNORM",
|
||||
"DXGI_FORMAT_BC2_UNORM_SRGB",
|
||||
"DXGI_FORMAT_BC3_TYPELESS",
|
||||
"DXGI_FORMAT_BC3_UNORM",
|
||||
"DXGI_FORMAT_BC3_UNORM_SRGB",
|
||||
"DXGI_FORMAT_BC4_TYPELESS",
|
||||
"DXGI_FORMAT_BC4_UNORM",
|
||||
"DXGI_FORMAT_BC4_SNORM",
|
||||
"DXGI_FORMAT_BC5_TYPELESS",
|
||||
"DXGI_FORMAT_BC5_UNORM",
|
||||
"DXGI_FORMAT_BC5_SNORM",
|
||||
"DXGI_FORMAT_B5G6R5_UNORM",
|
||||
"DXGI_FORMAT_B5G5R5A1_UNORM",
|
||||
"DXGI_FORMAT_B8G8R8A8_UNORM",
|
||||
"DXGI_FORMAT_B8G8R8X8_UNORM",
|
||||
"DXGI_FORMAT_R10G10B10_XR_BIAS_A2_UNORM",
|
||||
"DXGI_FORMAT_B8G8R8A8_TYPELESS",
|
||||
"DXGI_FORMAT_B8G8R8A8_UNORM_SRGB",
|
||||
"DXGI_FORMAT_B8G8R8X8_TYPELESS",
|
||||
"DXGI_FORMAT_B8G8R8X8_UNORM_SRGB",
|
||||
"DXGI_FORMAT_BC6H_TYPELESS",
|
||||
"DXGI_FORMAT_BC6H_UF16",
|
||||
"DXGI_FORMAT_BC6H_SF16",
|
||||
"DXGI_FORMAT_BC7_TYPELESS",
|
||||
"DXGI_FORMAT_BC7_UNORM",
|
||||
"DXGI_FORMAT_BC7_UNORM_SRGB",
|
||||
"DXGI_FORMAT_AYUV",
|
||||
"DXGI_FORMAT_Y410",
|
||||
"DXGI_FORMAT_Y416",
|
||||
"DXGI_FORMAT_NV12",
|
||||
"DXGI_FORMAT_P010",
|
||||
"DXGI_FORMAT_P016",
|
||||
"DXGI_FORMAT_420_OPAQUE",
|
||||
"DXGI_FORMAT_YUY2",
|
||||
"DXGI_FORMAT_Y210",
|
||||
"DXGI_FORMAT_Y216",
|
||||
"DXGI_FORMAT_NV11",
|
||||
"DXGI_FORMAT_AI44",
|
||||
"DXGI_FORMAT_IA44",
|
||||
"DXGI_FORMAT_P8",
|
||||
"DXGI_FORMAT_A8P8",
|
||||
"DXGI_FORMAT_B4G4R4A4_UNORM",
|
||||
|
||||
NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL,
|
||||
|
||||
"DXGI_FORMAT_P208",
|
||||
"DXGI_FORMAT_V208",
|
||||
"DXGI_FORMAT_V408"
|
||||
};
|
||||
|
||||
} // namespace platf::dxgi
|
||||
|
||||
namespace platf {
|
||||
std::shared_ptr<display_t> display(mem_type_e hwdevice_type) {
|
||||
if(hwdevice_type == mem_type_e::dxgi) {
|
||||
auto disp = std::make_shared<dxgi::display_vram_t>();
|
||||
|
||||
if(!disp->init()) {
|
||||
return disp;
|
||||
}
|
||||
}
|
||||
else if(hwdevice_type == mem_type_e::system) {
|
||||
auto disp = std::make_shared<dxgi::display_ram_t>();
|
||||
|
||||
if(!disp->init()) {
|
||||
return disp;
|
||||
}
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
} // namespace platf
|
||||
297
sunshine/platform/windows/display_ram.cpp
Normal file
297
sunshine/platform/windows/display_ram.cpp
Normal file
@@ -0,0 +1,297 @@
|
||||
#include "display.h"
|
||||
#include "sunshine/main.h"
|
||||
|
||||
namespace platf {
|
||||
using namespace std::literals;
|
||||
}
|
||||
|
||||
namespace platf::dxgi {
|
||||
struct img_t : public ::platf::img_t {
|
||||
~img_t() override {
|
||||
delete[] data;
|
||||
data = nullptr;
|
||||
}
|
||||
};
|
||||
|
||||
void blend_cursor_monochrome(const cursor_t &cursor, img_t &img) {
|
||||
int height = cursor.shape_info.Height / 2;
|
||||
int width = cursor.shape_info.Width;
|
||||
int pitch = cursor.shape_info.Pitch;
|
||||
|
||||
// img cursor.{x,y} < 0, skip parts of the cursor.img_data
|
||||
auto cursor_skip_y = -std::min(0, cursor.y);
|
||||
auto cursor_skip_x = -std::min(0, cursor.x);
|
||||
|
||||
// img cursor.{x,y} > img.{x,y}, truncate parts of the cursor.img_data
|
||||
auto cursor_truncate_y = std::max(0, cursor.y - img.height);
|
||||
auto cursor_truncate_x = std::max(0, cursor.x - img.width);
|
||||
|
||||
auto cursor_width = width - cursor_skip_x - cursor_truncate_x;
|
||||
auto cursor_height = height - cursor_skip_y - cursor_truncate_y;
|
||||
|
||||
if(cursor_height > height || cursor_width > width) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto img_skip_y = std::max(0, cursor.y);
|
||||
auto img_skip_x = std::max(0, cursor.x);
|
||||
|
||||
auto cursor_img_data = cursor.img_data.data() + cursor_skip_y * pitch;
|
||||
|
||||
int delta_height = std::min(cursor_height - cursor_truncate_y, std::max(0, img.height - img_skip_y));
|
||||
int delta_width = std::min(cursor_width - cursor_truncate_x, std::max(0, img.width - img_skip_x));
|
||||
|
||||
auto pixels_per_byte = width / pitch;
|
||||
auto bytes_per_row = delta_width / pixels_per_byte;
|
||||
|
||||
auto img_data = (int *)img.data;
|
||||
for(int i = 0; i < delta_height; ++i) {
|
||||
auto and_mask = &cursor_img_data[i * pitch];
|
||||
auto xor_mask = &cursor_img_data[(i + height) * pitch];
|
||||
|
||||
auto img_pixel_p = &img_data[(i + img_skip_y) * (img.row_pitch / img.pixel_pitch) + img_skip_x];
|
||||
|
||||
auto skip_x = cursor_skip_x;
|
||||
for(int x = 0; x < bytes_per_row; ++x) {
|
||||
for(auto bit = 0u; bit < 8; ++bit) {
|
||||
if(skip_x > 0) {
|
||||
--skip_x;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
int and_ = *and_mask & (1 << (7 - bit)) ? -1 : 0;
|
||||
int xor_ = *xor_mask & (1 << (7 - bit)) ? -1 : 0;
|
||||
|
||||
*img_pixel_p &= and_;
|
||||
*img_pixel_p ^= xor_;
|
||||
|
||||
++img_pixel_p;
|
||||
}
|
||||
|
||||
++and_mask;
|
||||
++xor_mask;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void apply_color_alpha(int *img_pixel_p, int cursor_pixel) {
|
||||
auto colors_out = (std::uint8_t *)&cursor_pixel;
|
||||
auto colors_in = (std::uint8_t *)img_pixel_p;
|
||||
|
||||
//TODO: When use of IDXGIOutput5 is implemented, support different color formats
|
||||
auto alpha = colors_out[3];
|
||||
if(alpha == 255) {
|
||||
*img_pixel_p = cursor_pixel;
|
||||
}
|
||||
else {
|
||||
colors_in[0] = colors_out[0] + (colors_in[0] * (255 - alpha) + 255 / 2) / 255;
|
||||
colors_in[1] = colors_out[1] + (colors_in[1] * (255 - alpha) + 255 / 2) / 255;
|
||||
colors_in[2] = colors_out[2] + (colors_in[2] * (255 - alpha) + 255 / 2) / 255;
|
||||
}
|
||||
}
|
||||
|
||||
void apply_color_masked(int *img_pixel_p, int cursor_pixel) {
|
||||
//TODO: When use of IDXGIOutput5 is implemented, support different color formats
|
||||
auto alpha = ((std::uint8_t *)&cursor_pixel)[3];
|
||||
if(alpha == 0xFF) {
|
||||
*img_pixel_p ^= cursor_pixel;
|
||||
}
|
||||
else {
|
||||
*img_pixel_p = cursor_pixel;
|
||||
}
|
||||
}
|
||||
|
||||
void blend_cursor_color(const cursor_t &cursor, img_t &img, const bool masked) {
|
||||
int height = cursor.shape_info.Height;
|
||||
int width = cursor.shape_info.Width;
|
||||
int pitch = cursor.shape_info.Pitch;
|
||||
|
||||
// img cursor.y < 0, skip parts of the cursor.img_data
|
||||
auto cursor_skip_y = -std::min(0, cursor.y);
|
||||
auto cursor_skip_x = -std::min(0, cursor.x);
|
||||
|
||||
// img cursor.{x,y} > img.{x,y}, truncate parts of the cursor.img_data
|
||||
auto cursor_truncate_y = std::max(0, cursor.y - img.height);
|
||||
auto cursor_truncate_x = std::max(0, cursor.x - img.width);
|
||||
|
||||
auto img_skip_y = std::max(0, cursor.y);
|
||||
auto img_skip_x = std::max(0, cursor.x);
|
||||
|
||||
auto cursor_width = width - cursor_skip_x - cursor_truncate_x;
|
||||
auto cursor_height = height - cursor_skip_y - cursor_truncate_y;
|
||||
|
||||
if(cursor_height > height || cursor_width > width) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto cursor_img_data = (int *)&cursor.img_data[cursor_skip_y * pitch];
|
||||
|
||||
int delta_height = std::min(cursor_height - cursor_truncate_y, std::max(0, img.height - img_skip_y));
|
||||
int delta_width = std::min(cursor_width - cursor_truncate_x, std::max(0, img.width - img_skip_x));
|
||||
|
||||
auto img_data = (int *)img.data;
|
||||
|
||||
for(int i = 0; i < delta_height; ++i) {
|
||||
auto cursor_begin = &cursor_img_data[i * cursor.shape_info.Width + cursor_skip_x];
|
||||
auto cursor_end = &cursor_begin[delta_width];
|
||||
|
||||
auto img_pixel_p = &img_data[(i + img_skip_y) * (img.row_pitch / img.pixel_pitch) + img_skip_x];
|
||||
std::for_each(cursor_begin, cursor_end, [&](int cursor_pixel) {
|
||||
if(masked) {
|
||||
apply_color_masked(img_pixel_p, cursor_pixel);
|
||||
}
|
||||
else {
|
||||
apply_color_alpha(img_pixel_p, cursor_pixel);
|
||||
}
|
||||
++img_pixel_p;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void blend_cursor(const cursor_t &cursor, img_t &img) {
|
||||
switch(cursor.shape_info.Type) {
|
||||
case DXGI_OUTDUPL_POINTER_SHAPE_TYPE_COLOR:
|
||||
blend_cursor_color(cursor, img, false);
|
||||
break;
|
||||
case DXGI_OUTDUPL_POINTER_SHAPE_TYPE_MONOCHROME:
|
||||
blend_cursor_monochrome(cursor, img);
|
||||
break;
|
||||
case DXGI_OUTDUPL_POINTER_SHAPE_TYPE_MASKED_COLOR:
|
||||
blend_cursor_color(cursor, img, true);
|
||||
break;
|
||||
default:
|
||||
BOOST_LOG(warning) << "Unsupported cursor format ["sv << cursor.shape_info.Type << ']';
|
||||
}
|
||||
}
|
||||
|
||||
capture_e display_ram_t::snapshot(::platf::img_t *img_base, std::chrono::milliseconds timeout, bool cursor_visible) {
|
||||
auto img = (img_t *)img_base;
|
||||
|
||||
HRESULT status;
|
||||
|
||||
DXGI_OUTDUPL_FRAME_INFO frame_info;
|
||||
|
||||
resource_t::pointer res_p {};
|
||||
auto capture_status = dup.next_frame(frame_info, timeout, &res_p);
|
||||
resource_t res { res_p };
|
||||
|
||||
if(capture_status != capture_e::ok) {
|
||||
return capture_status;
|
||||
}
|
||||
|
||||
if(frame_info.PointerShapeBufferSize > 0) {
|
||||
auto &img_data = cursor.img_data;
|
||||
|
||||
img_data.resize(frame_info.PointerShapeBufferSize);
|
||||
|
||||
UINT dummy;
|
||||
status = dup.dup->GetFramePointerShape(img_data.size(), img_data.data(), &dummy, &cursor.shape_info);
|
||||
if(FAILED(status)) {
|
||||
BOOST_LOG(error) << "Failed to get new pointer shape [0x"sv << util::hex(status).to_string_view() << ']';
|
||||
|
||||
return capture_e::error;
|
||||
}
|
||||
}
|
||||
|
||||
if(frame_info.LastMouseUpdateTime.QuadPart) {
|
||||
cursor.x = frame_info.PointerPosition.Position.x;
|
||||
cursor.y = frame_info.PointerPosition.Position.y;
|
||||
cursor.visible = frame_info.PointerPosition.Visible;
|
||||
}
|
||||
|
||||
// If frame has been updated
|
||||
if(frame_info.LastPresentTime.QuadPart != 0) {
|
||||
{
|
||||
texture2d_t src {};
|
||||
status = res->QueryInterface(IID_ID3D11Texture2D, (void **)&src);
|
||||
|
||||
if(FAILED(status)) {
|
||||
BOOST_LOG(error) << "Couldn't query interface [0x"sv << util::hex(status).to_string_view() << ']';
|
||||
return capture_e::error;
|
||||
}
|
||||
|
||||
//Copy from GPU to CPU
|
||||
device_ctx->CopyResource(texture.get(), src.get());
|
||||
}
|
||||
|
||||
if(img_info.pData) {
|
||||
device_ctx->Unmap(texture.get(), 0);
|
||||
img_info.pData = nullptr;
|
||||
}
|
||||
|
||||
status = device_ctx->Map(texture.get(), 0, D3D11_MAP_READ, 0, &img_info);
|
||||
if(FAILED(status)) {
|
||||
BOOST_LOG(error) << "Failed to map texture [0x"sv << util::hex(status).to_string_view() << ']';
|
||||
|
||||
return capture_e::error;
|
||||
}
|
||||
}
|
||||
|
||||
const bool mouse_update =
|
||||
(frame_info.LastMouseUpdateTime.QuadPart || frame_info.PointerShapeBufferSize > 0) &&
|
||||
(cursor_visible && cursor.visible);
|
||||
|
||||
const bool update_flag = frame_info.LastPresentTime.QuadPart != 0 || mouse_update;
|
||||
|
||||
if(!update_flag) {
|
||||
return capture_e::timeout;
|
||||
}
|
||||
|
||||
std::copy_n((std::uint8_t *)img_info.pData, height * img_info.RowPitch, (std::uint8_t *)img->data);
|
||||
|
||||
if(cursor_visible && cursor.visible) {
|
||||
blend_cursor(cursor, *img);
|
||||
}
|
||||
|
||||
return capture_e::ok;
|
||||
}
|
||||
|
||||
std::shared_ptr<platf::img_t> display_ram_t::alloc_img() {
|
||||
auto img = std::make_shared<img_t>();
|
||||
|
||||
img->pixel_pitch = 4;
|
||||
img->row_pitch = img_info.RowPitch;
|
||||
img->width = width;
|
||||
img->height = height;
|
||||
img->data = new std::uint8_t[img->row_pitch * height];
|
||||
|
||||
return img;
|
||||
}
|
||||
|
||||
int display_ram_t::dummy_img(platf::img_t *img) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
int display_ram_t::init() {
|
||||
if(display_base_t::init()) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
D3D11_TEXTURE2D_DESC t {};
|
||||
t.Width = width;
|
||||
t.Height = height;
|
||||
t.MipLevels = 1;
|
||||
t.ArraySize = 1;
|
||||
t.SampleDesc.Count = 1;
|
||||
t.Usage = D3D11_USAGE_STAGING;
|
||||
t.Format = format;
|
||||
t.CPUAccessFlags = D3D11_CPU_ACCESS_READ;
|
||||
|
||||
auto status = device->CreateTexture2D(&t, nullptr, &texture);
|
||||
|
||||
if(FAILED(status)) {
|
||||
BOOST_LOG(error) << "Failed to create texture [0x"sv << util::hex(status).to_string_view() << ']';
|
||||
return -1;
|
||||
}
|
||||
|
||||
// map the texture simply to get the pitch and stride
|
||||
status = device_ctx->Map(texture.get(), 0, D3D11_MAP_READ, 0, &img_info);
|
||||
if(FAILED(status)) {
|
||||
BOOST_LOG(error) << "Failed to map the texture [0x"sv << util::hex(status).to_string_view() << ']';
|
||||
return -1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
} // namespace platf::dxgi
|
||||
914
sunshine/platform/windows/display_vram.cpp
Normal file
914
sunshine/platform/windows/display_vram.cpp
Normal file
@@ -0,0 +1,914 @@
|
||||
#include <cmath>
|
||||
|
||||
#include <codecvt>
|
||||
|
||||
#include <d3dcompiler.h>
|
||||
#include <directxmath.h>
|
||||
|
||||
extern "C" {
|
||||
#include <libavcodec/avcodec.h>
|
||||
#include <libavutil/hwcontext_d3d11va.h>
|
||||
}
|
||||
|
||||
#include "display.h"
|
||||
#include "sunshine/main.h"
|
||||
#include "sunshine/video.h"
|
||||
|
||||
|
||||
#define SUNSHINE_SHADERS_DIR SUNSHINE_ASSETS_DIR "/shaders/directx"
|
||||
namespace platf {
|
||||
using namespace std::literals;
|
||||
}
|
||||
|
||||
static void free_frame(AVFrame *frame) {
|
||||
av_frame_free(&frame);
|
||||
}
|
||||
|
||||
using frame_t = util::safe_ptr<AVFrame, free_frame>;
|
||||
|
||||
namespace platf::dxgi {
|
||||
using input_layout_t = util::safe_ptr<ID3D11InputLayout, Release<ID3D11InputLayout>>;
|
||||
using render_target_t = util::safe_ptr<ID3D11RenderTargetView, Release<ID3D11RenderTargetView>>;
|
||||
using shader_res_t = util::safe_ptr<ID3D11ShaderResourceView, Release<ID3D11ShaderResourceView>>;
|
||||
using buf_t = util::safe_ptr<ID3D11Buffer, Release<ID3D11Buffer>>;
|
||||
using blend_t = util::safe_ptr<ID3D11BlendState, Release<ID3D11BlendState>>;
|
||||
using raster_state_t = util::safe_ptr<ID3D11RasterizerState, Release<ID3D11RasterizerState>>;
|
||||
using sampler_state_t = util::safe_ptr<ID3D11SamplerState, Release<ID3D11SamplerState>>;
|
||||
using vs_t = util::safe_ptr<ID3D11VertexShader, Release<ID3D11VertexShader>>;
|
||||
using ps_t = util::safe_ptr<ID3D11PixelShader, Release<ID3D11PixelShader>>;
|
||||
using blob_t = util::safe_ptr<ID3DBlob, Release<ID3DBlob>>;
|
||||
using depth_stencil_state_t = util::safe_ptr<ID3D11DepthStencilState, Release<ID3D11DepthStencilState>>;
|
||||
using depth_stencil_view_t = util::safe_ptr<ID3D11DepthStencilView, Release<ID3D11DepthStencilView>>;
|
||||
|
||||
template<class T>
|
||||
buf_t make_buffer(device_t::pointer device, const T &t) {
|
||||
static_assert(sizeof(T) % 16 == 0, "Buffer needs to be aligned on a 16-byte alignment");
|
||||
|
||||
D3D11_BUFFER_DESC buffer_desc {
|
||||
sizeof(T),
|
||||
D3D11_USAGE_IMMUTABLE,
|
||||
D3D11_BIND_CONSTANT_BUFFER
|
||||
};
|
||||
|
||||
D3D11_SUBRESOURCE_DATA init_data {
|
||||
&t
|
||||
};
|
||||
|
||||
buf_t::pointer buf_p;
|
||||
auto status = device->CreateBuffer(&buffer_desc, &init_data, &buf_p);
|
||||
if(status) {
|
||||
BOOST_LOG(error) << "Failed to create buffer: [0x"sv << util::hex(status).to_string_view() << ']';
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
return buf_t { buf_p };
|
||||
}
|
||||
|
||||
blend_t make_blend(device_t::pointer device, bool enable) {
|
||||
D3D11_BLEND_DESC bdesc {};
|
||||
auto &rt = bdesc.RenderTarget[0];
|
||||
rt.BlendEnable = enable;
|
||||
rt.RenderTargetWriteMask = D3D11_COLOR_WRITE_ENABLE_ALL;
|
||||
|
||||
if(enable) {
|
||||
rt.BlendOp = D3D11_BLEND_OP_ADD;
|
||||
rt.BlendOpAlpha = D3D11_BLEND_OP_ADD;
|
||||
|
||||
rt.SrcBlend = D3D11_BLEND_SRC_ALPHA;
|
||||
rt.DestBlend = D3D11_BLEND_INV_SRC_ALPHA;
|
||||
|
||||
rt.SrcBlendAlpha = D3D11_BLEND_ZERO;
|
||||
rt.DestBlendAlpha = D3D11_BLEND_ZERO;
|
||||
}
|
||||
|
||||
blend_t blend;
|
||||
auto status = device->CreateBlendState(&bdesc, &blend);
|
||||
if(status) {
|
||||
BOOST_LOG(error) << "Failed to create blend state: [0x"sv << util::hex(status).to_string_view() << ']';
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
return blend;
|
||||
}
|
||||
|
||||
blob_t convert_UV_vs_hlsl;
|
||||
blob_t convert_UV_ps_hlsl;
|
||||
blob_t scene_vs_hlsl;
|
||||
blob_t convert_Y_ps_hlsl;
|
||||
blob_t scene_ps_hlsl;
|
||||
|
||||
struct img_d3d_t : public platf::img_t {
|
||||
shader_res_t input_res;
|
||||
texture2d_t texture;
|
||||
std::shared_ptr<platf::display_t> display;
|
||||
|
||||
~img_d3d_t() override = default;
|
||||
};
|
||||
|
||||
util::buffer_t<std::uint8_t> make_cursor_image(util::buffer_t<std::uint8_t> &&img_data, DXGI_OUTDUPL_POINTER_SHAPE_INFO shape_info) {
|
||||
constexpr std::uint32_t black = 0xFF000000;
|
||||
constexpr std::uint32_t white = 0xFFFFFFFF;
|
||||
constexpr std::uint32_t transparent = 0;
|
||||
|
||||
switch(shape_info.Type) {
|
||||
case DXGI_OUTDUPL_POINTER_SHAPE_TYPE_MASKED_COLOR:
|
||||
std::for_each((std::uint32_t *)std::begin(img_data), (std::uint32_t *)std::end(img_data), [](auto &pixel) {
|
||||
if(pixel & 0xFF000000) {
|
||||
pixel = transparent;
|
||||
}
|
||||
});
|
||||
case DXGI_OUTDUPL_POINTER_SHAPE_TYPE_COLOR:
|
||||
return std::move(img_data);
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
shape_info.Height /= 2;
|
||||
|
||||
util::buffer_t<std::uint8_t> cursor_img { shape_info.Width * shape_info.Height * 4 };
|
||||
|
||||
auto bytes = shape_info.Pitch * shape_info.Height;
|
||||
auto pixel_begin = (std::uint32_t *)std::begin(cursor_img);
|
||||
auto pixel_data = pixel_begin;
|
||||
auto and_mask = std::begin(img_data);
|
||||
auto xor_mask = std::begin(img_data) + bytes;
|
||||
|
||||
for(auto x = 0; x < bytes; ++x) {
|
||||
for(auto c = 7; c >= 0; --c) {
|
||||
auto bit = 1 << c;
|
||||
auto color_type = ((*and_mask & bit) ? 1 : 0) + ((*xor_mask & bit) ? 2 : 0);
|
||||
|
||||
switch(color_type) {
|
||||
case 0: //black
|
||||
*pixel_data = black;
|
||||
break;
|
||||
case 2: //white
|
||||
*pixel_data = white;
|
||||
break;
|
||||
case 1: //transparent
|
||||
{
|
||||
*pixel_data = transparent;
|
||||
|
||||
break;
|
||||
}
|
||||
case 3: //inverse
|
||||
{
|
||||
auto top_p = pixel_data - shape_info.Width;
|
||||
auto left_p = pixel_data - 1;
|
||||
auto right_p = pixel_data + 1;
|
||||
auto bottom_p = pixel_data + shape_info.Width;
|
||||
|
||||
// Get the x coordinate of the pixel
|
||||
auto column = (pixel_data - pixel_begin) % shape_info.Width != 0;
|
||||
|
||||
if(top_p >= pixel_begin && *top_p == transparent) {
|
||||
*top_p = black;
|
||||
}
|
||||
|
||||
if(column != 0 && left_p >= pixel_begin && *left_p == transparent) {
|
||||
*left_p = black;
|
||||
}
|
||||
|
||||
if(bottom_p < (std::uint32_t *)std::end(cursor_img)) {
|
||||
*bottom_p = black;
|
||||
}
|
||||
|
||||
if(column != shape_info.Width - 1) {
|
||||
*right_p = black;
|
||||
}
|
||||
*pixel_data = white;
|
||||
}
|
||||
}
|
||||
|
||||
++pixel_data;
|
||||
}
|
||||
++and_mask;
|
||||
++xor_mask;
|
||||
}
|
||||
|
||||
return cursor_img;
|
||||
}
|
||||
|
||||
blob_t compile_shader(LPCSTR file, LPCSTR entrypoint, LPCSTR shader_model) {
|
||||
blob_t::pointer msg_p = nullptr;
|
||||
blob_t::pointer compiled_p;
|
||||
|
||||
DWORD flags = D3DCOMPILE_ENABLE_STRICTNESS;
|
||||
|
||||
#ifndef NDEBUG
|
||||
flags |= D3DCOMPILE_DEBUG | D3DCOMPILE_SKIP_OPTIMIZATION;
|
||||
#endif
|
||||
std::wstring_convert<std::codecvt_utf8_utf16<wchar_t>, wchar_t> converter;
|
||||
|
||||
auto wFile = converter.from_bytes(file);
|
||||
auto status = D3DCompileFromFile(wFile.c_str(), nullptr, nullptr, entrypoint, shader_model, flags, 0, &compiled_p, &msg_p);
|
||||
|
||||
if(msg_p) {
|
||||
BOOST_LOG(warning) << std::string_view { (const char *)msg_p->GetBufferPointer(), msg_p->GetBufferSize() - 1 };
|
||||
msg_p->Release();
|
||||
}
|
||||
|
||||
if(status) {
|
||||
BOOST_LOG(error) << "Couldn't compile ["sv << file << "] [0x"sv << util::hex(status).to_string_view() << ']';
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
return blob_t { compiled_p };
|
||||
}
|
||||
|
||||
blob_t compile_pixel_shader(LPCSTR file) {
|
||||
return compile_shader(file, "main_ps", "ps_5_0");
|
||||
}
|
||||
|
||||
blob_t compile_vertex_shader(LPCSTR file) {
|
||||
return compile_shader(file, "main_vs", "vs_5_0");
|
||||
}
|
||||
|
||||
class hwdevice_t : public platf::hwdevice_t {
|
||||
public:
|
||||
hwdevice_t(std::vector<hwdevice_t *> *hwdevices_p) : hwdevices_p { hwdevices_p } {}
|
||||
hwdevice_t() = delete;
|
||||
|
||||
void set_cursor_pos(LONG rel_x, LONG rel_y, bool visible) {
|
||||
cursor_visible = visible;
|
||||
|
||||
if(!visible) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto x = ((float)rel_x);
|
||||
auto y = ((float)rel_y);
|
||||
|
||||
cursor_view.TopLeftX = x;
|
||||
cursor_view.TopLeftY = y;
|
||||
}
|
||||
|
||||
int set_cursor_texture(texture2d_t::pointer texture, LONG width, LONG height) {
|
||||
auto device = (device_t::pointer)data;
|
||||
|
||||
cursor_view.Width = width;
|
||||
cursor_view.Height = height;
|
||||
|
||||
D3D11_SHADER_RESOURCE_VIEW_DESC desc {
|
||||
DXGI_FORMAT_B8G8R8A8_UNORM,
|
||||
D3D11_SRV_DIMENSION_TEXTURE2D
|
||||
};
|
||||
desc.Texture2D.MipLevels = 1;
|
||||
|
||||
auto status = device->CreateShaderResourceView(texture, &desc, &img.input_res);
|
||||
if(FAILED(status)) {
|
||||
BOOST_LOG(error) << "Failed to create cursor shader resource view [0x"sv << util::hex(status).to_string_view() << ']';
|
||||
return -1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int convert(platf::img_t &img_base) override {
|
||||
auto &img = (img_d3d_t &)img_base;
|
||||
|
||||
if(!img.input_res) {
|
||||
auto device = (device_t::pointer)data;
|
||||
|
||||
D3D11_SHADER_RESOURCE_VIEW_DESC desc {
|
||||
DXGI_FORMAT_B8G8R8A8_UNORM,
|
||||
D3D11_SRV_DIMENSION_TEXTURE2D
|
||||
};
|
||||
desc.Texture2D.MipLevels = 1;
|
||||
|
||||
auto status = device->CreateShaderResourceView(img.texture.get(), &desc, &img.input_res);
|
||||
if(FAILED(status)) {
|
||||
BOOST_LOG(error) << "Failed to create input shader resource view [0x"sv << util::hex(status).to_string_view() << ']';
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
auto input_res_p = img.input_res.get();
|
||||
|
||||
if(cursor_visible) {
|
||||
_init_view_port(img.width, img.height);
|
||||
|
||||
device_ctx_p->OMSetRenderTargets(1, &scene_rt, nullptr);
|
||||
device_ctx_p->VSSetShader(scene_vs.get(), nullptr, 0);
|
||||
device_ctx_p->PSSetShader(scene_ps.get(), nullptr, 0);
|
||||
device_ctx_p->PSSetShaderResources(0, 1, &input_res_p);
|
||||
|
||||
device_ctx_p->Draw(3, 0);
|
||||
|
||||
device_ctx_p->OMSetBlendState(blend_enable.get(), nullptr, 0xFFFFFFFFu);
|
||||
device_ctx_p->RSSetViewports(1, &cursor_view);
|
||||
device_ctx_p->PSSetShaderResources(0, 1, &this->img.input_res);
|
||||
device_ctx_p->Draw(3, 0);
|
||||
device_ctx_p->OMSetBlendState(blend_disable.get(), nullptr, 0xFFFFFFFFu);
|
||||
|
||||
input_res_p = scene_sr.get();
|
||||
}
|
||||
|
||||
_init_view_port(this->img.width, this->img.height);
|
||||
device_ctx_p->OMSetRenderTargets(1, &nv12_Y_rt, nullptr);
|
||||
device_ctx_p->VSSetShader(scene_vs.get(), nullptr, 0);
|
||||
device_ctx_p->PSSetShader(convert_Y_ps.get(), nullptr, 0);
|
||||
device_ctx_p->PSSetShaderResources(0, 1, &back_img.input_res);
|
||||
device_ctx_p->Draw(3, 0);
|
||||
|
||||
device_ctx_p->RSSetViewports(1, &outY_view);
|
||||
device_ctx_p->PSSetShaderResources(0, 1, &input_res_p);
|
||||
device_ctx_p->Draw(3, 0);
|
||||
|
||||
// Artifacts start appearing on the rendered image if Sunshine doesn't flush
|
||||
// before rendering on the UV part of the image.
|
||||
device_ctx_p->Flush();
|
||||
|
||||
_init_view_port(this->img.width / 2, this->img.height / 2);
|
||||
device_ctx_p->OMSetRenderTargets(1, &nv12_UV_rt, nullptr);
|
||||
device_ctx_p->VSSetShader(convert_UV_vs.get(), nullptr, 0);
|
||||
device_ctx_p->PSSetShader(convert_UV_ps.get(), nullptr, 0);
|
||||
device_ctx_p->PSSetShaderResources(0, 1, &back_img.input_res);
|
||||
device_ctx_p->Draw(3, 0);
|
||||
|
||||
device_ctx_p->RSSetViewports(1, &outUV_view);
|
||||
device_ctx_p->PSSetShaderResources(0, 1, &input_res_p);
|
||||
device_ctx_p->Draw(3, 0);
|
||||
device_ctx_p->Flush();
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
void set_colorspace(std::uint32_t colorspace, std::uint32_t color_range) override {
|
||||
switch(colorspace) {
|
||||
case 5: // SWS_CS_SMPTE170M
|
||||
color_p = &::video::colors[0];
|
||||
break;
|
||||
case 1: // SWS_CS_ITU709
|
||||
color_p = &::video::colors[2];
|
||||
break;
|
||||
case 9: // SWS_CS_BT2020
|
||||
default:
|
||||
BOOST_LOG(warning) << "Colorspace: ["sv << colorspace << "] not yet supported: switching to default"sv;
|
||||
color_p = &::video::colors[0];
|
||||
};
|
||||
|
||||
if(color_range > 1) {
|
||||
// Full range
|
||||
++color_p;
|
||||
}
|
||||
|
||||
auto color_matrix = make_buffer((device_t::pointer)data, *color_p);
|
||||
if(!color_matrix) {
|
||||
BOOST_LOG(warning) << "Failed to create color matrix"sv;
|
||||
return;
|
||||
}
|
||||
|
||||
device_ctx_p->PSSetConstantBuffers(0, 1, &color_matrix);
|
||||
this->color_matrix = std::move(color_matrix);
|
||||
}
|
||||
|
||||
int set_frame(AVFrame *frame) {
|
||||
this->hwframe.reset(frame);
|
||||
this->frame = frame;
|
||||
|
||||
auto device_p = (device_t::pointer)data;
|
||||
|
||||
auto out_width = frame->width;
|
||||
auto out_height = frame->height;
|
||||
|
||||
float in_width = img.display->width;
|
||||
float in_height = img.display->height;
|
||||
|
||||
// // Ensure aspect ratio is maintained
|
||||
auto scalar = std::fminf(out_width / in_width, out_height / in_height);
|
||||
auto out_width_f = in_width * scalar;
|
||||
auto out_height_f = in_height * scalar;
|
||||
|
||||
// result is always positive
|
||||
auto offsetX = (out_width - out_width_f) / 2;
|
||||
auto offsetY = (out_height - out_height_f) / 2;
|
||||
|
||||
outY_view = D3D11_VIEWPORT { offsetX, offsetY, out_width_f, out_height_f, 0.0f, 1.0f };
|
||||
outUV_view = D3D11_VIEWPORT { offsetX / 2, offsetY / 2, out_width_f / 2, out_height_f / 2, 0.0f, 1.0f };
|
||||
|
||||
D3D11_TEXTURE2D_DESC t {};
|
||||
t.Width = out_width;
|
||||
t.Height = out_height;
|
||||
t.MipLevels = 1;
|
||||
t.ArraySize = 1;
|
||||
t.SampleDesc.Count = 1;
|
||||
t.Usage = D3D11_USAGE_DEFAULT;
|
||||
t.Format = format;
|
||||
t.BindFlags = D3D11_BIND_RENDER_TARGET;
|
||||
|
||||
auto status = device_p->CreateTexture2D(&t, nullptr, &img.texture);
|
||||
if(FAILED(status)) {
|
||||
BOOST_LOG(error) << "Failed to create render target texture [0x"sv << util::hex(status).to_string_view() << ']';
|
||||
return -1;
|
||||
}
|
||||
|
||||
img.width = out_width;
|
||||
img.height = out_height;
|
||||
img.data = (std::uint8_t *)img.texture.get();
|
||||
img.row_pitch = out_width;
|
||||
img.pixel_pitch = 1;
|
||||
|
||||
float info_in[16 / sizeof(float)] { 1.0f / (float)out_width }; //aligned to 16-byte
|
||||
info_scene = make_buffer(device_p, info_in);
|
||||
|
||||
if(!info_in) {
|
||||
BOOST_LOG(error) << "Failed to create info scene buffer"sv;
|
||||
return -1;
|
||||
}
|
||||
|
||||
D3D11_RENDER_TARGET_VIEW_DESC nv12_rt_desc {
|
||||
DXGI_FORMAT_R8_UNORM,
|
||||
D3D11_RTV_DIMENSION_TEXTURE2D
|
||||
};
|
||||
|
||||
status = device_p->CreateRenderTargetView(img.texture.get(), &nv12_rt_desc, &nv12_Y_rt);
|
||||
if(FAILED(status)) {
|
||||
BOOST_LOG(error) << "Failed to create render target view [0x"sv << util::hex(status).to_string_view() << ']';
|
||||
return -1;
|
||||
}
|
||||
|
||||
nv12_rt_desc.Format = DXGI_FORMAT_R8G8_UNORM;
|
||||
status = device_p->CreateRenderTargetView(img.texture.get(), &nv12_rt_desc, &nv12_UV_rt);
|
||||
if(FAILED(status)) {
|
||||
BOOST_LOG(error) << "Failed to create render target view [0x"sv << util::hex(status).to_string_view() << ']';
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Need to have something refcounted
|
||||
if(!frame->buf[0]) {
|
||||
frame->buf[0] = av_buffer_allocz(sizeof(AVD3D11FrameDescriptor));
|
||||
}
|
||||
|
||||
auto desc = (AVD3D11FrameDescriptor *)frame->buf[0]->data;
|
||||
desc->texture = (ID3D11Texture2D *)img.data;
|
||||
desc->index = 0;
|
||||
|
||||
frame->data[0] = img.data;
|
||||
frame->data[1] = 0;
|
||||
|
||||
frame->linesize[0] = img.row_pitch;
|
||||
|
||||
frame->height = img.height;
|
||||
frame->width = img.width;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int init(
|
||||
std::shared_ptr<platf::display_t> display, device_t::pointer device_p, device_ctx_t::pointer device_ctx_p,
|
||||
pix_fmt_e pix_fmt) {
|
||||
|
||||
HRESULT status;
|
||||
|
||||
device_p->AddRef();
|
||||
data = device_p;
|
||||
|
||||
this->device_ctx_p = device_ctx_p;
|
||||
|
||||
cursor_visible = false;
|
||||
cursor_view.MinDepth = 0.0f;
|
||||
cursor_view.MaxDepth = 1.0f;
|
||||
|
||||
format = (pix_fmt == pix_fmt_e::nv12 ? DXGI_FORMAT_NV12 : DXGI_FORMAT_P010);
|
||||
status = device_p->CreateVertexShader(scene_vs_hlsl->GetBufferPointer(), scene_vs_hlsl->GetBufferSize(), nullptr, &scene_vs);
|
||||
if(status) {
|
||||
BOOST_LOG(error) << "Failed to create scene vertex shader [0x"sv << util::hex(status).to_string_view() << ']';
|
||||
return -1;
|
||||
}
|
||||
|
||||
status = device_p->CreatePixelShader(convert_Y_ps_hlsl->GetBufferPointer(), convert_Y_ps_hlsl->GetBufferSize(), nullptr, &convert_Y_ps);
|
||||
if(status) {
|
||||
BOOST_LOG(error) << "Failed to create convertY pixel shader [0x"sv << util::hex(status).to_string_view() << ']';
|
||||
return -1;
|
||||
}
|
||||
|
||||
status = device_p->CreatePixelShader(convert_UV_ps_hlsl->GetBufferPointer(), convert_UV_ps_hlsl->GetBufferSize(), nullptr, &convert_UV_ps);
|
||||
if(status) {
|
||||
BOOST_LOG(error) << "Failed to create convertUV pixel shader [0x"sv << util::hex(status).to_string_view() << ']';
|
||||
return -1;
|
||||
}
|
||||
|
||||
status = device_p->CreateVertexShader(convert_UV_vs_hlsl->GetBufferPointer(), convert_UV_vs_hlsl->GetBufferSize(), nullptr, &convert_UV_vs);
|
||||
if(status) {
|
||||
BOOST_LOG(error) << "Failed to create convertUV vertex shader [0x"sv << util::hex(status).to_string_view() << ']';
|
||||
return -1;
|
||||
}
|
||||
|
||||
status = device_p->CreatePixelShader(scene_ps_hlsl->GetBufferPointer(), scene_ps_hlsl->GetBufferSize(), nullptr, &scene_ps);
|
||||
if(status) {
|
||||
BOOST_LOG(error) << "Failed to create scene pixel shader [0x"sv << util::hex(status).to_string_view() << ']';
|
||||
return -1;
|
||||
}
|
||||
|
||||
blend_disable = make_blend(device_p, false);
|
||||
blend_enable = make_blend(device_p, true);
|
||||
|
||||
if(!blend_disable || !blend_enable) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if(_init_rt(scene_sr, scene_rt, display->width, display->height, DXGI_FORMAT_B8G8R8A8_UNORM)) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
color_matrix = make_buffer(device_p, ::video::colors[0]);
|
||||
if(!color_matrix) {
|
||||
BOOST_LOG(error) << "Failed to create color matrix buffer"sv;
|
||||
return -1;
|
||||
}
|
||||
|
||||
D3D11_INPUT_ELEMENT_DESC layout_desc {
|
||||
"SV_Position", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 0, D3D11_INPUT_PER_VERTEX_DATA, 0
|
||||
};
|
||||
|
||||
status = device_p->CreateInputLayout(
|
||||
&layout_desc, 1,
|
||||
convert_UV_vs_hlsl->GetBufferPointer(), convert_UV_vs_hlsl->GetBufferSize(),
|
||||
&input_layout);
|
||||
|
||||
img.display = std::move(display);
|
||||
|
||||
D3D11_SAMPLER_DESC sampler_desc {};
|
||||
sampler_desc.Filter = D3D11_FILTER_MIN_MAG_MIP_LINEAR;
|
||||
sampler_desc.AddressU = D3D11_TEXTURE_ADDRESS_CLAMP;
|
||||
sampler_desc.AddressV = D3D11_TEXTURE_ADDRESS_CLAMP;
|
||||
sampler_desc.AddressW = D3D11_TEXTURE_ADDRESS_WRAP;
|
||||
sampler_desc.ComparisonFunc = D3D11_COMPARISON_NEVER;
|
||||
sampler_desc.MinLOD = 0;
|
||||
sampler_desc.MaxLOD = D3D11_FLOAT32_MAX;
|
||||
|
||||
status = device_p->CreateSamplerState(&sampler_desc, &sampler_linear);
|
||||
if(FAILED(status)) {
|
||||
BOOST_LOG(error) << "Failed to create point sampler state [0x"sv << util::hex(status).to_string_view() << ']';
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Color the background black, so that the padding for keeping the aspect ratio
|
||||
// is black
|
||||
if(img.display->dummy_img(&back_img)) {
|
||||
BOOST_LOG(warning) << "Couldn't create an image to set background color to black"sv;
|
||||
return -1;
|
||||
}
|
||||
|
||||
D3D11_SHADER_RESOURCE_VIEW_DESC desc {
|
||||
DXGI_FORMAT_B8G8R8A8_UNORM,
|
||||
D3D11_SRV_DIMENSION_TEXTURE2D
|
||||
};
|
||||
desc.Texture2D.MipLevels = 1;
|
||||
|
||||
status = device_p->CreateShaderResourceView(back_img.texture.get(), &desc, &back_img.input_res);
|
||||
if(FAILED(status)) {
|
||||
BOOST_LOG(error) << "Failed to create input shader resource view [0x"sv << util::hex(status).to_string_view() << ']';
|
||||
return -1;
|
||||
}
|
||||
|
||||
device_ctx_p->OMSetBlendState(blend_disable.get(), nullptr, 0xFFFFFFFFu);
|
||||
device_ctx_p->PSSetSamplers(0, 1, &sampler_linear);
|
||||
device_ctx_p->PSSetConstantBuffers(0, 1, &color_matrix);
|
||||
device_ctx_p->VSSetConstantBuffers(0, 1, &info_scene);
|
||||
device_ctx_p->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLESTRIP);
|
||||
device_ctx_p->IASetInputLayout(input_layout.get());
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
~hwdevice_t() override {
|
||||
if(data) {
|
||||
((ID3D11Device *)data)->Release();
|
||||
}
|
||||
|
||||
auto it = std::find(std::begin(*hwdevices_p), std::end(*hwdevices_p), this);
|
||||
if(it != std::end(*hwdevices_p)) {
|
||||
hwdevices_p->erase(it);
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
void _init_view_port(float x, float y, float width, float height) {
|
||||
D3D11_VIEWPORT view {
|
||||
x, y,
|
||||
width, height,
|
||||
0.0f, 1.0f
|
||||
};
|
||||
|
||||
device_ctx_p->RSSetViewports(1, &view);
|
||||
}
|
||||
|
||||
void _init_view_port(float width, float height) {
|
||||
_init_view_port(0.0f, 0.0f, width, height);
|
||||
}
|
||||
|
||||
int _init_rt(shader_res_t &shader_res, render_target_t &render_target, int width, int height, DXGI_FORMAT format) {
|
||||
D3D11_TEXTURE2D_DESC desc {};
|
||||
|
||||
desc.Width = width;
|
||||
desc.Height = height;
|
||||
desc.Format = format;
|
||||
desc.Usage = D3D11_USAGE_DEFAULT;
|
||||
desc.BindFlags = D3D11_BIND_RENDER_TARGET | D3D11_BIND_SHADER_RESOURCE;
|
||||
desc.MipLevels = 1;
|
||||
desc.ArraySize = 1;
|
||||
desc.SampleDesc.Count = 1;
|
||||
|
||||
auto device = (device_t::pointer)data;
|
||||
|
||||
texture2d_t tex;
|
||||
auto status = device->CreateTexture2D(&desc, nullptr, &tex);
|
||||
if(status) {
|
||||
BOOST_LOG(error) << "Failed to create render target texture for luma [0x"sv << util::hex(status).to_string_view() << ']';
|
||||
return -1;
|
||||
}
|
||||
|
||||
|
||||
D3D11_SHADER_RESOURCE_VIEW_DESC shader_resource_desc {
|
||||
format,
|
||||
D3D11_SRV_DIMENSION_TEXTURE2D
|
||||
};
|
||||
shader_resource_desc.Texture2D.MipLevels = 1;
|
||||
|
||||
device->CreateShaderResourceView(tex.get(), &shader_resource_desc, &shader_res);
|
||||
if(status) {
|
||||
BOOST_LOG(error) << "Failed to create render target texture for luma [0x"sv << util::hex(status).to_string_view() << ']';
|
||||
return -1;
|
||||
}
|
||||
|
||||
D3D11_RENDER_TARGET_VIEW_DESC render_target_desc {
|
||||
format,
|
||||
D3D11_RTV_DIMENSION_TEXTURE2D
|
||||
};
|
||||
|
||||
device->CreateRenderTargetView(tex.get(), &render_target_desc, &render_target);
|
||||
if(status) {
|
||||
BOOST_LOG(error) << "Failed to create render target view [0x"sv << util::hex(status).to_string_view() << ']';
|
||||
return -1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
public:
|
||||
frame_t hwframe;
|
||||
|
||||
::video::color_t *color_p;
|
||||
|
||||
blend_t blend_enable;
|
||||
blend_t blend_disable;
|
||||
|
||||
buf_t info_scene;
|
||||
buf_t color_matrix;
|
||||
|
||||
sampler_state_t sampler_linear;
|
||||
|
||||
input_layout_t input_layout;
|
||||
|
||||
render_target_t nv12_Y_rt;
|
||||
render_target_t nv12_UV_rt;
|
||||
|
||||
render_target_t scene_rt;
|
||||
shader_res_t scene_sr;
|
||||
|
||||
img_d3d_t img;
|
||||
|
||||
// Clear nv12 render target to black
|
||||
img_d3d_t back_img;
|
||||
|
||||
vs_t convert_UV_vs;
|
||||
ps_t convert_UV_ps;
|
||||
ps_t convert_Y_ps;
|
||||
ps_t scene_ps;
|
||||
vs_t scene_vs;
|
||||
|
||||
D3D11_VIEWPORT outY_view;
|
||||
D3D11_VIEWPORT outUV_view;
|
||||
|
||||
D3D11_VIEWPORT cursor_view;
|
||||
bool cursor_visible;
|
||||
|
||||
DXGI_FORMAT format;
|
||||
|
||||
device_ctx_t::pointer device_ctx_p;
|
||||
|
||||
// The destructor will remove itself from the list of hardware devices, this is done synchronously
|
||||
std::vector<hwdevice_t *> *hwdevices_p;
|
||||
};
|
||||
|
||||
capture_e display_vram_t::snapshot(platf::img_t *img_base, std::chrono::milliseconds timeout, bool cursor_visible) {
|
||||
auto img = (img_d3d_t *)img_base;
|
||||
|
||||
HRESULT status;
|
||||
|
||||
DXGI_OUTDUPL_FRAME_INFO frame_info;
|
||||
|
||||
resource_t::pointer res_p {};
|
||||
auto capture_status = dup.next_frame(frame_info, timeout, &res_p);
|
||||
resource_t res { res_p };
|
||||
|
||||
if(capture_status != capture_e::ok) {
|
||||
return capture_status;
|
||||
}
|
||||
|
||||
const bool mouse_update_flag = frame_info.LastMouseUpdateTime.QuadPart != 0 || frame_info.PointerShapeBufferSize > 0;
|
||||
const bool frame_update_flag = frame_info.AccumulatedFrames != 0 || frame_info.LastPresentTime.QuadPart != 0;
|
||||
const bool update_flag = mouse_update_flag || frame_update_flag;
|
||||
|
||||
if(!update_flag) {
|
||||
return capture_e::timeout;
|
||||
}
|
||||
|
||||
if(frame_info.PointerShapeBufferSize > 0) {
|
||||
DXGI_OUTDUPL_POINTER_SHAPE_INFO shape_info {};
|
||||
|
||||
util::buffer_t<std::uint8_t> img_data { frame_info.PointerShapeBufferSize };
|
||||
|
||||
UINT dummy;
|
||||
status = dup.dup->GetFramePointerShape(img_data.size(), std::begin(img_data), &dummy, &shape_info);
|
||||
if(FAILED(status)) {
|
||||
BOOST_LOG(error) << "Failed to get new pointer shape [0x"sv << util::hex(status).to_string_view() << ']';
|
||||
|
||||
return capture_e::error;
|
||||
}
|
||||
|
||||
auto cursor_img = make_cursor_image(std::move(img_data), shape_info);
|
||||
|
||||
D3D11_SUBRESOURCE_DATA data {
|
||||
std::begin(cursor_img),
|
||||
4 * shape_info.Width,
|
||||
0
|
||||
};
|
||||
|
||||
// Create texture for cursor
|
||||
D3D11_TEXTURE2D_DESC t {};
|
||||
t.Width = shape_info.Width;
|
||||
t.Height = cursor_img.size() / data.SysMemPitch;
|
||||
t.MipLevels = 1;
|
||||
t.ArraySize = 1;
|
||||
t.SampleDesc.Count = 1;
|
||||
t.Usage = D3D11_USAGE_DEFAULT;
|
||||
t.Format = DXGI_FORMAT_B8G8R8A8_UNORM;
|
||||
t.BindFlags = D3D11_BIND_SHADER_RESOURCE;
|
||||
|
||||
texture2d_t texture;
|
||||
auto status = device->CreateTexture2D(&t, &data, &texture);
|
||||
if(FAILED(status)) {
|
||||
BOOST_LOG(error) << "Failed to create mouse texture [0x"sv << util::hex(status).to_string_view() << ']';
|
||||
return capture_e::error;
|
||||
}
|
||||
|
||||
for(auto *hwdevice : hwdevices) {
|
||||
if(hwdevice->set_cursor_texture(texture.get(), t.Width, t.Height)) {
|
||||
return capture_e::error;
|
||||
}
|
||||
}
|
||||
|
||||
cursor.texture = std::move(texture);
|
||||
cursor.width = t.Width;
|
||||
cursor.height = t.Height;
|
||||
}
|
||||
|
||||
if(frame_info.LastMouseUpdateTime.QuadPart) {
|
||||
for(auto *hwdevice : hwdevices) {
|
||||
hwdevice->set_cursor_pos(frame_info.PointerPosition.Position.x, frame_info.PointerPosition.Position.y, frame_info.PointerPosition.Visible && cursor_visible);
|
||||
}
|
||||
}
|
||||
|
||||
if(frame_update_flag) {
|
||||
texture2d_t src;
|
||||
status = res->QueryInterface(IID_ID3D11Texture2D, (void **)&src);
|
||||
|
||||
if(FAILED(status)) {
|
||||
BOOST_LOG(error) << "Couldn't query interface [0x"sv << util::hex(status).to_string_view() << ']';
|
||||
return capture_e::error;
|
||||
}
|
||||
|
||||
device_ctx->CopyResource(img->texture.get(), src.get());
|
||||
}
|
||||
|
||||
return capture_e::ok;
|
||||
}
|
||||
|
||||
std::shared_ptr<platf::img_t> display_vram_t::alloc_img() {
|
||||
auto img = std::make_shared<img_d3d_t>();
|
||||
|
||||
D3D11_TEXTURE2D_DESC t {};
|
||||
t.Width = width;
|
||||
t.Height = height;
|
||||
t.MipLevels = 1;
|
||||
t.ArraySize = 1;
|
||||
t.SampleDesc.Count = 1;
|
||||
t.Usage = D3D11_USAGE_DEFAULT;
|
||||
t.Format = format;
|
||||
t.BindFlags = D3D11_BIND_SHADER_RESOURCE;
|
||||
|
||||
auto status = device->CreateTexture2D(&t, nullptr, &img->texture);
|
||||
if(FAILED(status)) {
|
||||
BOOST_LOG(error) << "Failed to create img buf texture [0x"sv << util::hex(status).to_string_view() << ']';
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
img->data = (std::uint8_t *)img->texture.get();
|
||||
img->row_pitch = 0;
|
||||
img->pixel_pitch = 4;
|
||||
img->width = 0;
|
||||
img->height = 0;
|
||||
img->display = shared_from_this();
|
||||
|
||||
return img;
|
||||
}
|
||||
|
||||
int display_vram_t::dummy_img(platf::img_t *img_base) {
|
||||
auto img = (img_d3d_t *)img_base;
|
||||
|
||||
img->row_pitch = width * 4;
|
||||
auto dummy_data = std::make_unique<int[]>(width * height);
|
||||
D3D11_SUBRESOURCE_DATA data {
|
||||
dummy_data.get(),
|
||||
(UINT)img->row_pitch
|
||||
};
|
||||
std::fill_n(dummy_data.get(), width * height, 0);
|
||||
|
||||
D3D11_TEXTURE2D_DESC t {};
|
||||
t.Width = width;
|
||||
t.Height = height;
|
||||
t.MipLevels = 1;
|
||||
t.ArraySize = 1;
|
||||
t.SampleDesc.Count = 1;
|
||||
t.Usage = D3D11_USAGE_DEFAULT;
|
||||
t.Format = format;
|
||||
t.BindFlags = D3D11_BIND_SHADER_RESOURCE;
|
||||
|
||||
dxgi::texture2d_t tex;
|
||||
auto status = device->CreateTexture2D(&t, &data, &tex);
|
||||
if(FAILED(status)) {
|
||||
BOOST_LOG(error) << "Failed to create dummy texture [0x"sv << util::hex(status).to_string_view() << ']';
|
||||
return -1;
|
||||
}
|
||||
|
||||
img->texture = std::move(tex);
|
||||
img->data = (std::uint8_t *)img->texture.get();
|
||||
img->height = height;
|
||||
img->width = width;
|
||||
img->pixel_pitch = 4;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
std::shared_ptr<platf::hwdevice_t> display_vram_t::make_hwdevice(pix_fmt_e pix_fmt) {
|
||||
if(pix_fmt != platf::pix_fmt_e::nv12) {
|
||||
BOOST_LOG(error) << "display_vram_t doesn't support pixel format ["sv << from_pix_fmt(pix_fmt) << ']';
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
auto hwdevice = std::make_shared<hwdevice_t>(&hwdevices);
|
||||
|
||||
auto ret = hwdevice->init(
|
||||
shared_from_this(),
|
||||
device.get(),
|
||||
device_ctx.get(),
|
||||
pix_fmt);
|
||||
|
||||
if(ret) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
if(cursor.texture && hwdevice->set_cursor_texture(cursor.texture.get(), cursor.width, cursor.height)) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
hwdevices.emplace_back(hwdevice.get());
|
||||
|
||||
return hwdevice;
|
||||
}
|
||||
|
||||
int init() {
|
||||
BOOST_LOG(info) << "Compiling shaders..."sv;
|
||||
scene_vs_hlsl = compile_vertex_shader(SUNSHINE_SHADERS_DIR "/SceneVS.hlsl");
|
||||
if(!scene_vs_hlsl) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
convert_Y_ps_hlsl = compile_pixel_shader(SUNSHINE_SHADERS_DIR "/ConvertYPS.hlsl");
|
||||
if(!convert_Y_ps_hlsl) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
convert_UV_ps_hlsl = compile_pixel_shader(SUNSHINE_SHADERS_DIR "/ConvertUVPS.hlsl");
|
||||
if(!convert_UV_ps_hlsl) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
convert_UV_vs_hlsl = compile_vertex_shader(SUNSHINE_SHADERS_DIR "/ConvertUVVS.hlsl");
|
||||
if(!convert_UV_vs_hlsl) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
scene_ps_hlsl = compile_pixel_shader(SUNSHINE_SHADERS_DIR "/ScenePS.hlsl");
|
||||
if(!scene_ps_hlsl) {
|
||||
return -1;
|
||||
}
|
||||
BOOST_LOG(info) << "Compiled shaders"sv;
|
||||
|
||||
return 0;
|
||||
}
|
||||
} // namespace platf::dxgi
|
||||
312
sunshine/platform/windows/input.cpp
Executable file
312
sunshine/platform/windows/input.cpp
Executable file
@@ -0,0 +1,312 @@
|
||||
#include <windows.h>
|
||||
|
||||
#include <cmath>
|
||||
|
||||
#include <ViGEm/Client.h>
|
||||
|
||||
#include "sunshine/main.h"
|
||||
#include "sunshine/platform/common.h"
|
||||
|
||||
namespace platf {
|
||||
using namespace std::literals;
|
||||
|
||||
volatile HDESK _lastKnownInputDesktop = NULL;
|
||||
constexpr touch_port_t target_touch_port {
|
||||
0, 0,
|
||||
65535, 65535
|
||||
};
|
||||
|
||||
HDESK pairInputDesktop();
|
||||
|
||||
class vigem_t {
|
||||
public:
|
||||
using client_t = util::safe_ptr<_VIGEM_CLIENT_T, vigem_free>;
|
||||
using target_t = util::safe_ptr<_VIGEM_TARGET_T, vigem_target_free>;
|
||||
|
||||
int init() {
|
||||
VIGEM_ERROR status;
|
||||
|
||||
client.reset(vigem_alloc());
|
||||
|
||||
status = vigem_connect(client.get());
|
||||
if(!VIGEM_SUCCESS(status)) {
|
||||
BOOST_LOG(warning) << "Couldn't setup connection to ViGEm for gamepad support ["sv << util::hex(status).to_string_view() << ']';
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
x360s.resize(MAX_GAMEPADS);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int alloc_x360(int nr) {
|
||||
auto &x360 = x360s[nr];
|
||||
assert(!x360);
|
||||
|
||||
x360.reset(vigem_target_x360_alloc());
|
||||
auto status = vigem_target_add(client.get(), x360.get());
|
||||
if(!VIGEM_SUCCESS(status)) {
|
||||
BOOST_LOG(error) << "Couldn't add Gamepad to ViGEm connection ["sv << util::hex(status).to_string_view() << ']';
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
void free_target(int nr) {
|
||||
auto &x360 = x360s[nr];
|
||||
|
||||
if(x360 && vigem_target_is_attached(x360.get())) {
|
||||
auto status = vigem_target_remove(client.get(), x360.get());
|
||||
if(!VIGEM_SUCCESS(status)) {
|
||||
BOOST_LOG(warning) << "Couldn't detach gamepad from ViGEm ["sv << util::hex(status).to_string_view() << ']';
|
||||
}
|
||||
}
|
||||
|
||||
x360.reset();
|
||||
}
|
||||
|
||||
~vigem_t() {
|
||||
if(client) {
|
||||
for(auto &x360 : x360s) {
|
||||
if(x360 && vigem_target_is_attached(x360.get())) {
|
||||
auto status = vigem_target_remove(client.get(), x360.get());
|
||||
if(!VIGEM_SUCCESS(status)) {
|
||||
BOOST_LOG(warning) << "Couldn't detach gamepad from ViGEm ["sv << util::hex(status).to_string_view() << ']';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
vigem_disconnect(client.get());
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<target_t> x360s;
|
||||
client_t client;
|
||||
};
|
||||
|
||||
input_t input() {
|
||||
input_t result { new vigem_t {} };
|
||||
|
||||
auto vigem = (vigem_t *)result.get();
|
||||
if(vigem->init()) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
void send_input(INPUT &i) {
|
||||
retry:
|
||||
auto send = SendInput(1, &i, sizeof(INPUT));
|
||||
if(send != 1) {
|
||||
auto hDesk = pairInputDesktop();
|
||||
if(_lastKnownInputDesktop != hDesk) {
|
||||
_lastKnownInputDesktop = hDesk;
|
||||
goto retry;
|
||||
}
|
||||
BOOST_LOG(warning) << "Couldn't send input"sv;
|
||||
}
|
||||
}
|
||||
void abs_mouse(input_t &input, const touch_port_t &touch_port, float x, float y) {
|
||||
INPUT i {};
|
||||
|
||||
i.type = INPUT_MOUSE;
|
||||
auto &mi = i.mi;
|
||||
|
||||
mi.dwFlags =
|
||||
MOUSEEVENTF_MOVE |
|
||||
MOUSEEVENTF_ABSOLUTE |
|
||||
|
||||
// MOUSEEVENTF_VIRTUALDESK maps to the entirety of the desktop rather than the primary desktop
|
||||
MOUSEEVENTF_VIRTUALDESK;
|
||||
|
||||
auto scaled_x = std::lround((x + touch_port.offset_x) * ((float)target_touch_port.width / (float)touch_port.width));
|
||||
auto scaled_y = std::lround((y + touch_port.offset_y) * ((float)target_touch_port.height / (float)touch_port.height));
|
||||
|
||||
mi.dx = scaled_x;
|
||||
mi.dy = scaled_y;
|
||||
|
||||
send_input(i);
|
||||
}
|
||||
|
||||
void move_mouse(input_t &input, int deltaX, int deltaY) {
|
||||
INPUT i {};
|
||||
|
||||
i.type = INPUT_MOUSE;
|
||||
auto &mi = i.mi;
|
||||
|
||||
mi.dwFlags = MOUSEEVENTF_MOVE;
|
||||
mi.dx = deltaX;
|
||||
mi.dy = deltaY;
|
||||
|
||||
send_input(i);
|
||||
}
|
||||
|
||||
void button_mouse(input_t &input, int button, bool release) {
|
||||
constexpr auto KEY_STATE_DOWN = (SHORT)0x8000;
|
||||
|
||||
INPUT i {};
|
||||
|
||||
i.type = INPUT_MOUSE;
|
||||
auto &mi = i.mi;
|
||||
|
||||
int mouse_button;
|
||||
if(button == 1) {
|
||||
mi.dwFlags = release ? MOUSEEVENTF_LEFTUP : MOUSEEVENTF_LEFTDOWN;
|
||||
mouse_button = VK_LBUTTON;
|
||||
}
|
||||
else if(button == 2) {
|
||||
mi.dwFlags = release ? MOUSEEVENTF_MIDDLEUP : MOUSEEVENTF_MIDDLEDOWN;
|
||||
mouse_button = VK_MBUTTON;
|
||||
}
|
||||
else if(button == 3) {
|
||||
mi.dwFlags = release ? MOUSEEVENTF_RIGHTUP : MOUSEEVENTF_RIGHTDOWN;
|
||||
mouse_button = VK_RBUTTON;
|
||||
}
|
||||
else if(button == 4) {
|
||||
mi.dwFlags = release ? MOUSEEVENTF_XUP : MOUSEEVENTF_XDOWN;
|
||||
mi.mouseData = XBUTTON1;
|
||||
mouse_button = VK_XBUTTON1;
|
||||
}
|
||||
else {
|
||||
mi.dwFlags = release ? MOUSEEVENTF_XUP : MOUSEEVENTF_XDOWN;
|
||||
mi.mouseData = XBUTTON2;
|
||||
mouse_button = VK_XBUTTON2;
|
||||
}
|
||||
|
||||
auto key_state = GetAsyncKeyState(mouse_button);
|
||||
bool key_state_down = (key_state & KEY_STATE_DOWN) != 0;
|
||||
if(key_state_down != release) {
|
||||
BOOST_LOG(warning) << "Button state of mouse_button ["sv << button << "] does not match the desired state"sv;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
send_input(i);
|
||||
}
|
||||
|
||||
void scroll(input_t &input, int distance) {
|
||||
INPUT i {};
|
||||
|
||||
i.type = INPUT_MOUSE;
|
||||
auto &mi = i.mi;
|
||||
|
||||
mi.dwFlags = MOUSEEVENTF_WHEEL;
|
||||
mi.mouseData = distance;
|
||||
|
||||
send_input(i);
|
||||
}
|
||||
|
||||
void keyboard(input_t &input, uint16_t modcode, bool release) {
|
||||
if(modcode == VK_RMENU) {
|
||||
modcode = VK_LWIN;
|
||||
}
|
||||
|
||||
INPUT i {};
|
||||
i.type = INPUT_KEYBOARD;
|
||||
auto &ki = i.ki;
|
||||
|
||||
// For some reason, MapVirtualKey(VK_LWIN, MAPVK_VK_TO_VSC) doesn't seem to work :/
|
||||
if(modcode != VK_LWIN && modcode != VK_RWIN && modcode != VK_PAUSE) {
|
||||
ki.wScan = MapVirtualKey(modcode, MAPVK_VK_TO_VSC);
|
||||
ki.dwFlags = KEYEVENTF_SCANCODE;
|
||||
}
|
||||
else {
|
||||
ki.wVk = modcode;
|
||||
}
|
||||
|
||||
// https://docs.microsoft.com/en-us/windows/win32/inputdev/about-keyboard-input#keystroke-message-flags
|
||||
switch(modcode) {
|
||||
case VK_RMENU:
|
||||
case VK_RCONTROL:
|
||||
case VK_INSERT:
|
||||
case VK_DELETE:
|
||||
case VK_HOME:
|
||||
case VK_END:
|
||||
case VK_PRIOR:
|
||||
case VK_NEXT:
|
||||
case VK_UP:
|
||||
case VK_DOWN:
|
||||
case VK_LEFT:
|
||||
case VK_RIGHT:
|
||||
case VK_DIVIDE:
|
||||
ki.dwFlags |= KEYEVENTF_EXTENDEDKEY;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
if(release) {
|
||||
ki.dwFlags |= KEYEVENTF_KEYUP;
|
||||
}
|
||||
|
||||
send_input(i);
|
||||
}
|
||||
|
||||
int alloc_gamepad(input_t &input, int nr) {
|
||||
if(!input) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return ((vigem_t *)input.get())->alloc_x360(nr);
|
||||
}
|
||||
|
||||
void free_gamepad(input_t &input, int nr) {
|
||||
if(!input) {
|
||||
return;
|
||||
}
|
||||
|
||||
((vigem_t *)input.get())->free_target(nr);
|
||||
}
|
||||
void gamepad(input_t &input, int nr, const gamepad_state_t &gamepad_state) {
|
||||
// If there is no gamepad support
|
||||
if(!input) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto vigem = (vigem_t *)input.get();
|
||||
|
||||
auto &xusb = *(PXUSB_REPORT)&gamepad_state;
|
||||
auto &x360 = vigem->x360s[nr];
|
||||
|
||||
auto status = vigem_target_x360_update(vigem->client.get(), x360.get(), xusb);
|
||||
if(!VIGEM_SUCCESS(status)) {
|
||||
BOOST_LOG(fatal) << "Couldn't send gamepad input to ViGEm ["sv << util::hex(status).to_string_view() << ']';
|
||||
|
||||
log_flush();
|
||||
std::abort();
|
||||
}
|
||||
}
|
||||
|
||||
int thread_priority() {
|
||||
return SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_HIGHEST) ? 0 : 1;
|
||||
}
|
||||
|
||||
HDESK pairInputDesktop() {
|
||||
auto hDesk = OpenInputDesktop(DF_ALLOWOTHERACCOUNTHOOK, FALSE, GENERIC_ALL);
|
||||
if(NULL == hDesk) {
|
||||
auto err = GetLastError();
|
||||
BOOST_LOG(error) << "Failed to OpenInputDesktop [0x"sv << util::hex(err).to_string_view() << ']';
|
||||
}
|
||||
else {
|
||||
BOOST_LOG(info) << std::endl
|
||||
<< "Opened desktop [0x"sv << util::hex(hDesk).to_string_view() << ']';
|
||||
if(!SetThreadDesktop(hDesk)) {
|
||||
auto err = GetLastError();
|
||||
BOOST_LOG(error) << "Failed to SetThreadDesktop [0x"sv << util::hex(err).to_string_view() << ']';
|
||||
}
|
||||
CloseDesktop(hDesk);
|
||||
}
|
||||
|
||||
return hDesk;
|
||||
}
|
||||
|
||||
void freeInput(void *p) {
|
||||
auto vigem = (vigem_t *)p;
|
||||
|
||||
delete vigem;
|
||||
}
|
||||
} // namespace platf
|
||||
90
sunshine/platform/windows/misc.cpp
Normal file
90
sunshine/platform/windows/misc.cpp
Normal file
@@ -0,0 +1,90 @@
|
||||
#include <filesystem>
|
||||
#include <iomanip>
|
||||
#include <sstream>
|
||||
|
||||
|
||||
// prevent clang format from "optimizing" the header include order
|
||||
// clang-format off
|
||||
#include <winsock2.h>
|
||||
#include <iphlpapi.h>
|
||||
#include <windows.h>
|
||||
#include <winuser.h>
|
||||
#include <ws2tcpip.h>
|
||||
// clang-format on
|
||||
|
||||
#include "sunshine/main.h"
|
||||
#include "sunshine/utility.h"
|
||||
|
||||
using namespace std::literals;
|
||||
namespace platf {
|
||||
using adapteraddrs_t = util::c_ptr<IP_ADAPTER_ADDRESSES>;
|
||||
|
||||
std::filesystem::path appdata() {
|
||||
return L"."sv;
|
||||
}
|
||||
|
||||
std::string from_sockaddr(const sockaddr *const socket_address) {
|
||||
char data[INET6_ADDRSTRLEN];
|
||||
|
||||
auto family = socket_address->sa_family;
|
||||
if(family == AF_INET6) {
|
||||
inet_ntop(AF_INET6, &((sockaddr_in6 *)socket_address)->sin6_addr, data, INET6_ADDRSTRLEN);
|
||||
}
|
||||
|
||||
if(family == AF_INET) {
|
||||
inet_ntop(AF_INET, &((sockaddr_in *)socket_address)->sin_addr, data, INET_ADDRSTRLEN);
|
||||
}
|
||||
|
||||
return std::string { data };
|
||||
}
|
||||
|
||||
std::pair<std::uint16_t, std::string> from_sockaddr_ex(const sockaddr *const ip_addr) {
|
||||
char data[INET6_ADDRSTRLEN];
|
||||
|
||||
auto family = ip_addr->sa_family;
|
||||
std::uint16_t port;
|
||||
if(family == AF_INET6) {
|
||||
inet_ntop(AF_INET6, &((sockaddr_in6 *)ip_addr)->sin6_addr, data, INET6_ADDRSTRLEN);
|
||||
port = ((sockaddr_in6 *)ip_addr)->sin6_port;
|
||||
}
|
||||
|
||||
if(family == AF_INET) {
|
||||
inet_ntop(AF_INET, &((sockaddr_in *)ip_addr)->sin_addr, data, INET_ADDRSTRLEN);
|
||||
port = ((sockaddr_in *)ip_addr)->sin_port;
|
||||
}
|
||||
|
||||
return { port, std::string { data } };
|
||||
}
|
||||
|
||||
adapteraddrs_t get_adapteraddrs() {
|
||||
adapteraddrs_t info { nullptr };
|
||||
ULONG size = 0;
|
||||
|
||||
while(GetAdaptersAddresses(AF_UNSPEC, 0, nullptr, info.get(), &size) == ERROR_BUFFER_OVERFLOW) {
|
||||
info.reset((PIP_ADAPTER_ADDRESSES)malloc(size));
|
||||
}
|
||||
|
||||
return info;
|
||||
}
|
||||
|
||||
std::string get_mac_address(const std::string_view &address) {
|
||||
adapteraddrs_t info = get_adapteraddrs();
|
||||
for(auto adapter_pos = info.get(); adapter_pos != nullptr; adapter_pos = adapter_pos->Next) {
|
||||
for(auto addr_pos = adapter_pos->FirstUnicastAddress; addr_pos != nullptr; addr_pos = addr_pos->Next) {
|
||||
if(adapter_pos->PhysicalAddressLength != 0 && address == from_sockaddr(addr_pos->Address.lpSockaddr)) {
|
||||
std::stringstream mac_addr;
|
||||
mac_addr << std::hex;
|
||||
for(int i = 0; i < adapter_pos->PhysicalAddressLength; i++) {
|
||||
if(i > 0) {
|
||||
mac_addr << ':';
|
||||
}
|
||||
mac_addr << std::setw(2) << std::setfill('0') << (int)adapter_pos->PhysicalAddress[i];
|
||||
}
|
||||
return mac_addr.str();
|
||||
}
|
||||
}
|
||||
}
|
||||
BOOST_LOG(warning) << "Unable to find MAC address for "sv << address;
|
||||
return "00:00:00:00:00:00"s;
|
||||
}
|
||||
} // namespace platf
|
||||
@@ -1,729 +0,0 @@
|
||||
//
|
||||
// Created by loki on 1/12/20.
|
||||
//
|
||||
|
||||
#include <dxgi.h>
|
||||
#include <d3d11.h>
|
||||
#include <d3dcommon.h>
|
||||
#include <dxgi1_2.h>
|
||||
|
||||
#include <codecvt>
|
||||
#include <sunshine/config.h>
|
||||
|
||||
#include "sunshine/main.h"
|
||||
#include "common.h"
|
||||
|
||||
namespace platf {
|
||||
using namespace std::literals;
|
||||
}
|
||||
namespace platf::dxgi {
|
||||
template<class T>
|
||||
void Release(T *dxgi) {
|
||||
dxgi->Release();
|
||||
}
|
||||
|
||||
using factory1_t = util::safe_ptr<IDXGIFactory1, Release<IDXGIFactory1>>;
|
||||
using dxgi_t = util::safe_ptr<IDXGIDevice, Release<IDXGIDevice>>;
|
||||
using dxgi1_t = util::safe_ptr<IDXGIDevice1, Release<IDXGIDevice1>>;
|
||||
using device_t = util::safe_ptr<ID3D11Device, Release<ID3D11Device>>;
|
||||
using device_ctx_t = util::safe_ptr<ID3D11DeviceContext, Release<ID3D11DeviceContext>>;
|
||||
using adapter_t = util::safe_ptr<IDXGIAdapter1, Release<IDXGIAdapter1>>;
|
||||
using output_t = util::safe_ptr<IDXGIOutput, Release<IDXGIOutput>>;
|
||||
using output1_t = util::safe_ptr<IDXGIOutput1, Release<IDXGIOutput1>>;
|
||||
using dup_t = util::safe_ptr<IDXGIOutputDuplication, Release<IDXGIOutputDuplication>>;
|
||||
using texture2d_t = util::safe_ptr<ID3D11Texture2D, Release<ID3D11Texture2D>>;
|
||||
using resource_t = util::safe_ptr<IDXGIResource, Release<IDXGIResource>>;
|
||||
|
||||
extern const char *format_str[];
|
||||
|
||||
class duplication_t {
|
||||
public:
|
||||
dup_t dup;
|
||||
bool has_frame {};
|
||||
|
||||
capture_e next_frame(DXGI_OUTDUPL_FRAME_INFO &frame_info, resource_t::pointer *res_p) {
|
||||
auto capture_status = release_frame();
|
||||
if(capture_status != capture_e::ok) {
|
||||
return capture_status;
|
||||
}
|
||||
|
||||
auto status = dup->AcquireNextFrame(1000, &frame_info, res_p);
|
||||
|
||||
switch(status) {
|
||||
case S_OK:
|
||||
has_frame = true;
|
||||
return capture_e::ok;
|
||||
case DXGI_ERROR_WAIT_TIMEOUT:
|
||||
return capture_e::timeout;
|
||||
case WAIT_ABANDONED:
|
||||
case DXGI_ERROR_ACCESS_LOST:
|
||||
case DXGI_ERROR_ACCESS_DENIED:
|
||||
return capture_e::reinit;
|
||||
default:
|
||||
BOOST_LOG(error) << "Couldn't acquire next frame [0x"sv << util::hex(status).to_string_view();
|
||||
return capture_e::error;
|
||||
}
|
||||
}
|
||||
|
||||
capture_e reset(dup_t::pointer dup_p = dup_t::pointer()) {
|
||||
auto capture_status = release_frame();
|
||||
|
||||
dup.reset(dup_p);
|
||||
|
||||
return capture_status;
|
||||
}
|
||||
|
||||
capture_e release_frame() {
|
||||
if(!has_frame) {
|
||||
return capture_e::ok;
|
||||
}
|
||||
|
||||
auto status = dup->ReleaseFrame();
|
||||
switch (status) {
|
||||
case S_OK:
|
||||
has_frame = false;
|
||||
return capture_e::ok;
|
||||
case DXGI_ERROR_WAIT_TIMEOUT:
|
||||
return capture_e::timeout;
|
||||
case WAIT_ABANDONED:
|
||||
case DXGI_ERROR_ACCESS_LOST:
|
||||
case DXGI_ERROR_ACCESS_DENIED:
|
||||
has_frame = false;
|
||||
return capture_e::reinit;
|
||||
default:
|
||||
BOOST_LOG(error) << "Couldn't release frame [0x"sv << util::hex(status).to_string_view();
|
||||
return capture_e::error;
|
||||
}
|
||||
}
|
||||
|
||||
~duplication_t() {
|
||||
release_frame();
|
||||
}
|
||||
};
|
||||
|
||||
class display_t;
|
||||
struct img_t : public ::platf::img_t {
|
||||
~img_t() override {
|
||||
delete[] data;
|
||||
data = nullptr;
|
||||
}
|
||||
};
|
||||
|
||||
struct cursor_t {
|
||||
std::vector<std::uint8_t> img_data;
|
||||
|
||||
DXGI_OUTDUPL_POINTER_SHAPE_INFO shape_info;
|
||||
int x, y;
|
||||
bool visible;
|
||||
};
|
||||
|
||||
void blend_cursor_monochrome(const cursor_t &cursor, img_t &img) {
|
||||
int height = cursor.shape_info.Height / 2;
|
||||
int width = cursor.shape_info.Width;
|
||||
int pitch = cursor.shape_info.Pitch;
|
||||
|
||||
// img cursor.y < 0, skip parts of the cursor.img_data
|
||||
auto cursor_skip_y = std::min(0, cursor.y);
|
||||
auto cursor_skip_x = std::min(0, cursor.x);
|
||||
auto img_skip_y = std::max(0, cursor.y);
|
||||
auto img_skip_x = std::max(0, cursor.x);
|
||||
|
||||
if(cursor_skip_y > height || cursor_skip_x > width) {
|
||||
return;
|
||||
}
|
||||
|
||||
height -= cursor_skip_y;
|
||||
width -= cursor_skip_x;
|
||||
auto cursor_img_data = cursor.img_data.data() + cursor_skip_y * pitch;
|
||||
|
||||
int delta_height = std::min(height, std::max(0, img.height - img_skip_y));
|
||||
int delta_width = std::min(width, std::max(0, img.width - img_skip_x));
|
||||
|
||||
auto pixels_per_byte = width / pitch;
|
||||
auto bytes_per_row = delta_width / pixels_per_byte;
|
||||
|
||||
auto img_data = (int*)img.data;
|
||||
for(int i = 0; i < delta_height; ++i) {
|
||||
auto and_mask = &cursor_img_data[i * pitch];
|
||||
auto xor_mask = &cursor_img_data[(i + height) * pitch];
|
||||
|
||||
auto img_pixel_p = &img_data[(i + img_skip_y) * (img.row_pitch / img.pixel_pitch) + img_skip_x];
|
||||
|
||||
auto skip_y = cursor_skip_y;
|
||||
for(int x = 0; x < bytes_per_row; ++x) {
|
||||
for(auto bit = 0u; bit < 8; ++bit) {
|
||||
if(skip_y > 0) {
|
||||
--skip_y;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
int and_ = *and_mask & (1 << (7 - bit)) ? -1 : 0;
|
||||
int xor_ = *xor_mask & (1 << (7 - bit)) ? -1 : 0;
|
||||
|
||||
*img_pixel_p &= and_;
|
||||
*img_pixel_p ^= xor_;
|
||||
|
||||
++img_pixel_p;
|
||||
}
|
||||
|
||||
++and_mask;
|
||||
++xor_mask;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void blend_cursor_color(const cursor_t &cursor, img_t &img) {
|
||||
int height = cursor.shape_info.Height;
|
||||
int width = cursor.shape_info.Width;
|
||||
int pitch = cursor.shape_info.Pitch;
|
||||
|
||||
// img cursor.y < 0, skip parts of the cursor.img_data
|
||||
auto cursor_skip_y = std::min(0, cursor.y);
|
||||
auto cursor_skip_x = std::min(0, cursor.x);
|
||||
auto img_skip_y = std::max(0, cursor.y);
|
||||
auto img_skip_x = std::max(0, cursor.x);
|
||||
|
||||
if(cursor_skip_y > height || cursor_skip_x > width) {
|
||||
return;
|
||||
}
|
||||
|
||||
height -= cursor_skip_y;
|
||||
width -= cursor_skip_x;
|
||||
auto cursor_img_data = (int*)&cursor.img_data[cursor_skip_y * pitch];
|
||||
|
||||
int delta_height = std::min(height, std::max(0, img.height - img_skip_y));
|
||||
int delta_width = std::min(width, std::max(0, img.width - img_skip_x));
|
||||
|
||||
auto img_data = (int*)img.data;
|
||||
|
||||
for(int i = 0; i < delta_height; ++i) {
|
||||
auto cursor_begin = &cursor_img_data[i * width + cursor_skip_x];
|
||||
auto cursor_end = &cursor_begin[delta_width];
|
||||
|
||||
auto img_pixel_p = &img_data[(i + img_skip_y) * (img.row_pitch / img.pixel_pitch) + img_skip_x];
|
||||
std::for_each(cursor_begin, cursor_end, [&](int cursor_pixel) {
|
||||
auto colors_out = (std::uint8_t*)&cursor_pixel;
|
||||
auto colors_in = (std::uint8_t*)img_pixel_p;
|
||||
|
||||
//TODO: When use of IDXGIOutput5 is implemented, support different color formats
|
||||
auto alpha = colors_out[3];
|
||||
if(alpha == 255) {
|
||||
*img_pixel_p = cursor_pixel;
|
||||
}
|
||||
else {
|
||||
colors_in[0] = colors_out[0] + (colors_in[0] * (255 - alpha) + 255/2) / 255;
|
||||
colors_in[1] = colors_out[1] + (colors_in[1] * (255 - alpha) + 255/2) / 255;
|
||||
colors_in[2] = colors_out[2] + (colors_in[2] * (255 - alpha) + 255/2) / 255;
|
||||
}
|
||||
++img_pixel_p;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void blend_cursor(const cursor_t &cursor, img_t &img) {
|
||||
switch(cursor.shape_info.Type) {
|
||||
case DXGI_OUTDUPL_POINTER_SHAPE_TYPE_COLOR:
|
||||
blend_cursor_color(cursor, img);
|
||||
break;
|
||||
case DXGI_OUTDUPL_POINTER_SHAPE_TYPE_MONOCHROME:
|
||||
blend_cursor_monochrome(cursor, img);
|
||||
break;
|
||||
case DXGI_OUTDUPL_POINTER_SHAPE_TYPE_MASKED_COLOR:
|
||||
default:
|
||||
BOOST_LOG(warning) << "Unsupported cursor format ["sv << cursor.shape_info.Type << ']';
|
||||
}
|
||||
}
|
||||
|
||||
class display_t : public ::platf::display_t {
|
||||
public:
|
||||
capture_e snapshot(::platf::img_t *img_base, bool cursor_visible) override {
|
||||
auto img = (img_t *) img_base;
|
||||
HRESULT status;
|
||||
|
||||
DXGI_OUTDUPL_FRAME_INFO frame_info;
|
||||
|
||||
resource_t::pointer res_p {};
|
||||
auto capture_status = dup.next_frame(frame_info, &res_p);
|
||||
resource_t res{res_p};
|
||||
|
||||
if (capture_status != capture_e::ok) {
|
||||
return capture_status;
|
||||
}
|
||||
|
||||
if (frame_info.PointerShapeBufferSize > 0) {
|
||||
auto &img_data = cursor.img_data;
|
||||
|
||||
img_data.resize(frame_info.PointerShapeBufferSize);
|
||||
|
||||
UINT dummy;
|
||||
status = dup.dup->GetFramePointerShape(img_data.size(), img_data.data(), &dummy, &cursor.shape_info);
|
||||
if (FAILED(status)) {
|
||||
BOOST_LOG(error) << "Failed to get new pointer shape [0x"sv << util::hex(status).to_string_view() << ']';
|
||||
|
||||
return capture_e::error;
|
||||
}
|
||||
}
|
||||
|
||||
if(frame_info.LastMouseUpdateTime.QuadPart) {
|
||||
cursor.x = frame_info.PointerPosition.Position.x;
|
||||
cursor.y = frame_info.PointerPosition.Position.y;
|
||||
cursor.visible = frame_info.PointerPosition.Visible;
|
||||
}
|
||||
|
||||
// If frame has been updated
|
||||
if (frame_info.LastPresentTime.QuadPart != 0) {
|
||||
{
|
||||
texture2d_t::pointer src_p {};
|
||||
status = res->QueryInterface(IID_ID3D11Texture2D, (void **)&src_p);
|
||||
texture2d_t src{src_p};
|
||||
|
||||
if (FAILED(status)) {
|
||||
BOOST_LOG(error) << "Couldn't query interface [0x"sv << util::hex(status).to_string_view() << ']';
|
||||
return capture_e::error;
|
||||
}
|
||||
|
||||
//Copy from GPU to CPU
|
||||
device_ctx->CopyResource(texture.get(), src.get());
|
||||
}
|
||||
|
||||
if(current_img.pData) {
|
||||
device_ctx->Unmap(texture.get(), 0);
|
||||
current_img.pData = nullptr;
|
||||
}
|
||||
|
||||
status = device_ctx->Map(texture.get(), 0, D3D11_MAP_READ, 0, ¤t_img);
|
||||
if (FAILED(status)) {
|
||||
BOOST_LOG(error) << "Failed to map texture [0x"sv << util::hex(status).to_string_view() << ']';
|
||||
|
||||
return capture_e::error;
|
||||
}
|
||||
}
|
||||
|
||||
const bool update_flag =
|
||||
frame_info.LastMouseUpdateTime.QuadPart ||
|
||||
frame_info.LastPresentTime.QuadPart != 0 ||
|
||||
frame_info.PointerShapeBufferSize > 0;
|
||||
|
||||
|
||||
if(!update_flag) {
|
||||
return capture_e::timeout;
|
||||
}
|
||||
|
||||
if(img->width != width || img->height != height) {
|
||||
delete[] img->data;
|
||||
img->data = new std::uint8_t[height * current_img.RowPitch];
|
||||
|
||||
img->width = width;
|
||||
img->height = height;
|
||||
img->row_pitch = current_img.RowPitch;
|
||||
}
|
||||
|
||||
std::copy_n((std::uint8_t*)current_img.pData, height * current_img.RowPitch, (std::uint8_t*)img->data);
|
||||
if(cursor_visible && cursor.visible) {
|
||||
blend_cursor(cursor, *img);
|
||||
}
|
||||
|
||||
return capture_e::ok;
|
||||
}
|
||||
|
||||
std::unique_ptr<::platf::img_t> alloc_img() override {
|
||||
auto img = std::make_unique<img_t>();
|
||||
|
||||
img->data = nullptr;
|
||||
img->height = 0;
|
||||
img->width = 0;
|
||||
img->row_pitch = 0;
|
||||
img->pixel_pitch = 4;
|
||||
|
||||
return img;
|
||||
}
|
||||
|
||||
int init() {
|
||||
/* Uncomment when use of IDXGIOutput5 is implemented
|
||||
std::call_once(windows_cpp_once_flag, []() {
|
||||
DECLARE_HANDLE(DPI_AWARENESS_CONTEXT);
|
||||
const auto DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2 = ((DPI_AWARENESS_CONTEXT)-4);
|
||||
|
||||
typedef BOOL (*User32_SetProcessDpiAwarenessContext)(DPI_AWARENESS_CONTEXT value);
|
||||
|
||||
auto user32 = LoadLibraryA("user32.dll");
|
||||
auto f = (User32_SetProcessDpiAwarenessContext)GetProcAddress(user32, "SetProcessDpiAwarenessContext");
|
||||
if(f) {
|
||||
f(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2);
|
||||
}
|
||||
|
||||
FreeLibrary(user32);
|
||||
});
|
||||
*/
|
||||
current_img.pData = nullptr; // current_img is not yet mapped
|
||||
|
||||
dxgi::factory1_t::pointer factory_p {};
|
||||
dxgi::adapter_t::pointer adapter_p {};
|
||||
dxgi::output_t::pointer output_p {};
|
||||
dxgi::device_t::pointer device_p {};
|
||||
dxgi::device_ctx_t::pointer device_ctx_p {};
|
||||
|
||||
HRESULT status;
|
||||
|
||||
status = CreateDXGIFactory1(IID_IDXGIFactory1, (void**)&factory_p);
|
||||
factory.reset(factory_p);
|
||||
if(FAILED(status)) {
|
||||
BOOST_LOG(error) << "Failed to create DXGIFactory1 [0x"sv << util::hex(status).to_string_view() << ']';
|
||||
return -1;
|
||||
}
|
||||
|
||||
std::wstring_convert<std::codecvt_utf8_utf16<wchar_t>, wchar_t> converter;
|
||||
|
||||
auto adapter_name = converter.from_bytes(config::video.adapter_name);
|
||||
auto output_name = converter.from_bytes(config::video.output_name);
|
||||
|
||||
for(int x = 0; factory_p->EnumAdapters1(x, &adapter_p) != DXGI_ERROR_NOT_FOUND; ++x) {
|
||||
dxgi::adapter_t adapter_tmp { adapter_p };
|
||||
|
||||
DXGI_ADAPTER_DESC1 adapter_desc;
|
||||
adapter_tmp->GetDesc1(&adapter_desc);
|
||||
|
||||
if(!adapter_name.empty() && adapter_desc.Description != adapter_name) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for(int y = 0; adapter_tmp->EnumOutputs(y, &output_p) != DXGI_ERROR_NOT_FOUND; ++y) {
|
||||
dxgi::output_t output_tmp {output_p };
|
||||
|
||||
DXGI_OUTPUT_DESC desc;
|
||||
output_tmp->GetDesc(&desc);
|
||||
|
||||
if(!output_name.empty() && desc.DeviceName != output_name) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if(desc.AttachedToDesktop) {
|
||||
output = std::move(output_tmp);
|
||||
|
||||
width = desc.DesktopCoordinates.right - desc.DesktopCoordinates.left;
|
||||
height = desc.DesktopCoordinates.bottom - desc.DesktopCoordinates.top;
|
||||
}
|
||||
}
|
||||
|
||||
if(output) {
|
||||
adapter = std::move(adapter_tmp);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if(!output) {
|
||||
BOOST_LOG(error) << "Failed to locate an output device"sv;
|
||||
return -1;
|
||||
}
|
||||
|
||||
D3D_FEATURE_LEVEL featureLevels[] {
|
||||
D3D_FEATURE_LEVEL_12_1,
|
||||
D3D_FEATURE_LEVEL_12_0,
|
||||
D3D_FEATURE_LEVEL_11_1,
|
||||
D3D_FEATURE_LEVEL_11_0,
|
||||
D3D_FEATURE_LEVEL_10_1,
|
||||
D3D_FEATURE_LEVEL_10_0,
|
||||
D3D_FEATURE_LEVEL_9_3,
|
||||
D3D_FEATURE_LEVEL_9_2,
|
||||
D3D_FEATURE_LEVEL_9_1
|
||||
};
|
||||
|
||||
status = adapter->QueryInterface(IID_IDXGIAdapter, (void**)&adapter_p);
|
||||
if(FAILED(status)) {
|
||||
BOOST_LOG(error) << "Failed to query IDXGIAdapter interface"sv;
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
status = D3D11CreateDevice(
|
||||
adapter_p,
|
||||
D3D_DRIVER_TYPE_UNKNOWN,
|
||||
nullptr,
|
||||
D3D11_CREATE_DEVICE_VIDEO_SUPPORT,
|
||||
featureLevels, sizeof(featureLevels) / sizeof(D3D_FEATURE_LEVEL),
|
||||
D3D11_SDK_VERSION,
|
||||
&device_p,
|
||||
&feature_level,
|
||||
&device_ctx_p);
|
||||
|
||||
adapter_p->Release();
|
||||
|
||||
device.reset(device_p);
|
||||
device_ctx.reset(device_ctx_p);
|
||||
if(FAILED(status)) {
|
||||
BOOST_LOG(error) << "Failed to create D3D11 device [0x"sv << util::hex(status).to_string_view() << ']';
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
|
||||
DXGI_ADAPTER_DESC adapter_desc;
|
||||
adapter->GetDesc(&adapter_desc);
|
||||
|
||||
auto description = converter.to_bytes(adapter_desc.Description);
|
||||
BOOST_LOG(info) << std::endl
|
||||
<< "Device Description : " << description << std::endl
|
||||
<< "Device Vendor ID : 0x"sv << util::hex(adapter_desc.VendorId).to_string_view() << std::endl
|
||||
<< "Device Device ID : 0x"sv << util::hex(adapter_desc.DeviceId).to_string_view() << std::endl
|
||||
<< "Device Video Mem : "sv << adapter_desc.DedicatedVideoMemory / 1048576 << " MiB"sv << std::endl
|
||||
<< "Device Sys Mem : "sv << adapter_desc.DedicatedSystemMemory / 1048576 << " MiB"sv << std::endl
|
||||
<< "Share Sys Mem : "sv << adapter_desc.SharedSystemMemory / 1048576 << " MiB"sv << std::endl
|
||||
<< "Feature Level : 0x"sv << util::hex(feature_level).to_string_view() << std::endl
|
||||
<< "Capture size : "sv << width << 'x' << height;
|
||||
|
||||
// Bump up thread priority
|
||||
{
|
||||
dxgi::dxgi_t::pointer dxgi_p {};
|
||||
status = device->QueryInterface(IID_IDXGIDevice, (void**)&dxgi_p);
|
||||
dxgi::dxgi_t dxgi { dxgi_p };
|
||||
|
||||
if(FAILED(status)) {
|
||||
BOOST_LOG(error) << "Failed to query DXGI interface from device [0x"sv << util::hex(status).to_string_view() << ']';
|
||||
return -1;
|
||||
}
|
||||
|
||||
dxgi->SetGPUThreadPriority(7);
|
||||
}
|
||||
|
||||
// Try to reduce latency
|
||||
{
|
||||
dxgi::dxgi1_t::pointer dxgi_p {};
|
||||
status = device->QueryInterface(IID_IDXGIDevice, (void**)&dxgi_p);
|
||||
dxgi::dxgi1_t dxgi { dxgi_p };
|
||||
|
||||
if(FAILED(status)) {
|
||||
BOOST_LOG(error) << "Failed to query DXGI interface from device [0x"sv << util::hex(status).to_string_view() << ']';
|
||||
return -1;
|
||||
}
|
||||
|
||||
dxgi->SetMaximumFrameLatency(1);
|
||||
}
|
||||
|
||||
//FIXME: Duplicate output on RX580 in combination with DOOM (2016) --> BSOD
|
||||
//TODO: Use IDXGIOutput5 for improved performance
|
||||
{
|
||||
dxgi::output1_t::pointer output1_p {};
|
||||
status = output->QueryInterface(IID_IDXGIOutput1, (void**)&output1_p);
|
||||
dxgi::output1_t output1 {output1_p };
|
||||
|
||||
if(FAILED(status)) {
|
||||
BOOST_LOG(error) << "Failed to query IDXGIOutput1 from the output"sv;
|
||||
return -1;
|
||||
}
|
||||
|
||||
// We try this twice, in case we still get an error on reinitialization
|
||||
for(int x = 0; x < 2; ++x) {
|
||||
dxgi::dup_t::pointer dup_p {};
|
||||
status = output1->DuplicateOutput((IUnknown*)device.get(), &dup_p);
|
||||
if(SUCCEEDED(status)) {
|
||||
dup.reset(dup_p);
|
||||
break;
|
||||
}
|
||||
std::this_thread::sleep_for(200ms);
|
||||
}
|
||||
|
||||
if(FAILED(status)) {
|
||||
BOOST_LOG(error) << "DuplicateOutput Failed [0x"sv << util::hex(status).to_string_view() << ']';
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
DXGI_OUTDUPL_DESC dup_desc;
|
||||
dup.dup->GetDesc(&dup_desc);
|
||||
|
||||
format = dup_desc.ModeDesc.Format;
|
||||
|
||||
BOOST_LOG(debug) << "Source format ["sv << format_str[dup_desc.ModeDesc.Format] << ']';
|
||||
|
||||
D3D11_TEXTURE2D_DESC t {};
|
||||
t.Width = width;
|
||||
t.Height = height;
|
||||
t.MipLevels = 1;
|
||||
t.ArraySize = 1;
|
||||
t.SampleDesc.Count = 1;
|
||||
t.Usage = D3D11_USAGE_STAGING;
|
||||
t.Format = format;
|
||||
t.CPUAccessFlags = D3D11_CPU_ACCESS_READ;
|
||||
|
||||
dxgi::texture2d_t::pointer tex_p {};
|
||||
status = device->CreateTexture2D(&t, nullptr, &tex_p);
|
||||
|
||||
texture.reset(tex_p);
|
||||
|
||||
if(FAILED(status)) {
|
||||
BOOST_LOG(error) << "Failed to create texture [0x"sv << util::hex(status).to_string_view() << ']';
|
||||
return -1;
|
||||
}
|
||||
|
||||
// map the texture simply to get the pitch and stride
|
||||
status = device_ctx->Map(texture.get(), 0, D3D11_MAP_READ, 0, ¤t_img);
|
||||
if(FAILED(status)) {
|
||||
BOOST_LOG(error) << "Error: Failed to map the texture [0x"sv << util::hex(status).to_string_view() << ']';
|
||||
return -1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
~display_t() override {
|
||||
if(current_img.pData) {
|
||||
device_ctx->Unmap(texture.get(), 0);
|
||||
current_img.pData = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
factory1_t factory;
|
||||
adapter_t adapter;
|
||||
output_t output;
|
||||
device_t device;
|
||||
device_ctx_t device_ctx;
|
||||
duplication_t dup;
|
||||
cursor_t cursor;
|
||||
texture2d_t texture;
|
||||
|
||||
int width, height;
|
||||
|
||||
DXGI_FORMAT format;
|
||||
D3D_FEATURE_LEVEL feature_level;
|
||||
D3D11_MAPPED_SUBRESOURCE current_img;
|
||||
};
|
||||
|
||||
const char *format_str[] = {
|
||||
"DXGI_FORMAT_UNKNOWN",
|
||||
"DXGI_FORMAT_R32G32B32A32_TYPELESS",
|
||||
"DXGI_FORMAT_R32G32B32A32_FLOAT",
|
||||
"DXGI_FORMAT_R32G32B32A32_UINT",
|
||||
"DXGI_FORMAT_R32G32B32A32_SINT",
|
||||
"DXGI_FORMAT_R32G32B32_TYPELESS",
|
||||
"DXGI_FORMAT_R32G32B32_FLOAT",
|
||||
"DXGI_FORMAT_R32G32B32_UINT",
|
||||
"DXGI_FORMAT_R32G32B32_SINT",
|
||||
"DXGI_FORMAT_R16G16B16A16_TYPELESS",
|
||||
"DXGI_FORMAT_R16G16B16A16_FLOAT",
|
||||
"DXGI_FORMAT_R16G16B16A16_UNORM",
|
||||
"DXGI_FORMAT_R16G16B16A16_UINT",
|
||||
"DXGI_FORMAT_R16G16B16A16_SNORM",
|
||||
"DXGI_FORMAT_R16G16B16A16_SINT",
|
||||
"DXGI_FORMAT_R32G32_TYPELESS",
|
||||
"DXGI_FORMAT_R32G32_FLOAT",
|
||||
"DXGI_FORMAT_R32G32_UINT",
|
||||
"DXGI_FORMAT_R32G32_SINT",
|
||||
"DXGI_FORMAT_R32G8X24_TYPELESS",
|
||||
"DXGI_FORMAT_D32_FLOAT_S8X24_UINT",
|
||||
"DXGI_FORMAT_R32_FLOAT_X8X24_TYPELESS",
|
||||
"DXGI_FORMAT_X32_TYPELESS_G8X24_UINT",
|
||||
"DXGI_FORMAT_R10G10B10A2_TYPELESS",
|
||||
"DXGI_FORMAT_R10G10B10A2_UNORM",
|
||||
"DXGI_FORMAT_R10G10B10A2_UINT",
|
||||
"DXGI_FORMAT_R11G11B10_FLOAT",
|
||||
"DXGI_FORMAT_R8G8B8A8_TYPELESS",
|
||||
"DXGI_FORMAT_R8G8B8A8_UNORM",
|
||||
"DXGI_FORMAT_R8G8B8A8_UNORM_SRGB",
|
||||
"DXGI_FORMAT_R8G8B8A8_UINT",
|
||||
"DXGI_FORMAT_R8G8B8A8_SNORM",
|
||||
"DXGI_FORMAT_R8G8B8A8_SINT",
|
||||
"DXGI_FORMAT_R16G16_TYPELESS",
|
||||
"DXGI_FORMAT_R16G16_FLOAT",
|
||||
"DXGI_FORMAT_R16G16_UNORM",
|
||||
"DXGI_FORMAT_R16G16_UINT",
|
||||
"DXGI_FORMAT_R16G16_SNORM",
|
||||
"DXGI_FORMAT_R16G16_SINT",
|
||||
"DXGI_FORMAT_R32_TYPELESS",
|
||||
"DXGI_FORMAT_D32_FLOAT",
|
||||
"DXGI_FORMAT_R32_FLOAT",
|
||||
"DXGI_FORMAT_R32_UINT",
|
||||
"DXGI_FORMAT_R32_SINT",
|
||||
"DXGI_FORMAT_R24G8_TYPELESS",
|
||||
"DXGI_FORMAT_D24_UNORM_S8_UINT",
|
||||
"DXGI_FORMAT_R24_UNORM_X8_TYPELESS",
|
||||
"DXGI_FORMAT_X24_TYPELESS_G8_UINT",
|
||||
"DXGI_FORMAT_R8G8_TYPELESS",
|
||||
"DXGI_FORMAT_R8G8_UNORM",
|
||||
"DXGI_FORMAT_R8G8_UINT",
|
||||
"DXGI_FORMAT_R8G8_SNORM",
|
||||
"DXGI_FORMAT_R8G8_SINT",
|
||||
"DXGI_FORMAT_R16_TYPELESS",
|
||||
"DXGI_FORMAT_R16_FLOAT",
|
||||
"DXGI_FORMAT_D16_UNORM",
|
||||
"DXGI_FORMAT_R16_UNORM",
|
||||
"DXGI_FORMAT_R16_UINT",
|
||||
"DXGI_FORMAT_R16_SNORM",
|
||||
"DXGI_FORMAT_R16_SINT",
|
||||
"DXGI_FORMAT_R8_TYPELESS",
|
||||
"DXGI_FORMAT_R8_UNORM",
|
||||
"DXGI_FORMAT_R8_UINT",
|
||||
"DXGI_FORMAT_R8_SNORM",
|
||||
"DXGI_FORMAT_R8_SINT",
|
||||
"DXGI_FORMAT_A8_UNORM",
|
||||
"DXGI_FORMAT_R1_UNORM",
|
||||
"DXGI_FORMAT_R9G9B9E5_SHAREDEXP",
|
||||
"DXGI_FORMAT_R8G8_B8G8_UNORM",
|
||||
"DXGI_FORMAT_G8R8_G8B8_UNORM",
|
||||
"DXGI_FORMAT_BC1_TYPELESS",
|
||||
"DXGI_FORMAT_BC1_UNORM",
|
||||
"DXGI_FORMAT_BC1_UNORM_SRGB",
|
||||
"DXGI_FORMAT_BC2_TYPELESS",
|
||||
"DXGI_FORMAT_BC2_UNORM",
|
||||
"DXGI_FORMAT_BC2_UNORM_SRGB",
|
||||
"DXGI_FORMAT_BC3_TYPELESS",
|
||||
"DXGI_FORMAT_BC3_UNORM",
|
||||
"DXGI_FORMAT_BC3_UNORM_SRGB",
|
||||
"DXGI_FORMAT_BC4_TYPELESS",
|
||||
"DXGI_FORMAT_BC4_UNORM",
|
||||
"DXGI_FORMAT_BC4_SNORM",
|
||||
"DXGI_FORMAT_BC5_TYPELESS",
|
||||
"DXGI_FORMAT_BC5_UNORM",
|
||||
"DXGI_FORMAT_BC5_SNORM",
|
||||
"DXGI_FORMAT_B5G6R5_UNORM",
|
||||
"DXGI_FORMAT_B5G5R5A1_UNORM",
|
||||
"DXGI_FORMAT_B8G8R8A8_UNORM",
|
||||
"DXGI_FORMAT_B8G8R8X8_UNORM",
|
||||
"DXGI_FORMAT_R10G10B10_XR_BIAS_A2_UNORM",
|
||||
"DXGI_FORMAT_B8G8R8A8_TYPELESS",
|
||||
"DXGI_FORMAT_B8G8R8A8_UNORM_SRGB",
|
||||
"DXGI_FORMAT_B8G8R8X8_TYPELESS",
|
||||
"DXGI_FORMAT_B8G8R8X8_UNORM_SRGB",
|
||||
"DXGI_FORMAT_BC6H_TYPELESS",
|
||||
"DXGI_FORMAT_BC6H_UF16",
|
||||
"DXGI_FORMAT_BC6H_SF16",
|
||||
"DXGI_FORMAT_BC7_TYPELESS",
|
||||
"DXGI_FORMAT_BC7_UNORM",
|
||||
"DXGI_FORMAT_BC7_UNORM_SRGB",
|
||||
"DXGI_FORMAT_AYUV",
|
||||
"DXGI_FORMAT_Y410",
|
||||
"DXGI_FORMAT_Y416",
|
||||
"DXGI_FORMAT_NV12",
|
||||
"DXGI_FORMAT_P010",
|
||||
"DXGI_FORMAT_P016",
|
||||
"DXGI_FORMAT_420_OPAQUE",
|
||||
"DXGI_FORMAT_YUY2",
|
||||
"DXGI_FORMAT_Y210",
|
||||
"DXGI_FORMAT_Y216",
|
||||
"DXGI_FORMAT_NV11",
|
||||
"DXGI_FORMAT_AI44",
|
||||
"DXGI_FORMAT_IA44",
|
||||
"DXGI_FORMAT_P8",
|
||||
"DXGI_FORMAT_A8P8",
|
||||
"DXGI_FORMAT_B4G4R4A4_UNORM",
|
||||
|
||||
NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL,
|
||||
|
||||
"DXGI_FORMAT_P208",
|
||||
"DXGI_FORMAT_V208",
|
||||
"DXGI_FORMAT_V408"
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
namespace platf {
|
||||
std::shared_ptr<display_t> display() {
|
||||
auto disp = std::make_unique<dxgi::display_t>();
|
||||
|
||||
if (disp->init()) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
return disp;
|
||||
}
|
||||
}
|
||||
@@ -1,329 +0,0 @@
|
||||
//
|
||||
// Created by loki on 1/12/20.
|
||||
//
|
||||
|
||||
#include <roapi.h>
|
||||
#include <mmdeviceapi.h>
|
||||
#include <audioclient.h>
|
||||
|
||||
#include <codecvt>
|
||||
|
||||
#include <synchapi.h>
|
||||
|
||||
#include "sunshine/config.h"
|
||||
#include "sunshine/main.h"
|
||||
#include "common.h"
|
||||
|
||||
const CLSID CLSID_MMDeviceEnumerator = __uuidof(MMDeviceEnumerator);
|
||||
const IID IID_IMMDeviceEnumerator = __uuidof(IMMDeviceEnumerator);
|
||||
const IID IID_IAudioClient = __uuidof(IAudioClient);
|
||||
const IID IID_IAudioCaptureClient = __uuidof(IAudioCaptureClient);
|
||||
|
||||
using namespace std::literals;
|
||||
namespace platf::audio {
|
||||
template<class T>
|
||||
void Release(T *p) {
|
||||
p->Release();
|
||||
}
|
||||
|
||||
template<class T>
|
||||
void co_task_free(T *p) {
|
||||
CoTaskMemFree((LPVOID)p);
|
||||
}
|
||||
|
||||
using device_enum_t = util::safe_ptr<IMMDeviceEnumerator, Release<IMMDeviceEnumerator>>;
|
||||
using device_t = util::safe_ptr<IMMDevice, Release<IMMDevice>>;
|
||||
using audio_client_t = util::safe_ptr<IAudioClient, Release<IAudioClient>>;
|
||||
using audio_capture_t = util::safe_ptr<IAudioCaptureClient, Release<IAudioCaptureClient>>;
|
||||
using wave_format_t = util::safe_ptr<WAVEFORMATEX, co_task_free<WAVEFORMATEX>>;
|
||||
using handle_t = util::safe_ptr_v2<void, BOOL, CloseHandle>;
|
||||
|
||||
class mic_wasapi_t : public mic_t {
|
||||
public:
|
||||
capture_e sample(std::vector<std::int16_t> &sample_in) override {
|
||||
while(sample_buf_pos - std::begin(sample_buf) < sample_in.size()) {
|
||||
//FIXME: Use IAudioClient3 instead of IAudioClient, that would allows for adjusting the latency of the audio samples
|
||||
auto capture_result = _fill_buffer();
|
||||
|
||||
if(capture_result != capture_e::ok) {
|
||||
return capture_result;
|
||||
}
|
||||
}
|
||||
|
||||
std::copy_n(std::begin(sample_buf), sample_in.size(), std::begin(sample_in));
|
||||
|
||||
// The excess samples should be in front of the queue
|
||||
std::move(&sample_buf[sample_in.size()], sample_buf_pos, std::begin(sample_buf));
|
||||
sample_buf_pos -= sample_in.size();
|
||||
|
||||
return capture_e::ok;
|
||||
}
|
||||
|
||||
|
||||
int init(std::uint32_t sample_rate) {
|
||||
audio_event.reset(CreateEventA(nullptr, FALSE, FALSE, nullptr));
|
||||
if(!audio_event) {
|
||||
BOOST_LOG(error) << "Couldn't create Event handle"sv;
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
HRESULT status;
|
||||
|
||||
device_enum_t::pointer device_enum_p{};
|
||||
status = CoCreateInstance(
|
||||
CLSID_MMDeviceEnumerator,
|
||||
nullptr,
|
||||
CLSCTX_ALL,
|
||||
IID_IMMDeviceEnumerator,
|
||||
(void **) &device_enum_p);
|
||||
device_enum.reset(device_enum_p);
|
||||
|
||||
if (FAILED(status)) {
|
||||
BOOST_LOG(error) << "Couldn't create Device Enumerator [0x"sv << util::hex(status).to_string_view() << ']';
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
device_t::pointer device_p{};
|
||||
|
||||
if(config::audio.sink.empty()) {
|
||||
status = device_enum->GetDefaultAudioEndpoint(
|
||||
eRender,
|
||||
eConsole,
|
||||
&device_p);
|
||||
}
|
||||
else {
|
||||
std::wstring_convert<std::codecvt_utf8_utf16<wchar_t>, wchar_t> converter;
|
||||
auto wstring_device_id = converter.from_bytes(config::audio.sink);
|
||||
|
||||
status = device_enum->GetDevice(wstring_device_id.c_str(), &device_p);
|
||||
}
|
||||
device.reset(device_p);
|
||||
|
||||
if (FAILED(status)) {
|
||||
BOOST_LOG(error) << "Couldn't create audio Device [0x"sv << util::hex(status).to_string_view() << ']';
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
audio_client_t::pointer audio_client_p{};
|
||||
status = device->Activate(
|
||||
IID_IAudioClient,
|
||||
CLSCTX_ALL,
|
||||
nullptr,
|
||||
(void **) &audio_client_p);
|
||||
audio_client.reset(audio_client_p);
|
||||
|
||||
if (FAILED(status)) {
|
||||
BOOST_LOG(error) << "Couldn't activate audio Device [0x"sv << util::hex(status).to_string_view() << ']';
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
wave_format_t::pointer wave_format_p{};
|
||||
status = audio_client->GetMixFormat(&wave_format_p);
|
||||
wave_format.reset(wave_format_p);
|
||||
|
||||
if (FAILED(status)) {
|
||||
BOOST_LOG(error) << "Couldn't acquire Wave Format [0x"sv << util::hex(status).to_string_view() << ']';
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
wave_format->nChannels = 2;
|
||||
wave_format->wBitsPerSample = 16;
|
||||
wave_format->nSamplesPerSec = sample_rate;
|
||||
wave_format->nBlockAlign = wave_format->nChannels * wave_format->wBitsPerSample / 8;
|
||||
wave_format->nAvgBytesPerSec = wave_format->nSamplesPerSec * wave_format->nBlockAlign;
|
||||
|
||||
switch(wave_format->wFormatTag) {
|
||||
case WAVE_FORMAT_PCM:
|
||||
break;
|
||||
case WAVE_FORMAT_IEEE_FLOAT:
|
||||
wave_format->wFormatTag = WAVE_FORMAT_PCM;
|
||||
break;
|
||||
case WAVE_FORMAT_EXTENSIBLE: {
|
||||
auto wave_ex = (PWAVEFORMATEXTENSIBLE) wave_format.get();
|
||||
if (IsEqualGUID(KSDATAFORMAT_SUBTYPE_IEEE_FLOAT, wave_ex->SubFormat)) {
|
||||
wave_ex->SubFormat = KSDATAFORMAT_SUBTYPE_PCM;
|
||||
wave_ex->Samples.wValidBitsPerSample = 16;
|
||||
break;
|
||||
}
|
||||
|
||||
BOOST_LOG(error) << "Unsupported Sub Format for WAVE_FORMAT_EXTENSIBLE: [0x"sv << util::hex(wave_ex->SubFormat).to_string_view() << ']';
|
||||
return -1;
|
||||
}
|
||||
default:
|
||||
BOOST_LOG(error) << "Unsupported Wave Format: [0x"sv << util::hex(wave_format->wFormatTag).to_string_view() << ']';
|
||||
return -1;
|
||||
};
|
||||
|
||||
REFERENCE_TIME default_latency;
|
||||
audio_client->GetDevicePeriod(&default_latency, nullptr);
|
||||
default_latency_ms = default_latency / 10;
|
||||
|
||||
status = audio_client->Initialize(
|
||||
AUDCLNT_SHAREMODE_SHARED,
|
||||
AUDCLNT_STREAMFLAGS_LOOPBACK | AUDCLNT_STREAMFLAGS_EVENTCALLBACK,
|
||||
0, 0,
|
||||
wave_format.get(),
|
||||
nullptr);
|
||||
|
||||
if (FAILED(status)) {
|
||||
BOOST_LOG(error) << "Couldn't initialize audio client [0x"sv << util::hex(status).to_string_view() << ']';
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
std::uint32_t frames;
|
||||
status = audio_client->GetBufferSize(&frames);
|
||||
if (FAILED(status)) {
|
||||
BOOST_LOG(error) << "Couldn't acquire the number of audio frames [0x"sv << util::hex(status).to_string_view() << ']';
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
sample_buf = util::buffer_t<std::int16_t> { frames };
|
||||
sample_buf_pos = std::begin(sample_buf);
|
||||
|
||||
audio_capture_t::pointer audio_capture_p {};
|
||||
status = audio_client->GetService(IID_IAudioCaptureClient, (void**)&audio_capture_p);
|
||||
audio_capture.reset(audio_capture_p);
|
||||
if (FAILED(status)) {
|
||||
BOOST_LOG(error) << "Couldn't initialize audio capture client [0x"sv << util::hex(status).to_string_view() << ']';
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
status = audio_client->SetEventHandle(audio_event.get());
|
||||
if (FAILED(status)) {
|
||||
BOOST_LOG(error) << "Couldn't set event handle [0x"sv << util::hex(status).to_string_view() << ']';
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
status = audio_client->Start();
|
||||
if (FAILED(status)) {
|
||||
BOOST_LOG(error) << "Couldn't start recording [0x"sv << util::hex(status).to_string_view() << ']';
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
~mic_wasapi_t() override {
|
||||
if(audio_client) {
|
||||
audio_client->Stop();
|
||||
}
|
||||
}
|
||||
private:
|
||||
capture_e _fill_buffer() {
|
||||
HRESULT status;
|
||||
|
||||
// Total number of samples
|
||||
struct sample_aligned_t {
|
||||
std::uint32_t uninitialized;
|
||||
std::int16_t *samples;
|
||||
} sample_aligned;
|
||||
|
||||
// number of samples / number of channels
|
||||
struct block_aligned_t {
|
||||
std::uint32_t audio_sample_size;
|
||||
} block_aligned;
|
||||
|
||||
status = WaitForSingleObjectEx(audio_event.get(), default_latency_ms, FALSE);
|
||||
switch (status) {
|
||||
case WAIT_OBJECT_0:
|
||||
break;
|
||||
case WAIT_TIMEOUT:
|
||||
std::fill_n(std::begin(sample_buf), sample_buf.size(), 0);
|
||||
return capture_e::timeout;
|
||||
default:
|
||||
BOOST_LOG(error) << "Couldn't wait for audio event: [0x"sv << util::hex(status).to_string_view() << ']';
|
||||
return capture_e::error;
|
||||
}
|
||||
|
||||
std::uint32_t packet_size{};
|
||||
for (
|
||||
status = audio_capture->GetNextPacketSize(&packet_size);
|
||||
SUCCEEDED(status) && packet_size > 0;
|
||||
status = audio_capture->GetNextPacketSize(&packet_size)
|
||||
) {
|
||||
DWORD buffer_flags;
|
||||
status = audio_capture->GetBuffer(
|
||||
(BYTE **) &sample_aligned.samples,
|
||||
&block_aligned.audio_sample_size,
|
||||
&buffer_flags,
|
||||
nullptr, nullptr);
|
||||
|
||||
switch (status) {
|
||||
case S_OK:
|
||||
break;
|
||||
case AUDCLNT_E_DEVICE_INVALIDATED:
|
||||
return capture_e::reinit;
|
||||
default:
|
||||
BOOST_LOG(error) << "Couldn't capture audio [0x"sv << util::hex(status).to_string_view() << ']';
|
||||
return capture_e::error;
|
||||
}
|
||||
|
||||
sample_aligned.uninitialized = std::end(sample_buf) - sample_buf_pos;
|
||||
auto n = std::min(sample_aligned.uninitialized, block_aligned.audio_sample_size * wave_format->nChannels);
|
||||
|
||||
if (buffer_flags & AUDCLNT_BUFFERFLAGS_SILENT) {
|
||||
std::fill_n(sample_buf_pos, n, 0);
|
||||
} else {
|
||||
std::copy_n(sample_aligned.samples, n, sample_buf_pos);
|
||||
}
|
||||
|
||||
sample_buf_pos += n;
|
||||
|
||||
audio_capture->ReleaseBuffer(block_aligned.audio_sample_size);
|
||||
}
|
||||
|
||||
if (status == AUDCLNT_E_DEVICE_INVALIDATED) {
|
||||
return capture_e::reinit;
|
||||
}
|
||||
|
||||
if (FAILED(status)) {
|
||||
return capture_e::error;
|
||||
}
|
||||
|
||||
return capture_e::ok;
|
||||
}
|
||||
public:
|
||||
handle_t audio_event;
|
||||
|
||||
device_enum_t device_enum;
|
||||
device_t device;
|
||||
audio_client_t audio_client;
|
||||
wave_format_t wave_format;
|
||||
audio_capture_t audio_capture;
|
||||
|
||||
REFERENCE_TIME default_latency_ms;
|
||||
|
||||
util::buffer_t<std::int16_t> sample_buf;
|
||||
std::int16_t *sample_buf_pos;
|
||||
};
|
||||
}
|
||||
|
||||
namespace platf {
|
||||
class dummy_mic_t : public mic_t {
|
||||
public:
|
||||
capture_e sample(std::vector<std::int16_t> &sample_buf) override {
|
||||
return capture_e::ok;
|
||||
}
|
||||
};
|
||||
|
||||
std::unique_ptr<mic_t> microphone(std::uint32_t sample_rate) {
|
||||
Windows::Foundation::Initialize(RO_INIT_MULTITHREADED);
|
||||
auto mic = std::make_unique<audio::mic_wasapi_t>();
|
||||
|
||||
if(mic->init(sample_rate)) {
|
||||
return std::make_unique<dummy_mic_t>();
|
||||
}
|
||||
|
||||
return mic;
|
||||
}
|
||||
}
|
||||
@@ -2,16 +2,18 @@
|
||||
// Created by loki on 12/14/19.
|
||||
//
|
||||
|
||||
#define BOOST_BIND_GLOBAL_PLACEHOLDERS
|
||||
|
||||
#include "process.h"
|
||||
|
||||
#include <vector>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include <boost/property_tree/ptree.hpp>
|
||||
#include <boost/property_tree/json_parser.hpp>
|
||||
#include <boost/property_tree/ptree.hpp>
|
||||
|
||||
#include "utility.h"
|
||||
#include "main.h"
|
||||
#include "utility.h"
|
||||
|
||||
namespace proc {
|
||||
using namespace std::literals;
|
||||
@@ -48,7 +50,7 @@ int proc_t::execute(int app_id) {
|
||||
_app_id = -1;
|
||||
}
|
||||
|
||||
if(app_id >= _apps.size()) {
|
||||
if(app_id < 0 || app_id >= _apps.size()) {
|
||||
BOOST_LOG(error) << "Couldn't find app with ID ["sv << app_id << ']';
|
||||
|
||||
return 404;
|
||||
@@ -57,11 +59,11 @@ int proc_t::execute(int app_id) {
|
||||
// Ensure starting from a clean slate
|
||||
terminate();
|
||||
|
||||
_app_id = app_id;
|
||||
_app_id = app_id;
|
||||
auto &proc = _apps[app_id];
|
||||
|
||||
_undo_begin = std::begin(proc.prep_cmds);
|
||||
_undo_it = _undo_begin;
|
||||
_undo_it = _undo_begin;
|
||||
|
||||
if(!proc.output.empty() && proc.output != "null"sv) {
|
||||
_pipe.reset(fopen(proc.output.c_str(), "a"));
|
||||
@@ -80,17 +82,31 @@ int proc_t::execute(int app_id) {
|
||||
auto ret = exe(cmd, _env, _pipe, ec);
|
||||
|
||||
if(ec) {
|
||||
BOOST_LOG(error) << "System: "sv << ec.message();
|
||||
BOOST_LOG(error) << "Couldn't run ["sv << cmd << "]: System: "sv << ec.message();
|
||||
return -1;
|
||||
}
|
||||
|
||||
if(ret != 0) {
|
||||
BOOST_LOG(error) << "Return code ["sv << ret << ']';
|
||||
BOOST_LOG(error) << '[' << cmd << "] failed with code ["sv << ret << ']';
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
BOOST_LOG(debug) << "Starting ["sv << proc.cmd << ']';
|
||||
for(auto &cmd : proc.detached) {
|
||||
BOOST_LOG(info) << "Spawning ["sv << cmd << ']';
|
||||
if(proc.output.empty()) {
|
||||
bp::spawn(cmd, _env, bp::std_out > bp::null, bp::std_err > bp::null, ec);
|
||||
}
|
||||
else {
|
||||
bp::spawn(cmd, _env, bp::std_out > _pipe.get(), bp::std_err > _pipe.get(), ec);
|
||||
}
|
||||
|
||||
if(ec) {
|
||||
BOOST_LOG(warning) << "Couldn't spawn ["sv << cmd << "]: System: "sv << ec.message();
|
||||
}
|
||||
}
|
||||
|
||||
BOOST_LOG(info) << "Executing: ["sv << proc.cmd << ']';
|
||||
if(proc.cmd.empty()) {
|
||||
placebo = true;
|
||||
}
|
||||
@@ -102,7 +118,7 @@ int proc_t::execute(int app_id) {
|
||||
}
|
||||
|
||||
if(ec) {
|
||||
BOOST_LOG(info) << "System: "sv << ec.message();
|
||||
BOOST_LOG(warning) << "Couldn't run ["sv << proc.cmd << "]: System: "sv << ec.message();
|
||||
return -1;
|
||||
}
|
||||
|
||||
@@ -133,7 +149,7 @@ void proc_t::terminate() {
|
||||
std::abort();
|
||||
}
|
||||
|
||||
for(;_undo_it != _undo_begin; --_undo_it) {
|
||||
for(; _undo_it != _undo_begin; --_undo_it) {
|
||||
auto &cmd = (_undo_it - 1)->undo_cmd;
|
||||
|
||||
if(cmd.empty()) {
|
||||
@@ -163,6 +179,9 @@ void proc_t::terminate() {
|
||||
const std::vector<ctx_t> &proc_t::get_apps() const {
|
||||
return _apps;
|
||||
}
|
||||
std::vector<ctx_t> &proc_t::get_apps() {
|
||||
return _apps;
|
||||
}
|
||||
|
||||
proc_t::~proc_t() {
|
||||
terminate();
|
||||
@@ -175,9 +194,11 @@ std::string_view::iterator find_match(std::string_view::iterator begin, std::str
|
||||
do {
|
||||
++begin;
|
||||
switch(*begin) {
|
||||
case '(': ++stack;
|
||||
case '(':
|
||||
++stack;
|
||||
break;
|
||||
case ')': --stack;
|
||||
case ')':
|
||||
--stack;
|
||||
}
|
||||
} while(begin != end && stack != 0);
|
||||
|
||||
@@ -188,7 +209,7 @@ std::string_view::iterator find_match(std::string_view::iterator begin, std::str
|
||||
}
|
||||
|
||||
std::string parse_env_val(bp::native_environment &env, const std::string_view &val_raw) {
|
||||
auto pos = std::begin(val_raw);
|
||||
auto pos = std::begin(val_raw);
|
||||
auto dollar = std::find(pos, std::end(val_raw), '$');
|
||||
|
||||
std::stringstream ss;
|
||||
@@ -197,23 +218,23 @@ std::string parse_env_val(bp::native_environment &env, const std::string_view &v
|
||||
auto next = dollar + 1;
|
||||
if(next != std::end(val_raw)) {
|
||||
switch(*next) {
|
||||
case '(': {
|
||||
ss.write(pos, (dollar - pos));
|
||||
auto var_begin = next + 1;
|
||||
auto var_end = find_match(next, std::end(val_raw));
|
||||
case '(': {
|
||||
ss.write(pos, (dollar - pos));
|
||||
auto var_begin = next + 1;
|
||||
auto var_end = find_match(next, std::end(val_raw));
|
||||
|
||||
ss << env[std::string { var_begin, var_end }].to_string();
|
||||
ss << env[std::string { var_begin, var_end }].to_string();
|
||||
|
||||
pos = var_end + 1;
|
||||
next = var_end;
|
||||
pos = var_end + 1;
|
||||
next = var_end;
|
||||
|
||||
break;
|
||||
}
|
||||
case '$':
|
||||
ss.write(pos, (next - pos));
|
||||
pos = next + 1;
|
||||
++next;
|
||||
break;
|
||||
break;
|
||||
}
|
||||
case '$':
|
||||
ss.write(pos, (next - pos));
|
||||
pos = next + 1;
|
||||
++next;
|
||||
break;
|
||||
}
|
||||
|
||||
dollar = std::find(next, std::end(val_raw), '$');
|
||||
@@ -228,7 +249,7 @@ std::string parse_env_val(bp::native_environment &env, const std::string_view &v
|
||||
return ss.str();
|
||||
}
|
||||
|
||||
std::optional<proc::proc_t> parse(const std::string& file_name) {
|
||||
std::optional<proc::proc_t> parse(const std::string &file_name) {
|
||||
pt::ptree tree;
|
||||
|
||||
try {
|
||||
@@ -239,26 +260,45 @@ std::optional<proc::proc_t> parse(const std::string& file_name) {
|
||||
|
||||
auto this_env = boost::this_process::environment();
|
||||
|
||||
for(auto &[name, val] : env_vars) {
|
||||
this_env[name] = parse_env_val(this_env, val.get_value<std::string>());
|
||||
}
|
||||
|
||||
std::vector<proc::ctx_t> apps;
|
||||
for(auto &[_,app_node] : apps_node) {
|
||||
for(auto &[_, app_node] : apps_node) {
|
||||
proc::ctx_t ctx;
|
||||
|
||||
auto &prep_nodes = app_node.get_child("prep-cmd"s);
|
||||
auto output = app_node.get_optional<std::string>("output"s);
|
||||
auto name = parse_env_val(this_env, app_node.get<std::string>("name"s));
|
||||
auto cmd = app_node.get_optional<std::string>("cmd"s);
|
||||
auto prep_nodes_opt = app_node.get_child_optional("prep-cmd"s);
|
||||
auto detached_nodes_opt = app_node.get_child_optional("detached"s);
|
||||
auto output = app_node.get_optional<std::string>("output"s);
|
||||
auto name = parse_env_val(this_env, app_node.get<std::string>("name"s));
|
||||
auto cmd = app_node.get_optional<std::string>("cmd"s);
|
||||
|
||||
std::vector<proc::cmd_t> prep_cmds;
|
||||
prep_cmds.reserve(prep_nodes.size());
|
||||
for(auto &[_, prep_node] : prep_nodes) {
|
||||
auto do_cmd = parse_env_val(this_env, prep_node.get<std::string>("do"s));
|
||||
auto undo_cmd = prep_node.get_optional<std::string>("undo"s);
|
||||
if(prep_nodes_opt) {
|
||||
auto &prep_nodes = *prep_nodes_opt;
|
||||
|
||||
if(undo_cmd) {
|
||||
prep_cmds.emplace_back(std::move(do_cmd), parse_env_val(this_env, *undo_cmd));
|
||||
prep_cmds.reserve(prep_nodes.size());
|
||||
for(auto &[_, prep_node] : prep_nodes) {
|
||||
auto do_cmd = parse_env_val(this_env, prep_node.get<std::string>("do"s));
|
||||
auto undo_cmd = prep_node.get_optional<std::string>("undo"s);
|
||||
|
||||
if(undo_cmd) {
|
||||
prep_cmds.emplace_back(std::move(do_cmd), parse_env_val(this_env, *undo_cmd));
|
||||
}
|
||||
else {
|
||||
prep_cmds.emplace_back(std::move(do_cmd));
|
||||
}
|
||||
}
|
||||
else {
|
||||
prep_cmds.emplace_back(std::move(do_cmd));
|
||||
}
|
||||
|
||||
std::vector<std::string> detached;
|
||||
if(detached_nodes_opt) {
|
||||
auto &detached_nodes = *detached_nodes_opt;
|
||||
|
||||
detached.reserve(detached_nodes.size());
|
||||
for(auto &[_, detached_val] : detached_nodes) {
|
||||
detached.emplace_back(parse_env_val(this_env, detached_val.get_value<std::string>()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -270,21 +310,18 @@ std::optional<proc::proc_t> parse(const std::string& file_name) {
|
||||
ctx.cmd = parse_env_val(this_env, *cmd);
|
||||
}
|
||||
|
||||
ctx.name = std::move(name);
|
||||
ctx.name = std::move(name);
|
||||
ctx.prep_cmds = std::move(prep_cmds);
|
||||
ctx.detached = std::move(detached);
|
||||
|
||||
apps.emplace_back(std::move(ctx));
|
||||
}
|
||||
|
||||
bp::environment env = boost::this_process::environment();
|
||||
for(auto &[name,val] : env_vars) {
|
||||
this_env[name] = parse_env_val(this_env, val.get_value<std::string>());
|
||||
}
|
||||
|
||||
return proc::proc_t {
|
||||
std::move(this_env), std::move(apps)
|
||||
};
|
||||
} catch (std::exception &e) {
|
||||
}
|
||||
catch(std::exception &e) {
|
||||
BOOST_LOG(error) << e.what();
|
||||
}
|
||||
|
||||
@@ -295,7 +332,12 @@ void refresh(const std::string &file_name) {
|
||||
auto proc_opt = proc::parse(file_name);
|
||||
|
||||
if(proc_opt) {
|
||||
{
|
||||
proc::ctx_t ctx;
|
||||
ctx.name = "Desktop"s;
|
||||
proc_opt->get_apps().emplace(std::begin(proc_opt->get_apps()), std::move(ctx));
|
||||
}
|
||||
proc = std::move(*proc_opt);
|
||||
}
|
||||
}
|
||||
}
|
||||
} // namespace proc
|
||||
|
||||
@@ -9,8 +9,8 @@
|
||||
#define __kernel_entry
|
||||
#endif
|
||||
|
||||
#include <unordered_map>
|
||||
#include <optional>
|
||||
#include <unordered_map>
|
||||
|
||||
#include <boost/process.hpp>
|
||||
|
||||
@@ -30,6 +30,7 @@ struct cmd_t {
|
||||
};
|
||||
/*
|
||||
* pre_cmds -- guaranteed to be executed unless any of the commands fail.
|
||||
* detached -- commands detached from Sunshine
|
||||
* cmd -- Runs indefinitely until:
|
||||
* No session is running and a different set of commands it to be executed
|
||||
* Command exits
|
||||
@@ -41,6 +42,14 @@ struct cmd_t {
|
||||
struct ctx_t {
|
||||
std::vector<cmd_t> prep_cmds;
|
||||
|
||||
/**
|
||||
* Some applications, such as Steam,
|
||||
* either exit quickly, or keep running indefinitely.
|
||||
* Steam.exe is one such application.
|
||||
* That is why some applications need be run and forgotten about
|
||||
*/
|
||||
std::vector<std::string> detached;
|
||||
|
||||
std::string name;
|
||||
std::string cmd;
|
||||
std::string output;
|
||||
@@ -52,10 +61,9 @@ public:
|
||||
|
||||
proc_t(
|
||||
boost::process::environment &&env,
|
||||
std::vector<ctx_t> &&apps) :
|
||||
_app_id(-1),
|
||||
_env(std::move(env)),
|
||||
_apps(std::move(apps)) {}
|
||||
std::vector<ctx_t> &&apps) : _app_id(-1),
|
||||
_env(std::move(env)),
|
||||
_apps(std::move(apps)) {}
|
||||
|
||||
int execute(int app_id);
|
||||
|
||||
@@ -67,6 +75,8 @@ public:
|
||||
~proc_t();
|
||||
|
||||
const std::vector<ctx_t> &get_apps() const;
|
||||
std::vector<ctx_t> &get_apps();
|
||||
|
||||
void terminate();
|
||||
|
||||
private:
|
||||
@@ -87,8 +97,8 @@ private:
|
||||
};
|
||||
|
||||
void refresh(const std::string &file_name);
|
||||
std::optional<proc::proc_t> parse(const std::string& file_name);
|
||||
std::optional<proc::proc_t> parse(const std::string &file_name);
|
||||
|
||||
extern proc_t proc;
|
||||
}
|
||||
} // namespace proc
|
||||
#endif //SUNSHINE_PROCESS_H
|
||||
|
||||
156
sunshine/round_robin.h
Normal file
156
sunshine/round_robin.h
Normal file
@@ -0,0 +1,156 @@
|
||||
#ifndef KITTY_UTIL_ITERATOR_H
|
||||
#define KITTY_UTIL_ITERATOR_H
|
||||
|
||||
#include <iterator>
|
||||
|
||||
namespace util {
|
||||
template<class V, class T>
|
||||
class it_wrap_t : public std::iterator<std::random_access_iterator_tag, V> {
|
||||
public:
|
||||
typedef T iterator;
|
||||
typedef typename std::iterator<std::random_access_iterator_tag, V>::value_type class_t;
|
||||
|
||||
typedef class_t &reference;
|
||||
typedef class_t *pointer;
|
||||
|
||||
typedef std::ptrdiff_t diff_t;
|
||||
|
||||
iterator operator+=(diff_t step) {
|
||||
while(step-- > 0) {
|
||||
++_this();
|
||||
}
|
||||
|
||||
return _this();
|
||||
}
|
||||
|
||||
iterator operator-=(diff_t step) {
|
||||
while(step-- > 0) {
|
||||
--_this();
|
||||
}
|
||||
|
||||
return _this();
|
||||
}
|
||||
|
||||
iterator operator+(diff_t step) {
|
||||
iterator new_ = _this();
|
||||
|
||||
return new_ += step;
|
||||
}
|
||||
|
||||
iterator operator-(diff_t step) {
|
||||
iterator new_ = _this();
|
||||
|
||||
return new_ -= step;
|
||||
}
|
||||
|
||||
diff_t operator-(iterator first) {
|
||||
diff_t step = 0;
|
||||
while(first != _this()) {
|
||||
++step;
|
||||
++first;
|
||||
}
|
||||
|
||||
return step;
|
||||
}
|
||||
|
||||
iterator operator++() {
|
||||
_this().inc();
|
||||
return _this();
|
||||
}
|
||||
iterator operator--() {
|
||||
_this().dec();
|
||||
return _this();
|
||||
}
|
||||
|
||||
iterator operator++(int) {
|
||||
iterator new_ = _this();
|
||||
|
||||
++_this();
|
||||
|
||||
return new_;
|
||||
}
|
||||
|
||||
iterator operator--(int) {
|
||||
iterator new_ = _this();
|
||||
|
||||
--_this();
|
||||
|
||||
return new_;
|
||||
}
|
||||
|
||||
reference operator*() { return *_this().get(); }
|
||||
const reference operator*() const { return *_this().get(); }
|
||||
|
||||
pointer operator->() { return &*_this(); }
|
||||
const pointer operator->() const { return &*_this(); }
|
||||
|
||||
bool operator!=(const iterator &other) const {
|
||||
return !(_this() == other);
|
||||
}
|
||||
|
||||
bool operator<(const iterator &other) const {
|
||||
return !(_this() >= other);
|
||||
}
|
||||
|
||||
bool operator>=(const iterator &other) const {
|
||||
return _this() == other || _this() > other;
|
||||
}
|
||||
|
||||
bool operator<=(const iterator &other) const {
|
||||
return _this() == other || _this() < other;
|
||||
}
|
||||
|
||||
bool operator==(const iterator &other) const { return _this().eq(other); };
|
||||
bool operator>(const iterator &other) const { return _this().gt(other); }
|
||||
|
||||
private:
|
||||
iterator &_this() { return *static_cast<iterator *>(this); }
|
||||
const iterator &_this() const { return *static_cast<const iterator *>(this); }
|
||||
};
|
||||
|
||||
template<class V, class It>
|
||||
class round_robin_t : public it_wrap_t<V, round_robin_t<V, It>> {
|
||||
public:
|
||||
using iterator = It;
|
||||
using pointer = V *;
|
||||
|
||||
round_robin_t(iterator begin, iterator end) : _begin(begin), _end(end), _pos(begin) {}
|
||||
|
||||
void inc() {
|
||||
++_pos;
|
||||
|
||||
if(_pos == _end) {
|
||||
_pos = _begin;
|
||||
}
|
||||
}
|
||||
|
||||
void dec() {
|
||||
if(_pos == _begin) {
|
||||
_pos = _end;
|
||||
}
|
||||
|
||||
--_pos;
|
||||
}
|
||||
|
||||
bool eq(const round_robin_t &other) const {
|
||||
return *_pos == *other._pos;
|
||||
}
|
||||
|
||||
pointer get() const {
|
||||
return &*_pos;
|
||||
}
|
||||
|
||||
private:
|
||||
It _begin;
|
||||
It _end;
|
||||
|
||||
It _pos;
|
||||
};
|
||||
|
||||
template<class V, class It>
|
||||
round_robin_t<V, It> make_round_robin(It begin, It end) {
|
||||
return round_robin_t<V, It>(begin, end);
|
||||
}
|
||||
} // namespace util
|
||||
|
||||
#endif
|
||||
570
sunshine/rtsp.cpp
Normal file
570
sunshine/rtsp.cpp
Normal file
@@ -0,0 +1,570 @@
|
||||
//
|
||||
// Created by loki on 2/2/20.
|
||||
//
|
||||
|
||||
extern "C" {
|
||||
#include <moonlight-common-c/src/Rtsp.h>
|
||||
}
|
||||
|
||||
#include "config.h"
|
||||
#include "input.h"
|
||||
#include "main.h"
|
||||
#include "network.h"
|
||||
#include "rtsp.h"
|
||||
#include "stream.h"
|
||||
#include "sync.h"
|
||||
|
||||
namespace asio = boost::asio;
|
||||
|
||||
using asio::ip::tcp;
|
||||
using asio::ip::udp;
|
||||
|
||||
using namespace std::literals;
|
||||
|
||||
namespace stream {
|
||||
|
||||
//FIXME: Quick and dirty workaround for bug in MinGW 9.3 causing a linker error when using std::to_string
|
||||
template<class T>
|
||||
std::string to_string(T &&t) {
|
||||
std::stringstream ss;
|
||||
ss << std::forward<T>(t);
|
||||
return ss.str();
|
||||
}
|
||||
|
||||
constexpr auto RTSP_SETUP_PORT = 48010;
|
||||
|
||||
void free_msg(PRTSP_MESSAGE msg) {
|
||||
freeMessage(msg);
|
||||
|
||||
delete msg;
|
||||
}
|
||||
|
||||
class rtsp_server_t;
|
||||
|
||||
using msg_t = util::safe_ptr<RTSP_MESSAGE, free_msg>;
|
||||
using cmd_func_t = std::function<void(rtsp_server_t *, net::peer_t, msg_t &&)>;
|
||||
|
||||
void print_msg(PRTSP_MESSAGE msg);
|
||||
void cmd_not_found(net::host_t::pointer host, net::peer_t peer, msg_t &&req);
|
||||
|
||||
class rtsp_server_t {
|
||||
public:
|
||||
~rtsp_server_t() {
|
||||
if(_host) {
|
||||
clear();
|
||||
}
|
||||
}
|
||||
|
||||
int bind(std::uint16_t port) {
|
||||
{
|
||||
auto lg = _session_slots.lock();
|
||||
|
||||
_session_slots->resize(config::stream.channels);
|
||||
_slot_count = config::stream.channels;
|
||||
}
|
||||
|
||||
_host = net::host_create(_addr, 1, port);
|
||||
|
||||
return !(bool)_host;
|
||||
}
|
||||
|
||||
void session_raise(launch_session_t launch_session) {
|
||||
//FIXME: If client abandons us at this stage, _slot_count won't be raised again.
|
||||
--_slot_count;
|
||||
launch_event.raise(launch_session);
|
||||
}
|
||||
|
||||
int session_count() const {
|
||||
return config::stream.channels - _slot_count;
|
||||
}
|
||||
|
||||
template<class T, class X>
|
||||
void iterate(std::chrono::duration<T, X> timeout) {
|
||||
ENetEvent event;
|
||||
auto res = enet_host_service(_host.get(), &event, std::chrono::floor<std::chrono::milliseconds>(timeout).count());
|
||||
|
||||
if(res > 0) {
|
||||
switch(event.type) {
|
||||
case ENET_EVENT_TYPE_RECEIVE: {
|
||||
net::packet_t packet { event.packet };
|
||||
net::peer_t peer { event.peer };
|
||||
|
||||
msg_t req { new msg_t::element_type };
|
||||
|
||||
//TODO: compare addresses of the peers
|
||||
if(_queue_packet.second == nullptr) {
|
||||
parseRtspMessage(req.get(), (char *)packet->data, packet->dataLength);
|
||||
for(auto option = req->options; option != nullptr; option = option->next) {
|
||||
if("Content-length"sv == option->option) {
|
||||
_queue_packet = std::make_pair(peer, std::move(packet));
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
std::vector<char> full_payload;
|
||||
|
||||
auto old_msg = std::move(_queue_packet);
|
||||
auto &old_packet = old_msg.second;
|
||||
|
||||
std::string_view new_payload { (char *)packet->data, packet->dataLength };
|
||||
std::string_view old_payload { (char *)old_packet->data, old_packet->dataLength };
|
||||
full_payload.resize(new_payload.size() + old_payload.size());
|
||||
|
||||
std::copy(std::begin(old_payload), std::end(old_payload), std::begin(full_payload));
|
||||
std::copy(std::begin(new_payload), std::end(new_payload), std::begin(full_payload) + old_payload.size());
|
||||
|
||||
parseRtspMessage(req.get(), full_payload.data(), full_payload.size());
|
||||
}
|
||||
|
||||
print_msg(req.get());
|
||||
|
||||
msg_t resp;
|
||||
auto func = _map_cmd_cb.find(req->message.request.command);
|
||||
if(func != std::end(_map_cmd_cb)) {
|
||||
func->second(this, peer, std::move(req));
|
||||
}
|
||||
else {
|
||||
cmd_not_found(host(), peer, std::move(req));
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
case ENET_EVENT_TYPE_CONNECT:
|
||||
BOOST_LOG(info) << "CLIENT CONNECTED TO RTSP"sv;
|
||||
break;
|
||||
case ENET_EVENT_TYPE_DISCONNECT:
|
||||
BOOST_LOG(info) << "CLIENT DISCONNECTED FROM RTSP"sv;
|
||||
break;
|
||||
case ENET_EVENT_TYPE_NONE:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void map(const std::string_view &type, cmd_func_t cb) {
|
||||
_map_cmd_cb.emplace(type, std::move(cb));
|
||||
}
|
||||
|
||||
void clear(bool all = true) {
|
||||
auto lg = _session_slots.lock();
|
||||
|
||||
for(auto &slot : *_session_slots) {
|
||||
if(slot && (all || session::state(*slot) == session::state_e::STOPPING)) {
|
||||
session::stop(*slot);
|
||||
session::join(*slot);
|
||||
|
||||
slot.reset();
|
||||
|
||||
++_slot_count;
|
||||
}
|
||||
}
|
||||
|
||||
if(all) {
|
||||
std::for_each(_host->peers, _host->peers + _host->peerCount, [](auto &peer) {
|
||||
enet_peer_disconnect_now(&peer, 0);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void clear(std::shared_ptr<session_t> *session_p) {
|
||||
auto lg = _session_slots.lock();
|
||||
|
||||
session_p->reset();
|
||||
|
||||
++_slot_count;
|
||||
}
|
||||
|
||||
std::shared_ptr<session_t> *accept(std::shared_ptr<session_t> &session) {
|
||||
auto lg = _session_slots.lock();
|
||||
|
||||
for(auto &slot : *_session_slots) {
|
||||
if(!slot) {
|
||||
slot = session;
|
||||
return &slot;
|
||||
}
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
net::host_t::pointer host() const {
|
||||
return _host.get();
|
||||
}
|
||||
|
||||
safe::event_t<launch_session_t> launch_event;
|
||||
|
||||
private:
|
||||
// named _queue_packet because I want to make it an actual queue
|
||||
// It's like this for convenience sake
|
||||
std::pair<net::peer_t, net::packet_t> _queue_packet;
|
||||
|
||||
std::unordered_map<std::string_view, cmd_func_t> _map_cmd_cb;
|
||||
|
||||
util::sync_t<std::vector<std::shared_ptr<session_t>>> _session_slots;
|
||||
|
||||
int _slot_count;
|
||||
ENetAddress _addr;
|
||||
net::host_t _host;
|
||||
};
|
||||
|
||||
rtsp_server_t server;
|
||||
|
||||
void launch_session_raise(launch_session_t launch_session) {
|
||||
server.session_raise(launch_session);
|
||||
}
|
||||
|
||||
int session_count() {
|
||||
// Ensure session_count is up to date
|
||||
server.clear(false);
|
||||
|
||||
return server.session_count();
|
||||
}
|
||||
|
||||
void respond(net::host_t::pointer host, net::peer_t peer, msg_t &resp) {
|
||||
auto payload = std::make_pair(resp->payload, resp->payloadLength);
|
||||
|
||||
auto lg = util::fail_guard([&]() {
|
||||
resp->payload = payload.first;
|
||||
resp->payloadLength = payload.second;
|
||||
});
|
||||
|
||||
resp->payload = nullptr;
|
||||
resp->payloadLength = 0;
|
||||
|
||||
int serialized_len;
|
||||
util::c_ptr<char> raw_resp { serializeRtspMessage(resp.get(), &serialized_len) };
|
||||
BOOST_LOG(debug)
|
||||
<< "---Begin Response---"sv << std::endl
|
||||
<< std::string_view { raw_resp.get(), (std::size_t)serialized_len } << std::endl
|
||||
<< std::string_view { payload.first, (std::size_t)payload.second } << std::endl
|
||||
<< "---End Response---"sv << std::endl;
|
||||
|
||||
std::string_view tmp_resp { raw_resp.get(), (size_t)serialized_len };
|
||||
{
|
||||
auto packet = enet_packet_create(tmp_resp.data(), tmp_resp.size(), ENET_PACKET_FLAG_RELIABLE);
|
||||
if(enet_peer_send(peer, 0, packet)) {
|
||||
enet_packet_destroy(packet);
|
||||
return;
|
||||
}
|
||||
|
||||
enet_host_flush(host);
|
||||
}
|
||||
|
||||
if(payload.second > 0) {
|
||||
auto packet = enet_packet_create(payload.first, payload.second, ENET_PACKET_FLAG_RELIABLE);
|
||||
if(enet_peer_send(peer, 0, packet)) {
|
||||
enet_packet_destroy(packet);
|
||||
return;
|
||||
}
|
||||
|
||||
enet_host_flush(host);
|
||||
}
|
||||
}
|
||||
|
||||
void respond(net::host_t::pointer host, net::peer_t peer, POPTION_ITEM options, int statuscode, const char *status_msg, int seqn, const std::string_view &payload) {
|
||||
msg_t resp { new msg_t::element_type };
|
||||
createRtspResponse(resp.get(), nullptr, 0, const_cast<char *>("RTSP/1.0"), statuscode, const_cast<char *>(status_msg), seqn, options, const_cast<char *>(payload.data()), (int)payload.size());
|
||||
|
||||
respond(host, peer, resp);
|
||||
}
|
||||
|
||||
void cmd_not_found(net::host_t::pointer host, net::peer_t peer, msg_t &&req) {
|
||||
respond(host, peer, nullptr, 404, "NOT FOUND", req->sequenceNumber, {});
|
||||
}
|
||||
|
||||
void cmd_option(rtsp_server_t *server, net::peer_t peer, msg_t &&req) {
|
||||
OPTION_ITEM option {};
|
||||
|
||||
// I know these string literals will not be modified
|
||||
option.option = const_cast<char *>("CSeq");
|
||||
|
||||
auto seqn_str = to_string(req->sequenceNumber);
|
||||
option.content = const_cast<char *>(seqn_str.c_str());
|
||||
|
||||
respond(server->host(), peer, &option, 200, "OK", req->sequenceNumber, {});
|
||||
}
|
||||
|
||||
void cmd_describe(rtsp_server_t *server, net::peer_t peer, msg_t &&req) {
|
||||
OPTION_ITEM option {};
|
||||
|
||||
// I know these string literals will not be modified
|
||||
option.option = const_cast<char *>("CSeq");
|
||||
|
||||
auto seqn_str = to_string(req->sequenceNumber);
|
||||
option.content = const_cast<char *>(seqn_str.c_str());
|
||||
|
||||
std::stringstream ss;
|
||||
if(config::video.hevc_mode != 1) {
|
||||
ss << "sprop-parameter-sets=AAAAAU"sv << std::endl;
|
||||
}
|
||||
|
||||
for(int x = 0; x < audio::MAX_STREAM_CONFIG; ++x) {
|
||||
auto &stream_config = audio::stream_configs[x];
|
||||
std::uint8_t mapping[platf::speaker::MAX_SPEAKERS];
|
||||
|
||||
auto mapping_p = stream_config.mapping;
|
||||
|
||||
/**
|
||||
* GFE advertises incorrect mapping for normal quality configurations,
|
||||
* as a result, Moonlight rotates all channels from index '3' to the right
|
||||
* To work around this, rotate channels to the left from index '3'
|
||||
*/
|
||||
if(x == audio::SURROUND51 || x == audio::SURROUND71) {
|
||||
std::copy_n(mapping_p, stream_config.channelCount, mapping);
|
||||
std::rotate(mapping + 3, mapping + 4, mapping + audio::MAX_STREAM_CONFIG);
|
||||
|
||||
mapping_p = mapping;
|
||||
}
|
||||
|
||||
ss << "a=fmtp:97 surround-params="sv << stream_config.channelCount << stream_config.streams << stream_config.coupledStreams;
|
||||
|
||||
std::for_each_n(mapping_p, stream_config.channelCount, [&ss](std::uint8_t digit) {
|
||||
ss << (char)(digit + '0');
|
||||
});
|
||||
|
||||
ss << std::endl;
|
||||
}
|
||||
|
||||
respond(server->host(), peer, &option, 200, "OK", req->sequenceNumber, ss.str());
|
||||
}
|
||||
|
||||
void cmd_setup(rtsp_server_t *server, net::peer_t peer, msg_t &&req) {
|
||||
OPTION_ITEM options[2] {};
|
||||
|
||||
auto &seqn = options[0];
|
||||
auto &session_option = options[1];
|
||||
|
||||
seqn.option = const_cast<char *>("CSeq");
|
||||
|
||||
auto seqn_str = to_string(req->sequenceNumber);
|
||||
seqn.content = const_cast<char *>(seqn_str.c_str());
|
||||
|
||||
std::string_view target { req->message.request.target };
|
||||
auto begin = std::find(std::begin(target), std::end(target), '=') + 1;
|
||||
auto end = std::find(begin, std::end(target), '/');
|
||||
std::string_view type { begin, (size_t)std::distance(begin, end) };
|
||||
|
||||
if(type == "audio"sv) {
|
||||
seqn.next = &session_option;
|
||||
|
||||
session_option.option = const_cast<char *>("Session");
|
||||
session_option.content = const_cast<char *>("DEADBEEFCAFE;timeout = 90");
|
||||
}
|
||||
else if(type != "video"sv && type != "control"sv) {
|
||||
cmd_not_found(server->host(), peer, std::move(req));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
respond(server->host(), peer, &seqn, 200, "OK", req->sequenceNumber, {});
|
||||
}
|
||||
|
||||
void cmd_announce(rtsp_server_t *server, net::peer_t peer, msg_t &&req) {
|
||||
OPTION_ITEM option {};
|
||||
|
||||
// I know these string literals will not be modified
|
||||
option.option = const_cast<char *>("CSeq");
|
||||
|
||||
auto seqn_str = to_string(req->sequenceNumber);
|
||||
option.content = const_cast<char *>(seqn_str.c_str());
|
||||
|
||||
if(!server->launch_event.peek()) {
|
||||
// /launch has not been used
|
||||
|
||||
respond(server->host(), peer, &option, 503, "Service Unavailable", req->sequenceNumber, {});
|
||||
return;
|
||||
}
|
||||
auto launch_session { server->launch_event.pop() };
|
||||
|
||||
std::string_view payload { req->payload, (size_t)req->payloadLength };
|
||||
|
||||
std::vector<std::string_view> lines;
|
||||
|
||||
auto whitespace = [](char ch) {
|
||||
return ch == '\n' || ch == '\r';
|
||||
};
|
||||
|
||||
{
|
||||
auto pos = std::begin(payload);
|
||||
auto begin = pos;
|
||||
while(pos != std::end(payload)) {
|
||||
if(whitespace(*pos++)) {
|
||||
lines.emplace_back(begin, pos - begin - 1);
|
||||
|
||||
while(pos != std::end(payload) && whitespace(*pos)) { ++pos; }
|
||||
begin = pos;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::string_view client;
|
||||
std::unordered_map<std::string_view, std::string_view> args;
|
||||
|
||||
for(auto line : lines) {
|
||||
auto type = line.substr(0, 2);
|
||||
if(type == "s="sv) {
|
||||
client = line.substr(2);
|
||||
}
|
||||
else if(type == "a=") {
|
||||
auto pos = line.find(':');
|
||||
|
||||
auto name = line.substr(2, pos - 2);
|
||||
auto val = line.substr(pos + 1);
|
||||
|
||||
if(val[val.size() - 1] == ' ') {
|
||||
val = val.substr(0, val.size() - 1);
|
||||
}
|
||||
args.emplace(name, val);
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize any omitted parameters to defaults
|
||||
args.try_emplace("x-nv-video[0].encoderCscMode"sv, "0"sv);
|
||||
args.try_emplace("x-nv-vqos[0].bitStreamFormat"sv, "0"sv);
|
||||
args.try_emplace("x-nv-video[0].dynamicRangeMode"sv, "0"sv);
|
||||
args.try_emplace("x-nv-aqos.packetDuration"sv, "5"sv);
|
||||
|
||||
config_t config;
|
||||
|
||||
config.audio.flags[audio::config_t::HOST_AUDIO] = launch_session->host_audio;
|
||||
try {
|
||||
config.audio.channels = util::from_view(args.at("x-nv-audio.surround.numChannels"sv));
|
||||
config.audio.mask = util::from_view(args.at("x-nv-audio.surround.channelMask"sv));
|
||||
config.audio.packetDuration = util::from_view(args.at("x-nv-aqos.packetDuration"sv));
|
||||
|
||||
config.audio.flags[audio::config_t::HIGH_QUALITY] =
|
||||
util::from_view(args.at("x-nv-audio.surround.AudioQuality"sv));
|
||||
|
||||
config.packetsize = util::from_view(args.at("x-nv-video[0].packetSize"sv));
|
||||
|
||||
config.monitor.height = util::from_view(args.at("x-nv-video[0].clientViewportHt"sv));
|
||||
config.monitor.width = util::from_view(args.at("x-nv-video[0].clientViewportWd"sv));
|
||||
config.monitor.framerate = util::from_view(args.at("x-nv-video[0].maxFPS"sv));
|
||||
config.monitor.bitrate = util::from_view(args.at("x-nv-vqos[0].bw.maximumBitrateKbps"sv));
|
||||
config.monitor.slicesPerFrame = util::from_view(args.at("x-nv-video[0].videoEncoderSlicesPerFrame"sv));
|
||||
config.monitor.numRefFrames = util::from_view(args.at("x-nv-video[0].maxNumReferenceFrames"sv));
|
||||
config.monitor.encoderCscMode = util::from_view(args.at("x-nv-video[0].encoderCscMode"sv));
|
||||
config.monitor.videoFormat = util::from_view(args.at("x-nv-vqos[0].bitStreamFormat"sv));
|
||||
config.monitor.dynamicRange = util::from_view(args.at("x-nv-video[0].dynamicRangeMode"sv));
|
||||
}
|
||||
catch(std::out_of_range &) {
|
||||
|
||||
respond(server->host(), peer, &option, 400, "BAD REQUEST", req->sequenceNumber, {});
|
||||
return;
|
||||
}
|
||||
|
||||
if(config.monitor.videoFormat != 0 && config::video.hevc_mode == 1) {
|
||||
BOOST_LOG(warning) << "HEVC is disabled, yet the client requested HEVC"sv;
|
||||
|
||||
respond(server->host(), peer, &option, 400, "BAD REQUEST", req->sequenceNumber, {});
|
||||
return;
|
||||
}
|
||||
|
||||
auto session = session::alloc(config, launch_session->gcm_key, launch_session->iv);
|
||||
|
||||
auto slot = server->accept(session);
|
||||
if(!slot) {
|
||||
BOOST_LOG(info) << "Ran out of slots for client from ["sv << ']';
|
||||
|
||||
respond(server->host(), peer, &option, 503, "Service Unavailable", req->sequenceNumber, {});
|
||||
return;
|
||||
}
|
||||
|
||||
if(session::start(*session, platf::from_sockaddr((sockaddr *)&peer->address.address))) {
|
||||
BOOST_LOG(error) << "Failed to start a streaming session"sv;
|
||||
|
||||
server->clear(slot);
|
||||
respond(server->host(), peer, &option, 500, "Internal Server Error", req->sequenceNumber, {});
|
||||
return;
|
||||
}
|
||||
|
||||
respond(server->host(), peer, &option, 200, "OK", req->sequenceNumber, {});
|
||||
}
|
||||
|
||||
void cmd_play(rtsp_server_t *server, net::peer_t peer, msg_t &&req) {
|
||||
OPTION_ITEM option {};
|
||||
|
||||
// I know these string literals will not be modified
|
||||
option.option = const_cast<char *>("CSeq");
|
||||
|
||||
auto seqn_str = to_string(req->sequenceNumber);
|
||||
option.content = const_cast<char *>(seqn_str.c_str());
|
||||
|
||||
respond(server->host(), peer, &option, 200, "OK", req->sequenceNumber, {});
|
||||
}
|
||||
|
||||
void rtpThread(std::shared_ptr<safe::signal_t> shutdown_event) {
|
||||
server.map("OPTIONS"sv, &cmd_option);
|
||||
server.map("DESCRIBE"sv, &cmd_describe);
|
||||
server.map("SETUP"sv, &cmd_setup);
|
||||
server.map("ANNOUNCE"sv, &cmd_announce);
|
||||
|
||||
server.map("PLAY"sv, &cmd_play);
|
||||
|
||||
if(server.bind(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;
|
||||
shutdown_event->raise(true);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
while(!shutdown_event->peek()) {
|
||||
server.iterate(std::min(500ms, config::stream.ping_timeout));
|
||||
|
||||
if(broadcast_shutdown_event.peek()) {
|
||||
server.clear();
|
||||
}
|
||||
else {
|
||||
// cleanup all stopped sessions
|
||||
server.clear(false);
|
||||
}
|
||||
}
|
||||
|
||||
server.clear();
|
||||
}
|
||||
|
||||
void print_msg(PRTSP_MESSAGE msg) {
|
||||
std::string_view type = msg->type == TYPE_RESPONSE ? "RESPONSE"sv : "REQUEST"sv;
|
||||
|
||||
std::string_view payload { msg->payload, (size_t)msg->payloadLength };
|
||||
std::string_view protocol { msg->protocol };
|
||||
auto seqnm = msg->sequenceNumber;
|
||||
std::string_view messageBuffer { msg->messageBuffer };
|
||||
|
||||
BOOST_LOG(debug) << "type ["sv << type << ']';
|
||||
BOOST_LOG(debug) << "sequence number ["sv << seqnm << ']';
|
||||
BOOST_LOG(debug) << "protocol :: "sv << protocol;
|
||||
BOOST_LOG(debug) << "payload :: "sv << payload;
|
||||
|
||||
if(msg->type == TYPE_RESPONSE) {
|
||||
auto &resp = msg->message.response;
|
||||
|
||||
auto statuscode = resp.statusCode;
|
||||
std::string_view status { resp.statusString };
|
||||
|
||||
BOOST_LOG(debug) << "statuscode :: "sv << statuscode;
|
||||
BOOST_LOG(debug) << "status :: "sv << status;
|
||||
}
|
||||
else {
|
||||
auto &req = msg->message.request;
|
||||
|
||||
std::string_view command { req.command };
|
||||
std::string_view target { req.target };
|
||||
|
||||
BOOST_LOG(debug) << "command :: "sv << command;
|
||||
BOOST_LOG(debug) << "target :: "sv << target;
|
||||
}
|
||||
|
||||
for(auto option = msg->options; option != nullptr; option = option->next) {
|
||||
std::string_view content { option->content };
|
||||
std::string_view name { option->option };
|
||||
|
||||
BOOST_LOG(debug) << name << " :: "sv << content;
|
||||
}
|
||||
|
||||
BOOST_LOG(debug) << "---Begin MessageBuffer---"sv << std::endl
|
||||
<< messageBuffer << std::endl
|
||||
<< "---End MessageBuffer---"sv << std::endl;
|
||||
}
|
||||
} // namespace stream
|
||||
28
sunshine/rtsp.h
Normal file
28
sunshine/rtsp.h
Normal file
@@ -0,0 +1,28 @@
|
||||
//
|
||||
// Created by loki on 2/2/20.
|
||||
//
|
||||
|
||||
#ifndef SUNSHINE_RTSP_H
|
||||
#define SUNSHINE_RTSP_H
|
||||
|
||||
#include <atomic>
|
||||
|
||||
#include "crypto.h"
|
||||
#include "thread_safe.h"
|
||||
|
||||
namespace stream {
|
||||
struct launch_session_t {
|
||||
crypto::aes_t gcm_key;
|
||||
crypto::aes_t iv;
|
||||
|
||||
bool host_audio;
|
||||
};
|
||||
|
||||
void launch_session_raise(launch_session_t launch_session);
|
||||
int session_count();
|
||||
|
||||
void rtpThread(std::shared_ptr<safe::signal_t> shutdown_event);
|
||||
|
||||
} // namespace stream
|
||||
|
||||
#endif //SUNSHINE_RTSP_H
|
||||
1332
sunshine/stream.cpp
1332
sunshine/stream.cpp
File diff suppressed because it is too large
Load Diff
@@ -5,13 +5,23 @@
|
||||
#ifndef SUNSHINE_STREAM_H
|
||||
#define SUNSHINE_STREAM_H
|
||||
|
||||
#include <atomic>
|
||||
#include <boost/asio.hpp>
|
||||
|
||||
#include "audio.h"
|
||||
#include "crypto.h"
|
||||
#include "thread_safe.h"
|
||||
#include "video.h"
|
||||
|
||||
namespace stream {
|
||||
struct session_t;
|
||||
struct config_t {
|
||||
audio::config_t audio;
|
||||
video::config_t monitor;
|
||||
int packetsize;
|
||||
|
||||
std::optional<int> gcmap;
|
||||
};
|
||||
|
||||
namespace session {
|
||||
enum class state_e : int {
|
||||
STOPPED,
|
||||
STOPPING,
|
||||
@@ -19,18 +29,14 @@ enum class state_e : int {
|
||||
RUNNING,
|
||||
};
|
||||
|
||||
struct launch_session_t {
|
||||
crypto::aes_t gcm_key;
|
||||
crypto::aes_t iv;
|
||||
std::shared_ptr<session_t> alloc(config_t &config, crypto::aes_t &gcm_key, crypto::aes_t &iv);
|
||||
int start(session_t &session, const std::string &addr_string);
|
||||
void stop(session_t &session);
|
||||
void join(session_t &session);
|
||||
state_e state(session_t &session);
|
||||
} // namespace session
|
||||
|
||||
bool has_process;
|
||||
};
|
||||
|
||||
extern safe::event_t<launch_session_t> launch_event;
|
||||
extern std::atomic<state_e> session_state;
|
||||
|
||||
void rtpThread(std::shared_ptr<safe::event_t<bool>> shutdown_event);
|
||||
|
||||
}
|
||||
extern safe::signal_t broadcast_shutdown_event;
|
||||
} // namespace stream
|
||||
|
||||
#endif //SUNSHINE_STREAM_H
|
||||
|
||||
95
sunshine/sync.h
Normal file
95
sunshine/sync.h
Normal file
@@ -0,0 +1,95 @@
|
||||
//
|
||||
// Created by loki on 16-4-19.
|
||||
//
|
||||
|
||||
#ifndef SUNSHINE_SYNC_H
|
||||
#define SUNSHINE_SYNC_H
|
||||
|
||||
#include <array>
|
||||
#include <mutex>
|
||||
#include <utility>
|
||||
|
||||
namespace util {
|
||||
|
||||
template<class T, class M = std::mutex>
|
||||
class sync_t {
|
||||
public:
|
||||
using value_t = T;
|
||||
using mutex_t = M;
|
||||
|
||||
std::lock_guard<mutex_t> lock() {
|
||||
return std::lock_guard { _lock };
|
||||
}
|
||||
|
||||
template<class... Args>
|
||||
sync_t(Args &&...args) : raw { std::forward<Args>(args)... } {}
|
||||
|
||||
sync_t &operator=(sync_t &&other) noexcept {
|
||||
std::lock(_lock, other._lock);
|
||||
|
||||
raw = std::move(other.raw);
|
||||
|
||||
_lock.unlock();
|
||||
other._lock.unlock();
|
||||
|
||||
return *this;
|
||||
}
|
||||
|
||||
sync_t &operator=(sync_t &other) noexcept {
|
||||
std::lock(_lock, other._lock);
|
||||
|
||||
raw = other.raw;
|
||||
|
||||
_lock.unlock();
|
||||
other._lock.unlock();
|
||||
|
||||
return *this;
|
||||
}
|
||||
|
||||
template<class V>
|
||||
sync_t &operator=(V &&val) {
|
||||
auto lg = lock();
|
||||
|
||||
raw = val;
|
||||
|
||||
return *this;
|
||||
}
|
||||
|
||||
sync_t &operator=(const value_t &val) noexcept {
|
||||
auto lg = lock();
|
||||
|
||||
raw = val;
|
||||
|
||||
return *this;
|
||||
}
|
||||
|
||||
sync_t &operator=(value_t &&val) noexcept {
|
||||
auto lg = lock();
|
||||
|
||||
raw = std::move(val);
|
||||
|
||||
return *this;
|
||||
}
|
||||
|
||||
value_t *operator->() {
|
||||
return &raw;
|
||||
}
|
||||
|
||||
value_t &operator*() {
|
||||
return raw;
|
||||
}
|
||||
|
||||
const value_t &operator*() const {
|
||||
return raw;
|
||||
}
|
||||
|
||||
value_t raw;
|
||||
|
||||
private:
|
||||
mutex_t _lock;
|
||||
};
|
||||
|
||||
} // namespace util
|
||||
|
||||
|
||||
#endif //T_MAN_SYNC_H
|
||||
@@ -1,18 +1,18 @@
|
||||
#ifndef KITTY_TASK_POOL_H
|
||||
#define KITTY_TASK_POOL_H
|
||||
|
||||
#include <deque>
|
||||
#include <vector>
|
||||
#include <future>
|
||||
#include <chrono>
|
||||
#include <utility>
|
||||
#include <deque>
|
||||
#include <functional>
|
||||
#include <future>
|
||||
#include <mutex>
|
||||
#include <type_traits>
|
||||
#include <optional>
|
||||
#include <type_traits>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "utility.h"
|
||||
#include "move_by_copy.h"
|
||||
#include "utility.h"
|
||||
namespace util {
|
||||
|
||||
class _ImplBase {
|
||||
@@ -29,8 +29,7 @@ class _Impl : public _ImplBase {
|
||||
Function _func;
|
||||
|
||||
public:
|
||||
|
||||
_Impl(Function&& f) : _func(std::forward<Function>(f)) { }
|
||||
_Impl(Function &&f) : _func(std::forward<Function>(f)) {}
|
||||
|
||||
void run() override {
|
||||
_func();
|
||||
@@ -40,7 +39,7 @@ public:
|
||||
class TaskPool {
|
||||
public:
|
||||
typedef std::unique_ptr<_ImplBase> __task;
|
||||
typedef _ImplBase* task_id_t;
|
||||
typedef _ImplBase *task_id_t;
|
||||
|
||||
|
||||
typedef std::chrono::steady_clock::time_point __time_point;
|
||||
@@ -53,9 +52,10 @@ public:
|
||||
|
||||
timer_task_t(task_id_t task_id, std::future<R> &future) : task_id { task_id }, future { std::move(future) } {}
|
||||
};
|
||||
|
||||
protected:
|
||||
std::deque<__task> _tasks;
|
||||
std::vector<std::pair<__time_point, __task>> _timer_tasks;
|
||||
std::vector<std::pair<__time_point, __task>> _timer_tasks;
|
||||
std::mutex _task_mutex;
|
||||
|
||||
public:
|
||||
@@ -70,8 +70,8 @@ public:
|
||||
}
|
||||
|
||||
template<class Function, class... Args>
|
||||
auto push(Function && newTask, Args &&... args) {
|
||||
static_assert(std::is_invocable_v<Function, Args&&...>, "arguments don't match the function");
|
||||
auto push(Function &&newTask, Args &&...args) {
|
||||
static_assert(std::is_invocable_v<Function, Args &&...>, "arguments don't match the function");
|
||||
|
||||
using __return = std::invoke_result_t<Function, Args &&...>;
|
||||
using task_t = std::packaged_task<__return()>;
|
||||
@@ -81,12 +81,12 @@ public:
|
||||
};
|
||||
|
||||
task_t task(std::move(bind));
|
||||
|
||||
|
||||
auto future = task.get_future();
|
||||
|
||||
|
||||
std::lock_guard<std::mutex> lg(_task_mutex);
|
||||
_tasks.emplace_back(toRunnable(std::move(task)));
|
||||
|
||||
|
||||
return future;
|
||||
}
|
||||
|
||||
@@ -107,13 +107,19 @@ public:
|
||||
* @return an id to potentially delay the task
|
||||
*/
|
||||
template<class Function, class X, class Y, class... Args>
|
||||
auto pushDelayed(Function &&newTask, std::chrono::duration<X, Y> duration, Args &&... args) {
|
||||
static_assert(std::is_invocable_v<Function, Args&&...>, "arguments don't match the function");
|
||||
auto pushDelayed(Function &&newTask, std::chrono::duration<X, Y> duration, Args &&...args) {
|
||||
static_assert(std::is_invocable_v<Function, Args &&...>, "arguments don't match the function");
|
||||
|
||||
using __return = std::invoke_result_t<Function, Args &&...>;
|
||||
using task_t = std::packaged_task<__return()>;
|
||||
|
||||
__time_point time_point = std::chrono::steady_clock::now() + duration;
|
||||
|
||||
__time_point time_point;
|
||||
if constexpr(std::is_floating_point_v<X>) {
|
||||
time_point = std::chrono::steady_clock::now() + std::chrono::duration_cast<std::chrono::nanoseconds>(duration);
|
||||
}
|
||||
else {
|
||||
time_point = std::chrono::steady_clock::now() + duration;
|
||||
}
|
||||
|
||||
auto bind = [task = std::forward<Function>(newTask), tuple_args = std::make_tuple(std::forward<Args>(args)...)]() mutable {
|
||||
return std::apply(task, std::move(tuple_args));
|
||||
@@ -121,7 +127,7 @@ public:
|
||||
|
||||
task_t task(std::move(bind));
|
||||
|
||||
auto future = task.get_future();
|
||||
auto future = task.get_future();
|
||||
auto runnable = toRunnable(std::move(task));
|
||||
|
||||
task_id_t task_id = &*runnable;
|
||||
@@ -154,13 +160,14 @@ public:
|
||||
}
|
||||
|
||||
// smaller time goes to the back
|
||||
auto prev = it -1;
|
||||
auto prev = it - 1;
|
||||
while(it > _timer_tasks.cbegin()) {
|
||||
if(std::get<0>(*it) > std::get<0>(*prev)) {
|
||||
std::swap(*it, *prev);
|
||||
}
|
||||
|
||||
--prev; --it;
|
||||
--prev;
|
||||
--it;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -195,20 +202,20 @@ public:
|
||||
|
||||
std::optional<__task> pop() {
|
||||
std::lock_guard lg(_task_mutex);
|
||||
|
||||
|
||||
if(!_tasks.empty()) {
|
||||
__task task = std::move(_tasks.front());
|
||||
_tasks.pop_front();
|
||||
return std::move(task);
|
||||
}
|
||||
|
||||
|
||||
if(!_timer_tasks.empty() && std::get<0>(_timer_tasks.back()) <= std::chrono::steady_clock::now()) {
|
||||
__task task = std::move(std::get<1>(_timer_tasks.back()));
|
||||
_timer_tasks.pop_back();
|
||||
|
||||
|
||||
return std::move(task);
|
||||
}
|
||||
|
||||
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
@@ -227,12 +234,12 @@ public:
|
||||
|
||||
return std::get<0>(_timer_tasks.back());
|
||||
}
|
||||
private:
|
||||
|
||||
private:
|
||||
template<class Function>
|
||||
std::unique_ptr<_ImplBase> toRunnable(Function &&f) {
|
||||
return std::make_unique<_Impl<Function>>(std::forward<Function&&>(f));
|
||||
return std::make_unique<_Impl<Function>>(std::forward<Function &&>(f));
|
||||
}
|
||||
};
|
||||
}
|
||||
} // namespace util
|
||||
#endif
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
#ifndef KITTY_THREAD_POOL_H
|
||||
#define KITTY_THREAD_POOL_H
|
||||
|
||||
#include <thread>
|
||||
#include "task_pool.h"
|
||||
#include <thread>
|
||||
|
||||
namespace util {
|
||||
/*
|
||||
@@ -12,32 +12,33 @@ namespace util {
|
||||
class ThreadPool : public TaskPool {
|
||||
public:
|
||||
typedef TaskPool::__task __task;
|
||||
|
||||
|
||||
private:
|
||||
std::vector<std::thread> _thread;
|
||||
|
||||
std::condition_variable _cv;
|
||||
std::mutex _lock;
|
||||
|
||||
|
||||
bool _continue;
|
||||
|
||||
public:
|
||||
ThreadPool() : _continue { false } {}
|
||||
|
||||
explicit ThreadPool(int threads) : _thread(threads), _continue { true } {
|
||||
for (auto &t : _thread) {
|
||||
for(auto &t : _thread) {
|
||||
t = std::thread(&ThreadPool::_main, this);
|
||||
}
|
||||
}
|
||||
|
||||
~ThreadPool() noexcept {
|
||||
if (!_continue) return;
|
||||
if(!_continue) return;
|
||||
|
||||
stop();
|
||||
join();
|
||||
}
|
||||
|
||||
template<class Function, class... Args>
|
||||
auto push(Function && newTask, Args &&... args) {
|
||||
auto push(Function &&newTask, Args &&...args) {
|
||||
std::lock_guard lg(_lock);
|
||||
auto future = TaskPool::push(std::forward<Function>(newTask), std::forward<Args>(args)...);
|
||||
|
||||
@@ -52,7 +53,7 @@ public:
|
||||
}
|
||||
|
||||
template<class Function, class X, class Y, class... Args>
|
||||
auto pushDelayed(Function &&newTask, std::chrono::duration<X, Y> duration, Args &&... args) {
|
||||
auto pushDelayed(Function &&newTask, std::chrono::duration<X, Y> duration, Args &&...args) {
|
||||
std::lock_guard lg(_lock);
|
||||
auto future = TaskPool::pushDelayed(std::forward<Function>(newTask), duration, std::forward<Args>(args)...);
|
||||
|
||||
@@ -79,15 +80,14 @@ public:
|
||||
}
|
||||
|
||||
void join() {
|
||||
for (auto & t : _thread) {
|
||||
for(auto &t : _thread) {
|
||||
t.join();
|
||||
}
|
||||
}
|
||||
|
||||
public:
|
||||
|
||||
void _main() {
|
||||
while (_continue) {
|
||||
while(_continue) {
|
||||
if(auto task = this->pop()) {
|
||||
(*task)->run();
|
||||
}
|
||||
@@ -117,5 +117,5 @@ public:
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
} // namespace util
|
||||
#endif
|
||||
|
||||
@@ -5,63 +5,90 @@
|
||||
#ifndef SUNSHINE_THREAD_SAFE_H
|
||||
#define SUNSHINE_THREAD_SAFE_H
|
||||
|
||||
#include <vector>
|
||||
#include <mutex>
|
||||
#include <atomic>
|
||||
#include <condition_variable>
|
||||
#include <functional>
|
||||
#include <mutex>
|
||||
#include <vector>
|
||||
|
||||
#include "utility.h"
|
||||
|
||||
namespace safe {
|
||||
template<class T>
|
||||
class event_t {
|
||||
public:
|
||||
using status_t = util::optional_t<T>;
|
||||
|
||||
public:
|
||||
template<class...Args>
|
||||
template<class... Args>
|
||||
void raise(Args &&...args) {
|
||||
std::lock_guard lg { _lock };
|
||||
if(!_continue) {
|
||||
return;
|
||||
}
|
||||
|
||||
_status = status_t { std::forward<Args>(args)... };
|
||||
if constexpr(std::is_same_v<std::optional<T>, status_t>) {
|
||||
_status = std::make_optional<T>(std::forward<Args>(args)...);
|
||||
}
|
||||
else {
|
||||
_status = status_t { std::forward<Args>(args)... };
|
||||
}
|
||||
|
||||
_cv.notify_all();
|
||||
}
|
||||
|
||||
// pop and view shoud not be used interchangebly
|
||||
status_t pop() {
|
||||
std::unique_lock ul{_lock};
|
||||
std::unique_lock ul { _lock };
|
||||
|
||||
if (!_continue) {
|
||||
if(!_continue) {
|
||||
return util::false_v<status_t>;
|
||||
}
|
||||
|
||||
while (!_status) {
|
||||
while(!_status) {
|
||||
_cv.wait(ul);
|
||||
|
||||
if (!_continue) {
|
||||
if(!_continue) {
|
||||
return util::false_v<status_t>;
|
||||
}
|
||||
}
|
||||
|
||||
auto val = std::move(_status);
|
||||
_status = util::false_v<status_t>;
|
||||
_status = util::false_v<status_t>;
|
||||
return val;
|
||||
}
|
||||
|
||||
// pop and view shoud not be used interchangebly
|
||||
template<class Rep, class Period>
|
||||
status_t pop(std::chrono::duration<Rep, Period> delay) {
|
||||
std::unique_lock ul { _lock };
|
||||
|
||||
if(!_continue) {
|
||||
return util::false_v<status_t>;
|
||||
}
|
||||
|
||||
while(!_status) {
|
||||
if(!_continue || _cv.wait_for(ul, delay) == std::cv_status::timeout) {
|
||||
return util::false_v<status_t>;
|
||||
}
|
||||
}
|
||||
|
||||
auto val = std::move(_status);
|
||||
_status = util::false_v<status_t>;
|
||||
return val;
|
||||
}
|
||||
|
||||
// pop and view shoud not be used interchangebly
|
||||
const status_t &view() {
|
||||
std::unique_lock ul{_lock};
|
||||
std::unique_lock ul { _lock };
|
||||
|
||||
if (!_continue) {
|
||||
if(!_continue) {
|
||||
return util::false_v<status_t>;
|
||||
}
|
||||
|
||||
while (!_status) {
|
||||
while(!_status) {
|
||||
_cv.wait(ul);
|
||||
|
||||
if (!_continue) {
|
||||
if(!_continue) {
|
||||
return util::false_v<status_t>;
|
||||
}
|
||||
}
|
||||
@@ -72,42 +99,143 @@ public:
|
||||
bool peek() {
|
||||
std::lock_guard lg { _lock };
|
||||
|
||||
return (bool)_status;
|
||||
return _continue && (bool)_status;
|
||||
}
|
||||
|
||||
void stop() {
|
||||
std::lock_guard lg{_lock};
|
||||
std::lock_guard lg { _lock };
|
||||
|
||||
_continue = false;
|
||||
|
||||
_cv.notify_all();
|
||||
}
|
||||
|
||||
bool running() const {
|
||||
void reset() {
|
||||
std::lock_guard lg { _lock };
|
||||
|
||||
_continue = true;
|
||||
|
||||
_status = util::false_v<status_t>;
|
||||
}
|
||||
|
||||
[[nodiscard]] bool running() const {
|
||||
return _continue;
|
||||
}
|
||||
private:
|
||||
|
||||
bool _continue{true};
|
||||
status_t _status;
|
||||
private:
|
||||
bool _continue { true };
|
||||
status_t _status { util::false_v<status_t> };
|
||||
|
||||
std::condition_variable _cv;
|
||||
std::mutex _lock;
|
||||
};
|
||||
|
||||
template<class T>
|
||||
class queue_t {
|
||||
class alarm_raw_t {
|
||||
public:
|
||||
using status_t = util::optional_t<T>;
|
||||
|
||||
alarm_raw_t() : _status { util::false_v<status_t> } {}
|
||||
|
||||
void ring(const status_t &status) {
|
||||
std::lock_guard lg(_lock);
|
||||
|
||||
_status = status;
|
||||
_cv.notify_one();
|
||||
}
|
||||
|
||||
void ring(status_t &&status) {
|
||||
std::lock_guard lg(_lock);
|
||||
|
||||
_status = std::move(status);
|
||||
_cv.notify_one();
|
||||
}
|
||||
|
||||
template<class Rep, class Period>
|
||||
auto wait_for(const std::chrono::duration<Rep, Period> &rel_time) {
|
||||
std::unique_lock ul(_lock);
|
||||
|
||||
return _cv.wait_for(ul, rel_time, [this]() { return (bool)status(); });
|
||||
}
|
||||
|
||||
template<class Rep, class Period, class Pred>
|
||||
auto wait_for(const std::chrono::duration<Rep, Period> &rel_time, Pred &&pred) {
|
||||
std::unique_lock ul(_lock);
|
||||
|
||||
return _cv.wait_for(ul, rel_time, [this, &pred]() { return (bool)status() || pred(); });
|
||||
}
|
||||
|
||||
template<class Rep, class Period>
|
||||
auto wait_until(const std::chrono::duration<Rep, Period> &rel_time) {
|
||||
std::unique_lock ul(_lock);
|
||||
|
||||
return _cv.wait_until(ul, rel_time, [this]() { return (bool)status(); });
|
||||
}
|
||||
|
||||
template<class Rep, class Period, class Pred>
|
||||
auto wait_until(const std::chrono::duration<Rep, Period> &rel_time, Pred &&pred) {
|
||||
std::unique_lock ul(_lock);
|
||||
|
||||
return _cv.wait_until(ul, rel_time, [this, &pred]() { return (bool)status() || pred(); });
|
||||
}
|
||||
|
||||
auto wait() {
|
||||
std::unique_lock ul(_lock);
|
||||
_cv.wait(ul, [this]() { return (bool)status(); });
|
||||
}
|
||||
|
||||
template<class Pred>
|
||||
auto wait(Pred &&pred) {
|
||||
std::unique_lock ul(_lock);
|
||||
_cv.wait(ul, [this, &pred]() { return (bool)status() || pred(); });
|
||||
}
|
||||
|
||||
const status_t &status() const {
|
||||
return _status;
|
||||
}
|
||||
|
||||
status_t &status() {
|
||||
return _status;
|
||||
}
|
||||
|
||||
void reset() {
|
||||
_status = status_t {};
|
||||
}
|
||||
|
||||
private:
|
||||
std::mutex _lock;
|
||||
std::condition_variable _cv;
|
||||
|
||||
status_t _status;
|
||||
};
|
||||
|
||||
template<class T>
|
||||
using alarm_t = std::shared_ptr<alarm_raw_t<T>>;
|
||||
|
||||
template<class T>
|
||||
alarm_t<T> make_alarm() {
|
||||
return std::make_shared<alarm_raw_t<T>>();
|
||||
}
|
||||
|
||||
template<class T>
|
||||
class queue_t {
|
||||
public:
|
||||
template<class ...Args>
|
||||
void raise(Args &&... args) {
|
||||
std::lock_guard lg{_lock};
|
||||
using status_t = util::optional_t<T>;
|
||||
|
||||
queue_t(std::uint32_t max_elements) : _max_elements { max_elements } {}
|
||||
|
||||
template<class... Args>
|
||||
void raise(Args &&...args) {
|
||||
std::lock_guard ul { _lock };
|
||||
|
||||
if(!_continue) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(_queue.size() == _max_elements) {
|
||||
_queue.clear();
|
||||
}
|
||||
|
||||
_queue.emplace_back(std::forward<Args>(args)...);
|
||||
|
||||
_cv.notify_all();
|
||||
@@ -116,20 +244,40 @@ public:
|
||||
bool peek() {
|
||||
std::lock_guard lg { _lock };
|
||||
|
||||
return !_queue.empty();
|
||||
return _continue && !_queue.empty();
|
||||
}
|
||||
|
||||
status_t pop() {
|
||||
std::unique_lock ul{_lock};
|
||||
template<class Rep, class Period>
|
||||
status_t pop(std::chrono::duration<Rep, Period> delay) {
|
||||
std::unique_lock ul { _lock };
|
||||
|
||||
if (!_continue) {
|
||||
if(!_continue) {
|
||||
return util::false_v<status_t>;
|
||||
}
|
||||
|
||||
while (_queue.empty()) {
|
||||
while(_queue.empty()) {
|
||||
if(!_continue || _cv.wait_for(ul, delay) == std::cv_status::timeout) {
|
||||
return util::false_v<status_t>;
|
||||
}
|
||||
}
|
||||
|
||||
auto val = std::move(_queue.front());
|
||||
_queue.erase(std::begin(_queue));
|
||||
|
||||
return val;
|
||||
}
|
||||
|
||||
status_t pop() {
|
||||
std::unique_lock ul { _lock };
|
||||
|
||||
if(!_continue) {
|
||||
return util::false_v<status_t>;
|
||||
}
|
||||
|
||||
while(_queue.empty()) {
|
||||
_cv.wait(ul);
|
||||
|
||||
if (!_continue) {
|
||||
if(!_continue) {
|
||||
return util::false_v<status_t>;
|
||||
}
|
||||
}
|
||||
@@ -145,7 +293,7 @@ public:
|
||||
}
|
||||
|
||||
void stop() {
|
||||
std::lock_guard lg{_lock};
|
||||
std::lock_guard lg { _lock };
|
||||
|
||||
_continue = false;
|
||||
|
||||
@@ -157,14 +305,127 @@ public:
|
||||
}
|
||||
|
||||
private:
|
||||
|
||||
bool _continue{true};
|
||||
bool _continue { true };
|
||||
std::uint32_t _max_elements;
|
||||
|
||||
std::mutex _lock;
|
||||
std::condition_variable _cv;
|
||||
|
||||
std::vector<T> _queue;
|
||||
};
|
||||
|
||||
template<class T>
|
||||
class shared_t {
|
||||
public:
|
||||
using element_type = T;
|
||||
|
||||
using construct_f = std::function<int(element_type &)>;
|
||||
using destruct_f = std::function<void(element_type &)>;
|
||||
|
||||
struct ptr_t {
|
||||
shared_t *owner;
|
||||
|
||||
ptr_t() : owner { nullptr } {}
|
||||
explicit ptr_t(shared_t *owner) : owner { owner } {}
|
||||
|
||||
ptr_t(ptr_t &&ptr) noexcept : owner { ptr.owner } {
|
||||
ptr.owner = nullptr;
|
||||
}
|
||||
|
||||
ptr_t(const ptr_t &ptr) noexcept : owner { ptr.owner } {
|
||||
if(!owner) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto tmp = ptr.owner->ref();
|
||||
tmp.owner = nullptr;
|
||||
}
|
||||
|
||||
ptr_t &operator=(const ptr_t &ptr) noexcept {
|
||||
if(!ptr.owner) {
|
||||
release();
|
||||
|
||||
return *this;
|
||||
}
|
||||
|
||||
return *this = std::move(*ptr.owner->ref());
|
||||
}
|
||||
|
||||
ptr_t &operator=(ptr_t &&ptr) noexcept {
|
||||
if(owner) {
|
||||
release();
|
||||
}
|
||||
|
||||
std::swap(owner, ptr.owner);
|
||||
|
||||
return *this;
|
||||
}
|
||||
|
||||
~ptr_t() {
|
||||
if(owner) {
|
||||
release();
|
||||
}
|
||||
}
|
||||
|
||||
operator bool() const {
|
||||
return owner != nullptr;
|
||||
}
|
||||
|
||||
void release() {
|
||||
std::lock_guard lg { owner->_lock };
|
||||
|
||||
if(!--owner->_count) {
|
||||
owner->_destruct(*get());
|
||||
(*this)->~element_type();
|
||||
}
|
||||
|
||||
owner = nullptr;
|
||||
}
|
||||
|
||||
element_type *get() const {
|
||||
return reinterpret_cast<element_type *>(owner->_object_buf.data());
|
||||
}
|
||||
|
||||
element_type *operator->() {
|
||||
return reinterpret_cast<element_type *>(owner->_object_buf.data());
|
||||
}
|
||||
};
|
||||
|
||||
template<class FC, class FD>
|
||||
shared_t(FC &&fc, FD &&fd) : _construct { std::forward<FC>(fc) }, _destruct { std::forward<FD>(fd) } {}
|
||||
[[nodiscard]] ptr_t ref() {
|
||||
std::lock_guard lg { _lock };
|
||||
|
||||
if(!_count) {
|
||||
new(_object_buf.data()) element_type;
|
||||
if(_construct(*reinterpret_cast<element_type *>(_object_buf.data()))) {
|
||||
return ptr_t { nullptr };
|
||||
}
|
||||
}
|
||||
|
||||
++_count;
|
||||
|
||||
return ptr_t { this };
|
||||
}
|
||||
|
||||
private:
|
||||
construct_f _construct;
|
||||
destruct_f _destruct;
|
||||
|
||||
std::array<std::uint8_t, sizeof(element_type)> _object_buf;
|
||||
|
||||
std::uint32_t _count;
|
||||
std::mutex _lock;
|
||||
};
|
||||
|
||||
template<class T, class F_Construct, class F_Destruct>
|
||||
auto make_shared(F_Construct &&fc, F_Destruct &&fd) {
|
||||
return shared_t<T> {
|
||||
std::forward<F_Construct>(fc), std::forward<F_Destruct>(fd)
|
||||
};
|
||||
}
|
||||
|
||||
using signal_t = event_t<bool>;
|
||||
} // namespace safe
|
||||
|
||||
#endif //SUNSHINE_THREAD_SAFE_H
|
||||
|
||||
@@ -1,61 +1,109 @@
|
||||
#ifndef UTILITY_H
|
||||
#define UTILITY_H
|
||||
|
||||
#include <algorithm>
|
||||
#include <condition_variable>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <optional>
|
||||
#include <string_view>
|
||||
#include <type_traits>
|
||||
#include <variant>
|
||||
#include <vector>
|
||||
#include <memory>
|
||||
#include <type_traits>
|
||||
#include <algorithm>
|
||||
#include <optional>
|
||||
#include <mutex>
|
||||
#include <condition_variable>
|
||||
#include <string_view>
|
||||
#define KITTY_DECL_CONSTR(x)\
|
||||
x(x&&) noexcept = default;\
|
||||
x&operator=(x&&) noexcept = default;\
|
||||
|
||||
#define KITTY_WHILE_LOOP(x, y, z) \
|
||||
{ \
|
||||
x; \
|
||||
while(y) z \
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
struct argument_type;
|
||||
|
||||
template<typename T, typename U>
|
||||
struct argument_type<T(U)> { typedef U type; };
|
||||
|
||||
#define KITTY_USING_MOVE_T(move_t, t, init_val, z) \
|
||||
class move_t { \
|
||||
public: \
|
||||
using element_type = typename argument_type<void(t)>::type; \
|
||||
\
|
||||
move_t() : el { init_val } {} \
|
||||
template<class... Args> \
|
||||
move_t(Args &&...args) : el { std::forward<Args>(args)... } {} \
|
||||
move_t(const move_t &) = delete; \
|
||||
\
|
||||
move_t(move_t &&other) noexcept : el { std::move(other.el) } { \
|
||||
other.el = element_type { init_val }; \
|
||||
} \
|
||||
\
|
||||
move_t &operator=(const move_t &) = delete; \
|
||||
\
|
||||
move_t &operator=(move_t &&other) { \
|
||||
std::swap(el, other.el); \
|
||||
return *this; \
|
||||
} \
|
||||
element_type *operator->() { return ⪙ } \
|
||||
const element_type *operator->() const { return ⪙ } \
|
||||
\
|
||||
~move_t() z \
|
||||
\
|
||||
element_type el; \
|
||||
}
|
||||
|
||||
#define KITTY_DECL_CONSTR(x) \
|
||||
x(x &&) noexcept = default; \
|
||||
x &operator=(x &&) noexcept = default; \
|
||||
x();
|
||||
|
||||
#define KITTY_DEFAULT_CONSTR(x)\
|
||||
x(x&&) noexcept = default;\
|
||||
x&operator=(x&&) noexcept = default;\
|
||||
x() = default;
|
||||
#define KITTY_DEFAULT_CONSTR(x) \
|
||||
x(x &&) noexcept = default; \
|
||||
x &operator=(x &&) noexcept = default; \
|
||||
x() = default;
|
||||
|
||||
#define KITTY_DEFAULT_CONSTR_THROW(x)\
|
||||
x(x&&) = default;\
|
||||
x&operator=(x&&) = default;\
|
||||
x() = default;
|
||||
#define KITTY_DEFAULT_CONSTR_THROW(x) \
|
||||
x(x &&) = default; \
|
||||
x &operator=(x &&) = default; \
|
||||
x() = default;
|
||||
|
||||
#define TUPLE_2D(a,b, expr)\
|
||||
decltype(expr) a##_##b = expr;\
|
||||
auto &a = std::get<0>(a##_##b);\
|
||||
auto &b = std::get<1>(a##_##b)
|
||||
#define TUPLE_2D(a, b, expr) \
|
||||
decltype(expr) a##_##b = expr; \
|
||||
auto &a = std::get<0>(a##_##b); \
|
||||
auto &b = std::get<1>(a##_##b)
|
||||
|
||||
#define TUPLE_2D_REF(a,b, expr)\
|
||||
auto &a##_##b = expr;\
|
||||
auto &a = std::get<0>(a##_##b);\
|
||||
auto &b = std::get<1>(a##_##b)
|
||||
#define TUPLE_2D_REF(a, b, expr) \
|
||||
auto &a##_##b = expr; \
|
||||
auto &a = std::get<0>(a##_##b); \
|
||||
auto &b = std::get<1>(a##_##b)
|
||||
|
||||
#define TUPLE_3D(a,b,c, expr)\
|
||||
decltype(expr) a##_##b##_##c = expr;\
|
||||
auto &a = std::get<0>(a##_##b##_##c);\
|
||||
auto &b = std::get<1>(a##_##b##_##c);\
|
||||
auto &c = std::get<2>(a##_##b##_##c)
|
||||
#define TUPLE_3D(a, b, c, expr) \
|
||||
decltype(expr) a##_##b##_##c = expr; \
|
||||
auto &a = std::get<0>(a##_##b##_##c); \
|
||||
auto &b = std::get<1>(a##_##b##_##c); \
|
||||
auto &c = std::get<2>(a##_##b##_##c)
|
||||
|
||||
#define TUPLE_3D_REF(a,b,c, expr)\
|
||||
auto &a##_##b##_##c = expr;\
|
||||
auto &a = std::get<0>(a##_##b##_##c);\
|
||||
auto &b = std::get<1>(a##_##b##_##c);\
|
||||
auto &c = std::get<2>(a##_##b##_##c)
|
||||
#define TUPLE_3D_REF(a, b, c, expr) \
|
||||
auto &a##_##b##_##c = expr; \
|
||||
auto &a = std::get<0>(a##_##b##_##c); \
|
||||
auto &b = std::get<1>(a##_##b##_##c); \
|
||||
auto &c = std::get<2>(a##_##b##_##c)
|
||||
|
||||
#define TUPLE_EL(a, b, expr) \
|
||||
decltype(expr) a##_ = expr; \
|
||||
auto &a = std::get<b>(a##_)
|
||||
|
||||
#define TUPLE_EL_REF(a, b, expr) \
|
||||
auto &a = std::get<b>(expr)
|
||||
|
||||
namespace util {
|
||||
|
||||
template<template<typename...> class X, class...Y>
|
||||
template<template<typename...> class X, class... Y>
|
||||
struct __instantiation_of : public std::false_type {};
|
||||
|
||||
template<template<typename...> class X, class... Y>
|
||||
struct __instantiation_of<X, X<Y...>> : public std::true_type {};
|
||||
|
||||
template<template<typename...> class X, class T, class...Y>
|
||||
template<template<typename...> class X, class T, class... Y>
|
||||
static constexpr auto instantiation_of_v = __instantiation_of<X, T, Y...>::value;
|
||||
|
||||
template<bool V, class X, class Y>
|
||||
@@ -74,42 +122,16 @@ struct __either<false, X, Y> {
|
||||
template<bool V, class X, class Y>
|
||||
using either_t = typename __either<V, X, Y>::type;
|
||||
|
||||
template<class T, class V = void>
|
||||
struct __false_v;
|
||||
|
||||
template<class T>
|
||||
struct __false_v<T, std::enable_if_t<instantiation_of_v<std::optional, T>>> {
|
||||
static constexpr std::nullopt_t value = std::nullopt;
|
||||
};
|
||||
|
||||
template<class T>
|
||||
struct __false_v<T, std::enable_if_t<
|
||||
(std::is_pointer_v<T> || instantiation_of_v<std::unique_ptr, T> || instantiation_of_v<std::shared_ptr, T>)
|
||||
>> {
|
||||
static constexpr std::nullptr_t value = nullptr;
|
||||
};
|
||||
|
||||
template<class T>
|
||||
struct __false_v<T, std::enable_if_t<std::is_same_v<T, bool>>> {
|
||||
static constexpr bool value = false;
|
||||
};
|
||||
|
||||
template<class T>
|
||||
static constexpr auto false_v = __false_v<T>::value;
|
||||
|
||||
template<class T>
|
||||
using optional_t = either_t<
|
||||
(std::is_same_v<T, bool> ||
|
||||
instantiation_of_v<std::unique_ptr, T> ||
|
||||
instantiation_of_v<std::shared_ptr, T> ||
|
||||
std::is_pointer_v<T>),
|
||||
T, std::optional<T>>;
|
||||
template<class... Ts>
|
||||
struct overloaded : Ts... { using Ts::operator()...; };
|
||||
template<class... Ts>
|
||||
overloaded(Ts...) -> overloaded<Ts...>;
|
||||
|
||||
template<class T>
|
||||
class FailGuard {
|
||||
public:
|
||||
FailGuard() = delete;
|
||||
FailGuard(T && f) noexcept : _func { std::forward<T>(f) } {}
|
||||
FailGuard(T &&f) noexcept : _func { std::forward<T>(f) } {}
|
||||
FailGuard(FailGuard &&other) noexcept : _func { std::move(other._func) } {
|
||||
this->failure = other.failure;
|
||||
|
||||
@@ -129,12 +151,13 @@ public:
|
||||
|
||||
void disable() { failure = false; }
|
||||
bool failure { true };
|
||||
|
||||
private:
|
||||
T _func;
|
||||
};
|
||||
|
||||
template<class T>
|
||||
[[nodiscard]] auto fail_guard(T && f) {
|
||||
[[nodiscard]] auto fail_guard(T &&f) {
|
||||
return FailGuard<T> { std::forward<T>(f) };
|
||||
}
|
||||
|
||||
@@ -144,9 +167,9 @@ void append_struct(std::vector<uint8_t> &buf, const T &_struct) {
|
||||
|
||||
buf.reserve(data_len);
|
||||
|
||||
auto *data = (uint8_t *) & _struct;
|
||||
auto *data = (uint8_t *)&_struct;
|
||||
|
||||
for (size_t x = 0; x < data_len; ++x) {
|
||||
for(size_t x = 0; x < data_len; ++x) {
|
||||
buf.push_back(data[x]);
|
||||
}
|
||||
}
|
||||
@@ -155,24 +178,26 @@ template<class T>
|
||||
class Hex {
|
||||
public:
|
||||
typedef T elem_type;
|
||||
|
||||
private:
|
||||
const char _bits[16] {
|
||||
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'
|
||||
};
|
||||
|
||||
char _hex[sizeof(elem_type) * 2];
|
||||
|
||||
public:
|
||||
Hex(const elem_type &elem, bool rev) {
|
||||
if(!rev) {
|
||||
const uint8_t *data = reinterpret_cast<const uint8_t *>(&elem) + sizeof(elem_type) - 1;
|
||||
for (auto it = begin(); it < cend();) {
|
||||
for(auto it = begin(); it < cend();) {
|
||||
*it++ = _bits[*data / 16];
|
||||
*it++ = _bits[*data-- % 16];
|
||||
}
|
||||
}
|
||||
else {
|
||||
const uint8_t *data = reinterpret_cast<const uint8_t *>(&elem);
|
||||
for (auto it = begin(); it < cend();) {
|
||||
for(auto it = begin(); it < cend();) {
|
||||
*it++ = _bits[*data / 16];
|
||||
*it++ = _bits[*data++ % 16];
|
||||
}
|
||||
@@ -204,7 +229,7 @@ Hex<T> hex(const T &elem, bool rev = false) {
|
||||
|
||||
template<class It>
|
||||
std::string hex_vec(It begin, It end, bool rev = false) {
|
||||
auto str_size = 2*std::distance(begin, end);
|
||||
auto str_size = 2 * std::distance(begin, end);
|
||||
|
||||
|
||||
std::string hex;
|
||||
@@ -215,14 +240,14 @@ std::string hex_vec(It begin, It end, bool rev = false) {
|
||||
};
|
||||
|
||||
if(rev) {
|
||||
for (auto it = std::begin(hex); it < std::end(hex);) {
|
||||
for(auto it = std::begin(hex); it < std::end(hex);) {
|
||||
*it++ = _bits[((uint8_t)*begin) / 16];
|
||||
*it++ = _bits[((uint8_t)*begin++) % 16];
|
||||
}
|
||||
}
|
||||
else {
|
||||
--end;
|
||||
for (auto it = std::begin(hex); it < std::end(hex);) {
|
||||
for(auto it = std::begin(hex); it < std::end(hex);) {
|
||||
*it++ = _bits[((uint8_t)*end) / 16];
|
||||
*it++ = _bits[((uint8_t)*end--) % 16];
|
||||
}
|
||||
@@ -233,7 +258,7 @@ std::string hex_vec(It begin, It end, bool rev = false) {
|
||||
}
|
||||
|
||||
template<class C>
|
||||
std::string hex_vec(C&& c, bool rev = false) {
|
||||
std::string hex_vec(C &&c, bool rev = false) {
|
||||
return hex_vec(std::begin(c), std::end(c), rev);
|
||||
}
|
||||
|
||||
@@ -242,7 +267,7 @@ std::optional<T> from_hex(const std::string_view &hex, bool rev = false) {
|
||||
std::uint8_t buf[sizeof(T)];
|
||||
|
||||
static char constexpr shift_bit = 'a' - 'A';
|
||||
auto is_convertable = [] (char ch) -> bool {
|
||||
auto is_convertable = [](char ch) -> bool {
|
||||
if(isdigit(ch)) {
|
||||
return true;
|
||||
}
|
||||
@@ -261,9 +286,9 @@ std::optional<T> from_hex(const std::string_view &hex, bool rev = false) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
const char *data = hex.data() + hex.size() -1;
|
||||
const char *data = hex.data() + hex.size() - 1;
|
||||
|
||||
auto convert = [] (char ch) -> std::uint8_t {
|
||||
auto convert = [](char ch) -> std::uint8_t {
|
||||
if(ch >= '0' && ch <= '9') {
|
||||
return (std::uint8_t)ch - '0';
|
||||
}
|
||||
@@ -292,7 +317,7 @@ inline std::string from_hex_vec(const std::string &hex, bool rev = false) {
|
||||
std::string buf;
|
||||
|
||||
static char constexpr shift_bit = 'a' - 'A';
|
||||
auto is_convertable = [] (char ch) -> bool {
|
||||
auto is_convertable = [](char ch) -> bool {
|
||||
if(isdigit(ch)) {
|
||||
return true;
|
||||
}
|
||||
@@ -309,9 +334,9 @@ inline std::string from_hex_vec(const std::string &hex, bool rev = false) {
|
||||
auto buf_size = std::count_if(std::begin(hex), std::end(hex), is_convertable) / 2;
|
||||
buf.resize(buf_size);
|
||||
|
||||
const char *data = hex.data() + hex.size() -1;
|
||||
const char *data = hex.data() + hex.size() - 1;
|
||||
|
||||
auto convert = [] (char ch) -> std::uint8_t {
|
||||
auto convert = [](char ch) -> std::uint8_t {
|
||||
if(ch >= '0' && ch <= '9') {
|
||||
return (std::uint8_t)ch - '0';
|
||||
}
|
||||
@@ -343,112 +368,20 @@ public:
|
||||
std::size_t operator()(const value_type &value) const {
|
||||
const auto *p = reinterpret_cast<const char *>(&value);
|
||||
|
||||
return std::hash<std::string_view>{}(std::string_view { p, sizeof(value_type) });
|
||||
return std::hash<std::string_view> {}(std::string_view { p, sizeof(value_type) });
|
||||
}
|
||||
};
|
||||
|
||||
template<class T>
|
||||
auto enm(const T& val) -> const std::underlying_type_t<T>& {
|
||||
return *reinterpret_cast<const std::underlying_type_t<T>*>(&val);
|
||||
auto enm(const T &val) -> const std::underlying_type_t<T> & {
|
||||
return *reinterpret_cast<const std::underlying_type_t<T> *>(&val);
|
||||
}
|
||||
|
||||
template<class T>
|
||||
auto enm(T& val) -> std::underlying_type_t<T>& {
|
||||
return *reinterpret_cast<std::underlying_type_t<T>*>(&val);
|
||||
auto enm(T &val) -> std::underlying_type_t<T> & {
|
||||
return *reinterpret_cast<std::underlying_type_t<T> *>(&val);
|
||||
}
|
||||
|
||||
template<class ReturnType, class ...Args>
|
||||
struct Function {
|
||||
typedef ReturnType (*type)(Args...);
|
||||
};
|
||||
|
||||
template<class T, class ReturnType, typename Function<ReturnType, T>::type function>
|
||||
struct Destroy {
|
||||
typedef T pointer;
|
||||
|
||||
void operator()(pointer p) {
|
||||
function(p);
|
||||
}
|
||||
};
|
||||
|
||||
template<class T, typename Function<void, T*>::type function>
|
||||
using safe_ptr = std::unique_ptr<T, Destroy<T*, void, function>>;
|
||||
|
||||
// You cannot specialize an alias
|
||||
template<class T, class ReturnType, typename Function<ReturnType, T*>::type function>
|
||||
using safe_ptr_v2 = std::unique_ptr<T, Destroy<T*, ReturnType, function>>;
|
||||
|
||||
template<class T>
|
||||
void c_free(T *p) {
|
||||
free(p);
|
||||
}
|
||||
|
||||
template<class T>
|
||||
using c_ptr = safe_ptr<T, c_free<T>>;
|
||||
|
||||
template<class T>
|
||||
class FakeContainer {
|
||||
typedef T pointer;
|
||||
|
||||
pointer _begin;
|
||||
pointer _end;
|
||||
|
||||
public:
|
||||
FakeContainer(pointer begin, pointer end) : _begin(begin), _end(end) {}
|
||||
|
||||
pointer begin() { return _begin; }
|
||||
pointer end() { return _end; }
|
||||
|
||||
const pointer begin() const { return _begin; }
|
||||
const pointer end() const { return _end; }
|
||||
|
||||
const pointer cbegin() const { return _begin; }
|
||||
const pointer cend() const { return _end; }
|
||||
|
||||
pointer data() { return begin(); }
|
||||
const pointer data() const { return cbegin(); }
|
||||
|
||||
std::size_t size() const { return std::distance(begin(), end()); }
|
||||
};
|
||||
|
||||
template<class T>
|
||||
FakeContainer<T> toContainer(T begin, T end) {
|
||||
return { begin, end };
|
||||
}
|
||||
|
||||
template<class T>
|
||||
FakeContainer<T> toContainer(T begin, std::size_t end) {
|
||||
return { begin, begin + end };
|
||||
}
|
||||
|
||||
template<class T>
|
||||
FakeContainer<T*> toContainer(T * const begin) {
|
||||
T *end = begin;
|
||||
|
||||
auto default_val = T();
|
||||
while(*end != default_val) {
|
||||
++end;
|
||||
}
|
||||
|
||||
return toContainer(begin, end);
|
||||
}
|
||||
|
||||
template<class T, class H>
|
||||
struct _init_helper;
|
||||
|
||||
template<template<class...> class T, class H, class... Args>
|
||||
struct _init_helper<T<Args...>, H> {
|
||||
using type = T<Args...>;
|
||||
|
||||
static type move(Args&&... args, H&&) {
|
||||
return std::make_tuple(std::move(args)...);
|
||||
}
|
||||
|
||||
static type copy(const Args&... args, const H&) {
|
||||
return std::make_tuple(args...);
|
||||
}
|
||||
};
|
||||
|
||||
inline std::int64_t from_chars(const char *begin, const char *end) {
|
||||
std::int64_t res {};
|
||||
std::int64_t mul = 1;
|
||||
@@ -494,15 +427,281 @@ public:
|
||||
}
|
||||
};
|
||||
|
||||
// Compared to std::unique_ptr, it adds the ability to get the address of the pointer itself
|
||||
template<typename T, typename D = std::default_delete<T>>
|
||||
class uniq_ptr {
|
||||
public:
|
||||
using element_type = T;
|
||||
using pointer = element_type *;
|
||||
using deleter_type = D;
|
||||
|
||||
constexpr uniq_ptr() noexcept : _p { nullptr } {}
|
||||
constexpr uniq_ptr(std::nullptr_t) noexcept : _p { nullptr } {}
|
||||
|
||||
uniq_ptr(const uniq_ptr &other) noexcept = delete;
|
||||
uniq_ptr &operator=(const uniq_ptr &other) noexcept = delete;
|
||||
|
||||
template<class V>
|
||||
uniq_ptr(V *p) noexcept : _p { p } {
|
||||
static_assert(std::is_same_v<element_type, void> || std::is_same_v<element_type, V> || std::is_base_of_v<element_type, V>, "element_type must be base class of V");
|
||||
}
|
||||
|
||||
template<class V>
|
||||
uniq_ptr(std::unique_ptr<V, deleter_type> &&uniq) noexcept : _p { uniq.release() } {
|
||||
static_assert(std::is_same_v<element_type, void> || std::is_same_v<T, V> || std::is_base_of_v<element_type, V>, "element_type must be base class of V");
|
||||
}
|
||||
|
||||
template<class V>
|
||||
uniq_ptr(uniq_ptr<V, deleter_type> &&other) noexcept : _p { other.release() } {
|
||||
static_assert(std::is_same_v<element_type, void> || std::is_same_v<T, V> || std::is_base_of_v<element_type, V>, "element_type must be base class of V");
|
||||
}
|
||||
|
||||
template<class V>
|
||||
uniq_ptr &operator=(uniq_ptr<V, deleter_type> &&other) noexcept {
|
||||
static_assert(std::is_same_v<element_type, void> || std::is_same_v<T, V> || std::is_base_of_v<element_type, V>, "element_type must be base class of V");
|
||||
reset(other.release());
|
||||
|
||||
return *this;
|
||||
}
|
||||
|
||||
template<class V>
|
||||
uniq_ptr &operator=(std::unique_ptr<V, deleter_type> &&uniq) noexcept {
|
||||
static_assert(std::is_same_v<element_type, void> || std::is_same_v<T, V> || std::is_base_of_v<element_type, V>, "element_type must be base class of V");
|
||||
|
||||
reset(uniq.release());
|
||||
|
||||
return *this;
|
||||
}
|
||||
|
||||
~uniq_ptr() {
|
||||
reset();
|
||||
}
|
||||
|
||||
void reset(pointer p = pointer()) {
|
||||
if(_p) {
|
||||
_deleter(_p);
|
||||
}
|
||||
|
||||
_p = p;
|
||||
}
|
||||
|
||||
pointer release() {
|
||||
auto tmp = _p;
|
||||
_p = nullptr;
|
||||
return tmp;
|
||||
}
|
||||
|
||||
pointer get() {
|
||||
return _p;
|
||||
}
|
||||
|
||||
const pointer get() const {
|
||||
return _p;
|
||||
}
|
||||
|
||||
const std::add_lvalue_reference_t<element_type> operator*() const {
|
||||
return *_p;
|
||||
}
|
||||
std::add_lvalue_reference_t<element_type> operator*() {
|
||||
return *_p;
|
||||
}
|
||||
const pointer operator->() const {
|
||||
return _p;
|
||||
}
|
||||
pointer operator->() {
|
||||
return _p;
|
||||
}
|
||||
pointer *operator&() const {
|
||||
return &_p;
|
||||
}
|
||||
|
||||
pointer *operator&() {
|
||||
return &_p;
|
||||
}
|
||||
|
||||
deleter_type &get_deleter() {
|
||||
return _deleter;
|
||||
}
|
||||
|
||||
const deleter_type &get_deleter() const {
|
||||
return _deleter;
|
||||
}
|
||||
|
||||
explicit operator bool() const {
|
||||
return _p != nullptr;
|
||||
}
|
||||
|
||||
protected:
|
||||
pointer _p;
|
||||
deleter_type _deleter;
|
||||
};
|
||||
|
||||
template<class T1, class D1, class T2, class D2>
|
||||
bool operator==(const uniq_ptr<T1, D1> &x, const uniq_ptr<T2, D2> &y) {
|
||||
return x.get() == y.get();
|
||||
}
|
||||
|
||||
template<class T1, class D1, class T2, class D2>
|
||||
bool operator!=(const uniq_ptr<T1, D1> &x, const uniq_ptr<T2, D2> &y) {
|
||||
return x.get() != y.get();
|
||||
}
|
||||
|
||||
template<class T1, class D1, class T2, class D2>
|
||||
bool operator==(const std::unique_ptr<T1, D1> &x, const uniq_ptr<T2, D2> &y) {
|
||||
return x.get() == y.get();
|
||||
}
|
||||
|
||||
template<class T1, class D1, class T2, class D2>
|
||||
bool operator!=(const std::unique_ptr<T1, D1> &x, const uniq_ptr<T2, D2> &y) {
|
||||
return x.get() != y.get();
|
||||
}
|
||||
|
||||
template<class T1, class D1, class T2, class D2>
|
||||
bool operator==(const uniq_ptr<T1, D1> &x, const std::unique_ptr<T1, D1> &y) {
|
||||
return x.get() == y.get();
|
||||
}
|
||||
|
||||
template<class T1, class D1, class T2, class D2>
|
||||
bool operator!=(const uniq_ptr<T1, D1> &x, const std::unique_ptr<T1, D1> &y) {
|
||||
return x.get() != y.get();
|
||||
}
|
||||
|
||||
template<class T, class D>
|
||||
bool operator==(const uniq_ptr<T, D> &x, std::nullptr_t) {
|
||||
return !(bool)x;
|
||||
}
|
||||
|
||||
template<class T, class D>
|
||||
bool operator!=(const uniq_ptr<T, D> &x, std::nullptr_t) {
|
||||
return (bool)x;
|
||||
}
|
||||
|
||||
template<class T, class D>
|
||||
bool operator==(std::nullptr_t, const uniq_ptr<T, D> &y) {
|
||||
return !(bool)y;
|
||||
}
|
||||
|
||||
template<class T, class D>
|
||||
bool operator!=(std::nullptr_t, const uniq_ptr<T, D> &y) {
|
||||
return (bool)y;
|
||||
}
|
||||
|
||||
template<class T>
|
||||
class wrap_ptr {
|
||||
public:
|
||||
using element_type = T;
|
||||
using pointer = element_type *;
|
||||
using reference = element_type &;
|
||||
|
||||
wrap_ptr() : _own_ptr { false }, _p { nullptr } {}
|
||||
wrap_ptr(pointer p) : _own_ptr { false }, _p { p } {}
|
||||
wrap_ptr(std::unique_ptr<element_type> &&uniq_p) : _own_ptr { true }, _p { uniq_p.release() } {}
|
||||
wrap_ptr(wrap_ptr &&other) : _own_ptr { other._own_ptr }, _p { other._p } {
|
||||
other._own_ptr = false;
|
||||
}
|
||||
|
||||
wrap_ptr &operator=(wrap_ptr &&other) noexcept {
|
||||
if(_own_ptr) {
|
||||
delete _p;
|
||||
}
|
||||
|
||||
_p = other._p;
|
||||
|
||||
_own_ptr = other._own_ptr;
|
||||
other._own_ptr = false;
|
||||
|
||||
return *this;
|
||||
}
|
||||
|
||||
template<class V>
|
||||
wrap_ptr &operator=(std::unique_ptr<V> &&uniq_ptr) {
|
||||
static_assert(std::is_base_of_v<element_type, V>, "element_type must be base class of V");
|
||||
_own_ptr = true;
|
||||
_p = uniq_ptr.release();
|
||||
|
||||
return *this;
|
||||
}
|
||||
|
||||
wrap_ptr &operator=(pointer p) {
|
||||
if(_own_ptr) {
|
||||
delete _p;
|
||||
}
|
||||
|
||||
_p = p;
|
||||
_own_ptr = false;
|
||||
|
||||
return *this;
|
||||
}
|
||||
|
||||
~wrap_ptr() {
|
||||
if(_own_ptr) {
|
||||
delete _p;
|
||||
}
|
||||
|
||||
_own_ptr = false;
|
||||
}
|
||||
|
||||
const reference operator*() const {
|
||||
return *_p;
|
||||
}
|
||||
reference operator*() {
|
||||
return *_p;
|
||||
}
|
||||
const pointer operator->() const {
|
||||
return _p;
|
||||
}
|
||||
pointer operator->() {
|
||||
return _p;
|
||||
}
|
||||
|
||||
private:
|
||||
bool _own_ptr;
|
||||
pointer _p;
|
||||
};
|
||||
|
||||
template<class T>
|
||||
constexpr bool is_pointer_v =
|
||||
instantiation_of_v<std::unique_ptr, T> ||
|
||||
instantiation_of_v<std::shared_ptr, T> ||
|
||||
instantiation_of_v<uniq_ptr, T> ||
|
||||
std::is_pointer_v<T>;
|
||||
|
||||
template<class T, class V = void>
|
||||
struct __false_v;
|
||||
|
||||
template<class T>
|
||||
struct __false_v<T, std::enable_if_t<instantiation_of_v<std::optional, T>>> {
|
||||
static constexpr std::nullopt_t value = std::nullopt;
|
||||
};
|
||||
|
||||
template<class T>
|
||||
struct __false_v<T, std::enable_if_t<is_pointer_v<T>>> {
|
||||
static constexpr std::nullptr_t value = nullptr;
|
||||
};
|
||||
|
||||
template<class T>
|
||||
struct __false_v<T, std::enable_if_t<std::is_same_v<T, bool>>> {
|
||||
static constexpr bool value = false;
|
||||
};
|
||||
|
||||
template<class T>
|
||||
static constexpr auto false_v = __false_v<T>::value;
|
||||
|
||||
template<class T>
|
||||
using optional_t = either_t<
|
||||
(std::is_same_v<T, bool> || is_pointer_v<T>),
|
||||
T, std::optional<T>>;
|
||||
|
||||
template<class T>
|
||||
class buffer_t {
|
||||
public:
|
||||
buffer_t() : _els { 0 } {};
|
||||
buffer_t(buffer_t&&) noexcept = default;
|
||||
buffer_t &operator=(buffer_t&& other) noexcept {
|
||||
std::swap(_els, other._els);
|
||||
|
||||
_buf = std::move(other._buf);
|
||||
buffer_t(buffer_t &&o) noexcept : _els { o._els }, _buf { std::move(o._buf) } {
|
||||
o._els = 0;
|
||||
}
|
||||
buffer_t &operator=(buffer_t &&o) noexcept {
|
||||
std::swap(_els, o._els);
|
||||
std::swap(_buf, o._buf);
|
||||
|
||||
return *this;
|
||||
};
|
||||
@@ -549,7 +748,6 @@ private:
|
||||
std::unique_ptr<T[]> _buf;
|
||||
};
|
||||
|
||||
|
||||
template<class T>
|
||||
T either(std::optional<T> &&l, T &&r) {
|
||||
if(l) {
|
||||
@@ -559,27 +757,77 @@ T either(std::optional<T> &&l, T &&r) {
|
||||
return std::forward<T>(r);
|
||||
}
|
||||
|
||||
template<class ReturnType, class... Args>
|
||||
struct Function {
|
||||
typedef ReturnType (*type)(Args...);
|
||||
};
|
||||
|
||||
template<class T, class ReturnType, typename Function<ReturnType, T>::type function>
|
||||
struct Destroy {
|
||||
typedef T pointer;
|
||||
|
||||
void operator()(pointer p) {
|
||||
function(p);
|
||||
}
|
||||
};
|
||||
|
||||
template<class T, typename Function<void, T *>::type function>
|
||||
using safe_ptr = uniq_ptr<T, Destroy<T *, void, function>>;
|
||||
|
||||
// You cannot specialize an alias
|
||||
template<class T, class ReturnType, typename Function<ReturnType, T *>::type function>
|
||||
using safe_ptr_v2 = uniq_ptr<T, Destroy<T *, ReturnType, function>>;
|
||||
|
||||
template<class T>
|
||||
void c_free(T *p) {
|
||||
free(p);
|
||||
}
|
||||
|
||||
template<class T, class ReturnType, ReturnType (**function)(T *)>
|
||||
void dynamic(T *p) {
|
||||
(*function)(p);
|
||||
}
|
||||
|
||||
template<class T, void (**function)(T *)>
|
||||
using dyn_safe_ptr = safe_ptr<T, dynamic<T, void, function>>;
|
||||
|
||||
template<class T, class ReturnType, ReturnType (**function)(T *)>
|
||||
using dyn_safe_ptr_v2 = safe_ptr<T, dynamic<T, ReturnType, function>>;
|
||||
|
||||
template<class T>
|
||||
using c_ptr = safe_ptr<T, c_free<T>>;
|
||||
|
||||
template<class It>
|
||||
std::string_view view(It begin, It end) {
|
||||
return std::string_view { (const char *)begin, (std::size_t)(end - begin) };
|
||||
}
|
||||
|
||||
template<class T>
|
||||
std::string_view view(const T &data) {
|
||||
return std::string_view((const char *)&data, sizeof(T));
|
||||
}
|
||||
|
||||
namespace endian {
|
||||
template<class T = void>
|
||||
struct endianness {
|
||||
enum : bool {
|
||||
#if defined(__BYTE_ORDER) && __BYTE_ORDER == __BIG_ENDIAN || \
|
||||
defined(__BIG_ENDIAN__) || \
|
||||
defined(__ARMEB__) || \
|
||||
defined(__THUMBEB__) || \
|
||||
defined(__AARCH64EB__) || \
|
||||
defined(_MIBSEB) || defined(__MIBSEB) || defined(__MIBSEB__)
|
||||
defined(__BIG_ENDIAN__) || \
|
||||
defined(__ARMEB__) || \
|
||||
defined(__THUMBEB__) || \
|
||||
defined(__AARCH64EB__) || \
|
||||
defined(_MIBSEB) || defined(__MIBSEB) || defined(__MIBSEB__)
|
||||
// It's a big-endian target architecture
|
||||
little = false,
|
||||
#elif defined(__BYTE_ORDER) && __BYTE_ORDER == __LITTLE_ENDIAN || \
|
||||
defined(__LITTLE_ENDIAN__) || \
|
||||
defined(__ARMEL__) || \
|
||||
defined(__THUMBEL__) || \
|
||||
defined(__AARCH64EL__) || \
|
||||
defined(_MIPSEL) || defined(__MIPSEL) || defined(__MIPSEL__) || \
|
||||
defined(_WIN32)
|
||||
defined(__LITTLE_ENDIAN__) || \
|
||||
defined(__ARMEL__) || \
|
||||
defined(__THUMBEL__) || \
|
||||
defined(__AARCH64EL__) || \
|
||||
defined(_MIPSEL) || defined(__MIPSEL) || defined(__MIPSEL__) || \
|
||||
defined(_WIN32)
|
||||
// It's a little-endian target architecture
|
||||
little = true,
|
||||
little = true,
|
||||
#else
|
||||
#error "Unknown Endianness"
|
||||
#endif
|
||||
@@ -588,15 +836,14 @@ struct endianness {
|
||||
};
|
||||
|
||||
template<class T, class S = void>
|
||||
struct endian_helper { };
|
||||
struct endian_helper {};
|
||||
|
||||
template<class T>
|
||||
struct endian_helper<T, std::enable_if_t<
|
||||
!(instantiation_of_v<std::optional, T>)
|
||||
>> {
|
||||
!(instantiation_of_v<std::optional, T>)>> {
|
||||
static inline T big(T x) {
|
||||
if constexpr (endianness<T>::little) {
|
||||
uint8_t *data = reinterpret_cast<uint8_t*>(&x);
|
||||
if constexpr(endianness<T>::little) {
|
||||
uint8_t *data = reinterpret_cast<uint8_t *>(&x);
|
||||
|
||||
std::reverse(data, data + sizeof(x));
|
||||
}
|
||||
@@ -605,8 +852,8 @@ struct endian_helper<T, std::enable_if_t<
|
||||
}
|
||||
|
||||
static inline T little(T x) {
|
||||
if constexpr (endianness<T>::big) {
|
||||
uint8_t *data = reinterpret_cast<uint8_t*>(&x);
|
||||
if constexpr(endianness<T>::big) {
|
||||
uint8_t *data = reinterpret_cast<uint8_t *>(&x);
|
||||
|
||||
std::reverse(data, data + sizeof(x));
|
||||
}
|
||||
@@ -617,32 +864,31 @@ struct endian_helper<T, std::enable_if_t<
|
||||
|
||||
template<class T>
|
||||
struct endian_helper<T, std::enable_if_t<
|
||||
instantiation_of_v<std::optional, T>
|
||||
>> {
|
||||
static inline T little(T x) {
|
||||
if(!x) return x;
|
||||
instantiation_of_v<std::optional, T>>> {
|
||||
static inline T little(T x) {
|
||||
if(!x) return x;
|
||||
|
||||
if constexpr (endianness<T>::big) {
|
||||
auto *data = reinterpret_cast<uint8_t*>(&*x);
|
||||
if constexpr(endianness<T>::big) {
|
||||
auto *data = reinterpret_cast<uint8_t *>(&*x);
|
||||
|
||||
std::reverse(data, data + sizeof(*x));
|
||||
std::reverse(data, data + sizeof(*x));
|
||||
}
|
||||
|
||||
return x;
|
||||
}
|
||||
|
||||
return x;
|
||||
}
|
||||
|
||||
static inline T big(T x) {
|
||||
if(!x) return x;
|
||||
|
||||
static inline T big(T x) {
|
||||
if(!x) return x;
|
||||
if constexpr(endianness<T>::big) {
|
||||
auto *data = reinterpret_cast<uint8_t *>(&*x);
|
||||
|
||||
if constexpr (endianness<T>::big) {
|
||||
auto *data = reinterpret_cast<uint8_t*>(&*x);
|
||||
std::reverse(data, data + sizeof(*x));
|
||||
}
|
||||
|
||||
std::reverse(data, data + sizeof(*x));
|
||||
return x;
|
||||
}
|
||||
|
||||
return x;
|
||||
}
|
||||
};
|
||||
|
||||
template<class T>
|
||||
@@ -650,7 +896,6 @@ inline auto little(T x) { return endian_helper<T>::little(x); }
|
||||
|
||||
template<class T>
|
||||
inline auto big(T x) { return endian_helper<T>::big(x); }
|
||||
} /* endian */
|
||||
|
||||
} /* util */
|
||||
} // namespace endian
|
||||
} // namespace util
|
||||
#endif
|
||||
|
||||
@@ -18,12 +18,12 @@ union uuid_t {
|
||||
std::uniform_int_distribution<std::uint8_t> dist(0, std::numeric_limits<std::uint8_t>::max());
|
||||
|
||||
uuid_t buf;
|
||||
for (auto &el : buf.b8) {
|
||||
for(auto &el : buf.b8) {
|
||||
el = dist(engine);
|
||||
}
|
||||
|
||||
buf.b8[7] &= (std::uint8_t) 0b00101111;
|
||||
buf.b8[9] &= (std::uint8_t) 0b10011111;
|
||||
buf.b8[7] &= (std::uint8_t)0b00101111;
|
||||
buf.b8[9] &= (std::uint8_t)0b10011111;
|
||||
|
||||
return buf;
|
||||
}
|
||||
@@ -31,7 +31,7 @@ union uuid_t {
|
||||
static uuid_t generate() {
|
||||
std::random_device r;
|
||||
|
||||
std::default_random_engine engine{r()};
|
||||
std::default_random_engine engine { r() };
|
||||
|
||||
return generate(engine);
|
||||
}
|
||||
@@ -41,7 +41,7 @@ union uuid_t {
|
||||
|
||||
result.reserve(sizeof(uuid_t) * 2 + 4);
|
||||
|
||||
auto hex = util::hex(*this, true);
|
||||
auto hex = util::hex(*this, true);
|
||||
auto hex_view = hex.to_string_view();
|
||||
|
||||
std::string_view slices[] = {
|
||||
@@ -75,5 +75,5 @@ union uuid_t {
|
||||
return (b64[0] > other.b64[0] || (b64[0] == other.b64[0] && b64[1] > other.b64[1]));
|
||||
}
|
||||
};
|
||||
}
|
||||
} // namespace util
|
||||
#endif //T_MAN_UUID_H
|
||||
|
||||
1820
sunshine/video.cpp
1820
sunshine/video.cpp
File diff suppressed because it is too large
Load Diff
@@ -5,15 +5,55 @@
|
||||
#ifndef SUNSHINE_VIDEO_H
|
||||
#define SUNSHINE_VIDEO_H
|
||||
|
||||
#include "input.h"
|
||||
#include "platform/common.h"
|
||||
#include "thread_safe.h"
|
||||
|
||||
extern "C" {
|
||||
#include <libavcodec/avcodec.h>
|
||||
}
|
||||
|
||||
struct AVPacket;
|
||||
namespace video {
|
||||
void free_packet(AVPacket *packet);
|
||||
|
||||
using packet_t = util::safe_ptr<AVPacket, free_packet>;
|
||||
struct packet_raw_t : public AVPacket {
|
||||
void init_packet() {
|
||||
pts = AV_NOPTS_VALUE;
|
||||
dts = AV_NOPTS_VALUE;
|
||||
pos = -1;
|
||||
duration = 0;
|
||||
flags = 0;
|
||||
stream_index = 0;
|
||||
buf = nullptr;
|
||||
side_data = nullptr;
|
||||
side_data_elems = 0;
|
||||
}
|
||||
|
||||
template<class P>
|
||||
explicit packet_raw_t(P *user_data) : channel_data { user_data } {
|
||||
init_packet();
|
||||
}
|
||||
|
||||
explicit packet_raw_t(std::nullptr_t) : channel_data { nullptr } {
|
||||
init_packet();
|
||||
}
|
||||
|
||||
~packet_raw_t() {
|
||||
av_packet_unref(this);
|
||||
}
|
||||
|
||||
struct {
|
||||
std::string_view old;
|
||||
std::string_view replacement;
|
||||
} sps;
|
||||
|
||||
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>>>;
|
||||
|
||||
struct config_t {
|
||||
int width;
|
||||
@@ -27,7 +67,28 @@ struct config_t {
|
||||
int dynamicRange;
|
||||
};
|
||||
|
||||
void capture_display(packet_queue_t packets, idr_event_t idr_events, config_t config);
|
||||
}
|
||||
using float4 = float[4];
|
||||
using float3 = float[3];
|
||||
using float2 = float[2];
|
||||
|
||||
struct __attribute__((__aligned__(16))) color_t {
|
||||
float4 color_vec_y;
|
||||
float4 color_vec_u;
|
||||
float4 color_vec_v;
|
||||
float2 range_y;
|
||||
float2 range_uv;
|
||||
};
|
||||
|
||||
extern color_t colors[4];
|
||||
|
||||
void capture(
|
||||
safe::signal_t *shutdown_event,
|
||||
packet_queue_t packets,
|
||||
idr_event_t idr_events,
|
||||
config_t config,
|
||||
void *channel_data);
|
||||
|
||||
int init();
|
||||
} // namespace video
|
||||
|
||||
#endif //SUNSHINE_VIDEO_H
|
||||
|
||||
0
ViGEmClient → third-party/ViGEmClient
vendored
0
ViGEmClient → third-party/ViGEmClient
vendored
69
third-party/cbs/CMakeLists.txt
vendored
Normal file
69
third-party/cbs/CMakeLists.txt
vendored
Normal file
@@ -0,0 +1,69 @@
|
||||
cmake_minimum_required(VERSION 3.0)
|
||||
|
||||
project(CBS)
|
||||
|
||||
SET(CBS_SOURCE_FILES
|
||||
include/cbs/av1.h
|
||||
include/cbs/cbs_av1.h
|
||||
include/cbs/cbs_bsf.h
|
||||
include/cbs/cbs.h
|
||||
include/cbs/cbs_h2645.h
|
||||
include/cbs/cbs_h264.h
|
||||
include/cbs/cbs_h265.h
|
||||
include/cbs/cbs_jpeg.h
|
||||
include/cbs/cbs_mpeg2.h
|
||||
include/cbs/cbs_sei.h
|
||||
include/cbs/cbs_vp9.h
|
||||
include/cbs/h2645_parse.h
|
||||
include/cbs/h264.h
|
||||
include/cbs/hevc.h
|
||||
include/cbs/sei.h
|
||||
include/cbs/h264_levels.h
|
||||
|
||||
cbs.c
|
||||
cbs_h2645.c
|
||||
cbs_av1.c
|
||||
cbs_vp9.c
|
||||
cbs_mpeg2.c
|
||||
cbs_jpeg.c
|
||||
cbs_sei.c
|
||||
h2645_parse.c
|
||||
h264_levels.c
|
||||
|
||||
bytestream.h
|
||||
cbs_internal.h
|
||||
defs.h
|
||||
get_bits.h
|
||||
h264_ps.h
|
||||
h264_sei.h
|
||||
hevc_sei.h
|
||||
intmath.h
|
||||
mathops.h
|
||||
put_bits.h
|
||||
vlc.h
|
||||
config.h
|
||||
)
|
||||
|
||||
include_directories(include)
|
||||
|
||||
if(DEFINED SUNSHINE_PREPARED_BINARIES)
|
||||
include_directories(${SUNSHINE_PREPARED_BINARIES}/include)
|
||||
endif()
|
||||
|
||||
add_compile_definitions(
|
||||
HAVE_THREADS=1
|
||||
HAVE_FAST_UNALIGNED
|
||||
|
||||
PIC=1
|
||||
|
||||
CONFIG_CBS_AV1=1
|
||||
CONFIG_CBS_H264=1
|
||||
CONFIG_CBS_H265=1
|
||||
CONFIG_CBS_JPEG=1
|
||||
CONFIG_CBS_MPEG2=1
|
||||
CONFIG_CBS_VP9=1
|
||||
)
|
||||
|
||||
|
||||
add_library(cbs ${CBS_SOURCE_FILES})
|
||||
target_compile_options(cbs PRIVATE -Wall -Wno-incompatible-pointer-types -Wno-maybe-uninitialized -Wno-format -Wno-format-extra-args)
|
||||
351
third-party/cbs/bytestream.h
vendored
Normal file
351
third-party/cbs/bytestream.h
vendored
Normal file
@@ -0,0 +1,351 @@
|
||||
/*
|
||||
* Bytestream functions
|
||||
* copyright (c) 2006 Baptiste Coudurier <baptiste.coudurier@free.fr>
|
||||
* Copyright (c) 2012 Aneesh Dogra (lionaneesh) <lionaneesh@gmail.com>
|
||||
*
|
||||
* This file is part of FFmpeg.
|
||||
*
|
||||
* FFmpeg is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU Lesser General Public
|
||||
* License as published by the Free Software Foundation; either
|
||||
* version 2.1 of the License, or (at your option) any later version.
|
||||
*
|
||||
* FFmpeg is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
* Lesser General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public
|
||||
* License along with FFmpeg; if not, write to the Free Software
|
||||
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
*/
|
||||
|
||||
#ifndef AVCODEC_BYTESTREAM_H
|
||||
#define AVCODEC_BYTESTREAM_H
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <stdint.h>
|
||||
#include <string.h>
|
||||
|
||||
#include <libavutil/avassert.h>
|
||||
#include <libavutil/common.h>
|
||||
#include <libavutil/intreadwrite.h>
|
||||
|
||||
typedef struct GetByteContext {
|
||||
const uint8_t *buffer, *buffer_end, *buffer_start;
|
||||
} GetByteContext;
|
||||
|
||||
typedef struct PutByteContext {
|
||||
uint8_t *buffer, *buffer_end, *buffer_start;
|
||||
int eof;
|
||||
} PutByteContext;
|
||||
|
||||
#define DEF(type, name, bytes, read, write) \
|
||||
static av_always_inline type bytestream_get_##name(const uint8_t **b) { \
|
||||
(*b) += bytes; \
|
||||
return read(*b - bytes); \
|
||||
} \
|
||||
static av_always_inline void bytestream_put_##name(uint8_t **b, \
|
||||
const type value) { \
|
||||
write(*b, value); \
|
||||
(*b) += bytes; \
|
||||
} \
|
||||
static av_always_inline void bytestream2_put_##name##u(PutByteContext *p, \
|
||||
const type value) { \
|
||||
bytestream_put_##name(&p->buffer, value); \
|
||||
} \
|
||||
static av_always_inline void bytestream2_put_##name(PutByteContext *p, \
|
||||
const type value) { \
|
||||
if(!p->eof && (p->buffer_end - p->buffer >= bytes)) { \
|
||||
write(p->buffer, value); \
|
||||
p->buffer += bytes; \
|
||||
} \
|
||||
else \
|
||||
p->eof = 1; \
|
||||
} \
|
||||
static av_always_inline type bytestream2_get_##name##u(GetByteContext *g) { \
|
||||
return bytestream_get_##name(&g->buffer); \
|
||||
} \
|
||||
static av_always_inline type bytestream2_get_##name(GetByteContext *g) { \
|
||||
if(g->buffer_end - g->buffer < bytes) { \
|
||||
g->buffer = g->buffer_end; \
|
||||
return 0; \
|
||||
} \
|
||||
return bytestream2_get_##name##u(g); \
|
||||
} \
|
||||
static av_always_inline type bytestream2_peek_##name##u(GetByteContext *g) { \
|
||||
return read(g->buffer); \
|
||||
} \
|
||||
static av_always_inline type bytestream2_peek_##name(GetByteContext *g) { \
|
||||
if(g->buffer_end - g->buffer < bytes) \
|
||||
return 0; \
|
||||
return bytestream2_peek_##name##u(g); \
|
||||
}
|
||||
|
||||
DEF(uint64_t, le64, 8, AV_RL64, AV_WL64)
|
||||
DEF(unsigned int, le32, 4, AV_RL32, AV_WL32)
|
||||
DEF(unsigned int, le24, 3, AV_RL24, AV_WL24)
|
||||
DEF(unsigned int, le16, 2, AV_RL16, AV_WL16)
|
||||
DEF(uint64_t, be64, 8, AV_RB64, AV_WB64)
|
||||
DEF(unsigned int, be32, 4, AV_RB32, AV_WB32)
|
||||
DEF(unsigned int, be24, 3, AV_RB24, AV_WB24)
|
||||
DEF(unsigned int, be16, 2, AV_RB16, AV_WB16)
|
||||
DEF(unsigned int, byte, 1, AV_RB8, AV_WB8)
|
||||
|
||||
#if AV_HAVE_BIGENDIAN
|
||||
#define bytestream2_get_ne16 bytestream2_get_be16
|
||||
#define bytestream2_get_ne24 bytestream2_get_be24
|
||||
#define bytestream2_get_ne32 bytestream2_get_be32
|
||||
#define bytestream2_get_ne64 bytestream2_get_be64
|
||||
#define bytestream2_get_ne16u bytestream2_get_be16u
|
||||
#define bytestream2_get_ne24u bytestream2_get_be24u
|
||||
#define bytestream2_get_ne32u bytestream2_get_be32u
|
||||
#define bytestream2_get_ne64u bytestream2_get_be64u
|
||||
#define bytestream2_put_ne16 bytestream2_put_be16
|
||||
#define bytestream2_put_ne24 bytestream2_put_be24
|
||||
#define bytestream2_put_ne32 bytestream2_put_be32
|
||||
#define bytestream2_put_ne64 bytestream2_put_be64
|
||||
#define bytestream2_peek_ne16 bytestream2_peek_be16
|
||||
#define bytestream2_peek_ne24 bytestream2_peek_be24
|
||||
#define bytestream2_peek_ne32 bytestream2_peek_be32
|
||||
#define bytestream2_peek_ne64 bytestream2_peek_be64
|
||||
#else
|
||||
#define bytestream2_get_ne16 bytestream2_get_le16
|
||||
#define bytestream2_get_ne24 bytestream2_get_le24
|
||||
#define bytestream2_get_ne32 bytestream2_get_le32
|
||||
#define bytestream2_get_ne64 bytestream2_get_le64
|
||||
#define bytestream2_get_ne16u bytestream2_get_le16u
|
||||
#define bytestream2_get_ne24u bytestream2_get_le24u
|
||||
#define bytestream2_get_ne32u bytestream2_get_le32u
|
||||
#define bytestream2_get_ne64u bytestream2_get_le64u
|
||||
#define bytestream2_put_ne16 bytestream2_put_le16
|
||||
#define bytestream2_put_ne24 bytestream2_put_le24
|
||||
#define bytestream2_put_ne32 bytestream2_put_le32
|
||||
#define bytestream2_put_ne64 bytestream2_put_le64
|
||||
#define bytestream2_peek_ne16 bytestream2_peek_le16
|
||||
#define bytestream2_peek_ne24 bytestream2_peek_le24
|
||||
#define bytestream2_peek_ne32 bytestream2_peek_le32
|
||||
#define bytestream2_peek_ne64 bytestream2_peek_le64
|
||||
#endif
|
||||
|
||||
static av_always_inline void bytestream2_init(GetByteContext *g,
|
||||
const uint8_t *buf,
|
||||
int buf_size) {
|
||||
av_assert0(buf_size >= 0);
|
||||
g->buffer = buf;
|
||||
g->buffer_start = buf;
|
||||
g->buffer_end = buf + buf_size;
|
||||
}
|
||||
|
||||
static av_always_inline void bytestream2_init_writer(PutByteContext *p,
|
||||
uint8_t *buf,
|
||||
int buf_size) {
|
||||
av_assert0(buf_size >= 0);
|
||||
p->buffer = buf;
|
||||
p->buffer_start = buf;
|
||||
p->buffer_end = buf + buf_size;
|
||||
p->eof = 0;
|
||||
}
|
||||
|
||||
static av_always_inline int bytestream2_get_bytes_left(GetByteContext *g) {
|
||||
return g->buffer_end - g->buffer;
|
||||
}
|
||||
|
||||
static av_always_inline int bytestream2_get_bytes_left_p(PutByteContext *p) {
|
||||
return p->buffer_end - p->buffer;
|
||||
}
|
||||
|
||||
static av_always_inline void bytestream2_skip(GetByteContext *g,
|
||||
unsigned int size) {
|
||||
g->buffer += FFMIN(g->buffer_end - g->buffer, size);
|
||||
}
|
||||
|
||||
static av_always_inline void bytestream2_skipu(GetByteContext *g,
|
||||
unsigned int size) {
|
||||
g->buffer += size;
|
||||
}
|
||||
|
||||
static av_always_inline void bytestream2_skip_p(PutByteContext *p,
|
||||
unsigned int size) {
|
||||
int size2;
|
||||
if(p->eof)
|
||||
return;
|
||||
size2 = FFMIN(p->buffer_end - p->buffer, size);
|
||||
if(size2 != size)
|
||||
p->eof = 1;
|
||||
p->buffer += size2;
|
||||
}
|
||||
|
||||
static av_always_inline int bytestream2_tell(GetByteContext *g) {
|
||||
return (int)(g->buffer - g->buffer_start);
|
||||
}
|
||||
|
||||
static av_always_inline int bytestream2_tell_p(PutByteContext *p) {
|
||||
return (int)(p->buffer - p->buffer_start);
|
||||
}
|
||||
|
||||
static av_always_inline int bytestream2_size(GetByteContext *g) {
|
||||
return (int)(g->buffer_end - g->buffer_start);
|
||||
}
|
||||
|
||||
static av_always_inline int bytestream2_size_p(PutByteContext *p) {
|
||||
return (int)(p->buffer_end - p->buffer_start);
|
||||
}
|
||||
|
||||
static av_always_inline int bytestream2_seek(GetByteContext *g,
|
||||
int offset,
|
||||
int whence) {
|
||||
switch(whence) {
|
||||
case SEEK_CUR:
|
||||
offset = av_clip(offset, -(g->buffer - g->buffer_start),
|
||||
g->buffer_end - g->buffer);
|
||||
g->buffer += offset;
|
||||
break;
|
||||
case SEEK_END:
|
||||
offset = av_clip(offset, -(g->buffer_end - g->buffer_start), 0);
|
||||
g->buffer = g->buffer_end + offset;
|
||||
break;
|
||||
case SEEK_SET:
|
||||
offset = av_clip(offset, 0, g->buffer_end - g->buffer_start);
|
||||
g->buffer = g->buffer_start + offset;
|
||||
break;
|
||||
default:
|
||||
return AVERROR(EINVAL);
|
||||
}
|
||||
return bytestream2_tell(g);
|
||||
}
|
||||
|
||||
static av_always_inline int bytestream2_seek_p(PutByteContext *p,
|
||||
int offset,
|
||||
int whence) {
|
||||
p->eof = 0;
|
||||
switch(whence) {
|
||||
case SEEK_CUR:
|
||||
if(p->buffer_end - p->buffer < offset)
|
||||
p->eof = 1;
|
||||
offset = av_clip(offset, -(p->buffer - p->buffer_start),
|
||||
p->buffer_end - p->buffer);
|
||||
p->buffer += offset;
|
||||
break;
|
||||
case SEEK_END:
|
||||
if(offset > 0)
|
||||
p->eof = 1;
|
||||
offset = av_clip(offset, -(p->buffer_end - p->buffer_start), 0);
|
||||
p->buffer = p->buffer_end + offset;
|
||||
break;
|
||||
case SEEK_SET:
|
||||
if(p->buffer_end - p->buffer_start < offset)
|
||||
p->eof = 1;
|
||||
offset = av_clip(offset, 0, p->buffer_end - p->buffer_start);
|
||||
p->buffer = p->buffer_start + offset;
|
||||
break;
|
||||
default:
|
||||
return AVERROR(EINVAL);
|
||||
}
|
||||
return bytestream2_tell_p(p);
|
||||
}
|
||||
|
||||
static av_always_inline unsigned int bytestream2_get_buffer(GetByteContext *g,
|
||||
uint8_t *dst,
|
||||
unsigned int size) {
|
||||
int size2 = FFMIN(g->buffer_end - g->buffer, size);
|
||||
memcpy(dst, g->buffer, size2);
|
||||
g->buffer += size2;
|
||||
return size2;
|
||||
}
|
||||
|
||||
static av_always_inline unsigned int bytestream2_get_bufferu(GetByteContext *g,
|
||||
uint8_t *dst,
|
||||
unsigned int size) {
|
||||
memcpy(dst, g->buffer, size);
|
||||
g->buffer += size;
|
||||
return size;
|
||||
}
|
||||
|
||||
static av_always_inline unsigned int bytestream2_put_buffer(PutByteContext *p,
|
||||
const uint8_t *src,
|
||||
unsigned int size) {
|
||||
int size2;
|
||||
if(p->eof)
|
||||
return 0;
|
||||
size2 = FFMIN(p->buffer_end - p->buffer, size);
|
||||
if(size2 != size)
|
||||
p->eof = 1;
|
||||
memcpy(p->buffer, src, size2);
|
||||
p->buffer += size2;
|
||||
return size2;
|
||||
}
|
||||
|
||||
static av_always_inline unsigned int bytestream2_put_bufferu(PutByteContext *p,
|
||||
const uint8_t *src,
|
||||
unsigned int size) {
|
||||
memcpy(p->buffer, src, size);
|
||||
p->buffer += size;
|
||||
return size;
|
||||
}
|
||||
|
||||
static av_always_inline void bytestream2_set_buffer(PutByteContext *p,
|
||||
const uint8_t c,
|
||||
unsigned int size) {
|
||||
int size2;
|
||||
if(p->eof)
|
||||
return;
|
||||
size2 = FFMIN(p->buffer_end - p->buffer, size);
|
||||
if(size2 != size)
|
||||
p->eof = 1;
|
||||
memset(p->buffer, c, size2);
|
||||
p->buffer += size2;
|
||||
}
|
||||
|
||||
static av_always_inline void bytestream2_set_bufferu(PutByteContext *p,
|
||||
const uint8_t c,
|
||||
unsigned int size) {
|
||||
memset(p->buffer, c, size);
|
||||
p->buffer += size;
|
||||
}
|
||||
|
||||
static av_always_inline unsigned int bytestream2_get_eof(PutByteContext *p) {
|
||||
return p->eof;
|
||||
}
|
||||
|
||||
static av_always_inline unsigned int bytestream2_copy_bufferu(PutByteContext *p,
|
||||
GetByteContext *g,
|
||||
unsigned int size) {
|
||||
memcpy(p->buffer, g->buffer, size);
|
||||
p->buffer += size;
|
||||
g->buffer += size;
|
||||
return size;
|
||||
}
|
||||
|
||||
static av_always_inline unsigned int bytestream2_copy_buffer(PutByteContext *p,
|
||||
GetByteContext *g,
|
||||
unsigned int size) {
|
||||
int size2;
|
||||
|
||||
if(p->eof)
|
||||
return 0;
|
||||
size = FFMIN(g->buffer_end - g->buffer, size);
|
||||
size2 = FFMIN(p->buffer_end - p->buffer, size);
|
||||
if(size2 != size)
|
||||
p->eof = 1;
|
||||
|
||||
return bytestream2_copy_bufferu(p, g, size2);
|
||||
}
|
||||
|
||||
static av_always_inline unsigned int bytestream_get_buffer(const uint8_t **b,
|
||||
uint8_t *dst,
|
||||
unsigned int size) {
|
||||
memcpy(dst, *b, size);
|
||||
(*b) += size;
|
||||
return size;
|
||||
}
|
||||
|
||||
static av_always_inline void bytestream_put_buffer(uint8_t **b,
|
||||
const uint8_t *src,
|
||||
unsigned int size) {
|
||||
memcpy(*b, src, size);
|
||||
(*b) += size;
|
||||
}
|
||||
|
||||
#endif /* AVCODEC_BYTESTREAM_H */
|
||||
1050
third-party/cbs/cbs.c
vendored
Normal file
1050
third-party/cbs/cbs.c
vendored
Normal file
File diff suppressed because it is too large
Load Diff
1323
third-party/cbs/cbs_av1.c
vendored
Normal file
1323
third-party/cbs/cbs_av1.c
vendored
Normal file
File diff suppressed because it is too large
Load Diff
2063
third-party/cbs/cbs_av1_syntax_template.c
vendored
Normal file
2063
third-party/cbs/cbs_av1_syntax_template.c
vendored
Normal file
File diff suppressed because it is too large
Load Diff
1632
third-party/cbs/cbs_h2645.c
vendored
Normal file
1632
third-party/cbs/cbs_h2645.c
vendored
Normal file
File diff suppressed because it is too large
Load Diff
1175
third-party/cbs/cbs_h264_syntax_template.c
vendored
Normal file
1175
third-party/cbs/cbs_h264_syntax_template.c
vendored
Normal file
File diff suppressed because it is too large
Load Diff
2038
third-party/cbs/cbs_h265_syntax_template.c
vendored
Normal file
2038
third-party/cbs/cbs_h265_syntax_template.c
vendored
Normal file
File diff suppressed because it is too large
Load Diff
220
third-party/cbs/cbs_internal.h
vendored
Normal file
220
third-party/cbs/cbs_internal.h
vendored
Normal file
@@ -0,0 +1,220 @@
|
||||
/*
|
||||
* This file is part of FFmpeg.
|
||||
*
|
||||
* FFmpeg is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU Lesser General Public
|
||||
* License as published by the Free Software Foundation; either
|
||||
* version 2.1 of the License, or (at your option) any later version.
|
||||
*
|
||||
* FFmpeg is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
* Lesser General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public
|
||||
* License along with FFmpeg; if not, write to the Free Software
|
||||
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
*/
|
||||
|
||||
#ifndef AVCODEC_CBS_INTERNAL_H
|
||||
#define AVCODEC_CBS_INTERNAL_H
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#include <libavutil/buffer.h>
|
||||
#include <libavutil/log.h>
|
||||
|
||||
#include "cbs/cbs.h"
|
||||
#include "get_bits.h"
|
||||
#include "put_bits.h"
|
||||
|
||||
|
||||
enum CBSContentType {
|
||||
// Unit content is a simple structure.
|
||||
CBS_CONTENT_TYPE_POD,
|
||||
// Unit content contains some references to other structures, but all
|
||||
// managed via buffer reference counting. The descriptor defines the
|
||||
// structure offsets of every buffer reference.
|
||||
CBS_CONTENT_TYPE_INTERNAL_REFS,
|
||||
// Unit content is something more complex. The descriptor defines
|
||||
// special functions to manage the content.
|
||||
CBS_CONTENT_TYPE_COMPLEX,
|
||||
};
|
||||
|
||||
enum {
|
||||
// Maximum number of unit types described by the same unit type
|
||||
// descriptor.
|
||||
CBS_MAX_UNIT_TYPES = 3,
|
||||
// Maximum number of reference buffer offsets in any one unit.
|
||||
CBS_MAX_REF_OFFSETS = 2,
|
||||
// Special value used in a unit type descriptor to indicate that it
|
||||
// applies to a large range of types rather than a set of discrete
|
||||
// values.
|
||||
CBS_UNIT_TYPE_RANGE = -1,
|
||||
};
|
||||
|
||||
typedef const struct CodedBitstreamUnitTypeDescriptor {
|
||||
// Number of entries in the unit_types array, or the special value
|
||||
// CBS_UNIT_TYPE_RANGE to indicate that the range fields should be
|
||||
// used instead.
|
||||
int nb_unit_types;
|
||||
|
||||
// Array of unit types that this entry describes.
|
||||
const CodedBitstreamUnitType unit_types[CBS_MAX_UNIT_TYPES];
|
||||
|
||||
// Start and end of unit type range, used if nb_unit_types is
|
||||
// CBS_UNIT_TYPE_RANGE.
|
||||
const CodedBitstreamUnitType unit_type_range_start;
|
||||
const CodedBitstreamUnitType unit_type_range_end;
|
||||
|
||||
// The type of content described.
|
||||
enum CBSContentType content_type;
|
||||
// The size of the structure which should be allocated to contain
|
||||
// the decomposed content of this type of unit.
|
||||
size_t content_size;
|
||||
|
||||
// Number of entries in the ref_offsets array. Only used if the
|
||||
// content_type is CBS_CONTENT_TYPE_INTERNAL_REFS.
|
||||
int nb_ref_offsets;
|
||||
// The structure must contain two adjacent elements:
|
||||
// type *field;
|
||||
// AVBufferRef *field_ref;
|
||||
// where field points to something in the buffer referred to by
|
||||
// field_ref. This offset is then set to offsetof(struct, field).
|
||||
size_t ref_offsets[CBS_MAX_REF_OFFSETS];
|
||||
|
||||
void (*content_free)(void *opaque, uint8_t *data);
|
||||
int (*content_clone)(AVBufferRef **ref, CodedBitstreamUnit *unit);
|
||||
} CodedBitstreamUnitTypeDescriptor;
|
||||
|
||||
typedef struct CodedBitstreamType {
|
||||
enum AVCodecID codec_id;
|
||||
|
||||
// A class for the private data, used to declare private AVOptions.
|
||||
// This field is NULL for types that do not declare any options.
|
||||
// If this field is non-NULL, the first member of the filter private data
|
||||
// must be a pointer to AVClass.
|
||||
const AVClass *priv_class;
|
||||
|
||||
size_t priv_data_size;
|
||||
|
||||
// List of unit type descriptors for this codec.
|
||||
// Terminated by a descriptor with nb_unit_types equal to zero.
|
||||
const CodedBitstreamUnitTypeDescriptor *unit_types;
|
||||
|
||||
// Split frag->data into coded bitstream units, creating the
|
||||
// frag->units array. Fill data but not content on each unit.
|
||||
// The header argument should be set if the fragment came from
|
||||
// a header block, which may require different parsing for some
|
||||
// codecs (e.g. the AVCC header in H.264).
|
||||
int (*split_fragment)(CodedBitstreamContext *ctx,
|
||||
CodedBitstreamFragment *frag,
|
||||
int header);
|
||||
|
||||
// Read the unit->data bitstream and decompose it, creating
|
||||
// unit->content.
|
||||
int (*read_unit)(CodedBitstreamContext *ctx,
|
||||
CodedBitstreamUnit *unit);
|
||||
|
||||
// Write the data bitstream from unit->content into pbc.
|
||||
// Return value AVERROR(ENOSPC) indicates that pbc was too small.
|
||||
int (*write_unit)(CodedBitstreamContext *ctx,
|
||||
CodedBitstreamUnit *unit,
|
||||
PutBitContext *pbc);
|
||||
|
||||
// Read the data from all of frag->units and assemble it into
|
||||
// a bitstream for the whole fragment.
|
||||
int (*assemble_fragment)(CodedBitstreamContext *ctx,
|
||||
CodedBitstreamFragment *frag);
|
||||
|
||||
// Reset the codec internal state.
|
||||
void (*flush)(CodedBitstreamContext *ctx);
|
||||
|
||||
// Free the codec internal state.
|
||||
void (*close)(CodedBitstreamContext *ctx);
|
||||
} CodedBitstreamType;
|
||||
|
||||
|
||||
// Helper functions for trace output.
|
||||
|
||||
void ff_cbs_trace_header(CodedBitstreamContext *ctx,
|
||||
const char *name);
|
||||
|
||||
void ff_cbs_trace_syntax_element(CodedBitstreamContext *ctx, int position,
|
||||
const char *name, const int *subscripts,
|
||||
const char *bitstring, int64_t value);
|
||||
|
||||
|
||||
// Helper functions for read/write of common bitstream elements, including
|
||||
// generation of trace output.
|
||||
|
||||
int ff_cbs_read_unsigned(CodedBitstreamContext *ctx, GetBitContext *gbc,
|
||||
int width, const char *name,
|
||||
const int *subscripts, uint32_t *write_to,
|
||||
uint32_t range_min, uint32_t range_max);
|
||||
|
||||
int ff_cbs_write_unsigned(CodedBitstreamContext *ctx, PutBitContext *pbc,
|
||||
int width, const char *name,
|
||||
const int *subscripts, uint32_t value,
|
||||
uint32_t range_min, uint32_t range_max);
|
||||
|
||||
int ff_cbs_read_signed(CodedBitstreamContext *ctx, GetBitContext *gbc,
|
||||
int width, const char *name,
|
||||
const int *subscripts, int32_t *write_to,
|
||||
int32_t range_min, int32_t range_max);
|
||||
|
||||
int ff_cbs_write_signed(CodedBitstreamContext *ctx, PutBitContext *pbc,
|
||||
int width, const char *name,
|
||||
const int *subscripts, int32_t value,
|
||||
int32_t range_min, int32_t range_max);
|
||||
|
||||
// The largest unsigned value representable in N bits, suitable for use as
|
||||
// range_max in the above functions.
|
||||
#define MAX_UINT_BITS(length) ((UINT64_C(1) << (length)) - 1)
|
||||
|
||||
// The largest signed value representable in N bits, suitable for use as
|
||||
// range_max in the above functions.
|
||||
#define MAX_INT_BITS(length) ((INT64_C(1) << ((length)-1)) - 1)
|
||||
|
||||
// The smallest signed value representable in N bits, suitable for use as
|
||||
// range_min in the above functions.
|
||||
#define MIN_INT_BITS(length) (-(INT64_C(1) << ((length)-1)))
|
||||
|
||||
|
||||
#define CBS_UNIT_TYPE_POD(type, structure) \
|
||||
{ \
|
||||
.nb_unit_types = 1, \
|
||||
.unit_types = { type }, \
|
||||
.content_type = CBS_CONTENT_TYPE_POD, \
|
||||
.content_size = sizeof(structure), \
|
||||
}
|
||||
#define CBS_UNIT_TYPE_INTERNAL_REF(type, structure, ref_field) \
|
||||
{ \
|
||||
.nb_unit_types = 1, \
|
||||
.unit_types = { type }, \
|
||||
.content_type = CBS_CONTENT_TYPE_INTERNAL_REFS, \
|
||||
.content_size = sizeof(structure), \
|
||||
.nb_ref_offsets = 1, \
|
||||
.ref_offsets = { offsetof(structure, ref_field) }, \
|
||||
}
|
||||
#define CBS_UNIT_TYPE_COMPLEX(type, structure, free_func) \
|
||||
{ \
|
||||
.nb_unit_types = 1, \
|
||||
.unit_types = { type }, \
|
||||
.content_type = CBS_CONTENT_TYPE_COMPLEX, \
|
||||
.content_size = sizeof(structure), \
|
||||
.content_free = free_func, \
|
||||
}
|
||||
#define CBS_UNIT_TYPE_END_OF_LIST \
|
||||
{ .nb_unit_types = 0 }
|
||||
|
||||
|
||||
extern const CodedBitstreamType ff_cbs_type_av1;
|
||||
extern const CodedBitstreamType ff_cbs_type_h264;
|
||||
extern const CodedBitstreamType ff_cbs_type_h265;
|
||||
extern const CodedBitstreamType ff_cbs_type_jpeg;
|
||||
extern const CodedBitstreamType ff_cbs_type_mpeg2;
|
||||
extern const CodedBitstreamType ff_cbs_type_vp9;
|
||||
|
||||
|
||||
#endif /* AVCODEC_CBS_INTERNAL_H */
|
||||
482
third-party/cbs/cbs_jpeg.c
vendored
Normal file
482
third-party/cbs/cbs_jpeg.c
vendored
Normal file
@@ -0,0 +1,482 @@
|
||||
/*
|
||||
* 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 "cbs/cbs_jpeg.h"
|
||||
#include "cbs/cbs.h"
|
||||
|
||||
#include "cbs_internal.h"
|
||||
|
||||
|
||||
#define HEADER(name) \
|
||||
do { \
|
||||
ff_cbs_trace_header(ctx, name); \
|
||||
} while(0)
|
||||
|
||||
#define CHECK(call) \
|
||||
do { \
|
||||
err = (call); \
|
||||
if(err < 0) \
|
||||
return err; \
|
||||
} while(0)
|
||||
|
||||
#define SUBSCRIPTS(subs, ...) (subs > 0 ? ((int[subs + 1]) { subs, __VA_ARGS__ }) : NULL)
|
||||
|
||||
#define u(width, name, range_min, range_max) \
|
||||
xu(width, name, range_min, range_max, 0, )
|
||||
#define us(width, name, sub, range_min, range_max) \
|
||||
xu(width, name, range_min, range_max, 1, sub)
|
||||
|
||||
|
||||
#define READ
|
||||
#define READWRITE read
|
||||
#define RWContext GetBitContext
|
||||
#define FUNC(name) cbs_jpeg_read_##name
|
||||
|
||||
#define xu(width, name, range_min, range_max, subs, ...) \
|
||||
do { \
|
||||
uint32_t value; \
|
||||
CHECK(ff_cbs_read_unsigned(ctx, rw, width, #name, \
|
||||
SUBSCRIPTS(subs, __VA_ARGS__), \
|
||||
&value, range_min, range_max)); \
|
||||
current->name = value; \
|
||||
} while(0)
|
||||
|
||||
#include "cbs_jpeg_syntax_template.c"
|
||||
|
||||
#undef READ
|
||||
#undef READWRITE
|
||||
#undef RWContext
|
||||
#undef FUNC
|
||||
#undef xu
|
||||
|
||||
#define WRITE
|
||||
#define READWRITE write
|
||||
#define RWContext PutBitContext
|
||||
#define FUNC(name) cbs_jpeg_write_##name
|
||||
|
||||
#define xu(width, name, range_min, range_max, subs, ...) \
|
||||
do { \
|
||||
uint32_t value = current->name; \
|
||||
CHECK(ff_cbs_write_unsigned(ctx, rw, width, #name, \
|
||||
SUBSCRIPTS(subs, __VA_ARGS__), \
|
||||
value, range_min, range_max)); \
|
||||
} while(0)
|
||||
|
||||
|
||||
#include "cbs_jpeg_syntax_template.c"
|
||||
|
||||
#undef WRITE
|
||||
#undef READWRITE
|
||||
#undef RWContext
|
||||
#undef FUNC
|
||||
#undef xu
|
||||
|
||||
|
||||
static void cbs_jpeg_free_application_data(void *opaque, uint8_t *content) {
|
||||
JPEGRawApplicationData *ad = (JPEGRawApplicationData *)content;
|
||||
av_buffer_unref(&ad->Ap_ref);
|
||||
av_freep(&content);
|
||||
}
|
||||
|
||||
static void cbs_jpeg_free_comment(void *opaque, uint8_t *content) {
|
||||
JPEGRawComment *comment = (JPEGRawComment *)content;
|
||||
av_buffer_unref(&comment->Cm_ref);
|
||||
av_freep(&content);
|
||||
}
|
||||
|
||||
static void cbs_jpeg_free_scan(void *opaque, uint8_t *content) {
|
||||
JPEGRawScan *scan = (JPEGRawScan *)content;
|
||||
av_buffer_unref(&scan->data_ref);
|
||||
av_freep(&content);
|
||||
}
|
||||
|
||||
static int cbs_jpeg_split_fragment(CodedBitstreamContext *ctx,
|
||||
CodedBitstreamFragment *frag,
|
||||
int header) {
|
||||
AVBufferRef *data_ref;
|
||||
uint8_t *data;
|
||||
size_t data_size;
|
||||
int unit, start, end, marker, next_start, next_marker;
|
||||
int err, i, j, length;
|
||||
|
||||
if(frag->data_size < 4) {
|
||||
// Definitely too short to be meaningful.
|
||||
return AVERROR_INVALIDDATA;
|
||||
}
|
||||
|
||||
for(i = 0; i + 1 < frag->data_size && frag->data[i] != 0xff; i++)
|
||||
;
|
||||
if(i > 0) {
|
||||
av_log(ctx->log_ctx, AV_LOG_WARNING, "Discarding %d bytes at "
|
||||
"beginning of image.\n",
|
||||
i);
|
||||
}
|
||||
for(++i; i + 1 < frag->data_size && frag->data[i] == 0xff; i++)
|
||||
;
|
||||
if(i + 1 >= frag->data_size && frag->data[i]) {
|
||||
av_log(ctx->log_ctx, AV_LOG_ERROR, "Invalid JPEG image: "
|
||||
"no SOI marker found.\n");
|
||||
return AVERROR_INVALIDDATA;
|
||||
}
|
||||
marker = frag->data[i];
|
||||
if(marker != JPEG_MARKER_SOI) {
|
||||
av_log(ctx->log_ctx, AV_LOG_ERROR, "Invalid JPEG image: first "
|
||||
"marker is %02x, should be SOI.\n",
|
||||
marker);
|
||||
return AVERROR_INVALIDDATA;
|
||||
}
|
||||
for(++i; i + 1 < frag->data_size && frag->data[i] == 0xff; i++)
|
||||
;
|
||||
if(i + 1 >= frag->data_size) {
|
||||
av_log(ctx->log_ctx, AV_LOG_ERROR, "Invalid JPEG image: "
|
||||
"no image content found.\n");
|
||||
return AVERROR_INVALIDDATA;
|
||||
}
|
||||
marker = frag->data[i];
|
||||
start = i + 1;
|
||||
|
||||
for(unit = 0;; unit++) {
|
||||
if(marker == JPEG_MARKER_EOI) {
|
||||
break;
|
||||
}
|
||||
else if(marker == JPEG_MARKER_SOS) {
|
||||
next_marker = -1;
|
||||
end = start;
|
||||
for(i = start; i + 1 < frag->data_size; i++) {
|
||||
if(frag->data[i] != 0xff)
|
||||
continue;
|
||||
end = i;
|
||||
for(++i; i + 1 < frag->data_size &&
|
||||
frag->data[i] == 0xff;
|
||||
i++)
|
||||
;
|
||||
if(i + 1 < frag->data_size) {
|
||||
if(frag->data[i] == 0x00)
|
||||
continue;
|
||||
next_marker = frag->data[i];
|
||||
next_start = i + 1;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
else {
|
||||
i = start;
|
||||
if(i + 2 > frag->data_size) {
|
||||
av_log(ctx->log_ctx, AV_LOG_ERROR, "Invalid JPEG image: "
|
||||
"truncated at %02x marker.\n",
|
||||
marker);
|
||||
return AVERROR_INVALIDDATA;
|
||||
}
|
||||
length = AV_RB16(frag->data + i);
|
||||
if(i + length > frag->data_size) {
|
||||
av_log(ctx->log_ctx, AV_LOG_ERROR, "Invalid JPEG image: "
|
||||
"truncated at %02x marker segment.\n",
|
||||
marker);
|
||||
return AVERROR_INVALIDDATA;
|
||||
}
|
||||
end = start + length;
|
||||
|
||||
i = end;
|
||||
if(frag->data[i] != 0xff) {
|
||||
next_marker = -1;
|
||||
}
|
||||
else {
|
||||
for(++i; i + 1 < frag->data_size &&
|
||||
frag->data[i] == 0xff;
|
||||
i++)
|
||||
;
|
||||
if(i + 1 >= frag->data_size) {
|
||||
next_marker = -1;
|
||||
}
|
||||
else {
|
||||
next_marker = frag->data[i];
|
||||
next_start = i + 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(marker == JPEG_MARKER_SOS) {
|
||||
length = AV_RB16(frag->data + start);
|
||||
|
||||
if(length > end - start)
|
||||
return AVERROR_INVALIDDATA;
|
||||
|
||||
data_ref = NULL;
|
||||
data = av_malloc(end - start +
|
||||
AV_INPUT_BUFFER_PADDING_SIZE);
|
||||
if(!data)
|
||||
return AVERROR(ENOMEM);
|
||||
|
||||
memcpy(data, frag->data + start, length);
|
||||
for(i = start + length, j = length; i < end; i++, j++) {
|
||||
if(frag->data[i] == 0xff) {
|
||||
while(frag->data[i] == 0xff)
|
||||
++i;
|
||||
data[j] = 0xff;
|
||||
}
|
||||
else {
|
||||
data[j] = frag->data[i];
|
||||
}
|
||||
}
|
||||
data_size = j;
|
||||
|
||||
memset(data + data_size, 0, AV_INPUT_BUFFER_PADDING_SIZE);
|
||||
}
|
||||
else {
|
||||
data = frag->data + start;
|
||||
data_size = end - start;
|
||||
data_ref = frag->data_ref;
|
||||
}
|
||||
|
||||
err = ff_cbs_insert_unit_data(frag, unit, marker,
|
||||
data, data_size, data_ref);
|
||||
if(err < 0)
|
||||
return err;
|
||||
|
||||
if(next_marker == -1)
|
||||
break;
|
||||
marker = next_marker;
|
||||
start = next_start;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int cbs_jpeg_read_unit(CodedBitstreamContext *ctx,
|
||||
CodedBitstreamUnit *unit) {
|
||||
GetBitContext gbc;
|
||||
int err;
|
||||
|
||||
err = init_get_bits(&gbc, unit->data, 8 * unit->data_size);
|
||||
if(err < 0)
|
||||
return err;
|
||||
|
||||
if(unit->type >= JPEG_MARKER_SOF0 &&
|
||||
unit->type <= JPEG_MARKER_SOF3) {
|
||||
err = ff_cbs_alloc_unit_content(unit,
|
||||
sizeof(JPEGRawFrameHeader),
|
||||
NULL);
|
||||
if(err < 0)
|
||||
return err;
|
||||
|
||||
err = cbs_jpeg_read_frame_header(ctx, &gbc, unit->content);
|
||||
if(err < 0)
|
||||
return err;
|
||||
}
|
||||
else if(unit->type >= JPEG_MARKER_APPN &&
|
||||
unit->type <= JPEG_MARKER_APPN + 15) {
|
||||
err = ff_cbs_alloc_unit_content(unit,
|
||||
sizeof(JPEGRawApplicationData),
|
||||
&cbs_jpeg_free_application_data);
|
||||
if(err < 0)
|
||||
return err;
|
||||
|
||||
err = cbs_jpeg_read_application_data(ctx, &gbc, unit->content);
|
||||
if(err < 0)
|
||||
return err;
|
||||
}
|
||||
else if(unit->type == JPEG_MARKER_SOS) {
|
||||
JPEGRawScan *scan;
|
||||
int pos;
|
||||
|
||||
err = ff_cbs_alloc_unit_content(unit,
|
||||
sizeof(JPEGRawScan),
|
||||
&cbs_jpeg_free_scan);
|
||||
if(err < 0)
|
||||
return err;
|
||||
scan = unit->content;
|
||||
|
||||
err = cbs_jpeg_read_scan_header(ctx, &gbc, &scan->header);
|
||||
if(err < 0)
|
||||
return err;
|
||||
|
||||
pos = get_bits_count(&gbc);
|
||||
av_assert0(pos % 8 == 0);
|
||||
if(pos > 0) {
|
||||
scan->data_size = unit->data_size - pos / 8;
|
||||
scan->data_ref = av_buffer_ref(unit->data_ref);
|
||||
if(!scan->data_ref)
|
||||
return AVERROR(ENOMEM);
|
||||
scan->data = unit->data + pos / 8;
|
||||
}
|
||||
}
|
||||
else {
|
||||
switch(unit->type) {
|
||||
#define SEGMENT(marker, type, func, free) \
|
||||
case JPEG_MARKER_##marker: { \
|
||||
err = ff_cbs_alloc_unit_content(unit, \
|
||||
sizeof(type), free); \
|
||||
if(err < 0) \
|
||||
return err; \
|
||||
err = cbs_jpeg_read_##func(ctx, &gbc, unit->content); \
|
||||
if(err < 0) \
|
||||
return err; \
|
||||
} break
|
||||
SEGMENT(DQT, JPEGRawQuantisationTableSpecification, dqt, NULL);
|
||||
SEGMENT(DHT, JPEGRawHuffmanTableSpecification, dht, NULL);
|
||||
SEGMENT(COM, JPEGRawComment, comment, &cbs_jpeg_free_comment);
|
||||
#undef SEGMENT
|
||||
default:
|
||||
return AVERROR(ENOSYS);
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int cbs_jpeg_write_scan(CodedBitstreamContext *ctx,
|
||||
CodedBitstreamUnit *unit,
|
||||
PutBitContext *pbc) {
|
||||
JPEGRawScan *scan = unit->content;
|
||||
int err;
|
||||
|
||||
err = cbs_jpeg_write_scan_header(ctx, pbc, &scan->header);
|
||||
if(err < 0)
|
||||
return err;
|
||||
|
||||
if(scan->data) {
|
||||
if(scan->data_size * 8 > put_bits_left(pbc))
|
||||
return AVERROR(ENOSPC);
|
||||
|
||||
av_assert0(put_bits_count(pbc) % 8 == 0);
|
||||
|
||||
flush_put_bits(pbc);
|
||||
|
||||
memcpy(put_bits_ptr(pbc), scan->data, scan->data_size);
|
||||
skip_put_bytes(pbc, scan->data_size);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int cbs_jpeg_write_segment(CodedBitstreamContext *ctx,
|
||||
CodedBitstreamUnit *unit,
|
||||
PutBitContext *pbc) {
|
||||
int err;
|
||||
|
||||
if(unit->type >= JPEG_MARKER_SOF0 &&
|
||||
unit->type <= JPEG_MARKER_SOF3) {
|
||||
err = cbs_jpeg_write_frame_header(ctx, pbc, unit->content);
|
||||
}
|
||||
else if(unit->type >= JPEG_MARKER_APPN &&
|
||||
unit->type <= JPEG_MARKER_APPN + 15) {
|
||||
err = cbs_jpeg_write_application_data(ctx, pbc, unit->content);
|
||||
}
|
||||
else {
|
||||
switch(unit->type) {
|
||||
#define SEGMENT(marker, func) \
|
||||
case JPEG_MARKER_##marker: \
|
||||
err = cbs_jpeg_write_##func(ctx, pbc, unit->content); \
|
||||
break;
|
||||
SEGMENT(DQT, dqt);
|
||||
SEGMENT(DHT, dht);
|
||||
SEGMENT(COM, comment);
|
||||
default:
|
||||
return AVERROR_PATCHWELCOME;
|
||||
}
|
||||
}
|
||||
|
||||
return err;
|
||||
}
|
||||
|
||||
static int cbs_jpeg_write_unit(CodedBitstreamContext *ctx,
|
||||
CodedBitstreamUnit *unit,
|
||||
PutBitContext *pbc) {
|
||||
if(unit->type == JPEG_MARKER_SOS)
|
||||
return cbs_jpeg_write_scan(ctx, unit, pbc);
|
||||
else
|
||||
return cbs_jpeg_write_segment(ctx, unit, pbc);
|
||||
}
|
||||
|
||||
static int cbs_jpeg_assemble_fragment(CodedBitstreamContext *ctx,
|
||||
CodedBitstreamFragment *frag) {
|
||||
const CodedBitstreamUnit *unit;
|
||||
uint8_t *data;
|
||||
size_t size, dp, sp;
|
||||
int i;
|
||||
|
||||
size = 4; // SOI + EOI.
|
||||
for(i = 0; i < frag->nb_units; i++) {
|
||||
unit = &frag->units[i];
|
||||
size += 2 + unit->data_size;
|
||||
if(unit->type == JPEG_MARKER_SOS) {
|
||||
for(sp = 0; sp < unit->data_size; sp++) {
|
||||
if(unit->data[sp] == 0xff)
|
||||
++size;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
frag->data_ref = av_buffer_alloc(size + AV_INPUT_BUFFER_PADDING_SIZE);
|
||||
if(!frag->data_ref)
|
||||
return AVERROR(ENOMEM);
|
||||
data = frag->data_ref->data;
|
||||
|
||||
dp = 0;
|
||||
|
||||
data[dp++] = 0xff;
|
||||
data[dp++] = JPEG_MARKER_SOI;
|
||||
|
||||
for(i = 0; i < frag->nb_units; i++) {
|
||||
unit = &frag->units[i];
|
||||
|
||||
data[dp++] = 0xff;
|
||||
data[dp++] = unit->type;
|
||||
|
||||
if(unit->type != JPEG_MARKER_SOS) {
|
||||
memcpy(data + dp, unit->data, unit->data_size);
|
||||
dp += unit->data_size;
|
||||
}
|
||||
else {
|
||||
sp = AV_RB16(unit->data);
|
||||
av_assert0(sp <= unit->data_size);
|
||||
memcpy(data + dp, unit->data, sp);
|
||||
dp += sp;
|
||||
|
||||
for(; sp < unit->data_size; sp++) {
|
||||
if(unit->data[sp] == 0xff) {
|
||||
data[dp++] = 0xff;
|
||||
data[dp++] = 0x00;
|
||||
}
|
||||
else {
|
||||
data[dp++] = unit->data[sp];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data[dp++] = 0xff;
|
||||
data[dp++] = JPEG_MARKER_EOI;
|
||||
|
||||
av_assert0(dp == size);
|
||||
|
||||
memset(data + size, 0, AV_INPUT_BUFFER_PADDING_SIZE);
|
||||
frag->data = data;
|
||||
frag->data_size = size;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
const CodedBitstreamType ff_cbs_type_jpeg = {
|
||||
.codec_id = AV_CODEC_ID_MJPEG,
|
||||
|
||||
.split_fragment = &cbs_jpeg_split_fragment,
|
||||
.read_unit = &cbs_jpeg_read_unit,
|
||||
.write_unit = &cbs_jpeg_write_unit,
|
||||
.assemble_fragment = &cbs_jpeg_assemble_fragment,
|
||||
};
|
||||
189
third-party/cbs/cbs_jpeg_syntax_template.c
vendored
Normal file
189
third-party/cbs/cbs_jpeg_syntax_template.c
vendored
Normal file
@@ -0,0 +1,189 @@
|
||||
/*
|
||||
* 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
|
||||
*/
|
||||
|
||||
static int FUNC(frame_header)(CodedBitstreamContext *ctx, RWContext *rw,
|
||||
JPEGRawFrameHeader *current) {
|
||||
int err, i;
|
||||
|
||||
HEADER("Frame Header");
|
||||
|
||||
u(16, Lf, 8, 8 + 3 * JPEG_MAX_COMPONENTS);
|
||||
|
||||
u(8, P, 2, 16);
|
||||
u(16, Y, 0, JPEG_MAX_HEIGHT);
|
||||
u(16, X, 1, JPEG_MAX_WIDTH);
|
||||
u(8, Nf, 1, JPEG_MAX_COMPONENTS);
|
||||
|
||||
for(i = 0; i < current->Nf; i++) {
|
||||
us(8, C[i], i, 0, JPEG_MAX_COMPONENTS);
|
||||
us(4, H[i], i, 1, 4);
|
||||
us(4, V[i], i, 1, 4);
|
||||
us(8, Tq[i], i, 0, 3);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int FUNC(quantisation_table)(CodedBitstreamContext *ctx, RWContext *rw,
|
||||
JPEGRawQuantisationTable *current) {
|
||||
int err, i;
|
||||
|
||||
u(4, Pq, 0, 1);
|
||||
u(4, Tq, 0, 3);
|
||||
|
||||
if(current->Pq) {
|
||||
for(i = 0; i < 64; i++)
|
||||
us(16, Q[i], i, 1, 255);
|
||||
}
|
||||
else {
|
||||
for(i = 0; i < 64; i++)
|
||||
us(8, Q[i], i, 1, 255);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int FUNC(dqt)(CodedBitstreamContext *ctx, RWContext *rw,
|
||||
JPEGRawQuantisationTableSpecification *current) {
|
||||
int err, i, n;
|
||||
|
||||
HEADER("Quantisation Tables");
|
||||
|
||||
u(16, Lq, 2, 2 + 4 * 65);
|
||||
n = current->Lq / 65;
|
||||
|
||||
for(i = 0; i < n; i++)
|
||||
CHECK(FUNC(quantisation_table)(ctx, rw, ¤t->table[i]));
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int FUNC(huffman_table)(CodedBitstreamContext *ctx, RWContext *rw,
|
||||
JPEGRawHuffmanTable *current) {
|
||||
int err, i, j, ij;
|
||||
|
||||
u(4, Tc, 0, 1);
|
||||
u(4, Th, 0, 3);
|
||||
|
||||
for(i = 0; i < 16; i++)
|
||||
us(8, L[i], i, 0, 224);
|
||||
|
||||
ij = 0;
|
||||
for(i = 0; i < 16; i++) {
|
||||
for(j = 0; j < current->L[i]; j++) {
|
||||
if(ij >= 224)
|
||||
return AVERROR_INVALIDDATA;
|
||||
us(8, V[ij], ij, 0, 255);
|
||||
++ij;
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int FUNC(dht)(CodedBitstreamContext *ctx, RWContext *rw,
|
||||
JPEGRawHuffmanTableSpecification *current) {
|
||||
int err, i, j, n;
|
||||
|
||||
HEADER("Huffman Tables");
|
||||
|
||||
u(16, Lh, 2, 2 + 8 * (1 + 16 + 256));
|
||||
|
||||
n = 2;
|
||||
for(i = 0; n < current->Lh; i++) {
|
||||
if(i >= 8)
|
||||
return AVERROR_INVALIDDATA;
|
||||
|
||||
CHECK(FUNC(huffman_table)(ctx, rw, ¤t->table[i]));
|
||||
|
||||
++n;
|
||||
for(j = 0; j < 16; j++)
|
||||
n += 1 + current->table[i].L[j];
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int FUNC(scan_header)(CodedBitstreamContext *ctx, RWContext *rw,
|
||||
JPEGRawScanHeader *current) {
|
||||
int err, j;
|
||||
|
||||
HEADER("Scan");
|
||||
|
||||
u(16, Ls, 6, 6 + 2 * JPEG_MAX_COMPONENTS);
|
||||
|
||||
u(8, Ns, 1, 4);
|
||||
for(j = 0; j < current->Ns; j++) {
|
||||
us(8, Cs[j], j, 0, JPEG_MAX_COMPONENTS);
|
||||
us(4, Td[j], j, 0, 3);
|
||||
us(4, Ta[j], j, 0, 3);
|
||||
}
|
||||
|
||||
u(8, Ss, 0, 63);
|
||||
u(8, Se, 0, 63);
|
||||
u(4, Ah, 0, 13);
|
||||
u(4, Al, 0, 15);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int FUNC(application_data)(CodedBitstreamContext *ctx, RWContext *rw,
|
||||
JPEGRawApplicationData *current) {
|
||||
int err, i;
|
||||
|
||||
HEADER("Application Data");
|
||||
|
||||
u(16, Lp, 2, 65535);
|
||||
|
||||
if(current->Lp > 2) {
|
||||
#ifdef READ
|
||||
current->Ap_ref = av_buffer_alloc(current->Lp - 2);
|
||||
if(!current->Ap_ref)
|
||||
return AVERROR(ENOMEM);
|
||||
current->Ap = current->Ap_ref->data;
|
||||
#endif
|
||||
|
||||
for(i = 0; i < current->Lp - 2; i++)
|
||||
us(8, Ap[i], i, 0, 255);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int FUNC(comment)(CodedBitstreamContext *ctx, RWContext *rw,
|
||||
JPEGRawComment *current) {
|
||||
int err, i;
|
||||
|
||||
HEADER("Comment");
|
||||
|
||||
u(16, Lc, 2, 65535);
|
||||
|
||||
if(current->Lc > 2) {
|
||||
#ifdef READ
|
||||
current->Cm_ref = av_buffer_alloc(current->Lc - 2);
|
||||
if(!current->Cm_ref)
|
||||
return AVERROR(ENOMEM);
|
||||
current->Cm = current->Cm_ref->data;
|
||||
#endif
|
||||
|
||||
for(i = 0; i < current->Lc - 2; i++)
|
||||
us(8, Cm[i], i, 0, 255);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user