diff --git a/.clang-format b/.clang-format
index 97bdef1e1ef..e72a1e27d11 100644
--- a/.clang-format
+++ b/.clang-format
@@ -52,7 +52,7 @@ NamespaceIndentation: All
ObjCSpaceAfterProperty: true
ObjCSpaceBeforeProtocolList: true
PointerAlignment: Right
-ReflowComments: false
+ReflowComments: true
SpaceAfterCStyleCast: true
SpaceAfterLogicalNot: false
SpaceAfterTemplateKeyword: true
diff --git a/.github/ISSUE_TEMPLATE/bug-report.yml b/.github/ISSUE_TEMPLATE/bug-report.yml
index d25c409123f..d701774ed37 100644
--- a/.github/ISSUE_TEMPLATE/bug-report.yml
+++ b/.github/ISSUE_TEMPLATE/bug-report.yml
@@ -102,7 +102,6 @@ body:
- Linux - solus (Third Party)
- macOS - dmg
- macOS - Portfile
- - macOS - pkg
- Windows - Chocolatey (Third Party)
- Windows - installer
- Windows - portable
@@ -111,6 +110,7 @@ body:
- other (not listed)
- other (self built)
- other (fork of this repo)
+ - n/a
validations:
required: true
- type: dropdown
@@ -123,6 +123,7 @@ body:
- Intel
- Nvidia
- none (software encoding)
+ - n/a
validations:
required: true
- type: input
diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml
index 234fb791d39..a2d7f99fb44 100644
--- a/.github/workflows/CI.yml
+++ b/.github/workflows/CI.yml
@@ -474,7 +474,7 @@ jobs:
build_mac:
name: MacOS
runs-on: macos-11
- needs: setup_release
+ needs: [check_changelog, setup_release]
steps:
- name: Checkout
@@ -514,13 +514,10 @@ jobs:
# package
cpack -G DragNDrop
- mv ./cpack_artifacts/Sunshine.dmg ../artifacts/sunshine-macos-experimental-dragndrop.dmg
-
- cpack -G Bundle
- mv ./cpack_artifacts/Sunshine.dmg ../artifacts/sunshine-macos-experimental-bundle.dmg
+ mv ./cpack_artifacts/Sunshine.dmg ../artifacts/sunshine.dmg
- cpack -G ZIP
- mv ./cpack_artifacts/Sunshine.zip ../artifacts/sunshine-macos-experimental-archive.zip
+ # cpack -G Bundle
+ # mv ./cpack_artifacts/Sunshine.dmg ../artifacts/sunshine-bundle.dmg
- name: Upload Artifacts
uses: actions/upload-artifact@v3
@@ -528,33 +525,23 @@ jobs:
name: sunshine-macos
path: artifacts/
- # this step can be removed after packages are fixed
- - name: Delete experimental packages
- if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/master' }}
- working-directory: artifacts
- run: |
- rm -f ./sunshine-macos-experimental-dragndrop.dmg
- rm -f ./sunshine-macos-experimental-bundle.dmg
- rm -f ./sunshine-macos-experimental-archive.zip
-
-# # no artifacts to release currently
-# - name: Create/Update GitHub Release
-# if: ${{ needs.setup_release.outputs.create_release == 'true' }}
-# uses: ncipollo/release-action@v1
-# with:
-# name: ${{ needs.setup_release.outputs.release_name }}
-# tag: ${{ needs.setup_release.outputs.release_tag }}
-# commit: ${{ needs.setup_release.outputs.release_commit }}
-# artifacts: "*artifacts/*"
-# token: ${{ secrets.GH_BOT_TOKEN }}
-# allowUpdates: true
-# body: ${{ needs.setup_release.outputs.release_body }}
-# discussionCategory: announcements
-# prerelease: ${{ needs.setup_release.outputs.pre_release }}
+ - name: Create/Update GitHub Release
+ if: ${{ needs.setup_release.outputs.create_release == 'true' }}
+ uses: ncipollo/release-action@v1
+ with:
+ name: ${{ needs.setup_release.outputs.release_name }}
+ tag: ${{ needs.setup_release.outputs.release_tag }}
+ commit: ${{ needs.setup_release.outputs.release_commit }}
+ artifacts: "*artifacts/*"
+ token: ${{ secrets.GH_BOT_TOKEN }}
+ allowUpdates: true
+ body: ${{ needs.setup_release.outputs.release_body }}
+ discussionCategory: announcements
+ prerelease: ${{ needs.setup_release.outputs.pre_release }}
build_mac_port:
name: Macports
- needs: setup_release
+ needs: [check_changelog, setup_release]
runs-on: macos-11
steps:
@@ -728,25 +715,6 @@ jobs:
done
exit "$fail"
- - name: Package
- run: |
- # create packages
- sudo port pkg sunshine
- sudo port dmg sunshine
-
- work=$(port work sunshine)
- echo "Sunshine port work directory: ${work}"
-
- # move components out of port work directory
- sudo mv ${work}/Sunshine*component.pkg /tmp/
-
- # copy artifacts
- sudo mv ${work}/Sunshine*.pkg ./artifacts/sunshine.pkg
- sudo mv ${work}/Sunshine*.dmg ./artifacts/sunshine.dmg
-
- # move components back
- # sudo mv /tmp/Sunshine*component.pkg ${work}/
-
- name: Upload Artifacts
uses: actions/upload-artifact@v3
with:
@@ -770,7 +738,7 @@ jobs:
build_win:
name: Windows
runs-on: windows-2019
- needs: setup_release
+ needs: [check_changelog, setup_release]
steps:
- name: Checkout
@@ -813,7 +781,7 @@ jobs:
run: |
mkdir build
cd build
- cmake -DCMAKE_BUILD_TYPE=Release \
+ cmake -DCMAKE_BUILD_TYPE=RelWithDebInfo \
-DSUNSHINE_ASSETS_DIR=assets \
-G "MinGW Makefiles" \
..
@@ -833,6 +801,15 @@ jobs:
mv ./cpack_artifacts/Sunshine.exe ../artifacts/sunshine-windows-installer.exe
mv ./cpack_artifacts/Sunshine.zip ../artifacts/sunshine-windows-portable.zip
+ - name: Package Windows Debug Info
+ working-directory: build
+ run: |
+ # save the original binaries with debug info
+ 7z -r `
+ "-xr!CMakeFiles" `
+ "-xr!cpack_artifacts" `
+ a "../artifacts/sunshine-debuginfo-win32.zip" "*.exe"
+
- name: Upload Artifacts
uses: actions/upload-artifact@v3
with:
diff --git a/.github/workflows/localize.yml b/.github/workflows/localize.yml
index 2c57c114e76..d2a236c54e5 100644
--- a/.github/workflows/localize.yml
+++ b/.github/workflows/localize.yml
@@ -77,7 +77,7 @@ jobs:
run: echo "date=$(date +'%Y-%m-%d')" >> $GITHUB_OUTPUT
- name: Create/Update Pull Request
- uses: peter-evans/create-pull-request@v4
+ uses: peter-evans/create-pull-request@v5
with:
add-paths: |
locale/*.po
diff --git a/.readthedocs.yaml b/.readthedocs.yaml
index 75ec50a72e3..3a0f243378b 100644
--- a/.readthedocs.yaml
+++ b/.readthedocs.yaml
@@ -8,26 +8,16 @@ version: 2
# Set the version of Python
build:
- os: ubuntu-20.04
+ os: ubuntu-22.04
tools:
- python: "3.10"
-
-## apt packages required packages to run cmake on sunshine, note that additional packages are required
-# apt_packages:
-# - cmake
-# - libboost-filesystem-dev
-# - libboost-log-dev
-# - libboost-thread-dev
-
-## run cmake
-# jobs:
-# pre_build:
-# - cmake .
-
-## Include the submodules, required for cmake
-# submodules:
-# include: all
-# recursive: true
+ python: "3.11"
+ apt_packages:
+ - graphviz
+
+# submodules required for include statements
+submodules:
+ include: all
+ recursive: true
# Build documentation in the docs/ directory with Sphinx
sphinx:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index c074a75e274..419e16025e0 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,85 @@
# Changelog
+## [0.20.0] - 2023-05-28
+**Breaking**
+- (Windows) The Windows installer version of Sunshine is now always launched by the Sunshine Service. Manually launching Sunshine.exe from Program Files is no longer supported. This was necessary to address security issues caused by non-admin users having access to Sunshine's config data. If you have set up Task Scheduler or other mechanisms to launch Sunshine automatically, remove those from your system before updating.
+- (Windows) The Start Menu shortcut has been redesigned for use with the Sunshine Service. It now launches Sunshine in the background (if not already running) and opens the Web UI, avoiding the persistent Command Prompt window present in prior versions. The Start Menu shortcut is now the recommended method for opening the Web UI and launching Sunshine.
+- (Network/UPnP) If the Moonlight Internet Hosting Tool is installed alongside Sunshine, you must remove it or upgrade to v5.6 or later to prevent conflicts with Sunshine's UPnP support. As a reminder, the Moonlight Internet Hosting Tool is not required to stream over the Internet with Sunshine. Instead, simply enable UPnP in the Sunshine Web UI.
+- (Windows) If Steam is installed, the Steam Streaming Speakers driver will be automatically installed when starting a stream for the first time. This behavior can be disabled in the Audio/Video tab of the Web UI. This Steam driver enables support for surround sound and muting host audio without requiring any manual configuration.
+- (Input) The Back Button Timeout option has been renamed to Guide Button Emulation Timeout and has been disabled by default to ensure long presses on the Back button work by default. The previous behavior can be restored by setting the Guide Button Emulation Timeout to 2000.
+- (Windows) The service name of SunshineSvc has been changed to SunshineService to address a false positive in MalwareBytes. If you have any scripts or other logic on your system that is using the service name, you will need to update that for the new name.
+- (Windows) To support new installer features, install-service.bat no longer sets the service to auto-start by default. Users executing install-service.bat manually on the Sunshine portable build must also execute autostart-service.bat to start Sunshine on boot. However, installing the service manually like this is not recommended. Instead, use the Sunshine installer which handles service installation and configuration automatically.
+- (Linux/Fedora) Fedora 36 builds are removed due to upstream end of support
+
+**Added**
+- (Windows) Added an option to launch apps and prep/undo commands as administrator
+- (Installer/Windows) Added an option to choose whether Sunshine launches on boot. If not configured to launch on boot, use the Start Menu shortcut to start Sunshine when desired.
+- (Input/Windows) Added option to send VK codes instead of scancodes for compatibility with iOS/Android devices using non-English keyboard layouts
+- (UI) The Apply/Restart option is now available in the Web UI for all platforms
+- (Systray) Added a Restart option to the system tray context menu
+- (Video/Windows) Added host latency data to video frames. This requires future updates to Moonlight to display in the on-screen overlay.
+- (Audio) Added support for matching Audio Sink and Virtual Sink values on device names
+- (Client) Added friendly error messages for clients when streaming fails to start
+- (Video/Windows) Added warning log messages when Windows is hiding DRM-protected content from display capture
+- (Interop/Windows) Added warning log messages when GeForce Experience is currently using the same ports as Sunshine
+- (Linux/Fedora) Fedora 38 builds are now available
+
+**Changed**
+- (Video) Encoder selection now happens at each stream start for more reliable GPU detection
+- (Video/Windows) The host display now stays on while clients are actively streaming
+- (Audio) Streaming will no longer fail if audio capture is unavailable
+- (Audio/Windows) Sunshine will automatically switch back to the Virtual Sink if the default audio device is changed while streaming
+- (Audio) Changes to the host audio playback option will now take effect when resuming a session from Moonlight
+- (Audio/Windows) Sunshine will switch to a different default audio device if Steam Streaming Speakers are the default when Sunshine starts. This handles cases where Sunshine terminates unexpectedly without restoring the default audio device.
+- (Apps) The Connection Terminated dialog will no longer appear in Moonlight when the app on the host exits normally
+- (Systray/Windows) Quitting Sunshine via the system tray will now stop the Sunshine Service rather than leaving it running and allowing it to restart Sunshine
+- (UI) The 100.64.0.0/10 CGN IP address range is now treated as a LAN address range
+- (Video) Removed a workaround for some versions of Moonlight prior to mid-2022
+- (UI) The PIN field is now cleared after successfully pairing
+- (UI) User names are now treated as case-insensitive
+- (Linux) Changed udev rule to automatically grant access to virtual input devices
+- (UI) Several item descriptions were adjusted to reflect newer configuration recommendations
+- (UI) Placeholder text opacity has been reduced to improve contrast with non-placeholder text
+- (Video/Windows) Minor capture performance improvements
+
+**Fixed**
+- (Video) VRAM usage while streaming is significantly reduced, particularly with higher display resolutions and HDR
+- (Network/UPnP) UPnP support was rewritten to fix several major issues handling router restarts, IP address changes, and port forwarding expiration
+- (Input) Fixed modifier keys from the software keyboard on Android clients
+- (Audio) Fixed handling of default audio device changes while streaming
+- (Windows) Fixed streaming after Microsoft Remote Desktop or Fast User Switching has been used
+- (Input) Fixed some games not recognizing emulated Guide button presses
+- (Video/Windows) Fixed incorrect gamma when using an Advanced Color SDR display on the host
+- (Installer/Windows) The installer is no longer blurry on High DPI systems
+- (Systray/Windows) Fixed multiple system tray icons appearing if Sunshine exits unexpectedly
+- (Systray/Windows) Fixed the system tray icon not appearing in several situations
+- (Windows) Fixed hang on shutdown/restart if mDNS registration fails
+- (UI) Fixed missing response headers
+- (Stability) Fixed several possible crashes in cases where the client did not successfully connect
+- (Stability) Fixed several memory leaks
+- (Input/Windows) Back/Select input now correctly generates the Share button when emulating DS4 controllers
+- (Audio/Windows) Fixed various bugs in audio-info.exe that led to inaccurate output on some systems
+- (Video/Windows) Fixed incorrect resolution values from dxgi-info.exe on High DPI systems
+- (Video/Linux) Fixed poor quality encoding from H.264 on Intel encoders
+- (Config) Fixed a couple of typos in predefined resolutions
+
+**Dependencies**
+- Bump sphinx-copybutton from 0.5.1 to 0.5.2
+- Bump sphinx from 6.13 to 7.0.1
+- Bump third-party/nv-codec-headers from 2055784 to 2cd175b
+- Bump furo from 2023.3.27 to 2023.5.20
+
+**Misc**
+- (Build/Linux) Add X11 to PLATFORM_LIBARIES when found
+- (Build/macOS) Fix compilation with Clang 14
+- (Docs) Fix nvlax URL
+- (Docs) Add diagrams using graphviz
+- (Docs) Improvements to source code documentation
+- (Build) Unpin docker dependencies
+- (Build/Linux) Honor install prefix for tray icon
+- (Build/Windows) Unstripped binaries are now provided as a debuginfo package to support crash dump debugging
+- (Config) Config directories are now created recursively
+
## [0.19.1] - 2023-03-30
**Fixed**
- (Audio) Fixed no audio issue introduced in v0.19.0
@@ -409,3 +489,4 @@ settings. In v0.17.0, games now run under your user account without elevated pri
[0.18.4]: https://github.com/LizardByte/Sunshine/releases/tag/v0.18.4
[0.19.0]: https://github.com/LizardByte/Sunshine/releases/tag/v0.19.0
[0.19.1]: https://github.com/LizardByte/Sunshine/releases/tag/v0.19.1
+[0.20.0]: https://github.com/LizardByte/Sunshine/releases/tag/v0.20.0
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 641b2712b6f..ccca6fcec60 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -2,7 +2,7 @@ cmake_minimum_required(VERSION 3.18)
# `CMAKE_CUDA_ARCHITECTURES` requires 3.18
# todo - set version to 0.0.0 once confident in automated versioning
-project(Sunshine VERSION 0.19.1
+project(Sunshine VERSION 0.20.0
DESCRIPTION "Sunshine is a self-hosted game stream host for Moonlight."
HOMEPAGE_URL "https://app.lizardbyte.dev")
@@ -125,7 +125,7 @@ set(UPNPC_BUILD_TESTS OFF CACHE BOOL "Don't build tests for miniupnpc")
set(UPNPC_BUILD_SAMPLE OFF CACHE BOOL "Don't build samples for miniupnpc")
set(UPNPC_NO_INSTALL ON CACHE BOOL "Don't install any libraries build for miniupnpc")
add_subdirectory(third-party/miniupnp/miniupnpc)
-include_directories(third-party/miniupnp/miniupnpc/include)
+include_directories(SYSTEM third-party/miniupnp/miniupnpc/include)
find_package(Threads REQUIRED)
find_package(OpenSSL REQUIRED)
@@ -134,13 +134,11 @@ pkg_check_modules(CURL REQUIRED libcurl)
if(WIN32)
set(Boost_USE_STATIC_LIBS ON) # cmake-lint: disable=C0103
- # workaround to prevent link errors against icudata, icui18n
- set(Boost_NO_BOOST_CMAKE ON) # cmake-lint: disable=C0103
endif()
find_package(Boost COMPONENTS locale log filesystem program_options REQUIRED)
-list(APPEND SUNSHINE_COMPILE_OPTIONS -Wall -Wno-missing-braces -Wno-maybe-uninitialized -Wno-sign-compare)
+list(APPEND SUNSHINE_COMPILE_OPTIONS -Wall -Wno-sign-compare)
# enable system tray, we will disable this later if we cannot find the required package config on linux
set(SUNSHINE_TRAY 1)
@@ -151,13 +149,13 @@ if(WIN32)
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -static")
add_definitions(-DCURL_STATICLIB)
- include_directories(${CURL_STATIC_INCLUDE_DIRS})
+ include_directories(SYSTEM ${CURL_STATIC_INCLUDE_DIRS})
link_directories(${CURL_STATIC_LIBRARY_DIRS})
add_compile_definitions(SUNSHINE_PLATFORM="windows")
add_subdirectory(tools) # This is temporary, only tools for Windows are needed, for now
- include_directories(third-party/ViGEmClient/include)
+ include_directories(SYSTEM third-party/ViGEmClient/include)
if(NOT DEFINED SUNSHINE_ICON_PATH)
set(SUNSHINE_ICON_PATH "${CMAKE_CURRENT_SOURCE_DIR}/sunshine.ico")
@@ -353,12 +351,13 @@ else()
if(X11_FOUND)
add_compile_definitions(SUNSHINE_BUILD_X11)
- include_directories(${X11_INCLUDE_DIR})
+ include_directories(SYSTEM ${X11_INCLUDE_DIR})
+ list(APPEND PLATFORM_LIBRARIES ${X11_LIBRARIES})
list(APPEND PLATFORM_TARGET_FILES src/platform/linux/x11grab.cpp)
endif()
if(CUDA_FOUND)
- include_directories(third-party/nvfbc)
+ include_directories(SYSTEM third-party/nvfbc)
list(APPEND PLATFORM_TARGET_FILES
src/platform/linux/cuda.cu
src/platform/linux/cuda.cpp
@@ -369,7 +368,7 @@ else()
if(LIBDRM_FOUND AND LIBCAP_FOUND)
add_compile_definitions(SUNSHINE_BUILD_DRM)
- include_directories(${LIBDRM_INCLUDE_DIRS} ${LIBCAP_INCLUDE_DIRS})
+ 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)
list(APPEND SUNSHINE_DEFINITIONS EGL_NO_X11=1)
@@ -415,6 +414,7 @@ ${CMAKE_BINARY_DIR}/generated-src/${filename}.h")
GEN_WAYLAND(wlr-export-dmabuf-unstable-v1)
include_directories(
+ SYSTEM
${WAYLAND_INCLUDE_DIRS}
${CMAKE_BINARY_DIR}/generated-src
)
@@ -435,7 +435,7 @@ ${CMAKE_BINARY_DIR}/generated-src/${filename}.h")
message(WARNING "Couldn't find appindicator, disabling tray icon")
set(SUNSHINE_TRAY 0)
else()
- include_directories(${APPINDICATOR_INCLUDE_DIRS})
+ include_directories(SYSTEM ${APPINDICATOR_INCLUDE_DIRS})
link_directories(${APPINDICATOR_LIBRARY_DIRS})
list(APPEND PLATFORM_TARGET_FILES third-party/tray/tray_linux.c)
@@ -474,6 +474,7 @@ ${CMAKE_BINARY_DIR}/generated-src/${filename}.h")
pulse-simple)
include_directories(
+ SYSTEM
/usr/include/libevdev-1.0
third-party/nv-codec-headers/include
third-party/glad/include)
@@ -535,6 +536,8 @@ set(SUNSHINE_TARGET_FILES
src/thread_safe.h
src/sync.h
src/round_robin.h
+ src/stat_trackers.h
+ src/stat_trackers.cpp
${PLATFORM_TARGET_FILES})
set_source_files_properties(src/upnp.cpp PROPERTIES COMPILE_FLAGS -Wno-pedantic)
@@ -582,8 +585,10 @@ set(FFMPEG_LIBRARIES
${HDR10_PLUS_LIBRARY}
${FFMPEG_PLATFORM_LIBRARIES})
+include_directories(${CMAKE_CURRENT_SOURCE_DIR})
+
include_directories(
- ${CMAKE_CURRENT_SOURCE_DIR}
+ 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
@@ -638,6 +643,8 @@ if(WIN32)
set_target_properties(sunshine PROPERTIES LINK_SEARCH_START_STATIC 1)
set(CMAKE_FIND_LIBRARY_SUFFIXES ".dll")
find_library(ZLIB ZLIB1)
+ list(APPEND SUNSHINE_EXTERNAL_LIBRARIES
+ Wtsapi32.lib)
endif()
target_link_libraries(sunshine ${SUNSHINE_EXTERNAL_LIBRARIES} ${EXTRA_LIBS})
@@ -692,25 +699,31 @@ if(WIN32) # see options at: https://cmake.org/cmake/help/latest/cpack_gen/nsis.h
# Adding tools
install(TARGETS dxgi-info RUNTIME DESTINATION "tools" COMPONENT dxgi)
install(TARGETS audio-info RUNTIME DESTINATION "tools" COMPONENT audio)
- install(TARGETS sunshinesvc RUNTIME DESTINATION "tools" COMPONENT sunshinesvc)
- install(TARGETS elevator RUNTIME DESTINATION "tools" COMPONENT elevator)
# Mandatory tools
install(TARGETS ddprobe RUNTIME DESTINATION "tools" COMPONENT application)
+ install(TARGETS sunshinesvc RUNTIME DESTINATION "tools" COMPONENT application)
+
+ # Mandatory scripts
+ install(DIRECTORY "${SUNSHINE_SOURCE_ASSETS_DIR}/windows/misc/service/"
+ DESTINATION "scripts"
+ COMPONENT assets)
+ install(DIRECTORY "${SUNSHINE_SOURCE_ASSETS_DIR}/windows/misc/migration/"
+ DESTINATION "scripts"
+ COMPONENT assets)
+
+ # Configurable options for the service
+ install(DIRECTORY "${SUNSHINE_SOURCE_ASSETS_DIR}/windows/misc/autostart/"
+ DESTINATION "scripts"
+ COMPONENT autostart)
# scripts
install(DIRECTORY "${SUNSHINE_SOURCE_ASSETS_DIR}/windows/misc/firewall/"
DESTINATION "scripts"
COMPONENT firewall)
- install(DIRECTORY "${SUNSHINE_SOURCE_ASSETS_DIR}/windows/misc/service/"
- DESTINATION "scripts"
- COMPONENT service)
install(DIRECTORY "${SUNSHINE_SOURCE_ASSETS_DIR}/windows/misc/vigembus/"
DESTINATION "scripts"
COMPONENT vigembus)
- install(DIRECTORY "${SUNSHINE_SOURCE_ASSETS_DIR}/windows/misc/migration/"
- DESTINATION "scripts"
- COMPONENT assets)
# Sunshine assets
install(DIRECTORY "${SUNSHINE_SOURCE_ASSETS_DIR}/common/assets/"
@@ -729,7 +742,6 @@ if(WIN32) # see options at: https://cmake.org/cmake/help/latest/cpack_gen/nsis.h
# Extra install commands
# Restores permissions on the install directory
# Migrates config files from the root into the new config folder
- # Sets permissions on the config folder so that we can write in it
# Install service
SET(CPACK_NSIS_EXTRA_INSTALL_COMMANDS
"${CPACK_NSIS_EXTRA_INSTALL_COMMANDS}
@@ -737,10 +749,10 @@ if(WIN32) # see options at: https://cmake.org/cmake/help/latest/cpack_gen/nsis.h
ExecShell 'open' 'https://sunshinestream.readthedocs.io/'
nsExec::ExecToLog 'icacls \\\"$INSTDIR\\\" /reset'
nsExec::ExecToLog '\\\"$INSTDIR\\\\scripts\\\\migrate-config.bat\\\"'
- nsExec::ExecToLog 'icacls \\\"$INSTDIR\\\\config\\\" /grant:r Users:\\\(OI\\\)\\\(CI\\\)\\\(F\\\)'
nsExec::ExecToLog '\\\"$INSTDIR\\\\scripts\\\\add-firewall-rule.bat\\\"'
nsExec::ExecToLog '\\\"$INSTDIR\\\\scripts\\\\install-service.bat\\\"'
nsExec::ExecToLog '\\\"$INSTDIR\\\\scripts\\\\install-vigembus.bat\\\"'
+ nsExec::ExecToLog '\\\"$INSTDIR\\\\scripts\\\\autostart-service.bat\\\"'
NoController:
")
@@ -762,20 +774,19 @@ if(WIN32) # see options at: https://cmake.org/cmake/help/latest/cpack_gen/nsis.h
NoDelete:
")
- # Adding an option for the start menu and PATH
- # TODO: it asks to add it to the PATH but is not working https://gitlab.kitware.com/cmake/cmake/-/issues/15635
+ # Adding an option for the start menu
set(CPACK_NSIS_MODIFY_PATH "OFF")
set(CPACK_NSIS_EXECUTABLES_DIRECTORY ".")
# This will be shown on the installed apps Windows settings
set(CPACK_NSIS_INSTALLED_ICON_NAME "${CMAKE_PROJECT_NAME}.exe")
set(CPACK_NSIS_CREATE_ICONS_EXTRA
"${CPACK_NSIS_CREATE_ICONS_EXTRA}
- CreateShortCut '\$SMPROGRAMS\\\\$STARTMENU_FOLDER\\\\${CMAKE_PROJECT_NAME} (Foreground Mode).lnk' \
- '\$INSTDIR\\\\${CMAKE_PROJECT_NAME}.exe'
+ CreateShortCut '\$SMPROGRAMS\\\\$STARTMENU_FOLDER\\\\${CMAKE_PROJECT_NAME}.lnk' \
+ '\$INSTDIR\\\\${CMAKE_PROJECT_NAME}.exe' '--shortcut'
")
set(CPACK_NSIS_DELETE_ICONS_EXTRA
"${CPACK_NSIS_DELETE_ICONS_EXTRA}
- Delete '\$SMPROGRAMS\\\\$MUI_TEMP\\\\${CMAKE_PROJECT_NAME} (Foreground Mode).lnk'
+ Delete '\$SMPROGRAMS\\\\$MUI_TEMP\\\\${CMAKE_PROJECT_NAME}.lnk'
")
# Checking for previous installed versions
@@ -789,57 +800,48 @@ if(WIN32) # see options at: https://cmake.org/cmake/help/latest/cpack_gen/nsis.h
"https://sunshinestream.readthedocs.io" "Sunshine documentation"
"https://app.lizardbyte.dev" "LizardByte Web Site"
"https://app.lizardbyte.dev/support" "LizardByte Support")
+ set(CPACK_NSIS_MANIFEST_DPI_AWARE true)
# Setting components groups and dependencies
+ set(CPACK_COMPONENT_GROUP_CORE_EXPANDED true)
+
# sunshine binary
set(CPACK_COMPONENT_APPLICATION_DISPLAY_NAME "${CMAKE_PROJECT_NAME}")
- set(CPACK_COMPONENT_APPLICATION_DESCRIPTION "${CMAKE_PROJECT_NAME} main application.")
- set(CPACK_COMPONENT_APPLICATION_GROUP "core")
+ set(CPACK_COMPONENT_APPLICATION_DESCRIPTION "${CMAKE_PROJECT_NAME} main application and required components.")
+ set(CPACK_COMPONENT_APPLICATION_GROUP "Core")
set(CPACK_COMPONENT_APPLICATION_REQUIRED true)
set(CPACK_COMPONENT_APPLICATION_DEPENDS assets)
+ # service auto-start script
+ set(CPACK_COMPONENT_AUTOSTART_DISPLAY_NAME "Launch on Startup")
+ set(CPACK_COMPONENT_AUTOSTART_DESCRIPTION "If enabled, launches Sunshine automatically on system startup.")
+ set(CPACK_COMPONENT_AUTOSTART_GROUP "Core")
+
# assets
- set(CPACK_COMPONENT_ASSETS_DISPLAY_NAME "assets")
- set(CPACK_COMPONENT_ASSETS_DESCRIPTION "Shaders, default box art, and web ui.")
- set(CPACK_COMPONENT_ASSETS_GROUP "core")
+ set(CPACK_COMPONENT_ASSETS_DISPLAY_NAME "Required Assets")
+ set(CPACK_COMPONENT_ASSETS_DESCRIPTION "Shaders, default box art, and web UI.")
+ set(CPACK_COMPONENT_ASSETS_GROUP "Core")
set(CPACK_COMPONENT_ASSETS_REQUIRED true)
# audio tool
set(CPACK_COMPONENT_AUDIO_DISPLAY_NAME "audio-info")
set(CPACK_COMPONENT_AUDIO_DESCRIPTION "CLI tool providing information about sound devices.")
- set(CPACK_COMPONENT_AUDIO_GROUP "tools")
-
- # elevation tool
- set(CPACK_COMPONENT_ELEVATOR_DISPLAY_NAME "elevator")
- set(CPACK_COMPONENT_ELEVATOR_DESCRIPTION "CLI tool that assists with elevating \
- commands when permissions have been denied.")
- set(CPACK_COMPONENT_ELEVATOR_GROUP "tools")
+ set(CPACK_COMPONENT_AUDIO_GROUP "Tools")
# display tool
set(CPACK_COMPONENT_DXGI_DISPLAY_NAME "dxgi-info")
set(CPACK_COMPONENT_DXGI_DESCRIPTION "CLI tool providing information about graphics cards and displays.")
- set(CPACK_COMPONENT_DXGI_GROUP "tools")
-
- # service
- set(CPACK_COMPONENT_SUNSHINESVC_DISPLAY_NAME "sunshinesvc")
- set(CPACK_COMPONENT_SUNSHINESVC_DESCRIPTION "CLI tool providing ability to enable/disable the Sunshine service.")
- set(CPACK_COMPONENT_SUNSHINESVC_GROUP "tools")
-
- # service scripts
- set(CPACK_COMPONENT_SERVICE_DISPLAY_NAME "service-scripts")
- set(CPACK_COMPONENT_SERVICE_DESCRIPTION "Scripts to enable/disable the service.")
- set(CPACK_COMPONENT_SERVICE_GROUP "scripts")
- set(CPACK_COMPONENT_SERVICE_DEPENDS sunshinesvc)
+ set(CPACK_COMPONENT_DXGI_GROUP "Tools")
# firewall scripts
- set(CPACK_COMPONENT_FIREWALL_DISPLAY_NAME "firewall-scripts")
+ set(CPACK_COMPONENT_FIREWALL_DISPLAY_NAME "Add Firewall Exclusions")
set(CPACK_COMPONENT_FIREWALL_DESCRIPTION "Scripts to enable or disable firewall rules.")
- set(CPACK_COMPONENT_FIREWALL_GROUP "scripts")
+ set(CPACK_COMPONENT_FIREWALL_GROUP "Scripts")
# vigembus scripts
- set(CPACK_COMPONENT_VIGEMBUS_DISPLAY_NAME "vigembus-scripts")
+ set(CPACK_COMPONENT_VIGEMBUS_DISPLAY_NAME "Virtual Gamepad Support")
set(CPACK_COMPONENT_VIGEMBUS_DESCRIPTION "Scripts to install and uninstall ViGEmBus for virtual gamepad support.")
- set(CPACK_COMPONENT_VIGEMBUS_GROUP "scripts")
+ set(CPACK_COMPONENT_VIGEMBUS_GROUP "Scripts")
endif()
if(APPLE)
# TODO: bundle doesn't produce a valid .app use cpack -G DragNDrop
@@ -942,7 +944,7 @@ elseif(UNIX)
if(${SUNSHINE_TRAY} STREQUAL 1)
install(FILES "${CMAKE_SOURCE_DIR}/sunshine.svg"
- DESTINATION "/usr/share/icons")
+ DESTINATION "${CMAKE_INSTALL_PREFIX}/share/icons")
set(CPACK_DEBIAN_PACKAGE_DEPENDS "\
${CPACK_DEBIAN_PACKAGE_DEPENDS}, \
diff --git a/docker/archlinux.dockerfile b/docker/archlinux.dockerfile
index 0cbfcf7b7f8..5ff4a4ab0b8 100644
--- a/docker/archlinux.dockerfile
+++ b/docker/archlinux.dockerfile
@@ -131,7 +131,7 @@ userdel -r builder
# then create the lizard user
groupadd -f -g "${PGID}" "${UNAME}"
-useradd -lm -d ${HOME} -s /bin/bash -g "${PGID}" -G input -u "${PUID}" "${UNAME}"
+useradd -lm -d ${HOME} -s /bin/bash -g "${PGID}" -u "${PUID}" "${UNAME}"
mkdir -p ${HOME}/.config/sunshine
ln -s ${HOME}/.config/sunshine /config
chown -R ${UNAME} ${HOME}
diff --git a/docker/debian-bullseye.dockerfile b/docker/debian-bullseye.dockerfile
index 1d2d0fcf6e4..7c4396184ae 100644
--- a/docker/debian-bullseye.dockerfile
+++ b/docker/debian-bullseye.dockerfile
@@ -30,39 +30,39 @@ RUN <<_DEPS
set -e
apt-get update -y
apt-get install -y --no-install-recommends \
- build-essential=12.9* \
- cmake=3.18.4* \
- git=1:2.30.2* \
- libavdevice-dev=7:4.3.* \
- libboost-filesystem-dev=1.74.0* \
- libboost-locale-dev=1.74.0* \
- libboost-log-dev=1.74.0* \
- libboost-program-options-dev=1.74.0* \
- libboost-thread-dev=1.74.0* \
- libcap-dev=1:2.44* \
- libcurl4-openssl-dev=7.74.0* \
- libdrm-dev=2.4.104* \
- libevdev-dev=1.11.0* \
- libnuma-dev=2.0.12* \
- libopus-dev=1.3.1* \
- libpulse-dev=14.2* \
- libssl-dev=1.1.1* \
- libva-dev=2.10.0* \
- libvdpau-dev=1.4* \
- libwayland-dev=1.18.0* \
- libx11-dev=2:1.7.2* \
- libxcb-shm0-dev=1.14* \
- libxcb-xfixes0-dev=1.14* \
- libxcb1-dev=1.14* \
- libxfixes-dev=1:5.0.3* \
- libxrandr-dev=2:1.5.1* \
- libxtst-dev=2:1.2.3* \
- nodejs=12.22* \
- npm=7.5.2* \
- wget=1.21*
+ build-essential \
+ cmake=3.18.* \
+ git \
+ libavdevice-dev \
+ libboost-filesystem-dev=1.74.* \
+ libboost-locale-dev=1.74.* \
+ libboost-log-dev=1.74.* \
+ libboost-program-options-dev=1.74.* \
+ libboost-thread-dev=1.74.* \
+ libcap-dev \
+ libcurl4-openssl-dev \
+ libdrm-dev \
+ libevdev-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 \
+ nodejs \
+ npm \
+ wget
if [[ "${TARGETPLATFORM}" == 'linux/amd64' ]]; then
apt-get install -y --no-install-recommends \
- libmfx-dev=21.1.0*
+ libmfx-dev
fi
apt-get clean
rm -rf /var/lib/apt/lists/*
@@ -161,7 +161,7 @@ RUN <<_SETUP_USER
#!/bin/bash
set -e
groupadd -f -g "${PGID}" "${UNAME}"
-useradd -lm -d ${HOME} -s /bin/bash -g "${PGID}" -G input -u "${PUID}" "${UNAME}"
+useradd -lm -d ${HOME} -s /bin/bash -g "${PGID}" -u "${PUID}" "${UNAME}"
mkdir -p ${HOME}/.config/sunshine
ln -s ${HOME}/.config/sunshine /config
chown -R ${UNAME} ${HOME}
diff --git a/docker/fedora-37.dockerfile b/docker/fedora-37.dockerfile
index 2e1be80c368..b05f8cb1c21 100644
--- a/docker/fedora-37.dockerfile
+++ b/docker/fedora-37.dockerfile
@@ -30,37 +30,37 @@ set -e
dnf -y update
dnf -y group install "Development Tools"
dnf -y install \
- boost-devel-1.78.0* \
- cmake-3.24.1* \
- gcc-12.2.1* \
- gcc-c++-12.2.1* \
- git-2.39.2* \
- libappindicator-gtk3-devel-12.10.1* \
- libcap-devel-2.48* \
- libcurl-devel-7.85.0* \
- libdrm-devel-2.4.112* \
- libevdev-devel-1.13.0* \
- libva-devel-2.15.0* \
- libvdpau-devel-1.5* \
- libX11-devel-1.8.1* \
- libxcb-devel-1.13.1* \
- libXcursor-devel-1.2.1* \
- libXfixes-devel-6.0.0* \
- libXi-devel-1.8* \
- libXinerama-devel-1.1.4* \
- libXrandr-devel-1.5.2* \
- libXtst-devel-1.2.3* \
- mesa-libGL-devel-22.2.2* \
- npm-8.15.0* \
- numactl-devel-2.0.14* \
- openssl-devel-3.0.5* \
- opus-devel-1.3.1* \
- pulseaudio-libs-devel-16.1* \
- rpm-build-4.18.0* \
- wget-1.21.3* \
- which-2.21*
+ boost-devel-1.78.* \
+ cmake-3.26.* \
+ gcc-12.2.* \
+ gcc-c++-12.2.* \
+ git \
+ libappindicator-gtk3-devel \
+ libcap-devel \
+ libcurl-devel \
+ libdrm-devel \
+ libevdev-devel \
+ libva-devel \
+ libvdpau-devel \
+ libX11-devel \
+ libxcb-devel \
+ libXcursor-devel \
+ libXfixes-devel \
+ libXi-devel \
+ libXinerama-devel \
+ libXrandr-devel \
+ libXtst-devel \
+ mesa-libGL-devel \
+ nodejs-npm \
+ numactl-devel \
+ openssl-devel \
+ opus-devel \
+ pulseaudio-libs-devel \
+ rpm-build \
+ wget \
+ which
if [[ "${TARGETPLATFORM}" == 'linux/amd64' ]]; then
- dnf -y install intel-mediasdk-devel-22.4.4*
+ dnf -y install intel-mediasdk-devel
fi
dnf clean all
rm -rf /var/cache/yum
@@ -159,7 +159,7 @@ RUN <<_SETUP_USER
#!/bin/bash
set -e
groupadd -f -g "${PGID}" "${UNAME}"
-useradd -lm -d ${HOME} -s /bin/bash -g "${PGID}" -G input -u "${PUID}" "${UNAME}"
+useradd -lm -d ${HOME} -s /bin/bash -g "${PGID}" -u "${PUID}" "${UNAME}"
mkdir -p ${HOME}/.config/sunshine
ln -s ${HOME}/.config/sunshine /config
chown -R ${UNAME} ${HOME}
diff --git a/docker/fedora-36.dockerfile b/docker/fedora-38.dockerfile
similarity index 57%
rename from docker/fedora-36.dockerfile
rename to docker/fedora-38.dockerfile
index 33adf2250b4..9635f192cc7 100644
--- a/docker/fedora-36.dockerfile
+++ b/docker/fedora-38.dockerfile
@@ -4,7 +4,7 @@
# platforms_pr: linux/amd64
# no-cache-filters: sunshine-base,artifacts,sunshine
ARG BASE=fedora
-ARG TAG=36
+ARG TAG=38
FROM ${BASE}:${TAG} AS sunshine-base
FROM sunshine-base as sunshine-build
@@ -30,63 +30,64 @@ set -e
dnf -y update
dnf -y group install "Development Tools"
dnf -y install \
- boost-devel-1.76.0* \
- cmake-3.22.2* \
- gcc-12.0.1* \
- gcc-c++-12.0.1* \
- git-2.39.2* \
- libappindicator-gtk3-devel-12.10.0* \
- libcap-devel-2.48* \
- libcurl-devel-7.82.0* \
- libdrm-devel-2.4.110* \
- libevdev-devel-1.12.0* \
- libva-devel-2.14.0* \
- libvdpau-devel-1.5* \
- libX11-devel-1.7.3* \
- libxcb-devel-1.13.1* \
- libXcursor-devel-1.2.0* \
- libXfixes-devel-6.0.0* \
- libXi-devel-1.8* \
- libXinerama-devel-1.1.4* \
- libXrandr-devel-1.5.2* \
- libXtst-devel-1.2.3* \
- mesa-libGL-devel-22.0.1* \
- npm-8.3.1* \
- numactl-devel-2.0.14* \
- openssl-devel-3.0.2* \
- opus-devel-1.3.1* \
- pulseaudio-libs-devel-15.0* \
- rpm-build-4.17.0* \
- wget-1.21.3* \
- which-2.21*
+ boost-devel-1.78.0* \
+ cmake-3.26.* \
+ gcc-13.0.* \
+ gcc-c++-13.0.* \
+ git \
+ libappindicator-gtk3-devel \
+ libcap-devel \
+ libcurl-devel \
+ libdrm-devel \
+ libevdev-devel \
+ libva-devel \
+ libvdpau-devel \
+ libX11-devel \
+ libxcb-devel \
+ libXcursor-devel \
+ libXfixes-devel \
+ libXi-devel \
+ libXinerama-devel \
+ libXrandr-devel \
+ libXtst-devel \
+ mesa-libGL-devel \
+ nodejs-npm \
+ numactl-devel \
+ openssl-devel \
+ opus-devel \
+ pulseaudio-libs-devel \
+ rpm-build \
+ wget \
+ which
if [[ "${TARGETPLATFORM}" == 'linux/amd64' ]]; then
- dnf -y install intel-mediasdk-devel-22.3.0*
+ dnf -y install intel-mediasdk-devel
fi
dnf clean all
rm -rf /var/cache/yum
_DEPS
-# install cuda
-WORKDIR /build/cuda
-# versions: https://developer.nvidia.com/cuda-toolkit-archive
-ENV CUDA_VERSION="12.0.0"
-ENV CUDA_BUILD="525.60.13"
-# 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=/build/cuda --no-opengl-libs --no-man-page --no-drm
-rm ./cuda.run
-_INSTALL_CUDA
+# todo - enable cuda once it's supported for gcc 13 and fedora 38
+## install cuda
+#WORKDIR /build/cuda
+## versions: https://developer.nvidia.com/cuda-toolkit-archive
+#ENV CUDA_VERSION="12.0.0"
+#ENV CUDA_BUILD="525.60.13"
+## 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=/build/cuda --no-opengl-libs --no-man-page --no-drm
+#rm ./cuda.run
+#_INSTALL_CUDA
# copy repository
WORKDIR /build/sunshine/
@@ -99,11 +100,12 @@ RUN npm install
WORKDIR /build/sunshine/build
# cmake and cpack
+# todo - add cmake argument back in for cuda support "-DCMAKE_CUDA_COMPILER:PATH=/build/cuda/bin/nvcc \"
+# todo - re-enable "DSUNSHINE_ENABLE_CUDA"
RUN <<_MAKE
#!/bin/bash
set -e
cmake \
- -DCMAKE_CUDA_COMPILER:PATH=/build/cuda/bin/nvcc \
-DCMAKE_BUILD_TYPE=Release \
-DCMAKE_INSTALL_PREFIX=/usr \
-DSUNSHINE_ASSETS_DIR=share/sunshine \
@@ -111,7 +113,7 @@ cmake \
-DSUNSHINE_ENABLE_WAYLAND=ON \
-DSUNSHINE_ENABLE_X11=ON \
-DSUNSHINE_ENABLE_DRM=ON \
- -DSUNSHINE_ENABLE_CUDA=ON \
+ -DSUNSHINE_ENABLE_CUDA=OFF \
/build/sunshine
make -j "$(nproc)"
cpack -G RPM
@@ -159,7 +161,7 @@ RUN <<_SETUP_USER
#!/bin/bash
set -e
groupadd -f -g "${PGID}" "${UNAME}"
-useradd -lm -d ${HOME} -s /bin/bash -g "${PGID}" -G input -u "${PUID}" "${UNAME}"
+useradd -lm -d ${HOME} -s /bin/bash -g "${PGID}" -u "${PUID}" "${UNAME}"
mkdir -p ${HOME}/.config/sunshine
ln -s ${HOME}/.config/sunshine /config
chown -R ${UNAME} ${HOME}
diff --git a/docker/ubuntu-20.04.dockerfile b/docker/ubuntu-20.04.dockerfile
index 4de2c02ca2d..5689ba7134b 100644
--- a/docker/ubuntu-20.04.dockerfile
+++ b/docker/ubuntu-20.04.dockerfile
@@ -30,41 +30,41 @@ RUN <<_DEPS
set -e
apt-get update -y
apt-get install -y --no-install-recommends \
- build-essential=12.8* \
- gcc-10=10.3.0* \
- g++-10=10.3.0* \
- git=1:2.25.1* \
- libappindicator3-dev=12.10.1* \
- libavdevice-dev=7:4.2.* \
- libboost-filesystem-dev=1.71.0* \
- libboost-locale-dev=1.71.0* \
- libboost-log-dev=1.71.0* \
- libboost-program-options-dev=1.71.0* \
- libboost-thread-dev=1.71.0* \
- libcap-dev=1:2.32* \
- libcurl4-openssl-dev=7.68.0* \
- libdrm-dev=2.4.107* \
- libevdev-dev=1.9.0* \
- libnuma-dev=2.0.12* \
- libopus-dev=1.3.1* \
- libpulse-dev=1:13.99.1* \
- libssl-dev=1.1.1* \
- libva-dev=2.7.0* \
- libvdpau-dev=1.3* \
- libwayland-dev=1.18.0* \
- libx11-dev=2:1.6.9* \
- libxcb-shm0-dev=1.14* \
- libxcb-xfixes0-dev=1.14* \
- libxcb1-dev=1.14* \
- libxfixes-dev=1:5.0.3* \
- libxrandr-dev=2:1.5.2* \
- libxtst-dev=2:1.2.3* \
- nodejs=10.19.0* \
- npm=6.14.4* \
- wget=1.20.3*
+ build-essential \
+ gcc-10=10.3.* \
+ g++-10=10.3.* \
+ git \
+ libappindicator3-dev \
+ libavdevice-dev \
+ libboost-filesystem-dev=1.71.* \
+ libboost-locale-dev=1.71.* \
+ libboost-log-dev=1.71.* \
+ libboost-program-options-dev=1.71.* \
+ libboost-thread-dev=1.71.* \
+ libcap-dev \
+ libcurl4-openssl-dev \
+ libdrm-dev \
+ libevdev-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 \
+ nodejs \
+ npm \
+ wget
if [[ "${TARGETPLATFORM}" == 'linux/amd64' ]]; then
apt-get install -y --no-install-recommends \
- libmfx-dev=20.1.0*
+ libmfx-dev
fi
apt-get clean
rm -rf /var/lib/apt/lists/*
@@ -198,7 +198,7 @@ RUN <<_SETUP_USER
#!/bin/bash
set -e
groupadd -f -g "${PGID}" "${UNAME}"
-useradd -lm -d ${HOME} -s /bin/bash -g "${PGID}" -G input -u "${PUID}" "${UNAME}"
+useradd -lm -d ${HOME} -s /bin/bash -g "${PGID}" -u "${PUID}" "${UNAME}"
mkdir -p ${HOME}/.config/sunshine
ln -s ${HOME}/.config/sunshine /config
chown -R ${UNAME} ${HOME}
diff --git a/docker/ubuntu-22.04.dockerfile b/docker/ubuntu-22.04.dockerfile
index e8a69361894..143fc1e4b24 100644
--- a/docker/ubuntu-22.04.dockerfile
+++ b/docker/ubuntu-22.04.dockerfile
@@ -30,40 +30,40 @@ RUN <<_DEPS
set -e
apt-get update -y
apt-get install -y --no-install-recommends \
- build-essential=12.9* \
- cmake=3.22.1* \
- git=1:2.34.1* \
- libappindicator3-dev=12.10.1* \
- libavdevice-dev=7:4.4.* \
- libboost-filesystem-dev=1.74.0* \
- libboost-locale-dev=1.74.0* \
- libboost-log-dev=1.74.0* \
- libboost-program-options-dev=1.74.0* \
- libboost-thread-dev=1.74.0* \
- libcap-dev=1:2.44* \
- libcurl4-openssl-dev=7.81.0* \
- libdrm-dev=2.4.113* \
- libevdev-dev=1.12.1* \
- libnuma-dev=2.0.14* \
- libopus-dev=1.3.1* \
- libpulse-dev=1:15.99.1* \
- libssl-dev=3.0.2* \
- libva-dev=2.14.0* \
- libvdpau-dev=1.4* \
- libwayland-dev=1.20.0* \
- libx11-dev=2:1.7.5* \
- libxcb-shm0-dev=1.14* \
- libxcb-xfixes0-dev=1.14* \
- libxcb1-dev=1.14* \
- libxfixes-dev=1:6.0.0* \
- libxrandr-dev=2:1.5.2* \
- libxtst-dev=2:1.2.3* \
- nodejs=12.22.9* \
- npm=8.5.1* \
- wget=1.21.2*
+ build-essential \
+ cmake=3.22.* \
+ git \
+ libappindicator3-dev \
+ libavdevice-dev \
+ libboost-filesystem-dev=1.74.* \
+ libboost-locale-dev=1.74.* \
+ libboost-log-dev=1.74.* \
+ libboost-program-options-dev=1.74.* \
+ libboost-thread-dev=1.74.* \
+ libcap-dev \
+ libcurl4-openssl-dev \
+ libdrm-dev \
+ libevdev-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 \
+ nodejs \
+ npm \
+ wget
if [[ "${TARGETPLATFORM}" == 'linux/amd64' ]]; then
apt-get install -y --no-install-recommends \
- libmfx-dev=22.3.0*
+ libmfx-dev
fi
apt-get clean
rm -rf /var/lib/apt/lists/*
@@ -162,7 +162,7 @@ RUN <<_SETUP_USER
#!/bin/bash
set -e
groupadd -f -g "${PGID}" "${UNAME}"
-useradd -lm -d ${HOME} -s /bin/bash -g "${PGID}" -G input -u "${PUID}" "${UNAME}"
+useradd -lm -d ${HOME} -s /bin/bash -g "${PGID}" -u "${PUID}" "${UNAME}"
mkdir -p ${HOME}/.config/sunshine
ln -s ${HOME}/.config/sunshine /config
chown -R ${UNAME} ${HOME}
diff --git a/docs/Doxyfile b/docs/Doxyfile
index cac6c8f8e9b..5d909b5335e 100644
--- a/docs/Doxyfile
+++ b/docs/Doxyfile
@@ -184,7 +184,7 @@ FULL_PATH_NAMES = YES
# will be relative from the directory where doxygen is started.
# This tag requires that the tag FULL_PATH_NAMES is set to YES.
-STRIP_FROM_PATH =
+STRIP_FROM_PATH = ../
# The STRIP_FROM_INC_PATH tag can be used to strip a user-defined part of the
# path mentioned in the documentation of a class, which tells the reader which
@@ -193,7 +193,7 @@ STRIP_FROM_PATH =
# specify the list of include paths that are normally passed to the compiler
# using the -I flag.
-STRIP_FROM_INC_PATH =
+STRIP_FROM_INC_PATH = ../
# If the SHORT_NAMES tag is set to YES, doxygen will generate much shorter (but
# less readable) file names. This can be useful is your file systems doesn't
@@ -878,6 +878,7 @@ WARN_IF_UNDOC_ENUM_VAL = NO
# The default value is: NO.
WARN_AS_ERROR = NO
+# todo - ideally this will eventually become FAIL_ON_WARNINGS
# The WARN_FORMAT tag determines the format of the warning messages that doxygen
# can produce. The string should contain the $file, $line, and $text tags, which
@@ -2331,7 +2332,7 @@ ENABLE_PREPROCESSING = YES
# The default value is: NO.
# This tag requires that the tag ENABLE_PREPROCESSING is set to YES.
-MACRO_EXPANSION = NO
+MACRO_EXPANSION = YES
# If the EXPAND_ONLY_PREDEF and MACRO_EXPANSION tags are both set to YES then
# the macro expansion is limited to the macros specified with the PREDEFINED and
@@ -2354,7 +2355,7 @@ SEARCH_INCLUDES = YES
# RECURSIVE has no effect here.
# This tag requires that the tag SEARCH_INCLUDES is set to YES.
-INCLUDE_PATH =
+INCLUDE_PATH = ../third-party/ffmpeg-linux-x86_64/include/
# You can use the INCLUDE_FILE_PATTERNS tag to specify one or more wildcard
# patterns (like *.h and *.hpp) to filter out the header-files in the
@@ -2463,7 +2464,7 @@ HIDE_UNDOC_RELATIONS = YES
# set to NO
# The default value is: NO.
-HAVE_DOT = NO
+HAVE_DOT = YES
# The DOT_NUM_THREADS specifies the number of dot invocations doxygen is allowed
# to run in parallel. When set to 0 doxygen will base this on the number of
diff --git a/docs/requirements.txt b/docs/requirements.txt
index 6160650a168..7ce0be80b83 100644
--- a/docs/requirements.txt
+++ b/docs/requirements.txt
@@ -1,5 +1,5 @@
breathe==4.35.0
-furo==2023.3.27
+furo==2023.5.20
m2r2==0.3.3.post2
-Sphinx==6.1.3
-sphinx-copybutton==0.5.1
+Sphinx==7.0.1
+sphinx-copybutton==0.5.2
diff --git a/docs/source/about/advanced_usage.rst b/docs/source/about/advanced_usage.rst
index 19fe237791c..088d9045c29 100644
--- a/docs/source/about/advanced_usage.rst
+++ b/docs/source/about/advanced_usage.rst
@@ -156,14 +156,12 @@ back_button_timeout
^^^^^^^^^^^^^^^^^^^
**Description**
- If, after the timeout, the back/select button is still pressed down, Home/Guide button press is emulated.
-
- On Nvidia Shield, the home and power button are not passed to Moonlight.
+ If the Back/Select button is held down for the specified number of milliseconds, a Home/Guide button press is emulated.
.. Tip:: If back_button_timeout < 0, then the Home/Guide button will not be emulated.
**Default**
- ``2000``
+ ``-1``
**Example**
.. code-block:: text
@@ -200,6 +198,27 @@ key_repeat_frequency
key_repeat_frequency = 24.9
+always_send_scancodes
+^^^^^^^^^^^^^^^^^^^^^
+
+**Description**
+ Sending scancodes enhances compatibility with games and apps but may result in incorrect keyboard input
+ from certain clients that aren't using a US English keyboard layout.
+
+ Enable if keyboard input is not working at all in certain applications.
+
+ Disable if keys on the client are generating the wrong input on the host.
+
+ .. Caution:: Applies to Windows only.
+
+**Default**
+ ``enabled``
+
+**Example**
+ .. code-block:: text
+
+ always_send_scancodes = enabled
+
keybindings
^^^^^^^^^^^
@@ -373,7 +392,7 @@ resolutions
2560x1080,
3440x1440,
1920x1200,
- 3860x2160,
+ 3840x2160,
3840x1600,
]
@@ -389,7 +408,7 @@ resolutions
2560x1080,
3440x1440,
1920x1200,
- 3860x2160,
+ 3840x2160,
3840x1600,
]
@@ -447,6 +466,8 @@ audio_sink
tools\audio-info.exe
+ .. Tip:: If you have multiple audio devices with identical names, use the Device ID instead.
+
.. Tip:: If you want to mute the host speakers, use `virtual_sink`_ instead.
**Default**
@@ -466,7 +487,7 @@ audio_sink
**Windows**
.. code-block:: text
- audio_sink = {0.0.0.00000000}.{FD47D9CC-4218-4135-9CE2-0C195C87405B}
+ audio_sink = Speakers (High Definition Audio Device)
virtual_sink
^^^^^^^^^^^^
@@ -481,14 +502,31 @@ virtual_sink
- Stream Streaming Speakers (Linux, macOS, Windows)
- - To use this option, you must have Steam installed and have used Stream remote play at least once.
+ - Steam must be installed.
+ - Enable `install_steam_audio_drivers`_ or use Steam Remote Play at least once to install the drivers.
- `Virtual Audio Cable `_ (macOS, Windows)
**Example**
.. code-block:: text
- virtual_sink = {0.0.0.00000000}.{8edba70c-1125-467c-b89c-15da389bc1d4}
+ virtual_sink = Steam Streaming Speakers
+
+install_steam_audio_drivers
+^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+**Description**
+ Installs the Steam Streaming Speakers driver (if Steam is installed) to support surround sound and muting host audio.
+
+ .. Tip:: This option is only supported on Windows.
+
+**Default**
+ ``enabled``
+
+**Example**
+ .. code-block:: text
+
+ install_steam_audio_drivers = enabled
Network
-------
@@ -790,7 +828,7 @@ capture
nvfbc Use NVIDIA Frame Buffer Capture to capture direct to GPU memory. This is usually the fastest method for
NVIDIA cards. For GeForce cards it will only work with drivers patched with
`nvidia-patch `_
- or `nvlax `_.
+ or `nvlax `_.
wlr Capture for wlroots based Wayland compositors via DMA-BUF.
kms DRM/KMS screen capture from the kernel. This requires that sunshine has cap_sys_admin capability.
See :ref:`Linux Setup `.
diff --git a/docs/source/about/app_examples.rst b/docs/source/about/app_examples.rst
index dbeac1ac91d..0e39029797b 100644
--- a/docs/source/about/app_examples.rst
+++ b/docs/source/about/app_examples.rst
@@ -187,3 +187,32 @@ Changing Resolution and Refresh Rate (Windows)
.. Tip:: You can change your host resolution to match the client resolution automatically using the
`Nonary/ResolutionAutomation `_ project.
+
+
+Elevating Commands (Windows)
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+If you've installed Sunshine as a service (default), you can now specify if a command should be elevated with adminsitrative privileges.
+Simply enable the elevated option in the WEB UI, or add it to the JSON configuration.
+This is an option for both prep-cmd and regular commands and will launch the process with the current user without a UAC prompt.
+
+.. Note:: It's important to write the values "true" and "false" as string values, not as the typical true/false values in most JSON.
+
+**Example**
+ .. code-block:: json
+
+ {
+ "name": "Game With AntiCheat that Requires Admin",
+ "output": "",
+ "cmd": "ping 127.0.0.1",
+ "exclude-global-prep-cmd": "false",
+ "elevated": "true",
+ "prep-cmd": [
+ {
+ "do": "powershell.exe -command \"Start-Streaming\"",
+ "undo": "powershell.exe -command \"Stop-Streaming\"",
+ "elevated": "false"
+ }
+ ],
+ "image-path": ""
+ }
diff --git a/docs/source/about/installation.rst b/docs/source/about/installation.rst
index dede1f22d0c..323d78f4610 100644
--- a/docs/source/about/installation.rst
+++ b/docs/source/about/installation.rst
@@ -40,8 +40,8 @@ CUDA is used for NVFBC capture.
sunshine.pkg.tar.zst 11.8.0 450.80.02 50;52;60;61;62;70;75;80;86;90;35
sunshine_{arch}.flatpak 12.0.0 525.60.13 50;52;60;61;62;70;75;80;86;90
sunshine-debian-bullseye-{arch}.deb 11.8.0 450.80.02 50;52;60;61;62;70;75;80;86;90;35
- sunshine-fedora-36-{arch}.rpm 12.0.0 525.60.13 50;52;60;61;62;70;75;80;86;90
sunshine-fedora-37-{arch}.rpm 12.0.0 525.60.13 50;52;60;61;62;70;75;80;86;90
+ sunshine-fedora-38-{arch}.rpm unavailable unavailable none
sunshine-ubuntu-20.04-{arch}.deb 11.8.0 450.80.02 50;52;60;61;62;70;75;80;86;90;35
sunshine-ubuntu-22.04-{arch}.deb 11.8.0 450.80.02 50;52;60;61;62;70;75;80;86;90;35
=========================================== ============== ============== ================================
@@ -190,11 +190,11 @@ macOS
-----
Sunshine on macOS is experimental. Gamepads do not work. Other features may not work as expected.
-pkg
+dmg
^^^
-.. Warning:: The `pkg` does not include runtime dependencies.
+.. Warning:: The `dmg` does not include runtime dependencies.
-#. Download the ``sunshine.pkg`` file and install it as normal.
+#. Download the ``sunshine.dmg`` file and install it.
Uninstall:
.. code-block:: bash
diff --git a/docs/source/about/usage.rst b/docs/source/about/usage.rst
index db7af09ea7e..fa680ba6c2d 100644
--- a/docs/source/about/usage.rst
+++ b/docs/source/about/usage.rst
@@ -68,16 +68,11 @@ The `deb`, `rpm`, `Flatpak` and `AppImage` packages handle these steps automatic
Sunshine needs access to `uinput` to create mouse and gamepad events.
-#. Add user to group `input`, if this is the first time installing.
- .. code-block:: bash
-
- sudo usermod -a -G input $USER
-
#. Create `udev` rules.
- .. code-block::
+ .. code-block:: bash
- echo 'KERNEL=="uinput", GROUP="input", MODE="0660", OPTIONS+="static_node=uinput"' | \
- sudo tee /etc/udev/rules.d/85-sunshine-input.rules
+ echo 'KERNEL=="uinput", SUBSYSTEM=="misc", OPTIONS+="static_node=uinput", TAG+="uaccess"' | \
+ sudo tee /etc/udev/rules.d/85-sunshine.rules
#. Optionally, configure autostart service
diff --git a/docs/source/conf.py b/docs/source/conf.py
index 58bff26c731..e581783865f 100644
--- a/docs/source/conf.py
+++ b/docs/source/conf.py
@@ -23,7 +23,7 @@
# -- Project information -----------------------------------------------------
project = 'Sunshine'
-copyright = f'{datetime.now ().year}, {project}'
+project_copyright = f'{datetime.now ().year}, {project}'
author = 'ReenigneArcher'
# The full version, including alpha/beta/rc tags
@@ -95,8 +95,16 @@
)
todo_include_todos = True
-subprocess.run('doxygen', shell=True, cwd=source_dir)
-
# disable epub mimetype warnings
# https://github.com/readthedocs/readthedocs.org/blob/eadf6ac6dc6abc760a91e1cb147cc3c5f37d1ea8/docs/conf.py#L235-L236
suppress_warnings = ["epub.unknown_project_files"]
+
+# get doxygen version
+doxy_proc = subprocess.run('doxygen --version', shell=True, cwd=source_dir, capture_output=True)
+doxy_version = doxy_proc.stdout.decode('utf-8').strip()
+print('doxygen version: ' + doxy_version)
+
+# run doxygen
+doxy_proc = subprocess.run('doxygen Doxyfile', shell=True, cwd=source_dir)
+if doxy_proc.returncode != 0:
+ raise RuntimeError('doxygen failed with return code ' + str(doxy_proc.returncode))
diff --git a/docs/source/gamestream/gamestream.rst b/docs/source/gamestream/gamestream.rst
index 59ba6e1d0e7..aed014b287c 100644
--- a/docs/source/gamestream/gamestream.rst
+++ b/docs/source/gamestream/gamestream.rst
@@ -12,6 +12,14 @@ migration option. At the time of writing this GSMS offers the ability to migrate
working directory, command, and image are all set in Sunshine's ``apps.json`` file. The box-art image is also copied
to a specified directory.
+Internet Streaming
+------------------
+If you are using the Moonlight Internet Hosting Tool, you can remove it from your system when you migrate to Sunshine.
+To stream over the Internet with Sunshine and a UPnP-capable router, enable the UPnP option in the Sunshine Web UI.
+
+.. note:: Running Sunshine together with versions of the Moonlight Internet Hosting Tool prior to v5.6 will cause UPnP
+ port forwarding to become unreliable. Either uninstall the tool entirely or update it to v5.6 or later.
+
Limitations
-----------
Sunshine does have some limitations, as compared to Nvidia GameStream.
diff --git a/docs/source/source/src/audio.rst b/docs/source/source/src/audio.rst
index a8fb3c9ca45..08665fe6780 100644
--- a/docs/source/source/src/audio.rst
+++ b/docs/source/source/src/audio.rst
@@ -2,3 +2,4 @@ audio
=====
.. doxygenfile:: audio.h
+ :allow-dot-graphs:
diff --git a/docs/source/source/src/cbs.rst b/docs/source/source/src/cbs.rst
index 2abe9915772..6547c495821 100644
--- a/docs/source/source/src/cbs.rst
+++ b/docs/source/source/src/cbs.rst
@@ -2,3 +2,4 @@ cbs
===
.. doxygenfile:: cbs.h
+ :allow-dot-graphs:
diff --git a/docs/source/source/src/config.rst b/docs/source/source/src/config.rst
index 44e10565493..34bccff3ce2 100644
--- a/docs/source/source/src/config.rst
+++ b/docs/source/source/src/config.rst
@@ -2,3 +2,4 @@ config
======
.. doxygenfile:: config.h
+ :allow-dot-graphs:
diff --git a/docs/source/source/src/confighttp.rst b/docs/source/source/src/confighttp.rst
index 3b46ba5edf7..348f6521ba6 100644
--- a/docs/source/source/src/confighttp.rst
+++ b/docs/source/source/src/confighttp.rst
@@ -2,3 +2,4 @@ confighttp
==========
.. doxygenfile:: confighttp.h
+ :allow-dot-graphs:
diff --git a/docs/source/source/src/crypto.rst b/docs/source/source/src/crypto.rst
index 5abf24693db..aced9d35c68 100644
--- a/docs/source/source/src/crypto.rst
+++ b/docs/source/source/src/crypto.rst
@@ -2,3 +2,4 @@ crypto
======
.. doxygenfile:: crypto.h
+ :allow-dot-graphs:
diff --git a/docs/source/source/src/httpcommon.rst b/docs/source/source/src/httpcommon.rst
index 8afbdeb215f..67557f86860 100644
--- a/docs/source/source/src/httpcommon.rst
+++ b/docs/source/source/src/httpcommon.rst
@@ -2,3 +2,4 @@ httpcommon
==========
.. doxygenfile:: httpcommon.h
+ :allow-dot-graphs:
diff --git a/docs/source/source/src/input.rst b/docs/source/source/src/input.rst
index e1988b2406f..4c8db082dc6 100644
--- a/docs/source/source/src/input.rst
+++ b/docs/source/source/src/input.rst
@@ -2,3 +2,4 @@ input
=====
.. doxygenfile:: input.h
+ :allow-dot-graphs:
diff --git a/docs/source/source/src/main.rst b/docs/source/source/src/main.rst
index cd6716faa5c..5102a3e9448 100644
--- a/docs/source/source/src/main.rst
+++ b/docs/source/source/src/main.rst
@@ -2,3 +2,4 @@ main
====
.. doxygenfile:: main.h
+ :allow-dot-graphs:
diff --git a/docs/source/source/src/move_by_copy.rst b/docs/source/source/src/move_by_copy.rst
index 034c3aa240a..5ec37716a9a 100644
--- a/docs/source/source/src/move_by_copy.rst
+++ b/docs/source/source/src/move_by_copy.rst
@@ -2,3 +2,4 @@ move_by_copy
============
.. doxygenfile:: move_by_copy.h
+ :allow-dot-graphs:
diff --git a/docs/source/source/src/network.rst b/docs/source/source/src/network.rst
index 00df0e16a5f..a9121e16008 100644
--- a/docs/source/source/src/network.rst
+++ b/docs/source/source/src/network.rst
@@ -2,3 +2,4 @@ network
=======
.. doxygenfile:: network.h
+ :allow-dot-graphs:
diff --git a/docs/source/source/src/nvhttp.rst b/docs/source/source/src/nvhttp.rst
index ea4daa01507..8a3bbd008a5 100644
--- a/docs/source/source/src/nvhttp.rst
+++ b/docs/source/source/src/nvhttp.rst
@@ -2,3 +2,4 @@ nvhttp
======
.. doxygenfile:: nvhttp.h
+ :allow-dot-graphs:
diff --git a/docs/source/source/src/platform/linux/cuda.rst b/docs/source/source/src/platform/linux/cuda.rst
index cb975aa918d..5b6dffe770b 100644
--- a/docs/source/source/src/platform/linux/cuda.rst
+++ b/docs/source/source/src/platform/linux/cuda.rst
@@ -2,3 +2,4 @@ cuda
====
.. doxygenfile:: platform/linux/cuda.h
+ :allow-dot-graphs:
diff --git a/docs/source/source/src/platform/linux/vaapi.rst b/docs/source/source/src/platform/linux/vaapi.rst
index 8880e079eeb..973e78523b4 100644
--- a/docs/source/source/src/platform/linux/vaapi.rst
+++ b/docs/source/source/src/platform/linux/vaapi.rst
@@ -2,3 +2,4 @@ vaapi
=====
.. doxygenfile:: platform/linux/vaapi.h
+ :allow-dot-graphs:
diff --git a/docs/source/source/src/platform/linux/wayland.rst b/docs/source/source/src/platform/linux/wayland.rst
index 72f74fe2b83..670e4340ab0 100644
--- a/docs/source/source/src/platform/linux/wayland.rst
+++ b/docs/source/source/src/platform/linux/wayland.rst
@@ -2,3 +2,4 @@ wayland
=======
.. doxygenfile:: platform/linux/wayland.h
+ :allow-dot-graphs:
diff --git a/docs/source/source/src/platform/macos/misc.rst b/docs/source/source/src/platform/macos/misc.rst
index f000da0813f..26cbc1874c6 100644
--- a/docs/source/source/src/platform/macos/misc.rst
+++ b/docs/source/source/src/platform/macos/misc.rst
@@ -2,3 +2,4 @@ misc
====
.. doxygenfile:: platform/macos/misc.h
+ :allow-dot-graphs:
diff --git a/docs/source/source/src/platform/windows/misc.rst b/docs/source/source/src/platform/windows/misc.rst
index cca4b82fdd4..88c1620002a 100644
--- a/docs/source/source/src/platform/windows/misc.rst
+++ b/docs/source/source/src/platform/windows/misc.rst
@@ -2,3 +2,4 @@ misc
====
.. doxygenfile:: platform/windows/misc.h
+ :allow-dot-graphs:
diff --git a/docs/source/source/src/process.rst b/docs/source/source/src/process.rst
index fd4110a09a0..ad8f9764114 100644
--- a/docs/source/source/src/process.rst
+++ b/docs/source/source/src/process.rst
@@ -2,3 +2,4 @@ process
=======
.. doxygenfile:: process.h
+ :allow-dot-graphs:
diff --git a/docs/source/source/src/round_robin.rst b/docs/source/source/src/round_robin.rst
index 2a838b88f50..89aea6f5904 100644
--- a/docs/source/source/src/round_robin.rst
+++ b/docs/source/source/src/round_robin.rst
@@ -2,3 +2,4 @@ round_robin
===========
.. doxygenfile:: round_robin.h
+ :allow-dot-graphs:
diff --git a/docs/source/source/src/rtsp.rst b/docs/source/source/src/rtsp.rst
index 419f7d7fe2c..7aee0baf056 100644
--- a/docs/source/source/src/rtsp.rst
+++ b/docs/source/source/src/rtsp.rst
@@ -2,3 +2,4 @@ rtsp
====
.. doxygenfile:: rtsp.h
+ :allow-dot-graphs:
diff --git a/docs/source/source/src/stream.rst b/docs/source/source/src/stream.rst
index b9bcc4b4675..c771414016a 100644
--- a/docs/source/source/src/stream.rst
+++ b/docs/source/source/src/stream.rst
@@ -2,3 +2,4 @@ stream
======
.. doxygenfile:: stream.h
+ :allow-dot-graphs:
diff --git a/docs/source/source/src/sync.rst b/docs/source/source/src/sync.rst
index 43cea778eca..6f3d95929e2 100644
--- a/docs/source/source/src/sync.rst
+++ b/docs/source/source/src/sync.rst
@@ -2,3 +2,4 @@ sync
====
.. doxygenfile:: sync.h
+ :allow-dot-graphs:
diff --git a/docs/source/source/src/system_tray.rst b/docs/source/source/src/system_tray.rst
index 3b69c246977..7c1009e1c8c 100644
--- a/docs/source/source/src/system_tray.rst
+++ b/docs/source/source/src/system_tray.rst
@@ -2,3 +2,4 @@ system_tray
===========
.. doxygenfile:: system_tray.h
+ :allow-dot-graphs:
diff --git a/docs/source/source/src/task_pool.rst b/docs/source/source/src/task_pool.rst
index 8c37231452d..a556141aa4c 100644
--- a/docs/source/source/src/task_pool.rst
+++ b/docs/source/source/src/task_pool.rst
@@ -1,4 +1,5 @@
-tasl_pool
+task_pool
=========
.. doxygenfile:: task_pool.h
+ :allow-dot-graphs:
diff --git a/docs/source/source/src/thread_pool.rst b/docs/source/source/src/thread_pool.rst
index 3d563bd3486..32787512811 100644
--- a/docs/source/source/src/thread_pool.rst
+++ b/docs/source/source/src/thread_pool.rst
@@ -2,3 +2,4 @@ thread_pool
===========
.. doxygenfile:: thread_pool.h
+ :allow-dot-graphs:
diff --git a/docs/source/source/src/thread_safe.rst b/docs/source/source/src/thread_safe.rst
index 00f394c4df3..d7ecdda3f43 100644
--- a/docs/source/source/src/thread_safe.rst
+++ b/docs/source/source/src/thread_safe.rst
@@ -2,3 +2,4 @@ thread_safe
===========
.. doxygenfile:: thread_safe.h
+ :allow-dot-graphs:
diff --git a/docs/source/source/src/upnp.rst b/docs/source/source/src/upnp.rst
index b38e8d63dc0..987692353b7 100644
--- a/docs/source/source/src/upnp.rst
+++ b/docs/source/source/src/upnp.rst
@@ -2,3 +2,4 @@ upnp
====
.. doxygenfile:: upnp.h
+ :allow-dot-graphs:
diff --git a/docs/source/source/src/uuid.rst b/docs/source/source/src/uuid.rst
index 803f0e1c904..d21e0a6ceaf 100644
--- a/docs/source/source/src/uuid.rst
+++ b/docs/source/source/src/uuid.rst
@@ -2,3 +2,4 @@ uuid
====
.. doxygenfile:: uuid.h
+ :allow-dot-graphs:
diff --git a/docs/source/source/src/video.rst b/docs/source/source/src/video.rst
index 17bca35e8ba..2e45883f293 100644
--- a/docs/source/source/src/video.rst
+++ b/docs/source/source/src/video.rst
@@ -2,3 +2,4 @@ video
=====
.. doxygenfile:: video.h
+ :allow-dot-graphs:
diff --git a/packaging/linux/AppImage/AppRun b/packaging/linux/AppImage/AppRun
index e9f9f3f733c..ddc5fd38455 100644
--- a/packaging/linux/AppImage/AppRun
+++ b/packaging/linux/AppImage/AppRun
@@ -45,9 +45,8 @@ echo "
function install() {
# user input rules
- sudo usermod -a -G input $USER
# shellcheck disable=SC2002
- cat "$SUNSHINE_SHARE_HERE/udev/rules.d/85-sunshine.rules" | sudo tee /etc/udev/85-sunshine.rules
+ cat "$SUNSHINE_SHARE_HERE/udev/rules.d/85-sunshine.rules" | sudo tee /etc/udev/rules.d/85-sunshine.rules
# sunshine service
mkdir -p ~/.config/systemd/user
diff --git a/packaging/linux/flatpak/scripts/additional-install.sh b/packaging/linux/flatpak/scripts/additional-install.sh
index 43615144494..8a905b53810 100644
--- a/packaging/linux/flatpak/scripts/additional-install.sh
+++ b/packaging/linux/flatpak/scripts/additional-install.sh
@@ -6,8 +6,8 @@ cp /app/share/sunshine/systemd/user/sunshine.service $HOME/.config/systemd/user/
echo Sunshine User Service has been installed.
echo Use [systemctl --user enable sunshine] once to autostart Sunshine on login.
-# Udev rule and input group
+# Udev rule
UDEV=$(cat /app/share/sunshine/udev/rules.d/85-sunshine.rules)
echo Configuring mouse permission.
-flatpak-spawn --host pkexec sh -c "usermod -a -G input $USER && echo '$UDEV' > /etc/udev/rules.d/85-sunshine.rules"
+flatpak-spawn --host pkexec sh -c "echo '$UDEV' > /etc/udev/rules.d/85-sunshine.rules"
echo Restart computer for mouse permission to take effect.
diff --git a/packaging/linux/flatpak/scripts/remove-additional-install.sh b/packaging/linux/flatpak/scripts/remove-additional-install.sh
index 74cb95054ed..6148f62ea1e 100644
--- a/packaging/linux/flatpak/scripts/remove-additional-install.sh
+++ b/packaging/linux/flatpak/scripts/remove-additional-install.sh
@@ -6,6 +6,6 @@ rm $HOME/.config/systemd/user/sunshine.service
systemctl --user daemon-reload
echo Sunshine User Service has been removed.
-# Udev rule and input group
-flatpak-spawn --host pkexec sh -c "gpasswd -d $USER input && rm /etc/udev/rules.d/85-sunshine.rules"
+# Udev rule
+flatpak-spawn --host pkexec sh -c "rm /etc/udev/rules.d/85-sunshine.rules"
echo Mouse permission removed. Restart computer to take effect.
diff --git a/src/audio.cpp b/src/audio.cpp
index e926e01324b..b2c8f458f7f 100644
--- a/src/audio.cpp
+++ b/src/audio.cpp
@@ -1,3 +1,7 @@
+/**
+ * @file src/audio.cpp
+ * @brief todo
+ */
#include
#include
@@ -135,22 +139,30 @@ namespace audio {
return;
}
- auto &control = ref->control;
- if (!control) {
+ auto init_failure_fg = util::fail_guard([&shutdown_event]() {
+ BOOST_LOG(error) << "Unable to initialize audio capture. The stream will not have audio."sv;
+
+ // Wait for shutdown to be signalled if we fail init.
+ // This allows streaming to continue without audio.
shutdown_event->view();
+ });
+ auto &control = ref->control;
+ if (!control) {
return;
}
// Order of priority:
- // 1. Config
- // 2. Virtual if available
+ // 1. Virtual sink
+ // 2. Audio sink
// 3. Host
std::string *sink = &ref->sink.host;
if (!config::audio.sink.empty()) {
sink = &config::audio.sink;
}
- else if (ref->sink.null) {
+
+ // Prefer the virtual sink if host playback is disabled or there's no other sink
+ if (ref->sink.null && (!config.flags[config_t::HOST_AUDIO] || sink->empty())) {
auto &null = *ref->sink.null;
switch (stream->channelCount) {
case 2:
@@ -167,20 +179,24 @@ namespace audio {
// Only the first to start a session may change the default sink
if (!ref->sink_flag->exchange(true, std::memory_order_acquire)) {
- ref->restore_sink = !config.flags[config_t::HOST_AUDIO];
-
- // If the sink is empty (Host has no sink!), definately switch to the virtual.
- if (ref->sink.host.empty()) {
+ // If the selected sink is different than the current one, change sinks.
+ ref->restore_sink = ref->sink.host != *sink;
+ if (ref->restore_sink) {
if (control->set_sink(*sink)) {
return;
}
}
- // If the client requests audio on the host, don't change the default sink
- else if (!config.flags[config_t::HOST_AUDIO] && control->set_sink(*sink)) {
- return;
- }
}
+ auto frame_size = config.packetDuration * stream->sampleRate / 1000;
+ auto mic = control->microphone(stream->mapping, stream->channelCount, stream->sampleRate, frame_size);
+ if (!mic) {
+ return;
+ }
+
+ // Audio is initialized, so we don't want to print the failure message
+ init_failure_fg.disable();
+
// Capture takes place on this thread
platf::adjust_thread_priority(platf::thread_priority_e::critical);
@@ -194,16 +210,8 @@ namespace audio {
shutdown_event->view();
});
- auto frame_size = config.packetDuration * stream->sampleRate / 1000;
int samples_per_frame = frame_size * stream->channelCount;
- auto mic = control->microphone(stream->mapping, stream->channelCount, stream->sampleRate, frame_size);
- if (!mic) {
- BOOST_LOG(error) << "Couldn't create audio input"sv;
-
- return;
- }
-
while (!shutdown_event->peek()) {
std::vector sample_buffer;
sample_buffer.resize(samples_per_frame);
@@ -215,14 +223,15 @@ namespace audio {
case platf::capture_e::timeout:
continue;
case platf::capture_e::reinit:
+ BOOST_LOG(info) << "Reinitializing audio capture"sv;
mic.reset();
- mic = control->microphone(stream->mapping, stream->channelCount, stream->sampleRate, frame_size);
- if (!mic) {
- BOOST_LOG(error) << "Couldn't re-initialize audio input"sv;
-
- return;
- }
- return;
+ do {
+ mic = control->microphone(stream->mapping, stream->channelCount, stream->sampleRate, frame_size);
+ if (!mic) {
+ BOOST_LOG(warning) << "Couldn't re-initialize audio input"sv;
+ }
+ } while (!mic && !shutdown_event->view(5s));
+ continue;
default:
return;
}
@@ -280,7 +289,8 @@ namespace audio {
return;
}
- const std::string &sink = config::audio.sink.empty() ? ctx.sink.host : config::audio.sink;
+ // Change back to the host sink, unless there was none
+ const std::string &sink = ctx.sink.host.empty() ? config::audio.sink : ctx.sink.host;
if (!sink.empty()) {
// Best effort, it's allowed to fail
ctx.control->set_sink(sink);
diff --git a/src/audio.h b/src/audio.h
index 09a99b03321..fe22c94611d 100644
--- a/src/audio.h
+++ b/src/audio.h
@@ -1,5 +1,8 @@
-#ifndef SUNSHINE_AUDIO_H
-#define SUNSHINE_AUDIO_H
+/**
+ * @file src/audio.h
+ * @brief todo
+ */
+#pragma once
#include "thread_safe.h"
#include "utility.h"
@@ -44,5 +47,3 @@ namespace audio {
void
capture(safe::mail_t mail, config_t config, void *channel_data);
} // namespace audio
-
-#endif
diff --git a/src/cbs.cpp b/src/cbs.cpp
index eedaaff0877..52a3ed93f5e 100644
--- a/src/cbs.cpp
+++ b/src/cbs.cpp
@@ -1,3 +1,7 @@
+/**
+ * @file src/cbs.cpp
+ * @brief todo
+ */
extern "C" {
#include
#include
@@ -50,7 +54,7 @@ namespace cbs {
};
util::buffer_t
- write(const cbs::ctx_t &cbs_ctx, std::uint8_t nal, void *uh, AVCodecID codec_id) {
+ write(cbs::ctx_t &cbs_ctx, std::uint8_t nal, void *uh, AVCodecID codec_id) {
cbs::frag_t frag;
auto err = ff_cbs_insert_unit_content(&frag, -1, nal, uh, nullptr);
if (err < 0) {
@@ -87,9 +91,9 @@ namespace cbs {
make_sps_h264(const AVCodecContext *ctx) {
H264RawSPS sps {};
- /* b_per_p == ctx->max_b_frames for h264 */
- /* desired_b_depth == avoption("b_depth") == 1 */
- /* max_b_depth == std::min(av_log2(ctx->b_per_p) + 1, desired_b_depth) ==> 1 */
+ // b_per_p == ctx->max_b_frames for h264
+ // desired_b_depth == avoption("b_depth") == 1
+ // max_b_depth == std::min(av_log2(ctx->b_per_p) + 1, desired_b_depth) ==> 1
auto max_b_depth = 1;
auto dpb_frame = ctx->gop_size == 1 ? 0 : 1 + max_b_depth;
auto mb_width = (FFALIGN(ctx->width, 16) / 16) * 16;
diff --git a/src/cbs.h b/src/cbs.h
index fe532b985c8..575f1e40720 100644
--- a/src/cbs.h
+++ b/src/cbs.h
@@ -1,5 +1,8 @@
-#ifndef SUNSHINE_CBS_H
-#define SUNSHINE_CBS_H
+/**
+ * @file src/cbs.h
+ * @brief todo
+ */
+#pragma once
#include "utility.h"
@@ -28,10 +31,8 @@ namespace cbs {
make_sps_h264(const AVCodecContext *ctx, const AVPacket *packet);
/**
- * Check if SPS->VUI is present
- */
+ * Check if SPS->VUI is present
+ */
bool
validate_sps(const AVPacket *packet, int codec_id);
} // namespace cbs
-
-#endif
diff --git a/src/config.cpp b/src/config.cpp
index 2f16cbffce8..6e4c93e37f5 100644
--- a/src/config.cpp
+++ b/src/config.cpp
@@ -1,3 +1,7 @@
+/**
+ * @file src/config.cpp
+ * @brief todo
+ */
#include
#include
#include
@@ -16,6 +20,10 @@
#include "platform/common.h"
+#ifdef _WIN32
+ #include
+#endif
+
namespace fs = std::filesystem;
using namespace std::literals;
@@ -374,7 +382,11 @@ namespace config {
true // dwmflush
};
- audio_t audio {};
+ audio_t audio {
+ {}, // audio_sink
+ {}, // virtual_sink
+ true, // install_steam_drivers
+ };
stream_t stream {
10s, // ping_timeout
@@ -402,9 +414,9 @@ namespace config {
"1280x720"s,
"1920x1080"s,
"2560x1080"s,
- "3440x1440"s
+ "3440x1440"s,
"1920x1200"s,
- "3860x2160"s,
+ "3840x2160"s,
"3840x1600"s,
}, // supported resolutions
@@ -417,7 +429,7 @@ namespace config {
{ 0x11, 0xA2 },
{ 0x12, 0xA4 },
},
- 2s, // back_button_timeout
+ -1ms, // back_button_timeout
500ms, // key_repeat_delay
std::chrono::duration { 1 / 24.9 }, // key_repeat_period
@@ -429,6 +441,7 @@ namespace config {
true, // keyboard enabled
true, // mouse enabled
true, // controller enabled
+ true, // always send scancodes
};
sunshine_t sunshine {
@@ -816,12 +829,11 @@ namespace config {
boost::property_tree::read_json(jsonStream, jsonTree);
for (auto &[_, prep_cmd] : jsonTree.get_child("prep_cmd"s)) {
- auto do_cmd = prep_cmd.get("do"s);
- auto undo_cmd = prep_cmd.get("undo"s);
+ auto do_cmd = prep_cmd.get_optional("do"s);
+ auto undo_cmd = prep_cmd.get_optional("undo"s);
+ auto elevated = prep_cmd.get_optional("elevated"s);
- input.emplace_back(
- std::move(do_cmd),
- std::move(undo_cmd));
+ input.emplace_back(do_cmd.value_or(""), undo_cmd.value_or(""), elevated.value_or(false));
}
}
@@ -975,6 +987,7 @@ namespace config {
string_f(vars, "audio_sink", audio.sink);
string_f(vars, "virtual_sink", audio.virtual_sink);
+ bool_f(vars, "install_steam_audio_drivers", audio.install_steam_drivers);
string_restricted_f(vars, "origin_pin_allowed", nvhttp.origin_pin_allowed, { "pc"sv, "lan"sv, "wan"sv });
string_restricted_f(vars, "origin_web_ui_allowed", nvhttp.origin_web_ui_allowed, { "pc"sv, "lan"sv, "wan"sv });
@@ -1027,6 +1040,8 @@ namespace config {
bool_f(vars, "keyboard", input.keyboard);
bool_f(vars, "controller", input.controller);
+ bool_f(vars, "always_send_scancodes", input.always_send_scancodes);
+
int port = sunshine.port;
int_f(vars, "port"s, port);
sunshine.port = (std::uint16_t) port;
@@ -1089,6 +1104,10 @@ namespace config {
int
parse(int argc, char *argv[]) {
std::unordered_map cmd_vars;
+#ifdef _WIN32
+ bool shortcut_launch = false;
+ bool service_admin_launch = false;
+#endif
for (auto x = 1; x < argc; ++x) {
auto line = argv[x];
@@ -1097,6 +1116,14 @@ namespace config {
print_help(*argv);
return 1;
}
+#ifdef _WIN32
+ else if (line == "--shortcut"sv) {
+ shortcut_launch = true;
+ }
+ else if (line == "--shortcut-admin"sv) {
+ service_admin_launch = true;
+ }
+#endif
else if (*line == '-') {
if (*(line + 1) == '-') {
sunshine.cmd.name = line + 2;
@@ -1136,23 +1163,90 @@ namespace config {
}
}
- // create appdata folder if it does not exist
- if (!boost::filesystem::exists(platf::appdata().string())) {
- boost::filesystem::create_directory(platf::appdata().string());
+ bool config_loaded = false;
+ try {
+ // Create appdata folder if it does not exist
+ if (!boost::filesystem::exists(platf::appdata().string())) {
+ boost::filesystem::create_directories(platf::appdata().string());
+ }
+
+ // Create empty config file if it does not exist
+ if (!fs::exists(sunshine.config_file)) {
+ std::ofstream { sunshine.config_file };
+ }
+
+ // Read config file
+ auto vars = parse_config(read_file(sunshine.config_file.c_str()));
+
+ for (auto &[name, value] : cmd_vars) {
+ vars.insert_or_assign(std::move(name), std::move(value));
+ }
+
+ // Apply the config. Note: This will try to create any paths
+ // referenced in the config, so we may receive exceptions if
+ // the path is incorrect or inaccessible.
+ apply_config(std::move(vars));
+ config_loaded = true;
+ }
+ catch (const std::filesystem::filesystem_error &err) {
+ BOOST_LOG(fatal) << "Failed to apply config: "sv << err.what();
+ }
+ catch (const boost::filesystem::filesystem_error &err) {
+ BOOST_LOG(fatal) << "Failed to apply config: "sv << err.what();
}
- // create config file if it does not exist
- if (!fs::exists(sunshine.config_file)) {
- std::ofstream { sunshine.config_file }; // create empty config file
+ if (!config_loaded) {
+#ifdef _WIN32
+ BOOST_LOG(fatal) << "To relaunch Sunshine successfully, use the shortcut in the Start Menu. Do not run Sunshine.exe manually."sv;
+ std::this_thread::sleep_for(10s);
+#endif
+ return -1;
}
- auto vars = parse_config(read_file(sunshine.config_file.c_str()));
+#ifdef _WIN32
+ // We have to wait until the config is loaded to handle these launches,
+ // because we need to have the correct base port loaded in our config.
+ if (service_admin_launch) {
+ // This is a relaunch as admin to start the service
+ service_ctrl::start_service();
- for (auto &[name, value] : cmd_vars) {
- vars.insert_or_assign(std::move(name), std::move(value));
+ // Always return 1 to ensure Sunshine doesn't start normally
+ return 1;
}
+ else if (shortcut_launch) {
+ if (!service_ctrl::is_service_running()) {
+ // If the service isn't running, relaunch ourselves as admin to start it
+ WCHAR executable[MAX_PATH];
+ GetModuleFileNameW(NULL, executable, ARRAYSIZE(executable));
+
+ SHELLEXECUTEINFOW shell_exec_info {};
+ shell_exec_info.cbSize = sizeof(shell_exec_info);
+ shell_exec_info.fMask = SEE_MASK_NOASYNC | SEE_MASK_NO_CONSOLE | SEE_MASK_NOCLOSEPROCESS;
+ shell_exec_info.lpVerb = L"runas";
+ shell_exec_info.lpFile = executable;
+ shell_exec_info.lpParameters = L"--shortcut-admin";
+ shell_exec_info.nShow = SW_NORMAL;
+ if (!ShellExecuteExW(&shell_exec_info)) {
+ auto winerr = GetLastError();
+ std::cout << "Error: ShellExecuteEx() failed:"sv << winerr << std::endl;
+ return 1;
+ }
- apply_config(std::move(vars));
+ // Wait for the elevated process to finish starting the service
+ WaitForSingleObject(shell_exec_info.hProcess, INFINITE);
+ CloseHandle(shell_exec_info.hProcess);
+
+ // Wait for the UI to be ready for connections
+ service_ctrl::wait_for_ui_ready();
+ }
+
+ // Launch the web UI
+ launch_ui();
+
+ // Always return 1 to ensure Sunshine doesn't start normally
+ return 1;
+ }
+#endif
return 0;
}
diff --git a/src/config.h b/src/config.h
index fc77748bdde..2c32e7afd1b 100644
--- a/src/config.h
+++ b/src/config.h
@@ -1,5 +1,8 @@
-#ifndef SUNSHINE_CONFIG_H
-#define SUNSHINE_CONFIG_H
+/**
+ * @file src/config.h
+ * @brief todo
+ */
+#pragma once
#include
#include
@@ -62,6 +65,7 @@ namespace config {
struct audio_t {
std::string sink;
std::string virtual_sink;
+ bool install_steam_drivers;
};
struct stream_t {
@@ -105,6 +109,8 @@ namespace config {
bool keyboard;
bool mouse;
bool controller;
+
+ bool always_send_scancodes;
};
namespace flag {
@@ -119,14 +125,14 @@ namespace config {
}
struct prep_cmd_t {
- prep_cmd_t(std::string &&do_cmd, std::string &&undo_cmd):
- do_cmd(std::move(do_cmd)), undo_cmd(std::move(undo_cmd)) {}
- explicit prep_cmd_t(std::string &&do_cmd):
- do_cmd(std::move(do_cmd)) {}
+ prep_cmd_t(std::string &&do_cmd, std::string &&undo_cmd, bool &&elevated):
+ do_cmd(std::move(do_cmd)), undo_cmd(std::move(undo_cmd)), elevated(std::move(elevated)) {}
+ explicit prep_cmd_t(std::string &&do_cmd, bool &&elevated):
+ do_cmd(std::move(do_cmd)), elevated(std::move(elevated)) {}
std::string do_cmd;
std::string undo_cmd;
+ bool elevated;
};
-
struct sunshine_t {
int min_log_level;
std::bitset flags;
@@ -162,4 +168,3 @@ namespace config {
std::unordered_map
parse_config(const std::string_view &file_content);
} // namespace config
-#endif
diff --git a/src/confighttp.cpp b/src/confighttp.cpp
index c7a2a8b3480..6e8b2393730 100644
--- a/src/confighttp.cpp
+++ b/src/confighttp.cpp
@@ -1,5 +1,9 @@
-// Created by TheElixZammuto on 2021-05-09.
-// TODO: Authentication, better handling of routes common to nvhttp, cleanup
+/**
+ * @file src/confighttp.cpp
+ * @brief todo
+ *
+ * @todo Authentication, better handling of routes common to nvhttp, cleanup
+ */
#define BOOST_BIND_GLOBAL_PLACEHOLDERS
@@ -128,7 +132,7 @@ namespace confighttp {
auto password = authData.substr(index + 1);
auto hash = util::hex(crypto::hash(password + config::sunshine.salt)).to_string();
- if (username != config::sunshine.username || hash != config::sunshine.password) {
+ if (!boost::iequals(username, config::sunshine.username) || hash != config::sunshine.password) {
return false;
}
@@ -159,7 +163,9 @@ namespace confighttp {
std::string header = read_file(WEB_DIR "header.html");
std::string content = read_file(WEB_DIR "index.html");
- response->write(header + content);
+ SimpleWeb::CaseInsensitiveMultimap headers;
+ headers.emplace("Content-Type", "text/html; charset=utf-8");
+ response->write(header + content, headers);
}
void
@@ -170,7 +176,9 @@ namespace confighttp {
std::string header = read_file(WEB_DIR "header.html");
std::string content = read_file(WEB_DIR "pin.html");
- response->write(header + content);
+ SimpleWeb::CaseInsensitiveMultimap headers;
+ headers.emplace("Content-Type", "text/html; charset=utf-8");
+ response->write(header + content, headers);
}
void
@@ -179,11 +187,11 @@ namespace confighttp {
print_req(request);
- SimpleWeb::CaseInsensitiveMultimap headers;
- headers.emplace("Access-Control-Allow-Origin", "https://images.igdb.com/");
-
std::string header = read_file(WEB_DIR "header.html");
std::string content = 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/");
response->write(header + content, headers);
}
@@ -195,7 +203,9 @@ namespace confighttp {
std::string header = read_file(WEB_DIR "header.html");
std::string content = read_file(WEB_DIR "clients.html");
- response->write(header + content);
+ SimpleWeb::CaseInsensitiveMultimap headers;
+ headers.emplace("Content-Type", "text/html; charset=utf-8");
+ response->write(header + content, headers);
}
void
@@ -206,7 +216,9 @@ namespace confighttp {
std::string header = read_file(WEB_DIR "header.html");
std::string content = read_file(WEB_DIR "config.html");
- response->write(header + content);
+ SimpleWeb::CaseInsensitiveMultimap headers;
+ headers.emplace("Content-Type", "text/html; charset=utf-8");
+ response->write(header + content, headers);
}
void
@@ -217,7 +229,9 @@ namespace confighttp {
std::string header = read_file(WEB_DIR "header.html");
std::string content = read_file(WEB_DIR "password.html");
- response->write(header + content);
+ SimpleWeb::CaseInsensitiveMultimap headers;
+ headers.emplace("Content-Type", "text/html; charset=utf-8");
+ response->write(header + content, headers);
}
void
@@ -229,7 +243,9 @@ namespace confighttp {
}
std::string header = read_file(WEB_DIR "header-no-nav.html");
std::string content = read_file(WEB_DIR "welcome.html");
- response->write(header + content);
+ SimpleWeb::CaseInsensitiveMultimap headers;
+ headers.emplace("Content-Type", "text/html; charset=utf-8");
+ response->write(header + content, headers);
}
void
@@ -240,7 +256,9 @@ namespace confighttp {
std::string header = read_file(WEB_DIR "header.html");
std::string content = read_file(WEB_DIR "troubleshooting.html");
- response->write(header + content);
+ SimpleWeb::CaseInsensitiveMultimap headers;
+ headers.emplace("Content-Type", "text/html; charset=utf-8");
+ response->write(header + content, headers);
}
void
@@ -314,7 +332,9 @@ namespace confighttp {
print_req(request);
std::string content = read_file(config::stream.file_apps.c_str());
- response->write(content);
+ SimpleWeb::CaseInsensitiveMultimap headers;
+ headers.emplace("Content-Type", "application/json");
+ response->write(content, headers);
}
void
@@ -488,7 +508,7 @@ namespace confighttp {
const std::string coverdir = platf::appdata().string() + "/covers/";
if (!boost::filesystem::exists(coverdir)) {
- boost::filesystem::create_directory(coverdir);
+ boost::filesystem::create_directories(coverdir);
}
std::basic_string path = coverdir + http::url_escape(key) + ".png";
@@ -528,7 +548,6 @@ namespace confighttp {
outputTree.put("status", "true");
outputTree.put("platform", SUNSHINE_PLATFORM);
outputTree.put("version", PROJECT_VER);
- outputTree.put("restart_supported", platf::restart_supported());
auto vars = config::parse_config(read_file(config::sunshine.config_file.c_str()));
@@ -579,30 +598,8 @@ namespace confighttp {
print_req(request);
- std::stringstream ss;
- std::stringstream configStream;
- ss << request->content.rdbuf();
- pt::ptree outputTree;
- auto g = util::fail_guard([&]() {
- std::ostringstream data;
-
- pt::write_json(data, outputTree);
- response->write(data.str());
- });
-
- if (!platf::restart_supported()) {
- outputTree.put("status", false);
- outputTree.put("error", "Restart is not currently supported on this platform");
- return;
- }
-
- if (!platf::restart()) {
- outputTree.put("status", false);
- outputTree.put("error", "Restart failed");
- return;
- }
-
- outputTree.put("status", true);
+ // We may not return from this call
+ platf::restart();
}
void
@@ -638,7 +635,7 @@ namespace confighttp {
}
else {
auto hash = util::hex(crypto::hash(password + config::sunshine.salt)).to_string();
- if (config::sunshine.username.empty() || (username == config::sunshine.username && hash == config::sunshine.password)) {
+ if (config::sunshine.username.empty() || (boost::iequals(username, config::sunshine.username) && hash == config::sunshine.password)) {
if (newPassword.empty() || newPassword != confirmPassword) {
outputTree.put("status", false);
outputTree.put("error", "Password Mismatch");
diff --git a/src/confighttp.h b/src/confighttp.h
index f5fe158af19..471ed9eba07 100644
--- a/src/confighttp.h
+++ b/src/confighttp.h
@@ -1,7 +1,8 @@
-// Created by loki on 6/3/19.
-
-#ifndef SUNSHINE_CONFIGHTTP_H
-#define SUNSHINE_CONFIGHTTP_H
+/**
+ * @file src/confighttp.h
+ * @brief todo
+ */
+#pragma once
#include
#include
@@ -34,5 +35,3 @@ const std::map mime_types = {
{ "woff2", "font/woff2" },
{ "xml", "text/xml" },
};
-
-#endif // SUNSHINE_CONFIGHTTP_H
diff --git a/src/crypto.cpp b/src/crypto.cpp
index f50c536cbab..5dec0f8dd57 100644
--- a/src/crypto.cpp
+++ b/src/crypto.cpp
@@ -1,5 +1,7 @@
-// Created by loki on 5/31/19.
-
+/**
+ * @file src/crypto.cpp
+ * @brief todo
+ */
#include "crypto.h"
#include
@@ -35,13 +37,13 @@ namespace crypto {
}
}
- /*
- * When certificates from two or more instances of Moonlight have been added to x509_store_t,
- * only one of them will be verified by X509_verify_cert, resulting in only a single instance of
- * Moonlight to be able to use Sunshine
- *
- * To circumvent this, x509_store_t instance will be created for each instance of the certificates.
- */
+ /**
+ * When certificates from two or more instances of Moonlight have been added to x509_store_t,
+ * only one of them will be verified by X509_verify_cert, resulting in only a single instance of
+ * Moonlight to be able to use Sunshine
+ *
+ * To circumvent this, x509_store_t instance will be created for each instance of the certificates.
+ */
const char *
cert_chain_t::verify(x509_t::element_type *cert) {
int err_code = 0;
@@ -399,7 +401,7 @@ namespace crypto {
sign(const pkey_t &pkey, const std::string_view &data, const EVP_MD *md) {
md_ctx_t ctx { EVP_MD_CTX_create() };
- if (EVP_DigestSignInit(ctx.get(), nullptr, md, nullptr, pkey.get()) != 1) {
+ if (EVP_DigestSignInit(ctx.get(), nullptr, md, nullptr, (EVP_PKEY *) pkey.get()) != 1) {
return {};
}
@@ -472,7 +474,7 @@ namespace crypto {
bool
verify(const x509_t &x509, const std::string_view &data, const std::string_view &signature, const EVP_MD *md) {
- auto pkey = X509_get_pubkey(x509.get());
+ auto pkey = X509_get0_pubkey(x509.get());
md_ctx_t ctx { EVP_MD_CTX_create() };
diff --git a/src/crypto.h b/src/crypto.h
index 2de5bafaf4a..d8d0a35a607 100644
--- a/src/crypto.h
+++ b/src/crypto.h
@@ -1,7 +1,8 @@
-// Created by loki on 6/1/19.
-
-#ifndef SUNSHINE_CRYPTO_H
-#define SUNSHINE_CRYPTO_H
+/**
+ * @file src/crypto.h
+ * @brief todo
+ */
+#pragma once
#include
#include
@@ -123,11 +124,11 @@ namespace crypto {
gcm_t(const crypto::aes_t &key, bool padding = true);
/**
- * length of cipher must be at least: round_to_pkcs7_padded(plaintext.size()) + crypto::cipher::tag_size
- *
- * return -1 on error
- * return bytes written on success
- */
+ * length of cipher must be at least: round_to_pkcs7_padded(plaintext.size()) + crypto::cipher::tag_size
+ *
+ * return -1 on error
+ * return bytes written on success
+ */
int
encrypt(const std::string_view &plaintext, std::uint8_t *tagged_cipher, aes_t *iv);
@@ -145,15 +146,13 @@ namespace crypto {
cbc_t(const crypto::aes_t &key, bool padding = true);
/**
- * length of cipher must be at least: round_to_pkcs7_padded(plaintext.size())
- *
- * return -1 on error
- * return bytes written on success
- */
+ * length of cipher must be at least: round_to_pkcs7_padded(plaintext.size())
+ *
+ * return -1 on error
+ * return bytes written on success
+ */
int
encrypt(const std::string_view &plaintext, std::uint8_t *cipher, aes_t *iv);
};
} // namespace cipher
} // namespace crypto
-
-#endif //SUNSHINE_CRYPTO_H
diff --git a/src/httpcommon.cpp b/src/httpcommon.cpp
index 24810f51663..2cfbfe0dd42 100644
--- a/src/httpcommon.cpp
+++ b/src/httpcommon.cpp
@@ -1,3 +1,7 @@
+/**
+ * @file src/httpcommon.cpp
+ * @brief todo
+ */
#define BOOST_BIND_GLOBAL_PLACEHOLDERS
#include "process.h"
@@ -89,7 +93,7 @@ namespace http {
pt::write_json(file, outputTree);
}
catch (std::exception &e) {
- BOOST_LOG(error) << "generating user credentials: "sv << e.what();
+ BOOST_LOG(error) << "error writing to the credentials file, perhaps try this again as an administrator? Details: "sv << e.what();
return -1;
}
diff --git a/src/httpcommon.h b/src/httpcommon.h
index 4deecd143f6..02d42d265fa 100644
--- a/src/httpcommon.h
+++ b/src/httpcommon.h
@@ -1,3 +1,9 @@
+/**
+ * @file src/httpcommon.h
+ * @brief todo
+ */
+#pragma once
+
#include "network.h"
#include "thread_safe.h"
diff --git a/src/input.cpp b/src/input.cpp
index b0fc2791fa9..a9a38e2d91c 100644
--- a/src/input.cpp
+++ b/src/input.cpp
@@ -1,5 +1,7 @@
-// Created by loki on 6/20/19.
-
+/**
+ * @file src/input.cpp
+ * @brief todo
+ */
// define uint32_t for
#include
extern "C" {
@@ -17,6 +19,9 @@ extern "C" {
#include "thread_pool.h"
#include "utility.h"
+#include
+#include
+
using namespace std::literals;
namespace input {
@@ -59,8 +64,22 @@ namespace input {
gamepad_mask[id] = false;
}
+ typedef uint32_t key_press_id_t;
+ key_press_id_t
+ make_kpid(uint16_t vk, uint8_t flags) {
+ return (key_press_id_t) vk << 8 | flags;
+ }
+ uint16_t
+ vk_from_kpid(key_press_id_t kpid) {
+ return kpid >> 8;
+ }
+ uint8_t
+ flags_from_kpid(key_press_id_t kpid) {
+ return kpid & 0xFF;
+ }
+
static task_pool_util::TaskPool::task_id_t key_press_repeat_id {};
- static std::unordered_map key_press {};
+ static std::unordered_map key_press {};
static std::array mouse_press {};
static platf::input_t platf_input;
@@ -116,7 +135,7 @@ namespace input {
touch_port_event { std::move(touch_port_event) },
rumble_queue { std::move(rumble_queue) },
mouse_left_button_timeout {},
- touch_port { 0, 0, 0, 0, 0, 0, 1.0f } {}
+ touch_port { { 0, 0, 0, 0 }, 0, 0, 1.0f } {}
// Keep track of alt+ctrl+shift key combo
int shortcutFlags;
@@ -133,12 +152,12 @@ namespace input {
};
/**
- * Apply shortcut based on VKEY
- * On success
- * return > 0
- * On nothing
- * return 0
- */
+ * Apply shortcut based on VKEY
+ * On success
+ * return > 0
+ * On nothing
+ * return 0
+ */
inline int
apply_shortcut(short keyCode) {
constexpr auto VK_F1 = 0x70;
@@ -352,21 +371,20 @@ namespace input {
mouse_press[button] = !release;
}
- ///////////////////////////////////
- /*/
- * When Moonlight sends mouse input through absolute coordinates,
- * it's possible that BUTTON_RIGHT is pressed down immediately after releasing BUTTON_LEFT.
- * As a result, Sunshine will left-click on hyperlinks in the browser before right-clicking
- *
- * This can be solved by delaying BUTTON_LEFT, however, any delay on input is undesirable during gaming
- * As a compromise, Sunshine will only put delays on BUTTON_LEFT when
- * absolute mouse coordinates have been sent.
- *
- * Try to make sure BUTTON_RIGHT gets called before BUTTON_LEFT is released.
- *
- * input->mouse_left_button_timeout can only be nullptr
- * when the last mouse coordinates were absolute
- /*/
+ /**
+ * When Moonlight sends mouse input through absolute coordinates,
+ * it's possible that BUTTON_RIGHT is pressed down immediately after releasing BUTTON_LEFT.
+ * As a result, Sunshine will left-click on hyperlinks in the browser before right-clicking
+ *
+ * This can be solved by delaying BUTTON_LEFT, however, any delay on input is undesirable during gaming
+ * As a compromise, Sunshine will only put delays on BUTTON_LEFT when
+ * absolute mouse coordinates have been sent.
+ *
+ * Try to make sure BUTTON_RIGHT gets called before BUTTON_LEFT is released.
+ *
+ * input->mouse_left_button_timeout can only be nullptr
+ * when the last mouse coordinates were absolute
+ */
if (button == BUTTON_LEFT && release && !input->mouse_left_button_timeout) {
auto f = [=]() {
auto left_released = mouse_press[BUTTON_LEFT];
@@ -394,7 +412,6 @@ namespace input {
return;
}
- ///////////////////////////////////
platf::button_mouse(platf_input, button, release);
}
@@ -410,8 +427,8 @@ namespace input {
}
/**
- * Update flags for keyboard shortcut combo's
- */
+ * Update flags for keyboard shortcut combo's
+ */
inline void
update_shortcutFlags(int *flags, short keyCode, bool release) {
switch (keyCode) {
@@ -448,17 +465,66 @@ namespace input {
}
}
+ bool
+ is_modifier(uint16_t keyCode) {
+ switch (keyCode) {
+ case VKEY_SHIFT:
+ case VKEY_LSHIFT:
+ case VKEY_RSHIFT:
+ case VKEY_CONTROL:
+ case VKEY_LCONTROL:
+ case VKEY_RCONTROL:
+ case VKEY_MENU:
+ case VKEY_LMENU:
+ case VKEY_RMENU:
+ return true;
+ default:
+ return false;
+ }
+ }
+
void
- repeat_key(short key_code) {
+ send_key_and_modifiers(uint16_t key_code, bool release, uint8_t flags, uint8_t synthetic_modifiers) {
+ if (!release) {
+ // Press any synthetic modifiers required for this key
+ if (synthetic_modifiers & MODIFIER_SHIFT) {
+ platf::keyboard(platf_input, VKEY_SHIFT, false, flags);
+ }
+ if (synthetic_modifiers & MODIFIER_CTRL) {
+ platf::keyboard(platf_input, VKEY_CONTROL, false, flags);
+ }
+ if (synthetic_modifiers & MODIFIER_ALT) {
+ platf::keyboard(platf_input, VKEY_MENU, false, flags);
+ }
+ }
+
+ platf::keyboard(platf_input, map_keycode(key_code), release, flags);
+
+ if (!release) {
+ // Raise any synthetic modifier keys we pressed
+ if (synthetic_modifiers & MODIFIER_SHIFT) {
+ platf::keyboard(platf_input, VKEY_SHIFT, true, flags);
+ }
+ if (synthetic_modifiers & MODIFIER_CTRL) {
+ platf::keyboard(platf_input, VKEY_CONTROL, true, flags);
+ }
+ if (synthetic_modifiers & MODIFIER_ALT) {
+ platf::keyboard(platf_input, VKEY_MENU, true, flags);
+ }
+ }
+ }
+
+ void
+ repeat_key(uint16_t key_code, uint8_t flags, uint8_t synthetic_modifiers) {
// If key no longer pressed, stop repeating
- if (!key_press[key_code]) {
+ if (!key_press[make_kpid(key_code, flags)]) {
key_press_repeat_id = nullptr;
return;
}
- platf::keyboard(platf_input, map_keycode(key_code), false);
+ send_key_and_modifiers(key_code, false, flags, synthetic_modifiers);
- key_press_repeat_id = task_pool.pushDelayed(repeat_key, config::input.key_repeat_period, key_code).task_id;
+ key_press_repeat_id = task_pool.pushDelayed(repeat_key, config::input.key_repeat_period, key_code, flags, synthetic_modifiers).task_id;
}
void
@@ -470,7 +536,22 @@ namespace input {
auto release = util::endian::little(packet->header.magic) == KEY_UP_EVENT_MAGIC;
auto keyCode = packet->keyCode & 0x00FF;
- auto &pressed = key_press[keyCode];
+ // Set synthetic modifier flags if the keyboard packet is requesting modifier
+ // keys that are not current pressed.
+ uint8_t synthetic_modifiers = 0;
+ if (!release && !is_modifier(keyCode)) {
+ if (!(input->shortcutFlags & input_t::SHIFT) && (packet->modifiers & MODIFIER_SHIFT)) {
+ synthetic_modifiers |= MODIFIER_SHIFT;
+ }
+ if (!(input->shortcutFlags & input_t::CTRL) && (packet->modifiers & MODIFIER_CTRL)) {
+ synthetic_modifiers |= MODIFIER_CTRL;
+ }
+ if (!(input->shortcutFlags & input_t::ALT) && (packet->modifiers & MODIFIER_ALT)) {
+ synthetic_modifiers |= MODIFIER_ALT;
+ }
+ }
+
+ auto &pressed = key_press[make_kpid(keyCode, packet->flags)];
if (!pressed) {
if (!release) {
// A new key has been pressed down, we need to check for key combo's
@@ -484,7 +565,7 @@ namespace input {
}
if (config::input.key_repeat_delay.count() > 0) {
- key_press_repeat_id = task_pool.pushDelayed(repeat_key, config::input.key_repeat_delay, keyCode).task_id;
+ key_press_repeat_id = task_pool.pushDelayed(repeat_key, config::input.key_repeat_delay, keyCode, packet->flags, synthetic_modifiers).task_id;
}
}
else {
@@ -499,8 +580,9 @@ namespace input {
pressed = !release;
+ send_key_and_modifiers(keyCode, release, packet->flags, synthetic_modifiers);
+
update_shortcutFlags(&input->shortcutFlags, map_keycode(keyCode), release);
- platf::keyboard(platf_input, map_keycode(keyCode), release);
}
void
@@ -655,6 +737,9 @@ namespace input {
state.buttonFlags |= platf::HOME;
platf::gamepad(platf_input, gamepad.id, state);
+ // Sleep for a short time to allow the input to be detected
+ boost::this_thread::sleep_for(boost::chrono::milliseconds(100));
+
// Release Home button
state.buttonFlags &= ~platf::HOME;
platf::gamepad(platf_input, gamepad.id, state);
@@ -731,7 +816,7 @@ namespace input {
}
for (auto &kp : key_press) {
- platf::keyboard(platf_input, kp.first & 0x00FF, true);
+ platf::keyboard(platf_input, vk_from_kpid(kp.first) & 0x00FF, true, flags_from_kpid(kp.first));
key_press[kp.first] = false;
}
});
diff --git a/src/input.h b/src/input.h
index 2001fb9e21f..095c20ee717 100644
--- a/src/input.h
+++ b/src/input.h
@@ -1,7 +1,8 @@
-// Created by loki on 6/20/19.
-
-#ifndef SUNSHINE_INPUT_H
-#define SUNSHINE_INPUT_H
+/**
+ * @file src/input.h
+ * @brief todo
+ */
+#pragma once
#include
@@ -33,5 +34,3 @@ namespace input {
float scalar_inv;
};
} // namespace input
-
-#endif // SUNSHINE_INPUT_H
diff --git a/src/main.cpp b/src/main.cpp
index 941dcfc194c..b347cc0ae5a 100644
--- a/src/main.cpp
+++ b/src/main.cpp
@@ -1,5 +1,6 @@
/**
- * @file main.cpp
+ * @file src/main.cpp
+ * @brief Main entry point for Sunshine.
*/
// standard includes
@@ -34,6 +35,10 @@
extern "C" {
#include
#include
+
+#ifdef _WIN32
+ #include
+#endif
}
safe::mail_t mail::man;
@@ -107,6 +112,263 @@ namespace version {
}
} // namespace version
+namespace lifetime {
+ static char **argv;
+ static std::atomic_int desired_exit_code;
+
+ /**
+ * @brief Terminates Sunshine gracefully with the provided exit code.
+ * @param exit_code The exit code to return from main().
+ * @param async Specifies whether our termination will be non-blocking.
+ */
+ void
+ exit_sunshine(int exit_code, bool async) {
+ // Store the exit code of the first exit_sunshine() call
+ int zero = 0;
+ desired_exit_code.compare_exchange_strong(zero, exit_code);
+
+ // Raise SIGINT to start termination
+ std::raise(SIGINT);
+
+ // Termination will happen asynchronously, but the caller may
+ // have wanted synchronous behavior.
+ while (!async) {
+ std::this_thread::sleep_for(1s);
+ }
+ }
+
+ /**
+ * @brief Gets the argv array passed to main().
+ */
+ char **
+ get_argv() {
+ return argv;
+ }
+} // namespace lifetime
+
+#ifdef _WIN32
+namespace service_ctrl {
+ class service_controller {
+ public:
+ /**
+ * @brief Constructor for service_controller class.
+ * @param service_desired_access SERVICE_* desired access flags.
+ */
+ service_controller(DWORD service_desired_access) {
+ scm_handle = OpenSCManagerA(nullptr, nullptr, SC_MANAGER_CONNECT);
+ if (!scm_handle) {
+ auto winerr = GetLastError();
+ BOOST_LOG(error) << "OpenSCManager() failed: "sv << winerr;
+ return;
+ }
+
+ service_handle = OpenServiceA(scm_handle, "SunshineService", service_desired_access);
+ if (!service_handle) {
+ auto winerr = GetLastError();
+ BOOST_LOG(error) << "OpenService() failed: "sv << winerr;
+ return;
+ }
+ }
+
+ ~service_controller() {
+ if (service_handle) {
+ CloseServiceHandle(service_handle);
+ }
+
+ if (scm_handle) {
+ CloseServiceHandle(scm_handle);
+ }
+ }
+
+ /**
+ * @brief Asynchronously starts the Sunshine service.
+ */
+ bool
+ start_service() {
+ if (!service_handle) {
+ return false;
+ }
+
+ if (!StartServiceA(service_handle, 0, nullptr)) {
+ auto winerr = GetLastError();
+ if (winerr != ERROR_SERVICE_ALREADY_RUNNING) {
+ BOOST_LOG(error) << "StartService() failed: "sv << winerr;
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * @brief Query the service status.
+ * @param status The SERVICE_STATUS struct to populate.
+ */
+ bool
+ query_service_status(SERVICE_STATUS &status) {
+ if (!service_handle) {
+ return false;
+ }
+
+ if (!QueryServiceStatus(service_handle, &status)) {
+ auto winerr = GetLastError();
+ BOOST_LOG(error) << "QueryServiceStatus() failed: "sv << winerr;
+ return false;
+ }
+
+ return true;
+ }
+
+ private:
+ SC_HANDLE scm_handle = NULL;
+ SC_HANDLE service_handle = NULL;
+ };
+
+ /**
+ * @brief Check if the service is running.
+ *
+ * EXAMPLES:
+ * ```cpp
+ * is_service_running();
+ * ```
+ */
+ bool
+ is_service_running() {
+ service_controller sc { SERVICE_QUERY_STATUS };
+
+ SERVICE_STATUS status;
+ if (!sc.query_service_status(status)) {
+ return false;
+ }
+
+ return status.dwCurrentState == SERVICE_RUNNING;
+ }
+
+ /**
+ * @brief Start the service and wait for startup to complete.
+ *
+ * EXAMPLES:
+ * ```cpp
+ * start_service();
+ * ```
+ */
+ bool
+ start_service() {
+ service_controller sc { SERVICE_QUERY_STATUS | SERVICE_START };
+
+ std::cout << "Starting Sunshine..."sv;
+
+ // This operation is asynchronous, so we must wait for it to complete
+ if (!sc.start_service()) {
+ return false;
+ }
+
+ SERVICE_STATUS status;
+ do {
+ Sleep(1000);
+ std::cout << '.';
+ } while (sc.query_service_status(status) && status.dwCurrentState == SERVICE_START_PENDING);
+
+ if (status.dwCurrentState != SERVICE_RUNNING) {
+ BOOST_LOG(error) << SERVICE_NAME " failed to start: "sv << status.dwWin32ExitCode;
+ return false;
+ }
+
+ std::cout << std::endl;
+ return true;
+ }
+
+ /**
+ * @brief Wait for the UI to be ready after Sunshine startup.
+ *
+ * EXAMPLES:
+ * ```cpp
+ * wait_for_ui_ready();
+ * ```
+ */
+ bool
+ wait_for_ui_ready() {
+ std::cout << "Waiting for Web UI to be ready...";
+
+ // Wait up to 30 seconds for the web UI to start
+ for (int i = 0; i < 30; i++) {
+ PMIB_TCPTABLE tcp_table = nullptr;
+ ULONG table_size = 0;
+ ULONG err;
+
+ auto fg = util::fail_guard([&tcp_table]() {
+ free(tcp_table);
+ });
+
+ do {
+ // Query all open TCP sockets to look for our web UI port
+ err = GetTcpTable(tcp_table, &table_size, false);
+ if (err == ERROR_INSUFFICIENT_BUFFER) {
+ free(tcp_table);
+ tcp_table = (PMIB_TCPTABLE) malloc(table_size);
+ }
+ } while (err == ERROR_INSUFFICIENT_BUFFER);
+
+ if (err != NO_ERROR) {
+ BOOST_LOG(error) << "Failed to query TCP table: "sv << err;
+ return false;
+ }
+
+ uint16_t port_nbo = htons(map_port(confighttp::PORT_HTTPS));
+ for (DWORD i = 0; i < tcp_table->dwNumEntries; i++) {
+ auto &entry = tcp_table->table[i];
+
+ // Look for our port in the listening state
+ if (entry.dwLocalPort == port_nbo && entry.dwState == MIB_TCP_STATE_LISTEN) {
+ std::cout << std::endl;
+ return true;
+ }
+ }
+
+ Sleep(1000);
+ std::cout << '.';
+ }
+
+ std::cout << "timed out"sv << std::endl;
+ return false;
+ }
+} // namespace service_ctrl
+
+/**
+ * @brief Checks if NVIDIA's GameStream software is running.
+ * @return `true` if GameStream is enabled.
+ */
+bool
+is_gamestream_enabled() {
+ DWORD enabled;
+ DWORD size = sizeof(enabled);
+ return RegGetValueW(
+ HKEY_LOCAL_MACHINE,
+ L"SOFTWARE\\NVIDIA Corporation\\NvStream",
+ L"EnableStreaming",
+ RRF_RT_REG_DWORD,
+ nullptr,
+ &enabled,
+ &size) == ERROR_SUCCESS &&
+ enabled != 0;
+}
+
+#endif
+
+/**
+ * @brief Launch the Web UI.
+ *
+ * EXAMPLES:
+ * ```cpp
+ * launch_ui();
+ * ```
+ */
+void
+launch_ui() {
+ std::string url = "https://localhost:" + std::to_string(map_port(confighttp::PORT_HTTPS));
+ platf::open_url(url);
+}
+
/**
* @brief Flush the log.
*
@@ -159,13 +421,9 @@ LRESULT CALLBACK
SessionMonitorWindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {
switch (uMsg) {
case WM_ENDSESSION: {
- // Raise a SIGINT to trigger our cleanup logic and terminate ourselves
+ // Terminate ourselves with a blocking exit call
std::cout << "Received WM_ENDSESSION"sv << std::endl;
- std::raise(SIGINT);
-
- // The signal handling is asynchronous, so we will wait here to be terminated.
- // If for some reason we don't terminate in a few seconds, Windows will kill us.
- SuspendThread(GetCurrentThread());
+ lifetime::exit_sunshine(0, false);
return 0;
}
default:
@@ -186,6 +444,8 @@ SessionMonitorWindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {
*/
int
main(int argc, char *argv[]) {
+ lifetime::argv = argv;
+
task_pool_util::TaskPool::task_id_t force_shutdown = nullptr;
#ifdef _WIN32
@@ -354,14 +614,26 @@ main(int argc, char *argv[]) {
BOOST_LOG(error) << "Platform failed to initialize"sv;
}
+ auto proc_deinit_guard = proc::init();
+ if (!proc_deinit_guard) {
+ BOOST_LOG(error) << "Proc failed to initialize"sv;
+ }
+
reed_solomon_init();
auto input_deinit_guard = input::init();
- if (video::init()) {
- BOOST_LOG(error) << "Video failed to initialize"sv;
+ if (video::probe_encoders()) {
+ BOOST_LOG(error) << "Video failed to find working encoder"sv;
}
if (http::init()) {
- BOOST_LOG(error) << "http failed to initialize"sv;
+ BOOST_LOG(fatal) << "HTTP interface failed to initialize"sv;
+
+#ifdef _WIN32
+ BOOST_LOG(fatal) << "To relaunch Sunshine successfully, use the shortcut in the Start Menu. Do not run Sunshine.exe manually."sv;
+ std::this_thread::sleep_for(10s);
+#endif
+
+ return -1;
}
std::unique_ptr mDNS;
@@ -376,12 +648,20 @@ main(int argc, char *argv[]) {
// FIXME: Temporary workaround: Simple-Web_server needs to be updated or replaced
if (shutdown_event->peek()) {
- return 0;
+ return lifetime::desired_exit_code;
}
std::thread httpThread { nvhttp::start };
std::thread configThread { confighttp::start };
+#ifdef _WIN32
+ // If we're using the default port and GameStream is enabled, warn the user
+ if (config::sunshine.port == 47989 && is_gamestream_enabled()) {
+ BOOST_LOG(fatal) << "GameStream is still enabled in GeForce Experience! This *will* cause streaming problems with Sunshine!"sv;
+ BOOST_LOG(fatal) << "Disable GameStream on the SHIELD tab in GeForce Experience or change the Port setting on the Advanced tab in the Sunshine Web UI."sv;
+ }
+#endif
+
rtsp_stream::rtpThread();
httpThread.join();
@@ -395,7 +675,7 @@ main(int argc, char *argv[]) {
system_tray::end_tray();
#endif
- return 0;
+ return lifetime::desired_exit_code;
}
/**
diff --git a/src/main.h b/src/main.h
index aff1cfdd6bb..e98206f3838 100644
--- a/src/main.h
+++ b/src/main.h
@@ -1,10 +1,10 @@
/**
- * @file main.h
+ * @file src/main.h
+ * @brief Main header file for the Sunshine application.
*/
// macros
-#ifndef SUNSHINE_MAIN_H
-#define SUNSHINE_MAIN_H
+#pragma once
// standard includes
#include
@@ -33,20 +33,6 @@ main(int argc, char *argv[]);
void
log_flush();
void
-open_url(const std::string &url);
-void
-tray_open_ui_cb(struct tray_menu *item);
-void
-tray_donate_github_cb(struct tray_menu *item);
-void
-tray_donate_mee6_cb(struct tray_menu *item);
-void
-tray_donate_patreon_cb(struct tray_menu *item);
-void
-tray_donate_paypal_cb(struct tray_menu *item);
-void
-tray_quit_cb(struct tray_menu *item);
-void
print_help(const char *name);
std::string
read_file(const char *path);
@@ -54,6 +40,8 @@ int
write_file(const char *path, const std::string_view &contents);
std::uint16_t
map_port(int port);
+void
+launch_ui();
// namespaces
namespace mail {
@@ -79,4 +67,23 @@ namespace mail {
#undef MAIL
} // namespace mail
-#endif // SUNSHINE_MAIN_H
+
+namespace lifetime {
+ void
+ exit_sunshine(int exit_code, bool async);
+ char **
+ get_argv();
+} // namespace lifetime
+
+#ifdef _WIN32
+namespace service_ctrl {
+ bool
+ is_service_running();
+
+ bool
+ start_service();
+
+ bool
+ wait_for_ui_ready();
+} // namespace service_ctrl
+#endif
diff --git a/src/move_by_copy.h b/src/move_by_copy.h
index 3c2bda1208e..bac1faf98be 100644
--- a/src/move_by_copy.h
+++ b/src/move_by_copy.h
@@ -1,12 +1,15 @@
-#ifndef DOSSIER_MOVE_BY_COPY_H
-#define DOSSIER_MOVE_BY_COPY_H
+/**
+ * @file src/move_by_copy.h
+ * @brief todo
+ */
+#pragma once
#include
namespace move_by_copy_util {
- /*
- * When a copy is made, it moves the object
- * This allows you to move an object when a move can't be done.
- */
+ /**
+ * When a copy is made, it moves the object
+ * This allows you to move an object when a move can't be done.
+ */
template
class MoveByCopy {
public:
@@ -53,4 +56,3 @@ namespace move_by_copy_util {
return MoveByCopy(std::move(const_cast(movable)));
}
} // namespace move_by_copy_util
-#endif
diff --git a/src/network.cpp b/src/network.cpp
index 6549127c524..f65bdfaae89 100644
--- a/src/network.cpp
+++ b/src/network.cpp
@@ -1,5 +1,7 @@
-// Created by loki on 12/27/19.
-
+/**
+ * @file src/network.cpp
+ * @brief todo
+ */
#include "network.h"
#include "utility.h"
#include
@@ -16,7 +18,8 @@ namespace net {
std::vector> lan_ips {
ip_block("192.168.0.0/16"sv),
ip_block("172.16.0.0/12"sv),
- ip_block("10.0.0.0/8"sv)
+ ip_block("10.0.0.0/8"sv),
+ ip_block("100.64.0.0/10"sv)
};
std::uint32_t
diff --git a/src/network.h b/src/network.h
index ffed2fe692b..e1ca36c7531 100644
--- a/src/network.h
+++ b/src/network.h
@@ -1,7 +1,8 @@
-// Created by loki on 12/27/19.
-
-#ifndef SUNSHINE_NETWORK_H
-#define SUNSHINE_NETWORK_H
+/**
+ * @file src/network.h
+ * @brief todo
+ */
+#pragma once
#include
@@ -34,5 +35,3 @@ namespace net {
host_t
host_create(ENetAddress &addr, std::size_t peers, std::uint16_t port);
} // namespace net
-
-#endif // SUNSHINE_NETWORK_H
diff --git a/src/nvhttp.cpp b/src/nvhttp.cpp
index 15fc1f349a6..dfa0b9fef76 100644
--- a/src/nvhttp.cpp
+++ b/src/nvhttp.cpp
@@ -1,6 +1,7 @@
/**
-* @file nvhttp.h
-*/
+ * @file src/nvhttp.h
+ * @brief todo
+ */
// macros
#define BOOST_BIND_GLOBAL_PLACEHOLDERS
@@ -29,6 +30,7 @@
#include "rtsp.h"
#include "utility.h"
#include "uuid.h"
+#include "video.h"
using namespace std::literals;
namespace nvhttp {
@@ -279,6 +281,7 @@ namespace nvhttp {
if (sess.async_insert_pin.salt.size() < 32) {
tree.put("root.paired", 0);
tree.put("root..status_code", 400);
+ tree.put("root..status_message", "Salt too short");
return;
}
@@ -374,14 +377,7 @@ namespace nvhttp {
auto hash = crypto::hash(data);
// if hash not correct, probably MITM
- if (std::memcmp(hash.data(), sess.clienthash.data(), hash.size())) {
- // TODO: log
-
- map_id_sess.erase(client.uniqueID);
- tree.put("root.paired", 0);
- }
-
- if (crypto::verify256(crypto::x509(client.cert), secret, sign)) {
+ if (!std::memcmp(hash.data(), sess.clienthash.data(), hash.size()) && crypto::verify256(crypto::x509(client.cert), secret, sign)) {
tree.put("root.paired", 1);
add_cert->raise(crypto::x509(client.cert));
@@ -470,11 +466,12 @@ namespace nvhttp {
auto args = request->parse_query_string();
if (args.find("uniqueid"s) == std::end(args)) {
tree.put("root..status_code", 400);
+ tree.put("root..status_message", "Missing uniqueid parameter");
return;
}
- auto uniqID { std::move(get_arg(args, "uniqueid")) };
+ auto uniqID { get_arg(args, "uniqueid") };
auto sess_it = map_id_sess.find(uniqID);
args_t::const_iterator it;
@@ -521,19 +518,20 @@ namespace nvhttp {
}
else {
tree.put("root..status_code", 404);
+ tree.put("root..status_message", "Invalid pairing request");
}
}
/**
- * @brief Compare the user supplied pin to the Moonlight pin.
- * @param pin The user supplied pin.
- * @return `true` if the pin is correct, `false` otherwise.
- *
- * EXAMPLES:
- * ```cpp
- * bool pin_status = nvhttp::pin("1234");
- * ```
- */
+ * @brief Compare the user supplied pin to the Moonlight pin.
+ * @param pin The user supplied pin.
+ * @return `true` if the pin is correct, `false` otherwise.
+ *
+ * EXAMPLES:
+ * ```cpp
+ * bool pin_status = nvhttp::pin("1234");
+ * ```
+ */
bool
pin(std::string pin) {
pt::ptree tree;
@@ -621,23 +619,19 @@ namespace nvhttp {
tree.put("root.HttpsPort", map_port(PORT_HTTPS));
tree.put("root.ExternalPort", map_port(PORT_HTTP));
tree.put("root.mac", platf::get_mac_address(local_endpoint.address().to_string()));
- tree.put("root.MaxLumaPixelsHEVC", config::video.hevc_mode > 1 ? "1869449984" : "0");
+ tree.put("root.MaxLumaPixelsHEVC", video::active_hevc_mode > 1 ? "1869449984" : "0");
tree.put("root.LocalIP", local_endpoint.address().to_string());
- if (config::video.hevc_mode == 3) {
+ if (video::active_hevc_mode == 3) {
tree.put("root.ServerCodecModeSupport", "3843");
}
- else if (config::video.hevc_mode == 2) {
+ else if (video::active_hevc_mode == 2) {
tree.put("root.ServerCodecModeSupport", "259");
}
else {
tree.put("root.ServerCodecModeSupport", "3");
}
- if (!config::nvhttp.external_ip.empty()) {
- tree.put("root.ExternalIP", config::nvhttp.external_ip);
- }
-
pt::ptree display_nodes;
for (auto &resolution : config::nvhttp.resolutions) {
auto pred = [](auto ch) { return ch == ' ' || ch == '\t' || ch == 'x'; };
@@ -689,22 +683,6 @@ namespace nvhttp {
response->close_connection_after_response = true;
});
- auto args = request->parse_query_string();
- if (args.find("uniqueid"s) == std::end(args)) {
- tree.put("root..status_code", 400);
-
- return;
- }
-
- auto clientID = get_arg(args, "uniqueid");
-
- auto client = map_id_client.find(clientID);
- if (client == std::end(map_id_client)) {
- tree.put("root..status_code", 501);
-
- return;
- }
-
auto &apps = tree.add_child("root", pt::ptree {});
apps.put(".status_code", 200);
@@ -712,7 +690,7 @@ namespace nvhttp {
for (auto &proc : proc::proc.get_apps()) {
pt::ptree app;
- app.put("IsHdrSupported"s, config::video.hevc_mode == 3 ? 1 : 0);
+ app.put("IsHdrSupported"s, video::active_hevc_mode == 3 ? 1 : 0);
app.put("AppTitle"s, proc.name);
app.put("ID", proc.id);
@@ -736,6 +714,7 @@ namespace nvhttp {
if (rtsp_stream::session_count() == config::stream.channels) {
tree.put("root.resume", 0);
tree.put("root..status_code", 503);
+ tree.put("root..status_message", "The host's concurrent stream limit has been reached. Stop an existing stream or increase the 'Channels' value in the Sunshine Web UI.");
return;
}
@@ -748,6 +727,7 @@ namespace nvhttp {
args.find("appid"s) == std::end(args)) {
tree.put("root.resume", 0);
tree.put("root..status_code", 400);
+ tree.put("root..status_message", "Missing a required launch parameter");
return;
}
@@ -758,14 +738,30 @@ namespace nvhttp {
if (current_appid > 0) {
tree.put("root.resume", 0);
tree.put("root..status_code", 400);
+ tree.put("root..status_message", "An app is already running on this host");
return;
}
+ // Probe encoders again before streaming to ensure our chosen
+ // encoder matches the active GPU (which could have changed
+ // due to hotplugging, driver crash, primary monitor change,
+ // or any number of other factors).
+ if (rtsp_stream::session_count() == 0) {
+ if (video::probe_encoders()) {
+ tree.put("root..status_code", 503);
+ tree.put("root..status_message", "Failed to initialize video capture/encoding. Is a display connected and turned on?");
+ tree.put("root.gamesession", 0);
+
+ return;
+ }
+ }
+
if (appid > 0) {
auto err = proc::proc.execute(appid);
if (err) {
tree.put("root..status_code", err);
+ tree.put("root..status_message", "Failed to start the specified application");
tree.put("root.gamesession", 0);
return;
@@ -798,6 +794,7 @@ namespace nvhttp {
if (rtsp_stream::session_count() == config::stream.channels) {
tree.put("root.resume", 0);
tree.put("root..status_code", 503);
+ tree.put("root..status_message", "The host's concurrent stream limit has been reached. Stop an existing stream or increase the 'Channels' value in the Sunshine Web UI.");
return;
}
@@ -806,6 +803,7 @@ namespace nvhttp {
if (current_appid == 0) {
tree.put("root.resume", 0);
tree.put("root..status_code", 503);
+ tree.put("root..status_message", "No running app to resume");
return;
}
@@ -816,10 +814,32 @@ namespace nvhttp {
args.find("rikeyid"s) == std::end(args)) {
tree.put("root.resume", 0);
tree.put("root..status_code", 400);
+ tree.put("root..status_message", "Missing a required resume parameter");
return;
}
+ if (rtsp_stream::session_count() == 0) {
+ // Probe encoders again before streaming to ensure our chosen
+ // encoder matches the active GPU (which could have changed
+ // due to hotplugging, driver crash, primary monitor change,
+ // or any number of other factors).
+ if (video::probe_encoders()) {
+ tree.put("root.resume", 0);
+ tree.put("root..status_code", 503);
+ tree.put("root..status_message", "Failed to initialize video capture/encoding. Is a display connected and turned on?");
+
+ return;
+ }
+
+ // Newer Moonlight clients send localAudioPlayMode on /resume too,
+ // so we should use it if it's present in the args and there are
+ // no active sessions we could be interfering with.
+ if (args.find("localAudioPlayMode"s) != std::end(args)) {
+ host_audio = util::from_view(get_arg(args, "localAudioPlayMode"));
+ }
+ }
+
rtsp_stream::launch_session_raise(make_launch_session(host_audio, args));
tree.put("root..status_code", 200);
@@ -845,6 +865,7 @@ namespace nvhttp {
if (rtsp_stream::session_count() != 0) {
tree.put("root.resume", 0);
tree.put("root..status_code", 503);
+ tree.put("root..status_message", "All sessions must be disconnected before quitting");
return;
}
@@ -872,13 +893,13 @@ namespace nvhttp {
}
/**
- * @brief Start the nvhttp server.
- *
- * EXAMPLES:
- * ```cpp
- * nvhttp::start();
- * ```
- */
+ * @brief Start the nvhttp server.
+ *
+ * EXAMPLES:
+ * ```cpp
+ * nvhttp::start();
+ * ```
+ */
void
start() {
auto shutdown_event = mail::man->event(mail::shutdown);
@@ -904,7 +925,7 @@ namespace nvhttp {
auto add_cert = std::make_shared>(30);
- // /resume doesn't get the parameter "localAudioPlayMode"
+ // /resume doesn't always get the parameter "localAudioPlayMode"
// /launch will store it in host_audio
bool host_audio {};
@@ -913,7 +934,7 @@ namespace nvhttp {
// Verify certificates after establishing connection
https_server.verify = [&cert_chain, add_cert](SSL *ssl) {
- auto x509 = SSL_get_peer_certificate(ssl);
+ crypto::x509_t x509 { SSL_get_peer_certificate(ssl) };
if (!x509) {
BOOST_LOG(info) << "unknown -- denied"sv;
return 0;
@@ -924,7 +945,7 @@ namespace nvhttp {
auto fg = util::fail_guard([&]() {
char subject_name[256];
- X509_NAME_oneline(X509_get_subject_name(x509), subject_name, sizeof(subject_name));
+ X509_NAME_oneline(X509_get_subject_name(x509.get()), subject_name, sizeof(subject_name));
BOOST_LOG(debug) << subject_name << " -- "sv << (verified ? "verified"sv : "denied"sv);
});
@@ -939,7 +960,7 @@ namespace nvhttp {
cert_chain.add(std::move(cert));
}
- auto err_str = cert_chain.verify(x509);
+ auto err_str = cert_chain.verify(x509.get());
if (err_str) {
BOOST_LOG(warning) << "SSL Verification error :: "sv << err_str;
@@ -1018,13 +1039,13 @@ namespace nvhttp {
}
/**
- * @brief Remove all paired clients.
- *
- * EXAMPLES:
- * ```cpp
- * nvhttp::erase_all_clients();
- * ```
- */
+ * @brief Remove all paired clients.
+ *
+ * EXAMPLES:
+ * ```cpp
+ * nvhttp::erase_all_clients();
+ * ```
+ */
void
erase_all_clients() {
map_id_client.clear();
diff --git a/src/nvhttp.h b/src/nvhttp.h
index eccf9d349e9..3be24b3de06 100644
--- a/src/nvhttp.h
+++ b/src/nvhttp.h
@@ -1,10 +1,10 @@
/**
-* @file nvhttp.h
-*/
+ * @file src/nvhttp.h
+ * @brief todo
+ */
// macros
-#ifndef SUNSHINE_NVHTTP_H
-#define SUNSHINE_NVHTTP_H
+#pragma once
// standard includes
#include
@@ -18,24 +18,25 @@
namespace nvhttp {
/**
- * @brief The protocol version.
- */
+ * @brief The protocol version.
+ * @details The version of the GameStream protocol we are mocking.
+ * @note The negative 4th number indicates to Moonlight that this is Sunshine.
+ */
constexpr auto VERSION = "7.1.431.-1";
- // The negative 4th version number tells Moonlight that this is Sunshine
/**
- * @brief The GFE version we are replicating.
- */
+ * @brief The GFE version we are replicating.
+ */
constexpr auto GFE_VERSION = "3.23.0.74";
/**
- * @brief The HTTP port, as a difference from the config port.
- */
+ * @brief The HTTP port, as a difference from the config port.
+ */
constexpr auto PORT_HTTP = 0;
/**
- * @brief The HTTPS port, as a difference from the config port.
- */
+ * @brief The HTTPS port, as a difference from the config port.
+ */
constexpr auto PORT_HTTPS = -5;
// functions
@@ -46,5 +47,3 @@ namespace nvhttp {
void
erase_all_clients();
} // namespace nvhttp
-
-#endif // SUNSHINE_NVHTTP_H
diff --git a/src/platform/common.h b/src/platform/common.h
index 1175f889934..cdb5785e076 100644
--- a/src/platform/common.h
+++ b/src/platform/common.h
@@ -1,9 +1,8 @@
-//
-// Created by loki on 6/21/19.
-//
-
-#ifndef SUNSHINE_COMMON_H
-#define SUNSHINE_COMMON_H
+/**
+ * @file src/platform/common.h
+ * @brief todo
+ */
+#pragma once
#include
#include
@@ -169,7 +168,7 @@ namespace platf {
virtual ~deinit_t() = default;
};
- struct img_t {
+ struct img_t: std::enable_shared_from_this {
public:
img_t() = default;
@@ -186,6 +185,8 @@ namespace platf {
std::int32_t pixel_pitch {};
std::int32_t row_pitch {};
+ std::optional frame_timestamp;
+
virtual ~img_t() = default;
};
@@ -213,8 +214,8 @@ namespace platf {
}
/**
- * implementations must take ownership of 'frame'
- */
+ * implementations must take ownership of 'frame'
+ */
virtual int
set_frame(AVFrame *frame, AVBufferRef *hw_frames_ctx) {
BOOST_LOG(error) << "Illegal call to hwdevice_t::set_frame(). Did you forget to override it?";
@@ -225,14 +226,14 @@ namespace platf {
set_colorspace(std::uint32_t colorspace, std::uint32_t color_range) {};
/**
- * Implementations may set parameters during initialization of the hwframes context
- */
+ * Implementations may set parameters during initialization of the hwframes context
+ */
virtual void
init_hwframes(AVHWFramesContext *frames) {};
/**
- * Implementations may make modifications required before context derivation
- */
+ * Implementations may make modifications required before context derivation
+ */
virtual int
prepare_to_derive_context(int hw_device_type) {
return 0;
@@ -245,39 +246,53 @@ namespace platf {
ok,
reinit,
timeout,
+ interrupted,
error
};
class display_t {
public:
/**
- * When display has a new image ready or a timeout occurs, this callback will be called with the image.
- * If a frame was captured, frame_captured will be true. If a timeout occurred, it will be false.
- *
- * On Break Request -->
- * Returns nullptr
- *
- * On Success -->
- * Returns the image object that should be filled next.
- * This may or may not be the image send with the callback
- */
- using snapshot_cb_t = std::function(std::shared_ptr &img, bool frame_captured)>;
+ * When display has a new image ready or a timeout occurs, this callback will be called with the image.
+ * If a frame was captured, frame_captured will be true. If a timeout occurred, it will be false.
+ *
+ * On Break Request -->
+ * Returns false
+ *
+ * On Success -->
+ * Returns true
+ */
+ using push_captured_image_cb_t = std::function &&img, bool frame_captured)>;
+
+ /**
+ * Use to get free image from the pool. Calls must be synchronized.
+ * Blocks until there is free image in the pool or capture is interrupted.
+ *
+ * Returns:
+ * 'true' on success, img_out contains free image
+ * 'false' when capture has been interrupted, img_out contains nullptr
+ */
+ using pull_free_image_cb_t = std::function &img_out)>;
display_t() noexcept:
offset_x { 0 }, offset_y { 0 } {}
/**
- * snapshot_cb --> the callback
- * std::shared_ptr img --> The first image to use
- * bool *cursor --> A pointer to the flag that indicates wether the cursor should be captured as well
- *
- * Returns either:
- * capture_e::ok when stopping
- * capture_e::error on error
- * capture_e::reinit when need of reinitialization
- */
+ * push_captured_image_cb --> The callback that is called with captured image,
+ * must be called from the same thread as capture()
+ * pull_free_image_cb --> Capture backends call this callback to get empty image
+ * from the pool. If backend uses multiple threads, calls to this
+ * callback must be synchronized. Calls to this callback and
+ * push_captured_image_cb must be synchronized as well.
+ * bool *cursor --> A pointer to the flag that indicates wether the cursor should be captured as well
+ *
+ * Returns either:
+ * capture_e::ok when stopping
+ * capture_e::error on error
+ * capture_e::reinit when need of reinitialization
+ */
virtual capture_e
- capture(snapshot_cb_t &&snapshot_cb, std::shared_ptr img, bool *cursor) = 0;
+ capture(const push_captured_image_cb_t &push_captured_image_cb, const pull_free_image_cb_t &pull_free_image_cb, bool *cursor) = 0;
virtual std::shared_ptr
alloc_img() = 0;
@@ -352,14 +367,14 @@ namespace platf {
audio_control();
/**
- * display_name --> The name of the monitor that SHOULD be displayed
- * If display_name is empty --> Use the first monitor that's compatible you can find
- * If you require to use this parameter in a seperate thread --> make a copy of it.
- *
- * config --> Stream configuration
- *
- * Returns display_t based on hwdevice_type
- */
+ * display_name --> The name of the monitor that SHOULD be displayed
+ * If display_name is empty --> Use the first monitor that's compatible you can find
+ * If you require to use this parameter in a seperate thread --> make a copy of it.
+ *
+ * config --> Stream configuration
+ *
+ * Returns display_t based on hwdevice_type
+ */
std::shared_ptr
display(mem_type_e hwdevice_type, const std::string &display_name, const video::config_t &config);
@@ -368,7 +383,7 @@ namespace platf {
display_names(mem_type_e hwdevice_type);
boost::process::child
- run_unprivileged(const std::string &cmd, boost::filesystem::path &working_dir, boost::process::environment &env, FILE *file, std::error_code &ec, boost::process::group *group);
+ run_command(bool elevated, bool interactive, const std::string &cmd, boost::filesystem::path &working_dir, boost::process::environment &env, FILE *file, std::error_code &ec, boost::process::group *group);
enum class thread_priority_e : int {
low,
@@ -385,9 +400,7 @@ namespace platf {
void
streaming_will_stop();
- bool
- restart_supported();
- bool
+ void
restart();
struct batched_send_info_t {
@@ -409,6 +422,13 @@ namespace platf {
std::unique_ptr
enable_socket_qos(uintptr_t native_socket, boost::asio::ip::address &address, uint16_t port, qos_data_type_e data_type);
+ /**
+ * @brief Open a url in the default web browser.
+ * @param url The url to open.
+ */
+ void
+ open_url(const std::string &url);
+
input_t
input();
void
@@ -422,7 +442,7 @@ namespace platf {
void
hscroll(input_t &input, int distance);
void
- keyboard(input_t &input, uint16_t modcode, bool release);
+ keyboard(input_t &input, uint16_t modcode, bool release, uint8_t flags);
void
gamepad(input_t &input, int nr, const gamepad_state_t &gamepad_state);
void
@@ -447,5 +467,3 @@ namespace platf {
std::vector &
supported_gamepads();
} // namespace platf
-
-#endif //SUNSHINE_COMMON_H
diff --git a/src/platform/linux/audio.cpp b/src/platform/linux/audio.cpp
index 0b68fc01a33..e31f539f615 100644
--- a/src/platform/linux/audio.cpp
+++ b/src/platform/linux/audio.cpp
@@ -1,6 +1,7 @@
-//
-// Created by loki on 5/16/21.
-//
+/**
+ * @file src/platform/linux/audio.cpp
+ * @brief todo
+ */
#include
#include
diff --git a/src/platform/linux/cuda.cpp b/src/platform/linux/cuda.cpp
index d1f8c1485d9..c2a2e0fd591 100644
--- a/src/platform/linux/cuda.cpp
+++ b/src/platform/linux/cuda.cpp
@@ -1,3 +1,7 @@
+/**
+ * @file src/platform/linux/cuda.cpp
+ * @brief todo
+ */
#include
#include
@@ -346,7 +350,7 @@ namespace cuda {
handle.handle_flags[SESSION_HANDLE] = true;
- return std::move(handle);
+ return handle;
}
const char *
@@ -504,9 +508,16 @@ namespace cuda {
}
platf::capture_e
- capture(snapshot_cb_t &&snapshot_cb, std::shared_ptr img, bool *cursor) override {
+ capture(const push_captured_image_cb_t &push_captured_image_cb, const pull_free_image_cb_t &pull_free_image_cb, bool *cursor) override {
auto next_frame = std::chrono::steady_clock::now();
+ {
+ // We must create at least one texture on this thread before calling NvFBCToCudaSetUp()
+ // Otherwise it fails with "Unable to register an OpenGL buffer to a CUDA resource (result: 201)" message
+ std::shared_ptr img_dummy;
+ pull_free_image_cb(img_dummy);
+ }
+
// Force display_t::capture to initialize handle_t::capture
cursor_visible = !*cursor;
@@ -515,7 +526,7 @@ namespace cuda {
handle.reset();
});
- while (img) {
+ while (true) {
auto now = std::chrono::steady_clock::now();
if (next_frame > now) {
std::this_thread::sleep_for((next_frame - now) / 3 * 2);
@@ -526,16 +537,22 @@ namespace cuda {
}
next_frame = now + delay;
- auto status = snapshot(img.get(), 150ms, *cursor);
+ std::shared_ptr img_out;
+ auto status = snapshot(pull_free_image_cb, img_out, 150ms, *cursor);
switch (status) {
case platf::capture_e::reinit:
case platf::capture_e::error:
+ case platf::capture_e::interrupted:
return status;
case platf::capture_e::timeout:
- img = snapshot_cb(img, false);
+ if (!push_captured_image_cb(std::move(img_out), false)) {
+ return platf::capture_e::ok;
+ }
break;
case platf::capture_e::ok:
- img = snapshot_cb(img, true);
+ if (!push_captured_image_cb(std::move(img_out), true)) {
+ return platf::capture_e::ok;
+ }
break;
default:
BOOST_LOG(error) << "Unrecognized capture status ["sv << (int) status << ']';
@@ -618,7 +635,7 @@ namespace cuda {
}
platf::capture_e
- snapshot(platf::img_t *img, std::chrono::milliseconds timeout, bool cursor) {
+ snapshot(const pull_free_image_cb_t &pull_free_image_cb, std::shared_ptr &img_out, std::chrono::milliseconds timeout, bool cursor) {
if (cursor != cursor_visible) {
auto status = reinit(cursor);
if (status != platf::capture_e::ok) {
@@ -646,7 +663,12 @@ namespace cuda {
return platf::capture_e::error;
}
- if (((img_t *) img)->tex.copy((std::uint8_t *) device_ptr, img->height, img->row_pitch)) {
+ if (!pull_free_image_cb(img_out)) {
+ return platf::capture_e::interrupted;
+ }
+ auto img = (img_t *) img_out.get();
+
+ if (img->tex.copy((std::uint8_t *) device_ptr, img->height, img->row_pitch)) {
return platf::capture_e::error;
}
diff --git a/src/platform/linux/cuda.cu b/src/platform/linux/cuda.cu
index b81cd40d85a..107075d99cd 100644
--- a/src/platform/linux/cuda.cu
+++ b/src/platform/linux/cuda.cu
@@ -1,5 +1,10 @@
+/**
+ * @file src/platform/linux/cuda.cu
+ * @brief todo
+ */
// #include
#include
+#include
#include
#include
#include
@@ -29,13 +34,15 @@ using namespace std::literals;
using namespace std::literals;
-//////////////////// Special desclarations
+// Special declarations
/**
- * NVCC segfaults when including
- * Therefore, some declarations need to be added explicitely
+ * NVCC tends to have problems with standard headers.
+ * Don't include common.h, instead use bare minimum
+ * of standard headers and duplicate declarations of necessary classes.
+ * Not pretty and extremely error-prone, fix at earliest convenience.
*/
namespace platf {
-struct img_t {
+struct img_t: std::enable_shared_from_this {
public:
std::uint8_t *data {};
std::int32_t width {};
@@ -43,6 +50,8 @@ public:
std::int32_t pixel_pitch {};
std::int32_t row_pitch {};
+ std::optional frame_timestamp;
+
virtual ~img_t() = default;
};
} // namespace platf
@@ -70,10 +79,10 @@ struct alignas(16) color_extern_t {
static_assert(sizeof(video::color_t) == sizeof(video::color_extern_t), "color matrix struct mismatch");
-extern color_t colors[4];
+extern color_t colors[6];
} // namespace video
-//////////////////// End special declarations
+// End special declarations
namespace cuda {
auto constexpr INVALID_TEXTURE = std::numeric_limits::max();
@@ -225,7 +234,7 @@ std::optional tex_t::make(int height, int pitch) {
CU_CHECK_OPT(cudaCreateTextureObject(&tex.texture.linear, &res, &desc, nullptr), "Couldn't create cuda texture that uses linear interpolation");
- return std::move(tex);
+ return tex;
}
tex_t::tex_t() : array {}, texture { INVALID_TEXTURE } {}
diff --git a/src/platform/linux/cuda.h b/src/platform/linux/cuda.h
index e38349400b9..e2094d81b25 100644
--- a/src/platform/linux/cuda.h
+++ b/src/platform/linux/cuda.h
@@ -1,6 +1,12 @@
-#if !defined(SUNSHINE_PLATFORM_CUDA_H) && defined(SUNSHINE_BUILD_CUDA)
- #define SUNSHINE_PLATFORM_CUDA_H
+/**
+ * @file src/platform/linux/cuda.h
+ * @brief todo
+ */
+#pragma once
+#if defined(SUNSHINE_BUILD_CUDA)
+
+ #include
#include
#include
#include
@@ -88,11 +94,11 @@ namespace cuda {
sws_t(int in_width, int in_height, int out_width, int out_height, int pitch, int threadsPerBlock, ptr_t &&color_matrix);
/**
- * in_width, in_height -- The width and height of the captured image in pixels
- * out_width, out_height -- the width and height of the NV12 image in pixels
- *
- * pitch -- The size of a single row of pixels in bytes
- */
+ * in_width, in_height -- The width and height of the captured image in pixels
+ * out_width, out_height -- the width and height of the NV12 image in pixels
+ *
+ * pitch -- The size of a single row of pixels in bytes
+ */
static std::optional
make(int in_width, int in_height, int out_width, int out_height, int pitch);
diff --git a/src/platform/linux/graphics.cpp b/src/platform/linux/graphics.cpp
index 51db08467be..fcb7ab234be 100644
--- a/src/platform/linux/graphics.cpp
+++ b/src/platform/linux/graphics.cpp
@@ -1,3 +1,7 @@
+/**
+ * @file src/platform/linux/graphics.cpp
+ * @brief todo
+ */
#include "graphics.h"
#include "src/video.h"
@@ -765,7 +769,7 @@ namespace egl {
gl_drain_errors;
- return std::move(sws);
+ return sws;
}
int
diff --git a/src/platform/linux/graphics.h b/src/platform/linux/graphics.h
index 01fc5304f1e..fbb0e92d3b9 100644
--- a/src/platform/linux/graphics.h
+++ b/src/platform/linux/graphics.h
@@ -1,5 +1,8 @@
-#ifndef SUNSHINE_PLATFORM_LINUX_OPENGL_H
-#define SUNSHINE_PLATFORM_LINUX_OPENGL_H
+/**
+ * @file src/platform/linux/graphics.h
+ * @brief todo
+ */
+#pragma once
#include
#include
@@ -94,8 +97,8 @@ namespace gl {
}
/**
- * Copies a part of the framebuffer to texture
- */
+ * Copies a part of the framebuffer to texture
+ */
void
copy(int id, int texture, int offset_x, int offset_y, int width, int height);
};
@@ -352,5 +355,3 @@ namespace egl {
bool
fail();
} // namespace egl
-
-#endif
diff --git a/src/platform/linux/input.cpp b/src/platform/linux/input.cpp
index 5749b30773d..85980ef37b5 100644
--- a/src/platform/linux/input.cpp
+++ b/src/platform/linux/input.cpp
@@ -1,3 +1,7 @@
+/**
+ * @file src/platform/linux/input.cpp
+ * @brief todo
+ */
#include
#include
#include
@@ -143,9 +147,9 @@ namespace platf {
constexpr auto UNKNOWN = 0;
/**
- * @brief Initializes the keycode constants for translating
- * moonlight keycodes to linux/X11 keycodes
- */
+ * @brief Initializes the keycode constants for translating
+ * moonlight keycodes to linux/X11 keycodes.
+ */
static constexpr std::array
init_keycodes() {
std::array keycodes {};
@@ -1047,16 +1051,16 @@ namespace platf {
}
/**
- * @brief XTest absolute mouse move.
- * @param input The input_t instance to use.
- * @param x Absolute x position.
- * @param y Absolute y position.
- *
- * EXAMPLES:
- * ```cpp
- * x_abs_mouse(input, 0, 0);
- * ```
- */
+ * @brief XTest absolute mouse move.
+ * @param input The input_t instance to use.
+ * @param x Absolute x position.
+ * @param y Absolute y position.
+ *
+ * EXAMPLES:
+ * ```cpp
+ * x_abs_mouse(input, 0, 0);
+ * ```
+ */
static void
x_abs_mouse(input_t &input, float x, float y) {
#ifdef SUNSHINE_BUILD_X11
@@ -1070,17 +1074,17 @@ namespace platf {
}
/**
- * @brief Absolute mouse move.
- * @param input The input_t instance to use.
- * @param touch_port The touch_port instance to use.
- * @param x Absolute x position.
- * @param y Absolute y position.
- *
- * EXAMPLES:
- * ```cpp
- * abs_mouse(input, touch_port, 0, 0);
- * ```
- */
+ * @brief Absolute mouse move.
+ * @param input The input_t instance to use.
+ * @param touch_port The touch_port instance to use.
+ * @param x Absolute x position.
+ * @param y Absolute y position.
+ *
+ * EXAMPLES:
+ * ```cpp
+ * abs_mouse(input, touch_port, 0, 0);
+ * ```
+ */
void
abs_mouse(input_t &input, const touch_port_t &touch_port, float x, float y) {
auto touchscreen = ((input_raw_t *) input.get())->touch_input.get();
@@ -1101,16 +1105,16 @@ namespace platf {
}
/**
- * @brief XTest relative mouse move.
- * @param input The input_t instance to use.
- * @param deltaX Relative x position.
- * @param deltaY Relative y position.
- *
- * EXAMPLES:
- * ```cpp
- * x_move_mouse(input, 10, 10); // Move mouse 10 pixels down and right
- * ```
- */
+ * @brief XTest relative mouse move.
+ * @param input The input_t instance to use.
+ * @param deltaX Relative x position.
+ * @param deltaY Relative y position.
+ *
+ * EXAMPLES:
+ * ```cpp
+ * x_move_mouse(input, 10, 10); // Move mouse 10 pixels down and right
+ * ```
+ */
static void
x_move_mouse(input_t &input, int deltaX, int deltaY) {
#ifdef SUNSHINE_BUILD_X11
@@ -1124,16 +1128,16 @@ namespace platf {
}
/**
- * @brief Relative mouse move.
- * @param input The input_t instance to use.
- * @param deltaX Relative x position.
- * @param deltaY Relative y position.
- *
- * EXAMPLES:
- * ```cpp
- * move_mouse(input, 10, 10); // Move mouse 10 pixels down and right
- * ```
- */
+ * @brief Relative mouse move.
+ * @param input The input_t instance to use.
+ * @param deltaX Relative x position.
+ * @param deltaY Relative y position.
+ *
+ * EXAMPLES:
+ * ```cpp
+ * move_mouse(input, 10, 10); // Move mouse 10 pixels down and right
+ * ```
+ */
void
move_mouse(input_t &input, int deltaX, int deltaY) {
auto mouse = ((input_raw_t *) input.get())->mouse_input.get();
@@ -1154,16 +1158,16 @@ namespace platf {
}
/**
- * @brief XTest mouse button press/release.
- * @param input The input_t instance to use.
- * @param button Which mouse button to emulate.
- * @param release Whether the event was a press (false) or a release (true)
- *
- * EXAMPLES:
- * ```cpp
- * x_button_mouse(input, 1, false); // Press left mouse button
- * ```
- */
+ * @brief XTest mouse button press/release.
+ * @param input The input_t instance to use.
+ * @param button Which mouse button to emulate.
+ * @param release Whether the event was a press (false) or a release (true)
+ *
+ * EXAMPLES:
+ * ```cpp
+ * x_button_mouse(input, 1, false); // Press left mouse button
+ * ```
+ */
static void
x_button_mouse(input_t &input, int button, bool release) {
#ifdef SUNSHINE_BUILD_X11
@@ -1197,16 +1201,16 @@ namespace platf {
}
/**
- * @brief Mouse button press/release.
- * @param input The input_t instance to use.
- * @param button Which mouse button to emulate.
- * @param release Whether the event was a press (false) or a release (true)
- *
- * EXAMPLES:
- * ```cpp
- * button_mouse(input, 1, false); // Press left mouse button
- * ```
- */
+ * @brief Mouse button press/release.
+ * @param input The input_t instance to use.
+ * @param button Which mouse button to emulate.
+ * @param release Whether the event was a press (false) or a release (true)
+ *
+ * EXAMPLES:
+ * ```cpp
+ * button_mouse(input, 1, false); // Press left mouse button
+ * ```
+ */
void
button_mouse(input_t &input, int button, bool release) {
auto mouse = ((input_raw_t *) input.get())->mouse_input.get();
@@ -1245,17 +1249,17 @@ namespace platf {
}
/**
- * @brief XTest mouse scroll.
- * @param input The input_t instance to use.
- * @param distance How far to scroll
- * @param button_pos Which mouse button to emulate for positive scroll.
- * @param button_neg Which mouse button to emulate for negative scroll.
- *
- * EXAMPLES:
- * ```cpp
- * x_scroll(input, 10, 4, 5);
- * ```
- */
+ * @brief XTest mouse scroll.
+ * @param input The input_t instance to use.
+ * @param distance How far to scroll.
+ * @param button_pos Which mouse button to emulate for positive scroll.
+ * @param button_neg Which mouse button to emulate for negative scroll.
+ *
+ * EXAMPLES:
+ * ```cpp
+ * x_scroll(input, 10, 4, 5);
+ * ```
+ */
static void
x_scroll(input_t &input, int distance, int button_pos, int button_neg) {
#ifdef SUNSHINE_BUILD_X11
@@ -1274,15 +1278,15 @@ namespace platf {
}
/**
- * @brief Vertical mouse scroll.
- * @param input The input_t instance to use.
- * @param high_res_distance How far to scroll
- *
- * EXAMPLES:
- * ```cpp
- * scroll(input, 1200);
- * ```
- */
+ * @brief Vertical mouse scroll.
+ * @param input The input_t instance to use.
+ * @param high_res_distance How far to scroll.
+ *
+ * EXAMPLES:
+ * ```cpp
+ * scroll(input, 1200);
+ * ```
+ */
void
scroll(input_t &input, int high_res_distance) {
int distance = high_res_distance / 120;
@@ -1299,15 +1303,15 @@ namespace platf {
}
/**
- * @brief Horizontal mouse scroll.
- * @param input The input_t instance to use.
- * @param high_res_distance How far to scroll
- *
- * EXAMPLES:
- * ```cpp
- * hscroll(input, 1200);
- * ```
- */
+ * @brief Horizontal mouse scroll.
+ * @param input The input_t instance to use.
+ * @param high_res_distance How far to scroll.
+ *
+ * EXAMPLES:
+ * ```cpp
+ * hscroll(input, 1200);
+ * ```
+ */
void
hscroll(input_t &input, int high_res_distance) {
int distance = high_res_distance / 120;
@@ -1333,18 +1337,19 @@ namespace platf {
}
/**
- * @brief XTest keyboard emulation.
- * @param input The input_t instance to use.
- * @param modcode The moonlight key code.
- * @param release Whether the event was a press (false) or a release (true)
- *
- * EXAMPLES:
- * ```cpp
- * x_keyboard(input, 0x5A, false); // Press Z
- * ```
- */
+ * @brief XTest keyboard emulation.
+ * @param input The input_t instance to use.
+ * @param modcode The moonlight key code.
+ * @param release Whether the event was a press (false) or a release (true).
+ * @param flags SS_KBE_FLAG_* values.
+ *
+ * EXAMPLES:
+ * ```cpp
+ * x_keyboard(input, 0x5A, false, 0); // Press Z
+ * ```
+ */
static void
- x_keyboard(input_t &input, uint16_t modcode, bool release) {
+ x_keyboard(input_t &input, uint16_t modcode, bool release, uint8_t flags) {
#ifdef SUNSHINE_BUILD_X11
auto keycode = keysym(modcode);
if (keycode.keysym == UNKNOWN) {
@@ -1367,21 +1372,22 @@ namespace platf {
}
/**
- * @brief Keyboard emulation.
- * @param input The input_t instance to use.
- * @param modcode The moonlight key code.
- * @param release Whether the event was a press (false) or a release (true)
- *
- * EXAMPLES:
- * ```cpp
- * keyboard(input, 0x5A, false); // Press Z
- * ```
- */
+ * @brief Keyboard emulation.
+ * @param input The input_t instance to use.
+ * @param modcode The moonlight key code.
+ * @param release Whether the event was a press (false) or a release (true).
+ * @param flags SS_KBE_FLAG_* values.
+ *
+ * EXAMPLES:
+ * ```cpp
+ * keyboard(input, 0x5A, false, 0); // Press Z
+ * ```
+ */
void
- keyboard(input_t &input, uint16_t modcode, bool release) {
+ keyboard(input_t &input, uint16_t modcode, bool release, uint8_t flags) {
auto keyboard = ((input_raw_t *) input.get())->keyboard_input.get();
if (!keyboard) {
- x_keyboard(input, modcode, release);
+ x_keyboard(input, modcode, release, flags);
return;
}
@@ -1405,12 +1411,12 @@ namespace platf {
}
/**
- * Takes an UTF-32 encoded string and returns a hex string representation of the bytes (uppercase)
- *
- * ex: ['👱'] = "1F471" // see UTF encoding at https://www.compart.com/en/unicode/U+1F471
- *
- * adapted from: https://stackoverflow.com/a/7639754
- */
+ * Takes an UTF-32 encoded string and returns a hex string representation of the bytes (uppercase)
+ *
+ * ex: ['👱'] = "1F471" // see UTF encoding at https://www.compart.com/en/unicode/U+1F471
+ *
+ * adapted from: https://stackoverflow.com/a/7639754
+ */
std::string
to_hex(const std::basic_string &str) {
std::stringstream ss;
@@ -1425,16 +1431,16 @@ namespace platf {
}
/**
- * Here we receive a single UTF-8 encoded char at a time,
- * the trick is to convert it to UTF-32 then send CTRL+SHIFT+U+ in order to produce any
- * unicode character, see: https://en.wikipedia.org/wiki/Unicode_input
- *
- * ex:
- * - when receiving UTF-8 [0xF0 0x9F 0x91 0xB1] (which is '👱')
- * - we'll convert it to UTF-32 [0x1F471]
- * - then type: CTRL+SHIFT+U+1F471
- * see the conversion at: https://www.compart.com/en/unicode/U+1F471
- */
+ * Here we receive a single UTF-8 encoded char at a time,
+ * the trick is to convert it to UTF-32 then send CTRL+SHIFT+U+{HEXCODE} in order to produce any
+ * unicode character, see: https://en.wikipedia.org/wiki/Unicode_input
+ *
+ * ex:
+ * - when receiving UTF-8 [0xF0 0x9F 0x91 0xB1] (which is '👱')
+ * - we'll convert it to UTF-32 [0x1F471]
+ * - then type: CTRL+SHIFT+U+1F471
+ * see the conversion at: https://www.compart.com/en/unicode/U+1F471
+ */
void
unicode(input_t &input, char *utf8, int size) {
auto kb = ((input_raw_t *) input.get())->keyboard_input.get();
@@ -1547,13 +1553,13 @@ namespace platf {
}
/**
- * @brief Initalize a new keyboard and return it.
- *
- * EXAMPLES:
- * ```cpp
- * auto my_keyboard = keyboard();
- * ```
- */
+ * @brief Initialize a new keyboard and return it.
+ *
+ * EXAMPLES:
+ * ```cpp
+ * auto my_keyboard = keyboard();
+ * ```
+ */
evdev_t
keyboard() {
evdev_t dev { libevdev_new() };
@@ -1576,13 +1582,13 @@ namespace platf {
}
/**
- * @brief Initalize a new uinput virtual mouse and return it.
- *
- * EXAMPLES:
- * ```cpp
- * auto my_mouse = mouse();
- * ```
- */
+ * @brief Initialize a new `uinput` virtual mouse and return it.
+ *
+ * EXAMPLES:
+ * ```cpp
+ * auto my_mouse = mouse();
+ * ```
+ */
evdev_t
mouse() {
evdev_t dev { libevdev_new() };
@@ -1627,13 +1633,13 @@ namespace platf {
}
/**
- * @brief Initalize a new uinput virtual touchscreen and return it.
- *
- * EXAMPLES:
- * ```cpp
- * auto my_touchscreen = touchscreen();
- * ```
- */
+ * @brief Initialize a new `uinput` virtual touchscreen and return it.
+ *
+ * EXAMPLES:
+ * ```cpp
+ * auto my_touchscreen = touchscreen();
+ * ```
+ */
evdev_t
touchscreen() {
evdev_t dev { libevdev_new() };
@@ -1677,13 +1683,13 @@ namespace platf {
}
/**
- * @brief Initalize a new uinput virtual X360 gamepad and return it.
- *
- * EXAMPLES:
- * ```cpp
- * auto my_x360 = x360();
- * ```
- */
+ * @brief Initialize a new `uinput` virtual X360 gamepad and return it.
+ *
+ * EXAMPLES:
+ * ```cpp
+ * auto my_x360 = x360();
+ * ```
+ */
evdev_t
x360() {
evdev_t dev { libevdev_new() };
@@ -1754,13 +1760,13 @@ namespace platf {
}
/**
- * @brief Initalize the input system and return it.
- *
- * EXAMPLES:
- * ```cpp
- * auto my_input = input();
- * ```
- */
+ * @brief Initialize the input system and return it.
+ *
+ * EXAMPLES:
+ * ```cpp
+ * auto my_input = input();
+ * ```
+ */
input_t
input() {
input_t result { new input_raw_t() };
diff --git a/src/platform/linux/kmsgrab.cpp b/src/platform/linux/kmsgrab.cpp
index 392bb65db98..094aee5f4e9 100644
--- a/src/platform/linux/kmsgrab.cpp
+++ b/src/platform/linux/kmsgrab.cpp
@@ -1,3 +1,7 @@
+/**
+ * @file src/platform/linux/kmsgrab.cpp
+ * @brief todo
+ */
#include
#include
#include
@@ -552,7 +556,7 @@ namespace platf {
return -1;
}
- //TODO: surf_sd = fb->to_sd();
+ // TODO: surf_sd = fb->to_sd();
auto crct = card.crtc(plane->crtc_id);
kms::print(plane.get(), fb.get(), crct.get());
@@ -723,10 +727,10 @@ namespace platf {
}
capture_e
- capture(snapshot_cb_t &&snapshot_cb, std::shared_ptr img, bool *cursor) override {
+ capture(const push_captured_image_cb_t &push_captured_image_cb, const pull_free_image_cb_t &pull_free_image_cb, bool *cursor) override {
auto next_frame = std::chrono::steady_clock::now();
- while (img) {
+ while (true) {
auto now = std::chrono::steady_clock::now();
if (next_frame > now) {
@@ -738,16 +742,22 @@ namespace platf {
}
next_frame = now + delay;
- auto status = snapshot(img.get(), 1000ms, *cursor);
+ std::shared_ptr img_out;
+ auto status = snapshot(pull_free_image_cb, img_out, 1000ms, *cursor);
switch (status) {
case platf::capture_e::reinit:
case platf::capture_e::error:
+ case platf::capture_e::interrupted:
return status;
case platf::capture_e::timeout:
- img = snapshot_cb(img, false);
+ if (!push_captured_image_cb(std::move(img_out), false)) {
+ return platf::capture_e::ok;
+ }
break;
case platf::capture_e::ok:
- img = snapshot_cb(img, true);
+ if (!push_captured_image_cb(std::move(img_out), true)) {
+ return platf::capture_e::ok;
+ }
break;
default:
BOOST_LOG(error) << "Unrecognized capture status ["sv << (int) status << ']';
@@ -768,7 +778,7 @@ namespace platf {
}
capture_e
- snapshot(img_t *img_out_base, std::chrono::milliseconds timeout, bool cursor) {
+ snapshot(const pull_free_image_cb_t &pull_free_image_cb, std::shared_ptr &img_out, std::chrono::milliseconds timeout, bool cursor) {
file_t fb_fd[4];
egl::surface_descriptor_t sd;
@@ -793,10 +803,14 @@ namespace platf {
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->tex[0], 0, img_offset_x, img_offset_y, 0, width, height, 1, GL_BGRA, GL_UNSIGNED_BYTE, img_out_base->height * img_out_base->row_pitch, img_out_base->data);
+ if (!pull_free_image_cb(img_out)) {
+ return platf::capture_e::interrupted;
+ }
+
+ 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_opt && cursor) {
- cursor_opt->blend(*img_out_base, img_offset_x, img_offset_y);
+ cursor_opt->blend(*img_out, img_offset_x, img_offset_y);
}
return capture_e::ok;
@@ -855,14 +869,23 @@ namespace platf {
int
dummy_img(platf::img_t *img) override {
- return snapshot(img, 1s, false) != capture_e::ok;
+ // TODO: stop cheating and give black image
+ if (!img) {
+ return -1;
+ };
+ auto pull_dummy_img_callback = [&img](std::shared_ptr &img_out) -> bool {
+ img_out = img->shared_from_this();
+ return true;
+ };
+ std::shared_ptr img_out;
+ return snapshot(pull_dummy_img_callback, img_out, 1s, false) != platf::capture_e::ok;
}
capture_e
- capture(snapshot_cb_t &&snapshot_cb, std::shared_ptr img, bool *cursor) {
+ capture(const push_captured_image_cb_t &push_captured_image_cb, const pull_free_image_cb_t &pull_free_image_cb, bool *cursor) {
auto next_frame = std::chrono::steady_clock::now();
- while (img) {
+ while (true) {
auto now = std::chrono::steady_clock::now();
if (next_frame > now) {
@@ -874,16 +897,22 @@ namespace platf {
}
next_frame = now + delay;
- auto status = snapshot(img.get(), 1000ms, *cursor);
+ std::shared_ptr img_out;
+ auto status = snapshot(pull_free_image_cb, img_out, 1000ms, *cursor);
switch (status) {
case platf::capture_e::reinit:
case platf::capture_e::error:
+ case platf::capture_e::interrupted:
return status;
case platf::capture_e::timeout:
- img = snapshot_cb(img, false);
+ if (!push_captured_image_cb(std::move(img_out), false)) {
+ return platf::capture_e::ok;
+ }
break;
case platf::capture_e::ok:
- img = snapshot_cb(img, true);
+ if (!push_captured_image_cb(std::move(img_out), true)) {
+ return platf::capture_e::ok;
+ }
break;
default:
BOOST_LOG(error) << "Unrecognized capture status ["sv << (int) status << ']';
@@ -895,10 +924,13 @@ namespace platf {
}
capture_e
- snapshot(img_t *img_out_base, std::chrono::milliseconds /* timeout */, bool cursor) {
+ snapshot(const pull_free_image_cb_t &pull_free_image_cb, std::shared_ptr &img_out, std::chrono::milliseconds /* timeout */, bool cursor) {
file_t fb_fd[4];
- auto img = (egl::img_descriptor_t *) img_out_base;
+ if (!pull_free_image_cb(img_out)) {
+ return platf::capture_e::interrupted;
+ }
+ auto img = (egl::img_descriptor_t *) img_out.get();
img->reset();
auto status = refresh(fb_fd, &img->sd);
@@ -909,7 +941,7 @@ namespace platf {
img->sequence = ++sequence;
if (!cursor || !cursor_opt) {
- img_out_base->data = nullptr;
+ img->data = nullptr;
for (auto x = 0; x < 4; ++x) {
fb_fd[x].release();
@@ -971,15 +1003,15 @@ namespace platf {
}
/**
- * On Wayland, it's not possible to determine the position of the monitor on the desktop with KMS.
- * Wayland does allow applications to query attached monitors on the desktop,
- * however, the naming scheme is not standardized across implementations.
- *
- * As a result, correlating the KMS output to the wayland outputs is guess work at best.
- * But, it's necessary for absolute mouse coordinates to work.
- *
- * This is an ugly hack :(
- */
+ * On Wayland, it's not possible to determine the position of the monitor on the desktop with KMS.
+ * Wayland does allow applications to query attached monitors on the desktop,
+ * however, the naming scheme is not standardized across implementations.
+ *
+ * As a result, correlating the KMS output to the wayland outputs is guess work at best.
+ * But, it's necessary for absolute mouse coordinates to work.
+ *
+ * This is an ugly hack :(
+ */
void
correlate_to_wayland(std::vector &cds) {
auto monitors = wl::monitors();
diff --git a/src/platform/linux/misc.cpp b/src/platform/linux/misc.cpp
index ba41e625d44..73f6011a144 100644
--- a/src/platform/linux/misc.cpp
+++ b/src/platform/linux/misc.cpp
@@ -1,7 +1,7 @@
/**
- * @file misc.cpp
+ * @file src/misc.cpp
+ * @brief todo
*/
-
// standard includes
#include
@@ -103,15 +103,14 @@ namespace platf {
std::string
from_sockaddr(const sockaddr *const ip_addr) {
- char data[INET6_ADDRSTRLEN];
+ char data[INET6_ADDRSTRLEN] = {};
auto family = ip_addr->sa_family;
if (family == AF_INET6) {
inet_ntop(AF_INET6, &((sockaddr_in6 *) ip_addr)->sin6_addr, data,
INET6_ADDRSTRLEN);
}
-
- if (family == AF_INET) {
+ else if (family == AF_INET) {
inet_ntop(AF_INET, &((sockaddr_in *) ip_addr)->sin_addr, data,
INET_ADDRSTRLEN);
}
@@ -121,17 +120,16 @@ namespace platf {
std::pair
from_sockaddr_ex(const sockaddr *const ip_addr) {
- char data[INET6_ADDRSTRLEN];
+ char data[INET6_ADDRSTRLEN] = {};
auto family = ip_addr->sa_family;
- std::uint16_t port;
+ std::uint16_t port = 0;
if (family == AF_INET6) {
inet_ntop(AF_INET6, &((sockaddr_in6 *) ip_addr)->sin6_addr, data,
INET6_ADDRSTRLEN);
port = ((sockaddr_in6 *) ip_addr)->sin6_port;
}
-
- if (family == AF_INET) {
+ else if (family == AF_INET) {
inet_ntop(AF_INET, &((sockaddr_in *) ip_addr)->sin_addr, data,
INET_ADDRSTRLEN);
port = ((sockaddr_in *) ip_addr)->sin_port;
@@ -159,8 +157,7 @@ namespace platf {
}
bp::child
- run_unprivileged(const std::string &cmd, boost::filesystem::path &working_dir, bp::environment &env, FILE *file, std::error_code &ec, bp::group *group) {
- BOOST_LOG(warning) << "run_unprivileged() is not yet implemented for this platform. The new process will run with Sunshine's permissions."sv;
+ run_command(bool elevated, bool interactive, const std::string &cmd, boost::filesystem::path &working_dir, bp::environment &env, FILE *file, std::error_code &ec, bp::group *group) {
if (!group) {
if (!file) {
return bp::child(cmd, env, bp::start_dir(working_dir), bp::std_out > bp::null, bp::std_err > bp::null, ec);
@@ -179,6 +176,28 @@ namespace platf {
}
}
+ /**
+ * @brief Open a url in the default web browser.
+ * @param url The url to open.
+ */
+ void
+ open_url(const std::string &url) {
+ // set working dir to user home directory
+ auto working_dir = boost::filesystem::path(std::getenv("HOME"));
+ std::string cmd = R"(xdg-open ")" + url + R"(")";
+
+ boost::process::environment _env = boost::this_process::environment();
+ std::error_code ec;
+ auto child = run_command(false, false, cmd, working_dir, _env, nullptr, ec, nullptr);
+ if (ec) {
+ BOOST_LOG(warning) << "Couldn't open url ["sv << url << "]: System: "sv << ec.message();
+ }
+ else {
+ BOOST_LOG(info) << "Opened url ["sv << url << "]"sv;
+ child.detach();
+ }
+ }
+
void
adjust_thread_priority(thread_priority_e priority) {
// Unimplemented
@@ -194,16 +213,34 @@ namespace platf {
// Nothing to do
}
- bool
- restart_supported() {
- // Restart not supported yet
- return false;
+ void
+ restart_on_exit() {
+ char executable[PATH_MAX];
+ ssize_t len = readlink("/proc/self/exe", executable, PATH_MAX - 1);
+ if (len == -1) {
+ BOOST_LOG(fatal) << "readlink() failed: "sv << errno;
+ return;
+ }
+ executable[len] = '\0';
+
+ // ASIO doesn't use O_CLOEXEC, so we have to close all fds ourselves
+ int openmax = (int) sysconf(_SC_OPEN_MAX);
+ for (int fd = STDERR_FILENO + 1; fd < openmax; fd++) {
+ close(fd);
+ }
+
+ // Re-exec ourselves with the same arguments
+ if (execv(executable, lifetime::get_argv()) < 0) {
+ BOOST_LOG(fatal) << "execv() failed: "sv << errno;
+ return;
+ }
}
- bool
+ void
restart() {
- // Restart not supported yet
- return false;
+ // Gracefully clean up and restart ourselves instead of exiting
+ atexit(restart_on_exit);
+ lifetime::exit_sunshine(0, true);
}
bool
diff --git a/src/platform/linux/misc.h b/src/platform/linux/misc.h
index e4a59f672f1..8541bf1650e 100644
--- a/src/platform/linux/misc.h
+++ b/src/platform/linux/misc.h
@@ -1,5 +1,8 @@
-#ifndef SUNSHINE_PLATFORM_MISC_H
-#define SUNSHINE_PLATFORM_MISC_H
+/**
+ * @file src/platform/linux/misc.h
+ * @brief todo
+ */
+#pragma once
#include
#include
@@ -29,5 +32,3 @@ namespace dyn {
handle(const std::vector &libs);
} // namespace dyn
-
-#endif
\ No newline at end of file
diff --git a/src/platform/linux/publish.cpp b/src/platform/linux/publish.cpp
index 6c6d7ede36e..367c7aa3a20 100644
--- a/src/platform/linux/publish.cpp
+++ b/src/platform/linux/publish.cpp
@@ -1,5 +1,9 @@
-
-// adapted from https://www.avahi.org/doxygen/html/client-publish-service_8c-example.html
+/**
+ * @file src/platform/linux/publish.cpp
+ * @brief todo
+ * @note Adapted from https://www.avahi.org/doxygen/html/client-publish-service_8c-example.html
+ * @todo Use a common file for this and src/platform/macos/publish.cpp
+ */
#include
#include "misc.h"
@@ -12,7 +16,9 @@ using namespace std::literals;
namespace avahi {
- /** Error codes used by avahi */
+ /**
+ * @brief Error codes used by avahi.
+ */
enum err_e {
OK = 0, /**< OK */
ERR_FAILURE = -1, /**< Generic error code */
@@ -113,7 +119,9 @@ namespace avahi {
CLIENT_NO_FAIL = 2 /**< Don't fail if the daemon is not available when avahi_client_new() is called, instead enter CLIENT_CONNECTING state and wait for the daemon to appear */
};
- /** Some flags for publishing functions */
+ /**
+ * @brief Flags for publishing functions.
+ */
enum PublishFlags {
PUBLISH_UNIQUE = 1, /**< For raw records: The RRset is intended to be unique */
PUBLISH_NO_PROBE = 2, /**< For raw records: Though the RRset is intended to be unique no probes shall be sent */
@@ -434,4 +442,4 @@ namespace platf::publish {
return std::make_unique(std::thread { avahi::simple_poll_loop, poll.get() });
}
-} // namespace platf::publish
\ No newline at end of file
+} // namespace platf::publish
diff --git a/src/platform/linux/vaapi.cpp b/src/platform/linux/vaapi.cpp
index 12281ac4292..4a1e7df23ba 100644
--- a/src/platform/linux/vaapi.cpp
+++ b/src/platform/linux/vaapi.cpp
@@ -1,3 +1,7 @@
+/**
+ * @file src/platform/linux/vaapi.cpp
+ * @brief todo
+ */
#include
#include
@@ -7,8 +11,7 @@ extern "C" {
#include
#include
#if !VA_CHECK_VERSION(1, 9, 0)
-/* vaSyncBuffer stub allows Sunshine built against libva <2.9.0
- to link against ffmpeg on libva 2.9.0 or later */
+// vaSyncBuffer stub allows Sunshine built against libva <2.9.0 to link against ffmpeg on libva 2.9.0 or later
VAStatus
vaSyncBuffer(
VADisplay dpy,
@@ -25,6 +28,7 @@ vaSyncBuffer(
#include "src/main.h"
#include "src/platform/common.h"
#include "src/utility.h"
+#include "src/video.h"
using namespace std::literals;
@@ -55,10 +59,7 @@ namespace va {
// Needs to be closed manually
int fd;
- /*
- * Total size of this object (may include regions which are
- * not part of the surface).
- */
+ // Total size of this object (may include regions which are not part of the surface)
uint32_t size;
// Format modifier applied to this object, not sure what that means
uint64_t drm_format_modifier;
@@ -84,7 +85,9 @@ namespace va {
} layers[4];
};
- /** Currently defined profiles */
+ /**
+ * @brief Defined profiles
+ */
enum class profile_e {
// Profile ID used for video processing.
ProfileNone = -1,
@@ -134,9 +137,9 @@ namespace va {
IDCT = 3,
MoComp = 4,
Deblocking = 5,
- EncSlice = 6, /* slice level encode */
- EncPicture = 7, /* pictuer encode, JPEG, etc */
- /*
+ EncSlice = 6, /** slice level encode */
+ EncPicture = 7, /** picture encode, JPEG, etc */
+ /**
* For an implementation that supports a low power/high performance variant
* for slice level encode, it can choose to expose the
* VAEntrypointEncSliceLP entrypoint. Certain encoding tools may not be
@@ -147,7 +150,7 @@ namespace va {
EncSliceLP = 8,
VideoProc = 10, /**< Video pre/post-processing. */
/**
- * \brief FEI
+ * @brief FEI
*
* The purpose of FEI (Flexible Encoding Infrastructure) is to allow applications to
* have more controls and trade off quality for speed with their own IPs.
@@ -161,10 +164,10 @@ namespace va {
* and VAEncFEIDistortionBufferType) for FEI entry function.
* If separate PAK is set, two extra input buffers
* (VAEncFEIMVBufferType, VAEncFEIMBModeBufferType) are needed for PAK input.
- **/
+ */
FEI = 11,
/**
- * \brief Stats
+ * @brief Stats
*
* A pre-processing function for getting some statistics and motion vectors is added,
* and some extra controls for Encode pipeline are provided. The application can
@@ -178,19 +181,19 @@ namespace va {
* (VAStatsStatisticsParameterBufferType) and one or two output buffers
* (VAStatsStatisticsBufferType, VAStatsStatisticsBottomFieldBufferType (for interlace only)
* and VAStatsMVBufferType) are needed for this entry point.
- **/
+ */
Stats = 12,
/**
- * \brief ProtectedTEEComm
+ * @brief ProtectedTEEComm
*
* A function for communicating with TEE (Trusted Execution Environment).
- **/
+ */
ProtectedTEEComm = 13,
/**
- * \brief ProtectedContent
+ * @brief ProtectedContent
*
* A function for protected content to decrypt encrypted content.
- **/
+ */
ProtectedContent = 14,
};
@@ -474,11 +477,11 @@ namespace va {
};
/**
- * This is a private structure of FFmpeg, I need this to manually create
- * a VAAPI hardware context
- *
- * xdisplay will not be used internally by FFmpeg
- */
+ * This is a private structure of FFmpeg, I need this to manually create
+ * a VAAPI hardware context
+ *
+ * xdisplay will not be used internally by FFmpeg
+ */
typedef struct VAAPIDevicePriv {
union {
void *xdisplay;
@@ -488,22 +491,22 @@ namespace va {
} VAAPIDevicePriv;
/**
- * VAAPI connection details.
- *
- * Allocated as AVHWDeviceContext.hwctx
- */
+ * VAAPI connection details.
+ *
+ * Allocated as AVHWDeviceContext.hwctx
+ */
typedef struct AVVAAPIDeviceContext {
/**
- * The VADisplay handle, to be filled by the user.
- */
+ * The VADisplay handle, to be filled by the user.
+ */
va::VADisplay display;
/**
- * Driver quirks to apply - this is filled by av_hwdevice_ctx_init(),
- * with reference to a table of known drivers, unless the
- * AV_VAAPI_DRIVER_QUIRK_USER_SET bit is already present. The user
- * may need to refer to this field when performing any later
- * operations using VAAPI with the same VADisplay.
- */
+ * Driver quirks to apply - this is filled by av_hwdevice_ctx_init(),
+ * with reference to a table of known drivers, unless the
+ * AV_VAAPI_DRIVER_QUIRK_USER_SET bit is already present. The user
+ * may need to refer to this field when performing any later
+ * operations using VAAPI with the same VADisplay.
+ */
unsigned int driver_quirks;
} AVVAAPIDeviceContext;
@@ -512,6 +515,16 @@ namespace va {
BOOST_LOG(*(boost::log::sources::severity_logger *) level) << msg;
}
+ static void
+ vaapi_hwdevice_ctx_free(AVHWDeviceContext *ctx) {
+ auto hwctx = (AVVAAPIDeviceContext *) ctx->hwctx;
+ auto priv = (VAAPIDevicePriv *) ctx->user_opaque;
+
+ vaTerminate(hwctx->display);
+ close(priv->drm_fd);
+ av_freep(&priv);
+ }
+
int
vaapi_make_hwdevice_ctx(platf::hwdevice_t *base, AVBufferRef **hw_device_buf) {
if (!va::initialize) {
@@ -529,7 +542,6 @@ namespace va {
auto *priv = (VAAPIDevicePriv *) av_mallocz(sizeof(VAAPIDevicePriv));
priv->drm_fd = fd;
- priv->drm.fd = fd;
auto fg = util::fail_guard([fd, priv]() {
close(fd);
@@ -559,9 +571,13 @@ namespace va {
BOOST_LOG(debug) << "vaapi vendor: "sv << va::queryVendorString(display.get());
*hw_device_buf = av_hwdevice_ctx_alloc(AV_HWDEVICE_TYPE_VAAPI);
- auto ctx = (AVVAAPIDeviceContext *) ((AVHWDeviceContext *) (*hw_device_buf)->data)->hwctx;
- ctx->display = display.release();
+ auto ctx = (AVHWDeviceContext *) (*hw_device_buf)->data;
+ auto hwctx = (AVVAAPIDeviceContext *) ctx->hwctx;
+ // Ownership of the VADisplay and DRM fd is now ours to manage via the free() function
+ hwctx->display = display.release();
+ ctx->user_opaque = priv;
+ ctx->free = vaapi_hwdevice_ctx_free;
fg.disable();
auto err = av_hwdevice_ctx_init(*hw_device_buf);
@@ -626,11 +642,11 @@ namespace va {
return false;
}
- if (config::video.hevc_mode > 1 && !query(display.get(), profile_e::HEVCMain)) {
+ if (video::active_hevc_mode > 1 && !query(display.get(), profile_e::HEVCMain)) {
return false;
}
- if (config::video.hevc_mode > 2 && !query(display.get(), profile_e::HEVCMain10)) {
+ if (video::active_hevc_mode > 2 && !query(display.get(), profile_e::HEVCMain10)) {
return false;
}
diff --git a/src/platform/linux/vaapi.h b/src/platform/linux/vaapi.h
index 7f03c5a7d82..081d004897b 100644
--- a/src/platform/linux/vaapi.h
+++ b/src/platform/linux/vaapi.h
@@ -1,5 +1,8 @@
-#ifndef SUNSHINE_VAAPI_H
-#define SUNSHINE_VAAPI_H
+/**
+ * @file src/platform/linux/vaapi.h
+ * @brief todo
+ */
+#pragma once
#include "misc.h"
#include "src/platform/common.h"
@@ -9,12 +12,12 @@ namespace egl {
}
namespace va {
/**
- * Width --> Width of the image
- * Height --> Height of the image
- * offset_x --> Horizontal offset of the image in the texture
- * offset_y --> Vertical offset of the image in the texture
- * file_t card --> The file descriptor of the render device used for encoding
- */
+ * Width --> Width of the image
+ * Height --> Height of the image
+ * offset_x --> Horizontal offset of the image in the texture
+ * offset_y --> Vertical offset of the image in the texture
+ * file_t card --> The file descriptor of the render device used for encoding
+ */
std::shared_ptr
make_hwdevice(int width, int height, bool vram);
std::shared_ptr
@@ -29,4 +32,3 @@ namespace va {
int
init();
} // namespace va
-#endif
\ No newline at end of file
diff --git a/src/platform/linux/wayland.cpp b/src/platform/linux/wayland.cpp
index 5bd207cd318..d601ba95daa 100644
--- a/src/platform/linux/wayland.cpp
+++ b/src/platform/linux/wayland.cpp
@@ -1,3 +1,7 @@
+/**
+ * @file src/platform/linux/wayland.cpp
+ * @brief todo
+ */
#include
#include
diff --git a/src/platform/linux/wayland.h b/src/platform/linux/wayland.h
index 18caf990861..a4c3aef17d9 100644
--- a/src/platform/linux/wayland.h
+++ b/src/platform/linux/wayland.h
@@ -1,5 +1,8 @@
-#ifndef SUNSHINE_WAYLAND_H
-#define SUNSHINE_WAYLAND_H
+/**
+ * @file src/platform/linux/wayland.h
+ * @brief todo
+ */
+#pragma once
#include
@@ -180,9 +183,9 @@ namespace wl {
class display_t {
public:
/**
- * Initialize display with display_name
- * If display_name == nullptr -> display_name = std::getenv("WAYLAND_DISPLAY")
- */
+ * Initialize display with display_name
+ * If display_name == nullptr -> display_name = std::getenv("WAYLAND_DISPLAY")
+ */
int
init(const char *display_name = nullptr);
@@ -246,5 +249,3 @@ namespace wl {
init() { return -1; }
} // namespace wl
#endif
-
-#endif
\ No newline at end of file
diff --git a/src/platform/linux/wlgrab.cpp b/src/platform/linux/wlgrab.cpp
index db4d24a5093..6cf7fb78070 100644
--- a/src/platform/linux/wlgrab.cpp
+++ b/src/platform/linux/wlgrab.cpp
@@ -1,3 +1,7 @@
+/**
+ * @file src/platform/linux/wlgrab.cpp
+ * @brief todo
+ */
#include "src/platform/common.h"
#include "src/main.h"
@@ -80,7 +84,7 @@ namespace wl {
}
inline platf::capture_e
- snapshot(platf::img_t *img_out_base, std::chrono::milliseconds timeout, bool cursor) {
+ snapshot(const pull_free_image_cb_t &pull_free_image_cb, std::shared_ptr &img_out, std::chrono::milliseconds timeout, bool cursor) {
auto to = std::chrono::steady_clock::now() + timeout;
dmabuf.listen(interface.dmabuf_manager, output, cursor);
@@ -118,10 +122,10 @@ namespace wl {
class wlr_ram_t: public wlr_t {
public:
platf::capture_e
- capture(snapshot_cb_t &&snapshot_cb, std::shared_ptr img, bool *cursor) override {
+ capture(const push_captured_image_cb_t &push_captured_image_cb, const pull_free_image_cb_t &pull_free_image_cb, bool *cursor) override {
auto next_frame = std::chrono::steady_clock::now();
- while (img) {
+ while (true) {
auto now = std::chrono::steady_clock::now();
if (next_frame > now) {
@@ -132,16 +136,22 @@ namespace wl {
}
next_frame = now + delay;
- auto status = snapshot(img.get(), 1000ms, *cursor);
+ std::shared_ptr img_out;
+ auto status = snapshot(pull_free_image_cb, img_out, 1000ms, *cursor);
switch (status) {
case platf::capture_e::reinit:
case platf::capture_e::error:
+ case platf::capture_e::interrupted:
return status;
case platf::capture_e::timeout:
- img = snapshot_cb(img, false);
+ if (!push_captured_image_cb(std::move(img_out), false)) {
+ return platf::capture_e::ok;
+ }
break;
case platf::capture_e::ok:
- img = snapshot_cb(img, true);
+ if (!push_captured_image_cb(std::move(img_out), true)) {
+ return platf::capture_e::ok;
+ }
break;
default:
BOOST_LOG(error) << "Unrecognized capture status ["sv << (int) status << ']';
@@ -153,8 +163,8 @@ namespace wl {
}
platf::capture_e
- snapshot(platf::img_t *img_out_base, std::chrono::milliseconds timeout, bool cursor) {
- auto status = wlr_t::snapshot(img_out_base, timeout, cursor);
+ snapshot(const pull_free_image_cb_t &pull_free_image_cb, std::shared_ptr &img_out, std::chrono::milliseconds timeout, bool cursor) {
+ auto status = wlr_t::snapshot(pull_free_image_cb, img_out, timeout, cursor);
if (status != platf::capture_e::ok) {
return status;
}
@@ -167,6 +177,10 @@ namespace wl {
return platf::capture_e::reinit;
}
+ if (!pull_free_image_cb(img_out)) {
+ return platf::capture_e::interrupted;
+ }
+
gl::ctx.BindTexture(GL_TEXTURE_2D, (*rgb_opt)->tex[0]);
int w, h;
@@ -174,7 +188,7 @@ namespace wl {
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_base->height * img_out_base->row_pitch, img_out_base->data);
+ 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);
return platf::capture_e::ok;
@@ -229,10 +243,10 @@ namespace wl {
class wlr_vram_t: public wlr_t {
public:
platf::capture_e
- capture(snapshot_cb_t &&snapshot_cb, std::shared_ptr img, bool *cursor) override {
+ capture(const push_captured_image_cb_t &push_captured_image_cb, const pull_free_image_cb_t &pull_free_image_cb, bool *cursor) override {
auto next_frame = std::chrono::steady_clock::now();
- while (img) {
+ while (true) {
auto now = std::chrono::steady_clock::now();
if (next_frame > now) {
@@ -243,16 +257,22 @@ namespace wl {
}
next_frame = now + delay;
- auto status = snapshot(img.get(), 1000ms, *cursor);
+ std::shared_ptr img_out;
+ auto status = snapshot(pull_free_image_cb, img_out, 1000ms, *cursor);
switch (status) {
case platf::capture_e::reinit:
case platf::capture_e::error:
+ case platf::capture_e::interrupted:
return status;
case platf::capture_e::timeout:
- img = snapshot_cb(img, false);
+ if (!push_captured_image_cb(std::move(img_out), false)) {
+ return platf::capture_e::ok;
+ }
break;
case platf::capture_e::ok:
- img = snapshot_cb(img, true);
+ if (!push_captured_image_cb(std::move(img_out), true)) {
+ return platf::capture_e::ok;
+ }
break;
default:
BOOST_LOG(error) << "Unrecognized capture status ["sv << (int) status << ']';
@@ -264,13 +284,16 @@ namespace wl {
}
platf::capture_e
- snapshot(platf::img_t *img_out_base, std::chrono::milliseconds timeout, bool cursor) {
- auto status = wlr_t::snapshot(img_out_base, timeout, cursor);
+ snapshot(const pull_free_image_cb_t &pull_free_image_cb, std::shared_ptr &img_out, std::chrono::milliseconds timeout, bool cursor) {
+ auto status = wlr_t::snapshot(pull_free_image_cb, img_out, timeout, cursor);
if (status != platf::capture_e::ok) {
return status;
}
- auto img = (egl::img_descriptor_t *) img_out_base;
+ if (!pull_free_image_cb(img_out)) {
+ return platf::capture_e::interrupted;
+ }
+ auto img = (egl::img_descriptor_t *) img_out.get();
img->reset();
auto current_frame = dmabuf.current_frame;
@@ -311,7 +334,16 @@ namespace wl {
int
dummy_img(platf::img_t *img) override {
- return snapshot(img, 1000ms, false) != platf::capture_e::ok;
+ // TODO: stop cheating and give black image
+ if (!img) {
+ return -1;
+ };
+ auto pull_dummy_img_callback = [&img](std::shared_ptr &img_out) -> bool {
+ img_out = img->shared_from_this();
+ return true;
+ };
+ std::shared_ptr img_out;
+ return snapshot(pull_dummy_img_callback, img_out, 1000ms, false) != platf::capture_e::ok;
}
std::uint64_t sequence {};
diff --git a/src/platform/linux/x11grab.cpp b/src/platform/linux/x11grab.cpp
index ae294cf1a78..ad8ef0343ff 100644
--- a/src/platform/linux/x11grab.cpp
+++ b/src/platform/linux/x11grab.cpp
@@ -1,7 +1,7 @@
-//
-// Created by loki on 6/21/19.
-//
-
+/**
+ * @file src/platform/linux/x11grab.cpp
+ * @brief todo
+ */
#include "src/platform/common.h"
#include
@@ -389,10 +389,10 @@ namespace platf {
mem_type_e mem_type;
- /*
- * Last X (NOT the streamed monitor!) size.
- * This way we can trigger reinitialization if the dimensions changed while streaming
- */
+ /**
+ * Last X (NOT the streamed monitor!) size.
+ * This way we can trigger reinitialization if the dimensions changed while streaming
+ */
// int env_width, env_height;
x11_attr_t(mem_type_e mem_type):
@@ -468,18 +468,18 @@ namespace platf {
}
/**
- * Called when the display attributes should change.
- */
+ * Called when the display attributes should change.
+ */
void
refresh() {
- x11::GetWindowAttributes(xdisplay.get(), xwindow, &xattr); //Update xattr's
+ x11::GetWindowAttributes(xdisplay.get(), xwindow, &xattr); // Update xattr's
}
capture_e
- capture(snapshot_cb_t &&snapshot_cb, std::shared_ptr img, bool *cursor) override {
+ capture(const push_captured_image_cb_t &push_captured_image_cb, const pull_free_image_cb_t &pull_free_image_cb, bool *cursor) override {
auto next_frame = std::chrono::steady_clock::now();
- while (img) {
+ while (true) {
auto now = std::chrono::steady_clock::now();
if (next_frame > now) {
@@ -491,16 +491,22 @@ namespace platf {
}
next_frame = now + delay;
- auto status = snapshot(img.get(), 1000ms, *cursor);
+ std::shared_ptr img_out;
+ auto status = snapshot(pull_free_image_cb, img_out, 1000ms, *cursor);
switch (status) {
case platf::capture_e::reinit:
case platf::capture_e::error:
+ case platf::capture_e::interrupted:
return status;
case platf::capture_e::timeout:
- img = snapshot_cb(img, false);
+ if (!push_captured_image_cb(std::move(img_out), false)) {
+ return platf::capture_e::ok;
+ }
break;
case platf::capture_e::ok:
- img = snapshot_cb(img, true);
+ if (!push_captured_image_cb(std::move(img_out), true)) {
+ return platf::capture_e::ok;
+ }
break;
default:
BOOST_LOG(error) << "Unrecognized capture status ["sv << (int) status << ']';
@@ -512,26 +518,31 @@ namespace platf {
}
capture_e
- snapshot(img_t *img_out_base, std::chrono::milliseconds timeout, bool cursor) {
+ snapshot(const pull_free_image_cb_t &pull_free_image_cb, std::shared_ptr &img_out, std::chrono::milliseconds timeout, bool cursor) {
refresh();
- //The whole X server changed, so we must reinit everything
+ // The whole X server changed, so we must reinit everything
if (xattr.width != env_width || xattr.height != env_height) {
BOOST_LOG(warning) << "X dimensions changed in non-SHM mode, request reinit"sv;
return capture_e::reinit;
}
- XImage *img { x11::GetImage(xdisplay.get(), xwindow, offset_x, offset_y, width, height, AllPlanes, ZPixmap) };
- auto img_out = (x11_img_t *) img_out_base;
- img_out->width = img->width;
- img_out->height = img->height;
- img_out->data = (uint8_t *) img->data;
- img_out->row_pitch = img->bytes_per_line;
- img_out->pixel_pitch = img->bits_per_pixel / 8;
- img_out->img.reset(img);
+ if (!pull_free_image_cb(img_out)) {
+ return platf::capture_e::interrupted;
+ }
+ auto img = (x11_img_t *) img_out.get();
+
+ XImage *x_img { x11::GetImage(xdisplay.get(), xwindow, offset_x, offset_y, width, height, AllPlanes, ZPixmap) };
+
+ img->width = x_img->width;
+ img->height = x_img->height;
+ img->data = (uint8_t *) x_img->data;
+ img->row_pitch = x_img->bytes_per_line;
+ img->pixel_pitch = x_img->bits_per_pixel / 8;
+ img->img.reset(x_img);
if (cursor) {
- blend_cursor(xdisplay.get(), *img_out_base, offset_x, offset_y);
+ blend_cursor(xdisplay.get(), *img, offset_x, offset_y);
}
return capture_e::ok;
@@ -559,7 +570,16 @@ namespace platf {
int
dummy_img(img_t *img) override {
- snapshot(img, 0s, true);
+ // TODO: stop cheating and give black image
+ if (!img) {
+ return -1;
+ };
+ auto pull_dummy_img_callback = [&img](std::shared_ptr &img_out) -> bool {
+ img_out = img->shared_from_this();
+ return true;
+ };
+ std::shared_ptr img_out;
+ snapshot(pull_dummy_img_callback, img_out, 0s, true);
return 0;
}
};
@@ -594,10 +614,10 @@ namespace platf {
}
capture_e
- capture(snapshot_cb_t &&snapshot_cb, std::shared_ptr img, bool *cursor) override {
+ capture(const push_captured_image_cb_t &push_captured_image_cb, const pull_free_image_cb_t &pull_free_image_cb, bool *cursor) override {
auto next_frame = std::chrono::steady_clock::now();
- while (img) {
+ while (true) {
auto now = std::chrono::steady_clock::now();
if (next_frame > now) {
@@ -609,16 +629,22 @@ namespace platf {
}
next_frame = now + delay;
- auto status = snapshot(img.get(), 1000ms, *cursor);
+ std::shared_ptr img_out;
+ auto status = snapshot(pull_free_image_cb, img_out, 1000ms, *cursor);
switch (status) {
case platf::capture_e::reinit:
case platf::capture_e::error:
+ case platf::capture_e::interrupted:
return status;
case platf::capture_e::timeout:
- img = snapshot_cb(img, false);
+ if (!push_captured_image_cb(std::move(img_out), false)) {
+ return platf::capture_e::ok;
+ }
break;
case platf::capture_e::ok:
- img = snapshot_cb(img, true);
+ if (!push_captured_image_cb(std::move(img_out), true)) {
+ return platf::capture_e::ok;
+ }
break;
default:
BOOST_LOG(error) << "Unrecognized capture status ["sv << (int) status << ']';
@@ -630,8 +656,8 @@ namespace platf {
}
capture_e
- snapshot(img_t *img, std::chrono::milliseconds timeout, bool cursor) {
- //The whole X server changed, so we must reinit everything
+ snapshot(const pull_free_image_cb_t &pull_free_image_cb, std::shared_ptr &img_out, std::chrono::milliseconds timeout, bool cursor) {
+ // The whole X server changed, so we must reinit everything
if (xattr.width != env_width || xattr.height != env_height) {
BOOST_LOG(warning) << "X dimensions changed in SHM mode, request reinit"sv;
return capture_e::reinit;
@@ -645,10 +671,14 @@ namespace platf {
return capture_e::reinit;
}
- std::copy_n((std::uint8_t *) data.data, frame_size(), img->data);
+ if (!pull_free_image_cb(img_out)) {
+ return platf::capture_e::interrupted;
+ }
+
+ std::copy_n((std::uint8_t *) data.data, frame_size(), img_out->data);
if (cursor) {
- blend_cursor(shm_xdisplay.get(), *img, offset_x, offset_y);
+ blend_cursor(shm_xdisplay.get(), *img_out, offset_x, offset_y);
}
return capture_e::ok;
diff --git a/src/platform/linux/x11grab.h b/src/platform/linux/x11grab.h
index 89428a49525..24e96f6a1fd 100644
--- a/src/platform/linux/x11grab.h
+++ b/src/platform/linux/x11grab.h
@@ -1,5 +1,8 @@
-#ifndef SUNSHINE_X11_GRAB
-#define SUNSHINE_X11_GRAB
+/**
+ * @file src/platform/linux/x11grab.h
+ * @brief todo
+ */
+#pragma once
#include
@@ -34,11 +37,11 @@ namespace platf::x11 {
capture(egl::cursor_t &img);
/**
- * Capture and blend the cursor into the image
- *
- * img <-- destination image
- * offsetX, offsetY <--- Top left corner of the virtual screen
- */
+ * Capture and blend the cursor into the image
+ *
+ * img <-- destination image
+ * offsetX, offsetY <--- Top left corner of the virtual screen
+ */
void
blend(img_t &img, int offsetX, int offsetY);
@@ -66,5 +69,3 @@ namespace platf::x11 {
make_display() { return nullptr; }
#endif
} // namespace platf::x11
-
-#endif
\ No newline at end of file
diff --git a/src/platform/macos/av_audio.h b/src/platform/macos/av_audio.h
index 48a83ac4ae5..c0d221734da 100644
--- a/src/platform/macos/av_audio.h
+++ b/src/platform/macos/av_audio.h
@@ -1,5 +1,8 @@
-#ifndef SUNSHINE_PLATFORM_AV_AUDIO_H
-#define SUNSHINE_PLATFORM_AV_AUDIO_H
+/**
+ * @file src/platform/macos/av_audio.h
+ * @brief todo
+ */
+#pragma once
#import
@@ -22,5 +25,3 @@
- (int)setupMicrophone:(AVCaptureDevice *)device sampleRate:(UInt32)sampleRate frameSize:(UInt32)frameSize channels:(UInt8)channels;
@end
-
-#endif //SUNSHINE_PLATFORM_AV_AUDIO_H
diff --git a/src/platform/macos/av_audio.m b/src/platform/macos/av_audio.m
index ef1ce294488..af695179c7b 100644
--- a/src/platform/macos/av_audio.m
+++ b/src/platform/macos/av_audio.m
@@ -1,3 +1,7 @@
+/**
+ * @file src/platform/macos/av_audio.m
+ * @brief todo
+ */
#import "av_audio.h"
@implementation AVAudio
@@ -126,7 +130,7 @@ - (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 interlveaved PCM format but buffer contained %u streams", audioBufferList.mNumberBuffers);
// 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
diff --git a/src/platform/macos/av_img_t.h b/src/platform/macos/av_img_t.h
index f946c6d53ce..f42f9ceb12e 100644
--- a/src/platform/macos/av_img_t.h
+++ b/src/platform/macos/av_img_t.h
@@ -1,5 +1,8 @@
-#ifndef av_img_t_h
-#define av_img_t_h
+/**
+ * @file src/platform/macos/av_img_t.h
+ * @brief todo
+ */
+#pragma once
#include "src/platform/common.h"
@@ -14,5 +17,3 @@ namespace platf {
~av_img_t();
};
} // namespace platf
-
-#endif /* av_img_t_h */
diff --git a/src/platform/macos/av_video.h b/src/platform/macos/av_video.h
index d654ff6792b..83eabb8ebd4 100644
--- a/src/platform/macos/av_video.h
+++ b/src/platform/macos/av_video.h
@@ -1,5 +1,8 @@
-#ifndef SUNSHINE_PLATFORM_AV_VIDEO_H
-#define SUNSHINE_PLATFORM_AV_VIDEO_H
+/**
+ * @file src/platform/macos/av_video.h
+ * @brief todo
+ */
+#pragma once
#import
@@ -38,5 +41,3 @@ typedef bool (^FrameCallbackBlock)(CMSampleBufferRef);
- (dispatch_semaphore_t)capture:(FrameCallbackBlock)frameCallback;
@end
-
-#endif //SUNSHINE_PLATFORM_AV_VIDEO_H
diff --git a/src/platform/macos/av_video.m b/src/platform/macos/av_video.m
index 92f5ec083cc..6e3a9f81f66 100644
--- a/src/platform/macos/av_video.m
+++ b/src/platform/macos/av_video.m
@@ -1,3 +1,7 @@
+/**
+ * @file src/platform/macos/av_video.m
+ * @brief todo
+ */
#import "av_video.h"
@implementation AVVideo
diff --git a/src/platform/macos/display.mm b/src/platform/macos/display.mm
index f053ea03911..65f3c279ddc 100644
--- a/src/platform/macos/display.mm
+++ b/src/platform/macos/display.mm
@@ -1,3 +1,7 @@
+/**
+ * @file src/platform/macos/display.mm
+ * @brief todo
+ */
#include "src/platform/common.h"
#include "src/platform/macos/av_img_t.h"
#include "src/platform/macos/av_video.h"
@@ -37,38 +41,46 @@
}
capture_e
- capture(snapshot_cb_t &&snapshot_cb, std::shared_ptr img, bool *cursor) override {
- __block auto img_next = std::move(img);
-
+ capture(const push_captured_image_cb_t &push_captured_image_cb, const pull_free_image_cb_t &pull_free_image_cb, bool *cursor) override {
auto signal = [av_capture capture:^(CMSampleBufferRef sampleBuffer) {
- auto av_img_next = std::static_pointer_cast(img_next);
-
CFRetain(sampleBuffer);
CVPixelBufferRef pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer);
CVPixelBufferLockBaseAddress(pixelBuffer, kCVPixelBufferLock_ReadOnly);
- if (av_img_next->pixel_buffer != nullptr)
- CVPixelBufferUnlockBaseAddress(av_img_next->pixel_buffer, 0);
+ std::shared_ptr img_out;
+ if (!pull_free_image_cb(img_out)) {
+ // got interrupt signal
+ // returning false here stops capture backend
+ return false;
+ }
+ auto av_img = std::static_pointer_cast(img_out);
+
+ if (av_img->pixel_buffer != nullptr)
+ CVPixelBufferUnlockBaseAddress(av_img->pixel_buffer, 0);
- if (av_img_next->sample_buffer != nullptr)
- CFRelease(av_img_next->sample_buffer);
+ if (av_img->sample_buffer != nullptr)
+ CFRelease(av_img->sample_buffer);
- av_img_next->sample_buffer = sampleBuffer;
- av_img_next->pixel_buffer = pixelBuffer;
- img_next->data = (uint8_t *) CVPixelBufferGetBaseAddress(pixelBuffer);
+ av_img->sample_buffer = sampleBuffer;
+ av_img->pixel_buffer = pixelBuffer;
+ img_out->data = (uint8_t *) CVPixelBufferGetBaseAddress(pixelBuffer);
size_t extraPixels[4];
CVPixelBufferGetExtendedPixels(pixelBuffer, &extraPixels[0], &extraPixels[1], &extraPixels[2], &extraPixels[3]);
- img_next->width = CVPixelBufferGetWidth(pixelBuffer) + extraPixels[0] + extraPixels[1];
- img_next->height = CVPixelBufferGetHeight(pixelBuffer) + extraPixels[2] + extraPixels[3];
- img_next->row_pitch = CVPixelBufferGetBytesPerRow(pixelBuffer);
- img_next->pixel_pitch = img_next->row_pitch / img_next->width;
+ img_out->width = CVPixelBufferGetWidth(pixelBuffer) + extraPixels[0] + extraPixels[1];
+ img_out->height = CVPixelBufferGetHeight(pixelBuffer) + extraPixels[2] + extraPixels[3];
+ img_out->row_pitch = CVPixelBufferGetBytesPerRow(pixelBuffer);
+ img_out->pixel_pitch = img_out->row_pitch / img_out->width;
- img_next = snapshot_cb(img_next, true);
+ if (!push_captured_image_cb(std::move(img_out), false)) {
+ // got interrupt signal
+ // returning false here stops capture backend
+ return false;
+ }
- return img_next != nullptr;
+ return true;
}];
// FIXME: We should time out if an image isn't returned for a while
@@ -132,6 +144,7 @@
img->row_pitch = CVPixelBufferGetBytesPerRow(pixelBuffer);
img->pixel_pitch = img->row_pitch / img->width;
+ // returning false here stops capture backend
return false;
}];
@@ -141,12 +154,12 @@
}
/**
- * A bridge from the pure C++ code of the hwdevice_t class to the pure Objective C code.
- *
- * display --> an opaque pointer to an object of this class
- * width --> the intended capture width
- * height --> the intended capture height
- */
+ * A bridge from the pure C++ code of the hwdevice_t class to the pure Objective C code.
+ *
+ * display --> an opaque pointer to an object of this class
+ * width --> the intended capture width
+ * height --> the intended capture height
+ */
static void
setResolution(void *display, int width, int height) {
[static_cast(display) setFrameWidth:width frameHeight:height];
diff --git a/src/platform/macos/input.cpp b/src/platform/macos/input.cpp
index 19948a957dd..78079e3f386 100644
--- a/src/platform/macos/input.cpp
+++ b/src/platform/macos/input.cpp
@@ -1,3 +1,7 @@
+/**
+ * @file src/platform/macos/input.cpp
+ * @brief todo
+ */
#import
#include
#include
@@ -230,7 +234,7 @@ const KeyCodeMap kKeyCodesMap[] = {
}
void
- keyboard(input_t &input, uint16_t modcode, bool release) {
+ keyboard(input_t &input, uint16_t modcode, bool release, uint8_t flags) {
auto key = keysym(modcode);
BOOST_LOG(debug) << "got keycode: 0x"sv << std::hex << modcode << ", translated to: 0x" << std::hex << key << ", release:" << release;
diff --git a/src/platform/macos/microphone.mm b/src/platform/macos/microphone.mm
index df40db9d1a7..854ca6faffe 100644
--- a/src/platform/macos/microphone.mm
+++ b/src/platform/macos/microphone.mm
@@ -1,3 +1,7 @@
+/**
+ * @file src/platform/macos/microphone.mm
+ * @brief todo
+ */
#include "src/platform/common.h"
#include "src/platform/macos/av_audio.h"
diff --git a/src/platform/macos/misc.h b/src/platform/macos/misc.h
index 10b89bf4459..a6fb1df3244 100644
--- a/src/platform/macos/misc.h
+++ b/src/platform/macos/misc.h
@@ -1,5 +1,8 @@
-#ifndef SUNSHINE_PLATFORM_MISC_H
-#define SUNSHINE_PLATFORM_MISC_H
+/**
+ * @file src/platform/macos/misc.h
+ * @brief todo
+ */
+#pragma once
#include
@@ -14,5 +17,3 @@ namespace dyn {
handle(const std::vector &libs);
} // namespace dyn
-
-#endif
diff --git a/src/platform/macos/misc.mm b/src/platform/macos/misc.mm
index 3d8637297c5..fb2b41b2b39 100644
--- a/src/platform/macos/misc.mm
+++ b/src/platform/macos/misc.mm
@@ -1,8 +1,13 @@
+/**
+ * @file src/platform/macos/misc.mm
+ * @brief todo
+ */
#include
#include
#include
#include
#include
+#include
#include
#include
@@ -81,15 +86,14 @@
std::string
from_sockaddr(const sockaddr *const ip_addr) {
- char data[INET6_ADDRSTRLEN];
+ char data[INET6_ADDRSTRLEN] = {};
auto family = ip_addr->sa_family;
if (family == AF_INET6) {
inet_ntop(AF_INET6, &((sockaddr_in6 *) ip_addr)->sin6_addr, data,
INET6_ADDRSTRLEN);
}
-
- if (family == AF_INET) {
+ else if (family == AF_INET) {
inet_ntop(AF_INET, &((sockaddr_in *) ip_addr)->sin_addr, data,
INET_ADDRSTRLEN);
}
@@ -99,17 +103,16 @@
std::pair
from_sockaddr_ex(const sockaddr *const ip_addr) {
- char data[INET6_ADDRSTRLEN];
+ char data[INET6_ADDRSTRLEN] = {};
auto family = ip_addr->sa_family;
- std::uint16_t port;
+ std::uint16_t port = 0;
if (family == AF_INET6) {
inet_ntop(AF_INET6, &((sockaddr_in6 *) ip_addr)->sin6_addr, data,
INET6_ADDRSTRLEN);
port = ((sockaddr_in6 *) ip_addr)->sin6_port;
}
-
- if (family == AF_INET) {
+ else if (family == AF_INET) {
inet_ntop(AF_INET, &((sockaddr_in *) ip_addr)->sin_addr, data,
INET_ADDRSTRLEN);
port = ((sockaddr_in *) ip_addr)->sin_port;
@@ -158,8 +161,7 @@
}
bp::child
- run_unprivileged(const std::string &cmd, boost::filesystem::path &working_dir, bp::environment &env, FILE *file, std::error_code &ec, bp::group *group) {
- BOOST_LOG(warning) << "run_unprivileged() is not yet implemented for this platform. The new process will run with Sunshine's permissions."sv;
+ run_command(bool elevated, bool interactive, const std::string &cmd, boost::filesystem::path &working_dir, bp::environment &env, FILE *file, std::error_code &ec, bp::group *group) {
if (!group) {
if (!file) {
return bp::child(cmd, env, bp::start_dir(working_dir), bp::std_out > bp::null, bp::std_err > bp::null, ec);
@@ -178,6 +180,27 @@
}
}
+ /**
+ * @brief Open a url in the default web browser.
+ * @param url The url to open.
+ */
+ void
+ open_url(const std::string &url) {
+ boost::filesystem::path working_dir;
+ std::string cmd = R"(open ")" + url + R"(")";
+
+ boost::process::environment _env = boost::this_process::environment();
+ std::error_code ec;
+ auto child = run_command(false, false, cmd, working_dir, _env, nullptr, ec, nullptr);
+ if (ec) {
+ BOOST_LOG(warning) << "Couldn't open url ["sv << url << "]: System: "sv << ec.message();
+ }
+ else {
+ BOOST_LOG(info) << "Opened url ["sv << url << "]"sv;
+ child.detach();
+ }
+ }
+
void
adjust_thread_priority(thread_priority_e priority) {
// Unimplemented
@@ -193,16 +216,33 @@
// Nothing to do
}
- bool
- restart_supported() {
- // Restart not supported yet
- return false;
+ void
+ restart_on_exit() {
+ char executable[2048];
+ uint32_t size = sizeof(executable);
+ if (_NSGetExecutablePath(executable, &size) < 0) {
+ BOOST_LOG(fatal) << "NSGetExecutablePath() failed: "sv << errno;
+ return;
+ }
+
+ // ASIO doesn't use O_CLOEXEC, so we have to close all fds ourselves
+ int openmax = (int) sysconf(_SC_OPEN_MAX);
+ for (int fd = STDERR_FILENO + 1; fd < openmax; fd++) {
+ close(fd);
+ }
+
+ // Re-exec ourselves with the same arguments
+ if (execv(executable, lifetime::get_argv()) < 0) {
+ BOOST_LOG(fatal) << "execv() failed: "sv << errno;
+ return;
+ }
}
- bool
+ void
restart() {
- // Restart not supported yet
- return false;
+ // Gracefully clean up and restart ourselves instead of exiting
+ atexit(restart_on_exit);
+ lifetime::exit_sunshine(0, true);
}
bool
diff --git a/src/platform/macos/nv12_zero_device.cpp b/src/platform/macos/nv12_zero_device.cpp
index 0c62328f762..21046be3054 100644
--- a/src/platform/macos/nv12_zero_device.cpp
+++ b/src/platform/macos/nv12_zero_device.cpp
@@ -1,3 +1,7 @@
+/**
+ * @file src/platform/macos/nv12_zero_device.cpp
+ * @brief todo
+ */
#include "src/platform/macos/nv12_zero_device.h"
#include "src/platform/macos/av_img_t.h"
diff --git a/src/platform/macos/nv12_zero_device.h b/src/platform/macos/nv12_zero_device.h
index 53b6eff19d7..059896ea156 100644
--- a/src/platform/macos/nv12_zero_device.h
+++ b/src/platform/macos/nv12_zero_device.h
@@ -1,5 +1,8 @@
-#ifndef vtdevice_h
-#define vtdevice_h
+/**
+ * @file src/platform/macos/nv12_zero_device.h
+ * @brief todo
+ */
+#pragma once
#include "src/platform/common.h"
@@ -29,5 +32,3 @@ namespace platf {
};
} // namespace platf
-
-#endif /* vtdevice_h */
diff --git a/src/platform/macos/publish.cpp b/src/platform/macos/publish.cpp
index c22d991dc78..4cb80b8dbdb 100644
--- a/src/platform/macos/publish.cpp
+++ b/src/platform/macos/publish.cpp
@@ -1,5 +1,9 @@
-
-// adapted from https://www.avahi.org/doxygen/html/client-publish-service_8c-example.html
+/**
+ * @file src/platform/macos/publish.cpp
+ * @brief todo
+ * @note Adapted from https://www.avahi.org/doxygen/html/client-publish-service_8c-example.html
+ * @todo Use a common file for this and src/platform/linux/publish.cpp
+ */
#include
#include "misc.h"
@@ -12,7 +16,9 @@ using namespace std::literals;
namespace avahi {
- /** Error codes used by avahi */
+ /**
+ * @brief Error codes used by avahi.
+ */
enum err_e {
OK = 0, /**< OK */
ERR_FAILURE = -1, /**< Generic error code */
@@ -113,7 +119,9 @@ namespace avahi {
CLIENT_NO_FAIL = 2 /**< Don't fail if the daemon is not available when avahi_client_new() is called, instead enter CLIENT_CONNECTING state and wait for the daemon to appear */
};
- /** Some flags for publishing functions */
+ /**
+ * @brief Flags for publishing functions.
+ */
enum PublishFlags {
PUBLISH_UNIQUE = 1, /**< For raw records: The RRset is intended to be unique */
PUBLISH_NO_PROBE = 2, /**< For raw records: Though the RRset is intended to be unique no probes shall be sent */
@@ -434,4 +442,4 @@ namespace platf::publish {
return std::make_unique(std::thread { avahi::simple_poll_loop, poll.get() });
}
-}; // namespace platf::publish
+} // namespace platf::publish
diff --git a/src/platform/windows/PolicyConfig.h b/src/platform/windows/PolicyConfig.h
index b0a1264e481..21772227c19 100644
--- a/src/platform/windows/PolicyConfig.h
+++ b/src/platform/windows/PolicyConfig.h
@@ -1,13 +1,15 @@
-// ----------------------------------------------------------------------------
-// PolicyConfig.h
-// Undocumented COM-interface IPolicyConfig.
-// Use for set default audio render endpoint
-// @author EreTIk
-// http://eretik.omegahg.com/
-// ----------------------------------------------------------------------------
+/**
+ * @file src/platform/windows/PolicyConfig.h
+ * @brief Undocumented COM-interface IPolicyConfig.
+ * @details Use for setting default audio render endpoint.
+ * @author EreTIk
+ * @see http://eretik.omegahg.com/
+ */
#pragma once
+#include
+
#ifdef __MINGW32__
#undef DEFINE_GUID
#ifdef __cplusplus
@@ -99,81 +101,3 @@ interface IPolicyConfig: public IUnknown {
PCWSTR,
INT);
};
-
-interface DECLSPEC_UUID("568b9108-44bf-40b4-9006-86afe5b5a620") IPolicyConfigVista;
-class DECLSPEC_UUID("294935CE-F637-4E7C-A41B-AB255460B862") CPolicyConfigVistaClient;
-// ----------------------------------------------------------------------------
-// class CPolicyConfigVistaClient
-// {294935CE-F637-4E7C-A41B-AB255460B862}
-//
-// interface IPolicyConfigVista
-// {568b9108-44bf-40b4-9006-86afe5b5a620}
-//
-// Query interface:
-// CComPtr PolicyConfig;
-// PolicyConfig.CoCreateInstance(__uuidof(CPolicyConfigVistaClient));
-//
-// @compatible: Windows Vista and Later
-// ----------------------------------------------------------------------------
-interface IPolicyConfigVista: public IUnknown {
-public:
- virtual HRESULT
- GetMixFormat(
- PCWSTR,
- WAVEFORMATEX **); // not available on Windows 7, use method from IPolicyConfig
-
- virtual HRESULT STDMETHODCALLTYPE
- GetDeviceFormat(
- PCWSTR,
- INT,
- WAVEFORMATEX **);
-
- virtual HRESULT STDMETHODCALLTYPE
- SetDeviceFormat(
- PCWSTR,
- WAVEFORMATEX *,
- WAVEFORMATEX *);
-
- virtual HRESULT STDMETHODCALLTYPE GetProcessingPeriod(
- PCWSTR,
- INT,
- PINT64,
- PINT64); // not available on Windows 7, use method from IPolicyConfig
-
- virtual HRESULT STDMETHODCALLTYPE SetProcessingPeriod(
- PCWSTR,
- PINT64); // not available on Windows 7, use method from IPolicyConfig
-
- virtual HRESULT STDMETHODCALLTYPE
- GetShareMode(
- PCWSTR,
- struct DeviceShareMode *); // not available on Windows 7, use method from IPolicyConfig
-
- virtual HRESULT STDMETHODCALLTYPE
- SetShareMode(
- PCWSTR,
- struct DeviceShareMode *); // not available on Windows 7, use method from IPolicyConfig
-
- virtual HRESULT STDMETHODCALLTYPE
- GetPropertyValue(
- PCWSTR,
- const PROPERTYKEY &,
- PROPVARIANT *);
-
- virtual HRESULT STDMETHODCALLTYPE
- SetPropertyValue(
- PCWSTR,
- const PROPERTYKEY &,
- PROPVARIANT *);
-
- virtual HRESULT STDMETHODCALLTYPE
- SetDefaultEndpoint(
- PCWSTR wszDeviceId,
- ERole eRole);
-
- virtual HRESULT STDMETHODCALLTYPE SetEndpointVisibility(
- PCWSTR,
- INT); // not available on Windows 7, use method from IPolicyConfig
-};
-
-// ----------------------------------------------------------------------------
diff --git a/src/platform/windows/audio.cpp b/src/platform/windows/audio.cpp
index 9f38f5b6f87..eac55e89e22 100644
--- a/src/platform/windows/audio.cpp
+++ b/src/platform/windows/audio.cpp
@@ -1,7 +1,7 @@
-//
-// Created by loki on 1/12/20.
-//
-
+/**
+ * @file src/platform/windows/audio.cpp
+ * @brief todo
+ */
#include
#include
#include
@@ -10,6 +10,8 @@
#include
+#include
+
#define INITGUID
#include
#undef INITGUID
@@ -32,10 +34,20 @@ const IID IID_IMMDeviceEnumerator = __uuidof(IMMDeviceEnumerator);
const IID IID_IAudioClient = __uuidof(IAudioClient);
const IID IID_IAudioCaptureClient = __uuidof(IAudioCaptureClient);
+#if defined(__x86_64) || defined(_M_AMD64)
+ #define STEAM_DRIVER_SUBDIR L"x64"
+#elif defined(__i386) || defined(_M_IX86)
+ #define STEAM_DRIVER_SUBDIR L"x86"
+#else
+ #warning No known Steam audio driver for this architecture
+#endif
+
using namespace std::literals;
namespace platf::audio {
constexpr auto SAMPLE_RATE = 48000;
+ constexpr auto STEAM_AUDIO_DRIVER_PATH = L"%CommonProgramFiles(x86)%\\Steam\\drivers\\Windows10\\" STEAM_DRIVER_SUBDIR L"\\SteamStreamingSpeakers.inf";
+
template
void
Release(T *p) {
@@ -254,7 +266,7 @@ namespace platf::audio {
&device);
if (FAILED(status)) {
- BOOST_LOG(error) << "Couldn't create audio Device [0x"sv << util::hex(status).to_string_view() << ']';
+ BOOST_LOG(error) << "Couldn't get default audio endpoint [0x"sv << util::hex(status).to_string_view() << ']';
return nullptr;
}
@@ -262,6 +274,78 @@ namespace platf::audio {
return device;
}
+ class audio_notification_t: public ::IMMNotificationClient {
+ public:
+ audio_notification_t() {}
+
+ // IUnknown implementation (unused by IMMDeviceEnumerator)
+ ULONG STDMETHODCALLTYPE
+ AddRef() {
+ return 1;
+ }
+
+ ULONG STDMETHODCALLTYPE
+ Release() {
+ return 1;
+ }
+
+ HRESULT STDMETHODCALLTYPE
+ QueryInterface(REFIID riid, VOID **ppvInterface) {
+ if (IID_IUnknown == riid) {
+ AddRef();
+ *ppvInterface = (IUnknown *) this;
+ return S_OK;
+ }
+ else if (__uuidof(IMMNotificationClient) == riid) {
+ AddRef();
+ *ppvInterface = (IMMNotificationClient *) this;
+ return S_OK;
+ }
+ else {
+ *ppvInterface = NULL;
+ return E_NOINTERFACE;
+ }
+ }
+
+ // IMMNotificationClient
+ HRESULT STDMETHODCALLTYPE
+ OnDefaultDeviceChanged(EDataFlow flow, ERole role, LPCWSTR pwstrDeviceId) {
+ if (flow == eRender) {
+ default_render_device_changed_flag.store(true);
+ }
+ return S_OK;
+ }
+
+ HRESULT STDMETHODCALLTYPE
+ OnDeviceAdded(LPCWSTR pwstrDeviceId) { return S_OK; }
+
+ HRESULT STDMETHODCALLTYPE
+ OnDeviceRemoved(LPCWSTR pwstrDeviceId) { return S_OK; }
+
+ HRESULT STDMETHODCALLTYPE
+ OnDeviceStateChanged(
+ LPCWSTR pwstrDeviceId,
+ DWORD dwNewState) { return S_OK; }
+
+ HRESULT STDMETHODCALLTYPE
+ OnPropertyValueChanged(
+ LPCWSTR pwstrDeviceId,
+ const PROPERTYKEY key) { return S_OK; }
+
+ /**
+ * @brief Checks if the default rendering device changed and resets the change flag
+ *
+ * @return true if the device changed since last call
+ */
+ bool
+ check_default_render_device_changed() {
+ return default_render_device_changed_flag.exchange(false);
+ }
+
+ private:
+ std::atomic_bool default_render_device_changed_flag;
+ };
+
class mic_wasapi_t: public mic_t {
public:
capture_e
@@ -310,6 +394,13 @@ namespace platf::audio {
return -1;
}
+ status = device_enum->RegisterEndpointNotificationCallback(&endpt_notification);
+ if (FAILED(status)) {
+ BOOST_LOG(error) << "Couldn't register endpoint notification [0x"sv << util::hex(status).to_string_view() << ']';
+
+ return -1;
+ }
+
auto device = default_device(device_enum);
if (!device) {
return -1;
@@ -377,6 +468,10 @@ namespace platf::audio {
}
~mic_wasapi_t() override {
+ if (device_enum) {
+ device_enum->UnregisterEndpointNotificationCallback(&endpt_notification);
+ }
+
if (audio_client) {
audio_client->Stop();
}
@@ -398,6 +493,17 @@ namespace platf::audio {
std::uint32_t audio_sample_size;
} block_aligned;
+ // Check if the default audio device has changed
+ if (endpt_notification.check_default_render_device_changed()) {
+ // Invoke the audio_control_t's callback if it wants one
+ if (default_endpt_changed_cb) {
+ (*default_endpt_changed_cb)();
+ }
+
+ // Reinitialize to pick up the new default device
+ return capture_e::reinit;
+ }
+
status = WaitForSingleObjectEx(audio_event.get(), default_latency_ms, FALSE);
switch (status) {
case WAIT_OBJECT_0:
@@ -465,6 +571,9 @@ namespace platf::audio {
audio_client_t audio_client;
audio_capture_t audio_capture;
+ audio_notification_t endpt_notification;
+ std::optional> default_endpt_changed_cb;
+
REFERENCE_TIME default_latency_ms;
util::buffer_t sample_buf;
@@ -480,20 +589,6 @@ namespace platf::audio {
sink_t sink;
- audio::device_enum_t device_enum;
- auto status = CoCreateInstance(
- CLSID_MMDeviceEnumerator,
- nullptr,
- CLSCTX_ALL,
- IID_IMMDeviceEnumerator,
- (void **) &device_enum);
-
- if (FAILED(status)) {
- BOOST_LOG(error) << "Couldn't create Device Enumerator: [0x"sv << util::hex(status).to_string_view() << ']';
-
- return std::nullopt;
- }
-
auto device = default_device(device_enum);
if (!device) {
return std::nullopt;
@@ -505,7 +600,7 @@ namespace platf::audio {
sink.host = converter.to_bytes(wstring.get());
collection_t collection;
- status = device_enum->EnumAudioEndpoints(eRender, DEVICE_STATE_ACTIVE, &collection);
+ auto status = device_enum->EnumAudioEndpoints(eRender, DEVICE_STATE_ACTIVE, &collection);
if (FAILED(status)) {
BOOST_LOG(error) << "Couldn't enumerate: [0x"sv << util::hex(status).to_string_view() << ']';
@@ -515,7 +610,10 @@ namespace platf::audio {
UINT count;
collection->GetCount(&count);
- std::string virtual_device_id = config::audio.virtual_sink;
+ // If the sink isn't a device name, we'll assume it's a device ID
+ auto virtual_device_id = find_device_id_by_name(config::audio.virtual_sink).value_or(converter.from_bytes(config::audio.virtual_sink));
+ auto virtual_device_found = false;
+
for (auto x = 0; x < count; ++x) {
audio::device_t device;
collection->Item(x, &device);
@@ -526,6 +624,7 @@ namespace platf::audio {
audio::wstring_t wstring;
device->GetId(&wstring);
+ std::wstring device_id { wstring.get() };
audio::prop_t prop;
device->OpenPropertyStore(STGM_READ, &prop);
@@ -548,45 +647,42 @@ namespace platf::audio {
<< std::endl;
if (virtual_device_id.empty() && adapter_name == virtual_adapter_name) {
- virtual_device_id = converter.to_bytes(wstring.get());
+ virtual_device_id = std::move(device_id);
+ virtual_device_found = true;
+ break;
+ }
+ else if (virtual_device_id == device_id) {
+ virtual_device_found = true;
+ break;
}
}
- if (!virtual_device_id.empty()) {
+ if (virtual_device_found) {
+ auto name_suffix = converter.to_bytes(virtual_device_id);
sink.null = std::make_optional(sink_t::null_t {
- "virtual-"s.append(formats[format_t::stereo - 1].name) + virtual_device_id,
- "virtual-"s.append(formats[format_t::surr51 - 1].name) + virtual_device_id,
- "virtual-"s.append(formats[format_t::surr71 - 1].name) + virtual_device_id,
+ "virtual-"s.append(formats[format_t::stereo - 1].name) + name_suffix,
+ "virtual-"s.append(formats[format_t::surr51 - 1].name) + name_suffix,
+ "virtual-"s.append(formats[format_t::surr71 - 1].name) + name_suffix,
});
}
-
- return sink;
- }
-
- std::unique_ptr
- microphone(const std::uint8_t *mapping, int channels, std::uint32_t sample_rate, std::uint32_t frame_size) override {
- auto mic = std::make_unique();
-
- if (mic->init(sample_rate, frame_size, channels)) {
- return nullptr;
+ else if (!virtual_device_id.empty()) {
+ BOOST_LOG(warning) << "Unable to find the specified virtual sink: "sv << virtual_device_id;
}
- return mic;
+ return sink;
}
/**
- * If the requested sink is a virtual sink, meaning no speakers attached to
- * the host, then we can seamlessly set the format to stereo and surround sound.
- *
- * Any virtual sink detected will be prefixed by:
- * virtual-(format name)
- * If it doesn't contain that prefix, then the format will not be changed
- */
- std::optional
- set_format(const std::string &sink) {
+ * @brief Gets information encoded in the raw sink name
+ *
+ * @param sink The raw sink name
+ *
+ * @return A pair of type and the real sink name
+ */
+ std::pair
+ get_sink_info(const std::string &sink) {
std::string_view sv { sink.c_str(), sink.size() };
- format_t::type_e type = format_t::none;
// sink format:
// [virtual-(format name)]device_id
auto prefix = "virtual-"sv;
@@ -596,17 +692,50 @@ namespace platf::audio {
for (auto &format : formats) {
auto &name = format.name;
if (sv.find(name) == 0) {
- type = format.type;
- sv = sv.substr(name.size(), sv.size() - name.size());
-
- break;
+ return std::make_pair(format.type, sv.substr(name.size(), sv.size() - name.size()));
}
}
}
- auto wstring_device_id = converter.from_bytes(sv.data());
+ return std::make_pair(format_t::none, sv);
+ }
+
+ std::unique_ptr
+ microphone(const std::uint8_t *mapping, int channels, std::uint32_t sample_rate, std::uint32_t frame_size) override {
+ auto mic = std::make_unique();
+
+ if (mic->init(sample_rate, frame_size, channels)) {
+ return nullptr;
+ }
+
+ // If this is a virtual sink, set a callback that will change the sink back if it's changed
+ auto sink_info = get_sink_info(assigned_sink);
+ if (sink_info.first != format_t::none) {
+ mic->default_endpt_changed_cb = [this] {
+ BOOST_LOG(info) << "Resetting sink to ["sv << assigned_sink << "] after default changed";
+ set_sink(assigned_sink);
+ };
+ }
+
+ return mic;
+ }
- if (type == format_t::none) {
+ /**
+ * If the requested sink is a virtual sink, meaning no speakers attached to
+ * the host, then we can seamlessly set the format to stereo and surround sound.
+ *
+ * Any virtual sink detected will be prefixed by:
+ * virtual-(format name)
+ * If it doesn't contain that prefix, then the format will not be changed
+ */
+ std::optional
+ set_format(const std::string &sink) {
+ auto sink_info = get_sink_info(sink);
+
+ // If the sink isn't a device name, we'll assume it's a device ID
+ auto wstring_device_id = find_device_id_by_name(sink).value_or(converter.from_bytes(sink_info.second.data()));
+
+ if (sink_info.first == format_t::none) {
// wstring_device_id does not contain virtual-(format name)
// It's a simple deviceId, just pass it back
return std::make_optional(std::move(wstring_device_id));
@@ -620,14 +749,14 @@ namespace platf::audio {
return std::nullopt;
}
- set_wave_format(wave_format, formats[(int) type - 1]);
+ set_wave_format(wave_format, formats[(int) sink_info.first - 1]);
WAVEFORMATEXTENSIBLE p {};
status = policy->SetDeviceFormat(wstring_device_id.c_str(), wave_format.get(), (WAVEFORMATEX *) &p);
// Surround 5.1 might contain side-{left, right} instead of speaker in the back
// Try again with different speaker mask.
- if (status == 0x88890008 && type == format_t::surr51) {
+ if (status == 0x88890008 && sink_info.first == format_t::surr51) {
set_wave_format(wave_format, surround_51_side_speakers);
status = policy->SetDeviceFormat(wstring_device_id.c_str(), wave_format.get(), (WAVEFORMATEX *) &p);
}
@@ -651,15 +780,221 @@ namespace platf::audio {
for (int x = 0; x < (int) ERole_enum_count; ++x) {
auto status = policy->SetDefaultEndpoint(wstring_device_id->c_str(), (ERole) x);
if (status) {
- BOOST_LOG(warning) << "Couldn't set ["sv << sink << "] to role ["sv << x << ']';
+ // Depending on the format of the string, we could get either of these errors
+ if (status == HRESULT_FROM_WIN32(ERROR_NOT_FOUND) || status == E_INVALIDARG) {
+ BOOST_LOG(warning) << "Audio sink not found: "sv << sink;
+ }
+ else {
+ BOOST_LOG(warning) << "Couldn't set ["sv << sink << "] to role ["sv << x << "]: 0x"sv << util::hex(status).to_string_view();
+ }
++failure;
}
}
+ // Remember the assigned sink name, so we have it for later if we need to set it
+ // back after another application changes it
+ if (!failure) {
+ assigned_sink = sink;
+ }
+
return failure;
}
+ /**
+ * @brief Find the audio device ID given a user-specified name.
+ * @param name The name provided by the user.
+ * @return The matching device ID, or nothing if not found.
+ */
+ std::optional
+ find_device_id_by_name(const std::string &name) {
+ if (name.empty()) {
+ return std::nullopt;
+ }
+
+ collection_t collection;
+ auto status = device_enum->EnumAudioEndpoints(eRender, DEVICE_STATE_ACTIVE, &collection);
+ if (FAILED(status)) {
+ BOOST_LOG(error) << "Couldn't enumerate: [0x"sv << util::hex(status).to_string_view() << ']';
+
+ return std::nullopt;
+ }
+
+ UINT count;
+ collection->GetCount(&count);
+
+ auto wstring_name = converter.from_bytes(name.data());
+
+ for (auto x = 0; x < count; ++x) {
+ audio::device_t device;
+ collection->Item(x, &device);
+
+ if (!validate_device(device)) {
+ continue;
+ }
+
+ audio::wstring_t wstring_id;
+ device->GetId(&wstring_id);
+
+ audio::prop_t prop;
+ device->OpenPropertyStore(STGM_READ, &prop);
+
+ prop_var_t adapter_friendly_name;
+ prop_var_t device_friendly_name;
+ prop_var_t device_desc;
+
+ prop->GetValue(PKEY_Device_FriendlyName, &device_friendly_name.prop);
+ prop->GetValue(PKEY_DeviceInterface_FriendlyName, &adapter_friendly_name.prop);
+ prop->GetValue(PKEY_Device_DeviceDesc, &device_desc.prop);
+
+ auto adapter_name = no_null((LPWSTR) adapter_friendly_name.prop.pszVal);
+ auto device_name = no_null((LPWSTR) device_friendly_name.prop.pszVal);
+ auto device_description = no_null((LPWSTR) device_desc.prop.pszVal);
+
+ // Match the user-specified name against any of the user-visible strings
+ if (std::wcscmp(wstring_name.c_str(), adapter_name) == 0 ||
+ std::wcscmp(wstring_name.c_str(), device_name) == 0 ||
+ std::wcscmp(wstring_name.c_str(), device_description) == 0) {
+ return std::make_optional(std::wstring { wstring_id.get() });
+ }
+ }
+
+ return std::nullopt;
+ }
+
+ /**
+ * @brief Resets the default audio device from Steam Streaming Speakers.
+ */
+ void
+ reset_default_device() {
+ auto steam_device_id = find_device_id_by_name("Steam Streaming Speakers"s);
+ if (!steam_device_id) {
+ return;
+ }
+
+ {
+ // Get the current default audio device (if present)
+ auto current_default_dev = default_device(device_enum);
+ if (!current_default_dev) {
+ return;
+ }
+
+ audio::wstring_t current_default_id;
+ current_default_dev->GetId(¤t_default_id);
+
+ // If Steam Streaming Speakers are already not default, we're done.
+ if (*steam_device_id != current_default_id.get()) {
+ return;
+ }
+ }
+
+ // Disable the Steam Streaming Speakers temporarily to allow the OS to pick a new default.
+ auto hr = policy->SetEndpointVisibility(steam_device_id->c_str(), FALSE);
+ if (FAILED(hr)) {
+ BOOST_LOG(warning) << "Failed to disable Steam audio device: "sv << util::hex(hr).to_string_view();
+ return;
+ }
+
+ // Get the newly selected default audio device
+ auto new_default_dev = default_device(device_enum);
+
+ // Enable the Steam Streaming Speakers again
+ hr = policy->SetEndpointVisibility(steam_device_id->c_str(), TRUE);
+ if (FAILED(hr)) {
+ BOOST_LOG(warning) << "Failed to enable Steam audio device: "sv << util::hex(hr).to_string_view();
+ return;
+ }
+
+ // If there's now no audio device, the Steam Streaming Speakers were the only device available.
+ // There's no other device to set as the default, so just return.
+ if (!new_default_dev) {
+ return;
+ }
+
+ audio::wstring_t new_default_id;
+ new_default_dev->GetId(&new_default_id);
+
+ // Set the new default audio device
+ for (int x = 0; x < (int) ERole_enum_count; ++x) {
+ policy->SetDefaultEndpoint(new_default_id.get(), (ERole) x);
+ }
+
+ BOOST_LOG(info) << "Successfully reset default audio device"sv;
+ }
+
+ /**
+ * @brief Installs the Steam Streaming Speakers driver, if present.
+ * @return `true` if installation was successful.
+ */
+ bool
+ install_steam_audio_drivers() {
+#ifdef STEAM_DRIVER_SUBDIR
+ // MinGW's libnewdev.a is missing DiInstallDriverW() even though the headers have it,
+ // so we have to load it at runtime. It's Vista or later, so it will always be available.
+ auto newdev = LoadLibraryExW(L"newdev.dll", nullptr, LOAD_LIBRARY_SEARCH_SYSTEM32);
+ if (!newdev) {
+ BOOST_LOG(error) << "newdev.dll failed to load"sv;
+ return false;
+ }
+ auto fg = util::fail_guard([newdev]() {
+ FreeLibrary(newdev);
+ });
+
+ auto fn_DiInstallDriverW = (decltype(DiInstallDriverW) *) GetProcAddress(newdev, "DiInstallDriverW");
+ if (!fn_DiInstallDriverW) {
+ BOOST_LOG(error) << "DiInstallDriverW() is missing"sv;
+ return false;
+ }
+
+ // Get the current default audio device (if present)
+ auto old_default_dev = default_device(device_enum);
+
+ // Install the Steam Streaming Speakers driver
+ WCHAR driver_path[MAX_PATH] = {};
+ ExpandEnvironmentStringsW(STEAM_AUDIO_DRIVER_PATH, driver_path, ARRAYSIZE(driver_path));
+ if (fn_DiInstallDriverW(nullptr, driver_path, 0, nullptr)) {
+ BOOST_LOG(info) << "Successfully installed Steam Streaming Speakers"sv;
+
+ // Wait for 5 seconds to allow the audio subsystem to reconfigure things before
+ // modifying the default audio device or enumerating devices again.
+ Sleep(5000);
+
+ // If there was a previous default device, restore that original device as the
+ // default output device just in case installing the new one changed it.
+ if (old_default_dev) {
+ audio::wstring_t old_default_id;
+ old_default_dev->GetId(&old_default_id);
+
+ for (int x = 0; x < (int) ERole_enum_count; ++x) {
+ policy->SetDefaultEndpoint(old_default_id.get(), (ERole) x);
+ }
+ }
+
+ return true;
+ }
+ else {
+ auto err = GetLastError();
+ switch (err) {
+ case ERROR_ACCESS_DENIED:
+ BOOST_LOG(warning) << "Administrator privileges are required to install Steam Streaming Speakers"sv;
+ break;
+ case ERROR_FILE_NOT_FOUND:
+ case ERROR_PATH_NOT_FOUND:
+ BOOST_LOG(info) << "Steam audio drivers not found. This is expected if you don't have Steam installed."sv;
+ break;
+ default:
+ BOOST_LOG(warning) << "Failed to install Steam audio drivers: "sv << err;
+ break;
+ }
+
+ return false;
+ }
+#else
+ BOOST_LOG(warning) << "Unable to install Steam Streaming Speakers on unknown architecture"sv;
+ return false;
+#endif
+ }
+
int
init() {
auto status = CoCreateInstance(
@@ -675,12 +1010,26 @@ namespace platf::audio {
return -1;
}
+ status = CoCreateInstance(
+ CLSID_MMDeviceEnumerator,
+ nullptr,
+ CLSCTX_ALL,
+ IID_IMMDeviceEnumerator,
+ (void **) &device_enum);
+
+ if (FAILED(status)) {
+ BOOST_LOG(error) << "Couldn't create Device Enumerator: [0x"sv << util::hex(status).to_string_view() << ']';
+ return -1;
+ }
+
return 0;
}
~audio_control_t() override {}
policy_t policy;
+ audio::device_enum_t device_enum;
+ std::string assigned_sink;
};
} // namespace platf::audio
@@ -700,6 +1049,13 @@ namespace platf {
return nullptr;
}
+ // Install Steam Streaming Speakers if needed. We do this during audio_control() to ensure
+ // the sink information returned includes the new Steam Streaming Speakers device.
+ if (config::audio.install_steam_drivers && !control->find_device_id_by_name("Steam Streaming Speakers"s)) {
+ // This is best effort. Don't fail if it doesn't work.
+ control->install_steam_audio_drivers();
+ }
+
return control;
}
@@ -708,6 +1064,17 @@ namespace platf {
if (dxgi::init()) {
return nullptr;
}
- return std::make_unique();
+
+ // Initialize COM
+ auto co_init = std::make_unique();
+
+ // If Steam Streaming Speakers are currently the default audio device,
+ // change the default to something else (if another device is available).
+ audio::audio_control_t audio_ctrl;
+ if (audio_ctrl.init() == 0) {
+ audio_ctrl.reset_default_device();
+ }
+
+ return co_init;
}
} // namespace platf
diff --git a/src/platform/windows/display.h b/src/platform/windows/display.h
index ba6aac38432..2496cd3f55e 100644
--- a/src/platform/windows/display.h
+++ b/src/platform/windows/display.h
@@ -1,9 +1,8 @@
-//
-// Created by loki on 4/23/20.
-//
-
-#ifndef SUNSHINE_DISPLAY_H
-#define SUNSHINE_DISPLAY_H
+/**
+ * @file src/platform/windows/display.h
+ * @brief todo
+ */
+#pragma once
#include
#include
@@ -110,6 +109,7 @@ namespace platf::dxgi {
dup_t dup;
bool has_frame {};
bool use_dwmflush {};
+ std::chrono::steady_clock::time_point last_protected_content_warning_time {};
capture_e
next_frame(DXGI_OUTDUPL_FRAME_INFO &frame_info, std::chrono::milliseconds timeout, resource_t::pointer *res_p);
@@ -125,8 +125,9 @@ namespace platf::dxgi {
public:
int
init(const ::video::config_t &config, const std::string &display_name);
+
capture_e
- capture(snapshot_cb_t &&snapshot_cb, std::shared_ptr img, bool *cursor) override;
+ capture(const push_captured_image_cb_t &push_captured_image_cb, const pull_free_image_cb_t &pull_free_image_cb, bool *cursor) override;
std::chrono::nanoseconds delay;
@@ -168,19 +169,17 @@ namespace platf::dxgi {
colorspace_to_string(DXGI_COLOR_SPACE_TYPE type);
virtual capture_e
- snapshot(img_t *img, std::chrono::milliseconds timeout, bool cursor_visible) = 0;
+ snapshot(const pull_free_image_cb_t &pull_free_image_cb, std::shared_ptr &img_out, std::chrono::milliseconds timeout, bool cursor_visible) = 0;
virtual int
complete_img(img_t *img, bool dummy) = 0;
virtual std::vector
- get_supported_sdr_capture_formats() = 0;
- virtual std::vector
- get_supported_hdr_capture_formats() = 0;
+ get_supported_capture_formats() = 0;
};
class display_ram_t: public display_base_t {
public:
virtual capture_e
- snapshot(img_t *img, std::chrono::milliseconds timeout, bool cursor_visible) override;
+ snapshot(const pull_free_image_cb_t &pull_free_image_cb, std::shared_ptr &img_out, std::chrono::milliseconds timeout, bool cursor_visible) override;
std::shared_ptr
alloc_img() override;
@@ -189,9 +188,7 @@ namespace platf::dxgi {
int
complete_img(img_t *img, bool dummy) override;
std::vector
- get_supported_sdr_capture_formats() override;
- std::vector
- get_supported_hdr_capture_formats() override;
+ get_supported_capture_formats() override;
int
init(const ::video::config_t &config, const std::string &display_name);
@@ -204,7 +201,7 @@ namespace platf::dxgi {
class display_vram_t: public display_base_t, public std::enable_shared_from_this {
public:
virtual capture_e
- snapshot(img_t *img, std::chrono::milliseconds timeout, bool cursor_visible) override;
+ snapshot(const pull_free_image_cb_t &pull_free_image_cb, std::shared_ptr &img_out, std::chrono::milliseconds timeout, bool cursor_visible) override;
std::shared_ptr
alloc_img() override;
@@ -213,9 +210,7 @@ namespace platf::dxgi {
int
complete_img(img_t *img_base, bool dummy) override;
std::vector
- get_supported_sdr_capture_formats() override;
- std::vector
- get_supported_hdr_capture_formats() override;
+ get_supported_capture_formats() override;
int
init(const ::video::config_t &config, const std::string &display_name);
@@ -235,10 +230,10 @@ namespace platf::dxgi {
gpu_cursor_t cursor_alpha;
gpu_cursor_t cursor_xor;
- texture2d_t last_frame_copy;
+ texture2d_t old_surface_delayed_destruction;
+ std::chrono::steady_clock::time_point old_surface_timestamp;
+ std::variant> last_frame_variant;
std::atomic next_image_id;
};
} // namespace platf::dxgi
-
-#endif
diff --git a/src/platform/windows/display_base.cpp b/src/platform/windows/display_base.cpp
index f6c3bfb351f..e4483ae77d1 100644
--- a/src/platform/windows/display_base.cpp
+++ b/src/platform/windows/display_base.cpp
@@ -1,7 +1,7 @@
-//
-// Created by loki on 1/12/20.
-//
-
+/**
+ * @file src/platform/windows/display_base.cpp
+ * @brief todo
+ */
#include
#include
#include
@@ -40,6 +40,14 @@ namespace platf::dxgi {
switch (status) {
case S_OK:
+ // ProtectedContentMaskedOut seems to semi-randomly be TRUE or FALSE even when protected content
+ // is on screen the whole time, so we can't just print when it changes. Instead we'll keep track
+ // of the last time we printed the warning and print another if we haven't printed one recently.
+ if (frame_info.ProtectedContentMaskedOut && std::chrono::steady_clock::now() > last_protected_content_warning_time + 10s) {
+ BOOST_LOG(warning) << "Windows is currently blocking DRM-protected content from capture. You may see black regions where this content would be."sv;
+ last_protected_content_warning_time = std::chrono::steady_clock::now();
+ }
+
has_frame = true;
return capture_e::ok;
case DXGI_ERROR_WAIT_TIMEOUT:
@@ -92,7 +100,7 @@ namespace platf::dxgi {
}
capture_e
- display_base_t::capture(snapshot_cb_t &&snapshot_cb, std::shared_ptr<::platf::img_t> img, bool *cursor) {
+ display_base_t::capture(const push_captured_image_cb_t &push_captured_image_cb, const pull_free_image_cb_t &pull_free_image_cb, bool *cursor) {
auto next_frame = std::chrono::steady_clock::now();
// Use CREATE_WAITABLE_TIMER_HIGH_RESOLUTION if supported (Windows 10 1809+)
@@ -110,7 +118,16 @@ namespace platf::dxgi {
CloseHandle(timer);
});
- while (img) {
+ // Keep the display awake during capture. If the display goes to sleep during
+ // capture, best case is that capture stops until it powers back on. However,
+ // worst case it will trigger us to reinit DD, waking the display back up in
+ // a neverending cycle of waking and sleeping the display of an idle machine.
+ SetThreadExecutionState(ES_CONTINUOUS | ES_DISPLAY_REQUIRED);
+ auto clear_display_required = util::fail_guard([]() {
+ SetThreadExecutionState(ES_CONTINUOUS);
+ });
+
+ while (true) {
// This will return false if the HDR state changes or for any number of other
// display or GPU changes. We should reinit to examine the updated state of
// the display subsystem. It is recommended to call this once per frame.
@@ -135,16 +152,22 @@ namespace platf::dxgi {
next_frame = std::chrono::steady_clock::now() + delay;
}
- auto status = snapshot(img.get(), 1000ms, *cursor);
+ std::shared_ptr img_out;
+ auto status = snapshot(pull_free_image_cb, img_out, 1000ms, *cursor);
switch (status) {
case platf::capture_e::reinit:
case platf::capture_e::error:
+ case platf::capture_e::interrupted:
return status;
case platf::capture_e::timeout:
- img = snapshot_cb(img, false);
+ if (!push_captured_image_cb(std::move(img_out), false)) {
+ return capture_e::ok;
+ }
break;
case platf::capture_e::ok:
- img = snapshot_cb(img, true);
+ if (!push_captured_image_cb(std::move(img_out), true)) {
+ return capture_e::ok;
+ }
break;
default:
BOOST_LOG(error) << "Unrecognized capture status ["sv << (int) status << ']';
@@ -336,46 +359,58 @@ namespace platf::dxgi {
auto output_name = converter.from_bytes(display_name);
adapter_t::pointer adapter_p;
- for (int x = 0; factory->EnumAdapters1(x, &adapter_p) != DXGI_ERROR_NOT_FOUND; ++x) {
- dxgi::adapter_t adapter_tmp { adapter_p };
+ for (int tries = 0; tries < 2; ++tries) {
+ for (int x = 0; factory->EnumAdapters1(x, &adapter_p) != DXGI_ERROR_NOT_FOUND; ++x) {
+ dxgi::adapter_t adapter_tmp { adapter_p };
- DXGI_ADAPTER_DESC1 adapter_desc;
- adapter_tmp->GetDesc1(&adapter_desc);
+ DXGI_ADAPTER_DESC1 adapter_desc;
+ adapter_tmp->GetDesc1(&adapter_desc);
- if (!adapter_name.empty() && adapter_desc.Description != adapter_name) {
- continue;
- }
+ if (!adapter_name.empty() && adapter_desc.Description != adapter_name) {
+ continue;
+ }
- dxgi::output_t::pointer output_p;
- for (int y = 0; adapter_tmp->EnumOutputs(y, &output_p) != DXGI_ERROR_NOT_FOUND; ++y) {
- dxgi::output_t output_tmp { output_p };
+ dxgi::output_t::pointer output_p;
+ for (int y = 0; adapter_tmp->EnumOutputs(y, &output_p) != DXGI_ERROR_NOT_FOUND; ++y) {
+ dxgi::output_t output_tmp { output_p };
- DXGI_OUTPUT_DESC desc;
- output_tmp->GetDesc(&desc);
+ DXGI_OUTPUT_DESC desc;
+ output_tmp->GetDesc(&desc);
- if (!output_name.empty() && desc.DeviceName != output_name) {
- continue;
- }
+ if (!output_name.empty() && desc.DeviceName != output_name) {
+ continue;
+ }
- if (desc.AttachedToDesktop && test_dxgi_duplication(adapter_tmp, output_tmp)) {
- output = std::move(output_tmp);
+ if (desc.AttachedToDesktop && test_dxgi_duplication(adapter_tmp, output_tmp)) {
+ output = std::move(output_tmp);
- offset_x = desc.DesktopCoordinates.left;
- offset_y = desc.DesktopCoordinates.top;
- width = desc.DesktopCoordinates.right - offset_x;
- height = desc.DesktopCoordinates.bottom - offset_y;
+ offset_x = desc.DesktopCoordinates.left;
+ offset_y = desc.DesktopCoordinates.top;
+ width = desc.DesktopCoordinates.right - offset_x;
+ height = desc.DesktopCoordinates.bottom - offset_y;
- // left and bottom may be negative, yet absolute mouse coordinates start at 0x0
- // Ensure offset starts at 0x0
- offset_x -= GetSystemMetrics(SM_XVIRTUALSCREEN);
- offset_y -= GetSystemMetrics(SM_YVIRTUALSCREEN);
+ // left and bottom may be negative, yet absolute mouse coordinates start at 0x0
+ // Ensure offset starts at 0x0
+ offset_x -= GetSystemMetrics(SM_XVIRTUALSCREEN);
+ offset_y -= GetSystemMetrics(SM_YVIRTUALSCREEN);
+ }
+ }
+
+ if (output) {
+ adapter = std::move(adapter_tmp);
+ break;
}
}
if (output) {
- adapter = std::move(adapter_tmp);
break;
}
+
+ // If we made it here without finding an output, try to power on the display and retry.
+ if (tries == 0) {
+ SetThreadExecutionState(ES_DISPLAY_REQUIRED);
+ Sleep(500);
+ }
}
if (!output) {
@@ -510,14 +545,14 @@ namespace platf::dxgi {
}
}
- //FIXME: Duplicate output on RX580 in combination with DOOM (2016) --> BSOD
+ // FIXME: Duplicate output on RX580 in combination with DOOM (2016) --> BSOD
{
// IDXGIOutput5 is optional, but can provide improved performance and wide color support
dxgi::output5_t output5 {};
status = output->QueryInterface(IID_IDXGIOutput5, (void **) &output5);
if (SUCCEEDED(status)) {
// Ask the display implementation which formats it supports
- auto supported_formats = config.dynamicRange ? get_supported_hdr_capture_formats() : get_supported_sdr_capture_formats();
+ auto supported_formats = get_supported_capture_formats();
if (supported_formats.empty()) {
BOOST_LOG(warning) << "No compatible capture formats for this encoder"sv;
return -1;
diff --git a/src/platform/windows/display_ram.cpp b/src/platform/windows/display_ram.cpp
index 581d925c51b..631abce7be8 100644
--- a/src/platform/windows/display_ram.cpp
+++ b/src/platform/windows/display_ram.cpp
@@ -1,4 +1,10 @@
+/**
+ * @file src/platform/windows/display_ram.cpp
+ * @brief todo
+ */
#include "display.h"
+
+#include "misc.h"
#include "src/main.h"
namespace platf {
@@ -81,7 +87,7 @@ namespace platf::dxgi {
auto colors_out = (std::uint8_t *) &cursor_pixel;
auto colors_in = (std::uint8_t *) img_pixel_p;
- //TODO: When use of IDXGIOutput5 is implemented, support different color formats
+ // TODO: When use of IDXGIOutput5 is implemented, support different color formats
auto alpha = colors_out[3];
if (alpha == 255) {
*img_pixel_p = cursor_pixel;
@@ -95,7 +101,7 @@ namespace platf::dxgi {
void
apply_color_masked(int *img_pixel_p, int cursor_pixel) {
- //TODO: When use of IDXGIOutput5 is implemented, support different color formats
+ // TODO: When use of IDXGIOutput5 is implemented, support different color formats
auto alpha = ((std::uint8_t *) &cursor_pixel)[3];
if (alpha == 0xFF) {
*img_pixel_p ^= cursor_pixel;
@@ -171,9 +177,7 @@ namespace platf::dxgi {
}
capture_e
- display_ram_t::snapshot(::platf::img_t *img_base, std::chrono::milliseconds timeout, bool cursor_visible) {
- auto img = (img_t *) img_base;
-
+ display_ram_t::snapshot(const pull_free_image_cb_t &pull_free_image_cb, std::shared_ptr &img_out, std::chrono::milliseconds timeout, bool cursor_visible) {
HRESULT status;
DXGI_OUTDUPL_FRAME_INFO frame_info;
@@ -194,6 +198,12 @@ namespace platf::dxgi {
return capture_e::timeout;
}
+ std::optional frame_timestamp;
+ if (auto qpc_displayed = std::max(frame_info.LastPresentTime.QuadPart, frame_info.LastMouseUpdateTime.QuadPart)) {
+ // Translate QueryPerformanceCounter() value to steady_clock time point
+ frame_timestamp = std::chrono::steady_clock::now() - qpc_time_difference(qpc_counter(), qpc_displayed);
+ }
+
if (frame_info.PointerShapeBufferSize > 0) {
auto &img_data = cursor.img_data;
@@ -264,11 +274,16 @@ namespace platf::dxgi {
return capture_e::reinit;
}
- //Copy from GPU to CPU
+ // Copy from GPU to CPU
device_ctx->CopyResource(texture.get(), src.get());
}
}
+ if (!pull_free_image_cb(img_out)) {
+ return capture_e::interrupted;
+ }
+ auto img = (img_t *) img_out.get();
+
// If we don't know the final capture format yet, encode a dummy image
if (capture_format == DXGI_FORMAT_UNKNOWN) {
BOOST_LOG(debug) << "Capture format is still unknown. Encoding a blank image"sv;
@@ -304,6 +319,10 @@ namespace platf::dxgi {
blend_cursor(cursor, *img);
}
+ if (img) {
+ img->frame_timestamp = frame_timestamp;
+ }
+
return capture_e::ok;
}
@@ -358,14 +377,8 @@ namespace platf::dxgi {
}
std::vector
- display_ram_t::get_supported_sdr_capture_formats() {
- return { DXGI_FORMAT_B8G8R8A8_UNORM };
- }
-
- std::vector
- display_ram_t::get_supported_hdr_capture_formats() {
- // HDR is unsupported
- return {};
+ display_ram_t::get_supported_capture_formats() {
+ return { DXGI_FORMAT_B8G8R8A8_UNORM, DXGI_FORMAT_B8G8R8X8_UNORM };
}
int
diff --git a/src/platform/windows/display_vram.cpp b/src/platform/windows/display_vram.cpp
index b6d51335e1e..376a58521da 100644
--- a/src/platform/windows/display_vram.cpp
+++ b/src/platform/windows/display_vram.cpp
@@ -1,3 +1,7 @@
+/**
+ * @file src/platform/windows/display_vram.cpp
+ * @brief todo
+ */
#include
#include
@@ -11,6 +15,7 @@ extern "C" {
}
#include "display.h"
+#include "misc.h"
#include "src/main.h"
#include "src/video.h"
@@ -91,16 +96,16 @@ namespace platf::dxgi {
blob_t convert_UV_vs_hlsl;
blob_t convert_UV_ps_hlsl;
+ blob_t convert_UV_linear_ps_hlsl;
blob_t convert_UV_PQ_ps_hlsl;
blob_t scene_vs_hlsl;
blob_t convert_Y_ps_hlsl;
+ blob_t convert_Y_linear_ps_hlsl;
blob_t convert_Y_PQ_ps_hlsl;
blob_t scene_ps_hlsl;
blob_t scene_NW_ps_hlsl;
struct img_d3d_t: public platf::img_t {
- std::shared_ptr display;
-
// These objects are owned by the display_t's ID3D11Device
texture2d_t capture_texture;
render_target_t capture_rt;
@@ -116,6 +121,9 @@ namespace platf::dxgi {
// Unique identifier for this image
uint32_t id = 0;
+ // DXGI format of this image texture
+ DXGI_FORMAT format;
+
virtual ~img_d3d_t() override {
if (encoder_texture_handle) {
CloseHandle(encoder_texture_handle);
@@ -123,6 +131,52 @@ namespace platf::dxgi {
};
};
+ struct texture_lock_helper {
+ keyed_mutex_t _mutex;
+ bool _locked = false;
+
+ texture_lock_helper(const texture_lock_helper &) = delete;
+ texture_lock_helper &
+ operator=(const texture_lock_helper &) = delete;
+
+ texture_lock_helper(texture_lock_helper &&other) {
+ _mutex.reset(other._mutex.release());
+ _locked = other._locked;
+ other._locked = false;
+ }
+
+ texture_lock_helper &
+ operator=(texture_lock_helper &&other) {
+ if (_locked) _mutex->ReleaseSync(0);
+ _mutex.reset(other._mutex.release());
+ _locked = other._locked;
+ other._locked = false;
+ return *this;
+ }
+
+ texture_lock_helper(IDXGIKeyedMutex *mutex):
+ _mutex(mutex) {
+ if (_mutex) _mutex->AddRef();
+ }
+
+ ~texture_lock_helper() {
+ if (_locked) _mutex->ReleaseSync(0);
+ }
+
+ bool
+ lock() {
+ if (_locked) return true;
+ HRESULT status = _mutex->AcquireSync(0, INFINITE);
+ if (status == S_OK) {
+ _locked = true;
+ }
+ else {
+ BOOST_LOG(error) << "Failed to acquire texture mutex [0x"sv << util::hex(status).to_string_view() << ']';
+ }
+ return _locked;
+ }
+ };
+
util::buffer_t
make_cursor_xor_image(const util::buffer_t &img_data, DXGI_OUTDUPL_POINTER_SHAPE_INFO shape_info) {
constexpr std::uint32_t inverted = 0xFFFFFFFF;
@@ -311,6 +365,16 @@ namespace platf::dxgi {
public:
int
convert(platf::img_t &img_base) override {
+ // Garbage collect mapped capture images whose weak references have expired
+ for (auto it = img_ctx_map.begin(); it != img_ctx_map.end();) {
+ if (it->second.img_weak.expired()) {
+ it = img_ctx_map.erase(it);
+ }
+ else {
+ it++;
+ }
+ }
+
auto &img = (img_d3d_t &) img_base;
auto &img_ctx = img_ctx_map[img.id];
@@ -328,24 +392,23 @@ namespace platf::dxgi {
device_ctx->OMSetRenderTargets(1, &nv12_Y_rt, nullptr);
device_ctx->VSSetShader(scene_vs.get(), nullptr, 0);
- device_ctx->PSSetShader(convert_Y_ps.get(), nullptr, 0);
+ device_ctx->PSSetShader(img.format == DXGI_FORMAT_R16G16B16A16_FLOAT ? convert_Y_fp16_ps.get() : convert_Y_ps.get(), nullptr, 0);
device_ctx->RSSetViewports(1, &outY_view);
device_ctx->PSSetShaderResources(0, 1, &img_ctx.encoder_input_res);
device_ctx->Draw(3, 0);
- // Artifacts start appearing on the rendered image if Sunshine doesn't flush
- // before rendering on the UV part of the image.
- device_ctx->Flush();
-
device_ctx->OMSetRenderTargets(1, &nv12_UV_rt, nullptr);
device_ctx->VSSetShader(convert_UV_vs.get(), nullptr, 0);
- device_ctx->PSSetShader(convert_UV_ps.get(), nullptr, 0);
+ device_ctx->PSSetShader(img.format == DXGI_FORMAT_R16G16B16A16_FLOAT ? convert_UV_fp16_ps.get() : convert_UV_ps.get(), nullptr, 0);
device_ctx->RSSetViewports(1, &outUV_view);
device_ctx->Draw(3, 0);
// Release encoder mutex to allow capture code to reuse this image
img_ctx.encoder_mutex->ReleaseSync(0);
+ ID3D11ShaderResourceView *emptyShaderResourceView = nullptr;
+ device_ctx->PSSetShaderResources(0, 1, &emptyShaderResourceView);
+
return 0;
}
@@ -474,7 +537,7 @@ namespace platf::dxgi {
frame_texture->AddRef();
hwframe_texture.reset(frame_texture);
- float info_in[16 / sizeof(float)] { 1.0f / (float) out_width_f }; //aligned to 16-byte
+ float info_in[16 / sizeof(float)] { 1.0f / (float) out_width_f }; // aligned to 16-byte
info_scene = make_buffer(device.get(), info_in);
if (!info_scene) {
@@ -568,34 +631,48 @@ namespace platf::dxgi {
}
// If the display is in HDR and we're streaming HDR, we'll be converting scRGB to SMPTE 2084 PQ.
- // NB: We can consume scRGB in SDR with our regular shaders because it behaves like UNORM input.
if (format == DXGI_FORMAT_P010 && display->is_hdr()) {
- status = device->CreatePixelShader(convert_Y_PQ_ps_hlsl->GetBufferPointer(), convert_Y_PQ_ps_hlsl->GetBufferSize(), nullptr, &convert_Y_ps);
+ status = device->CreatePixelShader(convert_Y_PQ_ps_hlsl->GetBufferPointer(), convert_Y_PQ_ps_hlsl->GetBufferSize(), nullptr, &convert_Y_fp16_ps);
if (status) {
BOOST_LOG(error) << "Failed to create convertY pixel shader [0x"sv << util::hex(status).to_string_view() << ']';
return -1;
}
- status = device->CreatePixelShader(convert_UV_PQ_ps_hlsl->GetBufferPointer(), convert_UV_PQ_ps_hlsl->GetBufferSize(), nullptr, &convert_UV_ps);
+ status = device->CreatePixelShader(convert_UV_PQ_ps_hlsl->GetBufferPointer(), convert_UV_PQ_ps_hlsl->GetBufferSize(), nullptr, &convert_UV_fp16_ps);
if (status) {
BOOST_LOG(error) << "Failed to create convertUV pixel shader [0x"sv << util::hex(status).to_string_view() << ']';
return -1;
}
}
else {
- status = device->CreatePixelShader(convert_Y_ps_hlsl->GetBufferPointer(), convert_Y_ps_hlsl->GetBufferSize(), nullptr, &convert_Y_ps);
+ // If the display is in Advanced Color mode, the desktop format will be scRGB FP16.
+ // scRGB uses linear gamma, so we must use our linear to sRGB conversion shaders.
+ status = device->CreatePixelShader(convert_Y_linear_ps_hlsl->GetBufferPointer(), convert_Y_linear_ps_hlsl->GetBufferSize(), nullptr, &convert_Y_fp16_ps);
if (status) {
BOOST_LOG(error) << "Failed to create convertY pixel shader [0x"sv << util::hex(status).to_string_view() << ']';
return -1;
}
- status = device->CreatePixelShader(convert_UV_ps_hlsl->GetBufferPointer(), convert_UV_ps_hlsl->GetBufferSize(), nullptr, &convert_UV_ps);
+ status = device->CreatePixelShader(convert_UV_linear_ps_hlsl->GetBufferPointer(), convert_UV_linear_ps_hlsl->GetBufferSize(), nullptr, &convert_UV_fp16_ps);
if (status) {
BOOST_LOG(error) << "Failed to create convertUV pixel shader [0x"sv << util::hex(status).to_string_view() << ']';
return -1;
}
}
+ // These shaders consume standard 8-bit sRGB input
+ status = device->CreatePixelShader(convert_Y_ps_hlsl->GetBufferPointer(), convert_Y_ps_hlsl->GetBufferSize(), nullptr, &convert_Y_ps);
+ if (status) {
+ BOOST_LOG(error) << "Failed to create convertY pixel shader [0x"sv << util::hex(status).to_string_view() << ']';
+ return -1;
+ }
+
+ status = device->CreatePixelShader(convert_UV_ps_hlsl->GetBufferPointer(), convert_UV_ps_hlsl->GetBufferSize(), nullptr, &convert_UV_ps);
+ if (status) {
+ BOOST_LOG(error) << "Failed to create convertUV pixel shader [0x"sv << util::hex(status).to_string_view() << ']';
+ return -1;
+ }
+
color_matrix = make_buffer(device.get(), ::video::colors[0]);
if (!color_matrix) {
BOOST_LOG(error) << "Failed to create color matrix buffer"sv;
@@ -648,18 +725,21 @@ namespace platf::dxgi {
struct encoder_img_ctx_t {
// Used to determine if the underlying texture changes.
// Not safe for actual use by the encoder!
- texture2d_t::pointer capture_texture_p;
+ texture2d_t::const_pointer capture_texture_p;
texture2d_t encoder_texture;
shader_res_t encoder_input_res;
keyed_mutex_t encoder_mutex;
+ std::weak_ptr img_weak;
+
void
reset() {
capture_texture_p = nullptr;
encoder_texture.reset();
encoder_input_res.reset();
encoder_mutex.reset();
+ img_weak.reset();
}
};
@@ -703,6 +783,9 @@ namespace platf::dxgi {
}
img_ctx.capture_texture_p = img.capture_texture.get();
+
+ img_ctx.img_weak = img.weak_from_this();
+
return 0;
}
@@ -735,7 +818,9 @@ namespace platf::dxgi {
vs_t convert_UV_vs;
ps_t convert_UV_ps;
+ ps_t convert_UV_fp16_ps;
ps_t convert_Y_ps;
+ ps_t convert_Y_fp16_ps;
vs_t scene_vs;
D3D11_VIEWPORT outY_view;
@@ -793,9 +878,7 @@ namespace platf::dxgi {
}
capture_e
- display_vram_t::snapshot(platf::img_t *img_base, std::chrono::milliseconds timeout, bool cursor_visible) {
- auto img = (img_d3d_t *) img_base;
-
+ display_vram_t::snapshot(const pull_free_image_cb_t &pull_free_image_cb, std::shared_ptr &img_out, std::chrono::milliseconds timeout, bool cursor_visible) {
HRESULT status;
DXGI_OUTDUPL_FRAME_INFO frame_info;
@@ -816,6 +899,12 @@ namespace platf::dxgi {
return capture_e::timeout;
}
+ std::optional frame_timestamp;
+ if (auto qpc_displayed = std::max(frame_info.LastPresentTime.QuadPart, frame_info.LastMouseUpdateTime.QuadPart)) {
+ // Translate QueryPerformanceCounter() value to steady_clock time point
+ frame_timestamp = std::chrono::steady_clock::now() - qpc_time_difference(qpc_counter(), qpc_displayed);
+ }
+
if (frame_info.PointerShapeBufferSize > 0) {
DXGI_OUTDUPL_POINTER_SHAPE_INFO shape_info {};
@@ -843,9 +932,10 @@ namespace platf::dxgi {
cursor_xor.set_pos(frame_info.PointerPosition.Position.x, frame_info.PointerPosition.Position.y, frame_info.PointerPosition.Visible);
}
- if (frame_update_flag) {
- texture2d_t src {};
+ const bool blend_mouse_cursor_flag = (cursor_alpha.visible || cursor_xor.visible) && cursor_visible;
+ texture2d_t src {};
+ if (frame_update_flag) {
// Get the texture object from this frame
status = res->QueryInterface(IID_ID3D11Texture2D, (void **) &src);
if (FAILED(status)) {
@@ -867,23 +957,6 @@ namespace platf::dxgi {
if (capture_format == DXGI_FORMAT_UNKNOWN) {
capture_format = desc.Format;
BOOST_LOG(info) << "Capture format ["sv << dxgi_format_to_string(capture_format) << ']';
-
- D3D11_TEXTURE2D_DESC t {};
- t.Width = width;
- t.Height = height;
- t.MipLevels = 1;
- t.ArraySize = 1;
- t.SampleDesc.Count = 1;
- t.Usage = D3D11_USAGE_DEFAULT;
- t.Format = capture_format;
- t.BindFlags = 0;
-
- // Create a texture to store the most recent copy of the desktop
- status = device->CreateTexture2D(&t, nullptr, &last_frame_copy);
- if (FAILED(status)) {
- BOOST_LOG(error) << "Failed to create frame copy texture [0x"sv << util::hex(status).to_string_view() << ']';
- return capture_e::error;
- }
}
// It's also possible for the capture format to change on the fly. If that happens,
@@ -892,67 +965,201 @@ namespace platf::dxgi {
BOOST_LOG(info) << "Capture format changed ["sv << dxgi_format_to_string(capture_format) << " -> "sv << dxgi_format_to_string(desc.Format) << ']';
return capture_e::reinit;
}
+ }
- // Now that we know the capture format, we can finish creating the image
- if (complete_img(img, false)) {
- return capture_e::error;
- }
+ enum class lfa {
+ nothing,
+ replace_surface_with_img,
+ replace_img_with_surface,
+ copy_src_to_img,
+ copy_src_to_surface,
+ };
- // Copy the texture to use for cursor-only updates
- device_ctx->CopyResource(last_frame_copy.get(), src.get());
+ enum class ofa {
+ forward_last_img,
+ copy_last_surface_and_blend_cursor,
+ dummy_fallback,
+ };
- // Copy into the capture texture on the image with the mutex held
- status = img->capture_mutex->AcquireSync(0, INFINITE);
- if (status != S_OK) {
- BOOST_LOG(error) << "Failed to acquire capture mutex [0x"sv << util::hex(status).to_string_view() << ']';
- return capture_e::error;
+ auto last_frame_action = lfa::nothing;
+ auto out_frame_action = ofa::dummy_fallback;
+
+ if (capture_format == DXGI_FORMAT_UNKNOWN) {
+ // We don't know the final capture format yet, so we will encode a black dummy image
+ last_frame_action = lfa::nothing;
+ out_frame_action = ofa::dummy_fallback;
+ }
+ else {
+ if (src) {
+ // We got a new frame from DesktopDuplication...
+ if (blend_mouse_cursor_flag) {
+ // ...and we need to blend the mouse cursor onto it.
+ // Copy the frame to intermediate surface so we can blend this and future mouse cursor updates
+ // without new frames from DesktopDuplication. We use direct3d surface directly here and not
+ // an image from pull_free_image_cb mainly because it's lighter (surface sharing between
+ // direct3d devices produce significant memory overhead).
+ last_frame_action = lfa::copy_src_to_surface;
+ // Copy the intermediate surface to a new image from pull_free_image_cb and blend the mouse cursor onto it.
+ out_frame_action = ofa::copy_last_surface_and_blend_cursor;
+ }
+ else {
+ // ...and we don't need to blend the mouse cursor.
+ // Copy the frame to a new image from pull_free_image_cb and save the shared pointer to the image
+ // in case the mouse cursor appears without a new frame from DesktopDuplication.
+ last_frame_action = lfa::copy_src_to_img;
+ // Use saved last image shared pointer as output image evading copy.
+ out_frame_action = ofa::forward_last_img;
+ }
+ }
+ else if (!std::holds_alternative(last_frame_variant)) {
+ // We didn't get a new frame from DesktopDuplication...
+ if (blend_mouse_cursor_flag) {
+ // ...but we need to blend the mouse cursor.
+ if (std::holds_alternative