mirror of
https://github.com/LizardByte/Sunshine.git
synced 2025-08-10 00:52:16 +00:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9338705372 | |||
| 791a78fadd |
@@ -1,8 +0,0 @@
|
||||
build
|
||||
cmake-build-*
|
||||
.DS_Store
|
||||
|
||||
*.swp
|
||||
*.kdev4
|
||||
|
||||
.idea
|
||||
-12
@@ -1,12 +0,0 @@
|
||||
[submodule "moonlight-common-c"]
|
||||
path = moonlight-common-c
|
||||
url = https://github.com/moonlight-stream/moonlight-common-c.git
|
||||
[submodule "Simple-Web-Server"]
|
||||
path = Simple-Web-Server
|
||||
url = https://github.com/loki-47-6F-64/Simple-Web-Server.git
|
||||
[submodule "ViGEmClient"]
|
||||
path = ViGEmClient
|
||||
url = https://github.com/ViGEm/ViGEmClient
|
||||
[submodule "pre-compiled"]
|
||||
path = pre-compiled
|
||||
url = https://bitbucket.org/Loki-47-6F-64/pre-compiled.git
|
||||
+1260
File diff suppressed because one or more lines are too long
-199
@@ -1,199 +0,0 @@
|
||||
cmake_minimum_required(VERSION 2.8)
|
||||
|
||||
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
|
||||
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)
|
||||
endif()
|
||||
|
||||
add_subdirectory(Simple-Web-Server)
|
||||
add_subdirectory(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)
|
||||
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)
|
||||
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)
|
||||
list(PREPEND PLATFORM_LIBRARIES
|
||||
winmm
|
||||
ksuser
|
||||
wsock32
|
||||
ws2_32
|
||||
iphlpapi
|
||||
d3d11 dxgi
|
||||
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")
|
||||
else()
|
||||
list(APPEND SUNSHINE_DEFINITIONS APPS_JSON="apps_linux.json")
|
||||
|
||||
find_package(X11 REQUIRED)
|
||||
set(PLATFORM_TARGET_FILES
|
||||
sunshine/platform/linux.cpp
|
||||
sunshine/platform/linux_evdev.cpp)
|
||||
|
||||
set(PLATFORM_LIBRARIES
|
||||
Xfixes
|
||||
Xtst
|
||||
xcb
|
||||
xcb-shm
|
||||
xcb-xfixes
|
||||
${X11_LIBRARIES}
|
||||
evdev
|
||||
pulse
|
||||
pulse-simple
|
||||
)
|
||||
|
||||
set(PLATFORM_INCLUDE_DIRS
|
||||
${X11_INCLUDE_DIR}
|
||||
/usr/include/libevdev-1.0)
|
||||
|
||||
if(NOT DEFINED SUNSHINE_EXECUTABLE_PATH)
|
||||
set(SUNSHINE_EXECUTABLE_PATH "${CMAKE_CURRENT_BINARY_DIR}/sunshine")
|
||||
endif()
|
||||
configure_file(gen-deb.in gen-deb @ONLY)
|
||||
configure_file(sunshine.service.in sunshine.service @ONLY)
|
||||
endif()
|
||||
|
||||
set(Boost_USE_STATIC_LIBS ON)
|
||||
find_package(Boost COMPONENTS log filesystem REQUIRED)
|
||||
|
||||
set(SUNSHINE_TARGET_FILES
|
||||
moonlight-common-c/reedsolomon/rs.c
|
||||
moonlight-common-c/reedsolomon/rs.h
|
||||
moonlight-common-c/src/Input.h
|
||||
moonlight-common-c/src/Rtsp.h
|
||||
moonlight-common-c/src/RtspParser.c
|
||||
moonlight-common-c/src/Video.h
|
||||
sunshine/utility.h
|
||||
sunshine/uuid.h
|
||||
sunshine/config.h
|
||||
sunshine/config.cpp
|
||||
sunshine/main.cpp
|
||||
sunshine/main.h
|
||||
sunshine/crypto.cpp
|
||||
sunshine/crypto.h
|
||||
sunshine/nvhttp.cpp
|
||||
sunshine/nvhttp.h
|
||||
sunshine/rtsp.cpp
|
||||
sunshine/rtsp.h
|
||||
sunshine/stream.cpp
|
||||
sunshine/stream.h
|
||||
sunshine/video.cpp
|
||||
sunshine/video.h
|
||||
sunshine/input.cpp
|
||||
sunshine/input.h
|
||||
sunshine/audio.cpp
|
||||
sunshine/audio.h
|
||||
sunshine/platform/common.h
|
||||
sunshine/process.cpp
|
||||
sunshine/process.h
|
||||
sunshine/network.cpp
|
||||
sunshine/network.h
|
||||
sunshine/move_by_copy.h
|
||||
sunshine/task_pool.h
|
||||
sunshine/thread_pool.h
|
||||
sunshine/thread_safe.h
|
||||
sunshine/sync.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
|
||||
${FFMPEG_INCLUDE_DIRS}
|
||||
${PLATFORM_INCLUDE_DIRS}
|
||||
)
|
||||
|
||||
string(TOUPPER "x${CMAKE_BUILD_TYPE}" BUILD_TYPE)
|
||||
if("${BUILD_TYPE}" STREQUAL "XDEBUG")
|
||||
list(APPEND SUNSHINE_COMPILE_OPTIONS -O0 -pedantic -ggdb3)
|
||||
if(WIN32)
|
||||
set_source_files_properties(sunshine/nvhttp.cpp PROPERTIES COMPILE_FLAGS -O2)
|
||||
endif()
|
||||
else()
|
||||
add_definitions(-DNDEBUG)
|
||||
list(APPEND SUNSHINE_COMPILE_OPTIONS -O3)
|
||||
endif()
|
||||
|
||||
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 SUNSHINE_EXTERNAL_LIBRARIES
|
||||
${CMAKE_THREAD_LIBS_INIT}
|
||||
stdc++fs
|
||||
enet
|
||||
opus
|
||||
${FFMPEG_LIBRARIES}
|
||||
${Boost_LIBRARIES}
|
||||
${OPENSSL_LIBRARIES}
|
||||
${PLATFORM_LIBRARIES})
|
||||
|
||||
list(APPEND SUNSHINE_DEFINITIONS SUNSHINE_ASSETS_DIR="${SUNSHINE_ASSETS_DIR}")
|
||||
add_executable(sunshine ${SUNSHINE_TARGET_FILES})
|
||||
target_link_libraries(sunshine ${SUNSHINE_EXTERNAL_LIBRARIES})
|
||||
target_compile_definitions(sunshine PUBLIC ${SUNSHINE_DEFINITIONS})
|
||||
set_target_properties(sunshine PROPERTIES CXX_STANDARD 17)
|
||||
|
||||
target_compile_options(sunshine PRIVATE ${SUNSHINE_COMPILE_OPTIONS})
|
||||
@@ -1,144 +0,0 @@
|
||||
# - Try to find FFMPEG
|
||||
# Once done this will define
|
||||
# FFMPEG_FOUND - System has FFMPEG
|
||||
# FFMPEG_INCLUDE_DIRS - The FFMPEG include directories
|
||||
# FFMPEG_LIBRARIES - The libraries needed to use FFMPEG
|
||||
# FFMPEG_LIBRARY_DIRS - The directory to find FFMPEG libraries
|
||||
#
|
||||
# written by Roy Shilkrot 2013 http://www.morethantechnical.com/
|
||||
#
|
||||
|
||||
find_package(PkgConfig)
|
||||
|
||||
|
||||
MACRO(FFMPEG_FIND varname shortname headername)
|
||||
|
||||
IF(NOT WIN32)
|
||||
PKG_CHECK_MODULES(PC_${varname} ${shortname})
|
||||
|
||||
FIND_PATH(${varname}_INCLUDE_DIR "${shortname}/${headername}"
|
||||
HINTS ${PC_${varname}_INCLUDEDIR} ${PC_${varname}_INCLUDE_DIRS}
|
||||
NO_DEFAULT_PATH
|
||||
)
|
||||
ELSE()
|
||||
FIND_PATH(${varname}_INCLUDE_DIR "${shortname}/${headername}")
|
||||
ENDIF()
|
||||
|
||||
IF(${varname}_INCLUDE_DIR STREQUAL "${varname}_INCLUDE_DIR-NOTFOUND")
|
||||
message(STATUS "look for newer strcture")
|
||||
IF(NOT WIN32)
|
||||
PKG_CHECK_MODULES(PC_${varname} "lib${shortname}")
|
||||
|
||||
FIND_PATH(${varname}_INCLUDE_DIR "lib${shortname}/${headername}"
|
||||
HINTS ${PC_${varname}_INCLUDEDIR} ${PC_${varname}_INCLUDE_DIRS}
|
||||
NO_DEFAULT_PATH
|
||||
)
|
||||
ELSE()
|
||||
FIND_PATH(${varname}_INCLUDE_DIR "lib${shortname}/${headername}")
|
||||
IF(${${varname}_INCLUDE_DIR} STREQUAL "${varname}_INCLUDE_DIR-NOTFOUND")
|
||||
#Desperate times call for desperate measures
|
||||
MESSAGE(STATUS "globbing...")
|
||||
FILE(GLOB_RECURSE ${varname}_INCLUDE_DIR "/ffmpeg*/${headername}")
|
||||
MESSAGE(STATUS "found: ${${varname}_INCLUDE_DIR}")
|
||||
IF(${varname}_INCLUDE_DIR)
|
||||
GET_FILENAME_COMPONENT(${varname}_INCLUDE_DIR "${${varname}_INCLUDE_DIR}" PATH)
|
||||
GET_FILENAME_COMPONENT(${varname}_INCLUDE_DIR "${${varname}_INCLUDE_DIR}" PATH)
|
||||
ELSE()
|
||||
SET(${varname}_INCLUDE_DIR "${varname}_INCLUDE_DIR-NOTFOUND")
|
||||
ENDIF()
|
||||
ENDIF()
|
||||
ENDIF()
|
||||
ENDIF()
|
||||
|
||||
|
||||
IF(${${varname}_INCLUDE_DIR} STREQUAL "${varname}_INCLUDE_DIR-NOTFOUND")
|
||||
MESSAGE(STATUS "Can't find includes for ${shortname}...")
|
||||
ELSE()
|
||||
MESSAGE(STATUS "Found ${shortname} include dirs: ${${varname}_INCLUDE_DIR}")
|
||||
|
||||
#GET_DIRECTORY_PROPERTY(FFMPEG_PARENT DIRECTORY ${${varname}_INCLUDE_DIR} PARENT_DIRECTORY)
|
||||
GET_FILENAME_COMPONENT(FFMPEG_PARENT ${${varname}_INCLUDE_DIR} PATH)
|
||||
MESSAGE(STATUS "Using FFMpeg dir parent as hint: ${FFMPEG_PARENT}")
|
||||
|
||||
IF(NOT WIN32)
|
||||
FIND_LIBRARY(${varname}_LIBRARIES NAMES ${shortname}
|
||||
HINTS ${PC_${varname}_LIBDIR} ${PC_${varname}_LIBRARY_DIR} ${FFMPEG_PARENT})
|
||||
ELSE()
|
||||
FIND_PATH(${varname}_LIBRARIES "${shortname}.dll.a" HINTS ${FFMPEG_PARENT})
|
||||
# FILE(GLOB_RECURSE ${varname}_LIBRARIES "${FFMPEG_PARENT}/*${shortname}.lib")
|
||||
# GLOBing is very bad... but windows sux, this is the only thing that works
|
||||
ENDIF()
|
||||
|
||||
IF(${varname}_LIBRARIES STREQUAL "${varname}_LIBRARIES-NOTFOUND")
|
||||
MESSAGE(STATUS "look for newer structure for library")
|
||||
FIND_LIBRARY(${varname}_LIBRARIES NAMES lib${shortname}
|
||||
HINTS ${PC_${varname}_LIBDIR} ${PC_${varname}_LIBRARY_DIR} ${FFMPEG_PARENT})
|
||||
ENDIF()
|
||||
|
||||
|
||||
IF(${varname}_LIBRARIES STREQUAL "${varname}_LIBRARIES-NOTFOUND")
|
||||
MESSAGE(STATUS "Can't find lib for ${shortname}...")
|
||||
ELSE()
|
||||
MESSAGE(STATUS "Found ${shortname} libs: ${${varname}_LIBRARIES}")
|
||||
ENDIF()
|
||||
|
||||
|
||||
IF(NOT ${varname}_INCLUDE_DIR STREQUAL "${varname}_INCLUDE_DIR-NOTFOUND"
|
||||
AND NOT ${varname}_LIBRARIES STREQUAL ${varname}_LIBRARIES-NOTFOUND)
|
||||
|
||||
MESSAGE(STATUS "found ${shortname}: include ${${varname}_INCLUDE_DIR} lib ${${varname}_LIBRARIES}")
|
||||
SET(FFMPEG_${varname}_FOUND 1)
|
||||
SET(FFMPEG_${varname}_INCLUDE_DIRS ${${varname}_INCLUDE_DIR})
|
||||
SET(FFMPEG_${varname}_LIBS ${${varname}_LIBRARIES})
|
||||
ELSE()
|
||||
MESSAGE(STATUS "Can't find ${shortname}")
|
||||
ENDIF()
|
||||
|
||||
ENDIF()
|
||||
|
||||
ENDMACRO(FFMPEG_FIND)
|
||||
|
||||
FFMPEG_FIND(LIBAVFORMAT avformat avformat.h)
|
||||
FFMPEG_FIND(LIBAVDEVICE avdevice avdevice.h)
|
||||
FFMPEG_FIND(LIBAVCODEC avcodec avcodec.h)
|
||||
FFMPEG_FIND(LIBAVUTIL avutil avutil.h)
|
||||
FFMPEG_FIND(LIBSWSCALE swscale swscale.h)
|
||||
|
||||
SET(FFMPEG_FOUND "NO")
|
||||
IF (FFMPEG_LIBAVFORMAT_FOUND AND
|
||||
FFMPEG_LIBAVDEVICE_FOUND AND
|
||||
FFMPEG_LIBAVCODEC_FOUND AND
|
||||
FFMPEG_LIBAVUTIL_FOUND AND
|
||||
FFMPEG_LIBSWSCALE_FOUND
|
||||
)
|
||||
|
||||
|
||||
SET(FFMPEG_FOUND "YES")
|
||||
|
||||
SET(FFMPEG_INCLUDE_DIRS ${FFMPEG_LIBAVFORMAT_INCLUDE_DIRS})
|
||||
|
||||
SET(FFMPEG_LIBRARY_DIRS ${FFMPEG_LIBAVFORMAT_LIBRARY_DIRS})
|
||||
|
||||
SET(FFMPEG_LIBRARIES
|
||||
${FFMPEG_LIBAVFORMAT_LIBS}
|
||||
${FFMPEG_LIBAVDEVICE_LIBS}
|
||||
${FFMPEG_LIBAVCODEC_LIBS}
|
||||
${FFMPEG_LIBAVUTIL_LIBS}
|
||||
${FFMPEG_LIBSWSCALE_LIBS}
|
||||
)
|
||||
|
||||
ELSE ()
|
||||
|
||||
MESSAGE(STATUS "Could not find FFMPEG")
|
||||
|
||||
ENDIF()
|
||||
|
||||
message(STATUS ${FFMPEG_LIBRARIES} ${FFMPEG_LIBAVFORMAT_LIBRARIES})
|
||||
|
||||
include(FindPackageHandleStandardArgs)
|
||||
# handle the QUIETLY and REQUIRED arguments and set FFMPEG_FOUND to TRUE
|
||||
# if all listed variables are TRUE
|
||||
find_package_handle_standard_args(FFMPEG DEFAULT_MSG
|
||||
FFMPEG_LIBRARIES FFMPEG_INCLUDE_DIRS)
|
||||
|
||||
mark_as_advanced(FFMPEG_INCLUDE_DIRS FFMPEG_LIBRARY_DIRS FFMPEG_LIBRARIES)
|
||||
@@ -1,674 +0,0 @@
|
||||
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>.
|
||||
-117
@@ -1,117 +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 https://github.com/loki-47-6F-64/sunshine.git --recurse-submodules
|
||||
* cd sunshine && 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 https://github.com/loki-47-6F-64/sunshine.git --recurse-submodules
|
||||
* 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]
|
||||
|
||||
== Static build ==
|
||||
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 git-lfs
|
||||
|
||||
Compilation:
|
||||
* git lfs install
|
||||
* git clone https://github.com/loki-47-6F-64/sunshine.git --recurse-submodules
|
||||
* cd sunshine && mkdir build && cd build
|
||||
* cmake -DSUNSHINE_STANDALONE=ON -DSUNSHINE_ASSETS_DIR=assets -G"Unix Makefiles" ..
|
||||
* make
|
||||
|
||||
|
||||
|
||||
######### 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.
|
||||
Submodule Simple-Web-Server deleted from f37a41d48b
-1
Submodule ViGEmClient deleted from 52682b59c4
@@ -1,48 +0,0 @@
|
||||
image:
|
||||
- Ubuntu
|
||||
- Visual Studio 2019
|
||||
|
||||
environment:
|
||||
matrix:
|
||||
- BUILD_TYPE: Debug
|
||||
- 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
|
||||
- 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:
|
||||
- git submodule update --init --recursive
|
||||
- mkdir build
|
||||
- cd 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 -DSUNSHINE_STANDALONE=ON -DSUNSHINE_ASSETS_DIR=assets -G "MinGW Makefiles" ..
|
||||
- sh: make -j$(nproc)
|
||||
- cmd: mingw32-make -j2
|
||||
- cmd: set PATH=%OLDPATH%
|
||||
|
||||
after_build:
|
||||
- sh: ./gen-deb
|
||||
- cmd: Del ..\assets\apps_linux.json
|
||||
- cmd: 7z a Sunshine-Windows.zip ..\assets
|
||||
- cmd: 7z a Sunshine-Windows.zip sunshine.exe
|
||||
- cmd: 7z a Sunshine-Windows.zip tools\dxgi-info.exe
|
||||
- cmd: 7z a Sunshine-Windows.zip tools\audio-info.exe
|
||||
- cmd: appveyor PushArtifact Sunshine-Windows.zip
|
||||
- sh: appveyor PushArtifact package-deb/sunshine.deb
|
||||
- sh: appveyor PushArtifact sunshine.service
|
||||
@@ -1,23 +0,0 @@
|
||||
{
|
||||
"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":[]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
{
|
||||
"env":{
|
||||
"PATH":"$(PATH);C:\\Program Files (x86)\\Steam"
|
||||
},
|
||||
"apps":[
|
||||
{
|
||||
"name":"Steam BigPicture",
|
||||
|
||||
"output":"steam.txt",
|
||||
"prep-cmd":[
|
||||
{"do":"steam \"steam://open/bigpicture\""}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 998 B |
@@ -1,130 +0,0 @@
|
||||
# If no external IP address is given, the local IP address is used
|
||||
# external_ip = 123.456.789.12
|
||||
|
||||
# The private key must be 2048 bits
|
||||
# pkey = /dir/pkey.pem
|
||||
|
||||
# The certificate must be signed with a 2048 bit key
|
||||
# cert = /dir/cert.pem
|
||||
|
||||
# The name displayed by Moonlight
|
||||
# If not specified, the PC's hostname is used
|
||||
# sunshine_name = Sunshine
|
||||
|
||||
# The minimum log level printed to standard out
|
||||
#
|
||||
# none -> no logs are printed to standard out
|
||||
#
|
||||
# verbose = [0]
|
||||
# debug = [1]
|
||||
# info = [2]
|
||||
# warning = [3]
|
||||
# error = [4]
|
||||
# fatal = [5]
|
||||
# none = [6]
|
||||
#
|
||||
# min_log_level = info
|
||||
|
||||
# The origin of the remote endpoint address that is not denied for HTTP method /pin
|
||||
# Could be any of the following values:
|
||||
# pc|lan|wan
|
||||
# pc: Only localhost may access /pin
|
||||
# lan: Only those in LAN may access /pin
|
||||
# wan: Anyone may access /pin
|
||||
#
|
||||
# origin_pin_allowed = lan
|
||||
|
||||
# The file where current state of Sunshine is stored
|
||||
# file_state = sunshine_state.json
|
||||
|
||||
# How long to wait in milliseconds for data from moonlight before shutting down the stream
|
||||
# ping_timeout = 2000
|
||||
|
||||
# The file where configuration for the different applications that Sunshine can run during a stream
|
||||
# file_apps = apps.json
|
||||
|
||||
# How much error correcting packets must be send for every video
|
||||
# This is just some random number, don't know the optimal value
|
||||
# The higher fec_percentage, the lower space for the actual data to send per frame there is
|
||||
#
|
||||
# The value must be greater than 0 and lower than or equal to 100
|
||||
# fec_percentage = 10
|
||||
|
||||
# When multicasting, it could be usefull to have different configurations for each connected Client.
|
||||
# For example:
|
||||
# Clients connected through WAN and LAN have different bitrate contstraints.
|
||||
# Decoders may require different settings for color
|
||||
#
|
||||
# Unlike simply broadcasting to multiple Client, this will generate distinct video streams.
|
||||
# Note, CPU usage increases for each distinct video stream generated
|
||||
# channels = 1
|
||||
|
||||
# The back/select button on the controller
|
||||
# On the Shield, the home and powerbutton are not passed to Moonlight
|
||||
# If, after the timeout, the back button is still pressed down, Home/Guide button press is emulated.
|
||||
# If back_button_timeout < 0, then the Home/Guide button will not be emulated
|
||||
# back_button_timeout = 2000
|
||||
|
||||
|
||||
# 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
|
||||
#
|
||||
# !! Windows only !!
|
||||
# tools\audio-info.exe
|
||||
# audio_sink = {0.0.0.00000000}.{FD47D9CC-4218-4135-9CE2-0C195C87405B}
|
||||
|
||||
# !! Windows only !!
|
||||
# You can select the video card you want to stream:
|
||||
# The appropriate values can be found using the following command:
|
||||
# tools\dxgi-info.exe
|
||||
# adapter_name = Radeon RX 580 Series
|
||||
# output_name = \\.\DISPLAY1
|
||||
|
||||
|
||||
###############################################
|
||||
# FFmpeg software encoding parameters
|
||||
# Honestly, I have no idea what the optimal values would be.
|
||||
# Play around with this :)
|
||||
|
||||
# Constant Rate Factor. Between 1 and 52. It allows QP to go up during motion and down with still image, resulting in constant perceived quality
|
||||
# Higher value means more compression, but less quality
|
||||
# If crf == 0, then use QP directly instead
|
||||
# crf = 0
|
||||
|
||||
# Quantitization Parameter
|
||||
# Higher value means more compression, but less quality
|
||||
# If crf != 0, then this parameter is ignored
|
||||
# qp = 28
|
||||
|
||||
# Minimum number of threads used by ffmpeg to encode the video.
|
||||
# Increasing the value slightly reduces encoding efficiency, but the tradeoff is usually
|
||||
# worth it to gain the use of more CPU cores for encoding. The ideal value is the lowest
|
||||
# value that can reliably encode at your desired streaming settings on your hardware.
|
||||
# min_threads = 2
|
||||
|
||||
# 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_mode = 2
|
||||
|
||||
# See x264 --fullhelp for the different presets
|
||||
# preset = superfast
|
||||
# tune = zerolatency
|
||||
#
|
||||
#
|
||||
##############################################
|
||||
# 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
|
||||
-80
@@ -1,80 +0,0 @@
|
||||
#!/bin/sh
|
||||
|
||||
if [ ! "@SUNSHINE_UNDEFINED_VARIABLE@" = "" ]; then
|
||||
echo "Please run gen-deb generated by cmake inside the build directory"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -d package-deb ]; then
|
||||
echo "package-deb already exists: It will be replaced"
|
||||
rm -rf package-deb
|
||||
fi
|
||||
|
||||
export DEBIAN=package-deb/sunshine/DEBIAN
|
||||
export RULES=package-deb/sunshine/etc/udev/rules.d
|
||||
export BIN=package-deb/sunshine/usr/bin
|
||||
export ASSETS=package-deb/sunshine/etc/sunshine
|
||||
|
||||
mkdir -p $DEBIAN
|
||||
mkdir -p $RULES
|
||||
mkdir -p $BIN
|
||||
mkdir -p $ASSETS
|
||||
|
||||
if [ ! -f sunshine ]; then
|
||||
echo "Error: Can't find sunshine"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
cat << 'EOF' > $DEBIAN/conffiles
|
||||
/etc/sunshine/sunshine.conf
|
||||
/etc/sunshine/apps_linux.json
|
||||
EOF
|
||||
|
||||
cat << 'EOF' > $DEBIAN/control
|
||||
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
|
||||
Description: Gamestream host for Moonlight
|
||||
EOF
|
||||
|
||||
cat << 'EOF' > $DEBIAN/postinst
|
||||
#!/bin/sh
|
||||
|
||||
export GROUP_INPUT=input
|
||||
|
||||
if [ -f /etc/group ]; then
|
||||
if ! grep -q $GROUP_INPUT /etc/group; then
|
||||
echo "Creating group $GROUP_INPUT"
|
||||
|
||||
groupadd $GROUP_INPUT
|
||||
fi
|
||||
else
|
||||
echo "Warning: /etc/group not found"
|
||||
fi
|
||||
|
||||
# Prevent necessity of rebooting system
|
||||
chmod 0660 /dev/uinput
|
||||
chown root:$GROUP_INPUT /dev/uinput
|
||||
EOF
|
||||
|
||||
cat << 'EOF' > $RULES/85-sunshine-rules.rules
|
||||
KERNEL=="uinput", GROUP="input", MODE="0660"
|
||||
EOF
|
||||
|
||||
cp sunshine $BIN/sunshine
|
||||
cp @CMAKE_CURRENT_SOURCE_DIR@/assets/apps_linux.json $ASSETS/apps_linux.json
|
||||
cp @CMAKE_CURRENT_SOURCE_DIR@/assets/sunshine.conf $ASSETS/sunshine.conf
|
||||
|
||||
chmod 755 $DEBIAN/postinst
|
||||
chmod 755 $BIN/sunshine
|
||||
chmod 644 $RULES/85-sunshine-rules.rules
|
||||
|
||||
cd package-deb
|
||||
if fakeroot dpkg-deb --build sunshine; then
|
||||
echo "generated debian package: @CMAKE_CURRENT_BINARY_DIR@/package-deb/sunshine.deb"
|
||||
fi
|
||||
cd ..
|
||||
|
||||
Submodule moonlight-common-c deleted from cfeb0ffd90
-1
Submodule pre-compiled deleted from 51f776dbd4
@@ -1,13 +0,0 @@
|
||||
[Unit]
|
||||
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
|
||||
@@ -1,131 +0,0 @@
|
||||
#include <thread>
|
||||
|
||||
#include <opus/opus_multistream.h>
|
||||
|
||||
#include "platform/common.h"
|
||||
|
||||
#include "utility.h"
|
||||
#include "thread_safe.h"
|
||||
#include "audio.h"
|
||||
#include "main.h"
|
||||
|
||||
namespace audio {
|
||||
using namespace std::literals;
|
||||
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;
|
||||
};
|
||||
|
||||
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};
|
||||
constexpr auto SAMPLE_RATE = 48000;
|
||||
static opus_stream_config_t stereo = {
|
||||
SAMPLE_RATE,
|
||||
2,
|
||||
1,
|
||||
1,
|
||||
map_stereo
|
||||
};
|
||||
|
||||
static opus_stream_config_t Surround51 = {
|
||||
SAMPLE_RATE,
|
||||
6,
|
||||
4,
|
||||
2,
|
||||
map_surround51
|
||||
};
|
||||
|
||||
static opus_stream_config_t HighSurround51 = {
|
||||
SAMPLE_RATE,
|
||||
6,
|
||||
6,
|
||||
0,
|
||||
map_high_surround51
|
||||
};
|
||||
|
||||
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 frame_size = config.packetDuration * stream->sampleRate / 1000;
|
||||
while(auto sample = samples->pop()) {
|
||||
packet_t packet { 16*1024 }; // 16KB
|
||||
|
||||
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);
|
||||
packets->stop();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
packet.fake_resize(bytes);
|
||||
packets->raise(std::make_pair(channel_data, std::move(packet)));
|
||||
}
|
||||
}
|
||||
|
||||
void capture(safe::signal_t *shutdown_event, packet_queue_t packets, config_t config, void *channel_data) {
|
||||
auto samples = std::make_shared<sample_queue_t::element_type>();
|
||||
std::thread thread { encodeThread, packets, samples, config, channel_data };
|
||||
|
||||
auto fg = util::fail_guard([&]() {
|
||||
samples->stop();
|
||||
thread.join();
|
||||
});
|
||||
|
||||
//FIXME: Pick correct opus_stream_config_t based on config.channels
|
||||
auto stream = &stereo;
|
||||
|
||||
auto mic = platf::microphone(stream->sampleRate);
|
||||
if(!mic) {
|
||||
BOOST_LOG(error) << "Couldn't create audio input"sv ;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
auto frame_size = config.packetDuration * stream->sampleRate / 1000;
|
||||
int samples_per_frame = frame_size * stream->channelCount;
|
||||
|
||||
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 ;
|
||||
|
||||
return;
|
||||
}
|
||||
return;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
|
||||
samples->raise(std::move(sample_buffer));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
#ifndef SUNSHINE_AUDIO_H
|
||||
#define SUNSHINE_AUDIO_H
|
||||
|
||||
#include "utility.h"
|
||||
#include "thread_safe.h"
|
||||
namespace audio {
|
||||
struct config_t {
|
||||
int packetDuration;
|
||||
int channels;
|
||||
int mask;
|
||||
};
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -1,338 +0,0 @@
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
#include <functional>
|
||||
#include <unordered_map>
|
||||
|
||||
#include <boost/asio.hpp>
|
||||
|
||||
#include "utility.h"
|
||||
#include "config.h"
|
||||
|
||||
#define CA_DIR "credentials"
|
||||
#define PRIVATE_KEY_FILE CA_DIR "/cakey.pem"
|
||||
#define CERTIFICATE_FILE CA_DIR "/cacert.pem"
|
||||
|
||||
#define APPS_JSON_PATH SUNSHINE_ASSETS_DIR "/" APPS_JSON
|
||||
namespace config {
|
||||
using namespace std::literals;
|
||||
video_t video {
|
||||
0, // crf
|
||||
28, // qp
|
||||
|
||||
2, // min_threads
|
||||
|
||||
0, // hevc_mode
|
||||
"superfast"s, // preset
|
||||
"zerolatency"s, // tune
|
||||
{}, // adapter_name
|
||||
{} // output_name
|
||||
};
|
||||
|
||||
audio_t audio {};
|
||||
|
||||
stream_t stream {
|
||||
2s, // ping_timeout
|
||||
|
||||
APPS_JSON_PATH,
|
||||
|
||||
10, // fecPercentage
|
||||
1 // channels
|
||||
};
|
||||
|
||||
nvhttp_t nvhttp {
|
||||
"lan", // origin_pin
|
||||
PRIVATE_KEY_FILE,
|
||||
CERTIFICATE_FILE,
|
||||
|
||||
boost::asio::ip::host_name(), // sunshine_name,
|
||||
"sunshine_state.json"s // file_state
|
||||
};
|
||||
|
||||
input_t input {
|
||||
2s
|
||||
};
|
||||
|
||||
sunshine_t sunshine {
|
||||
2, // min_log_level
|
||||
0 // flags
|
||||
};
|
||||
|
||||
bool whitespace(char ch) {
|
||||
return ch == ' ' || ch == '\t';
|
||||
}
|
||||
|
||||
std::string to_string(const char *begin, const char *end) {
|
||||
return { begin, (std::size_t)(end - begin) };
|
||||
}
|
||||
|
||||
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();
|
||||
|
||||
auto eq = std::find(begin, end, '=');
|
||||
if(eq == end || eq == begin) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
auto end_name = std::find_if(std::make_reverse_iterator(eq), 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) };
|
||||
}
|
||||
|
||||
std::unordered_map<std::string, std::string> parse_config(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);
|
||||
|
||||
pos = (*newline == '\r') ? newline + 2 : newline + 1;
|
||||
if(!var) {
|
||||
continue;
|
||||
}
|
||||
|
||||
vars.emplace(std::move(*var));
|
||||
}
|
||||
|
||||
return vars;
|
||||
}
|
||||
|
||||
void string_f(std::unordered_map<std::string, std::string> &vars, const std::string &name, std::string &input) {
|
||||
auto it = vars.find(name);
|
||||
if(it == std::end(vars)) {
|
||||
return;
|
||||
}
|
||||
|
||||
input = std::move(it->second);
|
||||
|
||||
vars.erase(it);
|
||||
}
|
||||
|
||||
void string_restricted_f(std::unordered_map<std::string, std::string> &vars, const std::string &name, std::string &input, const std::vector<std::string_view> &allowed_vals) {
|
||||
std::string temp;
|
||||
string_f(vars, name, temp);
|
||||
|
||||
for(auto &allowed_val : allowed_vals) {
|
||||
if(temp == allowed_val) {
|
||||
input = std::move(temp);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void int_f(std::unordered_map<std::string, std::string> &vars, const std::string &name, 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);
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
int_f(vars, name, temp);
|
||||
|
||||
TUPLE_2D_REF(lower, upper, range);
|
||||
if(temp >= lower && temp <= upper) {
|
||||
input = temp;
|
||||
}
|
||||
}
|
||||
|
||||
void print_help(const char *name) {
|
||||
std::cout <<
|
||||
"Usage: "sv << name << " [options] [/path/to/configuration_file]"sv << std::endl <<
|
||||
" Any configurable option can be overwritten with: \"name=value\""sv << std::endl << std::endl <<
|
||||
" --help | print help"sv << std::endl << std::endl <<
|
||||
" flags"sv << std::endl <<
|
||||
" -0 | Read PIN from stdin"sv << std::endl <<
|
||||
" -1 | Do not load previously saved state and do retain any state after shutdown"sv << std::endl <<
|
||||
" | Effectively starting as if for the first time without overwriting any pairings with your devices"sv;
|
||||
}
|
||||
|
||||
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;
|
||||
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;
|
||||
}
|
||||
|
||||
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);
|
||||
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);
|
||||
string_f(vars, "sunshine_name", nvhttp.sunshine_name);
|
||||
string_f(vars, "file_state", nvhttp.file_state);
|
||||
string_f(vars, "external_ip", nvhttp.external_ip);
|
||||
|
||||
string_f(vars, "audio_sink", audio.sink);
|
||||
|
||||
string_restricted_f(vars, "origin_pin_allowed", nvhttp.origin_pin_allowed, {
|
||||
"pc"sv, "lan"sv, "wan"sv
|
||||
});
|
||||
|
||||
int to = -1;
|
||||
int_between_f(vars, "ping_timeout", to, {
|
||||
-1, std::numeric_limits<int>::max()
|
||||
});
|
||||
if(to != -1) {
|
||||
stream.ping_timeout = std::chrono::milliseconds(to);
|
||||
}
|
||||
|
||||
int_between_f(vars, "channels", stream.channels, {
|
||||
1, std::numeric_limits<int>::max()
|
||||
});
|
||||
|
||||
string_f(vars, "file_apps", stream.file_apps);
|
||||
int_between_f(vars, "fec_percentage", stream.fec_percentage, {
|
||||
1, 100
|
||||
});
|
||||
|
||||
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 };
|
||||
}
|
||||
|
||||
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
|
||||
});
|
||||
|
||||
if(!log_level_string.empty()) {
|
||||
if(log_level_string == "verbose"sv) {
|
||||
sunshine.min_log_level = 0;
|
||||
}
|
||||
else if(log_level_string == "debug"sv) {
|
||||
sunshine.min_log_level = 1;
|
||||
}
|
||||
else if(log_level_string == "info"sv) {
|
||||
sunshine.min_log_level = 2;
|
||||
}
|
||||
else if(log_level_string == "warning"sv) {
|
||||
sunshine.min_log_level = 3;
|
||||
}
|
||||
else if(log_level_string == "error"sv) {
|
||||
sunshine.min_log_level = 4;
|
||||
}
|
||||
else if(log_level_string == "fatal"sv) {
|
||||
sunshine.min_log_level = 5;
|
||||
}
|
||||
else if(log_level_string == "none"sv) {
|
||||
sunshine.min_log_level = 6;
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
std::cout << "Warning: Unrecognized configurable option ["sv << var << ']' << std::endl;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int parse(int argc, char *argv[]) {
|
||||
const char *config_file = SUNSHINE_ASSETS_DIR "/sunshine.conf";
|
||||
|
||||
std::unordered_map<std::string, std::string> cmd_vars;
|
||||
|
||||
for(auto x = argc -1; x > 0; --x) {
|
||||
auto line = argv[x];
|
||||
|
||||
if(line == "--help"sv) {
|
||||
print_help(*argv);
|
||||
return 1;
|
||||
}
|
||||
else if(*line == '-') {
|
||||
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) {
|
||||
config_file = line;
|
||||
}
|
||||
else {
|
||||
auto var = parse_line(line, line_end);
|
||||
if(!var) {
|
||||
print_help(*argv);
|
||||
return -1;
|
||||
}
|
||||
|
||||
cmd_vars.emplace(std::move(*var));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::ifstream in { config_file };
|
||||
|
||||
if(!in.is_open()) {
|
||||
std::cout << "Error: Couldn't open "sv << config_file << std::endl;
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
auto vars = parse_config(std::string {
|
||||
// Quick and dirty
|
||||
std::istreambuf_iterator<char>(in),
|
||||
std::istreambuf_iterator<char>()
|
||||
});
|
||||
|
||||
for(auto &var : cmd_vars) {
|
||||
vars.emplace(std::move(var));
|
||||
}
|
||||
|
||||
apply_config(std::move(vars));
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
@@ -1,82 +0,0 @@
|
||||
#ifndef SUNSHINE_CONFIG_H
|
||||
#define SUNSHINE_CONFIG_H
|
||||
|
||||
#include <chrono>
|
||||
#include <string>
|
||||
#include <bitset>
|
||||
|
||||
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 hevc_mode;
|
||||
std::string preset;
|
||||
std::string tune;
|
||||
|
||||
std::string adapter_name;
|
||||
std::string output_name;
|
||||
};
|
||||
|
||||
struct audio_t {
|
||||
std::string sink;
|
||||
};
|
||||
|
||||
struct stream_t {
|
||||
std::chrono::milliseconds ping_timeout;
|
||||
|
||||
std::string file_apps;
|
||||
|
||||
int fec_percentage;
|
||||
|
||||
// max unique instances of video and audio streams
|
||||
int channels;
|
||||
};
|
||||
|
||||
struct nvhttp_t {
|
||||
// Could be any of the following values:
|
||||
// pc|lan|wan
|
||||
std::string origin_pin_allowed;
|
||||
|
||||
std::string pkey; // must be 2048 bits
|
||||
std::string cert; // must be signed with a key of 2048 bits
|
||||
|
||||
std::string sunshine_name;
|
||||
|
||||
std::string file_state;
|
||||
|
||||
std::string external_ip;
|
||||
};
|
||||
|
||||
struct input_t {
|
||||
std::chrono::milliseconds back_button_timeout;
|
||||
};
|
||||
|
||||
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
|
||||
FLAG_SIZE
|
||||
};
|
||||
}
|
||||
|
||||
struct sunshine_t {
|
||||
int min_log_level;
|
||||
|
||||
std::bitset<flag::FLAG_SIZE> flags;
|
||||
};
|
||||
|
||||
extern video_t video;
|
||||
extern audio_t audio;
|
||||
extern stream_t stream;
|
||||
extern nvhttp_t nvhttp;
|
||||
extern input_t input;
|
||||
extern sunshine_t sunshine;
|
||||
|
||||
int parse(int argc, char *argv[]);
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -1,341 +0,0 @@
|
||||
//
|
||||
// Created by loki on 5/31/19.
|
||||
//
|
||||
|
||||
#include <openssl/pem.h>
|
||||
#include "crypto.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() } {}
|
||||
void cert_chain_t::add(x509_t &&cert) {
|
||||
x509_store_t x509_store { X509_STORE_new() };
|
||||
|
||||
X509_STORE_add_cert(x509_store.get(), cert.get());
|
||||
_certs.emplace_back(std::make_pair(std::move(cert), std::move(x509_store)));
|
||||
}
|
||||
|
||||
/*
|
||||
* When certificates from two or more instances of Moonlight have been added to x509_store_t,
|
||||
* only one of them will be verified by X509_verify_cert, resulting in only a single instance of
|
||||
* Moonlight to be able to use Sunshine
|
||||
*
|
||||
* To circumvent this, x509_store_t instance will be created for each instance of the certificates.
|
||||
*/
|
||||
const char *cert_chain_t::verify(x509_t::element_type *cert) {
|
||||
int err_code = 0;
|
||||
for(auto &[_,x509_store] : _certs) {
|
||||
auto fg = util::fail_guard([this]() {
|
||||
X509_STORE_CTX_cleanup(_cert_ctx.get());
|
||||
});
|
||||
|
||||
X509_STORE_CTX_init(_cert_ctx.get(), x509_store.get(), nullptr, nullptr);
|
||||
X509_STORE_CTX_set_cert(_cert_ctx.get(), cert);
|
||||
|
||||
auto err = X509_verify_cert(_cert_ctx.get());
|
||||
|
||||
if (err == 1) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
err_code = X509_STORE_CTX_get_error(_cert_ctx.get());
|
||||
|
||||
//FIXME: Checking for X509_V_ERR_UNABLE_TO_GET_ISSUER_CERT_LOCALLY is a temporary workaround to get mmonlight-embedded to work on the raspberry pi
|
||||
if(err_code == X509_V_ERR_UNABLE_TO_GET_ISSUER_CERT_LOCALLY) {
|
||||
return nullptr;
|
||||
}
|
||||
if (err_code != X509_V_ERR_DEPTH_ZERO_SELF_SIGNED_CERT && err_code != X509_V_ERR_INVALID_CA) {
|
||||
return X509_verify_cert_error_string(err_code);
|
||||
}
|
||||
}
|
||||
|
||||
return X509_verify_cert_error_string(err_code);
|
||||
}
|
||||
|
||||
cipher_t::cipher_t(const crypto::aes_t &key) : ctx { EVP_CIPHER_CTX_new() }, key { key }, padding { true } {}
|
||||
int cipher_t::decrypt(const std::string_view &cipher, std::vector<std::uint8_t> &plaintext) {
|
||||
int len;
|
||||
|
||||
auto fg = util::fail_guard([this]() {
|
||||
EVP_CIPHER_CTX_reset(ctx.get());
|
||||
});
|
||||
|
||||
// Gen 7 servers use 128-bit AES ECB
|
||||
if (EVP_DecryptInit_ex(ctx.get(), EVP_aes_128_ecb(), nullptr, key.data(), nullptr) != 1) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
EVP_CIPHER_CTX_set_padding(ctx.get(), padding);
|
||||
|
||||
plaintext.resize((cipher.size() + 15) / 16 * 16);
|
||||
auto size = (int)plaintext.size();
|
||||
// Encrypt into the caller's buffer, leaving room for the auth tag to be prepended
|
||||
if (EVP_DecryptUpdate(ctx.get(), plaintext.data(), &size, (const std::uint8_t*)cipher.data(), cipher.size()) != 1) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (EVP_DecryptFinal_ex(ctx.get(), plaintext.data(), &len) != 1) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
plaintext.resize(len + size);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int cipher_t::decrypt_gcm(aes_t &iv, const std::string_view &tagged_cipher,
|
||||
std::vector<std::uint8_t> &plaintext) {
|
||||
auto cipher = tagged_cipher.substr(16);
|
||||
auto tag = tagged_cipher.substr(0, 16);
|
||||
|
||||
auto fg = util::fail_guard([this]() {
|
||||
EVP_CIPHER_CTX_reset(ctx.get());
|
||||
});
|
||||
|
||||
if (EVP_DecryptInit_ex(ctx.get(), EVP_aes_128_gcm(), nullptr, nullptr, nullptr) != 1) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (EVP_CIPHER_CTX_ctrl(ctx.get(), EVP_CTRL_GCM_SET_IVLEN, iv.size(), nullptr) != 1) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (EVP_DecryptInit_ex(ctx.get(), nullptr, nullptr, key.data(), iv.data()) != 1) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
EVP_CIPHER_CTX_set_padding(ctx.get(), padding);
|
||||
plaintext.resize((cipher.size() + 15) / 16 * 16);
|
||||
|
||||
int size;
|
||||
if (EVP_DecryptUpdate(ctx.get(), plaintext.data(), &size, (const std::uint8_t*)cipher.data(), cipher.size()) != 1) {
|
||||
return -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) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
plaintext.resize(size + len);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int cipher_t::encrypt(const std::string_view &plaintext, std::vector<std::uint8_t> &cipher) {
|
||||
int len;
|
||||
|
||||
auto fg = util::fail_guard([this]() {
|
||||
EVP_CIPHER_CTX_reset(ctx.get());
|
||||
});
|
||||
|
||||
// Gen 7 servers use 128-bit AES ECB
|
||||
if (EVP_EncryptInit_ex(ctx.get(), EVP_aes_128_ecb(), nullptr, key.data(), nullptr) != 1) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
EVP_CIPHER_CTX_set_padding(ctx.get(), padding);
|
||||
|
||||
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) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (EVP_EncryptFinal_ex(ctx.get(), cipher.data() + size, &len) != 1) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
cipher.resize(len + size);
|
||||
return 0;
|
||||
}
|
||||
|
||||
aes_t gen_aes_key(const std::array<uint8_t, 16> &salt, const std::string_view &pin) {
|
||||
aes_t key;
|
||||
|
||||
std::string salt_pin;
|
||||
salt_pin.reserve(salt.size() + pin.size());
|
||||
|
||||
salt_pin.insert(std::end(salt_pin), std::begin(salt), std::end(salt));
|
||||
salt_pin.insert(std::end(salt_pin), std::begin(pin), std::end(pin));
|
||||
|
||||
auto hsh = hash(salt_pin);
|
||||
|
||||
std::copy(std::begin(hsh), std::begin(hsh) + key.size(), std::begin(key));
|
||||
|
||||
return key;
|
||||
}
|
||||
|
||||
sha256_t hash(const std::string_view &plaintext) {
|
||||
sha256_t hsh;
|
||||
|
||||
SHA256_CTX sha256;
|
||||
SHA256_Init(&sha256);
|
||||
SHA256_Update(&sha256, plaintext.data(), plaintext.size());
|
||||
SHA256_Final(hsh.data(), &sha256);
|
||||
|
||||
return hsh;
|
||||
}
|
||||
|
||||
x509_t x509(const std::string_view &x) {
|
||||
bio_t io { BIO_new(BIO_s_mem()) };
|
||||
|
||||
BIO_write(io.get(), x.data(), x.size());
|
||||
|
||||
X509 *p = nullptr;
|
||||
PEM_read_bio_X509(io.get(), &p, nullptr, nullptr);
|
||||
|
||||
return x509_t { p };
|
||||
}
|
||||
|
||||
pkey_t pkey(const std::string_view &k) {
|
||||
bio_t io { BIO_new(BIO_s_mem()) };
|
||||
|
||||
BIO_write(io.get(), k.data(), k.size());
|
||||
|
||||
EVP_PKEY *p = nullptr;
|
||||
PEM_read_bio_PrivateKey(io.get(), &p, nullptr, nullptr);
|
||||
|
||||
return pkey_t { p };
|
||||
}
|
||||
|
||||
std::string pem(x509_t &x509) {
|
||||
bio_t bio { BIO_new(BIO_s_mem()) };
|
||||
|
||||
PEM_write_bio_X509(bio.get(), x509.get());
|
||||
BUF_MEM *mem_ptr;
|
||||
BIO_get_mem_ptr(bio.get(), &mem_ptr);
|
||||
|
||||
return { mem_ptr->data, mem_ptr->length };
|
||||
}
|
||||
|
||||
std::string pem(pkey_t &pkey) {
|
||||
bio_t bio { BIO_new(BIO_s_mem()) };
|
||||
|
||||
PEM_write_bio_PrivateKey(bio.get(), pkey.get(), nullptr, nullptr, 0, nullptr, nullptr);
|
||||
BUF_MEM *mem_ptr;
|
||||
BIO_get_mem_ptr(bio.get(), &mem_ptr);
|
||||
|
||||
return { mem_ptr->data, mem_ptr->length };
|
||||
}
|
||||
|
||||
std::string_view signature(const x509_t &x) {
|
||||
// X509_ALGOR *_ = nullptr;
|
||||
|
||||
const ASN1_BIT_STRING *asn1 = nullptr;
|
||||
X509_get0_signature(&asn1, nullptr, x.get());
|
||||
|
||||
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());
|
||||
|
||||
return r;
|
||||
}
|
||||
|
||||
std::vector<uint8_t> sign(const pkey_t &pkey, const std::string_view &data, const EVP_MD *md) {
|
||||
md_ctx_t ctx { EVP_MD_CTX_create() };
|
||||
|
||||
if(EVP_DigestSignInit(ctx.get(), nullptr, md, nullptr, pkey.get()) != 1) {
|
||||
return {};
|
||||
}
|
||||
|
||||
if(EVP_DigestSignUpdate(ctx.get(), data.data(), data.size()) != 1) {
|
||||
return {};
|
||||
}
|
||||
|
||||
std::size_t slen = digest_size;
|
||||
|
||||
std::vector<uint8_t> digest;
|
||||
digest.resize(slen);
|
||||
|
||||
if(EVP_DigestSignFinal(ctx.get(), digest.data(), &slen) != 1) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return digest;
|
||||
}
|
||||
|
||||
creds_t gen_creds(const std::string_view &cn, std::uint32_t key_bits) {
|
||||
x509_t x509 { X509_new() };
|
||||
pkey_t pkey { EVP_PKEY_new() };
|
||||
|
||||
big_num_t big_num { BN_new() };
|
||||
BN_set_word(big_num.get(), RSA_F4);
|
||||
|
||||
auto rsa = RSA_new();
|
||||
RSA_generate_key_ex(rsa, key_bits, big_num.get(), nullptr);
|
||||
EVP_PKEY_assign_RSA(pkey.get(), rsa);
|
||||
|
||||
X509_set_version(x509.get(), 2);
|
||||
ASN1_INTEGER_set(X509_get_serialNumber(x509.get()), 0);
|
||||
|
||||
constexpr auto year = 60 * 60 * 24 * 365;
|
||||
#if OPENSSL_VERSION_NUMBER < 0x10100000L
|
||||
X509_gmtime_adj(X509_get_notBefore(x509.get()), 0);
|
||||
X509_gmtime_adj(X509_get_notAfter(x509.get()), 20 * year);
|
||||
#else
|
||||
asn1_string_t not_before { ASN1_STRING_dup(X509_get0_notBefore(x509.get())) };
|
||||
asn1_string_t not_after { ASN1_STRING_dup(X509_get0_notAfter(x509.get())) };
|
||||
|
||||
X509_gmtime_adj(not_before.get(), 0);
|
||||
X509_gmtime_adj(not_after.get(), 20 * year);
|
||||
|
||||
X509_set1_notBefore(x509.get(), not_before.get());
|
||||
X509_set1_notAfter(x509.get(), not_after.get());
|
||||
#endif
|
||||
|
||||
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(),
|
||||
-1, 0);
|
||||
|
||||
X509_set_issuer_name(x509.get(), name);
|
||||
X509_sign(x509.get(), pkey.get(), EVP_sha256());
|
||||
|
||||
return { pem(x509), pem(pkey) };
|
||||
}
|
||||
|
||||
std::vector<uint8_t> sign256(const pkey_t &pkey, const std::string_view &data) {
|
||||
return sign(pkey, data, EVP_sha256());
|
||||
}
|
||||
|
||||
bool verify(const x509_t &x509, const std::string_view &data, const std::string_view &signature, const EVP_MD *md) {
|
||||
auto pkey = X509_get_pubkey(x509.get());
|
||||
|
||||
md_ctx_t ctx { EVP_MD_CTX_create() };
|
||||
|
||||
if(EVP_DigestVerifyInit(ctx.get(), nullptr, md, nullptr, pkey) != 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if(EVP_DigestVerifyUpdate(ctx.get(), data.data(), data.size()) != 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if(EVP_DigestVerifyFinal(ctx.get(), (const uint8_t*)signature.data(), signature.size()) != 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool verify256(const x509_t &x509, const std::string_view &data, const std::string_view &signature) {
|
||||
return verify(x509, data, signature, EVP_sha256());
|
||||
}
|
||||
|
||||
void md_ctx_destroy(EVP_MD_CTX *ctx) {
|
||||
EVP_MD_CTX_destroy(ctx);
|
||||
}
|
||||
}
|
||||
@@ -1,85 +0,0 @@
|
||||
//
|
||||
// Created by loki on 6/1/19.
|
||||
//
|
||||
|
||||
#ifndef SUNSHINE_CRYPTO_H
|
||||
#define SUNSHINE_CRYPTO_H
|
||||
|
||||
#include <cassert>
|
||||
#include <array>
|
||||
#include <openssl/evp.h>
|
||||
#include <openssl/sha.h>
|
||||
#include <openssl/x509.h>
|
||||
#include <openssl/rand.h>
|
||||
|
||||
#include "utility.h"
|
||||
|
||||
namespace crypto {
|
||||
struct creds_t {
|
||||
std::string x509;
|
||||
std::string pkey;
|
||||
};
|
||||
constexpr std::size_t digest_size = 256;
|
||||
|
||||
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 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>;
|
||||
|
||||
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);
|
||||
pkey_t pkey(const std::string_view &k);
|
||||
std::string pem(x509_t &x509);
|
||||
std::string pem(pkey_t &pkey);
|
||||
|
||||
std::vector<uint8_t> sign256(const pkey_t &pkey, const std::string_view &data);
|
||||
bool verify256(const x509_t &x509, const std::string_view &data, const std::string_view &signature);
|
||||
|
||||
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);
|
||||
|
||||
class cert_chain_t {
|
||||
public:
|
||||
KITTY_DECL_CONSTR(cert_chain_t)
|
||||
|
||||
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;
|
||||
};
|
||||
|
||||
class cipher_t {
|
||||
public:
|
||||
cipher_t(const aes_t &key);
|
||||
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;
|
||||
|
||||
public:
|
||||
bool padding;
|
||||
};
|
||||
}
|
||||
|
||||
#endif //SUNSHINE_CRYPTO_H
|
||||
@@ -1,384 +0,0 @@
|
||||
//
|
||||
// Created by loki on 6/20/19.
|
||||
//
|
||||
|
||||
extern "C" {
|
||||
#include <moonlight-common-c/src/Input.h>
|
||||
}
|
||||
|
||||
#include <bitset>
|
||||
|
||||
#include "main.h"
|
||||
#include "config.h"
|
||||
#include "utility.h"
|
||||
#include "platform/common.h"
|
||||
#include "thread_pool.h"
|
||||
|
||||
namespace input {
|
||||
|
||||
constexpr auto MAX_GAMEPADS = std::min((std::size_t)platf::MAX_GAMEPADS, sizeof(std::int16_t)*8);
|
||||
enum class button_state_e {
|
||||
NONE,
|
||||
DOWN,
|
||||
UP
|
||||
};
|
||||
|
||||
template<std::size_t N>
|
||||
int alloc_id(std::bitset<N> &gamepad_mask) {
|
||||
for(int x = 0; x < gamepad_mask.size(); ++x) {
|
||||
if(!gamepad_mask[x]) {
|
||||
gamepad_mask[x] = true;
|
||||
return x;
|
||||
}
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
template<std::size_t N>
|
||||
void free_id(std::bitset<N> &gamepad_mask, int id) {
|
||||
gamepad_mask[id] = false;
|
||||
}
|
||||
|
||||
static 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) { }
|
||||
|
||||
std::uint16_t active_gamepad_state;
|
||||
std::vector<gamepad_t> gamepads;
|
||||
};
|
||||
|
||||
using namespace std::literals;
|
||||
|
||||
void print(PNV_MOUSE_MOVE_PACKET packet) {
|
||||
BOOST_LOG(debug)
|
||||
<< "--begin 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;
|
||||
}
|
||||
|
||||
void print(PNV_MOUSE_BUTTON_PACKET packet) {
|
||||
BOOST_LOG(debug)
|
||||
<< "--begin mouse button packet--"sv << std::endl
|
||||
<< "action ["sv << util::hex(packet->action).to_string_view() << ']' << std::endl
|
||||
<< "button ["sv << util::hex(packet->button).to_string_view() << ']' << std::endl
|
||||
<< "--end mouse button packet--"sv;
|
||||
}
|
||||
|
||||
void print(PNV_SCROLL_PACKET packet) {
|
||||
BOOST_LOG(debug)
|
||||
<< "--begin mouse scroll packet--"sv << std::endl
|
||||
<< "scrollAmt1 ["sv << util::endian::big(packet->scrollAmt1) << ']' << std::endl
|
||||
<< "--end mouse scroll packet--"sv;
|
||||
}
|
||||
|
||||
void print(PNV_KEYBOARD_PACKET packet) {
|
||||
BOOST_LOG(debug)
|
||||
<< "--begin keyboard packet--"sv << std::endl
|
||||
<< "keyAction ["sv << util::hex(packet->keyAction).to_string_view() << ']' << std::endl
|
||||
<< "keyCode ["sv << util::hex(packet->keyCode).to_string_view() << ']' << std::endl
|
||||
<< "modifiers ["sv << util::hex(packet->modifiers).to_string_view() << ']' << std::endl
|
||||
<< "--end keyboard packet--"sv;
|
||||
}
|
||||
|
||||
void print(PNV_MULTI_CONTROLLER_PACKET packet) {
|
||||
BOOST_LOG(debug)
|
||||
<< "--begin controller packet--"sv << std::endl
|
||||
<< "controllerNumber ["sv << packet->controllerNumber << ']' << std::endl
|
||||
<< "activeGamepadMask ["sv << util::hex(packet->activeGamepadMask).to_string_view() << ']' << std::endl
|
||||
<< "buttonFlags ["sv << util::hex(packet->buttonFlags).to_string_view() << ']' << std::endl
|
||||
<< "leftTrigger ["sv << util::hex(packet->leftTrigger).to_string_view() << ']' << std::endl
|
||||
<< "rightTrigger ["sv << util::hex(packet->rightTrigger).to_string_view() << ']' << std::endl
|
||||
<< "leftStickX ["sv << packet->leftStickX << ']' << std::endl
|
||||
<< "leftStickY ["sv << packet->leftStickY << ']' << std::endl
|
||||
<< "rightStickX ["sv << packet->rightStickX << ']' << std::endl
|
||||
<< "rightStickY ["sv << packet->rightStickY << ']' << std::endl
|
||||
<< "--end controller packet--"sv;
|
||||
}
|
||||
|
||||
constexpr int PACKET_TYPE_SCROLL_OR_KEYBOARD = PACKET_TYPE_SCROLL;
|
||||
void print(void *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_MULTI_CONTROLLER:
|
||||
print((PNV_MULTI_CONTROLLER_PACKET)input);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void passthrough(platf::input_t &input, PNV_MOUSE_MOVE_PACKET packet) {
|
||||
display_cursor = true;
|
||||
|
||||
platf::move_mouse(input, util::endian::big(packet->deltaX), util::endian::big(packet->deltaY));
|
||||
}
|
||||
|
||||
void passthrough(std::shared_ptr<input_t> &input, PNV_MOUSE_BUTTON_PACKET packet) {
|
||||
auto constexpr BUTTON_RELEASED = 0x09;
|
||||
|
||||
display_cursor = true;
|
||||
|
||||
auto button = util::endian::big(packet->button);
|
||||
if(button > 0 && button < mouse_press.size()) {
|
||||
mouse_press[button] = packet->action != BUTTON_RELEASED;
|
||||
}
|
||||
|
||||
platf::button_mouse(platf_input, button, packet->action == BUTTON_RELEASED);
|
||||
}
|
||||
|
||||
void passthrough(std::shared_ptr<input_t> &input, PNV_KEYBOARD_PACKET packet) {
|
||||
auto constexpr BUTTON_RELEASED = 0x04;
|
||||
|
||||
key_press[packet->keyCode] = packet->keyAction != BUTTON_RELEASED;
|
||||
platf::keyboard(platf_input, packet->keyCode & 0x00FF, packet->keyAction == BUTTON_RELEASED);
|
||||
}
|
||||
|
||||
void passthrough(platf::input_t &input, PNV_SCROLL_PACKET packet) {
|
||||
display_cursor = true;
|
||||
|
||||
platf::scroll(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 = packet->buttonFlags;
|
||||
platf::gamepad_state_t gamepad_state{
|
||||
bf,
|
||||
packet->leftTrigger,
|
||||
packet->rightTrigger,
|
||||
packet->leftStickX,
|
||||
packet->leftStickY,
|
||||
packet->rightStickX,
|
||||
packet->rightStickY
|
||||
};
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
bf = gamepad_state.buttonFlags ^ gamepad.gamepad_state.buttonFlags;
|
||||
bf_new = gamepad_state.buttonFlags;
|
||||
|
||||
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 &gamepad = input->gamepads[controller];
|
||||
|
||||
auto &state = gamepad.gamepad_state;
|
||||
|
||||
// Force the back button up
|
||||
gamepad.back_button_state = button_state_e::UP;
|
||||
state.buttonFlags &= ~platf::BACK;
|
||||
platf::gamepad(platf_input, gamepad.id, state);
|
||||
|
||||
// Press Home button
|
||||
state.buttonFlags |= platf::HOME;
|
||||
platf::gamepad(platf_input, gamepad.id, state);
|
||||
|
||||
// Release Home button
|
||||
state.buttonFlags &= ~platf::HOME;
|
||||
platf::gamepad(platf_input, gamepad.id, state);
|
||||
|
||||
gamepad.back_timeout_id = nullptr;
|
||||
}, config::input.back_button_timeout).task_id;
|
||||
}
|
||||
}
|
||||
else if (gamepad.back_timeout_id) {
|
||||
task_pool.cancel(gamepad.back_timeout_id);
|
||||
gamepad.back_timeout_id = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
platf::gamepad(platf_input, gamepad.id, gamepad_state);
|
||||
|
||||
gamepad.gamepad_state = gamepad_state;
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
switch(input_type) {
|
||||
case PACKET_TYPE_MOUSE_MOVE:
|
||||
passthrough(platf_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(platf_input, (PNV_SCROLL_PACKET)payload);
|
||||
}
|
||||
else {
|
||||
passthrough(input, (PNV_KEYBOARD_PACKET)payload);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
case PACKET_TYPE_MULTI_CONTROLLER:
|
||||
passthrough(input, (PNV_MULTI_CONTROLLER_PACKET)payload);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void passthrough(std::shared_ptr<input_t> &input, std::vector<std::uint8_t> &&input_data) {
|
||||
task_pool.push(passthrough_helper, input, util::cmove(input_data));
|
||||
}
|
||||
|
||||
void init() {
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
//
|
||||
// Created by loki on 6/20/19.
|
||||
//
|
||||
|
||||
#ifndef SUNSHINE_INPUT_H
|
||||
#define SUNSHINE_INPUT_H
|
||||
|
||||
|
||||
namespace input {
|
||||
|
||||
struct input_t;
|
||||
|
||||
void print(void *input);
|
||||
void passthrough(std::shared_ptr<input_t> &input, std::vector<std::uint8_t> &&input_data);
|
||||
|
||||
void init();
|
||||
|
||||
std::shared_ptr<input_t> alloc();
|
||||
}
|
||||
|
||||
#endif //SUNSHINE_INPUT_H
|
||||
@@ -1,140 +0,0 @@
|
||||
//
|
||||
// Created by loki on 5/30/19.
|
||||
//
|
||||
|
||||
#include "process.h"
|
||||
|
||||
#include <thread>
|
||||
#include <filesystem>
|
||||
#include <iostream>
|
||||
#include <csignal>
|
||||
|
||||
#include <boost/log/common.hpp>
|
||||
#include <boost/log/sinks.hpp>
|
||||
#include <boost/log/expressions.hpp>
|
||||
#include <boost/log/sources/severity_logger.hpp>
|
||||
|
||||
#include "input.h"
|
||||
#include "nvhttp.h"
|
||||
#include "rtsp.h"
|
||||
#include "config.h"
|
||||
#include "thread_pool.h"
|
||||
|
||||
#include "platform/common.h"
|
||||
extern "C" {
|
||||
#include <rs.h>
|
||||
}
|
||||
|
||||
using namespace std::literals;
|
||||
namespace bl = boost::log;
|
||||
|
||||
util::ThreadPool task_pool;
|
||||
bl::sources::severity_logger<int> verbose(0); // Dominating output
|
||||
bl::sources::severity_logger<int> debug(1); // Follow what is happening
|
||||
bl::sources::severity_logger<int> info(2); // Should be informed about
|
||||
bl::sources::severity_logger<int> warning(3); // Strange events
|
||||
bl::sources::severity_logger<int> error(4); // Recoverable errors
|
||||
bl::sources::severity_logger<int> fatal(5); // Unrecoverable errors
|
||||
|
||||
bool display_cursor;
|
||||
|
||||
using text_sink = bl::sinks::asynchronous_sink<bl::sinks::text_ostream_backend>;
|
||||
boost::shared_ptr<text_sink> sink;
|
||||
|
||||
struct NoDelete {
|
||||
void operator()(void *) {}
|
||||
};
|
||||
|
||||
BOOST_LOG_ATTRIBUTE_KEYWORD(severity, "Severity", int)
|
||||
|
||||
void log_flush() {
|
||||
sink->flush();
|
||||
}
|
||||
|
||||
std::map<int, std::function<void()>> signal_handlers;
|
||||
void on_signal_forwarder(int sig) {
|
||||
signal_handlers.at(sig)();
|
||||
}
|
||||
|
||||
template<class FN>
|
||||
void on_signal(int sig, FN &&fn) {
|
||||
signal_handlers.emplace(sig, std::forward<FN>(fn));
|
||||
|
||||
std::signal(sig, on_signal_forwarder);
|
||||
}
|
||||
|
||||
int main(int argc, char *argv[]) {
|
||||
if(config::parse(argc, argv)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
sink = boost::make_shared<text_sink>();
|
||||
|
||||
boost::shared_ptr<std::ostream> stream { &std::cout, NoDelete {} };
|
||||
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) {
|
||||
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;
|
||||
};
|
||||
|
||||
os << log_type << view.attribute_values()["Message"].extract<std::string>();
|
||||
});
|
||||
|
||||
bl::core::get()->add_sink(sink);
|
||||
auto fg = util::fail_guard(log_flush);
|
||||
|
||||
// Create signal handler after logging has been initialized
|
||||
auto shutdown_event = std::make_shared<safe::event_t<bool>>();
|
||||
on_signal(SIGINT, [shutdown_event]() {
|
||||
BOOST_LOG(info) << "Interrupt handler called"sv;
|
||||
shutdown_event->raise(true);
|
||||
});
|
||||
|
||||
auto proc_opt = proc::parse(config::stream.file_apps);
|
||||
if(!proc_opt) {
|
||||
return 7;
|
||||
}
|
||||
|
||||
{
|
||||
proc::ctx_t ctx;
|
||||
ctx.name = "Desktop"s;
|
||||
proc_opt->get_apps().emplace(std::begin(proc_opt->get_apps()), std::move(ctx));
|
||||
}
|
||||
|
||||
proc::proc = std::move(*proc_opt);
|
||||
|
||||
auto deinit_guard = platf::init();
|
||||
input::init();
|
||||
reed_solomon_init();
|
||||
|
||||
task_pool.start(1);
|
||||
|
||||
std::thread httpThread { nvhttp::start, shutdown_event };
|
||||
stream::rtpThread(shutdown_event);
|
||||
|
||||
httpThread.join();
|
||||
|
||||
return 0;
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
//
|
||||
// Created by loki on 12/22/19.
|
||||
//
|
||||
|
||||
#ifndef SUNSHINE_MAIN_H
|
||||
#define SUNSHINE_MAIN_H
|
||||
|
||||
#include <boost/log/common.hpp>
|
||||
#include "thread_pool.h"
|
||||
|
||||
extern util::ThreadPool task_pool;
|
||||
extern bool display_cursor;
|
||||
|
||||
extern boost::log::sources::severity_logger<int> verbose;
|
||||
extern boost::log::sources::severity_logger<int> debug;
|
||||
extern boost::log::sources::severity_logger<int> info;
|
||||
extern boost::log::sources::severity_logger<int> warning;
|
||||
extern boost::log::sources::severity_logger<int> error;
|
||||
extern boost::log::sources::severity_logger<int> fatal;
|
||||
|
||||
void log_flush();
|
||||
#endif //SUNSHINE_MAIN_H
|
||||
@@ -1,50 +0,0 @@
|
||||
#ifndef DOSSIER_MOVE_BY_COPY_H
|
||||
#define DOSSIER_MOVE_BY_COPY_H
|
||||
|
||||
#include <utility>
|
||||
namespace util {
|
||||
/*
|
||||
* When a copy is made, it moves the object
|
||||
* This allows you to move an object when a move can't be done.
|
||||
*/
|
||||
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)) { }
|
||||
|
||||
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);
|
||||
|
||||
return *this;
|
||||
}
|
||||
|
||||
operator move_type() {
|
||||
return std::move(_to_move);
|
||||
}
|
||||
};
|
||||
|
||||
template<class T>
|
||||
MoveByCopy<T> cmove(T &movable) {
|
||||
return MoveByCopy<T>(std::move(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)));
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -1,115 +0,0 @@
|
||||
//
|
||||
// Created by loki on 12/27/19.
|
||||
//
|
||||
|
||||
#include <algorithm>
|
||||
#include "network.h"
|
||||
#include "utility.h"
|
||||
|
||||
namespace net {
|
||||
using namespace std::literals;
|
||||
// In the format "xxx.xxx.xxx.xxx/x"
|
||||
std::pair<std::uint32_t, std::uint32_t> ip_block(const std::string_view &ip);
|
||||
|
||||
std::vector<std::pair<std::uint32_t, std::uint32_t>> pc_ips {
|
||||
ip_block("127.0.0.1/32"sv)
|
||||
};
|
||||
std::vector<std::tuple<std::uint32_t, std::uint32_t>> lan_ips {
|
||||
ip_block("192.168.0.0/16"sv),
|
||||
ip_block("172.16.0.0/12"),
|
||||
ip_block("10.0.0.0/8"sv)
|
||||
};
|
||||
|
||||
std::uint32_t ip(const std::string_view &ip_str) {
|
||||
auto begin = std::begin(ip_str);
|
||||
auto end = std::end(ip_str);
|
||||
auto temp_end = std::find(begin, end, '.');
|
||||
|
||||
std::uint32_t ip = 0;
|
||||
auto shift = 24;
|
||||
while(temp_end != end) {
|
||||
ip += (util::from_chars(begin, temp_end) << shift);
|
||||
shift -= 8;
|
||||
|
||||
begin = temp_end + 1;
|
||||
temp_end = std::find(begin, end, '.');
|
||||
}
|
||||
|
||||
ip += util::from_chars(begin, end);
|
||||
|
||||
return ip;
|
||||
}
|
||||
|
||||
// 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 addr = ip({ begin, (std::size_t)(end - begin) });
|
||||
|
||||
auto bits = 32 - util::from_chars(end + 1, std::end(ip_str));
|
||||
|
||||
return { addr, addr + ((1 << bits) - 1) };
|
||||
}
|
||||
|
||||
net_e from_enum_string(const std::string_view &view) {
|
||||
if(view == "wan") {
|
||||
return WAN;
|
||||
}
|
||||
if(view == "lan") {
|
||||
return LAN;
|
||||
}
|
||||
|
||||
return PC;
|
||||
}
|
||||
net_e from_address(const std::string_view &view) {
|
||||
auto addr = ip(view);
|
||||
|
||||
for(auto [ip_low, ip_high] : pc_ips) {
|
||||
if(addr >= ip_low && addr <= ip_high) {
|
||||
return PC;
|
||||
}
|
||||
}
|
||||
|
||||
for(auto [ip_low, ip_high] : lan_ips) {
|
||||
if(addr >= ip_low && addr <= ip_high) {
|
||||
return LAN;
|
||||
}
|
||||
}
|
||||
|
||||
return WAN;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
//
|
||||
// Created by loki on 12/27/19.
|
||||
//
|
||||
|
||||
#ifndef SUNSHINE_NETWORK_H
|
||||
#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,
|
||||
WAN
|
||||
};
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
#endif //SUNSHINE_NETWORK_H
|
||||
@@ -1,869 +0,0 @@
|
||||
//
|
||||
// Created by loki on 6/3/19.
|
||||
//
|
||||
|
||||
#include "process.h"
|
||||
|
||||
#include <filesystem>
|
||||
|
||||
#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>
|
||||
|
||||
#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 "utility.h"
|
||||
#include "rtsp.h"
|
||||
#include "crypto.h"
|
||||
#include "nvhttp.h"
|
||||
#include "platform/common.h"
|
||||
#include "network.h"
|
||||
#include "uuid.h"
|
||||
#include "main.h"
|
||||
|
||||
|
||||
namespace nvhttp {
|
||||
using namespace std::literals;
|
||||
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";
|
||||
|
||||
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>;
|
||||
|
||||
struct conf_intern_t {
|
||||
std::string servercert;
|
||||
std::string pkey;
|
||||
} conf_intern;
|
||||
|
||||
struct client_t {
|
||||
std::string uniqueID;
|
||||
std::vector<std::string> certs;
|
||||
};
|
||||
|
||||
struct pair_session_t {
|
||||
struct {
|
||||
std::string uniqueID;
|
||||
std::string cert;
|
||||
} client;
|
||||
|
||||
std::unique_ptr<crypto::aes_t> cipher_key;
|
||||
std::vector<uint8_t> clienthash;
|
||||
|
||||
std::string serversecret;
|
||||
std::string serverchallenge;
|
||||
|
||||
struct {
|
||||
util::Either<
|
||||
std::shared_ptr<typename SimpleWeb::ServerBase<SimpleWeb::HTTP>::Response>,
|
||||
std::shared_ptr<typename SimpleWeb::ServerBase<SimpleWeb::HTTPS>::Response>
|
||||
> response;
|
||||
std::string salt;
|
||||
} async_insert_pin;
|
||||
};
|
||||
|
||||
// uniqueID, session
|
||||
std::unordered_map<std::string, pair_session_t> map_id_sess;
|
||||
std::unordered_map<std::string, client_t> map_id_client;
|
||||
std::string unique_id;
|
||||
net::net_e origin_pin_allowed;
|
||||
|
||||
using args_t = SimpleWeb::CaseInsensitiveMultimap;
|
||||
using resp_https_t = std::shared_ptr<typename SimpleWeb::ServerBase<SimpleWeb::HTTPS>::Response>;
|
||||
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,
|
||||
REMOVE
|
||||
};
|
||||
|
||||
void save_state() {
|
||||
pt::ptree root;
|
||||
|
||||
root.put("root.uniqueid", unique_id);
|
||||
auto &nodes = root.add_child("root.devices", pt::ptree {});
|
||||
for(auto &[_,client] : map_id_client) {
|
||||
pt::ptree node;
|
||||
|
||||
node.put("uniqueid"s, client.uniqueID);
|
||||
|
||||
pt::ptree cert_nodes;
|
||||
for(auto &cert : client.certs) {
|
||||
pt::ptree cert_node;
|
||||
cert_node.put_value(cert);
|
||||
cert_nodes.push_back(std::make_pair(""s, cert_node));
|
||||
}
|
||||
node.add_child("certs"s, cert_nodes);
|
||||
|
||||
nodes.push_back(std::make_pair(""s, node));
|
||||
}
|
||||
|
||||
pt::write_json(config::nvhttp.file_state, root);
|
||||
}
|
||||
|
||||
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();
|
||||
return;
|
||||
}
|
||||
|
||||
pt::ptree root;
|
||||
try {
|
||||
pt::read_json(config::nvhttp.file_state, root);
|
||||
} catch (std::exception &e) {
|
||||
BOOST_LOG(warning) << e.what();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
unique_id = root.get<std::string>("root.uniqueid");
|
||||
auto device_nodes = root.get_child("root.devices");
|
||||
|
||||
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;
|
||||
|
||||
for(auto &[_, el] : device_node.get_child("certs")) {
|
||||
client.certs.emplace_back(el.get_value<std::string>());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
if(!config::sunshine.flags[config::flag::FRESH_STATE]) {
|
||||
save_state();
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
auto key = crypto::gen_aes_key(*salt, pin);
|
||||
sess.cipher_key = std::make_unique<crypto::aes_t>(key);
|
||||
|
||||
tree.put("root.paired", 1);
|
||||
tree.put("root.plaincert", util::hex_vec(conf_intern.servercert, true));
|
||||
tree.put("root.<xmlattr>.status_code", 200);
|
||||
}
|
||||
void serverchallengeresp(pair_session_t &sess, pt::ptree &tree, const args_t &args) {
|
||||
auto encrypted_response = util::from_hex_vec(args.at("serverchallengeresp"s), true);
|
||||
|
||||
std::vector<uint8_t> decrypted;
|
||||
crypto::cipher_t cipher(*sess.cipher_key);
|
||||
cipher.padding = false;
|
||||
|
||||
cipher.decrypt(encrypted_response, decrypted);
|
||||
|
||||
sess.clienthash = std::move(decrypted);
|
||||
|
||||
auto serversecret = sess.serversecret;
|
||||
auto sign = crypto::sign256(crypto::pkey(conf_intern.pkey), serversecret);
|
||||
|
||||
serversecret.insert(std::end(serversecret), std::begin(sign), std::end(sign));
|
||||
|
||||
tree.put("root.pairingsecret", util::hex_vec(serversecret, true));
|
||||
tree.put("root.paired", 1);
|
||||
tree.put("root.<xmlattr>.status_code", 200);
|
||||
}
|
||||
|
||||
void clientchallenge(pair_session_t &sess, pt::ptree &tree, const args_t &args) {
|
||||
auto challenge = util::from_hex_vec(args.at("clientchallenge"s), true);
|
||||
|
||||
crypto::cipher_t cipher(*sess.cipher_key);
|
||||
cipher.padding = false;
|
||||
|
||||
std::vector<uint8_t> decrypted;
|
||||
cipher.decrypt(challenge, decrypted);
|
||||
|
||||
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 serverchallenge = crypto::rand(16);
|
||||
|
||||
std::string plaintext;
|
||||
plaintext.reserve(hash.size() + serverchallenge.size());
|
||||
|
||||
plaintext.insert(std::end(plaintext), std::begin(hash), std::end(hash));
|
||||
plaintext.insert(std::end(plaintext), std::begin(serverchallenge), std::end(serverchallenge));
|
||||
|
||||
std::vector<uint8_t> encrypted;
|
||||
cipher.encrypt(plaintext, encrypted);
|
||||
|
||||
sess.serversecret = std::move(serversecret);
|
||||
sess.serverchallenge = std::move(serverchallenge);
|
||||
|
||||
tree.put("root.paired", 1);
|
||||
tree.put("root.challengeresponse", util::hex_vec(encrypted, true));
|
||||
tree.put("root.<xmlattr>.status_code", 200);
|
||||
}
|
||||
|
||||
void clientpairingsecret(std::shared_ptr<safe::queue_t<crypto::x509_t>> &add_cert, pair_session_t &sess, pt::ptree &tree, const args_t &args) {
|
||||
auto &client = sess.client;
|
||||
|
||||
auto pairingsecret = util::from_hex_vec(args.at("clientpairingsecret"), true);
|
||||
|
||||
std::string_view secret { pairingsecret.data(), 16 };
|
||||
std::string_view sign { pairingsecret.data() + secret.size(), crypto::digest_size };
|
||||
|
||||
assert((secret.size() + sign.size()) == pairingsecret.size());
|
||||
|
||||
auto x509 = crypto::x509(client.cert);
|
||||
auto x509_sign = crypto::signature(x509);
|
||||
|
||||
std::string data;
|
||||
data.reserve(sess.serverchallenge.size() + x509_sign.size() + secret.size());
|
||||
|
||||
data.insert(std::end(data), std::begin(sess.serverchallenge), std::end(sess.serverchallenge));
|
||||
data.insert(std::end(data), std::begin(x509_sign), std::end(x509_sign));
|
||||
data.insert(std::end(data), std::begin(secret), std::end(secret));
|
||||
|
||||
auto hash = crypto::hash(data);
|
||||
|
||||
// if hash not correct, probably MITM
|
||||
if(std::memcmp(hash.data(), sess.clienthash.data(), hash.size())) {
|
||||
//TODO: log
|
||||
|
||||
map_id_sess.erase(client.uniqueID);
|
||||
tree.put("root.paired", 0);
|
||||
}
|
||||
|
||||
if(crypto::verify256(crypto::x509(client.cert), secret, sign)) {
|
||||
tree.put("root.paired", 1);
|
||||
add_cert->raise(crypto::x509(client.cert));
|
||||
|
||||
auto it = map_id_sess.find(client.uniqueID);
|
||||
|
||||
update_id_client(client.uniqueID, std::move(client.cert), op_e::ADD);
|
||||
map_id_sess.erase(it);
|
||||
}
|
||||
else {
|
||||
map_id_sess.erase(client.uniqueID);
|
||||
tree.put("root.paired", 0);
|
||||
}
|
||||
|
||||
tree.put("root.<xmlattr>.status_code", 200);
|
||||
}
|
||||
|
||||
template<class T>
|
||||
struct tunnel;
|
||||
|
||||
template<>
|
||||
struct tunnel<SimpleWeb::HTTPS> {
|
||||
static auto constexpr to_string = "HTTPS"sv;
|
||||
};
|
||||
|
||||
template<>
|
||||
struct tunnel<SimpleWeb::HTTP> {
|
||||
static auto constexpr to_string = "NONE"sv;
|
||||
};
|
||||
|
||||
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;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
template<class T>
|
||||
void not_found(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;
|
||||
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();
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
auto args = request->parse_query_string();
|
||||
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);
|
||||
|
||||
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));
|
||||
|
||||
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);
|
||||
tree.put("root.<xmlattr>.status_code", 200);
|
||||
}
|
||||
}
|
||||
else if(it = args.find("clientchallenge"); it != std::end(args)) {
|
||||
clientchallenge(sess_it->second, tree, args);
|
||||
}
|
||||
else if(it = args.find("serverchallengeresp"); it != std::end(args)) {
|
||||
serverchallengeresp(sess_it->second, tree, args);
|
||||
}
|
||||
else if(it = args.find("clientpairingsecret"); it != std::end(args)) {
|
||||
clientpairingsecret(add_cert, sess_it->second, tree, args);
|
||||
}
|
||||
else {
|
||||
tree.put("root.<xmlattr>.status_code", 404);
|
||||
}
|
||||
|
||||
std::ostringstream data;
|
||||
|
||||
pt::write_xml(data, tree);
|
||||
response->write(data.str());
|
||||
}
|
||||
|
||||
template<class T>
|
||||
void pin(std::shared_ptr<typename SimpleWeb::ServerBase<T>::Response> response, std::shared_ptr<typename SimpleWeb::ServerBase<T>::Request> request) {
|
||||
print_req<T>(request);
|
||||
|
||||
auto address = request->remote_endpoint_address();
|
||||
auto ip_type = net::from_address(address);
|
||||
if(ip_type > origin_pin_allowed) {
|
||||
BOOST_LOG(info) << '[' << address << "] -- denied"sv;
|
||||
|
||||
response->write(SimpleWeb::StatusCode::client_error_forbidden);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
pt::ptree tree;
|
||||
|
||||
if(map_id_sess.empty()) {
|
||||
response->write(SimpleWeb::StatusCode::client_error_im_a_teapot);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
auto &sess = std::begin(map_id_sess)->second;
|
||||
getservercert(sess, tree, request->path_match[1]);
|
||||
|
||||
// response to the request for pin
|
||||
std::ostringstream data;
|
||||
pt::write_xml(data, tree);
|
||||
|
||||
auto &async_response = sess.async_insert_pin.response;
|
||||
if(async_response.has_left() && async_response.left()) {
|
||||
async_response.left()->write(data.str());
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
// reset async_response
|
||||
async_response = std::decay_t<decltype(async_response.left())>();
|
||||
// response to the current request
|
||||
response->write(SimpleWeb::StatusCode::success_ok);
|
||||
}
|
||||
|
||||
template<class T>
|
||||
void serverinfo(std::shared_ptr<typename SimpleWeb::ServerBase<T>::Response> response, std::shared_ptr<typename SimpleWeb::ServerBase<T>::Request> request) {
|
||||
print_req<T>(request);
|
||||
|
||||
int pair_status = 0;
|
||||
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)) {
|
||||
pair_status = 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pt::ptree tree;
|
||||
|
||||
tree.put("root.<xmlattr>.status_code", 200);
|
||||
tree.put("root.hostname", config::nvhttp.sunshine_name);
|
||||
|
||||
tree.put("root.appversion", VERSION);
|
||||
tree.put("root.GfeVersion", GFE_VERSION);
|
||||
tree.put("root.uniqueid", 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.LocalIP", request->local_endpoint_address());
|
||||
|
||||
if(config::video.hevc_mode == 2) {
|
||||
tree.put("root.ServerCodecModeSupport", "3843");
|
||||
}
|
||||
else if(config::video.hevc_mode == 1) {
|
||||
tree.put("root.ServerCodecModeSupport", "259");
|
||||
}
|
||||
else {
|
||||
tree.put("root.ServerCodecModeSupport", "3");
|
||||
}
|
||||
|
||||
if(!config::nvhttp.external_ip.empty()) {
|
||||
tree.put("root.ExternalIP", config::nvhttp.external_ip);
|
||||
}
|
||||
|
||||
auto current_appid = proc::proc.running();
|
||||
tree.put("root.PairStatus", pair_status);
|
||||
tree.put("root.currentgame", current_appid >= 0 ? current_appid + 1 : 0);
|
||||
tree.put("root.state", "_SERVER_BUSY");
|
||||
|
||||
std::ostringstream data;
|
||||
|
||||
pt::write_xml(data, tree);
|
||||
response->write(data.str());
|
||||
}
|
||||
|
||||
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([&]() {
|
||||
std::ostringstream data;
|
||||
|
||||
pt::write_xml(data, tree);
|
||||
response->write(data.str());
|
||||
});
|
||||
|
||||
auto client = map_id_client.find(clientID);
|
||||
if(client == std::end(map_id_client)) {
|
||||
tree.put("root.<xmlattr>.status_code", 501);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
auto &apps = tree.add_child("root", pt::ptree {});
|
||||
|
||||
apps.put("<xmlattr>.status_code", 200);
|
||||
|
||||
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("AppTitle"s, proc.name);
|
||||
app.put("ID"s, ++x);
|
||||
|
||||
apps.push_back(std::make_pair("App", std::move(app)));
|
||||
}
|
||||
}
|
||||
|
||||
void launch(resp_https_t response, req_https_t request) {
|
||||
print_req<SimpleWeb::HTTPS>(request);
|
||||
|
||||
pt::ptree tree;
|
||||
auto g = util::fail_guard([&]() {
|
||||
std::ostringstream data;
|
||||
|
||||
pt::write_xml(data, tree);
|
||||
response->write(data.str());
|
||||
});
|
||||
|
||||
BOOST_LOG(fatal) << stream::session_count();
|
||||
|
||||
if(stream::session_count() == config::stream.channels) {
|
||||
tree.put("root.resume", 0);
|
||||
tree.put("root.<xmlattr>.status_code", 503);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
auto args = request->parse_query_string();
|
||||
auto appid = util::from_view(args.at("appid")) -1;
|
||||
|
||||
auto current_appid = proc::proc.running();
|
||||
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);
|
||||
tree.put("root.gamesession", 0);
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
stream::launch_session_t launch_session;
|
||||
|
||||
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_session_raise(launch_session);
|
||||
|
||||
tree.put("root.<xmlattr>.status_code", 200);
|
||||
tree.put("root.gamesession", 1);
|
||||
}
|
||||
|
||||
void resume(resp_https_t response, req_https_t request) {
|
||||
print_req<SimpleWeb::HTTPS>(request);
|
||||
|
||||
pt::ptree tree;
|
||||
auto g = util::fail_guard([&]() {
|
||||
std::ostringstream data;
|
||||
|
||||
pt::write_xml(data, tree);
|
||||
response->write(data.str());
|
||||
});
|
||||
|
||||
// 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) {
|
||||
BOOST_LOG(fatal) << stream::session_count();
|
||||
tree.put("root.resume", 0);
|
||||
tree.put("root.<xmlattr>.status_code", 503);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
auto current_appid = proc::proc.running();
|
||||
if(current_appid == -1) {
|
||||
tree.put("root.resume", 0);
|
||||
tree.put("root.<xmlattr>.status_code", 503);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
stream::launch_session_t launch_session;
|
||||
|
||||
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;
|
||||
|
||||
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_session_raise(launch_session);
|
||||
|
||||
tree.put("root.<xmlattr>.status_code", 200);
|
||||
tree.put("root.resume", 1);
|
||||
}
|
||||
|
||||
void cancel(resp_https_t response, req_https_t request) {
|
||||
print_req<SimpleWeb::HTTPS>(request);
|
||||
|
||||
pt::ptree tree;
|
||||
auto g = util::fail_guard([&]() {
|
||||
std::ostringstream data;
|
||||
|
||||
pt::write_xml(data, tree);
|
||||
response->write(data.str());
|
||||
});
|
||||
|
||||
// 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);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
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) {
|
||||
print_req<SimpleWeb::HTTPS>(request);
|
||||
|
||||
std::ifstream in(SUNSHINE_ASSETS_DIR "/box.png");
|
||||
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;
|
||||
|
||||
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(fatal) << "Couldn't create directory ["sv << pkey_dir << "] :"sv << err_code.message();
|
||||
return -1;
|
||||
}
|
||||
|
||||
fs::create_directories(cert_dir, err_code);
|
||||
if (err_code) {
|
||||
BOOST_LOG(fatal) << "Couldn't create directory ["sv << cert_dir << "] :"sv << err_code.message();
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (write_file(pkey.c_str(), creds.pkey)) {
|
||||
BOOST_LOG(fatal) << "Couldn't open ["sv << config::nvhttp.pkey << ']';
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (write_file(cert.c_str(), creds.x509)) {
|
||||
BOOST_LOG(fatal) << "Couldn't open ["sv << config::nvhttp.cert << ']';
|
||||
return -1;
|
||||
}
|
||||
|
||||
fs::permissions(pkey_path,
|
||||
fs::perms::owner_read | fs::perms::owner_write,
|
||||
fs::perm_options::replace, err_code);
|
||||
|
||||
if (err_code) {
|
||||
BOOST_LOG(fatal) << "Couldn't change permissions of ["sv << config::nvhttp.pkey << "] :"sv << err_code.message();
|
||||
return -1;
|
||||
}
|
||||
|
||||
fs::permissions(cert_path,
|
||||
fs::perms::owner_read | fs::perms::group_read | fs::perms::others_read | fs::perms::owner_write,
|
||||
fs::perm_options::replace, err_code);
|
||||
|
||||
if (err_code) {
|
||||
BOOST_LOG(fatal) << "Couldn't change permissions of ["sv << config::nvhttp.cert << "] :"sv << err_code.message();
|
||||
return -1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
void start(std::shared_ptr<safe::signal_t> shutdown_event) {
|
||||
bool clean_slate = config::sunshine.flags[config::flag::FRESH_STATE];
|
||||
if(clean_slate) {
|
||||
unique_id = util::uuid_t::generate().string();
|
||||
|
||||
auto dir = std::filesystem::temp_directory_path() / "Sushine"sv;
|
||||
|
||||
config::nvhttp.cert = (dir / ("cert-"s + unique_id)).string();
|
||||
config::nvhttp.pkey = (dir / ("pkey-"s + unique_id)).string();
|
||||
}
|
||||
|
||||
|
||||
if(!fs::exists(config::nvhttp.pkey) || !fs::exists(config::nvhttp.cert)) {
|
||||
if(create_creds(config::nvhttp.pkey, config::nvhttp.cert)) {
|
||||
shutdown_event->raise(true);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
origin_pin_allowed = net::from_enum_string(config::nvhttp.origin_pin_allowed);
|
||||
|
||||
if(!clean_slate) {
|
||||
load_state();
|
||||
}
|
||||
|
||||
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);
|
||||
ctx->use_certificate_chain_file(config::nvhttp.cert);
|
||||
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 &cert : client.certs) {
|
||||
cert_chain.add(crypto::x509(cert));
|
||||
}
|
||||
}
|
||||
|
||||
auto add_cert = std::make_shared<safe::queue_t<crypto::x509_t>>();
|
||||
|
||||
// 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) {
|
||||
auto fg = util::fail_guard([&]() {
|
||||
char subject_name[256];
|
||||
|
||||
auto x509 = ctx.native_handle();
|
||||
X509_NAME_oneline(X509_get_subject_name(X509_STORE_CTX_get_current_cert(x509)), subject_name, sizeof(subject_name));
|
||||
|
||||
|
||||
BOOST_LOG(info) << subject_name << " -- "sv << (verified ? "verfied"sv : "denied"sv);
|
||||
});
|
||||
|
||||
if(verified) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
while(add_cert->peek()) {
|
||||
char subject_name[256];
|
||||
|
||||
auto cert = add_cert->pop();
|
||||
X509_NAME_oneline(X509_get_subject_name(cert.get()), subject_name, sizeof(subject_name));
|
||||
|
||||
BOOST_LOG(debug) << "Added cert ["sv << subject_name << ']';
|
||||
cert_chain.add(std::move(cert));
|
||||
}
|
||||
|
||||
auto err_str = cert_chain.verify(X509_STORE_CTX_get_current_cert(ctx.native_handle()));
|
||||
if(err_str) {
|
||||
BOOST_LOG(warning) << "SSL Verification error :: "sv << err_str;
|
||||
return 0;
|
||||
}
|
||||
|
||||
verified = 1;
|
||||
return 1;
|
||||
});
|
||||
|
||||
|
||||
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.resource["^/pin/([0-9]+)$"]["GET"] = pin<SimpleWeb::HTTPS>;
|
||||
https_server.resource["^/resume$"]["GET"] = resume;
|
||||
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;
|
||||
|
||||
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;
|
||||
|
||||
std::thread ssl { &https_server_t::start, &https_server };
|
||||
std::thread tcp { &http_server_t::start, &http_server };
|
||||
|
||||
// Wait for any event
|
||||
shutdown_event->view();
|
||||
|
||||
https_server.stop();
|
||||
http_server.stop();
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
//
|
||||
// Created by loki on 6/3/19.
|
||||
//
|
||||
|
||||
#ifndef SUNSHINE_NVHTTP_H
|
||||
#define SUNSHINE_NVHTTP_H
|
||||
|
||||
#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::signal_t> shutdown_event);
|
||||
}
|
||||
|
||||
#endif //SUNSHINE_NVHTTP_H
|
||||
@@ -1,109 +0,0 @@
|
||||
//
|
||||
// Created by loki on 6/21/19.
|
||||
//
|
||||
|
||||
#ifndef SUNSHINE_COMMON_H
|
||||
#define SUNSHINE_COMMON_H
|
||||
|
||||
#include <string>
|
||||
#include "sunshine/utility.h"
|
||||
|
||||
struct sockaddr;
|
||||
namespace platf {
|
||||
constexpr auto MAX_GAMEPADS = 32;
|
||||
|
||||
constexpr std::uint16_t DPAD_UP = 0x0001;
|
||||
constexpr std::uint16_t DPAD_DOWN = 0x0002;
|
||||
constexpr std::uint16_t DPAD_LEFT = 0x0004;
|
||||
constexpr std::uint16_t DPAD_RIGHT = 0x0008;
|
||||
constexpr std::uint16_t START = 0x0010;
|
||||
constexpr std::uint16_t BACK = 0x0020;
|
||||
constexpr std::uint16_t LEFT_STICK = 0x0040;
|
||||
constexpr std::uint16_t RIGHT_STICK = 0x0080;
|
||||
constexpr std::uint16_t LEFT_BUTTON = 0x0100;
|
||||
constexpr std::uint16_t RIGHT_BUTTON = 0x0200;
|
||||
constexpr std::uint16_t HOME = 0x0400;
|
||||
constexpr std::uint16_t A = 0x1000;
|
||||
constexpr std::uint16_t B = 0x2000;
|
||||
constexpr std::uint16_t X = 0x4000;
|
||||
constexpr std::uint16_t Y = 0x8000;
|
||||
|
||||
struct gamepad_state_t {
|
||||
std::uint16_t buttonFlags;
|
||||
std::uint8_t lt;
|
||||
std::uint8_t rt;
|
||||
std::int16_t lsX;
|
||||
std::int16_t lsY;
|
||||
std::int16_t rsX;
|
||||
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::int32_t height {};
|
||||
std::int32_t pixel_pitch {};
|
||||
std::int32_t row_pitch {};
|
||||
|
||||
img_t() = default;
|
||||
img_t(const img_t&) = delete;
|
||||
img_t(img_t&&) = delete;
|
||||
|
||||
virtual ~img_t() = default;
|
||||
};
|
||||
|
||||
enum class capture_e : int {
|
||||
ok,
|
||||
reinit,
|
||||
timeout,
|
||||
error
|
||||
};
|
||||
|
||||
class display_t {
|
||||
public:
|
||||
virtual capture_e snapshot(img_t *img, bool cursor) = 0;
|
||||
virtual std::unique_ptr<img_t> alloc_img() = 0;
|
||||
|
||||
virtual ~display_t() = default;
|
||||
};
|
||||
|
||||
class mic_t {
|
||||
public:
|
||||
virtual capture_e sample(std::vector<std::int16_t> &frame_buffer) = 0;
|
||||
|
||||
virtual ~mic_t() = default;
|
||||
};
|
||||
|
||||
|
||||
void freeInput(void*);
|
||||
|
||||
using input_t = util::safe_ptr<void, freeInput>;
|
||||
|
||||
std::string get_mac_address(const std::string_view &address);
|
||||
|
||||
std::string from_sockaddr(const sockaddr *const);
|
||||
std::pair<std::uint16_t, std::string> from_sockaddr_ex(const sockaddr *const);
|
||||
|
||||
std::unique_ptr<mic_t> microphone(std::uint32_t sample_rate);
|
||||
std::unique_ptr<display_t> display();
|
||||
|
||||
input_t input();
|
||||
void move_mouse(input_t &input, int deltaX, int deltaY);
|
||||
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();
|
||||
}
|
||||
|
||||
#endif //SUNSHINE_COMMON_H
|
||||
@@ -1,429 +0,0 @@
|
||||
//
|
||||
// Created by loki on 6/21/19.
|
||||
//
|
||||
|
||||
#include "common.h"
|
||||
|
||||
#include <fstream>
|
||||
#include <bitset>
|
||||
|
||||
#include <arpa/inet.h>
|
||||
#include <ifaddrs.h>
|
||||
|
||||
#include <X11/X.h>
|
||||
#include <X11/Xlib.h>
|
||||
#include <X11/Xutil.h>
|
||||
#include <X11/extensions/Xfixes.h>
|
||||
#include <xcb/shm.h>
|
||||
#include <xcb/xfixes.h>
|
||||
#include <sys/ipc.h>
|
||||
#include <sys/shm.h>
|
||||
|
||||
#include <pulse/simple.h>
|
||||
#include <pulse/error.h>
|
||||
|
||||
#include "sunshine/task_pool.h"
|
||||
#include "sunshine/config.h"
|
||||
#include "sunshine/main.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(pa_sample_format format, std::uint32_t sample_rate, std::uint8_t channels) : ss { format, sample_rate, channels }, 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::unique_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) {
|
||||
auto mic = std::make_unique<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::pair<std::uint16_t, std::string> from_sockaddr_ex(const sockaddr *const ip_addr) {
|
||||
char data[INET6_ADDRSTRLEN];
|
||||
|
||||
auto family = ip_addr->sa_family;
|
||||
std::uint16_t port;
|
||||
if(family == AF_INET6) {
|
||||
inet_ntop(AF_INET6, &((sockaddr_in6*)ip_addr)->sin6_addr, data, INET6_ADDRSTRLEN);
|
||||
port = ((sockaddr_in6*)ip_addr)->sin6_port;
|
||||
}
|
||||
|
||||
if(family == AF_INET) {
|
||||
inet_ntop(AF_INET, &((sockaddr_in*)ip_addr)->sin_addr, data, INET_ADDRSTRLEN);
|
||||
port = ((sockaddr_in*)ip_addr)->sin_port;
|
||||
}
|
||||
|
||||
return { port, std::string { data } };
|
||||
}
|
||||
|
||||
std::string get_mac_address(const std::string_view &address) {
|
||||
auto ifaddrs = get_ifaddrs();
|
||||
for(auto pos = ifaddrs.get(); pos != nullptr; pos = pos->ifa_next) {
|
||||
if(pos->ifa_addr && address == from_sockaddr(pos->ifa_addr)) {
|
||||
std::ifstream mac_file("/sys/class/net/"s + pos->ifa_name + "/address");
|
||||
if(mac_file.good()) {
|
||||
std::string mac_address;
|
||||
std::getline(mac_file, mac_address);
|
||||
return mac_address;
|
||||
}
|
||||
}
|
||||
}
|
||||
BOOST_LOG(warning) << "Unable to find MAC address for "sv << address;
|
||||
return "00:00:00:00:00:00"s;
|
||||
}
|
||||
|
||||
void freeImage(XImage *p) {
|
||||
XDestroyImage(p);
|
||||
}
|
||||
void freeX(XFixesCursorImage *p) {
|
||||
XFree(p);
|
||||
}
|
||||
}
|
||||
@@ -1,506 +0,0 @@
|
||||
#include <libevdev/libevdev.h>
|
||||
#include <libevdev/libevdev-uinput.h>
|
||||
|
||||
#include <X11/X.h>
|
||||
#include <X11/Xlib.h>
|
||||
#include <X11/Xutil.h>
|
||||
#include <X11/extensions/XTest.h>
|
||||
|
||||
#include <cstring>
|
||||
#include <filesystem>
|
||||
|
||||
#include "common.h"
|
||||
#include "sunshine/main.h"
|
||||
#include "sunshine/utility.h"
|
||||
|
||||
// Support older versions
|
||||
#ifndef REL_HWHEEL_HI_RES
|
||||
#define REL_HWHEEL_HI_RES 0x0c
|
||||
#endif
|
||||
|
||||
#ifndef REL_WHEEL_HI_RES
|
||||
#define REL_WHEEL_HI_RES 0x0b
|
||||
#endif
|
||||
|
||||
namespace platf {
|
||||
using namespace std::literals;
|
||||
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>;
|
||||
|
||||
struct input_raw_t {
|
||||
public:
|
||||
void clear_mouse() {
|
||||
std::filesystem::path mouse_path { "sunshine_mouse"sv };
|
||||
|
||||
if(std::filesystem::is_symlink(mouse_path)) {
|
||||
std::filesystem::remove(mouse_path);
|
||||
}
|
||||
|
||||
mouse_input.reset();
|
||||
}
|
||||
|
||||
void clear_gamepad(int nr) {
|
||||
std::stringstream ss;
|
||||
|
||||
ss << "sunshine_gamepad_"sv << nr;
|
||||
|
||||
std::filesystem::path gamepad_path { ss.str() };
|
||||
if(std::filesystem::is_symlink(gamepad_path)) {
|
||||
std::filesystem::remove(gamepad_path);
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
if(err) {
|
||||
BOOST_LOG(error) << "Could not create Sunshine Mouse: "sv << strerror(-err);
|
||||
return -1;
|
||||
}
|
||||
|
||||
std::filesystem::create_symlink(libevdev_uinput_get_devnode(mouse_input.get()), "sunshine_mouse"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);
|
||||
|
||||
input.reset(buf);
|
||||
gamepad_state = gamepad_state_t {};
|
||||
|
||||
if(err) {
|
||||
BOOST_LOG(error) << "Could not create Sunshine Gamepad: "sv << strerror(-err);
|
||||
return -1;
|
||||
}
|
||||
|
||||
std::stringstream ss;
|
||||
ss << "sunshine_gamepad_"sv << nr;
|
||||
std::filesystem::path gamepad_path { ss.str() };
|
||||
|
||||
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_mouse();
|
||||
for(int x = 0; x < gamepads.size(); ++x) {
|
||||
clear_gamepad(x);
|
||||
}
|
||||
}
|
||||
|
||||
~input_raw_t() {
|
||||
clear();
|
||||
}
|
||||
|
||||
evdev_t gamepad_dev;
|
||||
|
||||
std::vector<std::pair<uinput_t, gamepad_state_t>> gamepads;
|
||||
|
||||
evdev_t mouse_dev;
|
||||
uinput_t mouse_input;
|
||||
|
||||
keyboard_t keyboard;
|
||||
};
|
||||
|
||||
void move_mouse(input_t &input, int deltaX, int deltaY) {
|
||||
auto mouse = ((input_raw_t*)input.get())->mouse_input.get();
|
||||
|
||||
if(deltaX) {
|
||||
libevdev_uinput_write_event(mouse, EV_REL, REL_X, deltaX);
|
||||
}
|
||||
|
||||
if(deltaY) {
|
||||
libevdev_uinput_write_event(mouse, EV_REL, REL_Y, deltaY);
|
||||
}
|
||||
|
||||
libevdev_uinput_write_event(mouse, EV_SYN, SYN_REPORT, 0);
|
||||
}
|
||||
|
||||
void button_mouse(input_t &input, int button, bool release) {
|
||||
int btn_type;
|
||||
int scan;
|
||||
|
||||
if(button == 1) {
|
||||
btn_type = BTN_LEFT;
|
||||
scan = 90001;
|
||||
}
|
||||
else if(button == 2) {
|
||||
btn_type = BTN_MIDDLE;
|
||||
scan = 90003;
|
||||
}
|
||||
else if(button == 3) {
|
||||
btn_type = BTN_RIGHT;
|
||||
scan = 90002;
|
||||
}
|
||||
else if(button == 4) {
|
||||
btn_type = BTN_SIDE;
|
||||
scan = 90004;
|
||||
}
|
||||
else {
|
||||
btn_type = BTN_EXTRA;
|
||||
scan = 90005;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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();
|
||||
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);
|
||||
}
|
||||
|
||||
uint16_t keysym(uint16_t modcode) {
|
||||
constexpr auto VK_NUMPAD = 0x60;
|
||||
constexpr auto VK_F1 = 0x70;
|
||||
|
||||
if(modcode >= VK_NUMPAD && modcode < VK_NUMPAD + 10) {
|
||||
return XK_KP_0 + (modcode - VK_NUMPAD);
|
||||
}
|
||||
|
||||
if(modcode >= VK_F1 && modcode < VK_F1 + 13) {
|
||||
return XK_F1 + (modcode - VK_F1);
|
||||
}
|
||||
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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));
|
||||
|
||||
if(!kc) {
|
||||
return;
|
||||
}
|
||||
|
||||
XTestFakeKeyEvent(keyboard.get(), kc, !release, 0);
|
||||
|
||||
XSync(keyboard.get(), 0);
|
||||
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]);
|
||||
|
||||
|
||||
auto bf = gamepad_state.buttonFlags ^ gamepad_state_old.buttonFlags;
|
||||
auto bf_new = gamepad_state.buttonFlags;
|
||||
|
||||
if(bf) {
|
||||
// up pressed == -1, down pressed == 1, else 0
|
||||
if((DPAD_UP | DPAD_DOWN) & bf) {
|
||||
int button_state = bf_new & DPAD_UP ? -1 : (bf_new & DPAD_DOWN ? 1 : 0);
|
||||
|
||||
libevdev_uinput_write_event(uinput.get(), EV_ABS, ABS_HAT0Y, button_state);
|
||||
}
|
||||
|
||||
if((DPAD_LEFT | DPAD_RIGHT) & bf) {
|
||||
int button_state = bf_new & DPAD_LEFT ? -1 : (bf_new & DPAD_RIGHT ? 1 : 0);
|
||||
|
||||
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(gamepad_state_old.lt != gamepad_state.lt) {
|
||||
libevdev_uinput_write_event(uinput.get(), EV_ABS, ABS_Z, gamepad_state.lt);
|
||||
}
|
||||
|
||||
if(gamepad_state_old.rt != gamepad_state.rt) {
|
||||
libevdev_uinput_write_event(uinput.get(), EV_ABS, ABS_RZ, gamepad_state.rt);
|
||||
}
|
||||
|
||||
if(gamepad_state_old.lsX != gamepad_state.lsX) {
|
||||
libevdev_uinput_write_event(uinput.get(), EV_ABS, ABS_X, gamepad_state.lsX);
|
||||
}
|
||||
|
||||
if(gamepad_state_old.lsY != gamepad_state.lsY) {
|
||||
libevdev_uinput_write_event(uinput.get(), EV_ABS, ABS_Y, -gamepad_state.lsY);
|
||||
}
|
||||
|
||||
if(gamepad_state_old.rsX != gamepad_state.rsX) {
|
||||
libevdev_uinput_write_event(uinput.get(), EV_ABS, ABS_RX, gamepad_state.rsX);
|
||||
}
|
||||
|
||||
if(gamepad_state_old.rsY != gamepad_state.rsY) {
|
||||
libevdev_uinput_write_event(uinput.get(), EV_ABS, ABS_RY, -gamepad_state.rsY);
|
||||
}
|
||||
|
||||
gamepad_state_old = gamepad_state;
|
||||
libevdev_uinput_write_event(uinput.get(), EV_SYN, SYN_REPORT, 0);
|
||||
}
|
||||
|
||||
evdev_t mouse() {
|
||||
evdev_t dev { libevdev_new() };
|
||||
|
||||
libevdev_set_uniq(dev.get(), "Sunshine Mouse");
|
||||
libevdev_set_id_product(dev.get(), 0x4038);
|
||||
libevdev_set_id_vendor(dev.get(), 0x46D);
|
||||
libevdev_set_id_bustype(dev.get(), 0x3);
|
||||
libevdev_set_id_version(dev.get(), 0x111);
|
||||
libevdev_set_name(dev.get(), "Logitech Wireless Mouse PID:4038");
|
||||
|
||||
libevdev_enable_event_type(dev.get(), EV_KEY);
|
||||
libevdev_enable_event_code(dev.get(), EV_KEY, BTN_LEFT, nullptr);
|
||||
libevdev_enable_event_code(dev.get(), EV_KEY, BTN_RIGHT, nullptr);
|
||||
libevdev_enable_event_code(dev.get(), EV_KEY, BTN_MIDDLE, nullptr);
|
||||
libevdev_enable_event_code(dev.get(), EV_KEY, BTN_SIDE, nullptr);
|
||||
libevdev_enable_event_code(dev.get(), EV_KEY, BTN_EXTRA, nullptr);
|
||||
libevdev_enable_event_code(dev.get(), EV_KEY, BTN_FORWARD, nullptr);
|
||||
libevdev_enable_event_code(dev.get(), EV_KEY, BTN_BACK, nullptr);
|
||||
libevdev_enable_event_code(dev.get(), EV_KEY, BTN_TASK, nullptr);
|
||||
libevdev_enable_event_code(dev.get(), EV_KEY, 280, nullptr);
|
||||
libevdev_enable_event_code(dev.get(), EV_KEY, 281, nullptr);
|
||||
libevdev_enable_event_code(dev.get(), EV_KEY, 282, nullptr);
|
||||
libevdev_enable_event_code(dev.get(), EV_KEY, 283, nullptr);
|
||||
libevdev_enable_event_code(dev.get(), EV_KEY, 284, nullptr);
|
||||
libevdev_enable_event_code(dev.get(), EV_KEY, 285, nullptr);
|
||||
libevdev_enable_event_code(dev.get(), EV_KEY, 286, nullptr);
|
||||
libevdev_enable_event_code(dev.get(), EV_KEY, 287, nullptr);
|
||||
|
||||
libevdev_enable_event_type(dev.get(), EV_REL);
|
||||
libevdev_enable_event_code(dev.get(), EV_REL, REL_X, nullptr);
|
||||
libevdev_enable_event_code(dev.get(), EV_REL, REL_Y, nullptr);
|
||||
libevdev_enable_event_code(dev.get(), EV_REL, REL_WHEEL, nullptr);
|
||||
libevdev_enable_event_code(dev.get(), EV_REL, REL_WHEEL_HI_RES, nullptr);
|
||||
libevdev_enable_event_code(dev.get(), EV_REL, REL_HWHEEL, nullptr);
|
||||
libevdev_enable_event_code(dev.get(), EV_REL, REL_HWHEEL_HI_RES, nullptr);
|
||||
|
||||
libevdev_enable_event_type(dev.get(), EV_MSC);
|
||||
libevdev_enable_event_code(dev.get(), EV_MSC, MSC_SCAN, nullptr);
|
||||
|
||||
return dev;
|
||||
}
|
||||
|
||||
evdev_t x360() {
|
||||
evdev_t dev { libevdev_new() };
|
||||
|
||||
input_absinfo stick {
|
||||
0,
|
||||
-32768, 32767,
|
||||
16,
|
||||
128,
|
||||
0
|
||||
};
|
||||
|
||||
input_absinfo trigger {
|
||||
0,
|
||||
0, 255,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
};
|
||||
|
||||
input_absinfo dpad {
|
||||
0,
|
||||
-1, 1,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
};
|
||||
|
||||
libevdev_set_uniq(dev.get(), "Sunshine Gamepad");
|
||||
libevdev_set_id_product(dev.get(), 0x28E);
|
||||
libevdev_set_id_vendor(dev.get(), 0x45E);
|
||||
libevdev_set_id_bustype(dev.get(), 0x3);
|
||||
libevdev_set_id_version(dev.get(), 0x110);
|
||||
libevdev_set_name(dev.get(), "Microsoft X-Box 360 pad");
|
||||
|
||||
libevdev_enable_event_type(dev.get(), EV_KEY);
|
||||
libevdev_enable_event_code(dev.get(), EV_KEY, BTN_WEST, nullptr);
|
||||
libevdev_enable_event_code(dev.get(), EV_KEY, BTN_EAST, nullptr);
|
||||
libevdev_enable_event_code(dev.get(), EV_KEY, BTN_NORTH, nullptr);
|
||||
libevdev_enable_event_code(dev.get(), EV_KEY, BTN_SOUTH, nullptr);
|
||||
libevdev_enable_event_code(dev.get(), EV_KEY, BTN_THUMBL, nullptr);
|
||||
libevdev_enable_event_code(dev.get(), EV_KEY, BTN_THUMBR, nullptr);
|
||||
libevdev_enable_event_code(dev.get(), EV_KEY, BTN_TR, nullptr);
|
||||
libevdev_enable_event_code(dev.get(), EV_KEY, BTN_TL, nullptr);
|
||||
libevdev_enable_event_code(dev.get(), EV_KEY, BTN_SELECT, nullptr);
|
||||
libevdev_enable_event_code(dev.get(), EV_KEY, BTN_MODE, nullptr);
|
||||
libevdev_enable_event_code(dev.get(), EV_KEY, BTN_START, nullptr);
|
||||
|
||||
libevdev_enable_event_type(dev.get(), EV_ABS);
|
||||
libevdev_enable_event_code(dev.get(), EV_ABS, ABS_HAT0Y, &dpad);
|
||||
libevdev_enable_event_code(dev.get(), EV_ABS, ABS_HAT0X, &dpad);
|
||||
libevdev_enable_event_code(dev.get(), EV_ABS, ABS_Z, &trigger);
|
||||
libevdev_enable_event_code(dev.get(), EV_ABS, ABS_RZ, &trigger);
|
||||
libevdev_enable_event_code(dev.get(), EV_ABS, ABS_X, &stick);
|
||||
libevdev_enable_event_code(dev.get(), EV_ABS, ABS_RX, &stick);
|
||||
libevdev_enable_event_code(dev.get(), EV_ABS, ABS_Y, &stick);
|
||||
libevdev_enable_event_code(dev.get(), EV_ABS, ABS_RY, &stick);
|
||||
|
||||
return dev;
|
||||
}
|
||||
|
||||
input_t input() {
|
||||
input_t result { new input_raw_t() };
|
||||
auto &gp = *(input_raw_t*)result.get();
|
||||
|
||||
gp.keyboard.reset(XOpenDisplay(nullptr));
|
||||
|
||||
// If we do not have a keyboard, gamepad or mouse, no input is possible and we should abort
|
||||
if(!gp.keyboard) {
|
||||
BOOST_LOG(fatal) << "Could not open x11 display for keyboard"sv;
|
||||
log_flush();
|
||||
std::abort();
|
||||
}
|
||||
|
||||
gp.gamepads.resize(MAX_GAMEPADS);
|
||||
|
||||
// Ensure starting from clean slate
|
||||
gp.clear();
|
||||
gp.mouse_dev = mouse();
|
||||
gp.gamepad_dev = x360();
|
||||
|
||||
if(gp.create_mouse()) {
|
||||
log_flush();
|
||||
std::abort();
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
void freeInput(void *p) {
|
||||
auto *input = (input_raw_t*)p;
|
||||
delete input;
|
||||
}
|
||||
|
||||
std::unique_ptr<deinit_t> init() { return nullptr; }
|
||||
}
|
||||
@@ -1,341 +0,0 @@
|
||||
#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);
|
||||
|
||||
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;
|
||||
};
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
void freeInput(void *p) {
|
||||
auto vigem = (vigem_t*)p;
|
||||
|
||||
delete vigem;
|
||||
}
|
||||
}
|
||||
@@ -1,741 +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.{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 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);
|
||||
|
||||
// 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) {
|
||||
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::unique_ptr<display_t> display() {
|
||||
auto disp = std::make_unique<dxgi::display_t>();
|
||||
|
||||
if (disp->init()) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
return disp;
|
||||
}
|
||||
}
|
||||
@@ -1,342 +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 co_init_t : public deinit_t {
|
||||
public:
|
||||
co_init_t() {
|
||||
CoInitializeEx(nullptr, COINIT_MULTITHREADED | COINIT_SPEED_OVER_MEMORY);
|
||||
}
|
||||
|
||||
~co_init_t() override {
|
||||
CoUninitialize();
|
||||
}
|
||||
};
|
||||
|
||||
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 / 1000;
|
||||
|
||||
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:
|
||||
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) {
|
||||
auto mic = std::make_unique<audio::mic_wasapi_t>();
|
||||
|
||||
if(mic->init(sample_rate)) {
|
||||
return std::make_unique<dummy_mic_t>();
|
||||
}
|
||||
|
||||
return mic;
|
||||
}
|
||||
|
||||
std::unique_ptr<deinit_t> init() {
|
||||
return std::make_unique<platf::audio::co_init_t>();
|
||||
}
|
||||
}
|
||||
@@ -1,304 +0,0 @@
|
||||
//
|
||||
// Created by loki on 12/14/19.
|
||||
//
|
||||
|
||||
#include "process.h"
|
||||
|
||||
#include <vector>
|
||||
#include <string>
|
||||
|
||||
#include <boost/property_tree/ptree.hpp>
|
||||
#include <boost/property_tree/json_parser.hpp>
|
||||
|
||||
#include "utility.h"
|
||||
#include "main.h"
|
||||
|
||||
namespace proc {
|
||||
using namespace std::literals;
|
||||
namespace bp = boost::process;
|
||||
namespace pt = boost::property_tree;
|
||||
|
||||
proc_t proc;
|
||||
|
||||
void process_end(bp::child &proc, bp::group &proc_handle) {
|
||||
if(!proc.running()) {
|
||||
return;
|
||||
}
|
||||
|
||||
BOOST_LOG(debug) << "Force termination Child-Process"sv;
|
||||
proc_handle.terminate();
|
||||
|
||||
// avoid zombie process
|
||||
proc.wait();
|
||||
}
|
||||
|
||||
int exe(const std::string &cmd, bp::environment &env, file_t &file, std::error_code &ec) {
|
||||
if(!file) {
|
||||
return bp::system(cmd, env, bp::std_out > bp::null, bp::std_err > bp::null, ec);
|
||||
}
|
||||
|
||||
return bp::system(cmd, env, bp::std_out > file.get(), bp::std_err > file.get(), ec);
|
||||
}
|
||||
|
||||
int proc_t::execute(int app_id) {
|
||||
if(!running() && _app_id != -1) {
|
||||
// previous process exited on it's own, reset _process_handle
|
||||
_process_handle = bp::group();
|
||||
|
||||
_app_id = -1;
|
||||
}
|
||||
|
||||
if(app_id < 0 || app_id >= _apps.size()) {
|
||||
BOOST_LOG(error) << "Couldn't find app with ID ["sv << app_id << ']';
|
||||
|
||||
return 404;
|
||||
}
|
||||
|
||||
// Ensure starting from a clean slate
|
||||
terminate();
|
||||
|
||||
_app_id = app_id;
|
||||
auto &proc = _apps[app_id];
|
||||
|
||||
_undo_begin = std::begin(proc.prep_cmds);
|
||||
_undo_it = _undo_begin;
|
||||
|
||||
if(!proc.output.empty() && proc.output != "null"sv) {
|
||||
_pipe.reset(fopen(proc.output.c_str(), "a"));
|
||||
}
|
||||
|
||||
std::error_code ec;
|
||||
//Executed when returning from function
|
||||
auto fg = util::fail_guard([&]() {
|
||||
terminate();
|
||||
});
|
||||
|
||||
for(; _undo_it != std::end(proc.prep_cmds); ++_undo_it) {
|
||||
auto &cmd = _undo_it->do_cmd;
|
||||
|
||||
BOOST_LOG(info) << "Executing: ["sv << cmd << ']';
|
||||
auto ret = exe(cmd, _env, _pipe, ec);
|
||||
|
||||
if(ec) {
|
||||
BOOST_LOG(error) << "System: "sv << ec.message();
|
||||
return -1;
|
||||
}
|
||||
|
||||
if(ret != 0) {
|
||||
BOOST_LOG(error) << "Return code ["sv << ret << ']';
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
BOOST_LOG(debug) << "Starting ["sv << proc.cmd << ']';
|
||||
if(proc.cmd.empty()) {
|
||||
placebo = true;
|
||||
}
|
||||
else if(proc.output.empty() || proc.output == "null"sv) {
|
||||
_process = bp::child(_process_handle, proc.cmd, _env, bp::std_out > bp::null, bp::std_err > bp::null, ec);
|
||||
}
|
||||
else {
|
||||
_process = bp::child(_process_handle, proc.cmd, _env, bp::std_out > _pipe.get(), bp::std_err > _pipe.get(), ec);
|
||||
}
|
||||
|
||||
if(ec) {
|
||||
BOOST_LOG(info) << "System: "sv << ec.message();
|
||||
return -1;
|
||||
}
|
||||
|
||||
fg.disable();
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int proc_t::running() {
|
||||
if(placebo || _process.running()) {
|
||||
return _app_id;
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
void proc_t::terminate() {
|
||||
std::error_code ec;
|
||||
|
||||
// Ensure child process is terminated
|
||||
placebo = false;
|
||||
process_end(_process, _process_handle);
|
||||
_app_id = -1;
|
||||
|
||||
if(ec) {
|
||||
BOOST_LOG(fatal) << "System: "sv << ec.message();
|
||||
log_flush();
|
||||
std::abort();
|
||||
}
|
||||
|
||||
for(;_undo_it != _undo_begin; --_undo_it) {
|
||||
auto &cmd = (_undo_it - 1)->undo_cmd;
|
||||
|
||||
if(cmd.empty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
BOOST_LOG(debug) << "Executing: ["sv << cmd << ']';
|
||||
|
||||
auto ret = exe(cmd, _env, _pipe, ec);
|
||||
|
||||
if(ec) {
|
||||
BOOST_LOG(fatal) << "System: "sv << ec.message();
|
||||
log_flush();
|
||||
std::abort();
|
||||
}
|
||||
|
||||
if(ret != 0) {
|
||||
BOOST_LOG(fatal) << "Return code ["sv << ret << ']';
|
||||
log_flush();
|
||||
std::abort();
|
||||
}
|
||||
}
|
||||
|
||||
_pipe.reset();
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
std::string_view::iterator find_match(std::string_view::iterator begin, std::string_view::iterator end) {
|
||||
int stack = 0;
|
||||
|
||||
--begin;
|
||||
do {
|
||||
++begin;
|
||||
switch(*begin) {
|
||||
case '(': ++stack;
|
||||
break;
|
||||
case ')': --stack;
|
||||
}
|
||||
} while(begin != end && stack != 0);
|
||||
|
||||
if(begin == end) {
|
||||
throw std::out_of_range("Missing closing bracket \')\'");
|
||||
}
|
||||
return begin;
|
||||
}
|
||||
|
||||
std::string parse_env_val(bp::native_environment &env, const std::string_view &val_raw) {
|
||||
auto pos = std::begin(val_raw);
|
||||
auto dollar = std::find(pos, std::end(val_raw), '$');
|
||||
|
||||
std::stringstream ss;
|
||||
|
||||
while(dollar != std::end(val_raw)) {
|
||||
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));
|
||||
|
||||
ss << env[std::string { var_begin, var_end }].to_string();
|
||||
|
||||
pos = var_end + 1;
|
||||
next = var_end;
|
||||
|
||||
break;
|
||||
}
|
||||
case '$':
|
||||
ss.write(pos, (next - pos));
|
||||
pos = next + 1;
|
||||
++next;
|
||||
break;
|
||||
}
|
||||
|
||||
dollar = std::find(next, std::end(val_raw), '$');
|
||||
}
|
||||
else {
|
||||
dollar = next;
|
||||
}
|
||||
}
|
||||
|
||||
ss.write(pos, (dollar - pos));
|
||||
|
||||
return ss.str();
|
||||
}
|
||||
|
||||
std::optional<proc::proc_t> parse(const std::string& file_name) {
|
||||
pt::ptree tree;
|
||||
|
||||
try {
|
||||
pt::read_json(file_name, tree);
|
||||
|
||||
auto &apps_node = tree.get_child("apps"s);
|
||||
auto &env_vars = tree.get_child("env"s);
|
||||
|
||||
auto this_env = boost::this_process::environment();
|
||||
|
||||
std::vector<proc::ctx_t> apps;
|
||||
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);
|
||||
|
||||
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(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));
|
||||
}
|
||||
}
|
||||
|
||||
if(output) {
|
||||
ctx.output = parse_env_val(this_env, *output);
|
||||
}
|
||||
|
||||
if(cmd) {
|
||||
ctx.cmd = parse_env_val(this_env, *cmd);
|
||||
}
|
||||
|
||||
ctx.name = std::move(name);
|
||||
ctx.prep_cmds = std::move(prep_cmds);
|
||||
|
||||
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) {
|
||||
BOOST_LOG(error) << e.what();
|
||||
}
|
||||
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
void refresh(const std::string &file_name) {
|
||||
auto proc_opt = proc::parse(file_name);
|
||||
|
||||
if(proc_opt) {
|
||||
proc = std::move(*proc_opt);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,96 +0,0 @@
|
||||
//
|
||||
// Created by loki on 12/14/19.
|
||||
//
|
||||
|
||||
#ifndef SUNSHINE_PROCESS_H
|
||||
#define SUNSHINE_PROCESS_H
|
||||
|
||||
#ifndef __kernel_entry
|
||||
#define __kernel_entry
|
||||
#endif
|
||||
|
||||
#include <unordered_map>
|
||||
#include <optional>
|
||||
|
||||
#include <boost/process.hpp>
|
||||
|
||||
#include "utility.h"
|
||||
|
||||
namespace proc {
|
||||
using file_t = util::safe_ptr_v2<FILE, int, fclose>;
|
||||
|
||||
struct cmd_t {
|
||||
cmd_t(std::string &&do_cmd, std::string &&undo_cmd) : do_cmd(std::move(do_cmd)), undo_cmd(std::move(undo_cmd)) {}
|
||||
explicit cmd_t(std::string &&do_cmd) : do_cmd(std::move(do_cmd)) {}
|
||||
|
||||
std::string do_cmd;
|
||||
|
||||
// Executed when proc_t has finished running, meant to reverse 'do_cmd' if applicable
|
||||
std::string undo_cmd;
|
||||
};
|
||||
/*
|
||||
* pre_cmds -- guaranteed to be executed unless any of the commands fail.
|
||||
* cmd -- Runs indefinitely until:
|
||||
* No session is running and a different set of commands it to be executed
|
||||
* Command exits
|
||||
* cmd_output --
|
||||
* empty -- The output of the commands are appended to the output of sunshine
|
||||
* "null" -- The output of the commands are discarded
|
||||
* filename -- The output of the commands are appended to filename
|
||||
*/
|
||||
struct ctx_t {
|
||||
std::vector<cmd_t> prep_cmds;
|
||||
|
||||
std::string name;
|
||||
std::string cmd;
|
||||
std::string output;
|
||||
};
|
||||
|
||||
class proc_t {
|
||||
public:
|
||||
KITTY_DEFAULT_CONSTR_THROW(proc_t)
|
||||
|
||||
proc_t(
|
||||
boost::process::environment &&env,
|
||||
std::vector<ctx_t> &&apps) :
|
||||
_app_id(-1),
|
||||
_env(std::move(env)),
|
||||
_apps(std::move(apps)) {}
|
||||
|
||||
int execute(int app_id);
|
||||
|
||||
/**
|
||||
* @return _app_id if a process is running, otherwise returns -1
|
||||
*/
|
||||
int running();
|
||||
|
||||
~proc_t();
|
||||
|
||||
const std::vector<ctx_t> &get_apps() const;
|
||||
std::vector<ctx_t> &get_apps();
|
||||
|
||||
void terminate();
|
||||
|
||||
private:
|
||||
int _app_id;
|
||||
|
||||
boost::process::environment _env;
|
||||
std::vector<ctx_t> _apps;
|
||||
|
||||
// If no command associated with _app_id, yet it's still running
|
||||
bool placebo {};
|
||||
|
||||
boost::process::child _process;
|
||||
boost::process::group _process_handle;
|
||||
|
||||
file_t _pipe;
|
||||
std::vector<cmd_t>::const_iterator _undo_it;
|
||||
std::vector<cmd_t>::const_iterator _undo_begin;
|
||||
};
|
||||
|
||||
void refresh(const std::string &file_name);
|
||||
std::optional<proc::proc_t> parse(const std::string& file_name);
|
||||
|
||||
extern proc_t proc;
|
||||
}
|
||||
#endif //SUNSHINE_PROCESS_H
|
||||
@@ -1,504 +0,0 @@
|
||||
//
|
||||
// Created by loki on 2/2/20.
|
||||
//
|
||||
|
||||
extern "C" {
|
||||
#include <moonlight-common-c/src/Rtsp.h>
|
||||
}
|
||||
|
||||
#include "config.h"
|
||||
#include "main.h"
|
||||
#include "network.h"
|
||||
#include "rtsp.h"
|
||||
#include "input.h"
|
||||
#include "stream.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();
|
||||
}
|
||||
}
|
||||
|
||||
void bind(std::uint16_t port) {
|
||||
_session_slots.resize(config::stream.channels);
|
||||
_slot_count = config::stream.channels;
|
||||
|
||||
_host = net::host_create(_addr, 1, port);
|
||||
}
|
||||
|
||||
void session_raise(launch_session_t launch_session) {
|
||||
--_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);
|
||||
TUPLE_2D_REF(_, old_packet, old_msg);
|
||||
|
||||
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) {
|
||||
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);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
bool accept(const std::shared_ptr<session_t> &session) {
|
||||
for(auto &slot : _session_slots) {
|
||||
if(!slot) {
|
||||
slot = session;
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
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() {
|
||||
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::string_view payload;
|
||||
if(config::video.hevc_mode == 0) {
|
||||
payload = "surround-params=NONE"sv;
|
||||
}
|
||||
else {
|
||||
payload = "sprop-parameter-sets=AAAAAU;surround-params=NONE"sv;
|
||||
}
|
||||
|
||||
respond(server->host(), peer, &option, 200, "OK", req->sequenceNumber, payload);
|
||||
}
|
||||
|
||||
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;
|
||||
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.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 == 0) {
|
||||
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);
|
||||
if(!server->accept(session)) {
|
||||
BOOST_LOG(info) << "Ran out of slots for client from ["sv << ']';
|
||||
|
||||
respond(server->host(), peer, &option, 503, "Service Unavailable", req->sequenceNumber, {});
|
||||
return;
|
||||
}
|
||||
|
||||
session::start(*session, platf::from_sockaddr((sockaddr*)&peer->address.address));
|
||||
|
||||
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);
|
||||
|
||||
server.bind(RTSP_SETUP_PORT);
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
//
|
||||
// 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;
|
||||
};
|
||||
|
||||
void launch_session_raise(launch_session_t launch_session);
|
||||
int session_count();
|
||||
|
||||
void rtpThread(std::shared_ptr<safe::signal_t> shutdown_event);
|
||||
|
||||
}
|
||||
|
||||
#endif //SUNSHINE_RTSP_H
|
||||
@@ -1,895 +0,0 @@
|
||||
//
|
||||
// Created by loki on 6/5/19.
|
||||
//
|
||||
|
||||
#include "process.h"
|
||||
|
||||
#include <queue>
|
||||
#include <future>
|
||||
|
||||
#include <fstream>
|
||||
#include <openssl/err.h>
|
||||
|
||||
extern "C" {
|
||||
#include <moonlight-common-c/src/Video.h>
|
||||
#include <rs.h>
|
||||
}
|
||||
|
||||
#include "network.h"
|
||||
#include "config.h"
|
||||
#include "utility.h"
|
||||
#include "stream.h"
|
||||
#include "thread_safe.h"
|
||||
#include "sync.h"
|
||||
#include "input.h"
|
||||
#include "main.h"
|
||||
|
||||
#define IDX_START_A 0
|
||||
#define IDX_REQUEST_IDR_FRAME 0
|
||||
#define IDX_START_B 1
|
||||
#define IDX_INVALIDATE_REF_FRAMES 2
|
||||
#define IDX_LOSS_STATS 3
|
||||
#define IDX_INPUT_DATA 5
|
||||
#define IDX_RUMBLE_DATA 6
|
||||
#define IDX_TERMINATION 7
|
||||
|
||||
static const short packetTypes[] = {
|
||||
0x0305, // Start A
|
||||
0x0307, // Start B
|
||||
0x0301, // Invalidate reference frames
|
||||
0x0201, // Loss Stats
|
||||
0x0204, // Frame Stats (unused)
|
||||
0x0206, // Input data
|
||||
0x010b, // Rumble data
|
||||
0x0100, // Termination
|
||||
};
|
||||
|
||||
constexpr auto VIDEO_STREAM_PORT = 47998;
|
||||
constexpr auto CONTROL_PORT = 47999;
|
||||
constexpr auto AUDIO_STREAM_PORT = 48000;
|
||||
|
||||
namespace asio = boost::asio;
|
||||
namespace sys = boost::system;
|
||||
|
||||
using asio::ip::tcp;
|
||||
using asio::ip::udp;
|
||||
|
||||
using namespace std::literals;
|
||||
|
||||
namespace stream {
|
||||
|
||||
enum class socket_e : int {
|
||||
video,
|
||||
audio
|
||||
};
|
||||
|
||||
#pragma pack(push, 1)
|
||||
|
||||
struct video_packet_raw_t {
|
||||
uint8_t *payload() {
|
||||
return (uint8_t *)(this + 1);
|
||||
}
|
||||
|
||||
RTP_PACKET rtp;
|
||||
NV_VIDEO_PACKET packet;
|
||||
};
|
||||
|
||||
struct audio_packet_raw_t {
|
||||
uint8_t *payload() {
|
||||
return (uint8_t *)(this + 1);
|
||||
}
|
||||
|
||||
RTP_PACKET rtp;
|
||||
};
|
||||
|
||||
#pragma pack(pop)
|
||||
|
||||
using rh_t = util::safe_ptr<reed_solomon, reed_solomon_release>;
|
||||
using video_packet_t = util::c_ptr<video_packet_raw_t>;
|
||||
using audio_packet_t = util::c_ptr<audio_packet_raw_t>;
|
||||
|
||||
using message_queue_t = std::shared_ptr<safe::queue_t<std::pair<std::uint16_t, std::string>>>;
|
||||
using message_queue_queue_t = std::shared_ptr<safe::queue_t<std::tuple<socket_e, asio::ip::address, message_queue_t>>>;
|
||||
|
||||
static inline void while_starting_do_nothing(std::atomic<session::state_e> &state) {
|
||||
while(state.load(std::memory_order_acquire) == session::state_e::STARTING) {
|
||||
std::this_thread::sleep_for(1ms);
|
||||
}
|
||||
}
|
||||
|
||||
class control_server_t {
|
||||
public:
|
||||
control_server_t(control_server_t &&) noexcept = default;
|
||||
control_server_t &operator=(control_server_t &&) noexcept = default;
|
||||
|
||||
explicit control_server_t(std::uint16_t port) : _host { net::host_create(_addr, config::stream.channels, port) } {}
|
||||
|
||||
void emplace_addr_to_session(const std::string &addr, session_t &session) {
|
||||
auto lg = _map_addr_session.lock();
|
||||
|
||||
_map_addr_session->emplace(addr, std::make_pair(0u, &session));
|
||||
}
|
||||
|
||||
// Get session associated with address.
|
||||
// If none are found, try to find a session not yet claimed. (It will be marked by a port of value 0
|
||||
// If none of those are found, return nullptr
|
||||
session_t *get_session(const net::peer_t peer);
|
||||
|
||||
// Circular dependency:
|
||||
// iterate refers to session
|
||||
// session refers to broadcast_ctx_t
|
||||
// broadcast_ctx_t refers to control_server_t
|
||||
// Therefore, iterate is implemented further down the source file
|
||||
void iterate(std::chrono::milliseconds timeout);
|
||||
|
||||
void map(uint16_t type, std::function<void(session_t *, const std::string_view&)> cb) {
|
||||
_map_type_cb.emplace(type, std::move(cb));
|
||||
}
|
||||
|
||||
void send(const std::string_view &payload) {
|
||||
std::for_each(_host->peers, _host->peers + _host->peerCount, [payload](auto &peer) {
|
||||
auto packet = enet_packet_create(payload.data(), payload.size(), ENET_PACKET_FLAG_RELIABLE);
|
||||
if(enet_peer_send(&peer, 0, packet)) {
|
||||
enet_packet_destroy(packet);
|
||||
}
|
||||
});
|
||||
|
||||
enet_host_flush(_host.get());
|
||||
}
|
||||
|
||||
// Callbacks
|
||||
std::unordered_map<std::uint16_t, std::function<void(session_t *, const std::string_view&)>> _map_type_cb;
|
||||
|
||||
// Mapping ip:port to session
|
||||
util::sync_t<std::unordered_multimap<std::string, std::pair<std::uint16_t, session_t*>>> _map_addr_session;
|
||||
|
||||
ENetAddress _addr;
|
||||
net::host_t _host;
|
||||
};
|
||||
|
||||
struct broadcast_ctx_t {
|
||||
video::packet_queue_t video_packets;
|
||||
audio::packet_queue_t audio_packets;
|
||||
|
||||
message_queue_queue_t message_queue_queue;
|
||||
|
||||
std::thread recv_thread;
|
||||
std::thread video_thread;
|
||||
std::thread audio_thread;
|
||||
std::thread control_thread;
|
||||
|
||||
asio::io_service io;
|
||||
|
||||
udp::socket video_sock { io, udp::endpoint(udp::v4(), VIDEO_STREAM_PORT) };
|
||||
udp::socket audio_sock { io, udp::endpoint(udp::v4(), AUDIO_STREAM_PORT) };
|
||||
control_server_t control_server { CONTROL_PORT };
|
||||
};
|
||||
|
||||
struct session_t {
|
||||
config_t config;
|
||||
std::shared_ptr<input::input_t> input;
|
||||
|
||||
std::thread audioThread;
|
||||
std::thread videoThread;
|
||||
|
||||
std::chrono::steady_clock::time_point pingTimeout;
|
||||
|
||||
safe::shared_t<broadcast_ctx_t>::ptr_t broadcast_ref;
|
||||
|
||||
struct {
|
||||
int lowseq;
|
||||
udp::endpoint peer;
|
||||
video::idr_event_t idr_events;
|
||||
} video;
|
||||
|
||||
struct {
|
||||
std::uint16_t frame;
|
||||
udp::endpoint peer;
|
||||
} audio;
|
||||
|
||||
struct {
|
||||
net::peer_t peer;
|
||||
} control;
|
||||
|
||||
crypto::aes_t gcm_key;
|
||||
crypto::aes_t iv;
|
||||
|
||||
safe::signal_t shutdown_event;
|
||||
safe::signal_t controlEnd;
|
||||
|
||||
std::atomic<session::state_e> state;
|
||||
};
|
||||
|
||||
int start_broadcast(broadcast_ctx_t &ctx);
|
||||
void end_broadcast(broadcast_ctx_t &ctx);
|
||||
|
||||
|
||||
static auto broadcast = safe::make_shared<broadcast_ctx_t>(start_broadcast, end_broadcast);
|
||||
safe::signal_t broadcast_shutdown_event;
|
||||
|
||||
session_t *control_server_t::get_session(const net::peer_t peer) {
|
||||
TUPLE_2D(port, addr_string, platf::from_sockaddr_ex((sockaddr*)&peer->address.address));
|
||||
|
||||
auto lg = _map_addr_session.lock();
|
||||
TUPLE_2D(begin, end, _map_addr_session->equal_range(addr_string));
|
||||
|
||||
auto it = std::end(_map_addr_session.raw);
|
||||
for(auto pos = begin; pos != end; ++pos) {
|
||||
TUPLE_2D_REF(session_port, session_p, pos->second);
|
||||
|
||||
if(port == session_port) {
|
||||
return session_p;
|
||||
}
|
||||
else if(session_port == 0) {
|
||||
it = pos;
|
||||
}
|
||||
}
|
||||
|
||||
if(it != std::end(_map_addr_session.raw)) {
|
||||
TUPLE_2D_REF(session_port, session_p, it->second);
|
||||
|
||||
session_p->control.peer = peer;
|
||||
session_port = port;
|
||||
|
||||
return session_p;
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void control_server_t::iterate(std::chrono::milliseconds timeout) {
|
||||
ENetEvent event;
|
||||
auto res = enet_host_service(_host.get(), &event, timeout.count());
|
||||
|
||||
if(res > 0) {
|
||||
auto session = get_session(event.peer);
|
||||
if(!session) {
|
||||
BOOST_LOG(warning) << "Rejected connection from ["sv << platf::from_sockaddr((sockaddr*)&event.peer->address.address) << "]: it's not properly set up"sv;
|
||||
enet_peer_disconnect_now(event.peer, 0);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
session->pingTimeout = std::chrono::steady_clock::now() + config::stream.ping_timeout;
|
||||
|
||||
switch(event.type) {
|
||||
case ENET_EVENT_TYPE_RECEIVE:
|
||||
{
|
||||
net::packet_t packet { event.packet };
|
||||
|
||||
auto type = (std::uint16_t *)packet->data;
|
||||
std::string_view payload { (char*)packet->data + sizeof(*type), packet->dataLength - sizeof(*type) };
|
||||
|
||||
auto cb = _map_type_cb.find(*type);
|
||||
if(cb == std::end(_map_type_cb)) {
|
||||
BOOST_LOG(warning)
|
||||
<< "type [Unknown] { "sv << util::hex(*type).to_string_view() << " }"sv << std::endl
|
||||
<< "---data---"sv << std::endl << util::hex_vec(payload) << std::endl << "---end data---"sv;
|
||||
}
|
||||
|
||||
else {
|
||||
cb->second(session, payload);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case ENET_EVENT_TYPE_CONNECT:
|
||||
BOOST_LOG(info) << "CLIENT CONNECTED"sv;
|
||||
break;
|
||||
case ENET_EVENT_TYPE_DISCONNECT:
|
||||
BOOST_LOG(info) << "CLIENT DISCONNECTED"sv;
|
||||
// No more clients to send video data to ^_^
|
||||
if(session->state == session::state_e::RUNNING) {
|
||||
session::stop(*session);
|
||||
}
|
||||
break;
|
||||
case ENET_EVENT_TYPE_NONE:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
namespace fec {
|
||||
using rs_t = util::safe_ptr<reed_solomon, reed_solomon_release>;
|
||||
|
||||
struct fec_t {
|
||||
size_t data_shards;
|
||||
size_t nr_shards;
|
||||
size_t percentage;
|
||||
|
||||
size_t blocksize;
|
||||
util::buffer_t<char> shards;
|
||||
|
||||
std::string_view operator[](size_t el) const {
|
||||
return { &shards[el*blocksize], blocksize };
|
||||
}
|
||||
|
||||
size_t size() const {
|
||||
return nr_shards;
|
||||
}
|
||||
};
|
||||
|
||||
fec_t encode(const std::string_view &payload, size_t blocksize, size_t fecpercentage) {
|
||||
auto payload_size = payload.size();
|
||||
|
||||
auto pad = payload_size % blocksize != 0;
|
||||
|
||||
auto data_shards = payload_size / blocksize + (pad ? 1 : 0);
|
||||
auto parity_shards = (data_shards * fecpercentage + 99) / 100;
|
||||
auto nr_shards = data_shards + parity_shards;
|
||||
|
||||
if(nr_shards > DATA_SHARDS_MAX) {
|
||||
BOOST_LOG(error)
|
||||
<< "Number of fragments for reed solomon exceeds DATA_SHARDS_MAX"sv << std::endl
|
||||
<< nr_shards << " > "sv << DATA_SHARDS_MAX;
|
||||
|
||||
return { 0 };
|
||||
}
|
||||
|
||||
util::buffer_t<char> shards { nr_shards * blocksize };
|
||||
util::buffer_t<uint8_t*> shards_p { nr_shards };
|
||||
|
||||
// copy payload + padding
|
||||
auto next = std::copy(std::begin(payload), std::end(payload), std::begin(shards));
|
||||
std::fill(next, std::end(shards), 0); // padding with zero
|
||||
|
||||
for(auto x = 0; x < nr_shards; ++x) {
|
||||
shards_p[x] = (uint8_t*)&shards[x * blocksize];
|
||||
}
|
||||
|
||||
// packets = parity_shards + data_shards
|
||||
rs_t rs { reed_solomon_new(data_shards, parity_shards) };
|
||||
|
||||
reed_solomon_encode(rs.get(), shards_p.begin(), nr_shards, blocksize);
|
||||
|
||||
return {
|
||||
data_shards,
|
||||
nr_shards,
|
||||
fecpercentage,
|
||||
blocksize,
|
||||
std::move(shards)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
template<class F>
|
||||
std::vector<uint8_t> insert(uint64_t insert_size, uint64_t slice_size, const std::string_view &data, F &&f) {
|
||||
auto pad = data.size() % slice_size != 0;
|
||||
auto elements = data.size() / slice_size + (pad ? 1 : 0);
|
||||
|
||||
std::vector<uint8_t> result;
|
||||
result.resize(elements * insert_size + data.size());
|
||||
|
||||
auto next = std::begin(data);
|
||||
for(auto x = 0; x < elements - 1; ++x) {
|
||||
void *p = &result[x*(insert_size + slice_size)];
|
||||
|
||||
f(p, x, elements);
|
||||
|
||||
std::copy(next, next + slice_size, (char*)p + insert_size);
|
||||
next += slice_size;
|
||||
}
|
||||
|
||||
auto x = elements - 1;
|
||||
void *p = &result[x*(insert_size + slice_size)];
|
||||
|
||||
f(p, x, elements);
|
||||
|
||||
std::copy(next, std::end(data), (char*)p + insert_size);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
std::vector<uint8_t> replace(const std::string_view &original, const std::string_view &old, const std::string_view &_new) {
|
||||
std::vector<uint8_t> replaced;
|
||||
|
||||
auto begin = std::begin(original);
|
||||
auto next = std::search(begin, std::end(original), std::begin(old), std::end(old));
|
||||
|
||||
std::copy(begin, next, std::back_inserter(replaced));
|
||||
std::copy(std::begin(_new), std::end(_new), std::back_inserter(replaced));
|
||||
std::copy(next + old.size(), std::end(original), std::back_inserter(replaced));
|
||||
|
||||
return replaced;
|
||||
}
|
||||
|
||||
void controlBroadcastThread(safe::signal_t *shutdown_event, control_server_t *server) {
|
||||
server->map(packetTypes[IDX_START_A], [&](session_t *session, const std::string_view &payload) {
|
||||
BOOST_LOG(debug) << "type [IDX_START_A]"sv;
|
||||
});
|
||||
|
||||
server->map(packetTypes[IDX_START_B], [&](session_t *session, const std::string_view &payload) {
|
||||
BOOST_LOG(debug) << "type [IDX_START_B]"sv;
|
||||
});
|
||||
|
||||
server->map(packetTypes[IDX_LOSS_STATS], [&](session_t *session, const std::string_view &payload) {
|
||||
int32_t *stats = (int32_t*)payload.data();
|
||||
auto count = stats[0];
|
||||
std::chrono::milliseconds t { stats[1] };
|
||||
|
||||
auto lastGoodFrame = stats[3];
|
||||
|
||||
BOOST_LOG(verbose)
|
||||
<< "type [IDX_LOSS_STATS]"sv << std::endl
|
||||
<< "---begin stats---" << std::endl
|
||||
<< "loss count since last report [" << count << ']' << std::endl
|
||||
<< "time in milli since last report [" << t.count() << ']' << std::endl
|
||||
<< "last good frame [" << lastGoodFrame << ']' << std::endl
|
||||
<< "---end stats---";
|
||||
});
|
||||
|
||||
server->map(packetTypes[IDX_INVALIDATE_REF_FRAMES], [&](session_t *session, const std::string_view &payload) {
|
||||
auto frames = (std::int64_t *)payload.data();
|
||||
auto firstFrame = frames[0];
|
||||
auto lastFrame = frames[1];
|
||||
|
||||
BOOST_LOG(debug)
|
||||
<< "type [IDX_INVALIDATE_REF_FRAMES]"sv << std::endl
|
||||
<< "firstFrame [" << firstFrame << ']' << std::endl
|
||||
<< "lastFrame [" << lastFrame << ']';
|
||||
|
||||
session->video.idr_events->raise(std::make_pair(firstFrame, lastFrame));
|
||||
});
|
||||
|
||||
server->map(packetTypes[IDX_INPUT_DATA], [&](session_t *session, const std::string_view &payload) {
|
||||
BOOST_LOG(debug) << "type [IDX_INPUT_DATA]"sv;
|
||||
|
||||
int32_t tagged_cipher_length = util::endian::big(*(int32_t*)payload.data());
|
||||
std::string_view tagged_cipher { payload.data() + sizeof(tagged_cipher_length), (size_t)tagged_cipher_length };
|
||||
|
||||
crypto::cipher_t cipher { session->gcm_key };
|
||||
cipher.padding = false;
|
||||
|
||||
std::vector<uint8_t> plaintext;
|
||||
if(cipher.decrypt_gcm(session->iv, tagged_cipher, plaintext)) {
|
||||
// something went wrong :(
|
||||
|
||||
BOOST_LOG(error) << "Failed to verify tag"sv;
|
||||
|
||||
session::stop(*session);
|
||||
}
|
||||
|
||||
if(tagged_cipher_length >= 16 + session->iv.size()) {
|
||||
std::copy(payload.end() - 16, payload.end(), std::begin(session->iv));
|
||||
}
|
||||
|
||||
input::print(plaintext.data());
|
||||
input::passthrough(session->input, std::move(plaintext));
|
||||
});
|
||||
|
||||
while(!shutdown_event->peek()) {
|
||||
{
|
||||
auto lg = server->_map_addr_session.lock();
|
||||
|
||||
auto now = std::chrono::steady_clock::now();
|
||||
|
||||
KITTY_WHILE_LOOP(auto pos = std::begin(*server->_map_addr_session), pos != std::end(*server->_map_addr_session), {
|
||||
TUPLE_2D_REF(addr, port_session, *pos);
|
||||
auto session = port_session.second;
|
||||
|
||||
if(now > session->pingTimeout) {
|
||||
BOOST_LOG(info) << addr << ": Ping Timeout"sv;
|
||||
session::stop(*session);
|
||||
}
|
||||
|
||||
if(session->state.load(std::memory_order_acquire) == session::state_e::STOPPING) {
|
||||
pos = server->_map_addr_session->erase(pos);
|
||||
|
||||
enet_peer_disconnect_now(session->control.peer, 0);
|
||||
session->controlEnd.raise(true);
|
||||
continue;
|
||||
}
|
||||
|
||||
++pos;
|
||||
})
|
||||
}
|
||||
|
||||
if(proc::proc.running() == -1) {
|
||||
BOOST_LOG(debug) << "Process terminated"sv;
|
||||
|
||||
std::uint16_t reason = 0x0100;
|
||||
|
||||
std::array<std::uint16_t, 2> payload;
|
||||
payload[0] = packetTypes[IDX_TERMINATION];
|
||||
payload[1] = reason;
|
||||
|
||||
server->send(std::string_view {(char*)payload.data(), payload.size()});
|
||||
|
||||
shutdown_event->raise(true);
|
||||
continue;
|
||||
}
|
||||
|
||||
server->iterate(500ms);
|
||||
}
|
||||
}
|
||||
|
||||
void recvThread(broadcast_ctx_t &ctx) {
|
||||
std::map<asio::ip::address, message_queue_t> peer_to_video_session;
|
||||
std::map<asio::ip::address, message_queue_t> peer_to_audio_session;
|
||||
|
||||
auto &video_sock = ctx.video_sock;
|
||||
auto &audio_sock = ctx.audio_sock;
|
||||
|
||||
auto &message_queue_queue = ctx.message_queue_queue;
|
||||
auto &io = ctx.io;
|
||||
|
||||
udp::endpoint peer;
|
||||
|
||||
std::array<char, 2048> buf[2];
|
||||
std::function<void(const boost::system::error_code, size_t)> recv_func[2];
|
||||
|
||||
auto populate_peer_to_session = [&]() {
|
||||
while(message_queue_queue->peek()) {
|
||||
auto message_queue_opt = message_queue_queue->pop();
|
||||
TUPLE_3D_REF(socket_type, addr, message_queue, *message_queue_opt);
|
||||
|
||||
switch(socket_type) {
|
||||
case socket_e::video:
|
||||
if(message_queue) {
|
||||
peer_to_video_session.emplace(addr, message_queue);
|
||||
}
|
||||
else {
|
||||
peer_to_video_session.erase(addr);
|
||||
}
|
||||
break;
|
||||
case socket_e::audio:
|
||||
if(message_queue) {
|
||||
peer_to_audio_session.emplace(addr, message_queue);
|
||||
}
|
||||
else {
|
||||
peer_to_audio_session.erase(addr);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
auto recv_func_init = [&](udp::socket &sock, int buf_elem, std::map<asio::ip::address, message_queue_t> &peer_to_session) {
|
||||
recv_func[buf_elem] = [&,buf_elem](const boost::system::error_code &ec, size_t bytes) {
|
||||
auto fg = util::fail_guard([&]() {
|
||||
sock.async_receive_from(asio::buffer(buf[buf_elem]), peer, 0, recv_func[buf_elem]);
|
||||
});
|
||||
|
||||
auto type_str = buf_elem ? "AUDIO"sv : "VIDEO"sv;
|
||||
BOOST_LOG(debug) << "Recv: "sv << peer.address().to_string() << ":"sv << peer.port() << " :: " << type_str;
|
||||
|
||||
|
||||
populate_peer_to_session();
|
||||
|
||||
// No data, yet no error
|
||||
if(ec == boost::system::errc::connection_refused || ec == boost::system::errc::connection_reset) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(ec || !bytes) {
|
||||
BOOST_LOG(fatal) << "Couldn't receive data from udp socket: "sv << ec.message();
|
||||
|
||||
log_flush();
|
||||
std::abort();
|
||||
}
|
||||
|
||||
auto it = peer_to_session.find(peer.address());
|
||||
if(it != std::end(peer_to_session)) {
|
||||
BOOST_LOG(debug) << "RAISE: "sv << peer.address().to_string() << ":"sv << peer.port() << " :: " << type_str;
|
||||
it->second->raise(peer.port(), std::string { buf[buf_elem].data(), bytes });
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
recv_func_init(video_sock, 0, peer_to_video_session);
|
||||
recv_func_init(audio_sock, 1, peer_to_audio_session);
|
||||
|
||||
video_sock.async_receive_from(asio::buffer(buf[0]), peer, 0, recv_func[0]);
|
||||
audio_sock.async_receive_from(asio::buffer(buf[1]), peer, 0, recv_func[1]);
|
||||
|
||||
while(!broadcast_shutdown_event.peek()) {
|
||||
io.run();
|
||||
}
|
||||
}
|
||||
|
||||
void videoBroadcastThread(safe::signal_t *shutdown_event, udp::socket &sock, video::packet_queue_t packets) {
|
||||
while(auto packet = packets->pop()) {
|
||||
if(shutdown_event->peek()) {
|
||||
break;
|
||||
}
|
||||
|
||||
auto session = (session_t*)packet->channel_data;
|
||||
auto lowseq = session->video.lowseq;
|
||||
|
||||
std::string_view payload{(char *) packet->data, (size_t) packet->size};
|
||||
std::vector<uint8_t> payload_new;
|
||||
|
||||
auto nv_packet_header = "\0017charss"sv;
|
||||
std::copy(std::begin(nv_packet_header), std::end(nv_packet_header), std::back_inserter(payload_new));
|
||||
std::copy(std::begin(payload), std::end(payload), std::back_inserter(payload_new));
|
||||
|
||||
payload = {(char *) payload_new.data(), payload_new.size()};
|
||||
|
||||
// make sure moonlight recognizes the nalu code for IDR frames
|
||||
if (packet->flags & AV_PKT_FLAG_KEY) {
|
||||
// TODO: Not all encoders encode their IDR frames with the 4 byte NALU prefix
|
||||
std::string_view frame_old = "\000\000\001e"sv;
|
||||
std::string_view frame_new = "\000\000\000\001e"sv;
|
||||
if(session->config.monitor.videoFormat != 0) {
|
||||
frame_old = "\000\000\001("sv;
|
||||
frame_new = "\000\000\000\001("sv;
|
||||
}
|
||||
|
||||
assert(std::search(std::begin(payload), std::end(payload), std::begin(hevc_i_frame), std::end(hevc_i_frame)) ==
|
||||
std::end(payload));
|
||||
payload_new = replace(payload, frame_old, frame_new);
|
||||
payload = {(char *) payload_new.data(), payload_new.size()};
|
||||
}
|
||||
|
||||
// insert packet headers
|
||||
auto blocksize = session->config.packetsize + MAX_RTP_HEADER_SIZE;
|
||||
auto payload_blocksize = blocksize - sizeof(video_packet_raw_t);
|
||||
|
||||
auto fecPercentage = config::stream.fec_percentage;
|
||||
|
||||
payload_new = insert(sizeof(video_packet_raw_t), payload_blocksize,
|
||||
payload, [&](void *p, int fecIndex, int end) {
|
||||
video_packet_raw_t *video_packet = (video_packet_raw_t *)p;
|
||||
|
||||
video_packet->packet.flags = FLAG_CONTAINS_PIC_DATA;
|
||||
video_packet->packet.frameIndex = packet->pts;
|
||||
video_packet->packet.streamPacketIndex = ((uint32_t)lowseq + fecIndex) << 8;
|
||||
video_packet->packet.fecInfo = (
|
||||
fecIndex << 12 |
|
||||
end << 22 |
|
||||
fecPercentage << 4
|
||||
);
|
||||
|
||||
if(fecIndex == 0) {
|
||||
video_packet->packet.flags |= FLAG_SOF;
|
||||
}
|
||||
|
||||
if(fecIndex == end - 1) {
|
||||
video_packet->packet.flags |= FLAG_EOF;
|
||||
}
|
||||
|
||||
video_packet->rtp.sequenceNumber = util::endian::big<uint16_t>(lowseq + fecIndex);
|
||||
});
|
||||
|
||||
payload = {(char *) payload_new.data(), payload_new.size()};
|
||||
|
||||
auto shards = fec::encode(payload, blocksize, fecPercentage);
|
||||
if(shards.data_shards == 0) {
|
||||
BOOST_LOG(info) << "skipping frame..."sv << std::endl;
|
||||
continue;
|
||||
}
|
||||
|
||||
for (auto x = shards.data_shards; x < shards.size(); ++x) {
|
||||
video_packet_raw_t *inspect = (video_packet_raw_t *)shards[x].data();
|
||||
|
||||
inspect->packet.frameIndex = packet->pts;
|
||||
inspect->packet.fecInfo = (
|
||||
x << 12 |
|
||||
shards.data_shards << 22 |
|
||||
fecPercentage << 4
|
||||
);
|
||||
|
||||
inspect->rtp.sequenceNumber = util::endian::big<uint16_t>(lowseq + x);
|
||||
}
|
||||
|
||||
for (auto x = 0; x < shards.size(); ++x) {
|
||||
sock.send_to(asio::buffer(shards[x]), session->video.peer);
|
||||
}
|
||||
|
||||
if(packet->flags & AV_PKT_FLAG_KEY) {
|
||||
BOOST_LOG(verbose) << "Key Frame ["sv << packet->pts << "] :: send ["sv << shards.size() << "] shards..."sv;
|
||||
}
|
||||
else {
|
||||
BOOST_LOG(verbose) << "Frame ["sv << packet->pts << "] :: send ["sv << shards.size() << "] shards..."sv << std::endl;
|
||||
}
|
||||
|
||||
session->video.lowseq += shards.size();
|
||||
}
|
||||
|
||||
shutdown_event->raise(true);
|
||||
}
|
||||
|
||||
void audioBroadcastThread(safe::signal_t *shutdown_event, udp::socket &sock, audio::packet_queue_t packets) {
|
||||
while (auto packet = packets->pop()) {
|
||||
if(shutdown_event->peek()) {
|
||||
break;
|
||||
}
|
||||
|
||||
TUPLE_2D_REF(channel_data, packet_data, *packet);
|
||||
auto session = (session_t*)channel_data;
|
||||
|
||||
auto frame = session->audio.frame++;
|
||||
|
||||
audio_packet_t audio_packet { (audio_packet_raw_t*)malloc(sizeof(audio_packet_raw_t) + packet_data.size()) };
|
||||
|
||||
audio_packet->rtp.header = 0;
|
||||
audio_packet->rtp.packetType = 97;
|
||||
audio_packet->rtp.sequenceNumber = util::endian::big(frame);
|
||||
audio_packet->rtp.timestamp = 0;
|
||||
audio_packet->rtp.ssrc = 0;
|
||||
|
||||
std::copy(std::begin(packet_data), std::end(packet_data), audio_packet->payload());
|
||||
|
||||
sock.send_to(asio::buffer((char*)audio_packet.get(), sizeof(audio_packet_raw_t) + packet_data.size()), session->audio.peer);
|
||||
BOOST_LOG(verbose) << "Audio ["sv << frame << "] :: send..."sv;
|
||||
}
|
||||
|
||||
shutdown_event->raise(true);
|
||||
}
|
||||
|
||||
int start_broadcast(broadcast_ctx_t &ctx) {
|
||||
ctx.video_packets = std::make_shared<video::packet_queue_t::element_type>();
|
||||
ctx.audio_packets = std::make_shared<audio::packet_queue_t::element_type>();
|
||||
ctx.message_queue_queue = std::make_shared<message_queue_queue_t::element_type>();
|
||||
|
||||
ctx.video_thread = std::thread { videoBroadcastThread, &broadcast_shutdown_event, std::ref(ctx.video_sock), ctx.video_packets };
|
||||
ctx.audio_thread = std::thread { audioBroadcastThread, &broadcast_shutdown_event, std::ref(ctx.audio_sock), ctx.audio_packets };
|
||||
ctx.control_thread = std::thread { controlBroadcastThread, &broadcast_shutdown_event, &ctx.control_server };
|
||||
|
||||
ctx.recv_thread = std::thread { recvThread, std::ref(ctx) };
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
void end_broadcast(broadcast_ctx_t &ctx) {
|
||||
broadcast_shutdown_event.raise(true);
|
||||
|
||||
// Minimize delay stopping video/audio threads
|
||||
ctx.video_packets->stop();
|
||||
ctx.audio_packets->stop();
|
||||
|
||||
ctx.message_queue_queue->stop();
|
||||
ctx.io.stop();
|
||||
|
||||
ctx.video_sock.close();
|
||||
ctx.audio_sock.close();
|
||||
|
||||
ctx.video_packets.reset();
|
||||
ctx.audio_packets.reset();
|
||||
|
||||
BOOST_LOG(debug) << "Waiting for main listening thread to end..."sv;
|
||||
ctx.recv_thread.join();
|
||||
BOOST_LOG(debug) << "Waiting for main video thread to end..."sv;
|
||||
ctx.video_thread.join();
|
||||
BOOST_LOG(debug) << "Waiting for main audio thread to end..."sv;
|
||||
ctx.audio_thread.join();
|
||||
BOOST_LOG(debug) << "Waiting for main control thread to end..."sv;
|
||||
ctx.control_thread.join();
|
||||
BOOST_LOG(debug) << "All broadcasting threads ended"sv;
|
||||
|
||||
broadcast_shutdown_event.reset();
|
||||
}
|
||||
|
||||
int recv_ping(decltype(broadcast)::ptr_t ref, socket_e type, asio::ip::address &addr, std::chrono::milliseconds timeout) {
|
||||
auto constexpr ping = "PING"sv;
|
||||
|
||||
auto messages = std::make_shared<message_queue_t::element_type>();
|
||||
ref->message_queue_queue->raise(type, addr, messages);
|
||||
|
||||
auto fg = util::fail_guard([&]() {
|
||||
// remove message queue from session
|
||||
ref->message_queue_queue->raise(type, addr, nullptr);
|
||||
});
|
||||
|
||||
auto msg_opt = messages->pop(config::stream.ping_timeout);
|
||||
messages->stop();
|
||||
|
||||
if(!msg_opt) {
|
||||
BOOST_LOG(error) << "Initial Ping Timeout"sv;
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
TUPLE_2D_REF(port, msg, *msg_opt);
|
||||
if(msg != ping) {
|
||||
BOOST_LOG(error) << "First message is not a PING";
|
||||
BOOST_LOG(debug) << "Received from "sv << addr << ':' << port << " ["sv << util::hex_vec(msg) << ']';
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
return port;
|
||||
}
|
||||
|
||||
void videoThread(session_t *session, std::string addr_str) {
|
||||
auto fg = util::fail_guard([&]() {
|
||||
session::stop(*session);
|
||||
});
|
||||
|
||||
while_starting_do_nothing(session->state);
|
||||
|
||||
auto addr = asio::ip::make_address(addr_str);
|
||||
auto ref = broadcast.ref();
|
||||
auto port = recv_ping(ref, socket_e::video, addr, config::stream.ping_timeout);
|
||||
if(port < 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
session->video.peer.address(addr);
|
||||
session->video.peer.port(port);
|
||||
|
||||
BOOST_LOG(debug) << "Start capturing Video"sv;
|
||||
video::capture(&session->shutdown_event, ref->video_packets, session->video.idr_events, session->config.monitor, session);
|
||||
}
|
||||
|
||||
void audioThread(session_t *session, std::string addr_str) {
|
||||
auto fg = util::fail_guard([&]() {
|
||||
session::stop(*session);
|
||||
});
|
||||
|
||||
while_starting_do_nothing(session->state);
|
||||
|
||||
auto addr = asio::ip::make_address(addr_str);
|
||||
|
||||
auto ref = broadcast.ref();
|
||||
auto port = recv_ping(ref, socket_e::audio, addr, config::stream.ping_timeout);
|
||||
if(port < 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
session->audio.peer.address(addr);
|
||||
session->audio.peer.port(port);
|
||||
|
||||
BOOST_LOG(debug) << "Start capturing Audio"sv;
|
||||
audio::capture(&session->shutdown_event, ref->audio_packets, session->config.audio, session);
|
||||
}
|
||||
|
||||
namespace session {
|
||||
state_e state(session_t &session) {
|
||||
return session.state.load(std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
void stop(session_t &session) {
|
||||
while_starting_do_nothing(session.state);
|
||||
|
||||
auto expected = state_e::RUNNING;
|
||||
auto already_stopping = !session.state.compare_exchange_strong(expected, state_e::STOPPING);
|
||||
if(already_stopping) {
|
||||
return;
|
||||
}
|
||||
|
||||
session.shutdown_event.raise(true);
|
||||
}
|
||||
|
||||
void join(session_t &session) {
|
||||
BOOST_LOG(debug) << "Waiting for video to end..."sv;
|
||||
session.videoThread.join();
|
||||
BOOST_LOG(debug) << "Waiting for audio to end..."sv;
|
||||
session.audioThread.join();
|
||||
BOOST_LOG(debug) << "Waiting for control to end..."sv;
|
||||
session.controlEnd.view();
|
||||
BOOST_LOG(debug) << "Session ended"sv;
|
||||
}
|
||||
|
||||
void start(session_t &session, const std::string &addr_string) {
|
||||
session.input = input::alloc();
|
||||
|
||||
session.broadcast_ref = broadcast.ref();
|
||||
session.broadcast_ref->control_server.emplace_addr_to_session(addr_string, session);
|
||||
|
||||
session.pingTimeout = std::chrono::steady_clock::now() + config::stream.ping_timeout;
|
||||
|
||||
session.audioThread = std::thread {audioThread, &session, addr_string};
|
||||
session.videoThread = std::thread {videoThread, &session, addr_string};
|
||||
|
||||
session.state.store(state_e::RUNNING, std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
std::shared_ptr<session_t> alloc(config_t &config, crypto::aes_t &gcm_key, crypto::aes_t &iv) {
|
||||
auto session = std::make_shared<session_t>();
|
||||
|
||||
session->config = config;
|
||||
session->gcm_key = gcm_key;
|
||||
session->iv = iv;
|
||||
|
||||
session->video.idr_events = std::make_shared<video::idr_event_t::element_type>();
|
||||
session->video.lowseq = 0;
|
||||
|
||||
session->audio.frame = 1;
|
||||
|
||||
session->control.peer = nullptr;
|
||||
session->state.store(state_e::STOPPED, std::memory_order_relaxed);
|
||||
|
||||
return session;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
//
|
||||
// Created by loki on 6/5/19.
|
||||
//
|
||||
|
||||
#ifndef SUNSHINE_STREAM_H
|
||||
#define SUNSHINE_STREAM_H
|
||||
|
||||
#include <boost/asio.hpp>
|
||||
|
||||
#include "video.h"
|
||||
#include "audio.h"
|
||||
#include "crypto.h"
|
||||
|
||||
namespace stream {
|
||||
struct session_t;
|
||||
struct config_t {
|
||||
audio::config_t audio;
|
||||
video::config_t monitor;
|
||||
int packetsize;
|
||||
|
||||
bool sops;
|
||||
std::optional<int> gcmap;
|
||||
};
|
||||
|
||||
namespace session {
|
||||
enum class state_e : int {
|
||||
STOPPED,
|
||||
STOPPING,
|
||||
STARTING,
|
||||
RUNNING,
|
||||
};
|
||||
|
||||
std::shared_ptr<session_t> alloc(config_t &config, crypto::aes_t &gcm_key, crypto::aes_t &iv);
|
||||
void 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);
|
||||
}
|
||||
|
||||
extern safe::signal_t broadcast_shutdown_event;
|
||||
}
|
||||
|
||||
#endif //SUNSHINE_STREAM_H
|
||||
@@ -1,85 +0,0 @@
|
||||
//
|
||||
// Created by loki on 16-4-19.
|
||||
//
|
||||
|
||||
#ifndef SUNSHINE_SYNC_H
|
||||
#define SUNSHINE_SYNC_H
|
||||
|
||||
#include <utility>
|
||||
#include <mutex>
|
||||
#include <array>
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
|
||||
#endif //T_MAN_SYNC_H
|
||||
@@ -1,238 +0,0 @@
|
||||
#ifndef KITTY_TASK_POOL_H
|
||||
#define KITTY_TASK_POOL_H
|
||||
|
||||
#include <deque>
|
||||
#include <vector>
|
||||
#include <future>
|
||||
#include <chrono>
|
||||
#include <utility>
|
||||
#include <functional>
|
||||
#include <mutex>
|
||||
#include <type_traits>
|
||||
#include <optional>
|
||||
|
||||
#include "utility.h"
|
||||
#include "move_by_copy.h"
|
||||
namespace util {
|
||||
|
||||
class _ImplBase {
|
||||
public:
|
||||
//_unique_base_type _this_ptr;
|
||||
|
||||
inline virtual ~_ImplBase() = default;
|
||||
|
||||
virtual void run() = 0;
|
||||
};
|
||||
|
||||
template<class Function>
|
||||
class _Impl : public _ImplBase {
|
||||
Function _func;
|
||||
|
||||
public:
|
||||
|
||||
_Impl(Function&& f) : _func(std::forward<Function>(f)) { }
|
||||
|
||||
void run() override {
|
||||
_func();
|
||||
}
|
||||
};
|
||||
|
||||
class TaskPool {
|
||||
public:
|
||||
typedef std::unique_ptr<_ImplBase> __task;
|
||||
typedef _ImplBase* task_id_t;
|
||||
|
||||
|
||||
typedef std::chrono::steady_clock::time_point __time_point;
|
||||
|
||||
template<class R>
|
||||
class timer_task_t {
|
||||
public:
|
||||
task_id_t task_id;
|
||||
std::future<R> future;
|
||||
|
||||
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::mutex _task_mutex;
|
||||
|
||||
public:
|
||||
TaskPool() = default;
|
||||
TaskPool(TaskPool &&other) noexcept : _tasks { std::move(other._tasks) }, _timer_tasks { std::move(other._timer_tasks) } {}
|
||||
|
||||
TaskPool &operator=(TaskPool &&other) noexcept {
|
||||
std::swap(_tasks, other._tasks);
|
||||
std::swap(_timer_tasks, other._timer_tasks);
|
||||
|
||||
return *this;
|
||||
}
|
||||
|
||||
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");
|
||||
|
||||
using __return = std::invoke_result_t<Function, Args &&...>;
|
||||
using task_t = std::packaged_task<__return()>;
|
||||
|
||||
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));
|
||||
};
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
void pushDelayed(std::pair<__time_point, __task> &&task) {
|
||||
std::lock_guard lg(_task_mutex);
|
||||
|
||||
auto it = _timer_tasks.cbegin();
|
||||
for(; it < _timer_tasks.cend(); ++it) {
|
||||
if(std::get<0>(*it) < task.first) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
_timer_tasks.emplace(it, task.first, std::move(task.second));
|
||||
}
|
||||
|
||||
/**
|
||||
* @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");
|
||||
|
||||
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;
|
||||
|
||||
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));
|
||||
};
|
||||
|
||||
task_t task(std::move(bind));
|
||||
|
||||
auto future = task.get_future();
|
||||
auto runnable = toRunnable(std::move(task));
|
||||
|
||||
task_id_t task_id = &*runnable;
|
||||
|
||||
pushDelayed(std::pair { time_point, std::move(runnable) });
|
||||
|
||||
return timer_task_t<__return> { task_id, future };
|
||||
}
|
||||
|
||||
/**
|
||||
* @param duration The delay before executing the task
|
||||
*/
|
||||
template<class X, class Y>
|
||||
void delay(task_id_t task_id, std::chrono::duration<X, Y> duration) {
|
||||
std::lock_guard<std::mutex> lg(_task_mutex);
|
||||
|
||||
auto it = _timer_tasks.begin();
|
||||
for(; it < _timer_tasks.cend(); ++it) {
|
||||
const __task &task = std::get<1>(*it);
|
||||
|
||||
if(&*task == task_id) {
|
||||
std::get<0>(*it) = std::chrono::steady_clock::now() + duration;
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if(it == _timer_tasks.cend()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// smaller time goes to the back
|
||||
auto prev = it -1;
|
||||
while(it > _timer_tasks.cbegin()) {
|
||||
if(std::get<0>(*it) > std::get<0>(*prev)) {
|
||||
std::swap(*it, *prev);
|
||||
}
|
||||
|
||||
--prev; --it;
|
||||
}
|
||||
}
|
||||
|
||||
bool cancel(task_id_t task_id) {
|
||||
std::lock_guard lg(_task_mutex);
|
||||
|
||||
auto it = _timer_tasks.begin();
|
||||
for(; it < _timer_tasks.cend(); ++it) {
|
||||
const __task &task = std::get<1>(*it);
|
||||
|
||||
if(&*task == task_id) {
|
||||
_timer_tasks.erase(it);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
std::optional<std::pair<__time_point, __task>> pop(task_id_t task_id) {
|
||||
std::lock_guard lg(_task_mutex);
|
||||
|
||||
auto pos = std::find_if(std::begin(_timer_tasks), std::end(_timer_tasks), [&task_id](const auto &t) { return t.second.get() == task_id; });
|
||||
|
||||
if(pos == std::end(_timer_tasks)) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
return std::move(*pos);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
bool ready() {
|
||||
std::lock_guard<std::mutex> lg(_task_mutex);
|
||||
|
||||
return !_tasks.empty() || (!_timer_tasks.empty() && std::get<0>(_timer_tasks.back()) <= std::chrono::steady_clock::now());
|
||||
}
|
||||
|
||||
std::optional<__time_point> next() {
|
||||
std::lock_guard<std::mutex> lg(_task_mutex);
|
||||
|
||||
if(_timer_tasks.empty()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
return std::get<0>(_timer_tasks.back());
|
||||
}
|
||||
private:
|
||||
|
||||
template<class Function>
|
||||
std::unique_ptr<_ImplBase> toRunnable(Function &&f) {
|
||||
return std::make_unique<_Impl<Function>>(std::forward<Function&&>(f));
|
||||
}
|
||||
};
|
||||
}
|
||||
#endif
|
||||
@@ -1,121 +0,0 @@
|
||||
#ifndef KITTY_THREAD_POOL_H
|
||||
#define KITTY_THREAD_POOL_H
|
||||
|
||||
#include <thread>
|
||||
#include "task_pool.h"
|
||||
|
||||
namespace util {
|
||||
/*
|
||||
* Allow threads to execute unhindered
|
||||
* while keeping full controll over the threads.
|
||||
*/
|
||||
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) {
|
||||
t = std::thread(&ThreadPool::_main, this);
|
||||
}
|
||||
}
|
||||
|
||||
~ThreadPool() noexcept {
|
||||
if (!_continue) return;
|
||||
|
||||
stop();
|
||||
join();
|
||||
}
|
||||
|
||||
template<class Function, class... Args>
|
||||
auto push(Function && newTask, Args &&... args) {
|
||||
std::lock_guard lg(_lock);
|
||||
auto future = TaskPool::push(std::forward<Function>(newTask), std::forward<Args>(args)...);
|
||||
|
||||
_cv.notify_one();
|
||||
return future;
|
||||
}
|
||||
|
||||
void pushDelayed(std::pair<__time_point, __task> &&task) {
|
||||
std::lock_guard lg(_lock);
|
||||
|
||||
TaskPool::pushDelayed(std::move(task));
|
||||
}
|
||||
|
||||
template<class Function, class X, class Y, class... 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)...);
|
||||
|
||||
// Update all timers for wait_until
|
||||
_cv.notify_all();
|
||||
return future;
|
||||
}
|
||||
|
||||
void start(int threads) {
|
||||
_continue = true;
|
||||
|
||||
_thread.resize(threads);
|
||||
|
||||
for(auto &t : _thread) {
|
||||
t = std::thread(&ThreadPool::_main, this);
|
||||
}
|
||||
}
|
||||
|
||||
void stop() {
|
||||
std::lock_guard lg(_lock);
|
||||
|
||||
_continue = false;
|
||||
_cv.notify_all();
|
||||
}
|
||||
|
||||
void join() {
|
||||
for (auto & t : _thread) {
|
||||
t.join();
|
||||
}
|
||||
}
|
||||
|
||||
public:
|
||||
|
||||
void _main() {
|
||||
while (_continue) {
|
||||
if(auto task = this->pop()) {
|
||||
(*task)->run();
|
||||
}
|
||||
else {
|
||||
std::unique_lock uniq_lock(_lock);
|
||||
|
||||
if(ready()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if(!_continue) {
|
||||
break;
|
||||
}
|
||||
|
||||
if(auto tp = next()) {
|
||||
_cv.wait_until(uniq_lock, *tp);
|
||||
}
|
||||
else {
|
||||
_cv.wait(uniq_lock);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Execute remaining tasks
|
||||
while(auto task = this->pop()) {
|
||||
(*task)->run();
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
#endif
|
||||
@@ -1,331 +0,0 @@
|
||||
//
|
||||
// Created by loki on 6/10/19.
|
||||
//
|
||||
|
||||
#ifndef SUNSHINE_THREAD_SAFE_H
|
||||
#define SUNSHINE_THREAD_SAFE_H
|
||||
|
||||
#include <vector>
|
||||
#include <mutex>
|
||||
#include <condition_variable>
|
||||
#include <atomic>
|
||||
#include <functional>
|
||||
|
||||
#include "utility.h"
|
||||
|
||||
namespace safe {
|
||||
template<class T>
|
||||
class event_t {
|
||||
using status_t = util::optional_t<T>;
|
||||
|
||||
public:
|
||||
template<class...Args>
|
||||
void raise(Args &&...args) {
|
||||
std::lock_guard lg { _lock };
|
||||
if(!_continue) {
|
||||
return;
|
||||
}
|
||||
|
||||
_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};
|
||||
|
||||
if (!_continue) {
|
||||
return util::false_v<status_t>;
|
||||
}
|
||||
|
||||
while (!_status) {
|
||||
_cv.wait(ul);
|
||||
|
||||
if (!_continue) {
|
||||
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
|
||||
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};
|
||||
|
||||
if (!_continue) {
|
||||
return util::false_v<status_t>;
|
||||
}
|
||||
|
||||
while (!_status) {
|
||||
_cv.wait(ul);
|
||||
|
||||
if (!_continue) {
|
||||
return util::false_v<status_t>;
|
||||
}
|
||||
}
|
||||
|
||||
return _status;
|
||||
}
|
||||
|
||||
bool peek() {
|
||||
std::lock_guard lg { _lock };
|
||||
|
||||
return _continue && (bool)_status;
|
||||
}
|
||||
|
||||
void stop() {
|
||||
std::lock_guard lg{_lock};
|
||||
|
||||
_continue = false;
|
||||
|
||||
_cv.notify_all();
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
std::condition_variable _cv;
|
||||
std::mutex _lock;
|
||||
};
|
||||
|
||||
template<class T>
|
||||
class queue_t {
|
||||
using status_t = util::optional_t<T>;
|
||||
|
||||
public:
|
||||
template<class ...Args>
|
||||
void raise(Args &&... args) {
|
||||
std::lock_guard lg{_lock};
|
||||
|
||||
if(!_continue) {
|
||||
return;
|
||||
}
|
||||
|
||||
_queue.emplace_back(std::forward<Args>(args)...);
|
||||
|
||||
_cv.notify_all();
|
||||
}
|
||||
|
||||
bool peek() {
|
||||
std::lock_guard lg { _lock };
|
||||
|
||||
return _continue && !_queue.empty();
|
||||
}
|
||||
|
||||
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 (_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) {
|
||||
return util::false_v<status_t>;
|
||||
}
|
||||
}
|
||||
|
||||
auto val = std::move(_queue.front());
|
||||
_queue.erase(std::begin(_queue));
|
||||
|
||||
return val;
|
||||
}
|
||||
|
||||
std::vector<T> &unsafe() {
|
||||
return _queue;
|
||||
}
|
||||
|
||||
void stop() {
|
||||
std::lock_guard lg{_lock};
|
||||
|
||||
_continue = false;
|
||||
|
||||
_cv.notify_all();
|
||||
}
|
||||
|
||||
[[nodiscard]] bool running() const {
|
||||
return _continue;
|
||||
}
|
||||
|
||||
private:
|
||||
|
||||
bool _continue{true};
|
||||
|
||||
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 };
|
||||
auto c = owner->_count.fetch_sub(1, std::memory_order_acquire);
|
||||
|
||||
if(c - 1 == 0) {
|
||||
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() {
|
||||
auto c = _count.fetch_add(1, std::memory_order_acquire);
|
||||
if(!c) {
|
||||
std::lock_guard lg { _lock };
|
||||
|
||||
new(_object_buf.data()) element_type;
|
||||
if(_construct(*reinterpret_cast<element_type*>(_object_buf.data()))) {
|
||||
return ptr_t { nullptr };
|
||||
}
|
||||
}
|
||||
|
||||
return ptr_t { this };
|
||||
}
|
||||
private:
|
||||
construct_f _construct;
|
||||
destruct_f _destruct;
|
||||
|
||||
std::array<std::uint8_t, sizeof(element_type)> _object_buf;
|
||||
|
||||
std::atomic<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>;
|
||||
}
|
||||
|
||||
#endif //SUNSHINE_THREAD_SAFE_H
|
||||
@@ -1,658 +0,0 @@
|
||||
#ifndef UTILITY_H
|
||||
#define UTILITY_H
|
||||
|
||||
#include <variant>
|
||||
#include <vector>
|
||||
#include <memory>
|
||||
#include <type_traits>
|
||||
#include <algorithm>
|
||||
#include <optional>
|
||||
#include <mutex>
|
||||
#include <condition_variable>
|
||||
#include <string_view>
|
||||
|
||||
#define KITTY_WHILE_LOOP(x, y, z) { x;while(y) z }
|
||||
#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_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_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_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)
|
||||
|
||||
namespace util {
|
||||
|
||||
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>
|
||||
static constexpr auto instantiation_of_v = __instantiation_of<X, T, Y...>::value;
|
||||
|
||||
template<bool V, class X, class Y>
|
||||
struct __either;
|
||||
|
||||
template<class X, class Y>
|
||||
struct __either<true, X, Y> {
|
||||
using type = X;
|
||||
};
|
||||
|
||||
template<class X, class Y>
|
||||
struct __either<false, X, Y> {
|
||||
using type = 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 T>
|
||||
class FailGuard {
|
||||
public:
|
||||
FailGuard() = delete;
|
||||
FailGuard(T && f) noexcept : _func { std::forward<T>(f) } {}
|
||||
FailGuard(FailGuard &&other) noexcept : _func { std::move(other._func) } {
|
||||
this->failure = other.failure;
|
||||
|
||||
other.failure = false;
|
||||
}
|
||||
|
||||
FailGuard(const FailGuard &) = delete;
|
||||
|
||||
FailGuard &operator=(const FailGuard &) = delete;
|
||||
FailGuard &operator=(FailGuard &&other) = delete;
|
||||
|
||||
~FailGuard() noexcept {
|
||||
if(failure) {
|
||||
_func();
|
||||
}
|
||||
}
|
||||
|
||||
void disable() { failure = false; }
|
||||
bool failure { true };
|
||||
private:
|
||||
T _func;
|
||||
};
|
||||
|
||||
template<class T>
|
||||
[[nodiscard]] auto fail_guard(T && f) {
|
||||
return FailGuard<T> { std::forward<T>(f) };
|
||||
}
|
||||
|
||||
template<class T>
|
||||
void append_struct(std::vector<uint8_t> &buf, const T &_struct) {
|
||||
constexpr size_t data_len = sizeof(_struct);
|
||||
|
||||
buf.reserve(data_len);
|
||||
|
||||
auto *data = (uint8_t *) & _struct;
|
||||
|
||||
for (size_t x = 0; x < data_len; ++x) {
|
||||
buf.push_back(data[x]);
|
||||
}
|
||||
}
|
||||
|
||||
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();) {
|
||||
*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();) {
|
||||
*it++ = _bits[*data / 16];
|
||||
*it++ = _bits[*data++ % 16];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
char *begin() { return _hex; }
|
||||
char *end() { return _hex + sizeof(elem_type) * 2; }
|
||||
|
||||
const char *begin() const { return _hex; }
|
||||
const char *end() const { return _hex + sizeof(elem_type) * 2; }
|
||||
|
||||
const char *cbegin() const { return _hex; }
|
||||
const char *cend() const { return _hex + sizeof(elem_type) * 2; }
|
||||
|
||||
std::string to_string() const {
|
||||
return { begin(), end() };
|
||||
}
|
||||
|
||||
std::string_view to_string_view() const {
|
||||
return { begin(), sizeof(elem_type) * 2 };
|
||||
}
|
||||
};
|
||||
|
||||
template<class T>
|
||||
Hex<T> hex(const T &elem, bool rev = false) {
|
||||
return Hex<T>(elem, rev);
|
||||
}
|
||||
|
||||
template<class It>
|
||||
std::string hex_vec(It begin, It end, bool rev = false) {
|
||||
auto str_size = 2*std::distance(begin, end);
|
||||
|
||||
|
||||
std::string hex;
|
||||
hex.resize(str_size);
|
||||
|
||||
const char _bits[16] {
|
||||
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'
|
||||
};
|
||||
|
||||
if(rev) {
|
||||
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);) {
|
||||
*it++ = _bits[((uint8_t)*end) / 16];
|
||||
*it++ = _bits[((uint8_t)*end--) % 16];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return hex;
|
||||
}
|
||||
|
||||
template<class C>
|
||||
std::string hex_vec(C&& c, bool rev = false) {
|
||||
return hex_vec(std::begin(c), std::end(c), rev);
|
||||
}
|
||||
|
||||
template<class T>
|
||||
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 {
|
||||
if(isdigit(ch)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
ch |= shift_bit;
|
||||
|
||||
if('a' > ch || ch > 'z') {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
auto buf_size = std::count_if(std::begin(hex), std::end(hex), is_convertable) / 2;
|
||||
if(buf_size != sizeof(T)) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
const char *data = hex.data() + hex.size() -1;
|
||||
|
||||
auto convert = [] (char ch) -> std::uint8_t {
|
||||
if(ch >= '0' && ch <= '9') {
|
||||
return (std::uint8_t)ch - '0';
|
||||
}
|
||||
|
||||
return (std::uint8_t)(ch | (char)32) - 'a' + (char)10;
|
||||
};
|
||||
|
||||
for(auto &el : buf) {
|
||||
while(!is_convertable(*data)) { --data; }
|
||||
std::uint8_t ch_r = convert(*data--);
|
||||
|
||||
while(!is_convertable(*data)) { --data; }
|
||||
std::uint8_t ch_l = convert(*data--);
|
||||
|
||||
el = (ch_l << 4) | ch_r;
|
||||
}
|
||||
|
||||
if(rev) {
|
||||
std::reverse(std::begin(buf), std::end(buf));
|
||||
}
|
||||
|
||||
return *reinterpret_cast<T *>(buf);
|
||||
}
|
||||
|
||||
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 {
|
||||
if(isdigit(ch)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
ch |= shift_bit;
|
||||
|
||||
if('a' > ch || ch > 'z') {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
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;
|
||||
|
||||
auto convert = [] (char ch) -> std::uint8_t {
|
||||
if(ch >= '0' && ch <= '9') {
|
||||
return (std::uint8_t)ch - '0';
|
||||
}
|
||||
|
||||
return (std::uint8_t)(ch | (char)32) - 'a' + (char)10;
|
||||
};
|
||||
|
||||
for(auto &el : buf) {
|
||||
while(!is_convertable(*data)) { --data; }
|
||||
std::uint8_t ch_r = convert(*data--);
|
||||
|
||||
while(!is_convertable(*data)) { --data; }
|
||||
std::uint8_t ch_l = convert(*data--);
|
||||
|
||||
el = (ch_l << 4) | ch_r;
|
||||
}
|
||||
|
||||
if(rev) {
|
||||
std::reverse(std::begin(buf), std::end(buf));
|
||||
}
|
||||
|
||||
return buf;
|
||||
}
|
||||
|
||||
template<class T>
|
||||
class hash {
|
||||
public:
|
||||
using value_type = T;
|
||||
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) });
|
||||
}
|
||||
};
|
||||
|
||||
template<class T>
|
||||
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);
|
||||
}
|
||||
|
||||
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;
|
||||
while(begin != --end) {
|
||||
res += (std::int64_t)(*end - '0') * mul;
|
||||
|
||||
mul *= 10;
|
||||
}
|
||||
|
||||
return *begin != '-' ? res + (std::int64_t)(*begin - '0') * mul : -res;
|
||||
}
|
||||
|
||||
inline std::int64_t from_view(const std::string_view &number) {
|
||||
return from_chars(std::begin(number), std::end(number));
|
||||
}
|
||||
|
||||
template<class X, class Y>
|
||||
class Either : public std::variant<X, Y> {
|
||||
public:
|
||||
using std::variant<X, Y>::variant;
|
||||
|
||||
constexpr bool has_left() const {
|
||||
return std::holds_alternative<X>(*this);
|
||||
}
|
||||
constexpr bool has_right() const {
|
||||
return std::holds_alternative<Y>(*this);
|
||||
}
|
||||
|
||||
X &left() {
|
||||
return std::get<X>(*this);
|
||||
}
|
||||
|
||||
Y &right() {
|
||||
return std::get<Y>(*this);
|
||||
}
|
||||
|
||||
const X &left() const {
|
||||
return std::get<X>(*this);
|
||||
}
|
||||
|
||||
const Y &right() const {
|
||||
return std::get<Y>(*this);
|
||||
}
|
||||
};
|
||||
|
||||
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);
|
||||
|
||||
return *this;
|
||||
};
|
||||
|
||||
explicit buffer_t(size_t elements) : _els { elements }, _buf { std::make_unique<T[]>(elements) } {}
|
||||
explicit buffer_t(size_t elements, const T &t) : _els { elements }, _buf { std::make_unique<T[]>(elements) } {
|
||||
std::fill_n(_buf.get(), elements, t);
|
||||
}
|
||||
|
||||
T &operator[](size_t el) {
|
||||
return _buf[el];
|
||||
}
|
||||
|
||||
const T &operator[](size_t el) const {
|
||||
return _buf[el];
|
||||
}
|
||||
|
||||
size_t size() const {
|
||||
return _els;
|
||||
}
|
||||
|
||||
void fake_resize(std::size_t els) {
|
||||
_els = els;
|
||||
}
|
||||
|
||||
T *begin() {
|
||||
return _buf.get();
|
||||
}
|
||||
|
||||
const T *begin() const {
|
||||
return _buf.get();
|
||||
}
|
||||
|
||||
T *end() {
|
||||
return _buf.get() + _els;
|
||||
}
|
||||
|
||||
const T *end() const {
|
||||
return _buf.get() + _els;
|
||||
}
|
||||
|
||||
private:
|
||||
size_t _els;
|
||||
std::unique_ptr<T[]> _buf;
|
||||
};
|
||||
|
||||
|
||||
template<class T>
|
||||
T either(std::optional<T> &&l, T &&r) {
|
||||
if(l) {
|
||||
return std::move(*l);
|
||||
}
|
||||
|
||||
return std::forward<T>(r);
|
||||
}
|
||||
|
||||
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__)
|
||||
// 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)
|
||||
// It's a little-endian target architecture
|
||||
little = true,
|
||||
#else
|
||||
#error "Unknown Endianness"
|
||||
#endif
|
||||
big = !little
|
||||
};
|
||||
};
|
||||
|
||||
template<class T, class S = void>
|
||||
struct endian_helper { };
|
||||
|
||||
template<class T>
|
||||
struct endian_helper<T, std::enable_if_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);
|
||||
|
||||
std::reverse(data, data + sizeof(x));
|
||||
}
|
||||
|
||||
return x;
|
||||
}
|
||||
|
||||
static inline T little(T x) {
|
||||
if constexpr (endianness<T>::big) {
|
||||
uint8_t *data = reinterpret_cast<uint8_t*>(&x);
|
||||
|
||||
std::reverse(data, data + sizeof(x));
|
||||
}
|
||||
|
||||
return x;
|
||||
}
|
||||
};
|
||||
|
||||
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;
|
||||
|
||||
if constexpr (endianness<T>::big) {
|
||||
auto *data = reinterpret_cast<uint8_t*>(&*x);
|
||||
|
||||
std::reverse(data, data + sizeof(*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);
|
||||
|
||||
std::reverse(data, data + sizeof(*x));
|
||||
}
|
||||
|
||||
return x;
|
||||
}
|
||||
};
|
||||
|
||||
template<class T>
|
||||
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 */
|
||||
#endif
|
||||
@@ -1,79 +0,0 @@
|
||||
//
|
||||
// Created by loki on 8-2-19.
|
||||
//
|
||||
|
||||
#ifndef T_MAN_UUID_H
|
||||
#define T_MAN_UUID_H
|
||||
|
||||
#include <random>
|
||||
|
||||
namespace util {
|
||||
union uuid_t {
|
||||
std::uint8_t b8[16];
|
||||
std::uint16_t b16[8];
|
||||
std::uint32_t b32[4];
|
||||
std::uint64_t b64[2];
|
||||
|
||||
static uuid_t generate(std::default_random_engine &engine) {
|
||||
std::uniform_int_distribution<std::uint8_t> dist(0, std::numeric_limits<std::uint8_t>::max());
|
||||
|
||||
uuid_t buf;
|
||||
for (auto &el : buf.b8) {
|
||||
el = dist(engine);
|
||||
}
|
||||
|
||||
buf.b8[7] &= (std::uint8_t) 0b00101111;
|
||||
buf.b8[9] &= (std::uint8_t) 0b10011111;
|
||||
|
||||
return buf;
|
||||
}
|
||||
|
||||
static uuid_t generate() {
|
||||
std::random_device r;
|
||||
|
||||
std::default_random_engine engine{r()};
|
||||
|
||||
return generate(engine);
|
||||
}
|
||||
|
||||
[[nodiscard]] std::string string() const {
|
||||
std::string result;
|
||||
|
||||
result.reserve(sizeof(uuid_t) * 2 + 4);
|
||||
|
||||
auto hex = util::hex(*this, true);
|
||||
auto hex_view = hex.to_string_view();
|
||||
|
||||
std::string_view slices[] = {
|
||||
hex_view.substr(0, 8),
|
||||
hex_view.substr(8, 4),
|
||||
hex_view.substr(12, 4),
|
||||
hex_view.substr(16, 4)
|
||||
};
|
||||
auto last_slice = hex_view.substr(20, 12);
|
||||
|
||||
for(auto &slice : slices) {
|
||||
std::copy(std::begin(slice), std::end(slice), std::back_inserter(result));
|
||||
|
||||
result.push_back('-');
|
||||
}
|
||||
|
||||
std::copy(std::begin(last_slice), std::end(last_slice), std::back_inserter(result));
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
constexpr bool operator==(const uuid_t &other) const {
|
||||
return b64[0] == other.b64[0] && b64[1] == other.b64[1];
|
||||
}
|
||||
|
||||
constexpr bool operator<(const uuid_t &other) const {
|
||||
return (b64[0] < other.b64[0] || (b64[0] == other.b64[0] && b64[1] < other.b64[1]));
|
||||
}
|
||||
|
||||
constexpr bool operator>(const uuid_t &other) const {
|
||||
return (b64[0] > other.b64[0] || (b64[0] == other.b64[0] && b64[1] > other.b64[1]));
|
||||
}
|
||||
};
|
||||
}
|
||||
#endif //T_MAN_UUID_H
|
||||
@@ -1,425 +0,0 @@
|
||||
//
|
||||
// Created by loki on 6/6/19.
|
||||
//
|
||||
|
||||
#include <atomic>
|
||||
#include <thread>
|
||||
|
||||
extern "C" {
|
||||
#include <libswscale/swscale.h>
|
||||
}
|
||||
|
||||
#include "platform/common.h"
|
||||
#include "thread_pool.h"
|
||||
#include "config.h"
|
||||
#include "video.h"
|
||||
#include "main.h"
|
||||
|
||||
namespace video {
|
||||
using namespace std::literals;
|
||||
|
||||
void free_ctx(AVCodecContext *ctx) {
|
||||
avcodec_free_context(&ctx);
|
||||
}
|
||||
|
||||
void free_frame(AVFrame *frame) {
|
||||
av_frame_free(&frame);
|
||||
}
|
||||
|
||||
void free_packet(AVPacket *packet) {
|
||||
av_packet_free(&packet);
|
||||
}
|
||||
|
||||
using ctx_t = util::safe_ptr<AVCodecContext, free_ctx>;
|
||||
using frame_t = util::safe_ptr<AVFrame, free_frame>;
|
||||
using sws_t = util::safe_ptr<SwsContext, sws_freeContext>;
|
||||
using img_event_t = std::shared_ptr<safe::event_t<std::shared_ptr<platf::img_t>>>;
|
||||
|
||||
struct capture_ctx_t {
|
||||
img_event_t images;
|
||||
std::chrono::nanoseconds delay;
|
||||
};
|
||||
|
||||
struct capture_thread_ctx_t {
|
||||
std::shared_ptr<safe::queue_t<capture_ctx_t>> capture_ctx_queue;
|
||||
std::thread capture_thread;
|
||||
};
|
||||
|
||||
[[nodiscard]] auto open_codec(ctx_t &ctx, AVCodec *codec, AVDictionary **options) {
|
||||
avcodec_open2(ctx.get(), codec, options);
|
||||
|
||||
return util::fail_guard([&]() {
|
||||
avcodec_close(ctx.get());
|
||||
});
|
||||
}
|
||||
|
||||
int capture_display(platf::img_t *img, std::unique_ptr<platf::display_t> &disp) {
|
||||
auto status = disp->snapshot(img, display_cursor);
|
||||
switch (status) {
|
||||
case platf::capture_e::reinit: {
|
||||
// We try this twice, in case we still get an error on reinitialization
|
||||
for(int x = 0; x < 2; ++x) {
|
||||
disp.reset();
|
||||
disp = platf::display();
|
||||
|
||||
if(disp) {
|
||||
break;
|
||||
}
|
||||
|
||||
std::this_thread::sleep_for(200ms);
|
||||
}
|
||||
|
||||
if(!disp) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
case platf::capture_e::error:
|
||||
return -1;
|
||||
case platf::capture_e::timeout:
|
||||
return 0;
|
||||
case platf::capture_e::ok:
|
||||
return 1;
|
||||
default:
|
||||
BOOST_LOG(error) << "Unrecognized capture status ["sv << (int)status << ']';
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
void captureThread(std::shared_ptr<safe::queue_t<capture_ctx_t>> capture_ctx_queue) {
|
||||
std::vector<capture_ctx_t> capture_ctxs;
|
||||
|
||||
auto fg = util::fail_guard([&]() {
|
||||
capture_ctx_queue->stop();
|
||||
|
||||
// Stop all sessions listening to this thread
|
||||
for(auto &capture_ctx : capture_ctxs) {
|
||||
capture_ctx.images->stop();
|
||||
}
|
||||
for(auto &capture_ctx : capture_ctx_queue->unsafe()) {
|
||||
capture_ctx.images->stop();
|
||||
}
|
||||
});
|
||||
|
||||
std::chrono::nanoseconds delay = 1s;
|
||||
|
||||
auto disp = platf::display();
|
||||
while(capture_ctx_queue->running()) {
|
||||
while(capture_ctx_queue->peek()) {
|
||||
capture_ctxs.emplace_back(std::move(*capture_ctx_queue->pop()));
|
||||
|
||||
delay = std::min(delay, capture_ctxs.back().delay);
|
||||
}
|
||||
|
||||
std::shared_ptr<platf::img_t> img = disp->alloc_img();
|
||||
auto result = capture_display(img.get(), disp);
|
||||
if(result < 0) {
|
||||
return;
|
||||
}
|
||||
if(!result) {
|
||||
continue;
|
||||
}
|
||||
|
||||
KITTY_WHILE_LOOP(auto capture_ctx = std::begin(capture_ctxs), capture_ctx != std::end(capture_ctxs), {
|
||||
if(!capture_ctx->images->running()) {
|
||||
auto tmp_delay = capture_ctx->delay;
|
||||
capture_ctx = capture_ctxs.erase(capture_ctx);
|
||||
|
||||
if(tmp_delay == delay) {
|
||||
delay = std::min_element(std::begin(capture_ctxs), std::end(capture_ctxs), [](const auto &l, const auto &r) {
|
||||
return l.delay < r.delay;
|
||||
})->delay;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
capture_ctx->images->raise(img);
|
||||
++capture_ctx;
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
void encode(int64_t frame, ctx_t &ctx, sws_t &sws, frame_t &yuv_frame, platf::img_t &img, packet_queue_t &packets, void *channel_data) {
|
||||
av_frame_make_writable(yuv_frame.get());
|
||||
|
||||
const int linesizes[2] {
|
||||
img.row_pitch, 0
|
||||
};
|
||||
|
||||
auto data = img.data;
|
||||
int ret = sws_scale(sws.get(), (uint8_t*const*)&data, linesizes, 0, img.height, yuv_frame->data, yuv_frame->linesize);
|
||||
|
||||
if(ret <= 0) {
|
||||
exit(1);
|
||||
}
|
||||
|
||||
yuv_frame->pts = frame;
|
||||
|
||||
/* send the frame to the encoder */
|
||||
ret = avcodec_send_frame(ctx.get(), yuv_frame.get());
|
||||
if (ret < 0) {
|
||||
BOOST_LOG(fatal) << "Could not send a frame for encoding"sv;
|
||||
log_flush();
|
||||
std::abort();
|
||||
}
|
||||
|
||||
while (ret >= 0) {
|
||||
auto packet = std::make_unique<packet_t::element_type>(nullptr);
|
||||
|
||||
ret = avcodec_receive_packet(ctx.get(), packet.get());
|
||||
if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) {
|
||||
return;
|
||||
}
|
||||
else if (ret < 0) {
|
||||
BOOST_LOG(fatal) << "Could not encode video packet"sv;
|
||||
log_flush();
|
||||
std::abort();
|
||||
}
|
||||
|
||||
packet->channel_data = channel_data;
|
||||
packets->raise(std::move(packet));
|
||||
}
|
||||
}
|
||||
|
||||
int start_capture(capture_thread_ctx_t &capture_thread_ctx) {
|
||||
capture_thread_ctx.capture_ctx_queue = std::make_shared<safe::queue_t<capture_ctx_t>>();
|
||||
|
||||
capture_thread_ctx.capture_thread = std::thread {
|
||||
captureThread, capture_thread_ctx.capture_ctx_queue
|
||||
};
|
||||
|
||||
return 0;
|
||||
}
|
||||
void end_capture(capture_thread_ctx_t &capture_thread_ctx) {
|
||||
capture_thread_ctx.capture_ctx_queue->stop();
|
||||
|
||||
capture_thread_ctx.capture_thread.join();
|
||||
}
|
||||
|
||||
void capture(
|
||||
safe::signal_t *shutdown_event,
|
||||
packet_queue_t packets,
|
||||
idr_event_t idr_events,
|
||||
config_t config,
|
||||
void *channel_data) {
|
||||
|
||||
int framerate = config.framerate;
|
||||
|
||||
auto images = std::make_shared<img_event_t::element_type>();
|
||||
// Keep a reference counter to ensure the capture thread only runs when other threads have a reference to the capture thread
|
||||
static auto capture_thread = safe::make_shared<capture_thread_ctx_t>(start_capture, end_capture);
|
||||
auto ref = capture_thread.ref();
|
||||
if(!ref) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto delay = std::chrono::floor<std::chrono::nanoseconds>(1s) / framerate;
|
||||
ref->capture_ctx_queue->raise(capture_ctx_t {
|
||||
images, delay
|
||||
});
|
||||
|
||||
if(!ref->capture_ctx_queue->running()) {
|
||||
return;
|
||||
}
|
||||
|
||||
AVCodec *codec;
|
||||
|
||||
if(config.videoFormat == 0) {
|
||||
codec = avcodec_find_encoder(AV_CODEC_ID_H264);
|
||||
}
|
||||
else {
|
||||
codec = avcodec_find_encoder(AV_CODEC_ID_HEVC);
|
||||
}
|
||||
|
||||
ctx_t ctx{avcodec_alloc_context3(codec)};
|
||||
|
||||
frame_t yuv_frame{av_frame_alloc()};
|
||||
|
||||
ctx->width = config.width;
|
||||
ctx->height = config.height;
|
||||
ctx->time_base = AVRational{1, framerate};
|
||||
ctx->framerate = AVRational{framerate, 1};
|
||||
|
||||
if(config.videoFormat == 0) {
|
||||
ctx->profile = FF_PROFILE_H264_HIGH;
|
||||
}
|
||||
else if(config.dynamicRange == 0) {
|
||||
ctx->profile = FF_PROFILE_HEVC_MAIN;
|
||||
}
|
||||
else {
|
||||
ctx->profile = FF_PROFILE_HEVC_MAIN_10;
|
||||
}
|
||||
|
||||
if(config.dynamicRange == 0) {
|
||||
ctx->pix_fmt = AV_PIX_FMT_YUV420P;
|
||||
}
|
||||
else {
|
||||
ctx->pix_fmt = AV_PIX_FMT_YUV420P10;
|
||||
}
|
||||
|
||||
ctx->color_range = (config.encoderCscMode & 0x1) ? AVCOL_RANGE_JPEG : AVCOL_RANGE_MPEG;
|
||||
|
||||
int swsColorSpace;
|
||||
switch (config.encoderCscMode >> 1) {
|
||||
case 0:
|
||||
default:
|
||||
// Rec. 601
|
||||
ctx->color_primaries = AVCOL_PRI_SMPTE170M;
|
||||
ctx->color_trc = AVCOL_TRC_SMPTE170M;
|
||||
ctx->colorspace = AVCOL_SPC_SMPTE170M;
|
||||
swsColorSpace = SWS_CS_SMPTE170M;
|
||||
break;
|
||||
|
||||
case 1:
|
||||
// Rec. 709
|
||||
ctx->color_primaries = AVCOL_PRI_BT709;
|
||||
ctx->color_trc = AVCOL_TRC_BT709;
|
||||
ctx->colorspace = AVCOL_SPC_BT709;
|
||||
swsColorSpace = SWS_CS_ITU709;
|
||||
break;
|
||||
|
||||
case 2:
|
||||
// Rec. 2020
|
||||
ctx->color_primaries = AVCOL_PRI_BT2020;
|
||||
ctx->color_trc = AVCOL_TRC_BT2020_10;
|
||||
ctx->colorspace = AVCOL_SPC_BT2020_NCL;
|
||||
swsColorSpace = SWS_CS_BT2020;
|
||||
break;
|
||||
}
|
||||
|
||||
// B-frames delay decoder output, so never use them
|
||||
ctx->max_b_frames = 0;
|
||||
|
||||
// Use an infinite GOP length since I-frames are generated on demand
|
||||
ctx->gop_size = std::numeric_limits<int>::max();
|
||||
ctx->keyint_min = ctx->gop_size;
|
||||
|
||||
// Some client decoders have limits on the number of reference frames
|
||||
ctx->refs = config.numRefFrames;
|
||||
|
||||
// Clients will request for the fewest slices per frame to get the
|
||||
// most efficient encode, but we may want to provide more slices than
|
||||
// requested to ensure we have enough parallelism for good performance.
|
||||
ctx->slices = std::max(config.slicesPerFrame, config::video.min_threads);
|
||||
ctx->thread_type = FF_THREAD_SLICE;
|
||||
ctx->thread_count = ctx->slices;
|
||||
|
||||
AVDictionary *options {nullptr};
|
||||
av_dict_set(&options, "preset", config::video.preset.c_str(), 0);
|
||||
av_dict_set(&options, "tune", config::video.tune.c_str(), 0);
|
||||
|
||||
if(config.bitrate > 500) {
|
||||
config.bitrate *= 1000;
|
||||
ctx->rc_max_rate = config.bitrate;
|
||||
ctx->rc_buffer_size = config.bitrate / 100;
|
||||
ctx->bit_rate = config.bitrate;
|
||||
ctx->rc_min_rate = config.bitrate;
|
||||
}
|
||||
else if(config::video.crf != 0) {
|
||||
av_dict_set_int(&options, "crf", config::video.crf, 0);
|
||||
}
|
||||
else {
|
||||
av_dict_set_int(&options, "qp", config::video.qp, 0);
|
||||
}
|
||||
|
||||
if(config.videoFormat == 1) {
|
||||
// x265's Info SEI is so long that it causes the IDR picture data to be
|
||||
// kicked to the 2nd packet in the frame, breaking Moonlight's parsing logic.
|
||||
// It also looks like gop_size isn't passed on to x265, so we have to set
|
||||
// 'keyint=-1' in the parameters ourselves.
|
||||
av_dict_set(&options, "x265-params", "info=0:keyint=-1", 0);
|
||||
}
|
||||
|
||||
ctx->flags |= (AV_CODEC_FLAG_CLOSED_GOP | AV_CODEC_FLAG_LOW_DELAY);
|
||||
ctx->flags2 |= AV_CODEC_FLAG2_FAST;
|
||||
|
||||
auto lg = open_codec(ctx, codec, &options);
|
||||
|
||||
yuv_frame->format = ctx->pix_fmt;
|
||||
yuv_frame->width = ctx->width;
|
||||
yuv_frame->height = ctx->height;
|
||||
|
||||
av_frame_get_buffer(yuv_frame.get(), 0);
|
||||
|
||||
int64_t frame = 1;
|
||||
int64_t key_frame = 1;
|
||||
|
||||
auto img_width = 0;
|
||||
auto img_height = 0;
|
||||
|
||||
// Initiate scaling context with correct height and width
|
||||
sws_t sws;
|
||||
|
||||
// Temporary image to ensure something is send to Moonlight even if no frame has been captured yet.
|
||||
int dummy_data = 0;
|
||||
auto img = std::make_shared<platf::img_t>();
|
||||
img->row_pitch = 4;
|
||||
img->height = 1;
|
||||
img->width = 1;
|
||||
img->pixel_pitch = 4;
|
||||
img->data = (std::uint8_t*)&dummy_data;
|
||||
|
||||
auto next_frame = std::chrono::steady_clock::now();
|
||||
while(true) {
|
||||
if(shutdown_event->peek() || !images->running()) {
|
||||
break;
|
||||
}
|
||||
|
||||
if(idr_events->peek()) {
|
||||
yuv_frame->pict_type = AV_PICTURE_TYPE_I;
|
||||
|
||||
auto event = idr_events->pop();
|
||||
TUPLE_2D_REF(_, end, *event);
|
||||
|
||||
frame = end;
|
||||
key_frame = end + config.framerate;
|
||||
}
|
||||
else if(frame == key_frame) {
|
||||
yuv_frame->pict_type = AV_PICTURE_TYPE_I;
|
||||
}
|
||||
|
||||
std::this_thread::sleep_until(next_frame);
|
||||
next_frame += delay;
|
||||
|
||||
// When Moonlight request an IDR frame, send frames even if there is no new captured frame
|
||||
if(frame > (key_frame + config.framerate) || images->peek()) {
|
||||
if(auto tmp_img = images->pop(delay)) {
|
||||
img = std::move(tmp_img);
|
||||
}
|
||||
else if(images->running()) {
|
||||
continue;
|
||||
}
|
||||
else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
auto new_width = img->width;
|
||||
auto new_height = img->height;
|
||||
|
||||
if(img_width != new_width || img_height != new_height) {
|
||||
img_width = new_width;
|
||||
img_height = new_height;
|
||||
|
||||
sws.reset(
|
||||
sws_getContext(
|
||||
img_width, img_height, AV_PIX_FMT_BGR0,
|
||||
ctx->width, ctx->height, ctx->pix_fmt,
|
||||
SWS_LANCZOS | SWS_ACCURATE_RND,
|
||||
nullptr, nullptr, nullptr));
|
||||
|
||||
sws_setColorspaceDetails(sws.get(), sws_getCoefficients(SWS_CS_DEFAULT), 0,
|
||||
sws_getCoefficients(swsColorSpace), config.encoderCscMode & 0x1,
|
||||
0, 1 << 16, 1 << 16);
|
||||
}
|
||||
|
||||
encode(frame++, ctx, sws, yuv_frame, *img, packets, channel_data);
|
||||
|
||||
yuv_frame->pict_type = AV_PICTURE_TYPE_NONE;
|
||||
}
|
||||
|
||||
images->stop();
|
||||
}
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
//
|
||||
// Created by loki on 6/9/19.
|
||||
//
|
||||
|
||||
#ifndef SUNSHINE_VIDEO_H
|
||||
#define SUNSHINE_VIDEO_H
|
||||
|
||||
#include "thread_safe.h"
|
||||
#include "platform/common.h"
|
||||
|
||||
extern "C" {
|
||||
#include <libavcodec/avcodec.h>
|
||||
}
|
||||
|
||||
struct AVPacket;
|
||||
namespace video {
|
||||
void free_packet(AVPacket *packet);
|
||||
|
||||
struct packet_raw_t : public AVPacket {
|
||||
template<class P>
|
||||
explicit packet_raw_t(P *user_data) : channel_data { user_data } {
|
||||
av_init_packet(this);
|
||||
}
|
||||
|
||||
explicit packet_raw_t(std::nullptr_t null) : channel_data { nullptr } {
|
||||
av_init_packet(this);
|
||||
}
|
||||
|
||||
~packet_raw_t() {
|
||||
av_packet_unref(this);
|
||||
}
|
||||
|
||||
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;
|
||||
int height;
|
||||
int framerate;
|
||||
int bitrate;
|
||||
int slicesPerFrame;
|
||||
int numRefFrames;
|
||||
int encoderCscMode;
|
||||
int videoFormat;
|
||||
int dynamicRange;
|
||||
};
|
||||
|
||||
void capture(
|
||||
safe::signal_t *shutdown_event,
|
||||
packet_queue_t packets,
|
||||
idr_event_t idr_events,
|
||||
config_t config,
|
||||
void *channel_data);
|
||||
}
|
||||
|
||||
#endif //SUNSHINE_VIDEO_H
|
||||
@@ -1,19 +0,0 @@
|
||||
# the name of the target operating system
|
||||
SET(CMAKE_SYSTEM_NAME Windows)
|
||||
|
||||
# which compilers to use for C and C++
|
||||
SET(CMAKE_C_COMPILER x86_64-w64-mingw32-gcc)
|
||||
SET(CMAKE_CXX_COMPILER x86_64-w64-mingw32-g++)
|
||||
SET(CMAKE_RC_COMPILER x86_64-w64-mingw32-windres)
|
||||
|
||||
# here is the target environment located
|
||||
SET(CMAKE_FIND_ROOT_PATH /usr/x86_64-w64-mingw32)
|
||||
#SET(CMAKE_SYSROOT ${CMAKE_FIND_ROOT_PATH})
|
||||
#SET(CMAKE_EXE_LINKER_FLAGS "-static ${CMAKE_EXE_LINKER_FLAGS}")
|
||||
|
||||
# adjust the default behaviour of the FIND_XXX() commands:
|
||||
# search headers and libraries in the target environment, search
|
||||
# programs in the host environment
|
||||
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
|
||||
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
|
||||
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
|
||||
@@ -1,21 +0,0 @@
|
||||
cmake_minimum_required(VERSION 2.8)
|
||||
|
||||
project(sunshine_tools)
|
||||
|
||||
include_directories(${CMAKE_SOURCE_DIR})
|
||||
|
||||
add_executable(dxgi-info dxgi.cpp)
|
||||
set_target_properties(dxgi-info PROPERTIES CXX_STANDARD 17)
|
||||
target_link_libraries(dxgi-info
|
||||
${CMAKE_THREAD_LIBS_INIT}
|
||||
dxgi
|
||||
${PLATFORM_LIBRARIES})
|
||||
target_compile_options(dxgi-info PRIVATE ${SUNSHINE_COMPILE_OPTIONS})
|
||||
|
||||
add_executable(audio-info audio.cpp)
|
||||
set_target_properties(audio-info PROPERTIES CXX_STANDARD 17)
|
||||
target_link_libraries(audio-info
|
||||
${CMAKE_THREAD_LIBS_INIT}
|
||||
ksuser
|
||||
${PLATFORM_LIBRARIES})
|
||||
target_compile_options(audio-info PRIVATE ${SUNSHINE_COMPILE_OPTIONS})
|
||||
-254
@@ -1,254 +0,0 @@
|
||||
//
|
||||
// Created by loki on 1/24/20.
|
||||
//
|
||||
|
||||
#include <roapi.h>
|
||||
#include <mmdeviceapi.h>
|
||||
#include <audioclient.h>
|
||||
|
||||
#include <synchapi.h>
|
||||
|
||||
#define INITGUID
|
||||
#include <propkeydef.h>
|
||||
#undef INITGUID
|
||||
|
||||
#include <iostream>
|
||||
|
||||
#include "sunshine/utility.h"
|
||||
|
||||
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);
|
||||
|
||||
using namespace std::literals;
|
||||
const CLSID CLSID_MMDeviceEnumerator = __uuidof(MMDeviceEnumerator);
|
||||
const IID IID_IMMDeviceEnumerator = __uuidof(IMMDeviceEnumerator);
|
||||
const IID IID_IAudioClient = __uuidof(IAudioClient);
|
||||
const IID IID_IAudioCaptureClient = __uuidof(IAudioCaptureClient);
|
||||
|
||||
int device_state_filter = DEVICE_STATE_ACTIVE;
|
||||
namespace 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 collection_t = util::safe_ptr<IMMDeviceCollection, Release<IMMDeviceCollection>>;
|
||||
using prop_t = util::safe_ptr<IPropertyStore, Release<IPropertyStore>>;
|
||||
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 wstring_t = util::safe_ptr<WCHAR, co_task_free<WCHAR>>;
|
||||
|
||||
using handle_t = util::safe_ptr_v2<void, BOOL, CloseHandle>;
|
||||
|
||||
class prop_var_t {
|
||||
public:
|
||||
prop_var_t() {
|
||||
PropVariantInit(&prop);
|
||||
}
|
||||
|
||||
~prop_var_t() {
|
||||
PropVariantClear(&prop);
|
||||
}
|
||||
|
||||
PROPVARIANT prop;
|
||||
};
|
||||
|
||||
const wchar_t *no_null(const wchar_t *str) {
|
||||
return str ? str : L"Unknown";
|
||||
}
|
||||
void print_device(device_t &device) {
|
||||
HRESULT status;
|
||||
|
||||
audio::wstring_t::pointer wstring_p {};
|
||||
DWORD device_state;
|
||||
|
||||
device->GetState(&device_state);
|
||||
device->GetId(&wstring_p);
|
||||
audio::wstring_t wstring { wstring_p };
|
||||
|
||||
audio::prop_t::pointer prop_p {};
|
||||
device->OpenPropertyStore(STGM_READ, &prop_p);
|
||||
audio::prop_t prop { prop_p };
|
||||
|
||||
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);
|
||||
|
||||
if(!(device_state & device_state_filter)) {
|
||||
return;
|
||||
}
|
||||
|
||||
std::wstring device_state_string = L"Unknown"s;
|
||||
switch(device_state) {
|
||||
case DEVICE_STATE_ACTIVE:
|
||||
device_state_string = L"Active"s;
|
||||
break;
|
||||
case DEVICE_STATE_DISABLED:
|
||||
device_state_string = L"Disabled"s;
|
||||
break;
|
||||
case DEVICE_STATE_UNPLUGGED:
|
||||
device_state_string = L"Unplugged"s;
|
||||
break;
|
||||
case DEVICE_STATE_NOTPRESENT:
|
||||
device_state_string = L"Not present"s;
|
||||
break;
|
||||
}
|
||||
|
||||
std::wcout
|
||||
<< 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 << no_null((LPWSTR)adapter_friendly_name.prop.pszVal) << std::endl
|
||||
<< L"Device description : "sv << no_null((LPWSTR)device_desc.prop.pszVal) << std::endl
|
||||
<< L"Device state : "sv << device_state_string << std::endl << std::endl;
|
||||
|
||||
if(device_state != DEVICE_STATE_ACTIVE) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Ensure WaveFromat is compatible
|
||||
audio_client_t::pointer audio_client_p{};
|
||||
status = device->Activate(
|
||||
IID_IAudioClient,
|
||||
CLSCTX_ALL,
|
||||
nullptr,
|
||||
(void **) &audio_client_p);
|
||||
audio_client_t audio_client { audio_client_p };
|
||||
|
||||
if (FAILED(status)) {
|
||||
std::cout << "Couldn't activate Device: [0x"sv << util::hex(status).to_string_view() << ']' << std::endl;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
wave_format_t::pointer wave_format_p{};
|
||||
status = audio_client->GetMixFormat(&wave_format_p);
|
||||
wave_format_t wave_format { wave_format_p };
|
||||
|
||||
if (FAILED(status)) {
|
||||
std::cout << "Couldn't acquire Wave Format [0x"sv << util::hex(status).to_string_view() << ']' << std::endl;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
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)) {
|
||||
break;
|
||||
}
|
||||
|
||||
std::cout << "Unsupported Sub Format for WAVE_FORMAT_EXTENSIBLE: [0x"sv << util::hex(wave_ex->SubFormat).to_string_view() << ']' << std::endl;
|
||||
}
|
||||
default:
|
||||
std::cout << "Unsupported Wave Format: [0x"sv << util::hex(wave_format->wFormatTag).to_string_view() << ']' << std::endl;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
void print_help() {
|
||||
std::cout
|
||||
<< "==== Help ===="sv << std::endl
|
||||
<< "Usage:"sv << std::endl
|
||||
<< " audio-info [Active|Disabled|Unplugged|Not-Present]" << std::endl;
|
||||
}
|
||||
|
||||
int main(int argc, char *argv[]) {
|
||||
CoInitializeEx(nullptr, COINIT_MULTITHREADED | COINIT_SPEED_OVER_MEMORY);
|
||||
|
||||
auto fg = util::fail_guard([]() {
|
||||
CoUninitialize();
|
||||
});
|
||||
|
||||
if(argc > 1) {
|
||||
device_state_filter = 0;
|
||||
}
|
||||
|
||||
for(auto x = 1; x < argc; ++x) {
|
||||
for(auto p = argv[x]; *p != '\0'; ++p) {
|
||||
if(*p == ' ') {
|
||||
*p = '-';
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
*p = std::tolower(*p);
|
||||
}
|
||||
|
||||
if(argv[x] == "active"sv) {
|
||||
device_state_filter |= DEVICE_STATE_ACTIVE;
|
||||
}
|
||||
else if(argv[x] == "disabled"sv) {
|
||||
device_state_filter |= DEVICE_STATE_DISABLED;
|
||||
}
|
||||
else if(argv[x] == "unplugged"sv) {
|
||||
device_state_filter |= DEVICE_STATE_UNPLUGGED;
|
||||
}
|
||||
else if(argv[x] == "not-present"sv) {
|
||||
device_state_filter |= DEVICE_STATE_NOTPRESENT;
|
||||
}
|
||||
else {
|
||||
print_help();
|
||||
return 2;
|
||||
}
|
||||
}
|
||||
|
||||
HRESULT status;
|
||||
|
||||
audio::device_enum_t::pointer device_enum_p{};
|
||||
status = CoCreateInstance(
|
||||
CLSID_MMDeviceEnumerator,
|
||||
nullptr,
|
||||
CLSCTX_ALL,
|
||||
IID_IMMDeviceEnumerator,
|
||||
(void **) &device_enum_p);
|
||||
audio::device_enum_t device_enum { device_enum_p };
|
||||
|
||||
if (FAILED(status)) {
|
||||
std::cout << "Couldn't create Device Enumerator: [0x"sv << util::hex(status).to_string_view() << ']' << std::endl;
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
audio::collection_t::pointer collection_p {};
|
||||
status = device_enum->EnumAudioEndpoints(eRender, DEVICE_STATEMASK_ALL, &collection_p);
|
||||
audio::collection_t collection { collection_p };
|
||||
|
||||
if (FAILED(status)) {
|
||||
std::cout << "Couldn't enumerate: [0x"sv << util::hex(status).to_string_view() << ']' << std::endl;
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
UINT count;
|
||||
collection->GetCount(&count);
|
||||
|
||||
std::cout << "====== Found "sv << count << " potential audio devices ======"sv << std::endl;
|
||||
for(auto x = 0; x < count; ++x) {
|
||||
audio::device_t::pointer device_p {};
|
||||
collection->Item(x, &device_p);
|
||||
audio::device_t device { device_p };
|
||||
|
||||
audio::print_device(device);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
@@ -1,74 +0,0 @@
|
||||
//
|
||||
// Created by loki on 1/23/20.
|
||||
//
|
||||
|
||||
#include <dxgi.h>
|
||||
#include <d3dcommon.h>
|
||||
|
||||
#include <iostream>
|
||||
|
||||
#include "sunshine/utility.h"
|
||||
|
||||
using namespace std::literals;
|
||||
namespace dxgi {
|
||||
template<class T>
|
||||
void Release(T *dxgi) {
|
||||
dxgi->Release();
|
||||
}
|
||||
|
||||
using factory1_t = util::safe_ptr<IDXGIFactory1, Release<IDXGIFactory1>>;
|
||||
using adapter_t = util::safe_ptr<IDXGIAdapter1, Release<IDXGIAdapter1>>;
|
||||
using output_t = util::safe_ptr<IDXGIOutput, Release<IDXGIOutput>>;
|
||||
|
||||
}
|
||||
|
||||
int main(int argc, char *argv[]) {
|
||||
HRESULT status;
|
||||
|
||||
dxgi::factory1_t::pointer factory_p {};
|
||||
status = CreateDXGIFactory1(IID_IDXGIFactory1, (void**)&factory_p);
|
||||
dxgi::factory1_t factory { factory_p };
|
||||
if(FAILED(status)) {
|
||||
std::cout << "Failed to create DXGIFactory1 [0x"sv << util::hex(status).to_string_view() << ']' << std::endl;
|
||||
return -1;
|
||||
}
|
||||
|
||||
dxgi::adapter_t::pointer adapter_p {};
|
||||
for(int x = 0; factory->EnumAdapters1(x, &adapter_p) != DXGI_ERROR_NOT_FOUND; ++x) {
|
||||
dxgi::adapter_t adapter { adapter_p };
|
||||
|
||||
DXGI_ADAPTER_DESC1 adapter_desc;
|
||||
adapter->GetDesc1(&adapter_desc);
|
||||
|
||||
std::cout
|
||||
<< "====== ADAPTER ====="sv << std::endl;
|
||||
std::wcout
|
||||
<< L"Device Name : "sv << adapter_desc.Description << std::endl;
|
||||
std::cout
|
||||
<< "Device Vendor ID : 0x"sv << util::hex(adapter_desc.VendorId).to_string_view() << std::endl
|
||||
<< "Device Device ID : 0x"sv << util::hex(adapter_desc.DeviceId).to_string_view() << std::endl
|
||||
<< "Device Video Mem : "sv << adapter_desc.DedicatedVideoMemory / 1048576 << " MiB"sv << std::endl
|
||||
<< "Device Sys Mem : "sv << adapter_desc.DedicatedSystemMemory / 1048576 << " MiB"sv << std::endl
|
||||
<< "Share Sys Mem : "sv << adapter_desc.SharedSystemMemory / 1048576 << " MiB"sv << std::endl << std::endl
|
||||
<< " ====== OUTPUT ======"sv << std::endl;
|
||||
|
||||
dxgi::output_t::pointer output_p {};
|
||||
for(int y = 0; adapter->EnumOutputs(y, &output_p) != DXGI_ERROR_NOT_FOUND; ++y) {
|
||||
dxgi::output_t output {output_p };
|
||||
|
||||
DXGI_OUTPUT_DESC desc;
|
||||
output->GetDesc(&desc);
|
||||
|
||||
auto width = desc.DesktopCoordinates.right - desc.DesktopCoordinates.left;
|
||||
auto height = desc.DesktopCoordinates.bottom - desc.DesktopCoordinates.top;
|
||||
|
||||
std::wcout
|
||||
<< L" Output Name : "sv << desc.DeviceName << std::endl;
|
||||
std::cout
|
||||
<< " AttachedToDesktop : "sv << (desc.AttachedToDesktop ? "yes"sv : "no"sv) << std::endl
|
||||
<< " Resolution : "sv << width << 'x' << height << std::endl << std::endl;
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
Reference in New Issue
Block a user