diff --git a/.gitmodules b/.gitmodules index faccb0d5b59..67522f450ed 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,35 +1,43 @@ +[submodule "third-party/build-deps"] + path = third-party/build-deps + url = https://github.com/LizardByte/build-deps.git + branch = dist [submodule "third-party/moonlight-common-c"] path = third-party/moonlight-common-c url = https://github.com/moonlight-stream/moonlight-common-c.git branch = master -[submodule "third-party/Simple-Web-Server"] - path = third-party/Simple-Web-Server - url = https://gitlab.com/eidheim/Simple-Web-Server.git +[submodule "third-party/nanors"] + path = third-party/nanors + url = https://github.com/sleepybishop/nanors.git branch = master -[submodule "third-party/ViGEmClient"] - path = third-party/ViGEmClient - url = https://github.com/LizardByte/Virtual-Gamepad-Emulation-Client.git +[submodule "third-party/nlohmann_json"] + path = third-party/nlohmann_json + url = https://github.com/nlohmann/json branch = master [submodule "third-party/nv-codec-headers"] path = third-party/nv-codec-headers url = https://github.com/FFmpeg/nv-codec-headers branch = sdk/12.0 +[submodule "third-party/nvapi-open-source-sdk"] + path = third-party/nvapi-open-source-sdk + url = https://github.com/LizardByte/nvapi-open-source-sdk + branch = sdk +[submodule "third-party/Simple-Web-Server"] + path = third-party/Simple-Web-Server + url = https://gitlab.com/eidheim/Simple-Web-Server.git + branch = master [submodule "third-party/TPCircularBuffer"] path = third-party/TPCircularBuffer url = https://github.com/michaeltyson/TPCircularBuffer branch = master -[submodule "third-party/nanors"] - path = third-party/nanors - url = https://github.com/sleepybishop/nanors.git - branch = master [submodule "third-party/tray"] path = third-party/tray url = https://github.com/LizardByte/tray branch = master -[submodule "third-party/nvapi-open-source-sdk"] - path = third-party/nvapi-open-source-sdk - url = https://github.com/LizardByte/nvapi-open-source-sdk - branch = sdk +[submodule "third-party/ViGEmClient"] + path = third-party/ViGEmClient + url = https://github.com/LizardByte/Virtual-Gamepad-Emulation-Client.git + branch = master [submodule "third-party/wayland-protocols"] path = third-party/wayland-protocols url = https://gitlab.freedesktop.org/wayland/wayland-protocols @@ -38,7 +46,3 @@ path = third-party/wlr-protocols url = https://gitlab.freedesktop.org/wlroots/wlr-protocols branch = master -[submodule "third-party/build-deps"] - path = third-party/build-deps - url = https://github.com/LizardByte/build-deps.git - branch = dist diff --git a/CMakeLists.txt b/CMakeLists.txt index 9c21b380ab9..53d8bde2881 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -20,7 +20,7 @@ if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) endif() # set the module path, used for includes -set(CMAKE_MODULE_PATH ${CMAKE_CURRENT_SOURCE_DIR}/cmake) +set(CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}/cmake") # set version info for this build include(${CMAKE_MODULE_PATH}/prep/build_version.cmake) diff --git a/cmake/FindSystemd.cmake b/cmake/FindSystemd.cmake new file mode 100644 index 00000000000..c41ca64d248 --- /dev/null +++ b/cmake/FindSystemd.cmake @@ -0,0 +1,34 @@ +# - Try to find Systemd +# Once done this will define +# +# SYSTEMD_FOUND - system has systemd +# SYSTEMD_USER_UNIT_INSTALL_DIR - the systemd system unit install directory +# SYSTEMD_SYSTEM_UNIT_INSTALL_DIR - the systemd user unit install directory + +IF (NOT WIN32) + + find_package(PkgConfig QUIET) + if(PKG_CONFIG_FOUND) + pkg_check_modules(SYSTEMD "systemd") + endif() + + if (SYSTEMD_FOUND) + execute_process(COMMAND ${PKG_CONFIG_EXECUTABLE} + --variable=systemduserunitdir systemd + OUTPUT_VARIABLE SYSTEMD_USER_UNIT_INSTALL_DIR) + + string(REGEX REPLACE "[ \t\n]+" "" SYSTEMD_USER_UNIT_INSTALL_DIR + "${SYSTEMD_USER_UNIT_INSTALL_DIR}") + + execute_process(COMMAND ${PKG_CONFIG_EXECUTABLE} + --variable=systemdsystemunitdir systemd + OUTPUT_VARIABLE SYSTEMD_SYSTEM_UNIT_INSTALL_DIR) + + string(REGEX REPLACE "[ \t\n]+" "" SYSTEMD_SYSTEM_UNIT_INSTALL_DIR + "${SYSTEMD_SYSTEM_UNIT_INSTALL_DIR}") + + mark_as_advanced(SYSTEMD_USER_UNIT_INSTALL_DIR SYSTEMD_SYSTEM_UNIT_INSTALL_DIR) + + endif () + +ENDIF () diff --git a/cmake/FindUdev.cmake b/cmake/FindUdev.cmake new file mode 100644 index 00000000000..8343f791d35 --- /dev/null +++ b/cmake/FindUdev.cmake @@ -0,0 +1,28 @@ +# - Try to find Udev +# Once done this will define +# +# UDEV_FOUND - system has udev +# UDEV_RULES_INSTALL_DIR - the udev rules install directory + +IF (NOT WIN32) + + find_package(PkgConfig QUIET) + if(PKG_CONFIG_FOUND) + pkg_check_modules(UDEV "udev") + endif() + + if (UDEV_FOUND) + execute_process(COMMAND ${PKG_CONFIG_EXECUTABLE} + --variable=udevdir udev + OUTPUT_VARIABLE UDEV_RULES_INSTALL_DIR) + + string(REGEX REPLACE "[ \t\n]+" "" UDEV_RULES_INSTALL_DIR + "${UDEV_RULES_INSTALL_DIR}") + + set(UDEV_RULES_INSTALL_DIR "${UDEV_RULES_INSTALL_DIR}/rules.d") + + mark_as_advanced(UDEV_RULES_INSTALL_DIR) + + endif () + +ENDIF () diff --git a/cmake/compile_definitions/common.cmake b/cmake/compile_definitions/common.cmake index c096920a06b..e51dbf56526 100644 --- a/cmake/compile_definitions/common.cmake +++ b/cmake/compile_definitions/common.cmake @@ -23,70 +23,74 @@ elseif(UNIX) endif() endif() -include_directories(SYSTEM third-party/nv-codec-headers/include) +include_directories(SYSTEM "${CMAKE_SOURCE_DIR}/third-party/nv-codec-headers/include") file(GLOB NVENC_SOURCES CONFIGURE_DEPENDS "src/nvenc/*.cpp" "src/nvenc/*.h") list(APPEND PLATFORM_TARGET_FILES ${NVENC_SOURCES}) -configure_file(src/version.h.in version.h @ONLY) -include_directories(${CMAKE_CURRENT_BINARY_DIR}) +configure_file("${CMAKE_SOURCE_DIR}/src/version.h.in" version.h @ONLY) +include_directories("${CMAKE_CURRENT_BINARY_DIR}") set(SUNSHINE_TARGET_FILES - third-party/nanors/rs.c - third-party/nanors/rs.h - third-party/moonlight-common-c/src/Input.h - third-party/moonlight-common-c/src/Rtsp.h - third-party/moonlight-common-c/src/RtspParser.c - third-party/moonlight-common-c/src/Video.h - third-party/tray/tray.h - src/upnp.cpp - src/upnp.h - src/cbs.cpp - src/utility.h - src/uuid.h - src/config.h - src/config.cpp - src/main.cpp - src/main.h - src/crypto.cpp - src/crypto.h - src/nvhttp.cpp - src/nvhttp.h - src/httpcommon.cpp - src/httpcommon.h - src/confighttp.cpp - src/confighttp.h - src/rtsp.cpp - src/rtsp.h - src/stream.cpp - src/stream.h - src/video.cpp - src/video.h - src/video_colorspace.cpp - src/video_colorspace.h - src/input.cpp - src/input.h - src/audio.cpp - src/audio.h - src/platform/common.h - src/process.cpp - src/process.h - src/network.cpp - src/network.h - src/move_by_copy.h - src/system_tray.cpp - src/system_tray.h - src/task_pool.h - src/thread_pool.h - src/thread_safe.h - src/sync.h - src/round_robin.h - src/stat_trackers.h - src/stat_trackers.cpp + "${CMAKE_SOURCE_DIR}/third-party/nanors/rs.c" + "${CMAKE_SOURCE_DIR}/third-party/nanors/rs.h" + "${CMAKE_SOURCE_DIR}/third-party/moonlight-common-c/src/Input.h" + "${CMAKE_SOURCE_DIR}/third-party/moonlight-common-c/src/Rtsp.h" + "${CMAKE_SOURCE_DIR}/third-party/moonlight-common-c/src/RtspParser.c" + "${CMAKE_SOURCE_DIR}/third-party/moonlight-common-c/src/Video.h" + "${CMAKE_SOURCE_DIR}/third-party/tray/tray.h" + "${CMAKE_SOURCE_DIR}/src/upnp.cpp" + "${CMAKE_SOURCE_DIR}/src/upnp.h" + "${CMAKE_SOURCE_DIR}/src/cbs.cpp" + "${CMAKE_SOURCE_DIR}/src/utility.h" + "${CMAKE_SOURCE_DIR}/src/uuid.h" + "${CMAKE_SOURCE_DIR}/src/config.h" + "${CMAKE_SOURCE_DIR}/src/config.cpp" + "${CMAKE_SOURCE_DIR}/src/file_handler.cpp" + "${CMAKE_SOURCE_DIR}/src/file_handler.h" + "${CMAKE_SOURCE_DIR}/src/logging.cpp" + "${CMAKE_SOURCE_DIR}/src/logging.h" + "${CMAKE_SOURCE_DIR}/src/main.cpp" + "${CMAKE_SOURCE_DIR}/src/main.h" + "${CMAKE_SOURCE_DIR}/src/crypto.cpp" + "${CMAKE_SOURCE_DIR}/src/crypto.h" + "${CMAKE_SOURCE_DIR}/src/nvhttp.cpp" + "${CMAKE_SOURCE_DIR}/src/nvhttp.h" + "${CMAKE_SOURCE_DIR}/src/httpcommon.cpp" + "${CMAKE_SOURCE_DIR}/src/httpcommon.h" + "${CMAKE_SOURCE_DIR}/src/confighttp.cpp" + "${CMAKE_SOURCE_DIR}/src/confighttp.h" + "${CMAKE_SOURCE_DIR}/src/rtsp.cpp" + "${CMAKE_SOURCE_DIR}/src/rtsp.h" + "${CMAKE_SOURCE_DIR}/src/stream.cpp" + "${CMAKE_SOURCE_DIR}/src/stream.h" + "${CMAKE_SOURCE_DIR}/src/video.cpp" + "${CMAKE_SOURCE_DIR}/src/video.h" + "${CMAKE_SOURCE_DIR}/src/video_colorspace.cpp" + "${CMAKE_SOURCE_DIR}/src/video_colorspace.h" + "${CMAKE_SOURCE_DIR}/src/input.cpp" + "${CMAKE_SOURCE_DIR}/src/input.h" + "${CMAKE_SOURCE_DIR}/src/audio.cpp" + "${CMAKE_SOURCE_DIR}/src/audio.h" + "${CMAKE_SOURCE_DIR}/src/platform/common.h" + "${CMAKE_SOURCE_DIR}/src/process.cpp" + "${CMAKE_SOURCE_DIR}/src/process.h" + "${CMAKE_SOURCE_DIR}/src/network.cpp" + "${CMAKE_SOURCE_DIR}/src/network.h" + "${CMAKE_SOURCE_DIR}/src/move_by_copy.h" + "${CMAKE_SOURCE_DIR}/src/system_tray.cpp" + "${CMAKE_SOURCE_DIR}/src/system_tray.h" + "${CMAKE_SOURCE_DIR}/src/task_pool.h" + "${CMAKE_SOURCE_DIR}/src/thread_pool.h" + "${CMAKE_SOURCE_DIR}/src/thread_safe.h" + "${CMAKE_SOURCE_DIR}/src/sync.h" + "${CMAKE_SOURCE_DIR}/src/round_robin.h" + "${CMAKE_SOURCE_DIR}/src/stat_trackers.h" + "${CMAKE_SOURCE_DIR}/src/stat_trackers.cpp" ${PLATFORM_TARGET_FILES}) -set_source_files_properties(src/upnp.cpp PROPERTIES COMPILE_FLAGS -Wno-pedantic) +set_source_files_properties("${CMAKE_SOURCE_DIR}/src/upnp.cpp" PROPERTIES COMPILE_FLAGS -Wno-pedantic) -set_source_files_properties(third-party/nanors/rs.c +set_source_files_properties("${CMAKE_SOURCE_DIR}/third-party/nanors/rs.c" PROPERTIES COMPILE_FLAGS "-include deps/obl/autoshim.h -ftree-vectorize") if(NOT SUNSHINE_ASSETS_DIR_DEF) @@ -96,14 +100,14 @@ list(APPEND SUNSHINE_DEFINITIONS SUNSHINE_ASSETS_DIR="${SUNSHINE_ASSETS_DIR_DEF} list(APPEND SUNSHINE_DEFINITIONS SUNSHINE_TRAY=${SUNSHINE_TRAY}) -include_directories(${CMAKE_CURRENT_SOURCE_DIR}) +include_directories("${CMAKE_SOURCE_DIR}") include_directories( SYSTEM - ${CMAKE_CURRENT_SOURCE_DIR}/third-party - ${CMAKE_CURRENT_SOURCE_DIR}/third-party/moonlight-common-c/enet/include - ${CMAKE_CURRENT_SOURCE_DIR}/third-party/nanors - ${CMAKE_CURRENT_SOURCE_DIR}/third-party/nanors/deps/obl + "${CMAKE_SOURCE_DIR}/third-party" + "${CMAKE_SOURCE_DIR}/third-party/moonlight-common-c/enet/include" + "${CMAKE_SOURCE_DIR}/third-party/nanors" + "${CMAKE_SOURCE_DIR}/third-party/nanors/deps/obl" ${FFMPEG_INCLUDE_DIRS} ${PLATFORM_INCLUDE_DIRS} ) @@ -111,7 +115,7 @@ include_directories( string(TOUPPER "x${CMAKE_BUILD_TYPE}" BUILD_TYPE) if("${BUILD_TYPE}" STREQUAL "XDEBUG") if(WIN32) - set_source_files_properties(src/nvhttp.cpp PROPERTIES COMPILE_FLAGS -O2) + set_source_files_properties("${CMAKE_SOURCE_DIR}/src/nvhttp.cpp" PROPERTIES COMPILE_FLAGS -O2) endif() else() add_definitions(-DNDEBUG) @@ -126,4 +130,5 @@ list(APPEND SUNSHINE_EXTERNAL_LIBRARIES ${Boost_LIBRARIES} ${OPENSSL_LIBRARIES} ${CURL_LIBRARIES} - ${PLATFORM_LIBRARIES}) + ${PLATFORM_LIBRARIES} + nlohmann_json::nlohmann_json) diff --git a/cmake/compile_definitions/linux.cmake b/cmake/compile_definitions/linux.cmake index d4ebb597312..613a090947d 100644 --- a/cmake/compile_definitions/linux.cmake +++ b/cmake/compile_definitions/linux.cmake @@ -89,12 +89,12 @@ if(${SUNSHINE_ENABLE_CUDA}) endif() endif() if(CUDA_FOUND) - include_directories(SYSTEM third-party/nvfbc) + include_directories(SYSTEM "${CMAKE_SOURCE_DIR}/third-party/nvfbc") list(APPEND PLATFORM_TARGET_FILES - src/platform/linux/cuda.h - src/platform/linux/cuda.cu - src/platform/linux/cuda.cpp - third-party/nvfbc/NvFBC.h) + "${CMAKE_SOURCE_DIR}/src/platform/linux/cuda.h" + "${CMAKE_SOURCE_DIR}/src/platform/linux/cuda.cu" + "${CMAKE_SOURCE_DIR}/src/platform/linux/cuda.cpp" + "${CMAKE_SOURCE_DIR}/third-party/nvfbc/NvFBC.h") add_compile_definitions(SUNSHINE_BUILD_CUDA) endif() @@ -112,7 +112,7 @@ if(LIBDRM_FOUND AND LIBCAP_FOUND) include_directories(SYSTEM ${LIBDRM_INCLUDE_DIRS} ${LIBCAP_INCLUDE_DIRS}) list(APPEND PLATFORM_LIBRARIES ${LIBDRM_LIBRARIES} ${LIBCAP_LIBRARIES}) list(APPEND PLATFORM_TARGET_FILES - src/platform/linux/kmsgrab.cpp) + "${CMAKE_SOURCE_DIR}/src/platform/linux/kmsgrab.cpp") list(APPEND SUNSHINE_DEFINITIONS EGL_NO_X11=1) elseif(NOT LIBDRM_FOUND) message(WARNING "Missing libdrm") @@ -131,8 +131,8 @@ if(LIBVA_FOUND) include_directories(SYSTEM ${LIBVA_INCLUDE_DIR}) list(APPEND PLATFORM_LIBRARIES ${LIBVA_LIBRARIES} ${LIBVA_DRM_LIBRARIES}) list(APPEND PLATFORM_TARGET_FILES - src/platform/linux/vaapi.h - src/platform/linux/vaapi.cpp) + "${CMAKE_SOURCE_DIR}/src/platform/linux/vaapi.h" + "${CMAKE_SOURCE_DIR}/src/platform/linux/vaapi.cpp") endif() # wayland @@ -162,9 +162,9 @@ if(WAYLAND_FOUND) list(APPEND PLATFORM_LIBRARIES ${WAYLAND_LIBRARIES}) list(APPEND PLATFORM_TARGET_FILES - src/platform/linux/wlgrab.cpp - src/platform/linux/wayland.h - src/platform/linux/wayland.cpp) + "${CMAKE_SOURCE_DIR}/src/platform/linux/wlgrab.cpp" + "${CMAKE_SOURCE_DIR}/src/platform/linux/wayland.h" + "${CMAKE_SOURCE_DIR}/src/platform/linux/wayland.cpp") endif() # x11 @@ -178,8 +178,8 @@ if(X11_FOUND) include_directories(SYSTEM ${X11_INCLUDE_DIR}) list(APPEND PLATFORM_LIBRARIES ${X11_LIBRARIES}) list(APPEND PLATFORM_TARGET_FILES - src/platform/linux/x11grab.h - src/platform/linux/x11grab.cpp) + "${CMAKE_SOURCE_DIR}/src/platform/linux/x11grab.h" + "${CMAKE_SOURCE_DIR}/src/platform/linux/x11grab.cpp") endif() if(NOT ${CUDA_FOUND} @@ -211,7 +211,7 @@ if(${SUNSHINE_ENABLE_TRAY}) include_directories(SYSTEM ${APPINDICATOR_INCLUDE_DIRS} ${LIBNOTIFY_INCLUDE_DIRS}) link_directories(${APPINDICATOR_LIBRARY_DIRS} ${LIBNOTIFY_LIBRARY_DIRS}) - list(APPEND PLATFORM_TARGET_FILES third-party/tray/tray_linux.c) + list(APPEND PLATFORM_TARGET_FILES "${CMAKE_SOURCE_DIR}/third-party/tray/tray_linux.c") list(APPEND SUNSHINE_EXTERNAL_LIBRARIES ${APPINDICATOR_LIBRARIES} ${LIBNOTIFY_LIBRARIES}) endif() else() @@ -224,19 +224,19 @@ if (${SUNSHINE_TRAY} EQUAL 0 AND SUNSHINE_REQUIRE_TRAY) endif() list(APPEND PLATFORM_TARGET_FILES - src/platform/linux/publish.cpp - src/platform/linux/graphics.h - src/platform/linux/graphics.cpp - src/platform/linux/misc.h - src/platform/linux/misc.cpp - src/platform/linux/audio.cpp - src/platform/linux/input.cpp - third-party/glad/src/egl.c - third-party/glad/src/gl.c - third-party/glad/include/EGL/eglplatform.h - third-party/glad/include/KHR/khrplatform.h - third-party/glad/include/glad/gl.h - third-party/glad/include/glad/egl.h) + "${CMAKE_SOURCE_DIR}/src/platform/linux/publish.cpp" + "${CMAKE_SOURCE_DIR}/src/platform/linux/graphics.h" + "${CMAKE_SOURCE_DIR}/src/platform/linux/graphics.cpp" + "${CMAKE_SOURCE_DIR}/src/platform/linux/misc.h" + "${CMAKE_SOURCE_DIR}/src/platform/linux/misc.cpp" + "${CMAKE_SOURCE_DIR}/src/platform/linux/audio.cpp" + "${CMAKE_SOURCE_DIR}/src/platform/linux/input.cpp" + "${CMAKE_SOURCE_DIR}/third-party/glad/src/egl.c" + "${CMAKE_SOURCE_DIR}/third-party/glad/src/gl.c" + "${CMAKE_SOURCE_DIR}/third-party/glad/include/EGL/eglplatform.h" + "${CMAKE_SOURCE_DIR}/third-party/glad/include/KHR/khrplatform.h" + "${CMAKE_SOURCE_DIR}/third-party/glad/include/glad/gl.h" + "${CMAKE_SOURCE_DIR}/third-party/glad/include/glad/egl.h") list(APPEND PLATFORM_LIBRARIES Boost::dynamic_linking @@ -249,5 +249,5 @@ list(APPEND PLATFORM_LIBRARIES include_directories( SYSTEM /usr/include/libevdev-1.0 - third-party/nv-codec-headers/include - third-party/glad/include) + "${CMAKE_SOURCE_DIR}/third-party/nv-codec-headers/include" + "${CMAKE_SOURCE_DIR}/third-party/glad/include") diff --git a/cmake/compile_definitions/macos.cmake b/cmake/compile_definitions/macos.cmake index 3bcf9528361..fff301b856a 100644 --- a/cmake/compile_definitions/macos.cmake +++ b/cmake/compile_definitions/macos.cmake @@ -18,32 +18,32 @@ list(APPEND SUNSHINE_EXTERNAL_LIBRARIES set(PLATFORM_INCLUDE_DIRS ${Boost_INCLUDE_DIR}) -set(APPLE_PLIST_FILE ${SUNSHINE_SOURCE_ASSETS_DIR}/macos/assets/Info.plist) +set(APPLE_PLIST_FILE "${SUNSHINE_SOURCE_ASSETS_DIR}/macos/assets/Info.plist") # todo - tray is not working on macos set(SUNSHINE_TRAY 0) set(PLATFORM_TARGET_FILES - src/platform/macos/av_audio.h - src/platform/macos/av_audio.m - src/platform/macos/av_img_t.h - src/platform/macos/av_video.h - src/platform/macos/av_video.m - src/platform/macos/display.mm - src/platform/macos/input.cpp - src/platform/macos/microphone.mm - src/platform/macos/misc.mm - src/platform/macos/misc.h - src/platform/macos/nv12_zero_device.cpp - src/platform/macos/nv12_zero_device.h - src/platform/macos/publish.cpp - third-party/TPCircularBuffer/TPCircularBuffer.c - third-party/TPCircularBuffer/TPCircularBuffer.h + "${CMAKE_SOURCE_DIR}/src/platform/macos/av_audio.h" + "${CMAKE_SOURCE_DIR}/src/platform/macos/av_audio.m" + "${CMAKE_SOURCE_DIR}/src/platform/macos/av_img_t.h" + "${CMAKE_SOURCE_DIR}/src/platform/macos/av_video.h" + "${CMAKE_SOURCE_DIR}/src/platform/macos/av_video.m" + "${CMAKE_SOURCE_DIR}/src/platform/macos/display.mm" + "${CMAKE_SOURCE_DIR}/src/platform/macos/input.cpp" + "${CMAKE_SOURCE_DIR}/src/platform/macos/microphone.mm" + "${CMAKE_SOURCE_DIR}/src/platform/macos/misc.mm" + "${CMAKE_SOURCE_DIR}/src/platform/macos/misc.h" + "${CMAKE_SOURCE_DIR}/src/platform/macos/nv12_zero_device.cpp" + "${CMAKE_SOURCE_DIR}/src/platform/macos/nv12_zero_device.h" + "${CMAKE_SOURCE_DIR}/src/platform/macos/publish.cpp" + "${CMAKE_SOURCE_DIR}/third-party/TPCircularBuffer/TPCircularBuffer.c" + "${CMAKE_SOURCE_DIR}/third-party/TPCircularBuffer/TPCircularBuffer.h" ${APPLE_PLIST_FILE}) if(SUNSHINE_ENABLE_TRAY) list(APPEND SUNSHINE_EXTERNAL_LIBRARIES ${COCOA}) list(APPEND PLATFORM_TARGET_FILES - third-party/tray/tray_darwin.m) + "${CMAKE_SOURCE_DIR}/third-party/tray/tray_darwin.m") endif() diff --git a/cmake/compile_definitions/windows.cmake b/cmake/compile_definitions/windows.cmake index 703106f189a..e3729927fc1 100644 --- a/cmake/compile_definitions/windows.cmake +++ b/cmake/compile_definitions/windows.cmake @@ -18,42 +18,42 @@ add_definitions(-DMINIUPNP_STATICLIB) add_subdirectory(tools) # todo - this is temporary, only tools for Windows are needed, for now # nvidia -include_directories(SYSTEM third-party/nvapi-open-source-sdk) +include_directories(SYSTEM "${CMAKE_SOURCE_DIR}/third-party/nvapi-open-source-sdk") file(GLOB NVPREFS_FILES CONFIGURE_DEPENDS - "third-party/nvapi-open-source-sdk/*.h" - "src/platform/windows/nvprefs/*.cpp" - "src/platform/windows/nvprefs/*.h") + "${CMAKE_SOURCE_DIR}/third-party/nvapi-open-source-sdk/*.h" + "${CMAKE_SOURCE_DIR}/src/platform/windows/nvprefs/*.cpp" + "${CMAKE_SOURCE_DIR}/src/platform/windows/nvprefs/*.h") # vigem -include_directories(SYSTEM third-party/ViGEmClient/include) -set_source_files_properties(third-party/ViGEmClient/src/ViGEmClient.cpp +include_directories(SYSTEM "${CMAKE_SOURCE_DIR}/third-party/ViGEmClient/include") +set_source_files_properties("${CMAKE_SOURCE_DIR}/third-party/ViGEmClient/src/ViGEmClient.cpp" PROPERTIES COMPILE_DEFINITIONS "UNICODE=1;ERROR_INVALID_DEVICE_OBJECT_PARAMETER=650") -set_source_files_properties(third-party/ViGEmClient/src/ViGEmClient.cpp +set_source_files_properties("${CMAKE_SOURCE_DIR}/third-party/ViGEmClient/src/ViGEmClient.cpp" PROPERTIES COMPILE_FLAGS "-Wno-unknown-pragmas -Wno-misleading-indentation -Wno-class-memaccess") # sunshine icon if(NOT DEFINED SUNSHINE_ICON_PATH) - set(SUNSHINE_ICON_PATH "${CMAKE_CURRENT_SOURCE_DIR}/sunshine.ico") + set(SUNSHINE_ICON_PATH "${CMAKE_SOURCE_DIR}/sunshine.ico") endif() -configure_file(src/platform/windows/windows.rs.in windows.rc @ONLY) +configure_file("${CMAKE_SOURCE_DIR}/src/platform/windows/windows.rs.in" windows.rc @ONLY) set(PLATFORM_TARGET_FILES "${CMAKE_CURRENT_BINARY_DIR}/windows.rc" - src/platform/windows/publish.cpp - src/platform/windows/misc.h - src/platform/windows/misc.cpp - src/platform/windows/input.cpp - src/platform/windows/display.h - src/platform/windows/display_base.cpp - src/platform/windows/display_vram.cpp - src/platform/windows/display_ram.cpp - src/platform/windows/audio.cpp - third-party/ViGEmClient/src/ViGEmClient.cpp - third-party/ViGEmClient/include/ViGEm/Client.h - third-party/ViGEmClient/include/ViGEm/Common.h - third-party/ViGEmClient/include/ViGEm/Util.h - third-party/ViGEmClient/include/ViGEm/km/BusShared.h + "${CMAKE_SOURCE_DIR}/src/platform/windows/publish.cpp" + "${CMAKE_SOURCE_DIR}/src/platform/windows/misc.h" + "${CMAKE_SOURCE_DIR}/src/platform/windows/misc.cpp" + "${CMAKE_SOURCE_DIR}/src/platform/windows/input.cpp" + "${CMAKE_SOURCE_DIR}/src/platform/windows/display.h" + "${CMAKE_SOURCE_DIR}/src/platform/windows/display_base.cpp" + "${CMAKE_SOURCE_DIR}/src/platform/windows/display_vram.cpp" + "${CMAKE_SOURCE_DIR}/src/platform/windows/display_ram.cpp" + "${CMAKE_SOURCE_DIR}/src/platform/windows/audio.cpp" + "${CMAKE_SOURCE_DIR}/third-party/ViGEmClient/src/ViGEmClient.cpp" + "${CMAKE_SOURCE_DIR}/third-party/ViGEmClient/include/ViGEm/Client.h" + "${CMAKE_SOURCE_DIR}/third-party/ViGEmClient/include/ViGEm/Common.h" + "${CMAKE_SOURCE_DIR}/third-party/ViGEmClient/include/ViGEm/Util.h" + "${CMAKE_SOURCE_DIR}/third-party/ViGEmClient/include/ViGEm/km/BusShared.h" ${NVPREFS_FILES}) set(OPENSSL_LIBRARIES @@ -74,9 +74,10 @@ list(PREPEND PLATFORM_LIBRARIES synchronization.lib avrt iphlpapi + shlwapi ${CURL_STATIC_LIBRARIES}) if(SUNSHINE_ENABLE_TRAY) list(APPEND PLATFORM_TARGET_FILES - third-party/tray/tray_windows.c) + "${CMAKE_SOURCE_DIR}/third-party/tray/tray_windows.c") endif() diff --git a/cmake/dependencies/common.cmake b/cmake/dependencies/common.cmake index 8aeb92a8d87..a1f35128005 100644 --- a/cmake/dependencies/common.cmake +++ b/cmake/dependencies/common.cmake @@ -4,10 +4,10 @@ # submodules # moonlight common library set(ENET_NO_INSTALL ON CACHE BOOL "Don't install any libraries build for enet") -add_subdirectory(third-party/moonlight-common-c/enet) +add_subdirectory("${CMAKE_SOURCE_DIR}/third-party/moonlight-common-c/enet") # web server -add_subdirectory(third-party/Simple-Web-Server) +add_subdirectory("${CMAKE_SOURCE_DIR}/third-party/Simple-Web-Server") # common dependencies find_package(OpenSSL REQUIRED) @@ -19,18 +19,21 @@ pkg_check_modules(CURL REQUIRED libcurl) pkg_check_modules(MINIUPNP miniupnpc REQUIRED) include_directories(SYSTEM ${MINIUPNP_INCLUDE_DIRS}) +# nlohmann_json +add_subdirectory("${CMAKE_SOURCE_DIR}/third-party/nlohmann_json") + # ffmpeg pre-compiled binaries if(WIN32) if(NOT CMAKE_SYSTEM_PROCESSOR STREQUAL "AMD64") message(FATAL_ERROR "Unsupported system processor:" ${CMAKE_SYSTEM_PROCESSOR}) endif() set(FFMPEG_PLATFORM_LIBRARIES mfplat ole32 strmiids mfuuid vpl) - set(FFMPEG_PREPARED_BINARIES "${CMAKE_CURRENT_SOURCE_DIR}/third-party/build-deps/ffmpeg/windows-x86_64") + set(FFMPEG_PREPARED_BINARIES "${CMAKE_SOURCE_DIR}/third-party/build-deps/ffmpeg/windows-x86_64") elseif(APPLE) if(CMAKE_SYSTEM_PROCESSOR STREQUAL "x86_64") - set(FFMPEG_PREPARED_BINARIES "${CMAKE_CURRENT_SOURCE_DIR}/third-party/build-deps/ffmpeg/macos-x86_64") + set(FFMPEG_PREPARED_BINARIES "${CMAKE_SOURCE_DIR}/third-party/build-deps/ffmpeg/macos-x86_64") elseif(CMAKE_SYSTEM_PROCESSOR STREQUAL "arm64") - set(FFMPEG_PREPARED_BINARIES "${CMAKE_CURRENT_SOURCE_DIR}/third-party/build-deps/ffmpeg/macos-aarch64") + set(FFMPEG_PREPARED_BINARIES "${CMAKE_SOURCE_DIR}/third-party/build-deps/ffmpeg/macos-aarch64") elseif(CMAKE_SYSTEM_PROCESSOR STREQUAL "powerpc") message(FATAL_ERROR "PowerPC is not supported on macOS") else() @@ -40,43 +43,43 @@ elseif(UNIX) set(FFMPEG_PLATFORM_LIBRARIES va va-drm va-x11 vdpau X11) if(CMAKE_SYSTEM_PROCESSOR STREQUAL "x86_64") list(APPEND FFMPEG_PLATFORM_LIBRARIES mfx) - set(FFMPEG_PREPARED_BINARIES "${CMAKE_CURRENT_SOURCE_DIR}/third-party/build-deps/ffmpeg/linux-x86_64") + set(FFMPEG_PREPARED_BINARIES "${CMAKE_SOURCE_DIR}/third-party/build-deps/ffmpeg/linux-x86_64") set(CPACK_DEB_PLATFORM_PACKAGE_DEPENDS "libmfx1,") set(CPACK_RPM_PLATFORM_PACKAGE_REQUIRES "intel-mediasdk >= 22.3.0,") elseif(CMAKE_SYSTEM_PROCESSOR STREQUAL "aarch64") - set(FFMPEG_PREPARED_BINARIES "${CMAKE_CURRENT_SOURCE_DIR}/third-party/build-deps/ffmpeg/linux-aarch64") + set(FFMPEG_PREPARED_BINARIES "${CMAKE_SOURCE_DIR}/third-party/build-deps/ffmpeg/linux-aarch64") elseif(CMAKE_SYSTEM_PROCESSOR STREQUAL "ppc64le" OR CMAKE_SYSTEM_PROCESSOR STREQUAL "ppc64") - set(FFMPEG_PREPARED_BINARIES "${CMAKE_CURRENT_SOURCE_DIR}/third-party/build-deps/ffmpeg/linux-powerpc64le") + set(FFMPEG_PREPARED_BINARIES "${CMAKE_SOURCE_DIR}/third-party/build-deps/ffmpeg/linux-powerpc64le") else() message(FATAL_ERROR "Unsupported system processor:" ${CMAKE_SYSTEM_PROCESSOR}) endif() endif() set(FFMPEG_INCLUDE_DIRS - ${FFMPEG_PREPARED_BINARIES}/include) -if(EXISTS ${FFMPEG_PREPARED_BINARIES}/lib/libhdr10plus.a) + "${FFMPEG_PREPARED_BINARIES}/include") +if(EXISTS "${FFMPEG_PREPARED_BINARIES}/lib/libhdr10plus.a") set(HDR10_PLUS_LIBRARY - ${FFMPEG_PREPARED_BINARIES}/lib/libhdr10plus.a) + "${FFMPEG_PREPARED_BINARIES}/lib/libhdr10plus.a") endif() set(FFMPEG_LIBRARIES - ${FFMPEG_PREPARED_BINARIES}/lib/libavcodec.a - ${FFMPEG_PREPARED_BINARIES}/lib/libavutil.a - ${FFMPEG_PREPARED_BINARIES}/lib/libcbs.a - ${FFMPEG_PREPARED_BINARIES}/lib/libSvtAv1Enc.a - ${FFMPEG_PREPARED_BINARIES}/lib/libswscale.a - ${FFMPEG_PREPARED_BINARIES}/lib/libx264.a - ${FFMPEG_PREPARED_BINARIES}/lib/libx265.a + "${FFMPEG_PREPARED_BINARIES}/lib/libavcodec.a" + "${FFMPEG_PREPARED_BINARIES}/lib/libavutil.a" + "${FFMPEG_PREPARED_BINARIES}/lib/libcbs.a" + "${FFMPEG_PREPARED_BINARIES}/lib/libSvtAv1Enc.a" + "${FFMPEG_PREPARED_BINARIES}/lib/libswscale.a" + "${FFMPEG_PREPARED_BINARIES}/lib/libx264.a" + "${FFMPEG_PREPARED_BINARIES}/lib/libx265.a" ${HDR10_PLUS_LIBRARY} ${FFMPEG_PLATFORM_LIBRARIES}) # platform specific dependencies if(WIN32) - include(${CMAKE_MODULE_PATH}/dependencies/windows.cmake) + include("${CMAKE_MODULE_PATH}/dependencies/windows.cmake") elseif(UNIX) - include(${CMAKE_MODULE_PATH}/dependencies/unix.cmake) + include("${CMAKE_MODULE_PATH}/dependencies/unix.cmake") if(APPLE) - include(${CMAKE_MODULE_PATH}/dependencies/macos.cmake) + include("${CMAKE_MODULE_PATH}/dependencies/macos.cmake") else() - include(${CMAKE_MODULE_PATH}/dependencies/linux.cmake) + include("${CMAKE_MODULE_PATH}/dependencies/linux.cmake") endif() endif() diff --git a/cmake/dependencies/windows.cmake b/cmake/dependencies/windows.cmake index 3ce9a9da234..376c44da65a 100644 --- a/cmake/dependencies/windows.cmake +++ b/cmake/dependencies/windows.cmake @@ -1,6 +1,4 @@ # windows specific dependencies set(Boost_USE_STATIC_LIBS ON) # cmake-lint: disable=C0103 -# Boost >= 1.82.0 is required for boost::json::value::set_at_pointer() support -# todo - are we actually using json? I think this was attempted to be used in a PR, but we ended up not using json -find_package(Boost 1.82.0 COMPONENTS locale log filesystem program_options json REQUIRED) +find_package(Boost 1.71.0 COMPONENTS locale log filesystem program_options REQUIRED) diff --git a/cmake/packaging/linux.cmake b/cmake/packaging/linux.cmake index 517224d3cb9..8563414a40e 100644 --- a/cmake/packaging/linux.cmake +++ b/cmake/packaging/linux.cmake @@ -8,10 +8,13 @@ if(${SUNSHINE_BUILD_APPIMAGE} OR ${SUNSHINE_BUILD_FLATPAK}) install(FILES "${CMAKE_CURRENT_BINARY_DIR}/sunshine.service" DESTINATION "${SUNSHINE_ASSETS_DIR}/systemd/user") else() + find_package(Systemd) + find_package(Udev) + install(FILES "${SUNSHINE_SOURCE_ASSETS_DIR}/linux/misc/85-sunshine.rules" - DESTINATION "${CMAKE_INSTALL_LIBDIR}/udev/rules.d") + DESTINATION "${UDEV_RULES_INSTALL_DIR}") install(FILES "${CMAKE_CURRENT_BINARY_DIR}/sunshine.service" - DESTINATION "${CMAKE_INSTALL_LIBDIR}/systemd/user") + DESTINATION "${SYSTEMD_USER_UNIT_INSTALL_DIR}") endif() # Post install diff --git a/cmake/packaging/windows.cmake b/cmake/packaging/windows.cmake index dc2add19854..1ea1afe191d 100644 --- a/cmake/packaging/windows.cmake +++ b/cmake/packaging/windows.cmake @@ -41,7 +41,7 @@ install(DIRECTORY "${SUNSHINE_SOURCE_ASSETS_DIR}/windows/assets/" COMPONENT assets) # set(CPACK_NSIS_MUI_HEADERIMAGE "") # TODO: image should be 150x57 bmp -set(CPACK_PACKAGE_ICON "${CMAKE_CURRENT_SOURCE_DIR}\\\\sunshine.ico") +set(CPACK_PACKAGE_ICON "${CMAKE_SOURCE_DIR}\\\\sunshine.ico") set(CPACK_NSIS_INSTALLED_ICON_NAME "${PROJECT__DIR}\\\\${PROJECT_EXE}") # The name of the directory that will be created in C:/Program files/ set(CPACK_PACKAGE_INSTALL_DIRECTORY "${CPACK_PACKAGE_NAME}") diff --git a/cmake/prep/build_version.cmake b/cmake/prep/build_version.cmake index 49f85f9b585..b8cff63e14f 100644 --- a/cmake/prep/build_version.cmake +++ b/cmake/prep/build_version.cmake @@ -14,12 +14,12 @@ if((DEFINED ENV{BRANCH}) AND (DEFINED ENV{BUILD_VERSION}) AND (DEFINED ENV{COMMI else() find_package(Git) if(GIT_EXECUTABLE) - MESSAGE("${CMAKE_CURRENT_SOURCE_DIR}") - get_filename_component(SRC_DIR ${CMAKE_CURRENT_SOURCE_DIR} DIRECTORY) + MESSAGE("${CMAKE_SOURCE_DIR}") + get_filename_component(SRC_DIR "${CMAKE_SOURCE_DIR}" DIRECTORY) #Get current Branch execute_process( COMMAND ${GIT_EXECUTABLE} rev-parse --abbrev-ref HEAD - #WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} + #WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}" OUTPUT_VARIABLE GIT_DESCRIBE_BRANCH RESULT_VARIABLE GIT_DESCRIBE_ERROR_CODE OUTPUT_STRIP_TRAILING_WHITESPACE @@ -27,7 +27,7 @@ else() # Gather current commit execute_process( COMMAND ${GIT_EXECUTABLE} rev-parse --short HEAD - #WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} + #WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}" OUTPUT_VARIABLE GIT_DESCRIBE_VERSION RESULT_VARIABLE GIT_DESCRIBE_ERROR_CODE OUTPUT_STRIP_TRAILING_WHITESPACE @@ -35,7 +35,7 @@ else() # Check if Dirty execute_process( COMMAND ${GIT_EXECUTABLE} diff --quiet --exit-code - #WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} + #WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}" RESULT_VARIABLE GIT_IS_DIRTY OUTPUT_STRIP_TRAILING_WHITESPACE ) diff --git a/cmake/prep/constants.cmake b/cmake/prep/constants.cmake index 4f7a9e48aeb..b80be1e3480 100644 --- a/cmake/prep/constants.cmake +++ b/cmake/prep/constants.cmake @@ -1,5 +1,5 @@ # source assets will be installed from this directory -set(SUNSHINE_SOURCE_ASSETS_DIR "${CMAKE_CURRENT_SOURCE_DIR}/src_assets") +set(SUNSHINE_SOURCE_ASSETS_DIR "${CMAKE_SOURCE_DIR}/src_assets") # enable system tray, we will disable this later if we cannot find the required package config on linux set(SUNSHINE_TRAY 1) diff --git a/cmake/targets/common.cmake b/cmake/targets/common.cmake index 7c446e466df..cb5fe4e67d4 100644 --- a/cmake/targets/common.cmake +++ b/cmake/targets/common.cmake @@ -36,6 +36,6 @@ target_compile_options(sunshine PRIVATE $<$:${SUNSHINE_COM #WebUI build add_custom_target(web-ui ALL - WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} + WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}" COMMENT "Installing NPM Dependencies and Building the Web UI" COMMAND bash -c \"npm install && SUNSHINE_SOURCE_ASSETS_DIR=${SUNSHINE_SOURCE_ASSETS_DIR} SUNSHINE_ASSETS_DIR=${CMAKE_BINARY_DIR} npm run build\") # cmake-lint: disable=C0301 diff --git a/docker/clion-toolchain.dockerfile b/docker/clion-toolchain.dockerfile new file mode 100644 index 00000000000..bb4604f54f5 --- /dev/null +++ b/docker/clion-toolchain.dockerfile @@ -0,0 +1,100 @@ +# syntax=docker/dockerfile:1.4 +# artifacts: false +# platforms: linux/amd64 +# platforms_pr: linux/amd64 +# no-cache-filters: toolchain-base,toolchain +ARG BASE=ubuntu +ARG TAG=22.04 +FROM ${BASE}:${TAG} AS toolchain-base + +ENV DEBIAN_FRONTEND=noninteractive + +FROM toolchain-base as toolchain + +ARG TARGETPLATFORM +RUN echo "target_platform: ${TARGETPLATFORM}" + +SHELL ["/bin/bash", "-o", "pipefail", "-c"] +# install dependencies +RUN <<_DEPS +#!/bin/bash +set -e +apt-get update -y +apt-get install -y --no-install-recommends \ + build-essential \ + cmake=3.22.* \ + ca-certificates \ + gcc=4:11.2.* \ + g++=4:11.2.* \ + gdb \ + git \ + libayatana-appindicator3-dev \ + libavdevice-dev \ + libboost-filesystem-dev=1.74.* \ + libboost-locale-dev=1.74.* \ + libboost-log-dev=1.74.* \ + libboost-program-options-dev=1.74.* \ + libcap-dev \ + libcurl4-openssl-dev \ + libdrm-dev \ + libevdev-dev \ + libminiupnpc-dev \ + libnotify-dev \ + libnuma-dev \ + libopus-dev \ + libpulse-dev \ + libssl-dev \ + libva-dev \ + libvdpau-dev \ + libwayland-dev \ + libx11-dev \ + libxcb-shm0-dev \ + libxcb-xfixes0-dev \ + libxcb1-dev \ + libxfixes-dev \ + libxrandr-dev \ + libxtst-dev \ + udev \ + wget +if [[ "${TARGETPLATFORM}" == 'linux/amd64' ]]; then + apt-get install -y --no-install-recommends \ + libmfx-dev +fi +apt-get clean +rm -rf /var/lib/apt/lists/* +_DEPS + +#Install Node +# hadolint ignore=SC1091 +RUN <<_INSTALL_NODE +#!/bin/bash +set -e +node_version="20.9.0" +wget -qO- https://raw.githubusercontent.com/nvm-sh/nvm/master/install.sh | bash +source "$HOME/.nvm/nvm.sh" +nvm install "$node_version" +nvm use "$node_version" +nvm alias default "$node_version" +_INSTALL_NODE + +# install cuda +WORKDIR /build/cuda +# versions: https://developer.nvidia.com/cuda-toolkit-archive +ENV CUDA_VERSION="11.8.0" +ENV CUDA_BUILD="520.61.05" +# hadolint ignore=SC3010 +RUN <<_INSTALL_CUDA +#!/bin/bash +set -e +cuda_prefix="https://developer.download.nvidia.com/compute/cuda/" +cuda_suffix="" +if [[ "${TARGETPLATFORM}" == 'linux/arm64' ]]; then + cuda_suffix="_sbsa" +fi +url="${cuda_prefix}${CUDA_VERSION}/local_installers/cuda_${CUDA_VERSION}_${CUDA_BUILD}_linux${cuda_suffix}.run" +echo "cuda url: ${url}" +wget "$url" --progress=bar:force:noscroll -q --show-progress -O ./cuda.run +chmod a+x ./cuda.run +./cuda.run --silent --toolkit --toolkitpath=/usr/local --no-opengl-libs --no-man-page --no-drm +rm ./cuda.run +_INSTALL_CUDA diff --git a/docker/debian-bookworm.dockerfile b/docker/debian-bookworm.dockerfile index 6eb1108bf11..a62e092eab4 100644 --- a/docker/debian-bookworm.dockerfile +++ b/docker/debian-bookworm.dockerfile @@ -61,6 +61,7 @@ apt-get install -y --no-install-recommends \ libxtst-dev \ nodejs \ npm \ + udev \ wget if [[ "${TARGETPLATFORM}" == 'linux/amd64' ]]; then apt-get install -y --no-install-recommends \ diff --git a/docker/debian-bullseye.dockerfile b/docker/debian-bullseye.dockerfile index d9862432cec..f355307631d 100644 --- a/docker/debian-bullseye.dockerfile +++ b/docker/debian-bullseye.dockerfile @@ -60,6 +60,7 @@ apt-get install -y --no-install-recommends \ libxfixes-dev \ libxrandr-dev \ libxtst-dev \ + udev \ wget if [[ "${TARGETPLATFORM}" == 'linux/amd64' ]]; then apt-get install -y --no-install-recommends \ diff --git a/docker/ubuntu-20.04.dockerfile b/docker/ubuntu-20.04.dockerfile index 4715475ac6e..d677830db5c 100644 --- a/docker/ubuntu-20.04.dockerfile +++ b/docker/ubuntu-20.04.dockerfile @@ -61,6 +61,7 @@ apt-get install -y --no-install-recommends \ libxfixes-dev \ libxrandr-dev \ libxtst-dev \ + udev \ wget if [[ "${TARGETPLATFORM}" == 'linux/amd64' ]]; then apt-get install -y --no-install-recommends \ diff --git a/docker/ubuntu-22.04.dockerfile b/docker/ubuntu-22.04.dockerfile index 0e975df0558..ab6ec096a3b 100644 --- a/docker/ubuntu-22.04.dockerfile +++ b/docker/ubuntu-22.04.dockerfile @@ -60,6 +60,7 @@ apt-get install -y --no-install-recommends \ libxfixes-dev \ libxrandr-dev \ libxtst-dev \ + udev \ wget if [[ "${TARGETPLATFORM}" == 'linux/amd64' ]]; then apt-get install -y --no-install-recommends \ diff --git a/docs/requirements.txt b/docs/requirements.txt index ecdb40c475f..688a0a896ce 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,5 +1,5 @@ breathe==4.35.0 -furo==2023.9.10 +furo==2024.1.29 m2r2==0.3.3.post2 rstcheck[sphinx]==6.2.0 rstfmt==0.0.14 diff --git a/docs/source/about/advanced_usage.rst b/docs/source/about/advanced_usage.rst index 08fb5834ace..fe603e38174 100644 --- a/docs/source/about/advanced_usage.rst +++ b/docs/source/about/advanced_usage.rst @@ -975,19 +975,19 @@ keybindings ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ **Description** - Minimum number of threads used for software encoding. + Minimum number of CPU threads used for encoding. .. note:: 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. **Default** - ``1`` + ``2`` **Example** .. code-block:: text - min_threads = 1 + min_threads = 2 `hevc_mode `__ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -1182,6 +1182,59 @@ keybindings nvenc_twopass = quarter_res +`nvenc_spatial_aq `__ +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +**Description** + Assign higher QP values to flat regions of the video. + Recommended to enable when streaming at lower bitrates. + + .. Note:: This option only applies when using NVENC `encoder`_. + +**Choices** + +.. table:: + :widths: auto + + ========== =========== + Value Description + ========== =========== + disabled Don't enable Spatial AQ (faster) + enabled Enable Spatial AQ (slower) + ========== =========== + +**Default** + ``disabled`` + +**Example** + .. code-block:: text + + nvenc_spatial_aq = disabled + +`nvenc_vbv_increase `__ +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +**Description** + Single-frame VBV/HRD percentage increase. + By default sunshine uses single-frame VBV/HRD, which means any encoded video frame size is not expected to exceed requested bitrate divided by requested frame rate. + Relaxing this restriction can be beneficial and act as low-latency variable bitrate, but may also lead to packet loss if the network doesn't have buffer headroom to handle bitrate spikes. + Maximum accepted value is 400, which corresponds to 5x increased encoded video frame upper size limit. + + .. Note:: This option only applies when using NVENC `encoder`_. + + .. Warning:: Can lead to network packet loss. + +**Default** + ``0`` + +**Range** + ``0-400`` + +**Example** + .. code-block:: text + + nvenc_vbv_increase = 0 + `nvenc_realtime_hags `__ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -1214,6 +1267,69 @@ keybindings nvenc_realtime_hags = enabled +`nvenc_latency_over_power `__ +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +**Description** + Adaptive P-State algorithm which NVIDIA drivers employ doesn't work well with low latency streaming, so sunshine requests high power mode explicitly. + + .. Note:: This option only applies when using NVENC `encoder`_. + + .. Warning:: Disabling it is not recommended since this can lead to significantly increased encoding latency. + + .. Caution:: Applies to Windows only. + +**Choices** + +.. table:: + :widths: auto + + ========== =========== + Value Description + ========== =========== + disabled Sunshine doesn't change GPU power preferences (not recommended) + enabled Sunshine requests high power mode explicitly + ========== =========== + +**Default** + ``enabled`` + +**Example** + .. code-block:: text + + nvenc_latency_over_power = enabled + +`nvenc_opengl_vulkan_on_dxgi `__ +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +**Description** + Sunshine can't capture fullscreen OpenGL and Vulkan programs at full frame rate unless they present on top of DXGI. + This is system-wide setting that is reverted on sunshine program exit. + + .. Note:: This option only applies when using NVENC `encoder`_. + + .. Caution:: Applies to Windows only. + +**Choices** + +.. table:: + :widths: auto + + ========== =========== + Value Description + ========== =========== + disabled Sunshine leaves global Vulkan/OpenGL present method unchanged + enabled Sunshine changes global Vulkan/OpenGL present method to "Prefer layered on DXGI Swapchain" + ========== =========== + +**Default** + ``enabled`` + +**Example** + .. code-block:: text + + nvenc_opengl_vulkan_on_dxgi = enabled + `nvenc_h264_cavlc `__ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/docs/source/about/guides/app_examples.rst b/docs/source/about/guides/app_examples.rst index 514bb5c710d..ca834e4a4f2 100644 --- a/docs/source/about/guides/app_examples.rst +++ b/docs/source/about/guides/app_examples.rst @@ -48,13 +48,13 @@ Steam Big Picture .. tab:: Windows - +----------------------+-----------------------------------+ - | Application Name | ``Steam Big Picture`` | - +----------------------+-----------------------------------+ - | Detached Commands | ``steam steam://open/bigpicture`` | - +----------------------+-----------------------------------+ - | Image | ``steam.png`` | - +----------------------+-----------------------------------+ + +----------------------+-----------------------------+ + | Application Name | ``Steam Big Picture`` | + +----------------------+-----------------------------+ + | Detached Commands | ``steam://open/bigpicture`` | + +----------------------+-----------------------------+ + | Image | ``steam.png`` | + +----------------------+-----------------------------+ Epic Game Store game ^^^^^^^^^^^^^^^^^^^^ @@ -67,11 +67,11 @@ URI (Epic) .. tab:: Windows - +----------------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------+ - | Application Name | ``Surviving Mars`` | - +----------------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------+ - | Detached Commands | ``cmd /C "start com.epicgames.launcher://apps/d759128018124dcabb1fbee9bb28e178%3A20729b9176c241f0b617c5723e70ec2d%3AOvenbird?action=launch&silent=true"`` | - +----------------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------+ + +----------------------+--------------------------------------------------------------------------------------------------------------------------------------------+ + | Application Name | ``Surviving Mars`` | + +----------------------+--------------------------------------------------------------------------------------------------------------------------------------------+ + | Detached Commands | ``com.epicgames.launcher://apps/d759128018124dcabb1fbee9bb28e178%3A20729b9176c241f0b617c5723e70ec2d%3AOvenbird?action=launch&silent=true`` | + +----------------------+--------------------------------------------------------------------------------------------------------------------------------------------+ Binary (Epic w/ working directory) """""""""""""""""""""""""""""""""" @@ -124,11 +124,11 @@ URI (Steam) .. tab:: Windows - +----------------------+---------------------------------------------+ - | Application Name | ``Surviving Mars`` | - +----------------------+---------------------------------------------+ - | Detached Commands | ``cmd /C "start steam://rungameid/464920"`` | - +----------------------+---------------------------------------------+ + +----------------------+------------------------------+ + | Application Name | ``Surviving Mars`` | + +----------------------+------------------------------+ + | Detached Commands | ``steam://rungameid/464920`` | + +----------------------+------------------------------+ Binary (Steam w/ working directory) """"""""""""""""""""""""""""""""""" @@ -258,11 +258,11 @@ Changing Resolution and Refresh Rate .. tab:: KDE Plasma (Wayland, X11) - +----------------------+----------------------------------------------------------------------------------------------------------------------------------+ - | Command Preparations | Do: ``sh -c "kscreen-doctor output.HDMI-A-1.mode.${SUNSHINE_CLIENT_WIDTH}x${SUNSHINE_CLIENT_HEIGHT}@${SUNSHINE_CLIENT_FPS}"`` | - | +----------------------------------------------------------------------------------------------------------------------------------+ - | | Undo: ``kscreen-doctor output.HDMI-A-1.mode.3840x2160@120`` | - +----------------------+----------------------------------------------------------------------------------------------------------------------------------+ + +----------------------+-------------------------------------------------------------------------------------------------------------------------------+ + | Command Preparations | Do: ``sh -c "kscreen-doctor output.HDMI-A-1.mode.${SUNSHINE_CLIENT_WIDTH}x${SUNSHINE_CLIENT_HEIGHT}@${SUNSHINE_CLIENT_FPS}"`` | + | +-------------------------------------------------------------------------------------------------------------------------------+ + | | Undo: ``kscreen-doctor output.HDMI-A-1.mode.3840x2160@120`` | + +----------------------+-------------------------------------------------------------------------------------------------------------------------------+ .. tab:: NVIDIA diff --git a/docs/source/about/setup.rst b/docs/source/about/setup.rst index b2d2421ba56..8a280fcf644 100644 --- a/docs/source/about/setup.rst +++ b/docs/source/about/setup.rst @@ -310,7 +310,7 @@ Install ``Ctrl+x``, then ``Y`` to exit and save changes. - #. Download the ``Portfile`` to ``~/Downloads`` and run the following code. + #. Download and install by running the following code. .. code-block:: bash diff --git a/docs/source/building/linux.rst b/docs/source/building/linux.rst index fdd8be7f155..789409e4174 100644 --- a/docs/source/building/linux.rst +++ b/docs/source/building/linux.rst @@ -106,6 +106,7 @@ Install Requirements libboost-log-dev \ libboost-program-options-dev \ libcap-dev \ # KMS + libcurl4-openssl-dev \ libdrm-dev \ # KMS libevdev-dev \ libminiupnpc-dev \ @@ -156,6 +157,7 @@ Install Requirements libboost-log-dev \ libboost-program-options-dev \ libcap-dev \ # KMS + libcurl4-openssl-dev \ libdrm-dev \ # KMS libevdev-dev \ libminiupnpc-dev \ diff --git a/docs/source/source_code/src/file_handler.rst b/docs/source/source_code/src/file_handler.rst new file mode 100644 index 00000000000..221b8cbde03 --- /dev/null +++ b/docs/source/source_code/src/file_handler.rst @@ -0,0 +1,5 @@ +file_handler +============ + +.. doxygenfile:: file_handler.h + :allow-dot-graphs: diff --git a/docs/source/source_code/src/logging.rst b/docs/source/source_code/src/logging.rst new file mode 100644 index 00000000000..6b037c20e46 --- /dev/null +++ b/docs/source/source_code/src/logging.rst @@ -0,0 +1,5 @@ +logging +======= + +.. doxygenfile:: logging.h + :allow-dot-graphs: diff --git a/package.json b/package.json index b9a70637337..4a709402bb3 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ "@popperjs/core": "2.11.8", "@vitejs/plugin-vue": "4.6.2", "bootstrap": "5.3.2", - "vite": "4.4.9", + "vite": "4.5.2", "vite-plugin-ejs": "1.6.4", "vue": "3.4.5" } diff --git a/src/audio.cpp b/src/audio.cpp index 44791122b72..a3555eaa080 100644 --- a/src/audio.cpp +++ b/src/audio.cpp @@ -10,6 +10,7 @@ #include "audio.h" #include "config.h" +#include "logging.h" #include "main.h" #include "thread_safe.h" #include "utility.h" diff --git a/src/cbs.cpp b/src/cbs.cpp index c06b7c4a8b5..a2ba6f2517e 100644 --- a/src/cbs.cpp +++ b/src/cbs.cpp @@ -11,7 +11,7 @@ extern "C" { } #include "cbs.h" -#include "main.h" +#include "logging.h" #include "utility.h" using namespace std::literals; diff --git a/src/config.cpp b/src/config.cpp index eb25f58a7da..5cd08cee94e 100644 --- a/src/config.cpp +++ b/src/config.cpp @@ -15,6 +15,8 @@ #include #include "config.h" +#include "file_handler.h" +#include "logging.h" #include "main.h" #include "nvhttp.h" #include "rtsp.h" @@ -320,7 +322,7 @@ namespace config { 0, // hevc_mode 0, // av1_mode - 1, // min_threads + 2, // min_threads { "superfast"s, // preset "zerolatency"s, // tune @@ -329,6 +331,8 @@ namespace config { {}, // nv true, // nv_realtime_hags + true, // nv_opengl_vulkan_on_dxgi + true, // nv_sunshine_high_power_mode {}, // nv_legacy { @@ -937,9 +941,13 @@ namespace config { string_f(vars, "sw_tune", video.sw.sw_tune); int_between_f(vars, "nvenc_preset", video.nv.quality_preset, { 1, 7 }); + int_between_f(vars, "nvenc_vbv_increase", video.nv.vbv_percentage_increase, { 0, 400 }); + bool_f(vars, "nvenc_spatial_aq", video.nv.adaptive_quantization); generic_f(vars, "nvenc_twopass", video.nv.two_pass, nv::twopass_from_view); bool_f(vars, "nvenc_h264_cavlc", video.nv.h264_cavlc); bool_f(vars, "nvenc_realtime_hags", video.nv_realtime_hags); + bool_f(vars, "nvenc_opengl_vulkan_on_dxgi", video.nv_opengl_vulkan_on_dxgi); + bool_f(vars, "nvenc_latency_over_power", video.nv_sunshine_high_power_mode); #ifndef __APPLE__ video.nv_legacy.preset = video.nv.quality_preset + 11; @@ -947,6 +955,8 @@ namespace config { video.nv.two_pass == nvenc::nvenc_two_pass::full_resolution ? NV_ENC_TWO_PASS_FULL_RESOLUTION : NV_ENC_MULTI_PASS_DISABLED; video.nv_legacy.h264_coder = video.nv.h264_cavlc ? NV_ENC_H264_ENTROPY_CODING_MODE_CAVLC : NV_ENC_H264_ENTROPY_CODING_MODE_CABAC; + video.nv_legacy.aq = video.nv.adaptive_quantization; + video.nv_legacy.vbv_percentage_increase = video.nv.vbv_percentage_increase; #endif int_f(vars, "qsv_preset", video.qsv.qsv_preset, qsv::preset_from_view); @@ -1206,7 +1216,7 @@ namespace config { } // Read config file - auto vars = parse_config(read_file(sunshine.config_file.c_str())); + auto vars = parse_config(file_handler::read_file(sunshine.config_file.c_str())); for (auto &[name, value] : cmd_vars) { vars.insert_or_assign(std::move(name), std::move(value)); diff --git a/src/config.h b/src/config.h index ba0ee8a37fd..e08a87f3c4d 100644 --- a/src/config.h +++ b/src/config.h @@ -30,11 +30,15 @@ namespace config { nvenc::nvenc_config nv; bool nv_realtime_hags; + bool nv_opengl_vulkan_on_dxgi; + bool nv_sunshine_high_power_mode; struct { int preset; int multipass; int h264_coder; + int aq; + int vbv_percentage_increase; } nv_legacy; struct { diff --git a/src/confighttp.cpp b/src/confighttp.cpp index 61331d1fc57..9328e27a15a 100644 --- a/src/confighttp.cpp +++ b/src/confighttp.cpp @@ -29,7 +29,9 @@ #include "config.h" #include "confighttp.h" #include "crypto.h" +#include "file_handler.h" #include "httpcommon.h" +#include "logging.h" #include "main.h" #include "network.h" #include "nvhttp.h" @@ -161,7 +163,7 @@ namespace confighttp { print_req(request); - std::string content = read_file(WEB_DIR "index.html"); + std::string content = file_handler::read_file(WEB_DIR "index.html"); SimpleWeb::CaseInsensitiveMultimap headers; headers.emplace("Content-Type", "text/html; charset=utf-8"); response->write(content, headers); @@ -173,7 +175,7 @@ namespace confighttp { print_req(request); - std::string content = read_file(WEB_DIR "pin.html"); + std::string content = file_handler::read_file(WEB_DIR "pin.html"); SimpleWeb::CaseInsensitiveMultimap headers; headers.emplace("Content-Type", "text/html; charset=utf-8"); response->write(content, headers); @@ -185,7 +187,7 @@ namespace confighttp { print_req(request); - std::string content = read_file(WEB_DIR "apps.html"); + std::string content = file_handler::read_file(WEB_DIR "apps.html"); SimpleWeb::CaseInsensitiveMultimap headers; headers.emplace("Content-Type", "text/html; charset=utf-8"); headers.emplace("Access-Control-Allow-Origin", "https://images.igdb.com/"); @@ -198,7 +200,7 @@ namespace confighttp { print_req(request); - std::string content = read_file(WEB_DIR "clients.html"); + std::string content = file_handler::read_file(WEB_DIR "clients.html"); SimpleWeb::CaseInsensitiveMultimap headers; headers.emplace("Content-Type", "text/html; charset=utf-8"); response->write(content, headers); @@ -210,7 +212,7 @@ namespace confighttp { print_req(request); - std::string content = read_file(WEB_DIR "config.html"); + std::string content = file_handler::read_file(WEB_DIR "config.html"); SimpleWeb::CaseInsensitiveMultimap headers; headers.emplace("Content-Type", "text/html; charset=utf-8"); response->write(content, headers); @@ -222,7 +224,7 @@ namespace confighttp { print_req(request); - std::string content = read_file(WEB_DIR "password.html"); + std::string content = file_handler::read_file(WEB_DIR "password.html"); SimpleWeb::CaseInsensitiveMultimap headers; headers.emplace("Content-Type", "text/html; charset=utf-8"); response->write(content, headers); @@ -235,7 +237,7 @@ namespace confighttp { send_redirect(response, request, "/"); return; } - std::string content = read_file(WEB_DIR "welcome.html"); + std::string content = file_handler::read_file(WEB_DIR "welcome.html"); SimpleWeb::CaseInsensitiveMultimap headers; headers.emplace("Content-Type", "text/html; charset=utf-8"); response->write(content, headers); @@ -247,7 +249,7 @@ namespace confighttp { print_req(request); - std::string content = read_file(WEB_DIR "troubleshooting.html"); + std::string content = file_handler::read_file(WEB_DIR "troubleshooting.html"); SimpleWeb::CaseInsensitiveMultimap headers; headers.emplace("Content-Type", "text/html; charset=utf-8"); response->write(content, headers); @@ -323,7 +325,7 @@ namespace confighttp { print_req(request); - std::string content = read_file(config::stream.file_apps.c_str()); + std::string content = file_handler::read_file(config::stream.file_apps.c_str()); SimpleWeb::CaseInsensitiveMultimap headers; headers.emplace("Content-Type", "application/json"); response->write(content, headers); @@ -335,7 +337,7 @@ namespace confighttp { print_req(request); - std::string content = read_file(config::sunshine.log_file.c_str()); + std::string content = file_handler::read_file(config::sunshine.log_file.c_str()); SimpleWeb::CaseInsensitiveMultimap headers; headers.emplace("Content-Type", "text/plain"); response->write(SimpleWeb::StatusCode::success_ok, content, headers); @@ -541,7 +543,7 @@ namespace confighttp { outputTree.put("platform", SUNSHINE_PLATFORM); outputTree.put("version", PROJECT_VER); - auto vars = config::parse_config(read_file(config::sunshine.config_file.c_str())); + auto vars = config::parse_config(file_handler::read_file(config::sunshine.config_file.c_str())); for (auto &[name, value] : vars) { outputTree.put(std::move(name), std::move(value)); @@ -574,7 +576,7 @@ namespace confighttp { configStream << kv.first << " = " << value << std::endl; } - write_file(config::sunshine.config_file.c_str(), configStream.str()); + file_handler::write_file(config::sunshine.config_file.c_str(), configStream.str()); } catch (std::exception &e) { BOOST_LOG(warning) << "SaveConfig: "sv << e.what(); @@ -776,7 +778,7 @@ namespace confighttp { start() { auto shutdown_event = mail::man->event(mail::shutdown); - auto port_https = map_port(PORT_HTTPS); + auto port_https = net::map_port(PORT_HTTPS); auto address_family = net::af_from_enum_string(config::sunshine.address_family); https_server_t server { config::nvhttp.cert, config::nvhttp.pkey }; diff --git a/src/file_handler.cpp b/src/file_handler.cpp new file mode 100644 index 00000000000..d0783431808 --- /dev/null +++ b/src/file_handler.cpp @@ -0,0 +1,69 @@ +/** + * @file file_handler.cpp + * @brief File handling functions. + */ + +// standard includes +#include +#include + +// local includes +#include "file_handler.h" +#include "logging.h" + +namespace file_handler { + + /** + * @brief Read a file to string. + * @param path The path of the file. + * @return `std::string` : The contents of the file. + * + * EXAMPLES: + * ```cpp + * std::string contents = read_file("path/to/file"); + * ``` + */ + std::string + read_file(const char *path) { + if (!std::filesystem::exists(path)) { + BOOST_LOG(debug) << "Missing file: " << path; + return {}; + } + + std::ifstream in(path); + + std::string input; + std::string base64_cert; + + while (!in.eof()) { + std::getline(in, input); + base64_cert += input + '\n'; + } + + return base64_cert; + } + + /** + * @brief Writes a file. + * @param path The path of the file. + * @param contents The contents to write. + * @return `int` : `0` on success, `-1` on failure. + * + * EXAMPLES: + * ```cpp + * int write_status = write_file("path/to/file", "file contents"); + * ``` + */ + 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; + } +} // namespace file_handler diff --git a/src/file_handler.h b/src/file_handler.h new file mode 100644 index 00000000000..aa2387f8469 --- /dev/null +++ b/src/file_handler.h @@ -0,0 +1,14 @@ +/** + * @file file_handler.h + * @brief Header file for file handling functions. + */ +#pragma once + +#include + +namespace file_handler { + std::string + read_file(const char *path); + int + write_file(const char *path, const std::string_view &contents); +} // namespace file_handler diff --git a/src/httpcommon.cpp b/src/httpcommon.cpp index b6ea0958105..aa92b3bd3d8 100644 --- a/src/httpcommon.cpp +++ b/src/httpcommon.cpp @@ -21,8 +21,9 @@ #include "config.h" #include "crypto.h" +#include "file_handler.h" #include "httpcommon.h" -#include "main.h" +#include "logging.h" #include "network.h" #include "nvhttp.h" #include "platform/common.h" @@ -160,12 +161,12 @@ namespace http { return -1; } - if (write_file(pkey.c_str(), creds.pkey)) { + if (file_handler::write_file(pkey.c_str(), creds.pkey)) { BOOST_LOG(error) << "Couldn't open ["sv << config::nvhttp.pkey << ']'; return -1; } - if (write_file(cert.c_str(), creds.x509)) { + if (file_handler::write_file(cert.c_str(), creds.x509)) { BOOST_LOG(error) << "Couldn't open ["sv << config::nvhttp.cert << ']'; return -1; } diff --git a/src/input.cpp b/src/input.cpp index 63c10d8be77..b7416fffac2 100644 --- a/src/input.cpp +++ b/src/input.cpp @@ -18,6 +18,7 @@ extern "C" { #include "config.h" #include "input.h" +#include "logging.h" #include "main.h" #include "platform/common.h" #include "thread_pool.h" diff --git a/src/logging.cpp b/src/logging.cpp new file mode 100644 index 00000000000..70a2ae82a00 --- /dev/null +++ b/src/logging.cpp @@ -0,0 +1,73 @@ +/** + * @file src/logging.cpp + * @brief Logging implementation file for the Sunshine application. + */ + +// standard includes +#include + +// lib includes +#include +#include +#include +#include +#include + +// local includes +#include "logging.h" + +using namespace std::literals; + +namespace bl = boost::log; + +boost::shared_ptr> sink; + +bl::sources::severity_logger verbose(0); // Dominating output +bl::sources::severity_logger debug(1); // Follow what is happening +bl::sources::severity_logger info(2); // Should be informed about +bl::sources::severity_logger warning(3); // Strange events +bl::sources::severity_logger error(4); // Recoverable errors +bl::sources::severity_logger fatal(5); // Unrecoverable errors + +/** + * @brief Flush the log. + * + * EXAMPLES: + * ```cpp + * log_flush(); + * ``` + */ +void +log_flush() { + sink->flush(); +} + +/** + * @brief Print help to stdout. + * @param name The name of the program. + * + * EXAMPLES: + * ```cpp + * print_help("sunshine"); + * ``` + */ +void +print_help(const char *name) { + std::cout + << "Usage: "sv << name << " [options] [/path/to/configuration_file] [--cmd]"sv << std::endl + << " Any configurable option can be overwritten with: \"name=value\""sv << std::endl + << std::endl + << " Note: The configuration will be created if it doesn't exist."sv << std::endl + << std::endl + << " --help | print help"sv << std::endl + << " --creds username password | set user credentials for the Web manager"sv << std::endl + << " --version | print the version of sunshine"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 << std::endl + << " -2 | Force replacement of headers in video stream"sv << std::endl + << " -p | Enable/Disable UPnP"sv << std::endl + << std::endl; +} diff --git a/src/logging.h b/src/logging.h new file mode 100644 index 00000000000..47a08555a0b --- /dev/null +++ b/src/logging.h @@ -0,0 +1,27 @@ +/** + * @file src/logging.h + * @brief Logging header file for the Sunshine application. + */ + +// macros +#pragma once + +// lib includes +#include +#include + +extern boost::shared_ptr> sink; +using text_sink = boost::log::sinks::asynchronous_sink; + +extern boost::log::sources::severity_logger verbose; +extern boost::log::sources::severity_logger debug; +extern boost::log::sources::severity_logger info; +extern boost::log::sources::severity_logger warning; +extern boost::log::sources::severity_logger error; +extern boost::log::sources::severity_logger fatal; + +// functions +void +log_flush(); +void +print_help(const char *name); diff --git a/src/main.cpp b/src/main.cpp index 45febf7025b..1d1bb305398 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -21,7 +21,9 @@ #include "config.h" #include "confighttp.h" #include "httpcommon.h" +#include "logging.h" #include "main.h" +#include "network.h" #include "nvhttp.h" #include "platform/common.h" #include "process.h" @@ -52,18 +54,9 @@ nvprefs::nvprefs_interface nvprefs_instance; #endif thread_pool_util::ThreadPool task_pool; -bl::sources::severity_logger verbose(0); // Dominating output -bl::sources::severity_logger debug(1); // Follow what is happening -bl::sources::severity_logger info(2); // Should be informed about -bl::sources::severity_logger warning(3); // Strange events -bl::sources::severity_logger error(4); // Recoverable errors -bl::sources::severity_logger fatal(5); // Unrecoverable errors bool display_cursor = true; -using text_sink = bl::sinks::asynchronous_sink; -boost::shared_ptr sink; - struct NoDelete { void operator()(void *) {} @@ -71,36 +64,6 @@ struct NoDelete { BOOST_LOG_ATTRIBUTE_KEYWORD(severity, "Severity", int) -/** - * @brief Print help to stdout. - * @param name The name of the program. - * - * EXAMPLES: - * ```cpp - * print_help("sunshine"); - * ``` - */ -void -print_help(const char *name) { - std::cout - << "Usage: "sv << name << " [options] [/path/to/configuration_file] [--cmd]"sv << std::endl - << " Any configurable option can be overwritten with: \"name=value\""sv << std::endl - << std::endl - << " Note: The configuration will be created if it doesn't exist."sv << std::endl - << std::endl - << " --help | print help"sv << std::endl - << " --creds username password | set user credentials for the Web manager"sv << std::endl - << " --version | print the version of sunshine"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 << std::endl - << " -2 | Force replacement of headers in video stream"sv << std::endl - << " -p | Enable/Disable UPnP"sv << std::endl - << std::endl; -} - namespace help { int entry(const char *name, int argc, char *argv[]) { @@ -335,7 +298,7 @@ namespace service_ctrl { return false; } - uint16_t port_nbo = htons(map_port(confighttp::PORT_HTTPS)); + uint16_t port_nbo = htons(net::map_port(confighttp::PORT_HTTPS)); for (DWORD i = 0; i < tcp_table->dwNumEntries; i++) { auto &entry = tcp_table->table[i]; @@ -386,7 +349,7 @@ is_gamestream_enabled() { */ void launch_ui() { - std::string url = "https://localhost:" + std::to_string(map_port(confighttp::PORT_HTTPS)); + std::string url = "https://localhost:" + std::to_string(net::map_port(confighttp::PORT_HTTPS)); platf::open_url(url); } @@ -400,23 +363,10 @@ launch_ui() { */ void launch_ui_with_path(std::string path) { - std::string url = "https://localhost:" + std::to_string(map_port(confighttp::PORT_HTTPS)) + path; + std::string url = "https://localhost:" + std::to_string(net::map_port(confighttp::PORT_HTTPS)) + path; platf::open_url(url); } -/** - * @brief Flush the log. - * - * EXAMPLES: - * ```cpp - * log_flush(); - * ``` - */ -void -log_flush() { - sink->flush(); -} - std::map> signal_handlers; void on_signal_forwarder(int sig) { @@ -488,6 +438,9 @@ SessionMonitorWindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) { */ int main(int argc, char *argv[]) { + // the version should be printed to the log before anything else + BOOST_LOG(info) << PROJECT_NAME << " version: " << PROJECT_VER; + lifetime::argv = argv; task_pool_util::TaskPool::task_id_t force_shutdown = nullptr; @@ -689,7 +642,6 @@ main(int argc, char *argv[]) { #endif - BOOST_LOG(info) << PROJECT_NAME << " version: " << PROJECT_VER << std::endl; task_pool.start(1); #if defined SUNSHINE_TRAY && SUNSHINE_TRAY >= 1 @@ -806,82 +758,3 @@ main(int argc, char *argv[]) { return lifetime::desired_exit_code; } - -/** - * @brief Read a file to string. - * @param path The path of the file. - * @return `std::string` : The contents of the file. - * - * EXAMPLES: - * ```cpp - * std::string contents = read_file("path/to/file"); - * ``` - */ -std::string -read_file(const char *path) { - if (!std::filesystem::exists(path)) { - BOOST_LOG(debug) << "Missing file: " << path; - return {}; - } - - std::ifstream in(path); - - std::string input; - std::string base64_cert; - - while (!in.eof()) { - std::getline(in, input); - base64_cert += input + '\n'; - } - - return base64_cert; -} - -/** - * @brief Writes a file. - * @param path The path of the file. - * @param contents The contents to write. - * @return `int` : `0` on success, `-1` on failure. - * - * EXAMPLES: - * ```cpp - * int write_status = write_file("path/to/file", "file contents"); - * ``` - */ -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; -} - -/** - * @brief Map a specified port based on the base port. - * @param port The port to map as a difference from the base port. - * @return `std:uint16_t` : The mapped port number. - * - * EXAMPLES: - * ```cpp - * std::uint16_t mapped_port = map_port(1); - * ``` - */ -std::uint16_t -map_port(int port) { - // calculate the port from the config port - auto mapped_port = (std::uint16_t)((int) config::sunshine.port + port); - - // Ensure port is in the range of 1024-65535 - if (mapped_port < 1024 || mapped_port > 65535) { - BOOST_LOG(warning) << "Port out of range: "sv << mapped_port; - } - - // TODO: Ensure port is not already in use by another application - - return mapped_port; -} diff --git a/src/main.h b/src/main.h index 8eb3e7ee277..02a21fd321e 100644 --- a/src/main.h +++ b/src/main.h @@ -10,9 +10,6 @@ #include #include -// lib includes -#include - // local includes #include "thread_pool.h" #include "thread_safe.h" @@ -26,27 +23,10 @@ extern nvprefs::nvprefs_interface nvprefs_instance; extern thread_pool_util::ThreadPool task_pool; extern bool display_cursor; -extern boost::log::sources::severity_logger verbose; -extern boost::log::sources::severity_logger debug; -extern boost::log::sources::severity_logger info; -extern boost::log::sources::severity_logger warning; -extern boost::log::sources::severity_logger error; -extern boost::log::sources::severity_logger fatal; - // functions int main(int argc, char *argv[]); void -log_flush(); -void -print_help(const char *name); -std::string -read_file(const char *path); -int -write_file(const char *path, const std::string_view &contents); -std::uint16_t -map_port(int port); -void launch_ui(); void launch_ui_with_path(std::string path); diff --git a/src/network.cpp b/src/network.cpp index 5778c8db9ad..5c158ee6c52 100644 --- a/src/network.cpp +++ b/src/network.cpp @@ -3,6 +3,8 @@ * @brief todo */ #include "network.h" +#include "config.h" +#include "logging.h" #include "utility.h" #include @@ -174,13 +176,39 @@ namespace net { } } + /** + * @brief Returns the encryption mode for the given remote endpoint address. + * @param address The address used to look up the desired encryption mode. + * @return The WAN or LAN encryption mode, based on the provided address. + */ + int + encryption_mode_for_address(boost::asio::ip::address address) { + auto nettype = net::from_address(address.to_string()); + if (nettype == net::net_e::PC || nettype == net::net_e::LAN) { + return config::stream.lan_encryption_mode; + } + else { + return config::stream.wan_encryption_mode; + } + } + host_t host_create(af_e af, ENetAddress &addr, std::size_t peers, std::uint16_t port) { + static std::once_flag enet_init_flag; + std::call_once(enet_init_flag, []() { + enet_initialize(); + }); + auto any_addr = net::af_to_any_address_string(af); enet_address_set_host(&addr, any_addr.data()); enet_address_set_port(&addr, port); - return host_t { enet_host_create(af == IPV4 ? AF_INET : AF_INET6, &addr, peers, 0, 0, 0) }; + auto host = host_t { enet_host_create(af == IPV4 ? AF_INET : AF_INET6, &addr, peers, 0, 0, 0) }; + + // Enable opportunistic QoS tagging (automatically disables if the network appears to drop tagged packets) + enet_socket_set_option(host->socket, ENET_SOCKOPT_QOS, 1); + + return host; } void @@ -195,4 +223,29 @@ namespace net { enet_host_destroy(host); } + + /** + * @brief Map a specified port based on the base port. + * @param port The port to map as a difference from the base port. + * @return `std:uint16_t` : The mapped port number. + * + * EXAMPLES: + * ```cpp + * std::uint16_t mapped_port = net::map_port(1); + * ``` + */ + std::uint16_t + map_port(int port) { + // calculate the port from the config port + auto mapped_port = (std::uint16_t)((int) config::sunshine.port + port); + + // Ensure port is in the range of 1024-65535 + if (mapped_port < 1024 || mapped_port > 65535) { + BOOST_LOG(warning) << "Port out of range: "sv << mapped_port; + } + + // TODO: Ensure port is not already in use by another application + + return mapped_port; + } } // namespace net diff --git a/src/network.h b/src/network.h index 57bc65e8fc3..a4f74fb404d 100644 --- a/src/network.h +++ b/src/network.h @@ -16,6 +16,9 @@ namespace net { void free_host(ENetHost *host); + std::uint16_t + map_port(int port); + using host_t = util::safe_ptr; using peer_t = ENetPeer *; using packet_t = util::safe_ptr; @@ -84,4 +87,12 @@ namespace net { */ std::string addr_to_url_escaped_string(boost::asio::ip::address address); + + /** + * @brief Returns the encryption mode for the given remote endpoint address. + * @param address The address used to look up the desired encryption mode. + * @return The WAN or LAN encryption mode, based on the provided address. + */ + int + encryption_mode_for_address(boost::asio::ip::address address); } // namespace net diff --git a/src/nvenc/nvenc_base.cpp b/src/nvenc/nvenc_base.cpp index f305f7682ad..63107324f37 100644 --- a/src/nvenc/nvenc_base.cpp +++ b/src/nvenc/nvenc_base.cpp @@ -1,6 +1,7 @@ #include "nvenc_base.h" #include "src/config.h" +#include "src/logging.h" #include "src/utility.h" namespace { @@ -222,6 +223,9 @@ namespace nvenc { if (get_encoder_cap(NV_ENC_CAPS_SUPPORT_CUSTOM_VBV_BUF_SIZE)) { enc_config.rcParams.vbvBufferSize = client_config.bitrate * 1000 / client_config.framerate; + if (config.vbv_percentage_increase > 0) { + enc_config.rcParams.vbvBufferSize += enc_config.rcParams.vbvBufferSize * config.vbv_percentage_increase / 100; + } } auto set_h264_hevc_common_format_config = [&](auto &format_config) { @@ -369,9 +373,10 @@ namespace nvenc { if (init_params.enableEncodeAsync) extra += " async"; if (buffer_is_10bit()) extra += " 10-bit"; if (enc_config.rcParams.multiPass != NV_ENC_MULTI_PASS_DISABLED) extra += " two-pass"; + if (config.vbv_percentage_increase > 0 && get_encoder_cap(NV_ENC_CAPS_SUPPORT_CUSTOM_VBV_BUF_SIZE)) extra += " vbv+" + std::to_string(config.vbv_percentage_increase); if (encoder_params.rfi) extra += " rfi"; if (init_params.enableWeightedPrediction) extra += " weighted-prediction"; - if (enc_config.rcParams.enableAQ) extra += " adaptive-quantization"; + if (enc_config.rcParams.enableAQ) extra += " spatial-aq"; if (enc_config.rcParams.enableMinQP) extra += " qpmin=" + std::to_string(enc_config.rcParams.minQP.qpInterP); if (config.insert_filler_data) extra += " filler-data"; BOOST_LOG(info) << "NvEnc: created encoder " << quality_preset_string_from_guid(init_params.presetGUID) << extra; diff --git a/src/nvenc/nvenc_config.h b/src/nvenc/nvenc_config.h index 632146b7db0..c4aae12a86e 100644 --- a/src/nvenc/nvenc_config.h +++ b/src/nvenc/nvenc_config.h @@ -20,6 +20,9 @@ namespace nvenc { // Use optional preliminary pass for better motion vectors, bitrate distribution and stricter VBV(HRD), uses CUDA cores nvenc_two_pass two_pass = nvenc_two_pass::quarter_resolution; + // Percentage increase of VBV/HRD from the default single frame, allows low-latency variable bitrate + int vbv_percentage_increase = 0; + // Improves fades compression, uses CUDA cores bool weighted_prediction = false; diff --git a/src/nvenc/nvenc_d3d11.cpp b/src/nvenc/nvenc_d3d11.cpp index e86d2268634..7db6fdd2fa4 100644 --- a/src/nvenc/nvenc_d3d11.cpp +++ b/src/nvenc/nvenc_d3d11.cpp @@ -1,3 +1,5 @@ +#include "src/logging.h" + #ifdef _WIN32 #include "nvenc_d3d11.h" diff --git a/src/nvenc/nvenc_utils.cpp b/src/nvenc/nvenc_utils.cpp index 261e90969a2..1b8b7ec9f10 100644 --- a/src/nvenc/nvenc_utils.cpp +++ b/src/nvenc/nvenc_utils.cpp @@ -1,3 +1,5 @@ +#include + #include "nvenc_utils.h" namespace nvenc { diff --git a/src/nvhttp.cpp b/src/nvhttp.cpp index 738e7a47688..8e976029225 100644 --- a/src/nvhttp.cpp +++ b/src/nvhttp.cpp @@ -22,7 +22,9 @@ // local includes #include "config.h" #include "crypto.h" +#include "file_handler.h" #include "httpcommon.h" +#include "logging.h" #include "main.h" #include "network.h" #include "nvhttp.h" @@ -147,6 +149,7 @@ namespace nvhttp { // uniqueID, session std::unordered_map map_id_sess; client_t client_root; + std::atomic session_id_counter; using args_t = SimpleWeb::CaseInsensitiveMultimap; using resp_https_t = std::shared_ptr::Response>; @@ -290,41 +293,53 @@ namespace nvhttp { } } - rtsp_stream::launch_session_t + std::shared_ptr make_launch_session(bool host_audio, const args_t &args) { - rtsp_stream::launch_session_t launch_session; + auto launch_session = std::make_shared(); + + launch_session->id = ++session_id_counter; auto rikey = util::from_hex_vec(get_arg(args, "rikey"), true); - std::copy(rikey.cbegin(), rikey.cend(), std::back_inserter(launch_session.gcm_key)); + std::copy(rikey.cbegin(), rikey.cend(), std::back_inserter(launch_session->gcm_key)); - launch_session.host_audio = host_audio; + launch_session->host_audio = host_audio; std::stringstream mode = std::stringstream(get_arg(args, "mode", "0x0x0")); // Split mode by the char "x", to populate width/height/fps int x = 0; std::string segment; while (std::getline(mode, segment, 'x')) { - if (x == 0) launch_session.width = atoi(segment.c_str()); - if (x == 1) launch_session.height = atoi(segment.c_str()); - if (x == 2) launch_session.fps = atoi(segment.c_str()); + if (x == 0) launch_session->width = atoi(segment.c_str()); + if (x == 1) launch_session->height = atoi(segment.c_str()); + if (x == 2) launch_session->fps = atoi(segment.c_str()); x++; } - launch_session.unique_id = (get_arg(args, "uniqueid", "unknown")); - launch_session.appid = util::from_view(get_arg(args, "appid", "unknown")); - launch_session.enable_sops = util::from_view(get_arg(args, "sops", "0")); - launch_session.surround_info = util::from_view(get_arg(args, "surroundAudioInfo", "196610")); - launch_session.gcmap = util::from_view(get_arg(args, "gcmap", "0")); - launch_session.enable_hdr = util::from_view(get_arg(args, "hdrMode", "0")); + launch_session->unique_id = (get_arg(args, "uniqueid", "unknown")); + launch_session->appid = util::from_view(get_arg(args, "appid", "unknown")); + launch_session->enable_sops = util::from_view(get_arg(args, "sops", "0")); + launch_session->surround_info = util::from_view(get_arg(args, "surroundAudioInfo", "196610")); + launch_session->gcmap = util::from_view(get_arg(args, "gcmap", "0")); + launch_session->enable_hdr = util::from_view(get_arg(args, "hdrMode", "0")); + + // Encrypted RTSP is enabled with client reported corever >= 1 + auto corever = util::from_view(get_arg(args, "corever", "0")); + if (corever >= 1) { + launch_session->rtsp_cipher = crypto::cipher::gcm_t { + launch_session->gcm_key, false + }; + launch_session->rtsp_iv_counter = 0; + } + launch_session->rtsp_url_scheme = launch_session->rtsp_cipher ? "rtspenc://"s : "rtsp://"s; // Generate the unique identifiers for this connection that we will send later during RTSP handshake unsigned char raw_payload[8]; RAND_bytes(raw_payload, sizeof(raw_payload)); - launch_session.av_ping_payload = util::hex_vec(raw_payload); - RAND_bytes((unsigned char *) &launch_session.control_connect_data, sizeof(launch_session.control_connect_data)); + launch_session->av_ping_payload = util::hex_vec(raw_payload); + RAND_bytes((unsigned char *) &launch_session->control_connect_data, sizeof(launch_session->control_connect_data)); - launch_session.iv.resize(16); + launch_session->iv.resize(16); uint32_t prepend_iv = util::endian::big(util::from_view(get_arg(args, "rikeyid"))); auto prepend_iv_p = (uint8_t *) &prepend_iv; - std::copy(prepend_iv_p, prepend_iv_p + sizeof(prepend_iv), std::begin(launch_session.iv)); + std::copy(prepend_iv_p, prepend_iv_p + sizeof(prepend_iv), std::begin(launch_session->iv)); return launch_session; } @@ -673,11 +688,19 @@ namespace nvhttp { tree.put("root.appversion", VERSION); tree.put("root.GfeVersion", GFE_VERSION); tree.put("root.uniqueid", http::unique_id); - tree.put("root.HttpsPort", map_port(PORT_HTTPS)); - tree.put("root.ExternalPort", map_port(PORT_HTTP)); - tree.put("root.mac", platf::get_mac_address(net::addr_to_normalized_string(local_endpoint.address()))); + tree.put("root.HttpsPort", net::map_port(PORT_HTTPS)); + tree.put("root.ExternalPort", net::map_port(PORT_HTTP)); tree.put("root.MaxLumaPixelsHEVC", video::active_hevc_mode > 1 ? "1869449984" : "0"); + // Only include the MAC address for requests sent from paired clients over HTTPS. + // For HTTP requests, use a placeholder MAC address that Moonlight knows to ignore. + if constexpr (std::is_same_v) { + tree.put("root.mac", platf::get_mac_address(net::addr_to_normalized_string(local_endpoint.address()))); + } + else { + tree.put("root.mac", "00:00:00:00:00:00"); + } + // Moonlight clients track LAN IPv6 addresses separately from LocalIP which is expected to // always be an IPv4 address. If we return that same IPv6 address here, it will clobber the // stored LAN IPv4 address. To avoid this, we need to return an IPv4 address in this field @@ -851,6 +874,17 @@ namespace nvhttp { host_audio = util::from_view(get_arg(args, "localAudioPlayMode")); auto launch_session = make_launch_session(host_audio, args); + auto encryption_mode = net::encryption_mode_for_address(request->remote_endpoint().address()); + if (!launch_session->rtsp_cipher && encryption_mode == config::ENCRYPTION_MODE_MANDATORY) { + BOOST_LOG(error) << "Rejecting client that cannot comply with mandatory encryption requirement"sv; + + tree.put("root..status_code", 403); + tree.put("root..status_message", "Encryption is mandatory for this host but unsupported by the client"); + tree.put("root.gamesession", 0); + + return; + } + if (appid > 0) { auto err = proc::proc.execute(appid, launch_session); if (err) { @@ -862,11 +896,13 @@ namespace nvhttp { } } - rtsp_stream::launch_session_raise(launch_session); - tree.put("root..status_code", 200); - tree.put("root.sessionUrl0", "rtsp://"s + net::addr_to_url_escaped_string(request->local_endpoint().address()) + ':' + std::to_string(map_port(rtsp_stream::RTSP_SETUP_PORT))); + tree.put("root.sessionUrl0", launch_session->rtsp_url_scheme + + net::addr_to_url_escaped_string(request->local_endpoint().address()) + ':' + + std::to_string(net::map_port(rtsp_stream::RTSP_SETUP_PORT))); tree.put("root.gamesession", 1); + + rtsp_stream::launch_session_raise(launch_session); } void @@ -933,11 +969,26 @@ namespace nvhttp { } } - rtsp_stream::launch_session_raise(make_launch_session(host_audio, args)); + auto launch_session = make_launch_session(host_audio, args); + + auto encryption_mode = net::encryption_mode_for_address(request->remote_endpoint().address()); + if (!launch_session->rtsp_cipher && encryption_mode == config::ENCRYPTION_MODE_MANDATORY) { + BOOST_LOG(error) << "Rejecting client that cannot comply with mandatory encryption requirement"sv; + + tree.put("root..status_code", 403); + tree.put("root..status_message", "Encryption is mandatory for this host but unsupported by the client"); + tree.put("root.gamesession", 0); + + return; + } tree.put("root..status_code", 200); - tree.put("root.sessionUrl0", "rtsp://"s + net::addr_to_url_escaped_string(request->local_endpoint().address()) + ':' + std::to_string(map_port(rtsp_stream::RTSP_SETUP_PORT))); + tree.put("root.sessionUrl0", launch_session->rtsp_url_scheme + + net::addr_to_url_escaped_string(request->local_endpoint().address()) + ':' + + std::to_string(net::map_port(rtsp_stream::RTSP_SETUP_PORT))); tree.put("root.resume", 1); + + rtsp_stream::launch_session_raise(launch_session); } void @@ -997,8 +1048,8 @@ namespace nvhttp { start() { auto shutdown_event = mail::man->event(mail::shutdown); - auto port_http = map_port(PORT_HTTP); - auto port_https = map_port(PORT_HTTPS); + auto port_http = net::map_port(PORT_HTTP); + auto port_https = net::map_port(PORT_HTTPS); auto address_family = net::af_from_enum_string(config::sunshine.address_family); bool clean_slate = config::sunshine.flags[config::flag::FRESH_STATE]; @@ -1007,8 +1058,8 @@ namespace nvhttp { load_state(); } - conf_intern.pkey = read_file(config::nvhttp.pkey.c_str()); - conf_intern.servercert = read_file(config::nvhttp.cert.c_str()); + conf_intern.pkey = file_handler::read_file(config::nvhttp.pkey.c_str()); + conf_intern.servercert = file_handler::read_file(config::nvhttp.cert.c_str()); crypto::cert_chain_t cert_chain; client_t &client = client_root; diff --git a/src/platform/common.h b/src/platform/common.h index ab0d085fc9a..7a4102ad5ba 100644 --- a/src/platform/common.h +++ b/src/platform/common.h @@ -10,7 +10,7 @@ #include #include -#include "src/main.h" +#include "src/logging.h" #include "src/thread_safe.h" #include "src/utility.h" #include "src/video_colorspace.h" @@ -558,6 +558,13 @@ namespace platf { std::vector display_names(mem_type_e hwdevice_type); + /** + * @brief Returns if GPUs/drivers have changed since the last call to this function. + * @return `true` if a change has occurred or if it is unknown whether a change occurred. + */ + bool + needs_encoder_reenumeration(); + boost::process::child run_command(bool elevated, bool interactive, const std::string &cmd, boost::filesystem::path &working_dir, const boost::process::environment &env, FILE *file, std::error_code &ec, boost::process::group *group); @@ -608,8 +615,17 @@ namespace platf { audio, video }; + + /** + * @brief Enables QoS on the given socket for traffic to the specified destination. + * @param native_socket The native socket handle. + * @param address The destination address for traffic sent on this socket. + * @param port The destination port for traffic sent on this socket. + * @param data_type The type of traffic sent on this socket. + * @param dscp_tagging Specifies whether to enable DSCP tagging on outgoing traffic. + */ std::unique_ptr - enable_socket_qos(uintptr_t native_socket, boost::asio::ip::address &address, uint16_t port, qos_data_type_e data_type); + enable_socket_qos(uintptr_t native_socket, boost::asio::ip::address &address, uint16_t port, qos_data_type_e data_type, bool dscp_tagging); /** * @brief Open a url in the default web browser. diff --git a/src/platform/linux/audio.cpp b/src/platform/linux/audio.cpp index e31f539f615..577287b77ef 100644 --- a/src/platform/linux/audio.cpp +++ b/src/platform/linux/audio.cpp @@ -14,6 +14,7 @@ #include "src/platform/common.h" #include "src/config.h" +#include "src/logging.h" #include "src/main.h" #include "src/thread_safe.h" diff --git a/src/platform/linux/cuda.cpp b/src/platform/linux/cuda.cpp index 0a04893682f..856fecc657a 100644 --- a/src/platform/linux/cuda.cpp +++ b/src/platform/linux/cuda.cpp @@ -4,6 +4,10 @@ */ #include +#include + +#include + #include #include @@ -15,6 +19,7 @@ extern "C" { #include "cuda.h" #include "graphics.h" +#include "src/logging.h" #include "src/main.h" #include "src/utility.h" #include "src/video.h" @@ -29,6 +34,8 @@ extern "C" { #define CU_CHECK_IGNORE(x, y) \ check((x), SUNSHINE_STRINGVIEW(y ": ")) +namespace fs = std::filesystem; + using namespace std::literals; namespace cuda { constexpr auto cudaDevAttrMaxThreadsPerBlock = (CUdevice_attribute) 1; @@ -69,6 +76,13 @@ namespace cuda { CU_CHECK_IGNORE(cdf->cuStreamDestroy(stream), "Couldn't destroy cuda stream"); } + void + unregisterResource(CUgraphicsResource resource) { + CU_CHECK_IGNORE(cdf->cuGraphicsUnregisterResource(resource), "Couldn't unregister resource"); + } + + using registered_resource_t = util::safe_ptr; + class img_t: public platf::img_t { public: tex_t tex; @@ -223,6 +237,236 @@ namespace cuda { } }; + /** + * @brief Opens the DRM device associated with the CUDA device index. + * @param index CUDA device index to open. + * @return File descriptor or -1 on failure. + */ + file_t + open_drm_fd_for_cuda_device(int index) { + CUdevice device; + CU_CHECK(cdf->cuDeviceGet(&device, index), "Couldn't get CUDA device"); + + // There's no way to directly go from CUDA to a DRM device, so we'll + // use sysfs to look up the DRM device name from the PCI ID. + char pci_bus_id[13]; + CU_CHECK(cdf->cuDeviceGetPCIBusId(pci_bus_id, sizeof(pci_bus_id), device), "Couldn't get CUDA device PCI bus ID"); + BOOST_LOG(debug) << "Found CUDA device with PCI bus ID: "sv << pci_bus_id; + + // Look for the name of the primary node in sysfs + char sysfs_path[PATH_MAX]; + std::snprintf(sysfs_path, sizeof(sysfs_path), "/sys/bus/pci/devices/%s/drm", pci_bus_id); + fs::path sysfs_dir { sysfs_path }; + for (auto &entry : fs::directory_iterator { sysfs_dir }) { + auto file = entry.path().filename(); + auto filestring = file.generic_u8string(); + if (std::string_view { filestring }.substr(0, 4) != "card"sv) { + continue; + } + + BOOST_LOG(debug) << "Found DRM primary node: "sv << filestring; + + fs::path dri_path { "/dev/dri"sv }; + auto device_path = dri_path / file; + return open(device_path.c_str(), O_RDWR); + } + + BOOST_LOG(error) << "Unable to find DRM device with PCI bus ID: "sv << pci_bus_id; + return -1; + } + + class gl_cuda_vram_t: public platf::avcodec_encode_device_t { + public: + /** + * @brief Initialize the GL->CUDA encoding device. + * @param in_width Width of captured frames. + * @param in_height Height of captured frames. + * @param offset_x Offset of content in captured frame. + * @param offset_y Offset of content in captured frame. + * @return 0 on success or -1 on failure. + */ + int + init(int in_width, int in_height, int offset_x, int offset_y) { + // This must be non-zero to tell the video core that it's a hardware encoding device. + data = (void *) 0x1; + + // TODO: Support more than one CUDA device + file = std::move(open_drm_fd_for_cuda_device(0)); + if (file.el < 0) { + char string[1024]; + BOOST_LOG(error) << "Couldn't open DRM FD for CUDA device: "sv << strerror_r(errno, string, sizeof(string)); + return -1; + } + + gbm.reset(gbm::create_device(file.el)); + if (!gbm) { + BOOST_LOG(error) << "Couldn't create GBM device: ["sv << util::hex(eglGetError()).to_string_view() << ']'; + return -1; + } + + display = egl::make_display(gbm.get()); + if (!display) { + return -1; + } + + auto ctx_opt = egl::make_ctx(display.get()); + if (!ctx_opt) { + return -1; + } + + ctx = std::move(*ctx_opt); + + width = in_width; + height = in_height; + + sequence = 0; + + this->offset_x = offset_x; + this->offset_y = offset_y; + + return 0; + } + + /** + * @brief Initialize color conversion into target CUDA frame. + * @param frame Destination CUDA frame to write into. + * @param hw_frames_ctx_buf FFmpeg hardware frame context. + * @return 0 on success or -1 on failure. + */ + int + set_frame(AVFrame *frame, AVBufferRef *hw_frames_ctx_buf) override { + this->hwframe.reset(frame); + this->frame = frame; + + if (!frame->buf[0]) { + if (av_hwframe_get_buffer(hw_frames_ctx_buf, frame, 0)) { + BOOST_LOG(error) << "Couldn't get hwframe for VAAPI"sv; + return -1; + } + } + + auto hw_frames_ctx = (AVHWFramesContext *) hw_frames_ctx_buf->data; + sw_format = hw_frames_ctx->sw_format; + + auto nv12_opt = egl::create_target(frame->width, frame->height, sw_format); + if (!nv12_opt) { + return -1; + } + + auto sws_opt = egl::sws_t::make(width, height, frame->width, frame->height, sw_format); + if (!sws_opt) { + return -1; + } + + this->sws = std::move(*sws_opt); + this->nv12 = std::move(*nv12_opt); + + auto cuda_ctx = (AVCUDADeviceContext *) hw_frames_ctx->device_ctx->hwctx; + + stream = make_stream(); + if (!stream) { + return -1; + } + + cuda_ctx->stream = stream.get(); + + CU_CHECK(cdf->cuGraphicsGLRegisterImage(&y_res, nv12->tex[0], GL_TEXTURE_2D, CU_GRAPHICS_REGISTER_FLAGS_READ_ONLY), + "Couldn't register Y plane texture"); + CU_CHECK(cdf->cuGraphicsGLRegisterImage(&uv_res, nv12->tex[1], GL_TEXTURE_2D, CU_GRAPHICS_REGISTER_FLAGS_READ_ONLY), + "Couldn't register UV plane texture"); + + return 0; + } + + /** + * @brief Convert the captured image into the target CUDA frame. + * @param img Captured screen image. + * @return 0 on success or -1 on failure. + */ + int + convert(platf::img_t &img) override { + auto &descriptor = (egl::img_descriptor_t &) img; + + if (descriptor.sequence == 0) { + // For dummy images, use a blank RGB texture instead of importing a DMA-BUF + rgb = egl::create_blank(img); + } + else if (descriptor.sequence > sequence) { + sequence = descriptor.sequence; + + rgb = egl::rgb_t {}; + + auto rgb_opt = egl::import_source(display.get(), descriptor.sd); + + if (!rgb_opt) { + return -1; + } + + rgb = std::move(*rgb_opt); + } + + // Perform the color conversion and scaling in GL + sws.load_vram(descriptor, offset_x, offset_y, rgb->tex[0]); + sws.convert(nv12->buf); + + auto fmt_desc = av_pix_fmt_desc_get(sw_format); + + // Map the GL textures to read for CUDA + CUgraphicsResource resources[2] = { y_res.get(), uv_res.get() }; + CU_CHECK(cdf->cuGraphicsMapResources(2, resources, stream.get()), "Couldn't map GL textures in CUDA"); + + // Copy from the GL textures to the target CUDA frame + for (int i = 0; i < 2; i++) { + CUDA_MEMCPY2D cpy = {}; + cpy.srcMemoryType = CU_MEMORYTYPE_ARRAY; + CU_CHECK(cdf->cuGraphicsSubResourceGetMappedArray(&cpy.srcArray, resources[i], 0, 0), "Couldn't get mapped plane array"); + + cpy.dstMemoryType = CU_MEMORYTYPE_DEVICE; + cpy.dstDevice = (CUdeviceptr) frame->data[i]; + cpy.dstPitch = frame->linesize[i]; + cpy.WidthInBytes = (frame->width * fmt_desc->comp[i].step) >> (i ? fmt_desc->log2_chroma_w : 0); + cpy.Height = frame->height >> (i ? fmt_desc->log2_chroma_h : 0); + + CU_CHECK_IGNORE(cdf->cuMemcpy2DAsync(&cpy, stream.get()), "Couldn't copy texture to CUDA frame"); + } + + // Unmap the textures to allow modification from GL again + CU_CHECK(cdf->cuGraphicsUnmapResources(2, resources, stream.get()), "Couldn't unmap GL textures from CUDA"); + return 0; + } + + /** + * @brief Configures shader parameters for the specified colorspace. + */ + void + apply_colorspace() override { + sws.apply_colorspace(colorspace); + } + + file_t file; + gbm::gbm_t gbm; + egl::display_t display; + egl::ctx_t ctx; + + // This must be destroyed before display_t + stream_t stream; + frame_t hwframe; + + egl::sws_t sws; + egl::nv12_t nv12; + AVPixelFormat sw_format; + + int width, height; + + std::uint64_t sequence; + egl::rgb_t rgb; + + registered_resource_t y_res; + registered_resource_t uv_res; + + int offset_x, offset_y; + }; + std::unique_ptr make_avcodec_encode_device(int width, int height, bool vram) { if (init()) { @@ -245,6 +489,29 @@ namespace cuda { return cuda; } + /** + * @brief Create a GL->CUDA encoding device for consuming captured dmabufs. + * @param in_width Width of captured frames. + * @param in_height Height of captured frames. + * @param offset_x Offset of content in captured frame. + * @param offset_y Offset of content in captured frame. + * @return FFmpeg encoding device context. + */ + std::unique_ptr + make_avcodec_gl_encode_device(int width, int height, int offset_x, int offset_y) { + if (init()) { + return nullptr; + } + + auto cuda = std::make_unique(); + + if (cuda->init(width, height, offset_x, offset_y)) { + return nullptr; + } + + return cuda; + } + namespace nvfbc { static PNVFBCCREATEINSTANCE createInstance {}; static NVFBC_API_FUNCTION_LIST func { NVFBC_VERSION }; diff --git a/src/platform/linux/cuda.h b/src/platform/linux/cuda.h index d5b97d65051..91564174745 100644 --- a/src/platform/linux/cuda.h +++ b/src/platform/linux/cuda.h @@ -27,6 +27,18 @@ namespace cuda { } std::unique_ptr make_avcodec_encode_device(int width, int height, bool vram); + + /** + * @brief Create a GL->CUDA encoding device for consuming captured dmabufs. + * @param in_width Width of captured frames. + * @param in_height Height of captured frames. + * @param offset_x Offset of content in captured frame. + * @param offset_y Offset of content in captured frame. + * @return FFmpeg encoding device context. + */ + std::unique_ptr + make_avcodec_gl_encode_device(int width, int height, int offset_x, int offset_y); + int init(); } // namespace cuda diff --git a/src/platform/linux/graphics.cpp b/src/platform/linux/graphics.cpp index 6fe51fc8c8c..b4129889562 100644 --- a/src/platform/linux/graphics.cpp +++ b/src/platform/linux/graphics.cpp @@ -3,10 +3,16 @@ * @brief todo */ #include "graphics.h" +#include "src/file_handler.h" +#include "src/logging.h" #include "src/video.h" #include +extern "C" { +#include +} + // I want to have as little build dependencies as possible // There aren't that many DRM_FORMAT I need to use, so define them here // @@ -643,6 +649,71 @@ namespace egl { return nv12; } + /** + * @brief Creates biplanar YUV textures to render into. + * @param width Width of the target frame. + * @param height Height of the target frame. + * @param format Format of the target frame. + * @return The new RGB texture. + */ + std::optional + create_target(int width, int height, AVPixelFormat format) { + nv12_t nv12 { + EGL_NO_DISPLAY, + EGL_NO_IMAGE, + EGL_NO_IMAGE, + gl::tex_t::make(2), + gl::frame_buf_t::make(2), + }; + + GLint y_format; + GLint uv_format; + + // Determine the size of each plane element + auto fmt_desc = av_pix_fmt_desc_get(format); + if (fmt_desc->comp[0].depth <= 8) { + y_format = GL_R8; + uv_format = GL_RG8; + } + else if (fmt_desc->comp[0].depth <= 16) { + y_format = GL_R16; + uv_format = GL_RG16; + } + else { + BOOST_LOG(error) << "Unsupported target pixel format: "sv << format; + return std::nullopt; + } + + gl::ctx.BindTexture(GL_TEXTURE_2D, nv12->tex[0]); + gl::ctx.TexStorage2D(GL_TEXTURE_2D, 1, y_format, width, height); + + gl::ctx.BindTexture(GL_TEXTURE_2D, nv12->tex[1]); + gl::ctx.TexStorage2D(GL_TEXTURE_2D, 1, uv_format, + width >> fmt_desc->log2_chroma_w, height >> fmt_desc->log2_chroma_h); + + nv12->buf.bind(std::begin(nv12->tex), std::end(nv12->tex)); + + GLenum attachments[] { + GL_COLOR_ATTACHMENT0, + GL_COLOR_ATTACHMENT1 + }; + + for (int x = 0; x < sizeof(attachments) / sizeof(decltype(attachments[0])); ++x) { + gl::ctx.BindFramebuffer(GL_FRAMEBUFFER, nv12->buf[x]); + gl::ctx.DrawBuffers(1, &attachments[x]); + + const float y_black[] = { 0.0f, 0.0f, 0.0f, 0.0f }; + const float uv_black[] = { 0.5f, 0.5f, 0.5f, 0.5f }; + gl::ctx.ClearBufferfv(GL_COLOR, 0, x == 0 ? y_black : uv_black); + } + + gl::ctx.BindFramebuffer(GL_FRAMEBUFFER, 0); + + gl_drain_errors; + + return nv12; + } + void sws_t::apply_colorspace(const video::sunshine_colorspace_t &colorspace) { auto color_p = video::color_vectors_from_colorspace(colorspace); @@ -709,7 +780,7 @@ namespace egl { for (int x = 0; x < count; ++x) { auto &compiled_source = compiled_sources[x]; - compiled_source = gl::shader_t::compile(read_file(sources[x]), shader_type[x % 2]); + compiled_source = gl::shader_t::compile(file_handler::read_file(sources[x]), shader_type[x % 2]); gl_drain_errors; if (compiled_source.has_right()) { @@ -806,10 +877,36 @@ namespace egl { } std::optional - sws_t::make(int in_width, int in_height, int out_width, int out_height, GLint gl_tex_internal_fmt) { + sws_t::make(int in_width, int in_height, int out_width, int out_height, AVPixelFormat format) { + GLint gl_format; + + // Decide the bit depth format of the backing texture based the target frame format + auto fmt_desc = av_pix_fmt_desc_get(format); + switch (fmt_desc->comp[0].depth) { + case 8: + gl_format = GL_RGBA8; + break; + + case 10: + gl_format = GL_RGB10_A2; + break; + + case 12: + gl_format = GL_RGBA12; + break; + + case 16: + gl_format = GL_RGBA16; + break; + + default: + BOOST_LOG(error) << "Unsupported pixel format for EGL frame: "sv << (int) format; + return std::nullopt; + } + auto tex = gl::tex_t::make(2); gl::ctx.BindTexture(GL_TEXTURE_2D, tex[0]); - gl::ctx.TexStorage2D(GL_TEXTURE_2D, 1, gl_tex_internal_fmt, in_width, in_height); + gl::ctx.TexStorage2D(GL_TEXTURE_2D, 1, gl_format, in_width, in_height); return make(in_width, in_height, out_width, out_height, std::move(tex)); } diff --git a/src/platform/linux/graphics.h b/src/platform/linux/graphics.h index 56995ca064f..a45b26fd173 100644 --- a/src/platform/linux/graphics.h +++ b/src/platform/linux/graphics.h @@ -11,7 +11,7 @@ #include #include "misc.h" -#include "src/main.h" +#include "src/logging.h" #include "src/platform/common.h" #include "src/utility.h" #include "src/video_colorspace.h" @@ -277,6 +277,16 @@ namespace egl { std::array &&fds, const surface_descriptor_t &y, const surface_descriptor_t &uv); + /** + * @brief Creates biplanar YUV textures to render into. + * @param width Width of the target frame. + * @param height Height of the target frame. + * @param format Format of the target frame. + * @return The new RGB texture. + */ + std::optional + create_target(int width, int height, AVPixelFormat format); + class cursor_t: public platf::img_t { public: int x, y; @@ -316,7 +326,7 @@ namespace egl { static std::optional make(int in_width, int in_height, int out_width, int out_height, gl::tex_t &&tex); static std::optional - make(int in_width, int in_height, int out_width, int out_height, GLint gl_tex_internal_fmt); + make(int in_width, int in_height, int out_width, int out_height, AVPixelFormat format); // Convert the loaded image into the first two framebuffers int diff --git a/src/platform/linux/input.cpp b/src/platform/linux/input.cpp index c62608ce228..43433e58a92 100644 --- a/src/platform/linux/input.cpp +++ b/src/platform/linux/input.cpp @@ -23,6 +23,7 @@ #include "src/config.h" #include "src/input.h" +#include "src/logging.h" #include "src/main.h" #include "src/platform/common.h" #include "src/utility.h" diff --git a/src/platform/linux/kmsgrab.cpp b/src/platform/linux/kmsgrab.cpp index faa6dfb4ed9..a18fc31a823 100644 --- a/src/platform/linux/kmsgrab.cpp +++ b/src/platform/linux/kmsgrab.cpp @@ -14,12 +14,14 @@ #include +#include "src/logging.h" #include "src/main.h" #include "src/platform/common.h" #include "src/round_robin.h" #include "src/utility.h" #include "src/video.h" +#include "cuda.h" #include "graphics.h" #include "vaapi.h" #include "wayland.h" @@ -726,13 +728,11 @@ namespace platf { } } + BOOST_LOG(error) << "Couldn't find monitor ["sv << monitor_index << ']'; + return -1; + // Neatly break from nested for loop break_loop: - if (monitor != monitor_index) { - BOOST_LOG(error) << "Couldn't find monitor ["sv << monitor_index << ']'; - - return -1; - } // Look for the cursor plane for this CRTC cursor_plane_id = -1; @@ -978,6 +978,20 @@ namespace platf { // We will map the entire region, but only copy what the source rectangle specifies size_t mapped_size = ((size_t) fb->pitches[0]) * fb->height; void *mapped_data = mmap(nullptr, mapped_size, PROT_READ, MAP_SHARED, plane_fd.el, fb->offsets[0]); + + // If we got ENOSYS back, let's try to map it as a dumb buffer instead (required for Nvidia GPUs) + if (mapped_data == MAP_FAILED && errno == ENOSYS) { + drm_mode_map_dumb map = {}; + map.handle = fb->handles[0]; + if (drmIoctl(card.fd.el, DRM_IOCTL_MODE_MAP_DUMB, &map) < 0) { + BOOST_LOG(error) << "Failed to map cursor FB as dumb buffer: "sv << strerror(errno); + captured_cursor.visible = false; + return; + } + + mapped_data = mmap(nullptr, mapped_size, PROT_READ, MAP_SHARED, card.fd.el, map.offset); + } + if (mapped_data == MAP_FAILED) { BOOST_LOG(error) << "Failed to mmap cursor FB: "sv << strerror(errno); captured_cursor.visible = false; @@ -1192,6 +1206,12 @@ namespace platf { } #endif +#ifdef SUNSHINE_BUILD_CUDA + if (mem_type == mem_type_e::cuda) { + return cuda::make_avcodec_encode_device(width, height, false); + } +#endif + return std::make_unique(); } @@ -1259,11 +1279,18 @@ namespace platf { auto &rgb = *rgb_opt; + gl::ctx.BindTexture(GL_TEXTURE_2D, rgb->tex[0]); + + // Don't remove these lines, see https://github.com/LizardByte/Sunshine/issues/453 + int w, h; + gl::ctx.GetTexLevelParameteriv(GL_TEXTURE_2D, 0, GL_TEXTURE_WIDTH, &w); + gl::ctx.GetTexLevelParameteriv(GL_TEXTURE_2D, 0, GL_TEXTURE_HEIGHT, &h); + BOOST_LOG(debug) << "width and height: w "sv << w << " h "sv << h; + if (!pull_free_image_cb(img_out)) { return platf::capture_e::interrupted; } - gl::ctx.BindTexture(GL_TEXTURE_2D, rgb->tex[0]); gl::ctx.GetTextureSubImage(rgb->tex[0], 0, img_offset_x, img_offset_y, 0, width, height, 1, GL_BGRA, GL_UNSIGNED_BYTE, img_out->height * img_out->row_pitch, img_out->data); if (cursor && captured_cursor.visible) { @@ -1308,6 +1335,12 @@ namespace platf { } #endif +#ifdef SUNSHINE_BUILD_CUDA + if (mem_type == mem_type_e::cuda) { + return cuda::make_avcodec_gl_encode_device(width, height, img_offset_x, img_offset_y); + } +#endif + BOOST_LOG(error) << "Unsupported pixel format for egl::display_vram_t: "sv << platf::from_pix_fmt(pix_fmt); return nullptr; } @@ -1427,13 +1460,18 @@ namespace platf { } #ifdef SUNSHINE_BUILD_VAAPI - if (!va::validate(card.render_fd.el)) { -#else - if (true) { -#endif + if (mem_type == mem_type_e::vaapi && !va::validate(card.render_fd.el)) { BOOST_LOG(warning) << "Monitor "sv << display_name << " doesn't support hardware encoding. Reverting back to GPU -> RAM -> GPU"sv; return -1; } +#endif + +#ifndef SUNSHINE_BUILD_CUDA + if (mem_type == mem_type_e::cuda) { + BOOST_LOG(warning) << "Attempting to use NVENC without CUDA support. Reverting back to GPU -> RAM -> GPU"sv; + return -1; + } +#endif return 0; } @@ -1445,7 +1483,7 @@ namespace platf { std::shared_ptr kms_display(mem_type_e hwdevice_type, const std::string &display_name, const ::video::config_t &config) { - if (hwdevice_type == mem_type_e::vaapi) { + if (hwdevice_type == mem_type_e::vaapi || hwdevice_type == mem_type_e::cuda) { auto disp = std::make_shared(hwdevice_type); if (!disp->init(display_name, config)) { diff --git a/src/platform/linux/misc.cpp b/src/platform/linux/misc.cpp index 27697c2dc9b..8ead76b0715 100644 --- a/src/platform/linux/misc.cpp +++ b/src/platform/linux/misc.cpp @@ -26,6 +26,7 @@ #include "graphics.h" #include "misc.h" #include "src/config.h" +#include "src/logging.h" #include "src/main.h" #include "src/platform/common.h" #include "vaapi.h" @@ -584,61 +585,107 @@ namespace platf { return true; } + // We can't track QoS state separately for each destination on this OS, + // so we keep a ref count to only disable QoS options when all clients + // are disconnected. + static std::atomic qos_ref_count = 0; + class qos_t: public deinit_t { public: - qos_t(int sockfd, int level, int option): - sockfd(sockfd), level(level), option(option) {} + qos_t(int sockfd, std::vector> options): + sockfd(sockfd), options(options) { + qos_ref_count++; + } virtual ~qos_t() { - int reset_val = -1; - if (setsockopt(sockfd, level, option, &reset_val, sizeof(reset_val)) < 0) { - BOOST_LOG(warning) << "Failed to reset IP TOS: "sv << errno; + if (--qos_ref_count == 0) { + for (const auto &tuple : options) { + auto reset_val = std::get<2>(tuple); + if (setsockopt(sockfd, std::get<0>(tuple), std::get<1>(tuple), &reset_val, sizeof(reset_val)) < 0) { + BOOST_LOG(warning) << "Failed to reset option: "sv << errno; + } + } } } private: int sockfd; - int level; - int option; + std::vector> options; }; + /** + * @brief Enables QoS on the given socket for traffic to the specified destination. + * @param native_socket The native socket handle. + * @param address The destination address for traffic sent on this socket. + * @param port The destination port for traffic sent on this socket. + * @param data_type The type of traffic sent on this socket. + * @param dscp_tagging Specifies whether to enable DSCP tagging on outgoing traffic. + */ std::unique_ptr - enable_socket_qos(uintptr_t native_socket, boost::asio::ip::address &address, uint16_t port, qos_data_type_e data_type) { + enable_socket_qos(uintptr_t native_socket, boost::asio::ip::address &address, uint16_t port, qos_data_type_e data_type, bool dscp_tagging) { int sockfd = (int) native_socket; + std::vector> reset_options; - int level; - int option; - if (address.is_v6()) { - level = SOL_IPV6; - option = IPV6_TCLASS; - } - else { - level = SOL_IP; - option = IP_TOS; - } + if (dscp_tagging) { + int level; + int option; - // The specific DSCP values here are chosen to be consistent with Windows - int dscp; - switch (data_type) { - case qos_data_type_e::video: - dscp = 40; - break; - case qos_data_type_e::audio: - dscp = 56; - break; - default: - BOOST_LOG(error) << "Unknown traffic type: "sv << (int) data_type; - return nullptr; - } + // With dual-stack sockets, Linux uses IPV6_TCLASS for IPv6 traffic + // and IP_TOS for IPv4 traffic. + if (address.is_v6() && !address.to_v6().is_v4_mapped()) { + level = SOL_IPV6; + option = IPV6_TCLASS; + } + else { + level = SOL_IP; + option = IP_TOS; + } - // Shift to put the DSCP value in the correct position in the TOS field - dscp <<= 2; + // The specific DSCP values here are chosen to be consistent with Windows, + // except that we use CS6 instead of CS7 for audio traffic. + int dscp = 0; + switch (data_type) { + case qos_data_type_e::video: + dscp = 40; + break; + case qos_data_type_e::audio: + dscp = 48; + break; + default: + BOOST_LOG(error) << "Unknown traffic type: "sv << (int) data_type; + break; + } - if (setsockopt(sockfd, level, option, &dscp, sizeof(dscp)) < 0) { - return nullptr; + if (dscp) { + // Shift to put the DSCP value in the correct position in the TOS field + dscp <<= 2; + + if (setsockopt(sockfd, level, option, &dscp, sizeof(dscp)) == 0) { + // Reset TOS to -1 when QoS is disabled + reset_options.emplace_back(std::make_tuple(level, option, -1)); + } + else { + BOOST_LOG(error) << "Failed to set TOS/TCLASS: "sv << errno; + } + } } - return std::make_unique(sockfd, level, option); + // We can use SO_PRIORITY to set outgoing traffic priority without DSCP tagging. + // + // NB: We set this after IP_TOS/IPV6_TCLASS since setting TOS value seems to + // reset SO_PRIORITY back to 0. + // + // 6 is the highest priority that can be used without SYS_CAP_ADMIN. + int priority = data_type == qos_data_type_e::audio ? 6 : 5; + if (setsockopt(sockfd, SOL_SOCKET, SO_PRIORITY, &priority, sizeof(priority)) == 0) { + // Reset SO_PRIORITY to 0 when QoS is disabled + reset_options.emplace_back(std::make_tuple(SOL_SOCKET, SO_PRIORITY, 0)); + } + else { + BOOST_LOG(error) << "Failed to set SO_PRIORITY: "sv << errno; + } + + return std::make_unique(sockfd, reset_options); } namespace source { @@ -727,6 +774,16 @@ namespace platf { return {}; } + /** + * @brief Returns if GPUs/drivers have changed since the last call to this function. + * @return `true` if a change has occurred or if it is unknown whether a change occurred. + */ + bool + needs_encoder_reenumeration() { + // We don't track GPU state, so we will always reenumerate. Fortunately, it is fast on Linux. + return true; + } + std::shared_ptr display(mem_type_e hwdevice_type, const std::string &display_name, const video::config_t &config) { #ifdef SUNSHINE_BUILD_CUDA diff --git a/src/platform/linux/publish.cpp b/src/platform/linux/publish.cpp index e83bfb7009e..bc876e7728b 100644 --- a/src/platform/linux/publish.cpp +++ b/src/platform/linux/publish.cpp @@ -7,7 +7,8 @@ #include #include "misc.h" -#include "src/main.h" +#include "src/logging.h" +#include "src/network.h" #include "src/nvhttp.h" #include "src/platform/common.h" #include "src/utility.h" @@ -348,7 +349,7 @@ namespace platf::publish { name.get(), SERVICE_TYPE, nullptr, nullptr, - map_port(nvhttp::PORT_HTTP), + net::map_port(nvhttp::PORT_HTTP), nullptr); if (ret < 0) { diff --git a/src/platform/linux/vaapi.cpp b/src/platform/linux/vaapi.cpp index 118e1bd8da0..9f1d3f325b2 100644 --- a/src/platform/linux/vaapi.cpp +++ b/src/platform/linux/vaapi.cpp @@ -26,7 +26,7 @@ vaSyncBuffer( #include "graphics.h" #include "misc.h" #include "src/config.h" -#include "src/main.h" +#include "src/logging.h" #include "src/platform/common.h" #include "src/utility.h" #include "src/video.h" @@ -195,25 +195,7 @@ namespace va { return -1; } - // Decide the bit depth format of the backing texture based the target frame format - GLint gl_format; - switch (hw_frames_ctx->sw_format) { - case AV_PIX_FMT_YUV420P: - case AV_PIX_FMT_NV12: - gl_format = GL_RGBA8; - break; - - case AV_PIX_FMT_YUV420P10: - case AV_PIX_FMT_P010: - gl_format = GL_RGB10_A2; - break; - - default: - BOOST_LOG(error) << "Unsupported pixel format for VA frame: "sv << hw_frames_ctx->sw_format; - return -1; - } - - auto sws_opt = egl::sws_t::make(width, height, frame->width, frame->height, gl_format); + auto sws_opt = egl::sws_t::make(width, height, frame->width, frame->height, hw_frames_ctx->sw_format); if (!sws_opt) { return -1; } diff --git a/src/platform/linux/wayland.cpp b/src/platform/linux/wayland.cpp index b762492b6ae..8dcd22c3639 100644 --- a/src/platform/linux/wayland.cpp +++ b/src/platform/linux/wayland.cpp @@ -9,7 +9,7 @@ #include #include "graphics.h" -#include "src/main.h" +#include "src/logging.h" #include "src/platform/common.h" #include "src/round_robin.h" #include "src/utility.h" diff --git a/src/platform/linux/wlgrab.cpp b/src/platform/linux/wlgrab.cpp index 0e22cbe2248..6acde691479 100644 --- a/src/platform/linux/wlgrab.cpp +++ b/src/platform/linux/wlgrab.cpp @@ -4,8 +4,11 @@ */ #include "src/platform/common.h" +#include "src/logging.h" #include "src/main.h" #include "src/video.h" + +#include "cuda.h" #include "vaapi.h" #include "wayland.h" @@ -182,6 +185,13 @@ namespace wl { } gl::ctx.BindTexture(GL_TEXTURE_2D, (*rgb_opt)->tex[0]); + + // Don't remove these lines, see https://github.com/LizardByte/Sunshine/issues/453 + int w, h; + gl::ctx.GetTexLevelParameteriv(GL_TEXTURE_2D, 0, GL_TEXTURE_WIDTH, &w); + gl::ctx.GetTexLevelParameteriv(GL_TEXTURE_2D, 0, GL_TEXTURE_HEIGHT, &h); + BOOST_LOG(debug) << "width and height: w "sv << w << " h "sv << h; + gl::ctx.GetTextureSubImage((*rgb_opt)->tex[0], 0, 0, 0, 0, width, height, 1, GL_BGRA, GL_UNSIGNED_BYTE, img_out->height * img_out->row_pitch, img_out->data); gl::ctx.BindTexture(GL_TEXTURE_2D, 0); @@ -217,6 +227,12 @@ namespace wl { } #endif +#ifdef SUNSHINE_BUILD_CUDA + if (mem_type == platf::mem_type_e::cuda) { + return cuda::make_avcodec_encode_device(width, height, false); + } +#endif + return std::make_unique(); } @@ -329,6 +345,12 @@ namespace wl { } #endif +#ifdef SUNSHINE_BUILD_CUDA + if (mem_type == platf::mem_type_e::cuda) { + return cuda::make_avcodec_gl_encode_device(width, height, 0, 0); + } +#endif + return std::make_unique(); } @@ -351,7 +373,7 @@ namespace platf { return nullptr; } - if (hwdevice_type == platf::mem_type_e::vaapi) { + if (hwdevice_type == platf::mem_type_e::vaapi || hwdevice_type == platf::mem_type_e::cuda) { auto wlr = std::make_shared(); if (wlr->init(hwdevice_type, display_name, config)) { return nullptr; diff --git a/src/platform/linux/x11grab.cpp b/src/platform/linux/x11grab.cpp index 5cdf2d63d39..0c1583002b9 100644 --- a/src/platform/linux/x11grab.cpp +++ b/src/platform/linux/x11grab.cpp @@ -17,6 +17,7 @@ #include #include "src/config.h" +#include "src/logging.h" #include "src/main.h" #include "src/task_pool.h" #include "src/video.h" diff --git a/src/platform/macos/av_audio.m b/src/platform/macos/av_audio.m index af695179c7b..cb0c83c2e97 100644 --- a/src/platform/macos/av_audio.m +++ b/src/platform/macos/av_audio.m @@ -130,9 +130,9 @@ - (void)captureOutput:(AVCaptureOutput *)output CMSampleBufferGetAudioBufferListWithRetainedBlockBuffer(sampleBuffer, NULL, &audioBufferList, sizeof(audioBufferList), NULL, NULL, 0, &blockBuffer); - // NSAssert(audioBufferList.mNumberBuffers == 1, @"Expected interlveaved PCM format but buffer contained %u streams", audioBufferList.mNumberBuffers); + // NSAssert(audioBufferList.mNumberBuffers == 1, @"Expected interleaved PCM format but buffer contained %u streams", audioBufferList.mNumberBuffers); - // this is safe, because an interleaved PCM stream has exactly one buffer + // this is safe, because an interleaved PCM stream has exactly one buffer, // and we don't want to do sanity checks in a performance critical exec path AudioBuffer audioBuffer = audioBufferList.mBuffers[0]; diff --git a/src/platform/macos/av_img_t.h b/src/platform/macos/av_img_t.h index 6002bdcb221..fb0d7ecd0f5 100644 --- a/src/platform/macos/av_img_t.h +++ b/src/platform/macos/av_img_t.h @@ -11,7 +11,7 @@ namespace platf { struct av_sample_buf_t { - av_sample_buf_t(CMSampleBufferRef buf): + explicit av_sample_buf_t(CMSampleBufferRef buf): buf((CMSampleBufferRef) CFRetain(buf)) {} ~av_sample_buf_t() { @@ -22,12 +22,12 @@ namespace platf { }; struct av_pixel_buf_t { - av_pixel_buf_t(CVPixelBufferRef buf): + explicit av_pixel_buf_t(CVPixelBufferRef buf): buf((CVPixelBufferRef) CFRetain(buf)), locked(false) {} - uint8_t * - lock() { + [[nodiscard]] uint8_t * + lock() const { if (!locked) { CVPixelBufferLockBaseAddress(buf, kCVPixelBufferLock_ReadOnly); } diff --git a/src/platform/macos/av_video.m b/src/platform/macos/av_video.m index c64597656cc..5cdf5a9898e 100644 --- a/src/platform/macos/av_video.m +++ b/src/platform/macos/av_video.m @@ -37,8 +37,8 @@ - (id)initWithDisplay:(CGDirectDisplayID)displayID frameRate:(int)frameRate { self.displayID = displayID; self.pixelFormat = kCVPixelFormatType_32BGRA; - self.frameWidth = CGDisplayModeGetPixelWidth(mode); - self.frameHeight = CGDisplayModeGetPixelHeight(mode); + self.frameWidth = (int) CGDisplayModeGetPixelWidth(mode); + self.frameHeight = (int) CGDisplayModeGetPixelHeight(mode); self.minFrameDuration = CMTimeMake(1, frameRate); self.session = [[AVCaptureSession alloc] init]; self.videoOutputs = [[NSMapTable alloc] init]; diff --git a/src/platform/macos/display.mm b/src/platform/macos/display.mm index f424cf4ece2..6d757176006 100644 --- a/src/platform/macos/display.mm +++ b/src/platform/macos/display.mm @@ -8,7 +8,7 @@ #include "src/platform/macos/nv12_zero_device.h" #include "src/config.h" -#include "src/main.h" +#include "src/logging.h" // Avoid conflict between AVFoundation and libavutil both defining AVMediaType #define AVMediaType AVMediaType_FFmpeg @@ -21,10 +21,10 @@ using namespace std::literals; struct av_display_t: public display_t { - AVVideo *av_capture; - CGDirectDisplayID display_id; + AVVideo *av_capture {}; + CGDirectDisplayID display_id {}; - ~av_display_t() { + ~av_display_t() override { [av_capture release]; } @@ -45,9 +45,9 @@ av_img->pixel_buffer = std::make_shared(pixelBuffer); img_out->data = av_img->pixel_buffer->lock(); - img_out->width = CVPixelBufferGetWidth(pixelBuffer); - img_out->height = CVPixelBufferGetHeight(pixelBuffer); - img_out->row_pitch = CVPixelBufferGetBytesPerRow(pixelBuffer); + img_out->width = (int) CVPixelBufferGetWidth(pixelBuffer); + img_out->height = (int) CVPixelBufferGetHeight(pixelBuffer); + img_out->row_pitch = (int) CVPixelBufferGetBytesPerRow(pixelBuffer); img_out->pixel_pitch = img_out->row_pitch / img_out->width; if (!push_captured_image_cb(std::move(img_out), true)) { @@ -101,9 +101,9 @@ av_img->pixel_buffer = std::make_shared(pixelBuffer); img->data = av_img->pixel_buffer->lock(); - img->width = CVPixelBufferGetWidth(pixelBuffer); - img->height = CVPixelBufferGetHeight(pixelBuffer); - img->row_pitch = CVPixelBufferGetBytesPerRow(pixelBuffer); + img->width = (int) CVPixelBufferGetWidth(pixelBuffer); + img->height = (int) CVPixelBufferGetHeight(pixelBuffer); + img->row_pitch = (int) CVPixelBufferGetBytesPerRow(pixelBuffer); img->pixel_pitch = img->row_pitch / img->width; // returning false here stops capture backend @@ -177,9 +177,19 @@ display_names.reserve([display_array count]); [display_array enumerateObjectsUsingBlock:^(NSDictionary *_Nonnull obj, NSUInteger idx, BOOL *_Nonnull stop) { NSString *name = obj[@"name"]; - display_names.push_back(name.UTF8String); + display_names.emplace_back(name.UTF8String); }]; return display_names; } + + /** + * @brief Returns if GPUs/drivers have changed since the last call to this function. + * @return `true` if a change has occurred or if it is unknown whether a change occurred. + */ + bool + needs_encoder_reenumeration() { + // We don't track GPU state, so we will always reenumerate. Fortunately, it is fast on macOS. + return true; + } } // namespace platf diff --git a/src/platform/macos/input.cpp b/src/platform/macos/input.cpp index 66d0d130861..2aa6012ef51 100644 --- a/src/platform/macos/input.cpp +++ b/src/platform/macos/input.cpp @@ -6,7 +6,7 @@ #include #include -#include "src/main.h" +#include "src/logging.h" #include "src/platform/common.h" #include "src/utility.h" @@ -21,17 +21,17 @@ namespace platf { struct macos_input_t { public: - CGDirectDisplayID display; - CGFloat displayScaling; - CGEventSourceRef source; + CGDirectDisplayID display {}; + CGFloat displayScaling {}; + CGEventSourceRef source {}; // keyboard related stuff - CGEventRef kb_event; - CGEventFlags kb_flags; + CGEventRef kb_event {}; + CGEventFlags kb_flags {}; // mouse related stuff - CGEventRef mouse_event; // mouse event source - bool mouse_down[3]; // mouse button status + CGEventRef mouse_event {}; // mouse event source + bool mouse_down[3] {}; // mouse button status std::chrono::steady_clock::steady_clock::time_point last_mouse_event[3][2]; // timestamp of last mouse events }; @@ -221,7 +221,7 @@ const KeyCodeMap kKeyCodesMap[] = { int keysym(int keycode) { - KeyCodeMap key_map; + KeyCodeMap key_map {}; key_map.win_keycode = keycode; const KeyCodeMap *temp_map = std::lower_bound( @@ -330,13 +330,13 @@ const KeyCodeMap kKeyCodesMap[] = { if (location.x < 0) location.x = 0; - if (location.x >= CGDisplayPixelsWide(display)) - location.x = CGDisplayPixelsWide(display) - 1; + if (location.x >= (double) CGDisplayPixelsWide(display)) + location.x = (double) CGDisplayPixelsWide(display) - 1; if (location.y < 0) location.y = 0; - if (location.y >= CGDisplayPixelsHigh(display)) - location.y = CGDisplayPixelsHigh(display) - 1; + if (location.y >= (double) CGDisplayPixelsHigh(display)) + location.y = (double) CGDisplayPixelsHigh(display) - 1; CGEventSetType(event, type); CGEventSetLocation(event, location); @@ -428,7 +428,7 @@ const KeyCodeMap kKeyCodesMap[] = { void scroll(input_t &input, int high_res_distance) { CGEventRef upEvent = CGEventCreateScrollWheelEvent( - NULL, + nullptr, kCGScrollEventUnitLine, 2, high_res_distance > 0 ? 1 : -1, high_res_distance); CGEventPost(kCGHIDEventTap, upEvent); diff --git a/src/platform/macos/microphone.mm b/src/platform/macos/microphone.mm index 854ca6faffe..836f134a482 100644 --- a/src/platform/macos/microphone.mm +++ b/src/platform/macos/microphone.mm @@ -6,15 +6,15 @@ #include "src/platform/macos/av_audio.h" #include "src/config.h" -#include "src/main.h" +#include "src/logging.h" namespace platf { using namespace std::literals; struct av_mic_t: public mic_t { - AVAudio *av_audio_capture; + AVAudio *av_audio_capture {}; - ~av_mic_t() { + ~av_mic_t() override { [av_audio_capture release]; } @@ -42,7 +42,7 @@ }; struct macos_audio_control_t: public audio_control_t { - AVCaptureDevice *audio_capture_device; + AVCaptureDevice *audio_capture_device {}; public: int diff --git a/src/platform/macos/misc.h b/src/platform/macos/misc.h index a6fb1df3244..ca74f0ea478 100644 --- a/src/platform/macos/misc.h +++ b/src/platform/macos/misc.h @@ -9,7 +9,7 @@ #include namespace dyn { - typedef void (*apiproc)(void); + typedef void (*apiproc)(); int load(void *handle, const std::vector> &funcs, bool strict = true); diff --git a/src/platform/macos/misc.mm b/src/platform/macos/misc.mm index 436763a00d3..0a8bf1b78ae 100644 --- a/src/platform/macos/misc.mm +++ b/src/platform/macos/misc.mm @@ -4,7 +4,7 @@ */ // Required for IPV6_PKTINFO with Darwin headers -#ifndef __APPLE_USE_RFC_3542 +#ifndef __APPLE_USE_RFC_3542 // NOLINT(bugprone-reserved-identifier) #define __APPLE_USE_RFC_3542 1 #endif @@ -18,6 +18,7 @@ #include #include "misc.h" +#include "src/logging.h" #include "src/main.h" #include "src/platform/common.h" @@ -53,7 +54,7 @@ // Xcode 12.2 and later, these functions are not weakly linked and will never // be null, and therefore generate this warning. Since we are weakly linking // when compiling with earlier Xcode versions, the check for null is - // necessary and so we ignore the warning. + // necessary, and so we ignore the warning. #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wunguarded-availability-new" #pragma clang diagnostic ignored "-Wtautological-pointer-compare" @@ -141,7 +142,7 @@ std::string mac_address; if (getifaddrs(&ifap) == 0) { - for (ifaptr = ifap; ifaptr != NULL; ifaptr = (ifaptr)->ifa_next) { + for (ifaptr = ifap; ifaptr != nullptr; ifaptr = (ifaptr)->ifa_next) { if (!strcmp((ifaptr)->ifa_name, pos->ifa_name) && (((ifaptr)->ifa_addr)->sa_family == AF_LINK)) { ptr = (unsigned char *) LLADDR((struct sockaddr_dl *) (ifaptr)->ifa_addr); char buff[100]; @@ -155,7 +156,7 @@ freeifaddrs(ifap); - if (ifaptr != NULL) { + if (ifaptr != nullptr) { BOOST_LOG(verbose) << "Found MAC of "sv << pos->ifa_name << ": "sv << mac_address; return mac_address; } @@ -336,7 +337,7 @@ union { char buf[std::max(CMSG_SPACE(sizeof(struct in_pktinfo)), CMSG_SPACE(sizeof(struct in6_pktinfo)))]; struct cmsghdr alignment; - } cmbuf; + } cmbuf {}; socklen_t cmbuflen = 0; msg.msg_control = cmbuf.buf; @@ -344,7 +345,7 @@ auto pktinfo_cm = CMSG_FIRSTHDR(&msg); if (send_info.source_address.is_v6()) { - struct in6_pktinfo pktInfo; + struct in6_pktinfo pktInfo {}; struct sockaddr_in6 saddr_v6 = to_sockaddr(send_info.source_address.to_v6(), 0); pktInfo.ipi6_addr = saddr_v6.sin6_addr; @@ -358,7 +359,7 @@ memcpy(CMSG_DATA(pktinfo_cm), &pktInfo, sizeof(pktInfo)); } else { - struct in_pktinfo pktInfo; + struct in_pktinfo pktInfo {}; struct sockaddr_in saddr_v4 = to_sockaddr(send_info.source_address.to_v4(), 0); pktInfo.ipi_spec_dst = saddr_v4.sin_addr; @@ -407,12 +408,113 @@ return true; } + // We can't track QoS state separately for each destination on this OS, + // so we keep a ref count to only disable QoS options when all clients + // are disconnected. + static std::atomic qos_ref_count = 0; + + class qos_t: public deinit_t { + public: + qos_t(int sockfd, std::vector> options): + sockfd(sockfd), options(options) { + qos_ref_count++; + } + + virtual ~qos_t() { + if (--qos_ref_count == 0) { + for (const auto &tuple : options) { + auto reset_val = std::get<2>(tuple); + if (setsockopt(sockfd, std::get<0>(tuple), std::get<1>(tuple), &reset_val, sizeof(reset_val)) < 0) { + BOOST_LOG(warning) << "Failed to reset option: "sv << errno; + } + } + } + } + + private: + int sockfd; + std::vector> options; + }; + + /** + * @brief Enables QoS on the given socket for traffic to the specified destination. + * @param native_socket The native socket handle. + * @param address The destination address for traffic sent on this socket. + * @param port The destination port for traffic sent on this socket. + * @param data_type The type of traffic sent on this socket. + * @param dscp_tagging Specifies whether to enable DSCP tagging on outgoing traffic. + */ std::unique_ptr - enable_socket_qos(uintptr_t native_socket, boost::asio::ip::address &address, uint16_t port, qos_data_type_e data_type) { - // Unimplemented - // - // NB: When implementing, remember to consider that some routes can drop DSCP-tagged packets completely! - return nullptr; + enable_socket_qos(uintptr_t native_socket, boost::asio::ip::address &address, uint16_t port, qos_data_type_e data_type, bool dscp_tagging) { + int sockfd = (int) native_socket; + std::vector> reset_options; + + // We can use SO_NET_SERVICE_TYPE to set link-layer prioritization without DSCP tagging + int service_type = 0; + switch (data_type) { + case qos_data_type_e::video: + service_type = NET_SERVICE_TYPE_VI; + break; + case qos_data_type_e::audio: + service_type = NET_SERVICE_TYPE_VO; + break; + default: + BOOST_LOG(error) << "Unknown traffic type: "sv << (int) data_type; + break; + } + + if (service_type) { + if (setsockopt(sockfd, SOL_SOCKET, SO_NET_SERVICE_TYPE, &service_type, sizeof(service_type)) == 0) { + // Reset SO_NET_SERVICE_TYPE to best-effort when QoS is disabled + reset_options.emplace_back(std::make_tuple(SOL_SOCKET, SO_NET_SERVICE_TYPE, NET_SERVICE_TYPE_BE)); + } + else { + BOOST_LOG(error) << "Failed to set SO_NET_SERVICE_TYPE: "sv << errno; + } + } + + if (dscp_tagging) { + int level; + int option; + if (address.is_v6()) { + level = IPPROTO_IPV6; + option = IPV6_TCLASS; + } + else { + level = IPPROTO_IP; + option = IP_TOS; + } + + // The specific DSCP values here are chosen to be consistent with Windows, + // except that we use CS6 instead of CS7 for audio traffic. + int dscp = 0; + switch (data_type) { + case qos_data_type_e::video: + dscp = 40; + break; + case qos_data_type_e::audio: + dscp = 48; + break; + default: + BOOST_LOG(error) << "Unknown traffic type: "sv << (int) data_type; + break; + } + + if (dscp) { + // Shift to put the DSCP value in the correct position in the TOS field + dscp <<= 2; + + if (setsockopt(sockfd, level, option, &dscp, sizeof(dscp)) == 0) { + // Reset TOS to -1 when QoS is disabled + reset_options.emplace_back(std::make_tuple(level, option, -1)); + } + else { + BOOST_LOG(error) << "Failed to set TOS/TCLASS: "sv << errno; + } + } + } + + return std::make_unique(sockfd, reset_options); } } // namespace platf diff --git a/src/platform/macos/nv12_zero_device.cpp b/src/platform/macos/nv12_zero_device.cpp index f376bdcd413..c217c637661 100644 --- a/src/platform/macos/nv12_zero_device.cpp +++ b/src/platform/macos/nv12_zero_device.cpp @@ -2,6 +2,8 @@ * @file src/platform/macos/nv12_zero_device.cpp * @brief todo */ +#include + #include "src/platform/macos/nv12_zero_device.h" #include "src/video.h" @@ -24,7 +26,7 @@ namespace platf { int nv12_zero_device::convert(platf::img_t &img) { - av_img_t *av_img = (av_img_t *) &img; + auto *av_img = (av_img_t *) &img; // Release any existing CVPixelBuffer previously retained for encoding av_buffer_unref(&av_frame->buf[0]); @@ -34,7 +36,7 @@ namespace platf { // // The presence of the AVBufferRef allows FFmpeg to simply add a reference to the buffer // rather than having to perform a deep copy of the data buffers in avcodec_send_frame(). - av_frame->buf[0] = av_buffer_create((uint8_t *) CFRetain(av_img->pixel_buffer->buf), 0, free_buffer, NULL, 0); + av_frame->buf[0] = av_buffer_create((uint8_t *) CFRetain(av_img->pixel_buffer->buf), 0, free_buffer, nullptr, 0); // Place a CVPixelBufferRef at data[3] as required by AV_PIX_FMT_VIDEOTOOLBOX av_frame->data[3] = (uint8_t *) av_img->pixel_buffer->buf; @@ -54,15 +56,15 @@ namespace platf { } int - nv12_zero_device::init(void *display, pix_fmt_e pix_fmt, resolution_fn_t resolution_fn, pixel_format_fn_t pixel_format_fn) { + nv12_zero_device::init(void *display, pix_fmt_e pix_fmt, resolution_fn_t resolution_fn, const pixel_format_fn_t &pixel_format_fn) { pixel_format_fn(display, pix_fmt == pix_fmt_e::nv12 ? kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange : kCVPixelFormatType_420YpCbCr10BiPlanarVideoRange); this->display = display; - this->resolution_fn = resolution_fn; + this->resolution_fn = std::move(resolution_fn); - // we never use this pointer but it's existence is checked/used + // we never use this pointer, but its existence is checked/used // by the platform independent code data = this; diff --git a/src/platform/macos/nv12_zero_device.h b/src/platform/macos/nv12_zero_device.h index f1ee2702aad..b83210cb94f 100644 --- a/src/platform/macos/nv12_zero_device.h +++ b/src/platform/macos/nv12_zero_device.h @@ -26,12 +26,12 @@ namespace platf { using pixel_format_fn_t = std::function; int - init(void *display, pix_fmt_e pix_fmt, resolution_fn_t resolution_fn, pixel_format_fn_t pixel_format_fn); + init(void *display, pix_fmt_e pix_fmt, resolution_fn_t resolution_fn, const pixel_format_fn_t &pixel_format_fn); int - convert(img_t &img); + convert(img_t &img) override; int - set_frame(AVFrame *frame, AVBufferRef *hw_frames_ctx); + set_frame(AVFrame *frame, AVBufferRef *hw_frames_ctx) override; private: util::safe_ptr av_frame; diff --git a/src/platform/macos/publish.cpp b/src/platform/macos/publish.cpp index 8fca07b848a..eb8823e5020 100644 --- a/src/platform/macos/publish.cpp +++ b/src/platform/macos/publish.cpp @@ -7,7 +7,8 @@ #include #include "misc.h" -#include "src/main.h" +#include "src/logging.h" +#include "src/network.h" #include "src/nvhttp.h" #include "src/platform/common.h" #include "src/utility.h" @@ -348,7 +349,7 @@ namespace platf::publish { name.get(), SERVICE_TYPE, nullptr, nullptr, - map_port(nvhttp::PORT_HTTP), + net::map_port(nvhttp::PORT_HTTP), nullptr); if (ret < 0) { @@ -402,7 +403,7 @@ namespace platf::publish { public: std::thread poll_thread; - deinit_t(std::thread poll_thread): + explicit deinit_t(std::thread poll_thread): poll_thread { std::move(poll_thread) } {} ~deinit_t() override { diff --git a/src/platform/windows/audio.cpp b/src/platform/windows/audio.cpp index 1311a694fd1..296112e1ad3 100644 --- a/src/platform/windows/audio.cpp +++ b/src/platform/windows/audio.cpp @@ -16,7 +16,7 @@ #include #include "src/config.h" -#include "src/main.h" +#include "src/logging.h" #include "src/platform/common.h" // Must be the last included file diff --git a/src/platform/windows/display_base.cpp b/src/platform/windows/display_base.cpp index 64954bbf283..3ae6e337d26 100644 --- a/src/platform/windows/display_base.cpp +++ b/src/platform/windows/display_base.cpp @@ -15,6 +15,7 @@ typedef long NTSTATUS; #include "display.h" #include "misc.h" #include "src/config.h" +#include "src/logging.h" #include "src/main.h" #include "src/platform/common.h" #include "src/stat_trackers.h" @@ -1124,4 +1125,35 @@ namespace platf { return display_names; } + /** + * @brief Returns if GPUs/drivers have changed since the last call to this function. + * @return `true` if a change has occurred or if it is unknown whether a change occurred. + */ + bool + needs_encoder_reenumeration() { + // Serialize access to the static DXGI factory + static std::mutex reenumeration_state_lock; + auto lg = std::lock_guard(reenumeration_state_lock); + + // Keep a reference to the DXGI factory, which will keep track of changes internally. + static dxgi::factory1_t factory; + if (!factory || !factory->IsCurrent()) { + factory.reset(); + + auto status = CreateDXGIFactory1(IID_IDXGIFactory1, (void **) &factory); + if (FAILED(status)) { + BOOST_LOG(error) << "Failed to create DXGIFactory1 [0x"sv << util::hex(status).to_string_view() << ']'; + factory.release(); + } + + // Always request reenumeration on the first streaming session just to ensure we + // can deal with any initialization races that may occur when the system is booting. + BOOST_LOG(info) << "Encoder reenumeration is required"sv; + return true; + } + else { + // The DXGI factory from last time is still current, so no encoder changes have occurred. + return false; + } + } } // namespace platf diff --git a/src/platform/windows/display_ram.cpp b/src/platform/windows/display_ram.cpp index ad078d12704..cbe37edfe6b 100644 --- a/src/platform/windows/display_ram.cpp +++ b/src/platform/windows/display_ram.cpp @@ -5,7 +5,7 @@ #include "display.h" #include "misc.h" -#include "src/main.h" +#include "src/logging.h" namespace platf { using namespace std::literals; diff --git a/src/platform/windows/display_vram.cpp b/src/platform/windows/display_vram.cpp index 71310c90c16..74b0bab7fbf 100644 --- a/src/platform/windows/display_vram.cpp +++ b/src/platform/windows/display_vram.cpp @@ -17,7 +17,7 @@ extern "C" { #include "display.h" #include "misc.h" #include "src/config.h" -#include "src/main.h" +#include "src/logging.h" #include "src/nvenc/nvenc_config.h" #include "src/nvenc/nvenc_d3d11.h" #include "src/nvenc/nvenc_utils.h" diff --git a/src/platform/windows/input.cpp b/src/platform/windows/input.cpp index 65d89c0eb9c..144dac1999a 100644 --- a/src/platform/windows/input.cpp +++ b/src/platform/windows/input.cpp @@ -12,6 +12,7 @@ #include "keylayout.h" #include "misc.h" #include "src/config.h" +#include "src/logging.h" #include "src/main.h" #include "src/platform/common.h" diff --git a/src/platform/windows/misc.cpp b/src/platform/windows/misc.cpp index 71455837656..1bbfe113a38 100644 --- a/src/platform/windows/misc.cpp +++ b/src/platform/windows/misc.cpp @@ -12,6 +12,7 @@ #include #include #include +#include // prevent clang format from "optimizing" the header include order // clang-format off @@ -29,6 +30,12 @@ #include // clang-format on +// Boost overrides NTDDI_VERSION, so we re-override it here +#undef NTDDI_VERSION +#define NTDDI_VERSION NTDDI_WIN10 +#include + +#include "src/logging.h" #include "src/main.h" #include "src/platform/common.h" #include "src/utility.h" @@ -550,6 +557,256 @@ namespace platf { return startup_info; } + /** + * @brief This function overrides HKEY_CURRENT_USER and HKEY_CLASSES_ROOT using the provided token. + * @param token The primary token identifying the user to use, or `NULL` to restore original keys. + * @return `true` if the override or restore operation was successful. + */ + bool + override_per_user_predefined_keys(HANDLE token) { + HKEY user_classes_root = NULL; + if (token) { + auto err = RegOpenUserClassesRoot(token, 0, GENERIC_ALL, &user_classes_root); + if (err != ERROR_SUCCESS) { + BOOST_LOG(error) << "Failed to open classes root for target user: "sv << err; + return false; + } + } + auto close_classes_root = util::fail_guard([user_classes_root]() { + if (user_classes_root) { + RegCloseKey(user_classes_root); + } + }); + + HKEY user_key = NULL; + if (token) { + impersonate_current_user(token, [&]() { + // RegOpenCurrentUser() doesn't take a token. It assumes we're impersonating the desired user. + auto err = RegOpenCurrentUser(GENERIC_ALL, &user_key); + if (err != ERROR_SUCCESS) { + BOOST_LOG(error) << "Failed to open user key for target user: "sv << err; + user_key = NULL; + } + }); + if (!user_key) { + return false; + } + } + auto close_user = util::fail_guard([user_key]() { + if (user_key) { + RegCloseKey(user_key); + } + }); + + auto err = RegOverridePredefKey(HKEY_CLASSES_ROOT, user_classes_root); + if (err != ERROR_SUCCESS) { + BOOST_LOG(error) << "Failed to override HKEY_CLASSES_ROOT: "sv << err; + return false; + } + + err = RegOverridePredefKey(HKEY_CURRENT_USER, user_key); + if (err != ERROR_SUCCESS) { + BOOST_LOG(error) << "Failed to override HKEY_CURRENT_USER: "sv << err; + RegOverridePredefKey(HKEY_CLASSES_ROOT, NULL); + return false; + } + + return true; + } + + /** + * @brief This function resolves the given raw command into a proper command string for CreateProcess(). + * @details This converts URLs and non-executable file paths into a runnable command like ShellExecute(). + * @param raw_cmd The raw command provided by the user. + * @param working_dir The working directory for the new process. + * @param token The user token currently being impersonated or `NULL` if running as ourselves. + * @return A command string suitable for use by CreateProcess(). + */ + std::wstring + resolve_command_string(const std::string &raw_cmd, const std::wstring &working_dir, HANDLE token) { + std::wstring raw_cmd_w = converter.from_bytes(raw_cmd); + + // First, convert the given command into parts so we can get the executable/file/URL without parameters + auto raw_cmd_parts = boost::program_options::split_winmain(raw_cmd_w); + if (raw_cmd_parts.empty()) { + // This is highly unexpected, but we'll just return the raw string and hope for the best. + BOOST_LOG(warning) << "Failed to split command string: "sv << raw_cmd; + return converter.from_bytes(raw_cmd); + } + + auto raw_target = raw_cmd_parts.at(0); + std::wstring lookup_string; + HRESULT res; + + if (PathIsURLW(raw_target.c_str())) { + std::array scheme; + + DWORD out_len = scheme.size(); + res = UrlGetPartW(raw_target.c_str(), scheme.data(), &out_len, URL_PART_SCHEME, 0); + if (res != S_OK) { + BOOST_LOG(warning) << "Failed to extract URL scheme from URL: "sv << raw_target << " ["sv << util::hex(res).to_string_view() << ']'; + return converter.from_bytes(raw_cmd); + } + + // If the target is a URL, the class is found using the URL scheme (prior to and not including the ':') + lookup_string = scheme.data(); + } + else { + // If the target is not a URL, assume it's a regular file path + auto extension = PathFindExtensionW(raw_target.c_str()); + if (extension == nullptr || *extension == 0) { + // If the file has no extension, assume it's a command and allow CreateProcess() + // to try to find it via PATH + return converter.from_bytes(raw_cmd); + } + + // For regular files, the class is found using the file extension (including the dot) + lookup_string = extension; + } + + std::array shell_command_string; + { + // Overriding these predefined keys affects process-wide state, so serialize all calls + // to ensure the handle state is consistent while we perform the command query. + static std::mutex per_user_key_mutex; + auto lg = std::lock_guard(per_user_key_mutex); + + // Override HKEY_CLASSES_ROOT and HKEY_CURRENT_USER to ensure we query the correct class info + if (!override_per_user_predefined_keys(token)) { + return converter.from_bytes(raw_cmd); + } + + // Find the command string for the specified class + DWORD out_len = shell_command_string.size(); + res = AssocQueryStringW(ASSOCF_NOTRUNCATE, ASSOCSTR_COMMAND, lookup_string.c_str(), L"open", shell_command_string.data(), &out_len); + + // In some cases (UWP apps), we might not have a command for this target. If that happens, + // we'll have to launch via cmd.exe. This prevents proper job tracking, but that was already + // broken for UWP apps anyway due to how they are started by Windows. Even 'start /wait' + // doesn't work properly for UWP, so really no termination tracking seems to work at all. + // + // FIXME: Maybe we can improve this in the future. + if (res == HRESULT_FROM_WIN32(ERROR_NO_ASSOCIATION)) { + BOOST_LOG(warning) << "Using trampoline to handle target: "sv << raw_cmd; + std::wcscpy(shell_command_string.data(), L"cmd.exe /c start \"\" \"%1\" %*"); + res = S_OK; + } + + // Reset per-user keys back to the original value + override_per_user_predefined_keys(NULL); + } + + if (res != S_OK) { + BOOST_LOG(warning) << "Failed to query command string for raw command: "sv << raw_cmd << " ["sv << util::hex(res).to_string_view() << ']'; + return converter.from_bytes(raw_cmd); + } + + // Finally, construct the real command string that will be passed into CreateProcess(). + // We support common substitutions (%*, %1, %2, %L, %W, %V, etc), but there are other + // uncommon ones that are unsupported here. + // + // https://web.archive.org/web/20111002101214/http://msdn.microsoft.com/en-us/library/windows/desktop/cc144101(v=vs.85).aspx + std::wstring cmd_string { shell_command_string.data() }; + size_t match_pos = 0; + while ((match_pos = cmd_string.find_first_of(L'%', match_pos)) != std::wstring::npos) { + std::wstring match_replacement; + + // If no additional character exists after the match, the dangling '%' is stripped + if (match_pos + 1 == cmd_string.size()) { + cmd_string.erase(match_pos, 1); + break; + } + + // Shell command replacements are strictly '%' followed by a single non-'%' character + auto next_char = std::tolower(cmd_string.at(match_pos + 1)); + switch (next_char) { + // Escape character + case L'%': + match_replacement = L'%'; + break; + + // Argument replacements + case L'0': + case L'1': + case L'2': + case L'3': + case L'4': + case L'5': + case L'6': + case L'7': + case L'8': + case L'9': { + // Arguments numbers are 1-based, except for %0 which is equivalent to %1 + int index = next_char - L'0'; + if (next_char != L'0') { + index--; + } + + // Replace with the matching argument, or nothing if the index is invalid + if (index < raw_cmd_parts.size()) { + match_replacement = raw_cmd_parts.at(index); + } + break; + } + + // All arguments following the target + case L'*': + for (int i = 1; i < raw_cmd_parts.size(); i++) { + if (i > 1) { + match_replacement += L' '; + } + match_replacement += raw_cmd_parts.at(i); + } + break; + + // Long file path of target + case L'l': + case L'd': + case L'v': { + std::array path; + std::array other_dirs { working_dir.c_str(), nullptr }; + + // PathFindOnPath() is a little gross because it uses the same + // buffer for input and output, so we need to copy our input + // into the path array. + std::wcsncpy(path.data(), raw_target.c_str(), path.size()); + if (path[path.size() - 1] != 0) { + // The path was so long it was truncated by this copy. We'll + // assume it was an absolute path (likely) and use it unmodified. + match_replacement = raw_target; + } + // See if we can find the path on our search path or working directory + else if (PathFindOnPathW(path.data(), other_dirs.data())) { + match_replacement = std::wstring { path.data() }; + } + else { + // We couldn't find the target, so we'll just hope for the best + match_replacement = raw_target; + } + break; + } + + // Working directory + case L'w': + match_replacement = working_dir; + break; + + default: + BOOST_LOG(warning) << "Unsupported argument replacement: %%" << next_char; + break; + } + + // Replace the % and following character with the match replacement + cmd_string.replace(match_pos, 2, match_replacement); + + // Skip beyond the match replacement itself to prevent recursive replacement + match_pos += match_replacement.size(); + } + + BOOST_LOG(info) << "Resolved user-provided command '"sv << raw_cmd << "' to '"sv << cmd_string << '\''; + return cmd_string; + } + /** * @brief Run a command on the users profile. * @@ -567,11 +824,7 @@ namespace platf { */ bp::child run_command(bool elevated, bool interactive, const std::string &cmd, boost::filesystem::path &working_dir, const bp::environment &env, FILE *file, std::error_code &ec, bp::group *group) { - BOOL ret; - // Convert cmd, env, and working_dir to the appropriate character sets for Win32 APIs - std::wstring wcmd = converter.from_bytes(cmd); std::wstring start_dir = converter.from_bytes(working_dir.string()); - HANDLE job = group ? group->native_handle() : nullptr; STARTUPINFOEXW startup_info = create_startup_info(file, job ? &job : nullptr, ec); PROCESS_INFORMATION process_info; @@ -596,6 +849,7 @@ namespace platf { // Create a new console for interactive processes and use no console for non-interactive processes creation_flags |= interactive ? CREATE_NEW_CONSOLE : CREATE_NO_WINDOW; + BOOL ret; if (is_running_as_system()) { // Duplicate the current user's token HANDLE user_token = retrieve_users_token(elevated); @@ -619,6 +873,7 @@ namespace platf { // Open the process as the current user account, elevation is handled in the token itself. ec = impersonate_current_user(user_token, [&]() { std::wstring env_block = create_environment_block(cloned_env); + std::wstring wcmd = resolve_command_string(cmd, start_dir, user_token); ret = CreateProcessAsUserW(user_token, NULL, (LPWSTR) wcmd.c_str(), @@ -652,6 +907,7 @@ namespace platf { } std::wstring env_block = create_environment_block(cloned_env); + std::wstring wcmd = resolve_command_string(cmd, start_dir, NULL); ret = CreateProcessW(NULL, (LPWSTR) wcmd.c_str(), NULL, @@ -674,15 +930,11 @@ namespace platf { */ void open_url(const std::string &url) { - // set working dir to Windows system directory - auto working_dir = boost::filesystem::path(std::getenv("SystemRoot")); - boost::process::environment _env = boost::this_process::environment(); + auto working_dir = boost::filesystem::path(); std::error_code ec; - // Launch this as a non-interactive non-elevated command to avoid an extra console window - std::string cmd = R"(cmd /C "start )" + url + R"(")"; - auto child = run_command(false, false, cmd, working_dir, _env, nullptr, ec, nullptr); + auto child = run_command(false, false, url, working_dir, _env, nullptr, ec, nullptr); if (ec) { BOOST_LOG(warning) << "Couldn't open url ["sv << url << "]: System: "sv << ec.message(); } @@ -1209,11 +1461,25 @@ namespace platf { QOS_FLOWID flow_id; }; + /** + * @brief Enables QoS on the given socket for traffic to the specified destination. + * @param native_socket The native socket handle. + * @param address The destination address for traffic sent on this socket. + * @param port The destination port for traffic sent on this socket. + * @param data_type The type of traffic sent on this socket. + * @param dscp_tagging Specifies whether to enable DSCP tagging on outgoing traffic. + */ std::unique_ptr - enable_socket_qos(uintptr_t native_socket, boost::asio::ip::address &address, uint16_t port, qos_data_type_e data_type) { + enable_socket_qos(uintptr_t native_socket, boost::asio::ip::address &address, uint16_t port, qos_data_type_e data_type, bool dscp_tagging) { SOCKADDR_IN saddr_v4; SOCKADDR_IN6 saddr_v6; PSOCKADDR dest_addr; + bool using_connect_hack = false; + + // Windows doesn't support any concept of traffic priority without DSCP tagging + if (!dscp_tagging) { + return nullptr; + } static std::once_flag load_qwave_once_flag; std::call_once(load_qwave_once_flag, []() { @@ -1252,9 +1518,40 @@ namespace platf { return nullptr; } + auto disconnect_fg = util::fail_guard([&]() { + if (using_connect_hack) { + SOCKADDR_IN6 empty = {}; + empty.sin6_family = AF_INET6; + if (connect((SOCKET) native_socket, (PSOCKADDR) &empty, sizeof(empty)) < 0) { + auto wsaerr = WSAGetLastError(); + BOOST_LOG(error) << "qWAVE dual-stack workaround failed: "sv << wsaerr; + } + } + }); + if (address.is_v6()) { - saddr_v6 = to_sockaddr(address.to_v6(), port); + auto address_v6 = address.to_v6(); + + saddr_v6 = to_sockaddr(address_v6, port); dest_addr = (PSOCKADDR) &saddr_v6; + + // qWAVE doesn't properly support IPv4-mapped IPv6 addresses, nor does it + // correctly support IPv4 addresses on a dual-stack socket (despite MSDN's + // claims to the contrary). To get proper QoS tagging when hosting in dual + // stack mode, we will temporarily connect() the socket to allow qWAVE to + // successfully initialize a flow, then disconnect it again so WSASendMsg() + // works later on. + if (address_v6.is_v4_mapped()) { + if (connect((SOCKET) native_socket, (PSOCKADDR) &saddr_v6, sizeof(saddr_v6)) < 0) { + auto wsaerr = WSAGetLastError(); + BOOST_LOG(error) << "qWAVE dual-stack workaround failed: "sv << wsaerr; + } + else { + BOOST_LOG(debug) << "Using qWAVE connect() workaround for QoS tagging"sv; + using_connect_hack = true; + dest_addr = nullptr; + } + } } else { saddr_v4 = to_sockaddr(address.to_v4(), port); diff --git a/src/platform/windows/nvprefs/driver_settings.cpp b/src/platform/windows/nvprefs/driver_settings.cpp index 54529fd5ddd..2fbda6dc698 100644 --- a/src/platform/windows/nvprefs/driver_settings.cpp +++ b/src/platform/windows/nvprefs/driver_settings.cpp @@ -1,6 +1,6 @@ -#include "nvprefs_common.h" - +// local includes #include "driver_settings.h" +#include "nvprefs_common.h" namespace { @@ -94,9 +94,8 @@ namespace nvprefs { driver_settings_t::restore_global_profile_to_undo(const undo_data_t &undo_data) { if (!session_handle) return false; - auto [opengl_swapchain_saved, opengl_swapchain_our_value, opengl_swapchain_undo_value] = undo_data.get_opengl_swapchain(); - - if (opengl_swapchain_saved) { + const auto &swapchain_data = undo_data.get_opengl_swapchain(); + if (swapchain_data) { NvAPI_Status status; NvDRSProfileHandle profile_handle = 0; @@ -111,14 +110,14 @@ namespace nvprefs { setting.version = NVDRS_SETTING_VER; status = NvAPI_DRS_GetSetting(session_handle, profile_handle, OGL_CPL_PREFER_DXPRESENT_ID, &setting); - if (status == NVAPI_OK && setting.settingLocation == NVDRS_CURRENT_PROFILE_LOCATION && setting.u32CurrentValue == opengl_swapchain_our_value) { - if (opengl_swapchain_undo_value) { + if (status == NVAPI_OK && setting.settingLocation == NVDRS_CURRENT_PROFILE_LOCATION && setting.u32CurrentValue == swapchain_data->our_value) { + if (swapchain_data->undo_value) { setting = {}; setting.version = NVDRS_SETTING_VER1; setting.settingId = OGL_CPL_PREFER_DXPRESENT_ID; setting.settingType = NVDRS_DWORD_TYPE; setting.settingLocation = NVDRS_CURRENT_PROFILE_LOCATION; - setting.u32CurrentValue = *opengl_swapchain_undo_value; + setting.u32CurrentValue = *swapchain_data->undo_value; status = NvAPI_DRS_SetSetting(session_handle, profile_handle, &setting); @@ -159,6 +158,11 @@ namespace nvprefs { undo_data.reset(); NvAPI_Status status; + if (!get_nvprefs_options().opengl_vulkan_on_dxgi) { + // User requested to leave OpenGL/Vulkan DXGI swapchain setting alone + return true; + } + NvDRSProfileHandle profile_handle = 0; status = NvAPI_DRS_GetBaseProfile(session_handle, &profile_handle); if (status != NVAPI_OK) { @@ -261,9 +265,26 @@ namespace nvprefs { setting.version = NVDRS_SETTING_VER1; status = NvAPI_DRS_GetSetting(session_handle, profile_handle, PREFERRED_PSTATE_ID, &setting); - if (status != NVAPI_OK || - setting.settingLocation != NVDRS_CURRENT_PROFILE_LOCATION || - setting.u32CurrentValue != PREFERRED_PSTATE_PREFER_MAX) { + if (!get_nvprefs_options().sunshine_high_power_mode) { + if (status == NVAPI_OK && + setting.settingLocation == NVDRS_CURRENT_PROFILE_LOCATION) { + // User requested to not use high power mode for sunshine.exe, + // remove the setting from application profile if it's been set previously + + status = NvAPI_DRS_DeleteProfileSetting(session_handle, profile_handle, PREFERRED_PSTATE_ID); + if (status != NVAPI_OK && status != NVAPI_SETTING_NOT_FOUND) { + nvapi_error_message(status); + error_message("NvAPI_DRS_DeleteProfileSetting() PREFERRED_PSTATE failed"); + return false; + } + modified = true; + + info_message(std::wstring(L"Removed PREFERRED_PSTATE for ") + sunshine_application_path); + } + } + else if (status != NVAPI_OK || + setting.settingLocation != NVDRS_CURRENT_PROFILE_LOCATION || + setting.u32CurrentValue != PREFERRED_PSTATE_PREFER_MAX) { // Set power setting if needed setting = {}; setting.version = NVDRS_SETTING_VER1; diff --git a/src/platform/windows/nvprefs/driver_settings.h b/src/platform/windows/nvprefs/driver_settings.h index fbbc9aa1f57..8e10098bae1 100644 --- a/src/platform/windows/nvprefs/driver_settings.h +++ b/src/platform/windows/nvprefs/driver_settings.h @@ -1,5 +1,14 @@ #pragma once +// nvapi headers +// disable clang-format header reordering +// as needs types from +// clang-format off +#include +#include +// clang-format on + +// local includes #include "undo_data.h" namespace nvprefs { diff --git a/src/platform/windows/nvprefs/nvapi_opensource_wrapper.cpp b/src/platform/windows/nvprefs/nvapi_opensource_wrapper.cpp index e1010737d55..3465754839c 100644 --- a/src/platform/windows/nvprefs/nvapi_opensource_wrapper.cpp +++ b/src/platform/windows/nvprefs/nvapi_opensource_wrapper.cpp @@ -1,5 +1,11 @@ +// standard library headers +#include + +// local includes +#include "driver_settings.h" #include "nvprefs_common.h" +// special nvapi header that should be the last include #include namespace { diff --git a/src/platform/windows/nvprefs/nvprefs_common.cpp b/src/platform/windows/nvprefs/nvprefs_common.cpp index ba15dfe3084..367cfc896d9 100644 --- a/src/platform/windows/nvprefs/nvprefs_common.cpp +++ b/src/platform/windows/nvprefs/nvprefs_common.cpp @@ -1,4 +1,9 @@ +// local includes #include "nvprefs_common.h" +#include "src/logging.h" + +// read user override preferences from global sunshine config +#include "src/config.h" namespace nvprefs { @@ -22,4 +27,12 @@ namespace nvprefs { BOOST_LOG(error) << "nvprefs: " << message; } + nvprefs_options + get_nvprefs_options() { + nvprefs_options options; + options.opengl_vulkan_on_dxgi = config::video.nv_opengl_vulkan_on_dxgi; + options.sunshine_high_power_mode = config::video.nv_sunshine_high_power_mode; + return options; + } + } // namespace nvprefs diff --git a/src/platform/windows/nvprefs/nvprefs_common.h b/src/platform/windows/nvprefs/nvprefs_common.h index 7d4a661924e..2b286d9e8ad 100644 --- a/src/platform/windows/nvprefs/nvprefs_common.h +++ b/src/platform/windows/nvprefs/nvprefs_common.h @@ -3,19 +3,6 @@ // sunshine utility header for generic smart pointers #include "src/utility.h" -// sunshine boost::log severity levels -#include "src/main.h" - -// standard library headers -#include -#include -#include -#include -#include -#include -#include -#include - // winapi headers // disable clang-format header reordering // clang-format off @@ -23,21 +10,12 @@ #include // clang-format on -// nvapi headers -// disable clang-format header reordering -// clang-format off -#include -#include -// clang-format on - -// boost headers -#include - namespace nvprefs { struct safe_handle: public util::safe_ptr_v2 { using util::safe_ptr_v2::safe_ptr_v2; - explicit operator bool() const { + explicit + operator bool() const { auto handle = get(); return handle != NULL && handle != INVALID_HANDLE_VALUE; } @@ -67,4 +45,12 @@ namespace nvprefs { void error_message(const std::string &message); + struct nvprefs_options { + bool opengl_vulkan_on_dxgi = true; + bool sunshine_high_power_mode = true; + }; + + nvprefs_options + get_nvprefs_options(); + } // namespace nvprefs diff --git a/src/platform/windows/nvprefs/nvprefs_interface.cpp b/src/platform/windows/nvprefs/nvprefs_interface.cpp index 961788aed2c..628248ad619 100644 --- a/src/platform/windows/nvprefs/nvprefs_interface.cpp +++ b/src/platform/windows/nvprefs/nvprefs_interface.cpp @@ -1,9 +1,9 @@ -#include "nvprefs_common.h" - -#include "nvprefs_interface.h" +// standard includes +#include +// local includes #include "driver_settings.h" -#include "undo_data.h" +#include "nvprefs_interface.h" #include "undo_file.h" namespace { diff --git a/src/platform/windows/nvprefs/nvprefs_interface.h b/src/platform/windows/nvprefs/nvprefs_interface.h index 43d588c37a1..583e72c540d 100644 --- a/src/platform/windows/nvprefs/nvprefs_interface.h +++ b/src/platform/windows/nvprefs/nvprefs_interface.h @@ -1,5 +1,8 @@ #pragma once +// standard library headers +#include + namespace nvprefs { class nvprefs_interface { diff --git a/src/platform/windows/nvprefs/undo_data.cpp b/src/platform/windows/nvprefs/undo_data.cpp index 7abd81f7279..388b02cface 100644 --- a/src/platform/windows/nvprefs/undo_data.cpp +++ b/src/platform/windows/nvprefs/undo_data.cpp @@ -1,70 +1,117 @@ -#include "nvprefs_common.h" +// external includes +#include +// local includes +#include "nvprefs_common.h" #include "undo_data.h" -namespace { - - const auto opengl_swapchain_our_value_key = "/opengl_swapchain/our_value"; - const auto opengl_swapchain_undo_value_key = "/opengl_swapchain/undo_value"; - -} // namespace +using json = nlohmann::json; -namespace nvprefs { +// Separate namespace for ADL, otherwise we need to define json +// functions in the same namespace as our types +namespace nlohmann { + using data_t = nvprefs::undo_data_t::data_t; + using opengl_swapchain_t = data_t::opengl_swapchain_t; - void - undo_data_t::set_opengl_swapchain(uint32_t our_value, std::optional undo_value) { - data.set_at_pointer(opengl_swapchain_our_value_key, our_value); - if (undo_value) { - data.set_at_pointer(opengl_swapchain_undo_value_key, *undo_value); - } - else { - data.set_at_pointer(opengl_swapchain_undo_value_key, nullptr); + template + struct adl_serializer> { + static void + to_json(json &j, const std::optional &opt) { + if (opt == std::nullopt) { + j = nullptr; + } + else { + j = *opt; + } } - } - std::tuple> - undo_data_t::get_opengl_swapchain() const { - auto get_value = [this](const auto &key) -> std::tuple> { - try { - auto value = data.at_pointer(key); - if (value.is_null()) { - return { true, std::nullopt }; - } - else if (value.is_number()) { - return { true, value.template to_number() }; - } + static void + from_json(const json &j, std::optional &opt) { + if (j.is_null()) { + opt = std::nullopt; } - catch (...) { + else { + opt = j.template get(); } - error_message(std::string("Couldn't find ") + key + " element"); - return { false, std::nullopt }; - }; + } + }; - auto [our_value_present, our_value] = get_value(opengl_swapchain_our_value_key); - auto [undo_value_present, undo_value] = get_value(opengl_swapchain_undo_value_key); + template <> + struct adl_serializer { + static void + to_json(json &j, const data_t &data) { + j = json { { "opengl_swapchain", data.opengl_swapchain } }; + } - if (!our_value_present || !undo_value_present || !our_value) { - return { false, 0, std::nullopt }; + static void + from_json(const json &j, data_t &data) { + j.at("opengl_swapchain").get_to(data.opengl_swapchain); } + }; - return { true, *our_value, undo_value }; + template <> + struct adl_serializer { + static void + to_json(json &j, const opengl_swapchain_t &opengl_swapchain) { + j = json { + { "our_value", opengl_swapchain.our_value }, + { "undo_value", opengl_swapchain.undo_value } + }; + } + + static void + from_json(const json &j, opengl_swapchain_t &opengl_swapchain) { + j.at("our_value").get_to(opengl_swapchain.our_value); + j.at("undo_value").get_to(opengl_swapchain.undo_value); + } + }; +} // namespace nlohmann + +namespace nvprefs { + + void + undo_data_t::set_opengl_swapchain(uint32_t our_value, std::optional undo_value) { + data.opengl_swapchain = data_t::opengl_swapchain_t { + our_value, + undo_value + }; + } + + std::optional + undo_data_t::get_opengl_swapchain() const { + return data.opengl_swapchain; } std::string undo_data_t::write() const { - return boost::json::serialize(data); + try { + // Keep this assignment otherwise data will be treated as an array due to + // initializer list shenanigangs. + const json json_data = data; + return json_data.dump(); + } + catch (const std::exception &err) { + error_message(std::string { "failed to serialize json data" }); + return {}; + } } void undo_data_t::read(const std::vector &buffer) { - data = boost::json::parse(std::string_view(buffer.data(), buffer.size())); + try { + data = json::parse(std::begin(buffer), std::end(buffer)); + } + catch (const std::exception &err) { + error_message(std::string { "failed to parse json data: " } + err.what()); + data = {}; + } } void undo_data_t::merge(const undo_data_t &newer_data) { - auto [opengl_swapchain_saved, opengl_swapchain_our_value, opengl_swapchain_undo_value] = newer_data.get_opengl_swapchain(); - if (opengl_swapchain_saved) { - set_opengl_swapchain(opengl_swapchain_our_value, opengl_swapchain_undo_value); + const auto &swapchain_data = newer_data.get_opengl_swapchain(); + if (swapchain_data) { + set_opengl_swapchain(swapchain_data->our_value, swapchain_data->undo_value); } } diff --git a/src/platform/windows/nvprefs/undo_data.h b/src/platform/windows/nvprefs/undo_data.h index 999483e190f..d5f30251348 100644 --- a/src/platform/windows/nvprefs/undo_data.h +++ b/src/platform/windows/nvprefs/undo_data.h @@ -1,24 +1,33 @@ #pragma once +// standard library headers +#include +#include +#include +#include + namespace nvprefs { class undo_data_t { public: + struct data_t { + struct opengl_swapchain_t { + uint32_t our_value; + std::optional undo_value; + }; + + std::optional opengl_swapchain; + }; + void set_opengl_swapchain(uint32_t our_value, std::optional undo_value); - std::tuple> + std::optional get_opengl_swapchain() const; - void - write(std::ostream &stream) const; - std::string write() const; - void - read(std::istream &stream); - void read(const std::vector &buffer); @@ -26,7 +35,7 @@ namespace nvprefs { merge(const undo_data_t &newer_data); private: - boost::json::value data; + data_t data; }; } // namespace nvprefs diff --git a/src/platform/windows/nvprefs/undo_file.cpp b/src/platform/windows/nvprefs/undo_file.cpp index 8a45955005d..9f2648ab997 100644 --- a/src/platform/windows/nvprefs/undo_file.cpp +++ b/src/platform/windows/nvprefs/undo_file.cpp @@ -1,5 +1,4 @@ -#include "nvprefs_common.h" - +// local includes #include "undo_file.h" namespace { diff --git a/src/platform/windows/nvprefs/undo_file.h b/src/platform/windows/nvprefs/undo_file.h index 46dcba61226..acbfbae2757 100644 --- a/src/platform/windows/nvprefs/undo_file.h +++ b/src/platform/windows/nvprefs/undo_file.h @@ -1,5 +1,10 @@ #pragma once +// standard library headers +#include + +// local includes +#include "nvprefs_common.h" #include "undo_data.h" namespace nvprefs { diff --git a/src/platform/windows/publish.cpp b/src/platform/windows/publish.cpp index 30aa30e24cb..47c16721e20 100644 --- a/src/platform/windows/publish.cpp +++ b/src/platform/windows/publish.cpp @@ -13,6 +13,7 @@ #include "misc.h" #include "src/config.h" +#include "src/logging.h" #include "src/main.h" #include "src/network.h" #include "src/nvhttp.h" @@ -116,7 +117,7 @@ namespace platf::publish { DNS_SERVICE_INSTANCE instance {}; instance.pszInstanceName = name.data(); - instance.wPort = map_port(nvhttp::PORT_HTTP); + instance.wPort = net::map_port(nvhttp::PORT_HTTP); instance.pszHostName = host.data(); // Setting these values ensures Windows mDNS answers comply with RFC 1035. diff --git a/src/process.cpp b/src/process.cpp index 7042dd00c71..fef585cdae6 100644 --- a/src/process.cpp +++ b/src/process.cpp @@ -22,6 +22,7 @@ #include "config.h" #include "crypto.h" +#include "logging.h" #include "main.h" #include "platform/common.h" #include "system_tray.h" @@ -117,7 +118,12 @@ namespace proc { return boost::filesystem::path(); } - BOOST_LOG(debug) << "Parsed executable ["sv << parts.at(0) << "] from command ["sv << cmd << ']'; + BOOST_LOG(debug) << "Parsed target ["sv << parts.at(0) << "] from command ["sv << cmd << ']'; + + // If the target is a URL, don't parse any further here + if (parts.at(0).find("://") != std::string::npos) { + return boost::filesystem::path(); + } // If the cmd path is not an absolute path, resolve it using our PATH variable boost::filesystem::path cmd_path(parts.at(0)); @@ -129,14 +135,14 @@ namespace proc { } } - BOOST_LOG(debug) << "Resolved executable ["sv << parts.at(0) << "] to path ["sv << cmd_path << ']'; + BOOST_LOG(debug) << "Resolved target ["sv << parts.at(0) << "] to path ["sv << cmd_path << ']'; // Now that we have a complete path, we can just use parent_path() return cmd_path.parent_path(); } int - proc_t::execute(int app_id, rtsp_stream::launch_session_t launch_session) { + proc_t::execute(int app_id, std::shared_ptr launch_session) { // Ensure starting from a clean slate terminate(); @@ -157,14 +163,14 @@ namespace proc { // Add Stream-specific environment variables _env["SUNSHINE_APP_ID"] = std::to_string(_app_id); _env["SUNSHINE_APP_NAME"] = _app.name; - _env["SUNSHINE_CLIENT_WIDTH"] = std::to_string(launch_session.width); - _env["SUNSHINE_CLIENT_HEIGHT"] = std::to_string(launch_session.height); - _env["SUNSHINE_CLIENT_FPS"] = std::to_string(launch_session.fps); - _env["SUNSHINE_CLIENT_HDR"] = launch_session.enable_hdr ? "true" : "false"; - _env["SUNSHINE_CLIENT_GCMAP"] = std::to_string(launch_session.gcmap); - _env["SUNSHINE_CLIENT_HOST_AUDIO"] = launch_session.host_audio ? "true" : "false"; - _env["SUNSHINE_CLIENT_ENABLE_SOPS"] = launch_session.enable_sops ? "true" : "false"; - int channelCount = launch_session.surround_info & (65535); + _env["SUNSHINE_CLIENT_WIDTH"] = std::to_string(launch_session->width); + _env["SUNSHINE_CLIENT_HEIGHT"] = std::to_string(launch_session->height); + _env["SUNSHINE_CLIENT_FPS"] = std::to_string(launch_session->fps); + _env["SUNSHINE_CLIENT_HDR"] = launch_session->enable_hdr ? "true" : "false"; + _env["SUNSHINE_CLIENT_GCMAP"] = std::to_string(launch_session->gcmap); + _env["SUNSHINE_CLIENT_HOST_AUDIO"] = launch_session->host_audio ? "true" : "false"; + _env["SUNSHINE_CLIENT_ENABLE_SOPS"] = launch_session->enable_sops ? "true" : "false"; + int channelCount = launch_session->surround_info & (65535); switch (channelCount) { case 2: _env["SUNSHINE_CLIENT_AUDIO_CONFIGURATION"] = "2.0"; diff --git a/src/process.h b/src/process.h index 433c7669902..c8754992652 100644 --- a/src/process.h +++ b/src/process.h @@ -75,7 +75,7 @@ namespace proc { _apps(std::move(apps)) {} int - execute(int app_id, rtsp_stream::launch_session_t launch_session); + execute(int app_id, std::shared_ptr launch_session); /** * @return _app_id if a process is running, otherwise returns 0 diff --git a/src/rtsp.cpp b/src/rtsp.cpp index e92e177ac2c..73f224b8ef1 100644 --- a/src/rtsp.cpp +++ b/src/rtsp.cpp @@ -17,6 +17,7 @@ extern "C" { #include "config.h" #include "input.h" +#include "logging.h" #include "main.h" #include "network.h" #include "rtsp.h" @@ -41,51 +42,225 @@ namespace rtsp_stream { delete msg; } +#pragma pack(push, 1) + + struct encrypted_rtsp_header_t { + // We set the MSB in encrypted RTSP messages to allow format-agnostic + // parsing code to be able to tell encrypted from plaintext messages. + static constexpr std::uint32_t ENCRYPTED_MESSAGE_TYPE_BIT = 0x80000000; + + uint8_t * + payload() { + return (uint8_t *) (this + 1); + } + + std::uint32_t + payload_length() { + return util::endian::big(typeAndLength) & ~ENCRYPTED_MESSAGE_TYPE_BIT; + } + + bool + is_encrypted() { + return !!(util::endian::big(typeAndLength) & ENCRYPTED_MESSAGE_TYPE_BIT); + } + + // This field is the length of the payload + ENCRYPTED_MESSAGE_TYPE_BIT in big-endian + std::uint32_t typeAndLength; + + // This field is the number used to initialize the bottom 4 bytes of the AES IV in big-endian + std::uint32_t sequenceNumber; + + // This field is the AES GCM authentication tag + std::uint8_t tag[16]; + }; + +#pragma pack(pop) + class rtsp_server_t; using msg_t = util::safe_ptr; - using cmd_func_t = std::function; + using cmd_func_t = std::function; void print_msg(PRTSP_MESSAGE msg); void - cmd_not_found(tcp::socket &sock, msg_t &&req); + cmd_not_found(tcp::socket &sock, launch_session_t &, msg_t &&req); void - respond(tcp::socket &sock, POPTION_ITEM options, int statuscode, const char *status_msg, int seqn, const std::string_view &payload); + respond(tcp::socket &sock, launch_session_t &session, POPTION_ITEM options, int statuscode, const char *status_msg, int seqn, const std::string_view &payload); class socket_t: public std::enable_shared_from_this { public: - socket_t(boost::asio::io_service &ios, std::function &&handle_data_fn): + socket_t(boost::asio::io_service &ios, std::function &&handle_data_fn): handle_data_fn { std::move(handle_data_fn) }, sock { ios } {} + /** + * @brief Queues an asynchronous read to begin the next message. + */ void read() { - if (begin == std::end(msg_buf)) { + if (begin == std::end(msg_buf) || (session->rtsp_cipher && begin + sizeof(encrypted_rtsp_header_t) >= std::end(msg_buf))) { BOOST_LOG(error) << "RTSP: read(): Exceeded maximum rtsp packet size: "sv << msg_buf.size(); - respond(sock, nullptr, 400, "BAD REQUEST", 0, {}); + respond(sock, *session, nullptr, 400, "BAD REQUEST", 0, {}); - sock.close(); + boost::system::error_code ec; + sock.close(ec); return; } - sock.async_read_some( - boost::asio::buffer(begin, (std::size_t)(std::end(msg_buf) - begin)), + if (session->rtsp_cipher) { + // For encrypted RTSP, we will read the the entire header first + boost::asio::async_read(sock, + boost::asio::buffer(begin, sizeof(encrypted_rtsp_header_t)), + boost::bind( + &socket_t::handle_read_encrypted_header, shared_from_this(), + boost::asio::placeholders::error, + boost::asio::placeholders::bytes_transferred)); + } + else { + sock.async_read_some( + boost::asio::buffer(begin, (std::size_t)(std::end(msg_buf) - begin)), + boost::bind( + &socket_t::handle_read_plaintext, shared_from_this(), + boost::asio::placeholders::error, + boost::asio::placeholders::bytes_transferred)); + } + } + + /** + * @brief Handles the initial read of the header of an encrypted message. + * @param socket The socket the message was received on. + * @param ec The error code of the read operation. + * @param bytes The number of bytes read. + */ + static void + handle_read_encrypted_header(std::shared_ptr &socket, const boost::system::error_code &ec, std::size_t bytes) { + BOOST_LOG(debug) << "handle_read_encrypted_header(): Handle read of size: "sv << bytes << " bytes"sv; + + auto sock_close = util::fail_guard([&socket]() { + boost::system::error_code ec; + socket->sock.close(ec); + + if (ec) { + BOOST_LOG(error) << "RTSP: handle_read_encrypted_header(): Couldn't close tcp socket: "sv << ec.message(); + } + }); + + if (ec || bytes < sizeof(encrypted_rtsp_header_t)) { + BOOST_LOG(error) << "RTSP: handle_read_encrypted_header(): Couldn't read from tcp socket: "sv << ec.message(); + + respond(socket->sock, *socket->session, nullptr, 400, "BAD REQUEST", 0, {}); + return; + } + + auto header = (encrypted_rtsp_header_t *) socket->begin; + if (!header->is_encrypted()) { + BOOST_LOG(error) << "RTSP: handle_read_encrypted_header(): Rejecting unencrypted RTSP message"sv; + + respond(socket->sock, *socket->session, nullptr, 400, "BAD REQUEST", 0, {}); + return; + } + + auto payload_length = header->payload_length(); + + // Check if we have enough space to read this message + if (socket->begin + sizeof(*header) + payload_length >= std::end(socket->msg_buf)) { + BOOST_LOG(error) << "RTSP: handle_read_encrypted_header(): Exceeded maximum rtsp packet size: "sv << socket->msg_buf.size(); + + respond(socket->sock, *socket->session, nullptr, 400, "BAD REQUEST", 0, {}); + return; + } + + sock_close.disable(); + + // Read the remainder of the header and full encrypted payload + boost::asio::async_read(socket->sock, + boost::asio::buffer(socket->begin + bytes, payload_length), boost::bind( - &socket_t::handle_read, shared_from_this(), + &socket_t::handle_read_encrypted_message, socket->shared_from_this(), boost::asio::placeholders::error, boost::asio::placeholders::bytes_transferred)); } + /** + * @brief Handles the final read of the content of an encrypted message. + * @param socket The socket the message was received on. + * @param ec The error code of the read operation. + * @param bytes The number of bytes read. + */ + static void + handle_read_encrypted_message(std::shared_ptr &socket, const boost::system::error_code &ec, std::size_t bytes) { + BOOST_LOG(debug) << "handle_read_encrypted(): Handle read of size: "sv << bytes << " bytes"sv; + + auto sock_close = util::fail_guard([&socket]() { + boost::system::error_code ec; + socket->sock.close(ec); + + if (ec) { + BOOST_LOG(error) << "RTSP: handle_read_encrypted_message(): Couldn't close tcp socket: "sv << ec.message(); + } + }); + + auto header = (encrypted_rtsp_header_t *) socket->begin; + auto payload_length = header->payload_length(); + auto seq = util::endian::big(header->sequenceNumber); + + if (ec || bytes < payload_length) { + BOOST_LOG(error) << "RTSP: handle_read_encrypted(): Couldn't read from tcp socket: "sv << ec.message(); + + respond(socket->sock, *socket->session, nullptr, 400, "BAD REQUEST", 0, {}); + return; + } + + // We use the deterministic IV construction algorithm specified in NIST SP 800-38D + // Section 8.2.1. The sequence number is our "invocation" field and the 'RC' in the + // high bytes is the "fixed" field. Because each client provides their own unique + // key, our values in the fixed field need only uniquely identify each independent + // use of the client's key with AES-GCM in our code. + // + // The sequence number is 32 bits long which allows for 2^32 RTSP messages to be + // received from each client before the IV repeats. + crypto::aes_t iv(12); + std::copy_n((uint8_t *) &seq, sizeof(seq), std::begin(iv)); + iv[10] = 'C'; // Client originated + iv[11] = 'R'; // RTSP + + std::vector plaintext; + if (socket->session->rtsp_cipher->decrypt(std::string_view { (const char *) header->tag, sizeof(header->tag) + bytes }, plaintext, &iv)) { + BOOST_LOG(error) << "Failed to verify RTSP message tag"sv; + + respond(socket->sock, *socket->session, nullptr, 400, "BAD REQUEST", 0, {}); + return; + } + + msg_t req { new msg_t::element_type {} }; + if (auto status = parseRtspMessage(req.get(), (char *) plaintext.data(), plaintext.size())) { + BOOST_LOG(error) << "Malformed RTSP message: ["sv << status << ']'; + + respond(socket->sock, *socket->session, nullptr, 400, "BAD REQUEST", 0, {}); + return; + } + + sock_close.disable(); + + print_msg(req.get()); + + socket->handle_data(std::move(req)); + } + + /** + * @brief Queues an asynchronous read of the payload portion of a plaintext message. + */ void - read_payload() { + read_plaintext_payload() { if (begin == std::end(msg_buf)) { - BOOST_LOG(error) << "RTSP: read_payload(): Exceeded maximum rtsp packet size: "sv << msg_buf.size(); + BOOST_LOG(error) << "RTSP: read_plaintext_payload(): Exceeded maximum rtsp packet size: "sv << msg_buf.size(); - respond(sock, nullptr, 400, "BAD REQUEST", 0, {}); + respond(sock, *session, nullptr, 400, "BAD REQUEST", 0, {}); - sock.close(); + boost::system::error_code ec; + sock.close(ec); return; } @@ -93,26 +268,32 @@ namespace rtsp_stream { sock.async_read_some( boost::asio::buffer(begin, (std::size_t)(std::end(msg_buf) - begin)), boost::bind( - &socket_t::handle_payload, shared_from_this(), + &socket_t::handle_plaintext_payload, shared_from_this(), boost::asio::placeholders::error, boost::asio::placeholders::bytes_transferred)); } + /** + * @brief Handles the read of the payload portion of a plaintext message. + * @param socket The socket the message was received on. + * @param ec The error code of the read operation. + * @param bytes The number of bytes read. + */ static void - handle_payload(std::shared_ptr &socket, const boost::system::error_code &ec, std::size_t bytes) { - BOOST_LOG(debug) << "handle_payload(): Handle read of size: "sv << bytes << " bytes"sv; + handle_plaintext_payload(std::shared_ptr &socket, const boost::system::error_code &ec, std::size_t bytes) { + BOOST_LOG(debug) << "handle_plaintext_payload(): Handle read of size: "sv << bytes << " bytes"sv; auto sock_close = util::fail_guard([&socket]() { boost::system::error_code ec; socket->sock.close(ec); if (ec) { - BOOST_LOG(error) << "RTSP: handle_payload(): Couldn't close tcp socket: "sv << ec.message(); + BOOST_LOG(error) << "RTSP: handle_plaintext_payload(): Couldn't close tcp socket: "sv << ec.message(); } }); if (ec) { - BOOST_LOG(error) << "RTSP: handle_payload(): Couldn't read from tcp socket: "sv << ec.message(); + BOOST_LOG(error) << "RTSP: handle_plaintext_payload(): Couldn't read from tcp socket: "sv << ec.message(); return; } @@ -122,14 +303,14 @@ namespace rtsp_stream { if (auto status = parseRtspMessage(req.get(), socket->msg_buf.data(), (std::size_t)(end - socket->msg_buf.data()))) { BOOST_LOG(error) << "Malformed RTSP message: ["sv << status << ']'; - respond(socket->sock, nullptr, 400, "BAD REQUEST", req->sequenceNumber, {}); + respond(socket->sock, *socket->session, nullptr, 400, "BAD REQUEST", 0, {}); return; } sock_close.disable(); auto fg = util::fail_guard([&socket]() { - socket->read_payload(); + socket->read_plaintext_payload(); }); auto content_length = 0; @@ -161,18 +342,24 @@ namespace rtsp_stream { socket->begin = end; } + /** + * @brief Handles the read of the header portion of a plaintext message. + * @param socket The socket the message was received on. + * @param ec The error code of the read operation. + * @param bytes The number of bytes read. + */ static void - handle_read(std::shared_ptr &socket, const boost::system::error_code &ec, std::size_t bytes) { - BOOST_LOG(debug) << "handle_read(): Handle read of size: "sv << bytes << " bytes"sv; + handle_read_plaintext(std::shared_ptr &socket, const boost::system::error_code &ec, std::size_t bytes) { + BOOST_LOG(debug) << "handle_read_plaintext(): Handle read of size: "sv << bytes << " bytes"sv; if (ec) { - BOOST_LOG(error) << "RTSP: handle_read(): Couldn't read from tcp socket: "sv << ec.message(); + BOOST_LOG(error) << "RTSP: handle_read_plaintext(): Couldn't read from tcp socket: "sv << ec.message(); boost::system::error_code ec; socket->sock.close(ec); if (ec) { - BOOST_LOG(error) << "RTSP: handle_read(): Couldn't close tcp socket: "sv << ec.message(); + BOOST_LOG(error) << "RTSP: handle_read_plaintext(): Couldn't close tcp socket: "sv << ec.message(); } return; @@ -201,15 +388,15 @@ namespace rtsp_stream { buf_size = end - socket->begin; fg.disable(); - handle_payload(socket, ec, buf_size); + handle_plaintext_payload(socket, ec, buf_size); } void handle_data(msg_t &&req) { - handle_data_fn(sock, std::move(req)); + handle_data_fn(sock, *session, std::move(req)); } - std::function handle_data_fn; + std::function handle_data_fn; tcp::socket sock; @@ -217,6 +404,8 @@ namespace rtsp_stream { char *crlf; char *begin = msg_buf.data(); + + std::shared_ptr session; }; class rtsp_server_t { @@ -251,8 +440,8 @@ namespace rtsp_stream { return -1; } - next_socket = std::make_shared(ios, [this](tcp::socket &sock, msg_t &&msg) { - handle_msg(sock, std::move(msg)); + next_socket = std::make_shared(ios, [this](tcp::socket &sock, launch_session_t &session, msg_t &&msg) { + handle_msg(sock, session, std::move(msg)); }); acceptor.async_accept(next_socket->sock, [this](const auto &ec) { @@ -269,16 +458,17 @@ namespace rtsp_stream { } void - handle_msg(tcp::socket &sock, msg_t &&req) { + handle_msg(tcp::socket &sock, launch_session_t &session, msg_t &&req) { auto func = _map_cmd_cb.find(req->message.request.command); if (func != std::end(_map_cmd_cb)) { - func->second(this, sock, std::move(req)); + func->second(this, sock, session, std::move(req)); } else { - cmd_not_found(sock, std::move(req)); + cmd_not_found(sock, session, std::move(req)); } - sock.shutdown(boost::asio::socket_base::shutdown_type::shutdown_both); + boost::system::error_code ec; + sock.shutdown(boost::asio::socket_base::shutdown_type::shutdown_both, ec); } void @@ -292,12 +482,26 @@ namespace rtsp_stream { } auto socket = std::move(next_socket); - socket->read(); - next_socket = std::make_shared(ios, [this](tcp::socket &sock, msg_t &&msg) { - handle_msg(sock, std::move(msg)); - }); + auto launch_session { launch_event.view(0s) }; + if (launch_session) { + // Associate the current RTSP session with this socket and start reading + socket->session = launch_session; + socket->read(); + } + else { + // This can happen due to normal things like port scanning, so let's not make these visible by default + BOOST_LOG(debug) << "No pending session for incoming RTSP connection"sv; + // If there is no session pending, close the connection immediately + boost::system::error_code ec; + socket->sock.close(ec); + } + + // Queue another asynchronous accept for the next incoming connection + next_socket = std::make_shared(ios, [this](tcp::socket &sock, launch_session_t &session, msg_t &&msg) { + handle_msg(sock, session, std::move(msg)); + }); acceptor.async_accept(next_socket->sock, [this](const auto &ec) { handle_accept(ec); }); @@ -313,16 +517,9 @@ namespace rtsp_stream { * @note If the client does not begin streaming within the ping_timeout, * the session will be discarded. * @param launch_session Streaming session information. - * - * EXAMPLES: - * ```cpp - * launch_session_t launch_session; - * rtsp_server_t server {}; - * server.session_raise(launch_session); - * ``` */ void - session_raise(rtsp_stream::launch_session_t launch_session) { + session_raise(std::shared_ptr launch_session) { auto now = std::chrono::steady_clock::now(); // If a launch event is still pending, don't overwrite it. @@ -332,7 +529,26 @@ namespace rtsp_stream { raised_timeout = now + config::stream.ping_timeout; --_slot_count; - launch_event.raise(launch_session); + launch_event.raise(std::move(launch_session)); + } + + /** + * @brief Clear state for the oldest launch session. + * @param launch_session_id The ID of the session to clear. + */ + void + session_clear(uint32_t launch_session_id) { + // We currently only support a single pending RTSP session, + // so the ID should always match the one for that session. + auto launch_session = launch_event.view(0s); + if (launch_session) { + if (launch_session->id != launch_session_id) { + BOOST_LOG(error) << "Attempted to clear unexpected session: "sv << launch_session_id << " vs "sv << launch_session->id; + } + else { + launch_event.pop(); + } + } } int @@ -340,7 +556,7 @@ namespace rtsp_stream { return config::stream.channels - _slot_count; } - safe::event_t launch_event; + safe::event_t> launch_event; /** * @brief Clear launch sessions. @@ -420,8 +636,17 @@ namespace rtsp_stream { rtsp_server_t server {}; void - launch_session_raise(rtsp_stream::launch_session_t launch_session) { - server.session_raise(launch_session); + launch_session_raise(std::shared_ptr launch_session) { + server.session_raise(std::move(launch_session)); + } + + /** + * @brief Clear state for the specified launch session. + * @param launch_session_id The ID of the session to clear. + */ + void + launch_session_clear(uint32_t launch_session_id) { + server.session_clear(launch_session_id); } int @@ -450,7 +675,7 @@ namespace rtsp_stream { } void - respond(tcp::socket &sock, msg_t &resp) { + respond(tcp::socket &sock, launch_session_t &session, msg_t &resp) { auto payload = std::make_pair(resp->payload, resp->payloadLength); // Restore response message for proper destruction @@ -470,30 +695,70 @@ namespace rtsp_stream { << 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 }; - - if (send(sock, tmp_resp)) { - return; + // Encrypt the RTSP message if encryption is enabled + if (session.rtsp_cipher) { + // We use the deterministic IV construction algorithm specified in NIST SP 800-38D + // Section 8.2.1. The sequence number is our "invocation" field and the 'RH' in the + // high bytes is the "fixed" field. Because each client provides their own unique + // key, our values in the fixed field need only uniquely identify each independent + // use of the client's key with AES-GCM in our code. + // + // The sequence number is 32 bits long which allows for 2^32 RTSP messages to be + // sent to each client before the IV repeats. + crypto::aes_t iv(12); + session.rtsp_iv_counter++; + std::copy_n((uint8_t *) &session.rtsp_iv_counter, sizeof(session.rtsp_iv_counter), std::begin(iv)); + iv[10] = 'H'; // Host originated + iv[11] = 'R'; // RTSP + + // Allocate the message with an empty header and reserved space for the payload + auto payload_length = serialized_len + payload.second; + std::vector message(sizeof(encrypted_rtsp_header_t)); + message.reserve(message.size() + payload_length); + + // Copy the complete plaintext into the message + std::copy_n(raw_resp.get(), serialized_len, std::back_inserter(message)); + std::copy_n(payload.first, payload.second, std::back_inserter(message)); + + // Initialize the message header + auto header = (encrypted_rtsp_header_t *) message.data(); + header->typeAndLength = util::endian::big(encrypted_rtsp_header_t::ENCRYPTED_MESSAGE_TYPE_BIT + payload_length); + header->sequenceNumber = util::endian::big(session.rtsp_iv_counter); + + // Encrypt the RTSP message in place + session.rtsp_cipher->encrypt(std::string_view { (const char *) header->payload(), (std::size_t) payload_length }, header->tag, &iv); + + // Send the full encrypted message + send(sock, std::string_view { (char *) message.data(), message.size() }); } + else { + std::string_view tmp_resp { raw_resp.get(), (size_t) serialized_len }; + + // Send the plaintext RTSP message header + if (send(sock, tmp_resp)) { + return; + } - send(sock, std::string_view { payload.first, (std::size_t) payload.second }); + // Send the plaintext RTSP message payload (if present) + send(sock, std::string_view { payload.first, (std::size_t) payload.second }); + } } void - respond(tcp::socket &sock, POPTION_ITEM options, int statuscode, const char *status_msg, int seqn, const std::string_view &payload) { + respond(tcp::socket &sock, launch_session_t &session, 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("RTSP/1.0"), statuscode, const_cast(status_msg), seqn, options, const_cast(payload.data()), (int) payload.size()); - respond(sock, resp); + respond(sock, session, resp); } void - cmd_not_found(tcp::socket &sock, msg_t &&req) { - respond(sock, nullptr, 404, "NOT FOUND", req->sequenceNumber, {}); + cmd_not_found(tcp::socket &sock, launch_session_t &session, msg_t &&req) { + respond(sock, session, nullptr, 404, "NOT FOUND", req->sequenceNumber, {}); } void - cmd_option(rtsp_server_t *server, tcp::socket &sock, msg_t &&req) { + cmd_option(rtsp_server_t *server, tcp::socket &sock, launch_session_t &session, msg_t &&req) { OPTION_ITEM option {}; // I know these string literals will not be modified @@ -502,11 +767,11 @@ namespace rtsp_stream { auto seqn_str = std::to_string(req->sequenceNumber); option.content = const_cast(seqn_str.c_str()); - respond(sock, &option, 200, "OK", req->sequenceNumber, {}); + respond(sock, session, &option, 200, "OK", req->sequenceNumber, {}); } void - cmd_describe(rtsp_server_t *server, tcp::socket &sock, msg_t &&req) { + cmd_describe(rtsp_server_t *server, tcp::socket &sock, launch_session_t &session, msg_t &&req) { OPTION_ITEM option {}; // I know these string literals will not be modified @@ -525,14 +790,7 @@ namespace rtsp_stream { uint32_t encryption_flags_requested = SS_ENC_CONTROL_V2; // Determine the encryption desired for this remote endpoint - auto nettype = net::from_address(sock.remote_endpoint().address().to_string()); - int encryption_mode; - if (nettype == net::net_e::PC || nettype == net::net_e::LAN) { - encryption_mode = config::stream.lan_encryption_mode; - } - else { - encryption_mode = config::stream.wan_encryption_mode; - } + auto encryption_mode = net::encryption_mode_for_address(sock.remote_endpoint().address()); if (encryption_mode != config::ENCRYPTION_MODE_NEVER) { // Advertise support for video encryption if it's not disabled encryption_flags_supported |= SS_ENC_VIDEO; @@ -587,11 +845,11 @@ namespace rtsp_stream { ss << std::endl; } - respond(sock, &option, 200, "OK", req->sequenceNumber, ss.str()); + respond(sock, session, &option, 200, "OK", req->sequenceNumber, ss.str()); } void - cmd_setup(rtsp_server_t *server, tcp::socket &sock, msg_t &&req) { + cmd_setup(rtsp_server_t *server, tcp::socket &sock, launch_session_t &session, msg_t &&req) { OPTION_ITEM options[4] {}; auto &seqn = options[0]; @@ -604,14 +862,6 @@ namespace rtsp_stream { auto seqn_str = std::to_string(req->sequenceNumber); seqn.content = const_cast(seqn_str.c_str()); - if (!server->launch_event.peek()) { - // /launch has not been used - - respond(sock, &seqn, 503, "Service Unavailable", req->sequenceNumber, {}); - return; - } - auto launch_session { server->launch_event.view() }; - 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), '/'); @@ -619,16 +869,16 @@ namespace rtsp_stream { std::uint16_t port; if (type == "audio"sv) { - port = map_port(stream::AUDIO_STREAM_PORT); + port = net::map_port(stream::AUDIO_STREAM_PORT); } else if (type == "video"sv) { - port = map_port(stream::VIDEO_STREAM_PORT); + port = net::map_port(stream::VIDEO_STREAM_PORT); } else if (type == "control"sv) { - port = map_port(stream::CONTROL_PORT); + port = net::map_port(stream::CONTROL_PORT); } else { - cmd_not_found(sock, std::move(req)); + cmd_not_found(sock, session, std::move(req)); return; } @@ -647,23 +897,23 @@ namespace rtsp_stream { port_option.content = port_value.data(); // Send identifiers that will be echoed in the other connections - auto connect_data = std::to_string(launch_session->control_connect_data); + auto connect_data = std::to_string(session.control_connect_data); if (type == "control"sv) { payload_option.option = const_cast("X-SS-Connect-Data"); payload_option.content = connect_data.data(); } else { payload_option.option = const_cast("X-SS-Ping-Payload"); - payload_option.content = launch_session->av_ping_payload.data(); + payload_option.content = session.av_ping_payload.data(); } port_option.next = &payload_option; - respond(sock, &seqn, 200, "OK", req->sequenceNumber, {}); + respond(sock, session, &seqn, 200, "OK", req->sequenceNumber, {}); } void - cmd_announce(rtsp_server_t *server, tcp::socket &sock, msg_t &&req) { + cmd_announce(rtsp_server_t *server, tcp::socket &sock, launch_session_t &session, msg_t &&req) { OPTION_ITEM option {}; // I know these string literals will not be modified @@ -672,14 +922,6 @@ namespace rtsp_stream { auto seqn_str = std::to_string(req->sequenceNumber); option.content = const_cast(seqn_str.c_str()); - if (!server->launch_event.peek()) { - // /launch has not been used - - respond(sock, &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 lines; @@ -739,7 +981,7 @@ namespace rtsp_stream { stream::config_t config; std::int64_t configuredBitrateKbps; - config.audio.flags[audio::config_t::HOST_AUDIO] = launch_session->host_audio; + config.audio.flags[audio::config_t::HOST_AUDIO] = session.host_audio; try { config.audio.channels = util::from_view(args.at("x-nv-audio.surround.numChannels"sv)); config.audio.mask = util::from_view(args.at("x-nv-audio.surround.channelMask"sv)); @@ -774,7 +1016,7 @@ namespace rtsp_stream { configuredBitrateKbps = util::from_view(args.at("x-ml-video.configuredBitrateKbps"sv)); } catch (std::out_of_range &) { - respond(sock, &option, 400, "BAD REQUEST", req->sequenceNumber, {}); + respond(sock, session, &option, 400, "BAD REQUEST", req->sequenceNumber, {}); return; } @@ -820,57 +1062,50 @@ namespace rtsp_stream { if (config.monitor.videoFormat == 1 && video::active_hevc_mode == 1) { BOOST_LOG(warning) << "HEVC is disabled, yet the client requested HEVC"sv; - respond(sock, &option, 400, "BAD REQUEST", req->sequenceNumber, {}); + respond(sock, session, &option, 400, "BAD REQUEST", req->sequenceNumber, {}); return; } if (config.monitor.videoFormat == 2 && video::active_av1_mode == 1) { BOOST_LOG(warning) << "AV1 is disabled, yet the client requested AV1"sv; - respond(sock, &option, 400, "BAD REQUEST", req->sequenceNumber, {}); + respond(sock, session, &option, 400, "BAD REQUEST", req->sequenceNumber, {}); return; } // Check that any required encryption is enabled - auto nettype = net::from_address(sock.remote_endpoint().address().to_string()); - int encryption_mode; - if (nettype == net::net_e::PC || nettype == net::net_e::LAN) { - encryption_mode = config::stream.lan_encryption_mode; - } - else { - encryption_mode = config::stream.wan_encryption_mode; - } + auto encryption_mode = net::encryption_mode_for_address(sock.remote_endpoint().address()); if (encryption_mode == config::ENCRYPTION_MODE_MANDATORY && (config.encryptionFlagsEnabled & (SS_ENC_VIDEO | SS_ENC_AUDIO)) != (SS_ENC_VIDEO | SS_ENC_AUDIO)) { BOOST_LOG(error) << "Rejecting client that cannot comply with mandatory encryption requirement"sv; - respond(sock, &option, 403, "Forbidden", req->sequenceNumber, {}); + respond(sock, session, &option, 403, "Forbidden", req->sequenceNumber, {}); return; } - auto session = stream::session::alloc(config, *launch_session); + auto stream_session = stream::session::alloc(config, session); - auto slot = server->accept(session); + auto slot = server->accept(stream_session); if (!slot) { BOOST_LOG(info) << "Ran out of slots for client from ["sv << ']'; - respond(sock, &option, 503, "Service Unavailable", req->sequenceNumber, {}); + respond(sock, session, &option, 503, "Service Unavailable", req->sequenceNumber, {}); return; } - if (stream::session::start(*session, sock.remote_endpoint().address().to_string())) { + if (stream::session::start(*stream_session, sock.remote_endpoint().address().to_string())) { BOOST_LOG(error) << "Failed to start a streaming session"sv; server->clear(slot); - respond(sock, &option, 500, "Internal Server Error", req->sequenceNumber, {}); + respond(sock, session, &option, 500, "Internal Server Error", req->sequenceNumber, {}); return; } - respond(sock, &option, 200, "OK", req->sequenceNumber, {}); + respond(sock, session, &option, 200, "OK", req->sequenceNumber, {}); } void - cmd_play(rtsp_server_t *server, tcp::socket &sock, msg_t &&req) { + cmd_play(rtsp_server_t *server, tcp::socket &sock, launch_session_t &session, msg_t &&req) { OPTION_ITEM option {}; // I know these string literals will not be modified @@ -879,7 +1114,7 @@ namespace rtsp_stream { auto seqn_str = std::to_string(req->sequenceNumber); option.content = const_cast(seqn_str.c_str()); - respond(sock, &option, 200, "OK", req->sequenceNumber, {}); + respond(sock, session, &option, 200, "OK", req->sequenceNumber, {}); } void @@ -891,12 +1126,11 @@ namespace rtsp_stream { server.map("DESCRIBE"sv, &cmd_describe); server.map("SETUP"sv, &cmd_setup); server.map("ANNOUNCE"sv, &cmd_announce); - server.map("PLAY"sv, &cmd_play); boost::system::error_code ec; - if (server.bind(net::af_from_enum_string(config::sunshine.address_family), map_port(rtsp_stream::RTSP_SETUP_PORT), ec)) { - BOOST_LOG(fatal) << "Couldn't bind RTSP server to port ["sv << map_port(rtsp_stream::RTSP_SETUP_PORT) << "], " << ec.message(); + if (server.bind(net::af_from_enum_string(config::sunshine.address_family), net::map_port(rtsp_stream::RTSP_SETUP_PORT), ec)) { + BOOST_LOG(fatal) << "Couldn't bind RTSP server to port ["sv << net::map_port(rtsp_stream::RTSP_SETUP_PORT) << "], " << ec.message(); shutdown_event->raise(true); return; diff --git a/src/rtsp.h b/src/rtsp.h index 3cdd5c7c5ea..20bb8453998 100644 --- a/src/rtsp.h +++ b/src/rtsp.h @@ -13,6 +13,8 @@ namespace rtsp_stream { constexpr auto RTSP_SETUP_PORT = 21; struct launch_session_t { + uint32_t id; + crypto::aes_t gcm_key; crypto::aes_t iv; @@ -29,10 +31,18 @@ namespace rtsp_stream { int surround_info; bool enable_hdr; bool enable_sops; + + std::optional rtsp_cipher; + std::string rtsp_url_scheme; + uint32_t rtsp_iv_counter; }; void - launch_session_raise(launch_session_t launch_session); + launch_session_raise(std::shared_ptr launch_session); + + void + launch_session_clear(uint32_t launch_session_id); + int session_count(); diff --git a/src/stream.cpp b/src/stream.cpp index 2e0c0f521a2..12fb8663fc8 100644 --- a/src/stream.cpp +++ b/src/stream.cpp @@ -19,6 +19,7 @@ extern "C" { #include "config.h" #include "input.h" +#include "logging.h" #include "main.h" #include "network.h" #include "stat_trackers.h" @@ -128,7 +129,7 @@ namespace stream { } std::uint8_t iv[12]; // 12-byte IV is ideal for AES-GCM - std::uint32_t unused; + std::uint32_t frameNumber; std::uint8_t tag[16]; }; @@ -246,16 +247,13 @@ namespace stream { // return bytes written on success // return -1 on error static inline int - encode_audio(bool encrypted, const audio::buffer_t &plaintext, audio_packet_t &destination, std::uint32_t avRiKeyIv, crypto::cipher::cbc_t &cbc) { + encode_audio(bool encrypted, const audio::buffer_t &plaintext, audio_packet_t &destination, crypto::aes_t &iv, crypto::cipher::cbc_t &cbc) { // If encryption isn't enabled if (!encrypted) { std::copy(std::begin(plaintext), std::end(plaintext), destination->payload()); return plaintext.size(); } - crypto::aes_t iv(16); - *(std::uint32_t *) iv.data() = util::endian::big(avRiKeyIv + destination->rtp.sequenceNumber); - return cbc.encrypt(std::string_view { (char *) std::begin(plaintext), plaintext.size() }, destination->payload(), &iv); } @@ -401,6 +399,8 @@ namespace stream { struct { crypto::cipher::gcm_t cipher; crypto::aes_t legacy_input_enc_iv; // Only used when the client doesn't support full control stream encryption + crypto::aes_t incoming_iv; + crypto::aes_t outgoing_iv; std::uint32_t connect_data; // Used for new clients with ML_FF_SESSION_ID_V1 std::string expected_peer_address; // Only used for legacy clients without ML_FF_SESSION_ID_V1 @@ -412,6 +412,8 @@ namespace stream { safe::mail_raw_t::event_t hdr_queue; } control; + std::uint32_t launch_session_id; + safe::mail_raw_t::event_t shutdown_event; safe::signal_t controlEnd; @@ -437,7 +439,7 @@ namespace stream { auto seq = session->control.seq++; - crypto::aes_t iv; + auto &iv = session->control.outgoing_iv; if (session->config.encryptionFlagsEnabled & SS_ENC_CONTROL_V2) { // We use the deterministic IV construction algorithm specified in NIST SP 800-38D // Section 8.2.1. The sequence number is our "invocation" field and the 'CH' in the @@ -524,6 +526,9 @@ namespace stream { } } + // Once the control stream connection is established, RTSP session state can be torn down + rtsp_stream::launch_session_clear(session_p->launch_session_id); + session_p->control.peer = peer; // Use the local address from the control connection as the source address @@ -977,7 +982,7 @@ namespace stream { std::string_view tagged_cipher { (char *) header->payload(), (size_t) tagged_cipher_length }; auto &cipher = session->control.cipher; - crypto::aes_t iv; + auto &iv = session->control.incoming_iv; if (session->config.encryptionFlagsEnabled & SS_ENC_CONTROL_V2) { // We use the deterministic IV construction algorithm specified in NIST SP 800-38D // Section 8.2.1. The sequence number is our "invocation" field and the 'CC' in the @@ -1031,8 +1036,12 @@ namespace stream { // This thread handles latency-sensitive control messages platf::adjust_thread_priority(platf::thread_priority_e::critical); - auto shutdown_event = mail::man->event(mail::broadcast_shutdown); - while (!shutdown_event->peek()) { + // Check for both the full shutdown event and the shutdown event for this + // broadcast to ensure we can inform connected clients of our graceful + // termination when we shut down. + auto shutdown_event = mail::man->event(mail::shutdown); + auto broadcast_shutdown_event = mail::man->event(mail::broadcast_shutdown); + while (!shutdown_event->peek() && !broadcast_shutdown_event->peek()) { bool has_session_awaiting_peer = false; { @@ -1041,6 +1050,11 @@ namespace stream { auto now = std::chrono::steady_clock::now(); KITTY_WHILE_LOOP(auto pos = std::begin(*server->_sessions), pos != std::end(*server->_sessions), { + // Don't perform additional session processing if we're shutting down + if (shutdown_event->peek() || broadcast_shutdown_event->peek()) { + break; + } + auto session = *pos; if (now > session->pingTimeout) { @@ -1241,6 +1255,7 @@ namespace stream { platf::adjust_thread_priority(platf::thread_priority_e::high); stat_trackers::min_max_avg_tracker frame_processing_latency_tracker; + crypto::aes_t iv(12); while (auto packet = packets->pop()) { if (shutdown_event->peek()) { @@ -1413,14 +1428,13 @@ namespace stream { // // The IV counter is 64 bits long which allows for 2^64 encrypted video packets // to be sent to each client before the IV repeats. - crypto::aes_t iv(12); std::copy_n((uint8_t *) &session->video.gcm_iv_counter, sizeof(session->video.gcm_iv_counter), std::begin(iv)); iv[11] = 'V'; // Video stream session->video.gcm_iv_counter++; // Encrypt the target buffer in place auto *prefix = (video_packet_enc_prefix_t *) shards.prefix(x); - prefix->unused = 0; + prefix->frameNumber = packet->frame_index(); std::copy(std::begin(iv), std::end(iv), prefix->iv); session->video.cipher->encrypt(std::string_view { (char *) inspect, (size_t) blocksize }, prefix->tag, &iv); } @@ -1486,6 +1500,7 @@ namespace stream { audio_packet_t audio_packet { (audio_packet_raw_t *) malloc(sizeof(audio_packet_raw_t) + max_block_size) }; fec::rs_t rs { reed_solomon_new(RTPA_DATA_SHARDS, RTPA_FEC_SHARDS) }; + crypto::aes_t iv(16); // For unknown reasons, the RS parity matrix computed by our RS implementation // doesn't match the one Nvidia uses for audio data. I'm not exactly sure why, @@ -1513,11 +1528,9 @@ namespace stream { auto sequenceNumber = session->audio.sequenceNumber; auto timestamp = session->audio.timestamp; - // This will be mapped to big-endianness later - // For now, encode_audio needs it to be the proper sequenceNumber - audio_packet->rtp.sequenceNumber = sequenceNumber; + *(std::uint32_t *) iv.data() = util::endian::big(session->audio.avRiKeyId + sequenceNumber); - auto bytes = encode_audio(session->config.encryptionFlagsEnabled & SS_ENC_AUDIO, packet_data, audio_packet, session->audio.avRiKeyId, session->audio.cipher); + auto bytes = encode_audio(session->config.encryptionFlagsEnabled & SS_ENC_AUDIO, packet_data, audio_packet, iv, session->audio.cipher); if (bytes < 0) { BOOST_LOG(error) << "Couldn't encode audio packet"sv; break; @@ -1587,9 +1600,9 @@ namespace stream { start_broadcast(broadcast_ctx_t &ctx) { auto address_family = net::af_from_enum_string(config::sunshine.address_family); auto protocol = address_family == net::IPV4 ? udp::v4() : udp::v6(); - auto control_port = map_port(CONTROL_PORT); - auto video_port = map_port(VIDEO_STREAM_PORT); - auto audio_port = map_port(AUDIO_STREAM_PORT); + auto control_port = net::map_port(CONTROL_PORT); + auto video_port = net::map_port(VIDEO_STREAM_PORT); + auto audio_port = net::map_port(AUDIO_STREAM_PORT); if (ctx.control_server.bind(address_family, control_port)) { BOOST_LOG(error) << "Couldn't bind Control server to port ["sv << control_port << "], likely another process already bound to the port"sv; @@ -1742,12 +1755,10 @@ namespace stream { return; } - // Enable QoS tagging on video traffic if requested by the client - if (session->config.videoQosType) { - auto address = session->video.peer.address(); - session->video.qos = platf::enable_socket_qos(ref->video_sock.native_handle(), address, - session->video.peer.port(), platf::qos_data_type_e::video); - } + // Enable local prioritization and QoS tagging on video traffic if requested by the client + auto address = session->video.peer.address(); + session->video.qos = platf::enable_socket_qos(ref->video_sock.native_handle(), address, + session->video.peer.port(), platf::qos_data_type_e::video, session->config.videoQosType != 0); BOOST_LOG(debug) << "Start capturing Video"sv; video::capture(session->mail, session->config.monitor, session); @@ -1767,12 +1778,10 @@ namespace stream { return; } - // Enable QoS tagging on audio traffic if requested by the client - if (session->config.audioQosType) { - auto address = session->audio.peer.address(); - session->audio.qos = platf::enable_socket_qos(ref->audio_sock.native_handle(), address, - session->audio.peer.port(), platf::qos_data_type_e::audio); - } + // Enable local prioritization and QoS tagging on audio traffic if requested by the client + auto address = session->audio.peer.address(); + session->audio.qos = platf::enable_socket_qos(ref->audio_sock.native_handle(), address, + session->audio.peer.port(), platf::qos_data_type_e::audio, session->config.audioQosType != 0); BOOST_LOG(debug) << "Start capturing Audio"sv; audio::capture(session->mail, session->config.audio, session); @@ -1887,6 +1896,7 @@ namespace stream { auto mail = std::make_shared(); session->shutdown_event = mail->event(mail::shutdown); + session->launch_session_id = launch_session.id; session->config = config; diff --git a/src/system_tray.cpp b/src/system_tray.cpp index 95a492dc297..621ace0cfb9 100644 --- a/src/system_tray.cpp +++ b/src/system_tray.cpp @@ -37,6 +37,7 @@ // local includes #include "confighttp.h" + #include "logging.h" #include "main.h" #include "platform/common.h" #include "process.h" @@ -291,7 +292,7 @@ namespace system_tray { tray.icon = TRAY_ICON_PLAYING; tray.notification_title = "Stream Started"; char msg[256]; - sprintf(msg, "Streaming started for %s", app_name.c_str()); + snprintf(msg, std::size(msg), "Streaming started for %s", app_name.c_str()); tray.notification_text = msg; tray.tooltip = msg; tray.notification_icon = TRAY_ICON_PLAYING; @@ -311,7 +312,7 @@ namespace system_tray { tray.icon = TRAY_ICON_PAUSING; tray_update(&tray); char msg[256]; - sprintf(msg, "Streaming paused for %s", app_name.c_str()); + snprintf(msg, std::size(msg), "Streaming paused for %s", app_name.c_str()); tray.icon = TRAY_ICON_PAUSING; tray.notification_title = "Stream Paused"; tray.notification_text = msg; @@ -333,7 +334,7 @@ namespace system_tray { tray.icon = TRAY_ICON; tray_update(&tray); char msg[256]; - sprintf(msg, "Application %s successfully stopped", app_name.c_str()); + snprintf(msg, std::size(msg), "Application %s successfully stopped", app_name.c_str()); tray.icon = TRAY_ICON; tray.notification_icon = TRAY_ICON; tray.notification_title = "Application Stopped"; diff --git a/src/thread_safe.h b/src/thread_safe.h index f4713bcf014..1135c441d74 100644 --- a/src/thread_safe.h +++ b/src/thread_safe.h @@ -82,7 +82,7 @@ namespace safe { } // pop and view should not be used interchangeably - const status_t & + status_t view() { std::unique_lock ul { _lock }; diff --git a/src/upnp.cpp b/src/upnp.cpp index 7a296a8b358..55c49aaf67c 100644 --- a/src/upnp.cpp +++ b/src/upnp.cpp @@ -7,6 +7,7 @@ #include "config.h" #include "confighttp.h" +#include "logging.h" #include "main.h" #include "network.h" #include "nvhttp.h" @@ -61,13 +62,13 @@ namespace upnp { class deinit_t: public platf::deinit_t { public: deinit_t() { - auto rtsp = std::to_string(::map_port(rtsp_stream::RTSP_SETUP_PORT)); - auto video = std::to_string(::map_port(stream::VIDEO_STREAM_PORT)); - auto audio = std::to_string(::map_port(stream::AUDIO_STREAM_PORT)); - auto control = std::to_string(::map_port(stream::CONTROL_PORT)); - auto gs_http = std::to_string(::map_port(nvhttp::PORT_HTTP)); - auto gs_https = std::to_string(::map_port(nvhttp::PORT_HTTPS)); - auto wm_http = std::to_string(::map_port(confighttp::PORT_HTTPS)); + auto rtsp = std::to_string(net::map_port(rtsp_stream::RTSP_SETUP_PORT)); + auto video = std::to_string(net::map_port(stream::VIDEO_STREAM_PORT)); + auto audio = std::to_string(net::map_port(stream::AUDIO_STREAM_PORT)); + auto control = std::to_string(net::map_port(stream::CONTROL_PORT)); + auto gs_http = std::to_string(net::map_port(nvhttp::PORT_HTTP)); + auto gs_https = std::to_string(net::map_port(nvhttp::PORT_HTTPS)); + auto wm_http = std::to_string(net::map_port(confighttp::PORT_HTTPS)); mappings.assign({ { { rtsp, rtsp, "TCP"s }, "Sunshine - RTSP"s }, @@ -178,7 +179,7 @@ namespace upnp { * @return `true` on success. */ bool - map_port(const IGDdatas &data, const urls_t &urls, const std::string &lan_addr, const mapping_t &mapping) { + map_upnp_port(const IGDdatas &data, const urls_t &urls, const std::string &lan_addr, const mapping_t &mapping) { char intClient[16]; char intPort[6]; char desc[80]; @@ -283,7 +284,7 @@ namespace upnp { * @param data urls_t from UPNP_GetValidIGD() */ void - unmap_all_ports(const urls_t &urls, const IGDdatas &data) { + unmap_all_upnp_ports(const urls_t &urls, const IGDdatas &data) { for (auto it = std::begin(mappings); it != std::end(mappings); ++it) { auto status = UPNP_DeletePortMapping( urls->controlURL, @@ -342,7 +343,7 @@ namespace upnp { BOOST_LOG(debug) << "Found valid IGD device: "sv << urls->rootdescURL; for (auto it = std::begin(mappings); it != std::end(mappings) && !shutdown_event->peek(); ++it) { - map_port(data, urls, lan_addr_str, *it); + map_upnp_port(data, urls, lan_addr_str, *it); } if (!mapped) { @@ -364,7 +365,7 @@ namespace upnp { if (mapped) { // Unmap ports upon termination BOOST_LOG(info) << "Unmapping UPNP ports..."sv; - unmap_all_ports(mapped_urls, data); + unmap_all_upnp_ports(mapped_urls, data); } } diff --git a/src/video.cpp b/src/video.cpp index 68402d29d81..e013249880b 100644 --- a/src/video.cpp +++ b/src/video.cpp @@ -10,13 +10,17 @@ #include extern "C" { +#include #include +#include +#include #include } #include "cbs.h" #include "config.h" #include "input.h" +#include "logging.h" #include "main.h" #include "nvenc/nvenc_base.h" #include "platform/common.h" @@ -101,30 +105,37 @@ namespace video { public: int convert(platf::img_t &img) override { - av_frame_make_writable(sw_frame.get()); - - const int linesizes[2] { - img.row_pitch, 0 - }; - - std::uint8_t *data[4]; - - data[0] = sw_frame->data[0] + offsetY; - if (sw_frame->format == AV_PIX_FMT_NV12) { - data[1] = sw_frame->data[1] + offsetUV * 2; - data[2] = nullptr; - } - else { - data[1] = sw_frame->data[1] + offsetUV; - data[2] = sw_frame->data[2] + offsetUV; - data[3] = nullptr; + // If we need to add aspect ratio padding, we need to scale into an intermediate output buffer + bool requires_padding = (sw_frame->width != sws_output_frame->width || sw_frame->height != sws_output_frame->height); + + // Setup the input frame using the caller's img_t + sws_input_frame->data[0] = img.data; + sws_input_frame->linesize[0] = img.row_pitch; + + // Perform color conversion and scaling to the final size + auto status = sws_scale_frame(sws.get(), requires_padding ? sws_output_frame.get() : sw_frame.get(), sws_input_frame.get()); + if (status < 0) { + char string[AV_ERROR_MAX_STRING_SIZE]; + BOOST_LOG(error) << "Couldn't scale frame: "sv << av_make_error_string(string, AV_ERROR_MAX_STRING_SIZE, status); + return -1; } - int ret = sws_scale(sws.get(), (std::uint8_t *const *) &img.data, linesizes, 0, img.height, data, sw_frame->linesize); - if (ret <= 0) { - BOOST_LOG(error) << "Couldn't convert image to required format and/or size"sv; - - return -1; + // If we require aspect ratio padding, copy the output frame into the final padded frame + if (requires_padding) { + auto fmt_desc = av_pix_fmt_desc_get((AVPixelFormat) sws_output_frame->format); + auto planes = av_pix_fmt_count_planes((AVPixelFormat) sws_output_frame->format); + for (int plane = 0; plane < planes; plane++) { + auto shift_h = plane == 0 ? 0 : fmt_desc->log2_chroma_h; + auto shift_w = plane == 0 ? 0 : fmt_desc->log2_chroma_w; + auto offset = ((offsetW >> shift_w) * fmt_desc->comp[plane].step) + (offsetH >> shift_h) * sw_frame->linesize[plane]; + + // Copy line-by-line to preserve leading padding for each row + for (int line = 0; line < sws_output_frame->height >> shift_h; line++) { + memcpy(sw_frame->data[plane] + offset + (line * sw_frame->linesize[plane]), + sws_output_frame->data[plane] + (line * sws_output_frame->linesize[plane]), + (size_t) (sws_output_frame->width >> shift_w) * fmt_desc->comp[plane].step); + } + } } // If frame is not a software frame, it means we still need to transfer from main memory @@ -170,43 +181,13 @@ namespace video { /** * When preserving aspect ratio, ensure that padding is black */ - int + void prefill() { auto frame = sw_frame ? sw_frame.get() : this->frame; - auto width = frame->width; - auto height = frame->height; - av_frame_get_buffer(frame, 0); - sws_t sws { - sws_getContext( - width, height, AV_PIX_FMT_BGR0, - width, height, (AVPixelFormat) frame->format, - SWS_LANCZOS | SWS_ACCURATE_RND, - nullptr, nullptr, nullptr) - }; - - if (!sws) { - return -1; - } - - util::buffer_t img { (std::size_t)(width * height) }; - std::fill(std::begin(img), std::end(img), 0); - - const int linesizes[2] { - width, 0 - }; - av_frame_make_writable(frame); - - auto data = img.begin(); - int ret = sws_scale(sws.get(), (std::uint8_t *const *) &data, linesizes, 0, height, frame->data, frame->linesize); - if (ret <= 0) { - BOOST_LOG(error) << "Couldn't convert image to required format and/or size"sv; - - return -1; - } - - return 0; + ptrdiff_t linesize[4] = { frame->linesize[0], frame->linesize[1], frame->linesize[2], frame->linesize[3] }; + av_image_fill_black(frame->data, linesize, (AVPixelFormat) frame->format, frame->color_range, frame->width, frame->height); } int @@ -223,9 +204,8 @@ namespace video { this->frame = frame; } - if (prefill()) { - return -1; - } + // Fill aspect ratio padding in the destination frame + prefill(); auto out_width = frame->width; auto out_height = frame->height; @@ -235,35 +215,69 @@ namespace video { out_width = in_width * scalar; out_height = in_height * scalar; - // result is always positive - auto offsetW = (frame->width - out_width) / 2; - auto offsetH = (frame->height - out_height) / 2; - offsetUV = (offsetW + offsetH * frame->width / 2) / 2; - offsetY = offsetW + offsetH * frame->width; + sws_input_frame.reset(av_frame_alloc()); + sws_input_frame->width = in_width; + sws_input_frame->height = in_height; + sws_input_frame->format = AV_PIX_FMT_BGR0; - sws.reset(sws_getContext( - in_width, in_height, AV_PIX_FMT_BGR0, - out_width, out_height, format, - SWS_LANCZOS | SWS_ACCURATE_RND, - nullptr, nullptr, nullptr)); + sws_output_frame.reset(av_frame_alloc()); + sws_output_frame->width = out_width; + sws_output_frame->height = out_height; + sws_output_frame->format = format; - return sws ? 0 : -1; + // Result is always positive + offsetW = (frame->width - out_width) / 2; + offsetH = (frame->height - out_height) / 2; + + sws.reset(sws_alloc_context()); + if (!sws) { + return -1; + } + + AVDictionary *options { nullptr }; + av_dict_set_int(&options, "srcw", sws_input_frame->width, 0); + av_dict_set_int(&options, "srch", sws_input_frame->height, 0); + av_dict_set_int(&options, "src_format", sws_input_frame->format, 0); + av_dict_set_int(&options, "dstw", sws_output_frame->width, 0); + av_dict_set_int(&options, "dsth", sws_output_frame->height, 0); + av_dict_set_int(&options, "dst_format", sws_output_frame->format, 0); + av_dict_set_int(&options, "sws_flags", SWS_LANCZOS | SWS_ACCURATE_RND, 0); + av_dict_set_int(&options, "threads", config::video.min_threads, 0); + + auto status = av_opt_set_dict(sws.get(), &options); + av_dict_free(&options); + if (status < 0) { + char string[AV_ERROR_MAX_STRING_SIZE]; + BOOST_LOG(error) << "Failed to set SWS options: "sv << av_make_error_string(string, AV_ERROR_MAX_STRING_SIZE, status); + return -1; + } + + status = sws_init_context(sws.get(), nullptr, nullptr); + if (status < 0) { + char string[AV_ERROR_MAX_STRING_SIZE]; + BOOST_LOG(error) << "Failed to initialize SWS: "sv << av_make_error_string(string, AV_ERROR_MAX_STRING_SIZE, status); + return -1; + } + + return 0; } // Store ownership when frame is hw_frame avcodec_frame_t hw_frame; avcodec_frame_t sw_frame; + avcodec_frame_t sws_input_frame; + avcodec_frame_t sws_output_frame; sws_t sws; - // offset of input image to output frame in pixels - int offsetUV; - int offsetY; + // Offset of input image to output frame in pixels + int offsetW; + int offsetH; }; enum flag_e : uint32_t { DEFAULT = 0, - PARALLEL_ENCODING = 1 << 1, + PARALLEL_ENCODING = 1 << 1, // Capture and encoding can run concurrently on separate threads H264_ONLY = 1 << 2, // When HEVC is too heavy LIMITED_GOP_SIZE = 1 << 3, // Some encoders don't like it when you have an infinite GOP_SIZE. *cough* VAAPI *cough* SINGLE_SLICE_ONLY = 1 << 4, // Never use multiple slices <-- Older intel iGPU's ruin it for everyone else :P @@ -271,6 +285,7 @@ namespace video { RELAXED_COMPLIANCE = 1 << 6, // Use FF_COMPLIANCE_UNOFFICIAL compliance mode NO_RC_BUF_LIMIT = 1 << 7, // Don't set rc_buffer_size REF_FRAMES_INVALIDATION = 1 << 8, // Support reference frames invalidation + ALWAYS_REPROBE = 1 << 9, // This is an encoder of last resort and we want to aggressively probe for a better one }; struct encoder_platform_formats_t { @@ -365,6 +380,10 @@ namespace video { std::vector sdr_options; std::vector hdr_options; std::vector fallback_options; + + // QP option to set in the case that CBR/VBR is not supported + // by the encoder. If CBR/VBR is guaranteed to be supported, + // don't specify this option to avoid wasteful encoder probing. std::optional qp; std::string name; @@ -581,7 +600,7 @@ namespace video { {}, // Fallback options {}, - std::nullopt, // QP + std::nullopt, // QP rate control fallback "av1_nvenc"s, }, { @@ -593,7 +612,7 @@ namespace video { {}, // Fallback options {}, - std::nullopt, // QP + std::nullopt, // QP rate control fallback "hevc_nvenc"s, }, { @@ -605,7 +624,7 @@ namespace video { {}, // Fallback options {}, - std::nullopt, // QP + std::nullopt, // QP rate control fallback "h264_nvenc"s, }, PARALLEL_ENCODING | REF_FRAMES_INVALIDATION // flags @@ -638,6 +657,7 @@ namespace video { { "tune"s, NV_ENC_TUNING_INFO_ULTRA_LOW_LATENCY }, { "rc"s, NV_ENC_PARAMS_RC_CBR }, { "multipass"s, &config::video.nv_legacy.multipass }, + { "aq"s, &config::video.nv_legacy.aq }, }, // SDR-specific options {}, @@ -645,7 +665,7 @@ namespace video { {}, // Fallback options {}, - std::nullopt, + std::nullopt, // QP rate control fallback "av1_nvenc"s, }, { @@ -658,6 +678,7 @@ namespace video { { "tune"s, NV_ENC_TUNING_INFO_ULTRA_LOW_LATENCY }, { "rc"s, NV_ENC_PARAMS_RC_CBR }, { "multipass"s, &config::video.nv_legacy.multipass }, + { "aq"s, &config::video.nv_legacy.aq }, }, // SDR-specific options { @@ -668,7 +689,7 @@ namespace video { { "profile"s, (int) nv::profile_hevc_e::main_10 }, }, {}, // Fallback options - std::nullopt, + std::nullopt, // QP rate control fallback "hevc_nvenc"s, }, { @@ -681,6 +702,7 @@ namespace video { { "rc"s, NV_ENC_PARAMS_RC_CBR }, { "coder"s, &config::video.nv_legacy.h264_coder }, { "multipass"s, &config::video.nv_legacy.multipass }, + { "aq"s, &config::video.nv_legacy.aq }, }, // SDR-specific options { @@ -688,7 +710,7 @@ namespace video { }, {}, // HDR-specific options {}, // Fallback options - std::make_optional({ "qp"s, &config::video.qp }), + std::nullopt, // QP rate control fallback "h264_nvenc"s, }, PARALLEL_ENCODING @@ -718,7 +740,7 @@ namespace video { {}, // Fallback options {}, - std::make_optional({ "qp"s, &config::video.qp }), + std::nullopt, // QP rate control fallback "av1_qsv"s, }, { @@ -742,7 +764,7 @@ namespace video { }, // Fallback options {}, - std::make_optional({ "qp"s, &config::video.qp }), + std::nullopt, // QP rate control fallback "hevc_qsv"s, }, { @@ -769,7 +791,7 @@ namespace video { { { "low_power"s, 0 }, // Some old/low-end Intel GPUs don't support low power encoding }, - std::make_optional({ "qp"s, &config::video.qp }), + std::nullopt, // QP rate control fallback "h264_qsv"s, }, PARALLEL_ENCODING | CBR_WITH_VBR | RELAXED_COMPLIANCE | NO_RC_BUF_LIMIT @@ -795,7 +817,7 @@ namespace video { {}, // SDR-specific options {}, // HDR-specific options {}, // Fallback options - std::make_optional({ "qp_p"s, &config::video.qp }), + std::nullopt, // QP rate control fallback "av1_amf"s, }, { @@ -816,7 +838,7 @@ namespace video { {}, // SDR-specific options {}, // HDR-specific options {}, // Fallback options - std::make_optional({ "qp_p"s, &config::video.qp }), + std::nullopt, // QP rate control fallback "hevc_amf"s, }, { @@ -840,7 +862,7 @@ namespace video { { { "usage"s, 2 /* AMF_VIDEO_ENCODER_USAGE_LOW_LATENCY */ }, // Workaround for https://github.com/GPUOpen-LibrariesAndSDKs/AMF/issues/410 }, - std::make_optional({ "qp_p"s, &config::video.qp }), + std::nullopt, // QP rate control fallback "h264_amf"s, }, PARALLEL_ENCODING @@ -866,7 +888,9 @@ namespace video { {}, // SDR-specific options {}, // HDR-specific options {}, // Fallback options - std::make_optional("qp"s, &config::video.qp), + + // QP rate control fallback + std::nullopt, #ifdef ENABLE_BROKEN_AV1_ENCODER // Due to bugs preventing on-demand IDR frames from working and very poor @@ -891,7 +915,7 @@ namespace video { {}, // SDR-specific options {}, // HDR-specific options {}, // Fallback options - std::make_optional("qp"s, &config::video.qp), + std::nullopt, // QP rate control fallback "libx265"s, }, { @@ -903,10 +927,10 @@ namespace video { {}, // SDR-specific options {}, // HDR-specific options {}, // Fallback options - std::make_optional("qp"s, &config::video.qp), + std::nullopt, // QP rate control fallback "libx264"s, }, - H264_ONLY | PARALLEL_ENCODING + H264_ONLY | PARALLEL_ENCODING | ALWAYS_REPROBE }; #ifdef __linux__ @@ -1698,6 +1722,12 @@ namespace video { } else { ctx->rc_buffer_size = bitrate / config.framerate; + +#ifndef __APPLE__ + if (encoder.name == "nvenc" && config::video.nv_legacy.vbv_percentage_increase > 0) { + ctx->rc_buffer_size += ctx->rc_buffer_size * config::video.nv_legacy.vbv_percentage_increase / 100; + } +#endif } } } @@ -2324,12 +2354,7 @@ namespace video { }; int - validate_config(std::shared_ptr &disp, const encoder_t &encoder, const config_t &config) { - reset_display(disp, encoder.platform_formats->dev_type, config::video.output_name, config); - if (!disp) { - return -1; - } - + validate_config(std::shared_ptr disp, const encoder_t &encoder, const config_t &config) { auto encode_device = make_encode_device(*disp, encoder, config); if (!encode_device) { return -1; @@ -2450,7 +2475,13 @@ namespace video { if (disp->is_codec_supported(encoder.hevc.name, config_autoselect)) { retry_hevc: auto max_ref_frames_hevc = validate_config(disp, encoder, config_max_ref_frames); - auto autoselect_hevc = max_ref_frames_hevc >= 0 ? max_ref_frames_hevc : validate_config(disp, encoder, config_autoselect); + + // If H.264 succeeded with max ref frames specified, assume that we can count on + // HEVC to also succeed with max ref frames specified if HEVC is supported. + auto autoselect_hevc = (max_ref_frames_hevc >= 0 || max_ref_frames_h264 >= 0) ? + max_ref_frames_hevc : + validate_config(disp, encoder, config_autoselect); + if (autoselect_hevc < 0 && encoder.hevc.qp && encoder.hevc[encoder_t::CBR]) { // It's possible the encoder isn't accepting Constant Bit Rate. Turn off CBR and make another attempt encoder.hevc.capabilities.set(); @@ -2482,7 +2513,13 @@ namespace video { if (disp->is_codec_supported(encoder.av1.name, config_autoselect)) { retry_av1: auto max_ref_frames_av1 = validate_config(disp, encoder, config_max_ref_frames); - auto autoselect_av1 = max_ref_frames_av1 >= 0 ? max_ref_frames_av1 : validate_config(disp, encoder, config_autoselect); + + // If H.264 succeeded with max ref frames specified, assume that we can count on + // AV1 to also succeed with max ref frames specified if AV1 is supported. + auto autoselect_av1 = (max_ref_frames_av1 >= 0 || max_ref_frames_h264 >= 0) ? + max_ref_frames_av1 : + validate_config(disp, encoder, config_autoselect); + if (autoselect_av1 < 0 && encoder.av1.qp && encoder.av1[encoder_t::CBR]) { // It's possible the encoder isn't accepting Constant Bit Rate. Turn off CBR and make another attempt encoder.av1.capabilities.set(); @@ -2520,6 +2557,12 @@ namespace video { hevc.videoFormat = 1; av1.videoFormat = 2; + // Reset the display since we're switching from SDR to HDR + reset_display(disp, encoder.platform_formats->dev_type, config::video.output_name, config); + if (!disp) { + return false; + } + // HDR is not supported with H.264. Don't bother even trying it. encoder.h264[flag] = flag != encoder_t::DYNAMIC_RANGE && validate_config(disp, encoder, h264) >= 0; @@ -2557,6 +2600,11 @@ namespace video { probe_encoders() { auto encoder_list = encoders; + // If we already have a good encoder, check to see if another probe is required + if (chosen_encoder && !(chosen_encoder->flags & ALWAYS_REPROBE) && !platf::needs_encoder_reenumeration()) { + return 0; + } + // Restart encoder selection auto previous_encoder = chosen_encoder; chosen_encoder = nullptr; diff --git a/src/video_colorspace.cpp b/src/video_colorspace.cpp index ca5e489bb4b..4f5955eed7e 100644 --- a/src/video_colorspace.cpp +++ b/src/video_colorspace.cpp @@ -1,6 +1,6 @@ #include "video_colorspace.h" -#include "main.h" +#include "logging.h" #include "video.h" extern "C" { diff --git a/src_assets/common/assets/web/config.html b/src_assets/common/assets/web/config.html index 71e01ae2e0c..e9d866dde51 100644 --- a/src_assets/common/assets/web/config.html +++ b/src_assets/common/assets/web/config.html @@ -761,8 +761,8 @@

- - + +
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
@@ -889,6 +889,35 @@

+ +
+ + +
+ Assign higher QP values to flat regions of the video.
+ Recommended to enable when streaming at lower bitrates. +
+
+ + +
+ + +
+ By default sunshine uses single-frame + VBV/HRD, + which means any encoded video frame size is not expected to exceed requested bitrate divided by requested frame + rate.
+ Relaxing this restriction can be beneficial and act as low-latency variable bitrate, + but may also lead to packet loss if the network doesn't have buffer headroom to handle bitrate spikes.
+ Maximum accepted value is 400, which corresponds to 5x increased encoded video frame upper size limit. +
+
+
@@ -918,6 +947,36 @@

+ +
+ + +
+ Adaptive P-State algorithm which NVIDIA drivers employ doesn't work well with low latency streaming, so + sunshine requests high power mode explicitly.
+ Disabling it is not recommended since this can lead to + significantly increased encoding latency. +
+
+ + +
+ + +
+ Sunshine can't capture fullscreen OpenGL and Vulkan programs at full frame rate unless they present on top + of DXGI.
+ This is system-wide setting that is reverted on sunshine program exit. +
+
+
@@ -1207,7 +1266,7 @@

"channels": 1, "fec_percentage": 20, "qp": 28, - "min_threads": 1, + "min_threads": 2, "hevc_mode": 0, "av1_mode": 0, "capture": "", @@ -1220,7 +1279,11 @@

options: { "nvenc_preset": 1, "nvenc_twopass": "quarter_res", + "nvenc_spatial_aq": "disabled", + "nvenc_vbv_increase": 0, "nvenc_realtime_hags": "enabled", + "nvenc_latency_over_power": "enabled", + "nvenc_opengl_vulkan_on_dxgi": "enabled", "nvenc_h264_cavlc": "disabled", }, }, diff --git a/src_assets/windows/assets/apps.json b/src_assets/windows/assets/apps.json index 72a56a7e01c..30b6ada06dd 100644 --- a/src_assets/windows/assets/apps.json +++ b/src_assets/windows/assets/apps.json @@ -1,7 +1,5 @@ { - "env": { - "PATH": "$(PATH);$(ProgramFiles(x86))\\Steam" - }, + "env": {}, "apps": [ { "name": "Desktop", @@ -9,9 +7,9 @@ }, { "name": "Steam Big Picture", - "detached": [ - "steam steam://open/bigpicture" - ], + "cmd": "steam://open/bigpicture", + "auto-detach": "true", + "wait-all": "true", "image-path": "steam.png" } ] diff --git a/third-party/moonlight-common-c b/third-party/moonlight-common-c index 298f356acbb..cbd0ec1b25e 160000 --- a/third-party/moonlight-common-c +++ b/third-party/moonlight-common-c @@ -1 +1 @@ -Subproject commit 298f356acbb57f56863680d41c0d307a2fd5cb91 +Subproject commit cbd0ec1b25edfb8ee8645fffa49ff95b6e04c70e diff --git a/third-party/nlohmann_json b/third-party/nlohmann_json new file mode 160000 index 00000000000..9cca280a4d0 --- /dev/null +++ b/third-party/nlohmann_json @@ -0,0 +1 @@ +Subproject commit 9cca280a4d0ccf0c08f47a99aa71d1b0e52f8d03 diff --git a/tools/CMakeLists.txt b/tools/CMakeLists.txt index e24be6e102c..b7767bc6637 100644 --- a/tools/CMakeLists.txt +++ b/tools/CMakeLists.txt @@ -2,7 +2,7 @@ cmake_minimum_required(VERSION 3.0) project(sunshine_tools) -include_directories(${CMAKE_SOURCE_DIR}) +include_directories("${CMAKE_SOURCE_DIR}") add_executable(dxgi-info dxgi.cpp) set_target_properties(dxgi-info PROPERTIES CXX_STANDARD 17) @@ -36,4 +36,3 @@ target_link_libraries(ddprobe d3d11 ${PLATFORM_LIBRARIES}) target_compile_options(ddprobe PRIVATE ${SUNSHINE_COMPILE_OPTIONS}) -