diff --git a/.clang-format b/.clang-format index 1c31d7ba896..d28dcc4eaba 100644 --- a/.clang-format +++ b/.clang-format @@ -23,6 +23,7 @@ BraceWrapping: AfterEnum: false AfterFunction: false AfterNamespace: false + AfterObjCDeclaration: false AfterUnion: false BeforeCatch: true BeforeElse: true diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000000..60b85432016 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,8 @@ +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "daily" + target-branch: "nightly" + open-pull-requests-limit: 20 diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index e9de6a36d3d..82bbc3438f0 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -51,30 +51,6 @@ jobs: echo Within 'CMakeLists.txt' change "project(Sunshine VERSION $cmakelists_version)" to "project(Sunshine VERSION ${{ needs.check_changelog.outputs.next_version_bare }})" exit 1 - - name: Check gen-deb.in Version - run: | - version=$(grep -o -E '^Version: [0-9]+\.[0-9]+\.[0-9]+' gen-deb.in | grep -o -E '[0-9]+\.[0-9]+\.[0-9]+') - echo "gendeb_version=${version}" >> $GITHUB_ENV - - name: Compare gen-deb.in Version - if: ${{ env.gendeb_version != needs.check_changelog.outputs.next_version_bare }} - run: | - echo gen-deb.in version: "$gendeb_version" - echo Changelog version: "${{ needs.check_changelog.outputs.next_version_bare }}" - echo Within 'gen-deb.in' change "Version: $gendeb_version" to "Version: ${{ needs.check_changelog.outputs.next_version_bare }}" - exit 1 - - - name: Check sunshine.desktop Versions - run: | - version=$(grep -o -E '^X-AppImage-Version=[0-9]+\.[0-9]+\.[0-9]+' sunshine.desktop | grep -o -E '[0-9]+\.[0-9]+\.[0-9]+') - echo "appimage_version=${version}" >> $GITHUB_ENV - - name: Compare sunshine.desktop Versions - if: ${{ env.appimage_version != needs.check_changelog.outputs.next_version_bare }} - run: | - echo sunshine.desktop Version: "$appimage_version" - echo Changelog version: "${{ needs.check_changelog.outputs.next_version_bare }}" - echo Within 'sunshine.desktop' change "X-AppImage-Version=$appimage_version" to "X-AppImage-Version=${{ needs.check_changelog.outputs.next_version_bare }}" - exit 1 - build_appimage: name: AppImage runs-on: ubuntu-20.04 @@ -135,7 +111,7 @@ jobs: wget https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-x86_64.AppImage && chmod +x linuxdeploy-x86_64.AppImage - ./linuxdeploy-x86_64.AppImage --appdir ../AppDir -e ../appimage-build/sunshine -i "../$ICON_FILE" -d "../$DESKTOP_FILE" --output appimage + ./linuxdeploy-x86_64.AppImage --appdir ../AppDir -e ../appimage-build/sunshine -i "../$ICON_FILE" -d "../appimage-build/$DESKTOP_FILE" --output appimage mv sunshine*.AppImage sunshine.AppImage mkdir sunshine && mv sunshine.AppImage sunshine/ @@ -160,7 +136,7 @@ jobs: path: artifacts/ - name: Create Release if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/master' }} - uses: SunshineStream/actions/create_release@v0 + uses: SunshineStream/actions/create_release@master with: token: ${{ secrets.GITHUB_TOKEN }} next_version: ${{ needs.check_changelog.outputs.next_version }} @@ -211,7 +187,7 @@ jobs: path: artifacts/ - name: Create Release if: ${{ matrix.package == '-p' && github.event_name == 'push' && github.ref == 'refs/heads/master' }} - uses: SunshineStream/actions/create_release@v0 + uses: SunshineStream/actions/create_release@master with: token: ${{ secrets.GITHUB_TOKEN }} next_version: ${{ needs.check_changelog.outputs.next_version }} @@ -275,7 +251,7 @@ jobs: path: artifacts/ - name: Create Release if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/master' }} - uses: SunshineStream/actions/create_release@v0 + uses: SunshineStream/actions/create_release@master with: token: ${{ secrets.GITHUB_TOKEN }} next_version: ${{ needs.check_changelog.outputs.next_version }} diff --git a/.github/workflows/clang.yml b/.github/workflows/clang.yml new file mode 100644 index 00000000000..9ac9d06a1d5 --- /dev/null +++ b/.github/workflows/clang.yml @@ -0,0 +1,35 @@ +name: clang-format-lint + +on: + pull_request: + branches: [master, nightly] + types: [opened, synchronize, edited, reopened] + +jobs: + lint: + name: Clang Format Lint + runs-on: ubuntu-latest + strategy: + fail-fast: false # false to test all, true to fail entire job if any fail + matrix: + inplace: [ true, false ] # removed ubuntu_18_04 for now + + steps: + - name: Checkout + uses: actions/checkout@v2 + + - name: Clang format lint + uses: DoozyX/clang-format-lint-action@v0.13 + with: + source: './sunshine' + extensions: 'cpp,h,m,mm' + clangFormatVersion: 13 + style: file + inplace: ${{ matrix.inplace }} + + - name: Upload Artifacts + if: ${{ matrix.inplace == true }} + uses: actions/upload-artifact@v2 + with: + name: sunshine + path: sunshine/ diff --git a/.gitmodules b/.gitmodules index 39650e86745..814f340942e 100644 --- a/.gitmodules +++ b/.gitmodules @@ -13,3 +13,6 @@ [submodule "third-party/nv-codec-headers"] path = third-party/nv-codec-headers url = https://github.com/FFmpeg/nv-codec-headers +[submodule "sunshine/platform/macos/TPCircularBuffer"] + path = sunshine/platform/macos/TPCircularBuffer + url = https://github.com/michaeltyson/TPCircularBuffer diff --git a/CHANGELOG.md b/CHANGELOG.md index f9d58a8b513..5a18ecbd748 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## [0.13.0] - 2022-02-27 +### Added +- (MacOS) Initial support for MacOS (#40) + ## [0.12.0] - 2022-02-13 ### Added - New command line argument `--version` diff --git a/CMakeLists.txt b/CMakeLists.txt index 2bdc2f08177..c3a1936e538 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,6 +1,6 @@ cmake_minimum_required(VERSION 3.0) -project(Sunshine VERSION 0.12.0) +project(Sunshine VERSION 0.13.0) set(CMAKE_MODULE_PATH ${CMAKE_CURRENT_SOURCE_DIR}/cmake) @@ -11,6 +11,22 @@ if(WIN32) PQOS_FLOWID=UINT32* QOS_NON_ADAPTIVE_FLOW=2) endif() +if(APPLE) + macro(ADD_FRAMEWORK fwname appname) + find_library(FRAMEWORK_${fwname} + NAMES ${fwname} + PATHS ${CMAKE_OSX_SYSROOT}/System/Library + PATH_SUFFIXES Frameworks + NO_DEFAULT_PATH) + if( ${FRAMEWORK_${fwname}} STREQUAL FRAMEWORK_${fwname}-NOTFOUND) + MESSAGE(ERROR ": Framework ${fwname} not found") + else() + TARGET_LINK_LIBRARIES(${appname} "${FRAMEWORK_${fwname}}/${fwname}") + MESSAGE(STATUS "Framework ${fwname} found at ${FRAMEWORK_${fwname}}") + endif() + endmacro(ADD_FRAMEWORK) +endif() + add_subdirectory(third-party/moonlight-common-c/enet) add_subdirectory(third-party/Simple-Web-Server) @@ -23,7 +39,9 @@ include_directories(third-party/miniupnp) find_package(Threads REQUIRED) find_package(OpenSSL REQUIRED) -set(Boost_USE_STATIC_LIBS ON) +if(NOT APPLE) + set(Boost_USE_STATIC_LIBS ON) +endif() find_package(Boost COMPONENTS log filesystem REQUIRED) list(APPEND SUNSHINE_COMPILE_OPTIONS -Wall -Wno-missing-braces -Wno-maybe-uninitialized -Wno-sign-compare) @@ -106,6 +124,46 @@ if(WIN32) set_source_files_properties(third-party/ViGEmClient/src/ViGEmClient.cpp PROPERTIES COMPILE_DEFINITIONS "UNICODE=1;ERROR_INVALID_DEVICE_OBJECT_PARAMETER=650") set_source_files_properties(third-party/ViGEmClient/src/ViGEmClient.cpp PROPERTIES COMPILE_FLAGS "-Wno-unknown-pragmas -Wno-misleading-indentation -Wno-class-memaccess") +elseif(APPLE) + add_compile_definitions(SUNSHINE_PLATFORM="macos") + list(APPEND SUNSHINE_DEFINITIONS APPS_JSON="apps_mac.json") + link_directories(/opt/local/lib) + link_directories(/usr/local/lib) + ADD_DEFINITIONS(-DBOOST_LOG_DYN_LINK) + + find_package(FFmpeg REQUIRED) + FIND_LIBRARY(APP_SERVICES_LIBRARY ApplicationServices ) + FIND_LIBRARY(AV_FOUNDATION_LIBRARY AVFoundation ) + FIND_LIBRARY(CORE_MEDIA_LIBRARY CoreMedia ) + FIND_LIBRARY(CORE_VIDEO_LIBRARY CoreVideo ) + FIND_LIBRARY(FOUNDATION_LIBRARY Foundation ) + list(APPEND SUNSHINE_EXTERNAL_LIBRARIES + ${APP_SERVICES_LIBRARY} + ${AV_FOUNDATION_LIBRARY} + ${CORE_MEDIA_LIBRARY} + ${CORE_VIDEO_LIBRARY} + ${FOUNDATION_LIBRARY}) + + set(PLATFORM_INCLUDE_DIRS + ${Boost_INCLUDE_DIR}) + + set(PLATFORM_TARGET_FILES + sunshine/platform/macos/av_audio.h + sunshine/platform/macos/av_audio.m + sunshine/platform/macos/av_img_t.h + sunshine/platform/macos/av_video.h + sunshine/platform/macos/av_video.m + sunshine/platform/macos/display.mm + sunshine/platform/macos/input.cpp + sunshine/platform/macos/microphone.mm + sunshine/platform/macos/misc.cpp + sunshine/platform/macos/misc.h + sunshine/platform/macos/nv12_zero_device.cpp + sunshine/platform/macos/nv12_zero_device.h + sunshine/platform/macos/publish.cpp + sunshine/platform/macos/TPCircularBuffer/TPCircularBuffer.c + sunshine/platform/macos/TPCircularBuffer/TPCircularBuffer.h + ${CMAKE_CURRENT_SOURCE_DIR}/assets/Info.plist) else() add_compile_definitions(SUNSHINE_PLATFORM="linux") list(APPEND SUNSHINE_DEFINITIONS APPS_JSON="apps_linux.json") @@ -255,6 +313,7 @@ else() set(SUNSHINE_EXECUTABLE_PATH "sunshine") endif() configure_file(gen-deb.in gen-deb @ONLY) + configure_file(sunshine.desktop.in sunshine.desktop @ONLY) configure_file(sunshine.service.in sunshine.service @ONLY) endif() @@ -352,7 +411,6 @@ list(APPEND SUNSHINE_EXTERNAL_LIBRARIES libminiupnpc-static ${CBS_EXTERNAL_LIBRARIES} ${CMAKE_THREAD_LIBS_INIT} - stdc++fs enet opus ${FFMPEG_LIBRARIES} @@ -368,7 +426,7 @@ list(APPEND SUNSHINE_DEFINITIONS SUNSHINE_ASSETS_DIR="${SUNSHINE_ASSETS_DIR}") list(APPEND SUNSHINE_DEFINITIONS SUNSHINE_CONFIG_DIR="${SUNSHINE_CONFIG_DIR}") list(APPEND SUNSHINE_DEFINITIONS SUNSHINE_DEFAULT_DIR="${SUNSHINE_DEFAULT_DIR}") add_executable(sunshine ${SUNSHINE_TARGET_FILES}) -target_link_libraries(sunshine ${SUNSHINE_EXTERNAL_LIBRARIES}) +target_link_libraries(sunshine ${SUNSHINE_EXTERNAL_LIBRARIES} ${EXTRA_LIBS}) target_compile_definitions(sunshine PUBLIC ${SUNSHINE_DEFINITIONS}) set_target_properties(sunshine PROPERTIES CXX_STANDARD 17 VERSION ${PROJECT_VERSION} @@ -380,6 +438,10 @@ if(NOT DEFINED CMAKE_CUDA_STANDARD) set(CMAKE_CUDA_STANDARD_REQUIRED ON) endif() +if(APPLE) + target_link_options(sunshine PRIVATE LINKER:-sectcreate,__TEXT,__info_plist,${CMAKE_CURRENT_SOURCE_DIR}/assets/Info.plist) +endif() + foreach(flag IN LISTS SUNSHINE_COMPILE_OPTIONS) list(APPEND SUNSHINE_COMPILE_OPTIONS_CUDA "$<$:--compiler-options=${flag}>") endforeach() diff --git a/Portfile b/Portfile new file mode 100644 index 00000000000..ea751ca8b33 --- /dev/null +++ b/Portfile @@ -0,0 +1,48 @@ +# -*- coding: utf-8; mode: tcl; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- vim:fenc=utf-8:ft=tcl:et:sw=4:ts=4:sts=4 + +PortSystem 1.0 +PortGroup cmake 1.1 +PortGroup github 1.0 +PortGroup boost 1.0 + +github.setup abusse sunshine macos-dev +version 20220224 + +categories multimedia +platforms darwin +license GPL-2 +maintainers {outlook.com:anselm.busse} + +fetch.type git +post-fetch { + system -W ${worksrcpath} "${git.cmd} submodule update --init --recursive" +} + +description Sunshine is a Gamestream host for Moonlight +long_description Sunshine is a Gamestream host for Moonlight + +homepage https://github.com/SunshineStream/Sunshine + +depends_lib port:avahi port:ffmpeg port:libopus + + +boost.version 1.76 + +configure.args -DBOOST_ROOT=[boost::install_area] \ + -DSUNSHINE_ASSETS_DIR=${prefix}/etc/sunshine + +cmake.out_of_source yes + +destroot { + xinstall -d -m 755 ${destroot}${prefix}/etc/${name} + xinstall ${worksrcpath}/assets/apps_mac.json ${destroot}${prefix}/etc/${name} + xinstall ${worksrcpath}/assets/box.png ${destroot}${prefix}/etc/${name} + xinstall ${worksrcpath}/assets/sunshine.conf ${destroot}${prefix}/etc/${name} + + xinstall -d -m 755 ${destroot}${prefix}/etc/${name}/web + xinstall {*}[glob ${worksrcpath}/assets/web/*.html] ${destroot}${prefix}/etc/${name}/web + xinstall -d -m 755 ${destroot}${prefix}/etc/${name}/web/third_party + xinstall {*}[glob ${worksrcpath}/assets/web/third_party/*] ${destroot}${prefix}/etc/${name}/web/third_party + + xinstall ${workpath}/build/${name} ${destroot}${prefix}/bin +} diff --git a/README.md b/README.md index a25a3da0912..72674eac740 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,7 @@ Sunshine is a Gamestream host for Moonlight # Building - [Linux](README.md#linux) +- [MacOS](README.md#macos) - [Windows](README.md#windows-10) ## Linux @@ -108,6 +109,53 @@ It's necessary to allow Sunshine to use KMS - If you use hardware acceleration on Linux using an Intel or an AMD GPU (with VAAPI), you will get tons of [graphical issues](https://github.com/loki-47-6F-64/sunshine/issues/228) if your ffmpeg version is < 4.3. If it is not available in your distribution's repositories, consider using a newer version of your distribution. - Ubuntu started to ship ffmpeg 4.3 starting with groovy (20.10). If you're using an older version, you could use [this PPA](https://launchpad.net/%7Esavoury1/+archive/ubuntu/ffmpeg4) instead of upgrading. **Using PPAs is dangerous and may break your system. Use it at your own risk.** +## macOS + +### Quickstart + +- Install [MacPorts](https://www.macports.org) +- Download the `Portfile` from this repository to `/tmp` +- In a Terminal run `cd /tmp && sudo port install` +- Sunshine configuration is in `/opt/local/etc` +- Run `sunshine` to start the Sunshine server +- You will be asked to grant access to screen recording and your microphone to be able to stream it + +### Manuel Build + +#### Requirements: +macOS Big Sur and Xcode 12.5+: + +Either, using [MacPorts](https://www.macports.org), install the following +``` +sudo port install cmake boost libopus ffmpeg +``` + +Or, using [Homebrew](https://brew.sh), install the follwoing: +``` +brew install boost cmake ffmpeg libopusenc +# if there are issues with an SSL header that is not found: +cd /usr/local/include +ln -s ../opt/openssl/include/openssl . +``` + +#### Compilation: +- `git clone https://github.com/SunshineStream/Sunshine.git --recurse-submodules` +- `cd sunshine && mkdir build && cd build` +- `cmake ..` +- `make -j ${nproc}` + +If cmake fails complaining to find Boost, try to set the path explicitly: `cmake -DBOOST_ROOT=[boost path] ..`, e.g., `cmake -DBOOST_ROOT=/opt/local/libexec/boost/1.76 ..` + +### Setup: +- Sunshine can only access microphones on macOS due to system limitations. To stream system audio use [Soundflower](https://github.com/mattingalls/Soundflower) or [BlackHole](https://github.com/ExistentialAudio/BlackHole) and select their sink as audio device in `sunshine.conf` +- `assets/sunshine.conf` is an example configuration file. Modify it as you see fit, then use it by running: + `sunshine path/to/sunshine.conf` +- `assets/apps.json` is an [example](README.md#application-list) of a list of applications that are started just before running a stream + +### Usage & Limitations: +- Command Keys are not forwarded by Moonlight. Right Option-Key is mapped to CMD-Key. +- Gamepads are not supported + ## Windows 10 ### Requirements: diff --git a/assets/apps_mac.json b/assets/apps_mac.json new file mode 100644 index 00000000000..746c69b3b5a --- /dev/null +++ b/assets/apps_mac.json @@ -0,0 +1,6 @@ +{ + "env":{ + "PATH":"$(PATH):$(HOME)/.local/bin" + }, + "apps":[ ] +} diff --git a/assets/info.plist b/assets/info.plist new file mode 100644 index 00000000000..c849435adc4 --- /dev/null +++ b/assets/info.plist @@ -0,0 +1,12 @@ + + + + + CFBundleIdentifier + com.github.sunshinestream.sunshine + CFBundleName + Sunshine + NSMicrophoneUsageDescription + This app requires access to your microphone to stream audio. + + diff --git a/assets/sunshine.conf b/assets/sunshine.conf index ccb20fad4f5..a1e020f03c9 100644 --- a/assets/sunshine.conf +++ b/assets/sunshine.conf @@ -155,6 +155,10 @@ # to stream audio, while muting the speakers. # virtual_sink = {0.0.0.00000000}.{8edba70c-1125-467c-b89c-15da389bc1d4} +# +# !! MacOS only !! +# audio_sink = BlackHole 2ch + # !! Windows only !! # You can select the video card you want to stream: # The appropriate values can be found using the following command: @@ -279,6 +283,30 @@ # VAProfileH264High : VAEntrypointEncSlice # adapter_name = /dev/dri/renderD128 +################################# VideoToolbox ############################### +####### software encoding ########## +# Video Toolbox can be allowed/required to use software encoding instead of +# hardware accelerated encoding. +# auto -- let sunshine decide on encoding +# disabled -- disable software encoding +# allowed -- allow software encoding +# forced -- force software encoding +########################## +# vt_software = auto +# +####### realtime encoding ########## +# Disabling realtime encoding might result in a delayed frame encoding or frame drop +########################## +# vt_realtime = enabled +# +###### h264/hevc entropy ###### +# auto -- let ffmpeg decide the entropy encoding +# cabac +# cavlc +########################## +# vt_coder = auto + + ############################################## # Some configurable parameters, are merely toggles for specific features # The first occurrence turns it on, the second occurence turns it off, the third occurence turns it on again, etc, etc diff --git a/assets/web/config.html b/assets/web/config.html index 12290d59e05..203e807fea5 100644 --- a/assets/web/config.html +++ b/assets/web/config.html @@ -712,6 +712,34 @@

Configuration

v-model="config.adapter_name" /> + +
+ +
+ + +
+
+ + +
+
+ + +
+
Success! Restart Sunshine to apply changes @@ -771,6 +799,10 @@

Configuration

id: "va-api", name: "VA-API encoder", }, + { + id: "vt", + name: "VideoToolbox encoder", + }, ], }; }, @@ -812,6 +844,9 @@

Configuration

this.config.nv_coder = this.config.nv_coder || "auto"; this.config.amd_quality = this.config.amd_quality || "default"; this.config.amd_rc = this.config.amd_rc || "auto"; + this.config.vt_coder = this.config.vt_coder || "auto"; + this.config.vt_software = this.config.vt_software || "auto"; + this.config.vt_realtime = this.config.vt_realtime || "enabled"; this.config.fps = this.config.fps || "[10, 30, 60, 90, 120]"; this.config.resolutions = this.config.resolutions || diff --git a/gen-deb.in b/gen-deb.in index 39d585d48b7..25ca36096f9 100755 --- a/gen-deb.in +++ b/gen-deb.in @@ -37,7 +37,7 @@ Package: sunshine Architecture: amd64 Maintainer: @loki Priority: optional -Version: 0.12.0 +Version: @PROJECT_VERSION@ Depends: libssl1.1, libavdevice58, libboost-thread1.67.0 | libboost-thread1.71.0 | libboost-thread1.74.0, libboost-filesystem1.67.0 | libboost-filesystem1.71.0 | libboost-filesystem1.74.0, libboost-log1.67.0 | libboost-log1.71.0 | libboost-log1.74.0, libpulse0, libopus0, libxcb-shm0, libxcb-xfixes0, libxtst6, libevdev2, libdrm2, libcap2 Description: Gamestream host for Moonlight EOF diff --git a/sunshine.desktop b/sunshine.desktop.in similarity index 85% rename from sunshine.desktop rename to sunshine.desktop.in index 8773a035f82..195bade32fb 100644 --- a/sunshine.desktop +++ b/sunshine.desktop.in @@ -8,5 +8,5 @@ Icon=sunshine Categories=Utility; Terminal=true X-AppImage-Name=sunshine -X-AppImage-Version=0.12.0 +X-AppImage-Version=@PROJECT_VERSION@ X-AppImage-Arch=x86_64 diff --git a/sunshine/config.cpp b/sunshine/config.cpp index a35d59551d2..6aaeb3058e2 100644 --- a/sunshine/config.cpp +++ b/sunshine/config.cpp @@ -162,6 +162,42 @@ int coder_from_view(const std::string_view &coder) { } } // namespace amd +namespace vt { + +enum coder_e : int { + _auto = 0, + cabac, + cavlc +}; + +int coder_from_view(const std::string_view &coder) { + if(coder == "auto"sv) return _auto; + if(coder == "cabac"sv || coder == "ac"sv) return cabac; + if(coder == "cavlc"sv || coder == "vlc"sv) return cavlc; + + return -1; +} + +int allow_software_from_view(const std::string_view &software) { + if(software == "allowed"sv || software == "forced") return 1; + + return 0; +} + +int force_software_from_view(const std::string_view &software) { + if(software == "forced") return 1; + + return 0; +} + +int rt_from_view(const std::string_view &rt) { + if(rt == "disabled" || rt == "off" || rt == "0") return 0; + + return 1; +} + +} // namespace vt + video_t video { 28, // qp @@ -685,6 +721,14 @@ void apply_config(std::unordered_map &&vars) { video.amd.rc_hevc = amd::rc_hevc_from_view(rc); } + int_f(vars, "vt_coder", video.vt.coder, vt::coder_from_view); + video.vt.allow_sw = 0; + int_f(vars, "vt_software", video.vt.allow_sw, vt::allow_software_from_view); + video.vt.require_sw = 0; + int_f(vars, "vt_software", video.vt.require_sw, vt::force_software_from_view); + video.vt.realtime = 1; + int_f(vars, "vt_realtime", video.vt.realtime, vt::rt_from_view); + string_f(vars, "encoder", video.encoder); string_f(vars, "adapter_name", video.adapter_name); string_f(vars, "output_name", video.output_name); diff --git a/sunshine/config.h b/sunshine/config.h index 624e422589f..183139612dc 100644 --- a/sunshine/config.h +++ b/sunshine/config.h @@ -34,6 +34,13 @@ struct video_t { int coder; } amd; + struct { + int allow_sw; + int require_sw; + int realtime; + int coder; + } vt; + std::string encoder; std::string adapter_name; std::string output_name; diff --git a/sunshine/confighttp.cpp b/sunshine/confighttp.cpp index f90b6d963e6..c21089de21f 100644 --- a/sunshine/confighttp.cpp +++ b/sunshine/confighttp.cpp @@ -93,8 +93,8 @@ bool authenticate(resp_https_t response, req_https_t request) { } //If credentials are shown, redirect the user to a /welcome page - if(config::sunshine.username.empty()){ - send_redirect(response,request,"/welcome"); + if(config::sunshine.username.empty()) { + send_redirect(response, request, "/welcome"); return false; } @@ -202,8 +202,8 @@ void getPasswordPage(resp_https_t response, req_https_t request) { void getWelcomePage(resp_https_t response, req_https_t request) { print_req(request); - if(!config::sunshine.username.empty()){ - send_redirect(response,request,"/"); + if(!config::sunshine.username.empty()) { + send_redirect(response, request, "/"); return; } std::string header = read_file(WEB_DIR "header-no-nav.html"); @@ -496,16 +496,18 @@ void savePassword(resp_https_t response, req_https_t request) { auto newPassword = inputTree.count("newPassword") > 0 ? inputTree.get("newPassword") : ""; auto confirmPassword = inputTree.count("confirmNewPassword") > 0 ? inputTree.get("confirmNewPassword") : ""; if(newUsername.length() == 0) newUsername = username; - if(newUsername.length() == 0){ + if(newUsername.length() == 0) { outputTree.put("status", false); outputTree.put("error", "Invalid Username"); - } else { + } + else { auto hash = util::hex(crypto::hash(password + config::sunshine.salt)).to_string(); if(config::sunshine.username.empty() || (username == config::sunshine.username && hash == config::sunshine.password)) { if(newPassword.empty() || newPassword != confirmPassword) { outputTree.put("status", false); outputTree.put("error", "Password Mismatch"); - } else { + } + else { http::save_user_creds(config::sunshine.credentials_file, newUsername, newPassword); http::reload_user_creds(config::sunshine.credentials_file); outputTree.put("status", true); @@ -555,9 +557,9 @@ void savePin(resp_https_t response, req_https_t request) { } } -void unpairAll(resp_https_t response, req_https_t request){ +void unpairAll(resp_https_t response, req_https_t request) { if(!authenticate(response, request)) return; - + print_req(request); pt::ptree outputTree; @@ -571,9 +573,9 @@ void unpairAll(resp_https_t response, req_https_t request){ outputTree.put("status", true); } -void closeApp(resp_https_t response, req_https_t request){ +void closeApp(resp_https_t response, req_https_t request) { if(!authenticate(response, request)) return; - + print_req(request); pt::ptree outputTree; @@ -597,35 +599,35 @@ void start() { ctx->use_certificate_chain_file(config::nvhttp.cert); ctx->use_private_key_file(config::nvhttp.pkey, boost::asio::ssl::context::pem); https_server_t server { ctx, 0 }; - server.default_resource = not_found; - server.resource["^/$"]["GET"] = getIndexPage; - server.resource["^/pin$"]["GET"] = getPinPage; - server.resource["^/apps$"]["GET"] = getAppsPage; - server.resource["^/clients$"]["GET"] = getClientsPage; - server.resource["^/config$"]["GET"] = getConfigPage; - server.resource["^/password$"]["GET"] = getPasswordPage; - server.resource["^/welcome$"]["GET"] = getWelcomePage; - server.resource["^/troubleshooting$"]["GET"] = getTroubleshootingPage; - server.resource["^/api/pin"]["POST"] = savePin; - server.resource["^/api/apps$"]["GET"] = getApps; - server.resource["^/api/apps$"]["POST"] = saveApp; - server.resource["^/api/config$"]["GET"] = getConfig; - server.resource["^/api/config$"]["POST"] = saveConfig; - server.resource["^/api/password$"]["POST"] = savePassword; - server.resource["^/api/apps/([0-9]+)$"]["DELETE"] = deleteApp; - server.resource["^/api/clients/unpair$"]["POST"] = unpairAll; - server.resource["^/api/apps/close"]["POST"] = closeApp; - server.resource["^/images/favicon.ico$"]["GET"] = getFaviconImage; - server.resource["^/images/logo-sunshine-45.png$"]["GET"] = getSunshineLogoImage; - server.resource["^/third_party/bootstrap.min.css$"]["GET"] = getBootstrapCss; - server.resource["^/third_party/bootstrap.bundle.min.js$"]["GET"] = getBootstrapJs; - server.resource["^/fontawesome/css/all.min.css$"]["GET"] = getFontAwesomeCss; - server.resource["^/fontawesome/webfonts/fa-brands-400.ttf$"]["GET"] = getFontAwesomeBrands; - server.resource["^/fontawesome/webfonts/fa-solid-900.ttf$"]["GET"] = getFontAwesomeSolid; - server.resource["^/third_party/vue.js$"]["GET"] = getVueJs; - server.config.reuse_address = true; - server.config.address = "0.0.0.0"s; - server.config.port = port_https; + server.default_resource = not_found; + server.resource["^/$"]["GET"] = getIndexPage; + server.resource["^/pin$"]["GET"] = getPinPage; + server.resource["^/apps$"]["GET"] = getAppsPage; + server.resource["^/clients$"]["GET"] = getClientsPage; + server.resource["^/config$"]["GET"] = getConfigPage; + server.resource["^/password$"]["GET"] = getPasswordPage; + server.resource["^/welcome$"]["GET"] = getWelcomePage; + server.resource["^/troubleshooting$"]["GET"] = getTroubleshootingPage; + server.resource["^/api/pin"]["POST"] = savePin; + server.resource["^/api/apps$"]["GET"] = getApps; + server.resource["^/api/apps$"]["POST"] = saveApp; + server.resource["^/api/config$"]["GET"] = getConfig; + server.resource["^/api/config$"]["POST"] = saveConfig; + server.resource["^/api/password$"]["POST"] = savePassword; + server.resource["^/api/apps/([0-9]+)$"]["DELETE"] = deleteApp; + server.resource["^/api/clients/unpair$"]["POST"] = unpairAll; + server.resource["^/api/apps/close"]["POST"] = closeApp; + server.resource["^/images/favicon.ico$"]["GET"] = getFaviconImage; + server.resource["^/images/logo-sunshine-45.png$"]["GET"] = getSunshineLogoImage; + server.resource["^/third_party/bootstrap.min.css$"]["GET"] = getBootstrapCss; + server.resource["^/third_party/bootstrap.bundle.min.js$"]["GET"] = getBootstrapJs; + server.resource["^/fontawesome/css/all.min.css$"]["GET"] = getFontAwesomeCss; + server.resource["^/fontawesome/webfonts/fa-brands-400.ttf$"]["GET"] = getFontAwesomeBrands; + server.resource["^/fontawesome/webfonts/fa-solid-900.ttf$"]["GET"] = getFontAwesomeSolid; + server.resource["^/third_party/vue.js$"]["GET"] = getVueJs; + server.config.reuse_address = true; + server.config.address = "0.0.0.0"s; + server.config.port = port_https; try { server.bind(); diff --git a/sunshine/httpcommon.cpp b/sunshine/httpcommon.cpp index 14249456c57..9107414495d 100644 --- a/sunshine/httpcommon.cpp +++ b/sunshine/httpcommon.cpp @@ -56,7 +56,8 @@ int init() { } if(user_creds_exist(config::sunshine.credentials_file)) { if(reload_user_creds(config::sunshine.credentials_file)) return -1; - } else { + } + else { BOOST_LOG(info) << "Open the Web UI to set your new username and password and getting started"; } return 0; diff --git a/sunshine/input.cpp b/sunshine/input.cpp index 2a7e4486bdd..050a8687b86 100644 --- a/sunshine/input.cpp +++ b/sunshine/input.cpp @@ -9,6 +9,7 @@ extern "C" { } #include +#include #include "config.h" #include "input.h" diff --git a/sunshine/main.cpp b/sunshine/main.cpp index b5126f6692f..bcab4b3bec9 100644 --- a/sunshine/main.cpp +++ b/sunshine/main.cpp @@ -24,8 +24,8 @@ #include "rtsp.h" #include "thread_pool.h" #include "upnp.h" -#include "video.h" #include "version.h" +#include "video.h" #include "platform/common.h" extern "C" { diff --git a/sunshine/nvhttp.cpp b/sunshine/nvhttp.cpp index 8afdb35a918..5da896e2416 100644 --- a/sunshine/nvhttp.cpp +++ b/sunshine/nvhttp.cpp @@ -765,7 +765,7 @@ void cancel(resp_https_t response, req_https_t request) { void appasset(resp_https_t response, req_https_t request) { print_req(request); - auto args = request->parse_query_string(); + auto args = request->parse_query_string(); auto app_image = proc::proc.get_app_image(util::from_view(args.at("appid"))); std::ifstream in(app_image, std::ios::binary); @@ -934,7 +934,7 @@ void start() { tcp.join(); } -void erase_all_clients(){ +void erase_all_clients() { map_id_client.clear(); save_state(); } diff --git a/sunshine/platform/common.h b/sunshine/platform/common.h index 9d4657492eb..4d1588a3e57 100644 --- a/sunshine/platform/common.h +++ b/sunshine/platform/common.h @@ -161,7 +161,7 @@ struct sink_t { // Play on host PC std::string host; - // On Windows, it is not possible to create a virtual sink + // On MacOS and Windows, it is not possible to create a virtual sink // Therefore, it is optional struct null_t { std::string stereo; diff --git a/sunshine/platform/linux/audio.cpp b/sunshine/platform/linux/audio.cpp index facb5f3b9d4..c210da246b0 100644 --- a/sunshine/platform/linux/audio.cpp +++ b/sunshine/platform/linux/audio.cpp @@ -77,7 +77,7 @@ std::unique_ptr microphone(const std::uint8_t *mapping, int channels, std }); pa_buffer_attr pa_attr = {}; - pa_attr.maxlength = frame_size * 8; + pa_attr.maxlength = frame_size * 8; int status; diff --git a/sunshine/platform/linux/x11grab.h b/sunshine/platform/linux/x11grab.h index 3d2868c8dbe..9fde2664740 100644 --- a/sunshine/platform/linux/x11grab.h +++ b/sunshine/platform/linux/x11grab.h @@ -21,7 +21,7 @@ void freeCursorCtx(cursor_ctx_raw_t *ctx); void freeDisplay(_XDisplay *xdisplay); using cursor_ctx_t = util::safe_ptr; -using xdisplay_t = util::safe_ptr<_XDisplay, freeDisplay>; +using xdisplay_t = util::safe_ptr<_XDisplay, freeDisplay>; class cursor_t { public: diff --git a/sunshine/platform/macos/av_audio.h b/sunshine/platform/macos/av_audio.h new file mode 100644 index 00000000000..8c04e5bb7b3 --- /dev/null +++ b/sunshine/platform/macos/av_audio.h @@ -0,0 +1,26 @@ +#ifndef SUNSHINE_PLATFORM_AV_AUDIO_H +#define SUNSHINE_PLATFORM_AV_AUDIO_H + +#import + +#include "sunshine/platform/macos/TPCircularBuffer/TPCircularBuffer.h" + +#define kBufferLength 2048 + +@interface AVAudio : NSObject { +@public + TPCircularBuffer audioSampleBuffer; +} + +@property(nonatomic, assign) AVCaptureSession *audioCaptureSession; +@property(nonatomic, assign) AVCaptureConnection *audioConnection; +@property(nonatomic, assign) NSCondition *samplesArrivedSignal; + ++ (NSArray *)microphoneNames; ++ (AVCaptureDevice *)findMicrophone:(NSString *)name; + +- (int)setupMicrophone:(AVCaptureDevice *)device sampleRate:(UInt32)sampleRate frameSize:(UInt32)frameSize channels:(UInt8)channels; + +@end + +#endif //SUNSHINE_PLATFORM_AV_AUDIO_H diff --git a/sunshine/platform/macos/av_audio.m b/sunshine/platform/macos/av_audio.m new file mode 100644 index 00000000000..c5cd899ef55 --- /dev/null +++ b/sunshine/platform/macos/av_audio.m @@ -0,0 +1,120 @@ +#import "av_audio.h" + +@implementation AVAudio + ++ (NSArray *)microphones { + AVCaptureDeviceDiscoverySession *discoverySession = [AVCaptureDeviceDiscoverySession discoverySessionWithDeviceTypes:@[AVCaptureDeviceTypeBuiltInMicrophone, + AVCaptureDeviceTypeExternalUnknown] + mediaType:AVMediaTypeAudio + position:AVCaptureDevicePositionUnspecified]; + return discoverySession.devices; +} + ++ (NSArray *)microphoneNames { + NSMutableArray *result = [[NSMutableArray alloc] init]; + + for(AVCaptureDevice *device in [AVAudio microphones]) { + [result addObject:[device localizedName]]; + } + + return result; +} + ++ (AVCaptureDevice *)findMicrophone:(NSString *)name { + for(AVCaptureDevice *device in [AVAudio microphones]) { + if([[device localizedName] isEqualToString:name]) { + return device; + } + } + + return nil; +} + +- (void)dealloc { + // make sure we don't process any further samples + self.audioConnection = nil; + // make sure nothing gets stuck on this signal + [self.samplesArrivedSignal signal]; + [self.samplesArrivedSignal release]; + TPCircularBufferCleanup(&audioSampleBuffer); + [super dealloc]; +} + +- (int)setupMicrophone:(AVCaptureDevice *)device sampleRate:(UInt32)sampleRate frameSize:(UInt32)frameSize channels:(UInt8)channels { + self.audioCaptureSession = [[AVCaptureSession alloc] init]; + + NSError *error; + AVCaptureDeviceInput *audioInput = [AVCaptureDeviceInput deviceInputWithDevice:device error:&error]; + if(audioInput == nil) { + return -1; + } + + if([self.audioCaptureSession canAddInput:audioInput]) { + [self.audioCaptureSession addInput:audioInput]; + } + else { + [audioInput dealloc]; + return -1; + } + + AVCaptureAudioDataOutput *audioOutput = [[AVCaptureAudioDataOutput alloc] init]; + + [audioOutput setAudioSettings:@{ + (NSString *)AVFormatIDKey: [NSNumber numberWithUnsignedInt:kAudioFormatLinearPCM], + (NSString *)AVSampleRateKey: [NSNumber numberWithUnsignedInt:sampleRate], + (NSString *)AVNumberOfChannelsKey: [NSNumber numberWithUnsignedInt:channels], + (NSString *)AVLinearPCMBitDepthKey: [NSNumber numberWithUnsignedInt:16], + (NSString *)AVLinearPCMIsFloatKey: @NO, + (NSString *)AVLinearPCMIsNonInterleaved: @NO + }]; + + dispatch_queue_attr_t qos = dispatch_queue_attr_make_with_qos_class(DISPATCH_QUEUE_CONCURRENT, + QOS_CLASS_USER_INITIATED, + DISPATCH_QUEUE_PRIORITY_HIGH); + dispatch_queue_t recordingQueue = dispatch_queue_create("audioSamplingQueue", qos); + + [audioOutput setSampleBufferDelegate:self queue:recordingQueue]; + + if([self.audioCaptureSession canAddOutput:audioOutput]) { + [self.audioCaptureSession addOutput:audioOutput]; + } + else { + [audioInput release]; + [audioOutput release]; + return -1; + } + + self.audioConnection = [audioOutput connectionWithMediaType:AVMediaTypeAudio]; + + [self.audioCaptureSession startRunning]; + + [audioInput release]; + [audioOutput release]; + + self.samplesArrivedSignal = [[NSCondition alloc] init]; + TPCircularBufferInit(&self->audioSampleBuffer, kBufferLength * channels); + + return 0; +} + +- (void)captureOutput:(AVCaptureOutput *)output + didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer + fromConnection:(AVCaptureConnection *)connection { + if(connection == self.audioConnection) { + AudioBufferList audioBufferList; + CMBlockBufferRef blockBuffer; + + 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); + + // this is safe, because an interleaved PCM stream has exactly one buffer + // and we don't want to do sanity checks in a performance critical exec path + AudioBuffer audioBuffer = audioBufferList.mBuffers[0]; + + TPCircularBufferProduceBytes(&self->audioSampleBuffer, audioBuffer.mData, audioBuffer.mDataByteSize); + [self.samplesArrivedSignal signal]; + } +} + +@end diff --git a/sunshine/platform/macos/av_img_t.h b/sunshine/platform/macos/av_img_t.h new file mode 100644 index 00000000000..7af3cbcc839 --- /dev/null +++ b/sunshine/platform/macos/av_img_t.h @@ -0,0 +1,18 @@ +#ifndef av_img_t_h +#define av_img_t_h + +#include "sunshine/platform/common.h" + +#include +#include + +namespace platf { +struct av_img_t : public img_t { + CVPixelBufferRef pixel_buffer = nullptr; + CMSampleBufferRef sample_buffer = nullptr; + + ~av_img_t(); +}; +} // namespace platf + +#endif /* av_img_t_h */ diff --git a/sunshine/platform/macos/av_video.h b/sunshine/platform/macos/av_video.h new file mode 100644 index 00000000000..df441cae8f7 --- /dev/null +++ b/sunshine/platform/macos/av_video.h @@ -0,0 +1,43 @@ +#ifndef SUNSHINE_PLATFORM_AV_VIDEO_H +#define SUNSHINE_PLATFORM_AV_VIDEO_H + +#import + + +struct CaptureSession { + AVCaptureVideoDataOutput *output; + NSCondition *captureStopped; +}; + +@interface AVVideo : NSObject + +#define kMaxDisplays 32 + +@property(nonatomic, assign) CGDirectDisplayID displayID; +@property(nonatomic, assign) CMTime minFrameDuration; +@property(nonatomic, assign) OSType pixelFormat; +@property(nonatomic, assign) int frameWidth; +@property(nonatomic, assign) int frameHeight; +@property(nonatomic, assign) float scaling; +@property(nonatomic, assign) int paddingLeft; +@property(nonatomic, assign) int paddingRight; +@property(nonatomic, assign) int paddingTop; +@property(nonatomic, assign) int paddingBottom; + +typedef bool (^FrameCallbackBlock)(CMSampleBufferRef); + +@property(nonatomic, assign) AVCaptureSession *session; +@property(nonatomic, assign) NSMapTable *videoOutputs; +@property(nonatomic, assign) NSMapTable *captureCallbacks; +@property(nonatomic, assign) NSMapTable *captureSignals; + ++ (NSArray *)displayNames; + +- (id)initWithDisplay:(CGDirectDisplayID)displayID frameRate:(int)frameRate; + +- (void)setFrameWidth:(int)frameWidth frameHeight:(int)frameHeight; +- (dispatch_semaphore_t)capture:(FrameCallbackBlock)frameCallback; + +@end + +#endif //SUNSHINE_PLATFORM_AV_VIDEO_H diff --git a/sunshine/platform/macos/av_video.m b/sunshine/platform/macos/av_video.m new file mode 100644 index 00000000000..f257d9c4a02 --- /dev/null +++ b/sunshine/platform/macos/av_video.m @@ -0,0 +1,184 @@ +#import "av_video.h" + +@implementation AVVideo + +// XXX: Currently, this function only returns the screen IDs as names, +// which is not very helpful to the user. The API to retrieve names +// was deprecated with 10.9+. +// However, there is a solution with little external code that can be used: +// https://stackoverflow.com/questions/20025868/cgdisplayioserviceport-is-deprecated-in-os-x-10-9-how-to-replace ++ (NSArray *)displayNames { + CGDirectDisplayID displays[kMaxDisplays]; + uint32_t count; + if(CGGetActiveDisplayList(kMaxDisplays, displays, &count) != kCGErrorSuccess) { + return [NSArray array]; + } + + NSMutableArray *result = [NSMutableArray array]; + + for(uint32_t i = 0; i < count; i++) { + [result addObject:@{ + @"id": [NSNumber numberWithUnsignedInt:displays[i]], + @"name": [NSString stringWithFormat:@"%d", displays[i]] + }]; + } + + return [NSArray arrayWithArray:result]; +} + +- (id)initWithDisplay:(CGDirectDisplayID)displayID frameRate:(int)frameRate { + self = [super init]; + + CGDisplayModeRef mode = CGDisplayCopyDisplayMode(displayID); + + self.displayID = displayID; + self.pixelFormat = kCVPixelFormatType_32BGRA; + self.frameWidth = CGDisplayModeGetPixelWidth(mode); + self.frameHeight = CGDisplayModeGetPixelHeight(mode); + self.scaling = CGDisplayPixelsWide(displayID) / CGDisplayModeGetPixelWidth(mode); + self.paddingLeft = 0; + self.paddingRight = 0; + self.paddingTop = 0; + self.paddingBottom = 0; + self.minFrameDuration = CMTimeMake(1, frameRate); + self.session = [[AVCaptureSession alloc] init]; + self.videoOutputs = [[NSMapTable alloc] init]; + self.captureCallbacks = [[NSMapTable alloc] init]; + self.captureSignals = [[NSMapTable alloc] init]; + + CFRelease(mode); + + AVCaptureScreenInput *screenInput = [[AVCaptureScreenInput alloc] initWithDisplayID:self.displayID]; + [screenInput setMinFrameDuration:self.minFrameDuration]; + + if([self.session canAddInput:screenInput]) { + [self.session addInput:screenInput]; + } + else { + [screenInput release]; + return nil; + } + + [self.session startRunning]; + + return self; +} + +- (void)dealloc { + [self.videoOutputs release]; + [self.captureCallbacks release]; + [self.captureSignals release]; + [self.session stopRunning]; + [super dealloc]; +} + +- (void)setFrameWidth:(int)frameWidth frameHeight:(int)frameHeight { + CGImageRef screenshot = CGDisplayCreateImage(self.displayID); + + self.frameWidth = frameWidth; + self.frameHeight = frameHeight; + + double screenRatio = (double)CGImageGetWidth(screenshot) / (double)CGImageGetHeight(screenshot); + double streamRatio = (double)frameWidth / (double)frameHeight; + + if(screenRatio < streamRatio) { + int padding = frameWidth - (frameHeight * screenRatio); + self.paddingLeft = padding / 2; + self.paddingRight = padding - self.paddingLeft; + self.paddingTop = 0; + self.paddingBottom = 0; + } + else { + int padding = frameHeight - (frameWidth / screenRatio); + self.paddingLeft = 0; + self.paddingRight = 0; + self.paddingTop = padding / 2; + self.paddingBottom = padding - self.paddingTop; + } + + // XXX: if the streamed image is larger than the native resolution, we add a black box around + // the frame. Instead the frame should be resized entirely. + int delta_width = frameWidth - (CGImageGetWidth(screenshot) + self.paddingLeft + self.paddingRight); + if(delta_width > 0) { + int adjust_left = delta_width / 2; + int adjust_right = delta_width - adjust_left; + self.paddingLeft += adjust_left; + self.paddingRight += adjust_right; + } + + int delta_height = frameHeight - (CGImageGetHeight(screenshot) + self.paddingTop + self.paddingBottom); + if(delta_height > 0) { + int adjust_top = delta_height / 2; + int adjust_bottom = delta_height - adjust_top; + self.paddingTop += adjust_top; + self.paddingBottom += adjust_bottom; + } + + CFRelease(screenshot); +} + +- (dispatch_semaphore_t)capture:(FrameCallbackBlock)frameCallback { + @synchronized(self) { + AVCaptureVideoDataOutput *videoOutput = [[AVCaptureVideoDataOutput alloc] init]; + + [videoOutput setVideoSettings:@{ + (NSString *)kCVPixelBufferPixelFormatTypeKey: [NSNumber numberWithUnsignedInt:self.pixelFormat], + (NSString *)kCVPixelBufferWidthKey: [NSNumber numberWithInt:self.frameWidth], + (NSString *)kCVPixelBufferExtendedPixelsRightKey: [NSNumber numberWithInt:self.paddingRight], + (NSString *)kCVPixelBufferExtendedPixelsLeftKey: [NSNumber numberWithInt:self.paddingLeft], + (NSString *)kCVPixelBufferExtendedPixelsTopKey: [NSNumber numberWithInt:self.paddingTop], + (NSString *)kCVPixelBufferExtendedPixelsBottomKey: [NSNumber numberWithInt:self.paddingBottom], + (NSString *)kCVPixelBufferHeightKey: [NSNumber numberWithInt:self.frameHeight] + }]; + + dispatch_queue_attr_t qos = dispatch_queue_attr_make_with_qos_class(DISPATCH_QUEUE_SERIAL, + QOS_CLASS_USER_INITIATED, + DISPATCH_QUEUE_PRIORITY_HIGH); + dispatch_queue_t recordingQueue = dispatch_queue_create("videoCaptureQueue", qos); + [videoOutput setSampleBufferDelegate:self queue:recordingQueue]; + + [self.session stopRunning]; + + if([self.session canAddOutput:videoOutput]) { + [self.session addOutput:videoOutput]; + } + else { + [videoOutput release]; + return nil; + } + + AVCaptureConnection *videoConnection = [videoOutput connectionWithMediaType:AVMediaTypeVideo]; + dispatch_semaphore_t signal = dispatch_semaphore_create(0); + + [self.videoOutputs setObject:videoOutput forKey:videoConnection]; + [self.captureCallbacks setObject:frameCallback forKey:videoConnection]; + [self.captureSignals setObject:signal forKey:videoConnection]; + + [self.session startRunning]; + + return signal; + } +} + +- (void)captureOutput:(AVCaptureOutput *)captureOutput + didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer + fromConnection:(AVCaptureConnection *)connection { + + FrameCallbackBlock callback = [self.captureCallbacks objectForKey:connection]; + + if(callback != nil) { + if(!callback(sampleBuffer)) { + @synchronized(self) { + [self.session stopRunning]; + [self.captureCallbacks removeObjectForKey:connection]; + [self.session removeOutput:[self.videoOutputs objectForKey:connection]]; + [self.videoOutputs removeObjectForKey:connection]; + dispatch_semaphore_signal([self.captureSignals objectForKey:connection]); + [self.captureSignals removeObjectForKey:connection]; + [self.session startRunning]; + } + } + } +} + +@end diff --git a/sunshine/platform/macos/display.mm b/sunshine/platform/macos/display.mm new file mode 100644 index 00000000000..f00297c2979 --- /dev/null +++ b/sunshine/platform/macos/display.mm @@ -0,0 +1,196 @@ +#include "sunshine/platform/common.h" +#include "sunshine/platform/macos/av_img_t.h" +#include "sunshine/platform/macos/av_video.h" +#include "sunshine/platform/macos/nv12_zero_device.h" + +#include "sunshine/config.h" +#include "sunshine/main.h" + +namespace fs = std::filesystem; + +namespace platf { +using namespace std::literals; + +av_img_t::~av_img_t() { + if(pixel_buffer != NULL) { + CVPixelBufferUnlockBaseAddress(pixel_buffer, 0); + } + + if(sample_buffer != nullptr) { + CFRelease(sample_buffer); + } + + data = nullptr; +} + +struct av_display_t : public display_t { + AVVideo *av_capture; + CGDirectDisplayID display_id; + + ~av_display_t() { + [av_capture release]; + } + + capture_e capture(snapshot_cb_t &&snapshot_cb, std::shared_ptr img, bool *cursor) override { + __block auto img_next = std::move(img); + + 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); + + if(av_img_next->sample_buffer != nullptr) + CFRelease(av_img_next->sample_buffer); + + av_img_next->sample_buffer = sampleBuffer; + av_img_next->pixel_buffer = pixelBuffer; + img_next->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_next = snapshot_cb(img_next); + + return img_next != nullptr; + }]; + + dispatch_semaphore_wait(signal, DISPATCH_TIME_FOREVER); + + return capture_e::ok; + } + + std::shared_ptr alloc_img() override { + return std::make_shared(); + } + + std::shared_ptr make_hwdevice(pix_fmt_e pix_fmt) override { + if(pix_fmt == pix_fmt_e::yuv420p) { + av_capture.pixelFormat = kCVPixelFormatType_32BGRA; + + return std::make_shared(); + } + else if(pix_fmt == pix_fmt_e::nv12) { + auto device = std::make_shared(); + + device->init(static_cast(av_capture), setResolution, setPixelFormat); + + return device; + } + else { + BOOST_LOG(error) << "Unsupported Pixel Format."sv; + return nullptr; + } + } + + int dummy_img(img_t *img) override { + auto signal = [av_capture capture:^(CMSampleBufferRef sampleBuffer) { + auto av_img = (av_img_t *)img; + + CFRetain(sampleBuffer); + + CVPixelBufferRef pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer); + CVPixelBufferLockBaseAddress(pixelBuffer, kCVPixelBufferLock_ReadOnly); + + // XXX: next_img->img should be moved to a smart pointer with + // the CFRelease as custom deallocator + if(av_img->pixel_buffer != nullptr) + CVPixelBufferUnlockBaseAddress(((av_img_t *)img)->pixel_buffer, 0); + + if(av_img->sample_buffer != nullptr) + CFRelease(av_img->sample_buffer); + + av_img->sample_buffer = sampleBuffer; + av_img->pixel_buffer = pixelBuffer; + img->data = (uint8_t *)CVPixelBufferGetBaseAddress(pixelBuffer); + + size_t extraPixels[4]; + CVPixelBufferGetExtendedPixels(pixelBuffer, &extraPixels[0], &extraPixels[1], &extraPixels[2], &extraPixels[3]); + + img->width = CVPixelBufferGetWidth(pixelBuffer) + extraPixels[0] + extraPixels[1]; + img->height = CVPixelBufferGetHeight(pixelBuffer) + extraPixels[2] + extraPixels[3]; + img->row_pitch = CVPixelBufferGetBytesPerRow(pixelBuffer); + img->pixel_pitch = img->row_pitch / img->width; + + return false; + }]; + + dispatch_semaphore_wait(signal, DISPATCH_TIME_FOREVER); + + return 0; + } + + /** + * 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]; + } + + static void setPixelFormat(void *display, OSType pixelFormat) { + static_cast(display).pixelFormat = pixelFormat; + } +}; + +std::shared_ptr display(platf::mem_type_e hwdevice_type, const std::string &display_name, int framerate) { + if(hwdevice_type != platf::mem_type_e::system) { + BOOST_LOG(error) << "Could not initialize display with the given hw device type."sv; + return nullptr; + } + + auto display = std::make_shared(); + + display->display_id = CGMainDisplayID(); + if(!display_name.empty()) { + auto display_array = [AVVideo displayNames]; + + for(NSDictionary *item in display_array) { + NSString *name = item[@"name"]; + if(name.UTF8String == display_name) { + NSNumber *display_id = item[@"id"]; + display->display_id = [display_id unsignedIntValue]; + } + } + } + + display->av_capture = [[AVVideo alloc] initWithDisplay:display->display_id frameRate:framerate]; + + if(!display->av_capture) { + BOOST_LOG(error) << "Video setup failed."sv; + return nullptr; + } + + display->width = display->av_capture.frameWidth; + display->height = display->av_capture.frameHeight; + + return display; +} + +std::vector display_names(mem_type_e hwdevice_type) { + __block std::vector display_names; + + auto display_array = [AVVideo displayNames]; + + display_names.reserve([display_array count]); + [display_array enumerateObjectsUsingBlock:^(NSDictionary *_Nonnull obj, NSUInteger idx, BOOL *_Nonnull stop) { + NSString *name = obj[@"name"]; + display_names.push_back(name.UTF8String); + }]; + + return display_names; +} +} diff --git a/sunshine/platform/macos/input.cpp b/sunshine/platform/macos/input.cpp new file mode 100644 index 00000000000..fbea6619730 --- /dev/null +++ b/sunshine/platform/macos/input.cpp @@ -0,0 +1,465 @@ +#import +#include +#include + +#include "sunshine/main.h" +#include "sunshine/platform/common.h" +#include "sunshine/utility.h" + +// Delay for a double click +// FIXME: we probably want to make this configurable +#define MULTICLICK_DELAY_NS 500000000 + +namespace platf { +using namespace std::literals; + +struct macos_input_t { +public: + CGDirectDisplayID display; + CGFloat displayScaling; + CGEventSourceRef source; + + // keyboard related stuff + CGEventRef kb_event; + CGEventFlags kb_flags; + + // mouse related stuff + CGEventRef mouse_event; // mouse event source + bool mouse_down[3]; // mouse button status + uint64_t last_mouse_event[3][2]; // timestamp of last mouse events +}; + +// A struct to hold a Windows keycode to Mac virtual keycode mapping. +struct KeyCodeMap { + int win_keycode; + int mac_keycode; +}; + +// Customized less operator for using std::lower_bound() on a KeyCodeMap array. +bool operator<(const KeyCodeMap &a, const KeyCodeMap &b) { + return a.win_keycode < b.win_keycode; +} + +// clang-format off +const KeyCodeMap kKeyCodesMap[] = { + { 0x08 /* VKEY_BACK */, kVK_Delete }, + { 0x09 /* VKEY_TAB */, kVK_Tab }, + { 0x0A /* VKEY_BACKTAB */, 0x21E4 }, + { 0x0C /* VKEY_CLEAR */, kVK_ANSI_KeypadClear }, + { 0x0D /* VKEY_RETURN */, kVK_Return }, + { 0x10 /* VKEY_SHIFT */, kVK_Shift }, + { 0x11 /* VKEY_CONTROL */, kVK_Control }, + { 0x12 /* VKEY_MENU */, kVK_Option }, + { 0x13 /* VKEY_PAUSE */, -1 }, + { 0x14 /* VKEY_CAPITAL */, kVK_CapsLock }, + { 0x15 /* VKEY_KANA */, kVK_JIS_Kana }, + { 0x15 /* VKEY_HANGUL */, -1 }, + { 0x17 /* VKEY_JUNJA */, -1 }, + { 0x18 /* VKEY_FINAL */, -1 }, + { 0x19 /* VKEY_HANJA */, -1 }, + { 0x19 /* VKEY_KANJI */, -1 }, + { 0x1B /* VKEY_ESCAPE */, kVK_Escape }, + { 0x1C /* VKEY_CONVERT */, -1 }, + { 0x1D /* VKEY_NONCONVERT */, -1 }, + { 0x1E /* VKEY_ACCEPT */, -1 }, + { 0x1F /* VKEY_MODECHANGE */, -1 }, + { 0x20 /* VKEY_SPACE */, kVK_Space }, + { 0x21 /* VKEY_PRIOR */, kVK_PageUp }, + { 0x22 /* VKEY_NEXT */, kVK_PageDown }, + { 0x23 /* VKEY_END */, kVK_End }, + { 0x24 /* VKEY_HOME */, kVK_Home }, + { 0x25 /* VKEY_LEFT */, kVK_LeftArrow }, + { 0x26 /* VKEY_UP */, kVK_UpArrow }, + { 0x27 /* VKEY_RIGHT */, kVK_RightArrow }, + { 0x28 /* VKEY_DOWN */, kVK_DownArrow }, + { 0x29 /* VKEY_SELECT */, -1 }, + { 0x2A /* VKEY_PRINT */, -1 }, + { 0x2B /* VKEY_EXECUTE */, -1 }, + { 0x2C /* VKEY_SNAPSHOT */, -1 }, + { 0x2D /* VKEY_INSERT */, kVK_Help }, + { 0x2E /* VKEY_DELETE */, kVK_ForwardDelete }, + { 0x2F /* VKEY_HELP */, kVK_Help }, + { 0x30 /* VKEY_0 */, kVK_ANSI_0 }, + { 0x31 /* VKEY_1 */, kVK_ANSI_1 }, + { 0x32 /* VKEY_2 */, kVK_ANSI_2 }, + { 0x33 /* VKEY_3 */, kVK_ANSI_3 }, + { 0x34 /* VKEY_4 */, kVK_ANSI_4 }, + { 0x35 /* VKEY_5 */, kVK_ANSI_5 }, + { 0x36 /* VKEY_6 */, kVK_ANSI_6 }, + { 0x37 /* VKEY_7 */, kVK_ANSI_7 }, + { 0x38 /* VKEY_8 */, kVK_ANSI_8 }, + { 0x39 /* VKEY_9 */, kVK_ANSI_9 }, + { 0x41 /* VKEY_A */, kVK_ANSI_A }, + { 0x42 /* VKEY_B */, kVK_ANSI_B }, + { 0x43 /* VKEY_C */, kVK_ANSI_C }, + { 0x44 /* VKEY_D */, kVK_ANSI_D }, + { 0x45 /* VKEY_E */, kVK_ANSI_E }, + { 0x46 /* VKEY_F */, kVK_ANSI_F }, + { 0x47 /* VKEY_G */, kVK_ANSI_G }, + { 0x48 /* VKEY_H */, kVK_ANSI_H }, + { 0x49 /* VKEY_I */, kVK_ANSI_I }, + { 0x4A /* VKEY_J */, kVK_ANSI_J }, + { 0x4B /* VKEY_K */, kVK_ANSI_K }, + { 0x4C /* VKEY_L */, kVK_ANSI_L }, + { 0x4D /* VKEY_M */, kVK_ANSI_M }, + { 0x4E /* VKEY_N */, kVK_ANSI_N }, + { 0x4F /* VKEY_O */, kVK_ANSI_O }, + { 0x50 /* VKEY_P */, kVK_ANSI_P }, + { 0x51 /* VKEY_Q */, kVK_ANSI_Q }, + { 0x52 /* VKEY_R */, kVK_ANSI_R }, + { 0x53 /* VKEY_S */, kVK_ANSI_S }, + { 0x54 /* VKEY_T */, kVK_ANSI_T }, + { 0x55 /* VKEY_U */, kVK_ANSI_U }, + { 0x56 /* VKEY_V */, kVK_ANSI_V }, + { 0x57 /* VKEY_W */, kVK_ANSI_W }, + { 0x58 /* VKEY_X */, kVK_ANSI_X }, + { 0x59 /* VKEY_Y */, kVK_ANSI_Y }, + { 0x5A /* VKEY_Z */, kVK_ANSI_Z }, + { 0x5B /* VKEY_LWIN */, kVK_Command }, + { 0x5C /* VKEY_RWIN */, kVK_RightCommand }, + { 0x5D /* VKEY_APPS */, kVK_RightCommand }, + { 0x5F /* VKEY_SLEEP */, -1 }, + { 0x60 /* VKEY_NUMPAD0 */, kVK_ANSI_Keypad0 }, + { 0x61 /* VKEY_NUMPAD1 */, kVK_ANSI_Keypad1 }, + { 0x62 /* VKEY_NUMPAD2 */, kVK_ANSI_Keypad2 }, + { 0x63 /* VKEY_NUMPAD3 */, kVK_ANSI_Keypad3 }, + { 0x64 /* VKEY_NUMPAD4 */, kVK_ANSI_Keypad4 }, + { 0x65 /* VKEY_NUMPAD5 */, kVK_ANSI_Keypad5 }, + { 0x66 /* VKEY_NUMPAD6 */, kVK_ANSI_Keypad6 }, + { 0x67 /* VKEY_NUMPAD7 */, kVK_ANSI_Keypad7 }, + { 0x68 /* VKEY_NUMPAD8 */, kVK_ANSI_Keypad8 }, + { 0x69 /* VKEY_NUMPAD9 */, kVK_ANSI_Keypad9 }, + { 0x6A /* VKEY_MULTIPLY */, kVK_ANSI_KeypadMultiply }, + { 0x6B /* VKEY_ADD */, kVK_ANSI_KeypadPlus }, + { 0x6C /* VKEY_SEPARATOR */, -1 }, + { 0x6D /* VKEY_SUBTRACT */, kVK_ANSI_KeypadMinus }, + { 0x6E /* VKEY_DECIMAL */, kVK_ANSI_KeypadDecimal }, + { 0x6F /* VKEY_DIVIDE */, kVK_ANSI_KeypadDivide }, + { 0x70 /* VKEY_F1 */, kVK_F1 }, + { 0x71 /* VKEY_F2 */, kVK_F2 }, + { 0x72 /* VKEY_F3 */, kVK_F3 }, + { 0x73 /* VKEY_F4 */, kVK_F4 }, + { 0x74 /* VKEY_F5 */, kVK_F5 }, + { 0x75 /* VKEY_F6 */, kVK_F6 }, + { 0x76 /* VKEY_F7 */, kVK_F7 }, + { 0x77 /* VKEY_F8 */, kVK_F8 }, + { 0x78 /* VKEY_F9 */, kVK_F9 }, + { 0x79 /* VKEY_F10 */, kVK_F10 }, + { 0x7A /* VKEY_F11 */, kVK_F11 }, + { 0x7B /* VKEY_F12 */, kVK_F12 }, + { 0x7C /* VKEY_F13 */, kVK_F13 }, + { 0x7D /* VKEY_F14 */, kVK_F14 }, + { 0x7E /* VKEY_F15 */, kVK_F15 }, + { 0x7F /* VKEY_F16 */, kVK_F16 }, + { 0x80 /* VKEY_F17 */, kVK_F17 }, + { 0x81 /* VKEY_F18 */, kVK_F18 }, + { 0x82 /* VKEY_F19 */, kVK_F19 }, + { 0x83 /* VKEY_F20 */, kVK_F20 }, + { 0x84 /* VKEY_F21 */, -1 }, + { 0x85 /* VKEY_F22 */, -1 }, + { 0x86 /* VKEY_F23 */, -1 }, + { 0x87 /* VKEY_F24 */, -1 }, + { 0x90 /* VKEY_NUMLOCK */, -1 }, + { 0x91 /* VKEY_SCROLL */, -1 }, + { 0xA0 /* VKEY_LSHIFT */, kVK_Shift }, + { 0xA1 /* VKEY_RSHIFT */, kVK_RightShift }, + { 0xA2 /* VKEY_LCONTROL */, kVK_Control }, + { 0xA3 /* VKEY_RCONTROL */, kVK_RightControl }, + { 0xA4 /* VKEY_LMENU */, kVK_Option }, + { 0xA5 /* VKEY_RMENU */, kVK_RightOption }, + { 0xA6 /* VKEY_BROWSER_BACK */, -1 }, + { 0xA7 /* VKEY_BROWSER_FORWARD */, -1 }, + { 0xA8 /* VKEY_BROWSER_REFRESH */, -1 }, + { 0xA9 /* VKEY_BROWSER_STOP */, -1 }, + { 0xAA /* VKEY_BROWSER_SEARCH */, -1 }, + { 0xAB /* VKEY_BROWSER_FAVORITES */, -1 }, + { 0xAC /* VKEY_BROWSER_HOME */, -1 }, + { 0xAD /* VKEY_VOLUME_MUTE */, -1 }, + { 0xAE /* VKEY_VOLUME_DOWN */, -1 }, + { 0xAF /* VKEY_VOLUME_UP */, -1 }, + { 0xB0 /* VKEY_MEDIA_NEXT_TRACK */, -1 }, + { 0xB1 /* VKEY_MEDIA_PREV_TRACK */, -1 }, + { 0xB2 /* VKEY_MEDIA_STOP */, -1 }, + { 0xB3 /* VKEY_MEDIA_PLAY_PAUSE */, -1 }, + { 0xB4 /* VKEY_MEDIA_LAUNCH_MAIL */, -1 }, + { 0xB5 /* VKEY_MEDIA_LAUNCH_MEDIA_SELECT */, -1 }, + { 0xB6 /* VKEY_MEDIA_LAUNCH_APP1 */, -1 }, + { 0xB7 /* VKEY_MEDIA_LAUNCH_APP2 */, -1 }, + { 0xBA /* VKEY_OEM_1 */, kVK_ANSI_Semicolon }, + { 0xBB /* VKEY_OEM_PLUS */, kVK_ANSI_Equal }, + { 0xBC /* VKEY_OEM_COMMA */, kVK_ANSI_Comma }, + { 0xBD /* VKEY_OEM_MINUS */, kVK_ANSI_Minus }, + { 0xBE /* VKEY_OEM_PERIOD */, kVK_ANSI_Period }, + { 0xBF /* VKEY_OEM_2 */, kVK_ANSI_Slash }, + { 0xC0 /* VKEY_OEM_3 */, kVK_ANSI_Grave }, + { 0xDB /* VKEY_OEM_4 */, kVK_ANSI_LeftBracket }, + { 0xDC /* VKEY_OEM_5 */, kVK_ANSI_Backslash }, + { 0xDD /* VKEY_OEM_6 */, kVK_ANSI_RightBracket }, + { 0xDE /* VKEY_OEM_7 */, kVK_ANSI_Quote }, + { 0xDF /* VKEY_OEM_8 */, -1 }, + { 0xE2 /* VKEY_OEM_102 */, -1 }, + { 0xE5 /* VKEY_PROCESSKEY */, -1 }, + { 0xE7 /* VKEY_PACKET */, -1 }, + { 0xF6 /* VKEY_ATTN */, -1 }, + { 0xF7 /* VKEY_CRSEL */, -1 }, + { 0xF8 /* VKEY_EXSEL */, -1 }, + { 0xF9 /* VKEY_EREOF */, -1 }, + { 0xFA /* VKEY_PLAY */, -1 }, + { 0xFB /* VKEY_ZOOM */, -1 }, + { 0xFC /* VKEY_NONAME */, -1 }, + { 0xFD /* VKEY_PA1 */, -1 }, + { 0xFE /* VKEY_OEM_CLEAR */, kVK_ANSI_KeypadClear } +}; +// clang-format on + +int keysym(int keycode) { + KeyCodeMap key_map; + + key_map.win_keycode = keycode; + const KeyCodeMap *temp_map = std::lower_bound( + kKeyCodesMap, kKeyCodesMap + sizeof(kKeyCodesMap) / sizeof(kKeyCodesMap[0]), key_map); + + if(temp_map >= kKeyCodesMap + sizeof(kKeyCodesMap) / sizeof(kKeyCodesMap[0]) || + temp_map->win_keycode != keycode || temp_map->mac_keycode == -1) { + return -1; + } + + return temp_map->mac_keycode; +} + +void keyboard(input_t &input, uint16_t modcode, bool release) { + auto key = keysym(modcode); + + BOOST_LOG(debug) << "got keycode: 0x"sv << std::hex << modcode << ", translated to: 0x" << std::hex << key << ", release:" << release; + + if(key < 0) { + return; + } + + auto macos_input = ((macos_input_t *)input.get()); + auto event = macos_input->kb_event; + + if(key == kVK_Shift || key == kVK_RightShift || + key == kVK_Command || key == kVK_RightCommand || + key == kVK_Option || key == kVK_RightOption || + key == kVK_Control || key == kVK_RightControl) { + + CGEventFlags mask; + + switch(key) { + case kVK_Shift: + case kVK_RightShift: + mask = kCGEventFlagMaskShift; + break; + case kVK_Command: + case kVK_RightCommand: + mask = kCGEventFlagMaskCommand; + break; + case kVK_Option: + case kVK_RightOption: + mask = kCGEventFlagMaskAlternate; + break; + case kVK_Control: + case kVK_RightControl: + mask = kCGEventFlagMaskControl; + break; + } + + macos_input->kb_flags = release ? macos_input->kb_flags & ~mask : macos_input->kb_flags | mask; + CGEventSetType(event, kCGEventFlagsChanged); + CGEventSetFlags(event, macos_input->kb_flags); + } + else { + CGEventSetIntegerValueField(event, kCGKeyboardEventKeycode, key); + CGEventSetType(event, release ? kCGEventKeyUp : kCGEventKeyDown); + } + + CGEventPost(kCGHIDEventTap, event); +} + +int alloc_gamepad(input_t &input, int nr, rumble_queue_t rumble_queue) { + BOOST_LOG(info) << "alloc_gamepad: Gamepad not yet implemented for MacOS."sv; + return -1; +} + +void free_gamepad(input_t &input, int nr) { + BOOST_LOG(info) << "free_gamepad: Gamepad not yet implemented for MacOS."sv; +} + +void gamepad(input_t &input, int nr, const gamepad_state_t &gamepad_state) { + BOOST_LOG(info) << "gamepad: Gamepad not yet implemented for MacOS."sv; +} + +// returns current mouse location: +inline CGPoint get_mouse_loc(input_t &input) { + return CGEventGetLocation(((macos_input_t *)input.get())->mouse_event); +} + +void post_mouse(input_t &input, CGMouseButton button, CGEventType type, CGPoint location, int click_count) { + BOOST_LOG(debug) << "mouse_event: "sv << button << ", type: "sv << type << ", location:"sv << location.x << ":"sv << location.y << " click_count: "sv << click_count; + + auto macos_input = (macos_input_t *)input.get(); + auto display = macos_input->display; + auto event = macos_input->mouse_event; + + if(location.x < 0) + location.x = 0; + if(location.x >= CGDisplayPixelsWide(display)) + location.x = CGDisplayPixelsWide(display) - 1; + + if(location.y < 0) + location.y = 0; + if(location.y >= CGDisplayPixelsHigh(display)) + location.y = CGDisplayPixelsHigh(display) - 1; + + CGEventSetType(event, type); + CGEventSetLocation(event, location); + CGEventSetIntegerValueField(event, kCGMouseEventButtonNumber, button); + CGEventSetIntegerValueField(event, kCGMouseEventClickState, click_count); + + CGEventPost(kCGHIDEventTap, event); + + // For why this is here, see: + // https://stackoverflow.com/questions/15194409/simulated-mouseevent-not-working-properly-osx + CGWarpMouseCursorPosition(location); +} + +inline CGEventType event_type_mouse(input_t &input) { + auto macos_input = ((macos_input_t *)input.get()); + + if(macos_input->mouse_down[0]) { + return kCGEventLeftMouseDragged; + } + else if(macos_input->mouse_down[1]) { + return kCGEventOtherMouseDragged; + } + else if(macos_input->mouse_down[2]) { + return kCGEventRightMouseDragged; + } + else { + return kCGEventMouseMoved; + } +} + +void move_mouse(input_t &input, int deltaX, int deltaY) { + auto current = get_mouse_loc(input); + + CGPoint location = CGPointMake(current.x + deltaX, current.y + deltaY); + + post_mouse(input, kCGMouseButtonLeft, event_type_mouse(input), location, 0); +} + +void abs_mouse(input_t &input, const touch_port_t &touch_port, float x, float y) { + auto scaling = ((macos_input_t *)input.get())->displayScaling; + + CGPoint location = CGPointMake(x * scaling, y * scaling); + + post_mouse(input, kCGMouseButtonLeft, event_type_mouse(input), location, 0); +} + +uint64_t time_diff(uint64_t start) { + uint64_t elapsed; + Nanoseconds elapsedNano; + + elapsed = mach_absolute_time() - start; + elapsedNano = AbsoluteToNanoseconds(*(AbsoluteTime *)&elapsed); + + return *(uint64_t *)&elapsedNano; +} + +void button_mouse(input_t &input, int button, bool release) { + CGMouseButton mac_button; + CGEventType event; + + auto mouse = ((macos_input_t *)input.get()); + + switch(button) { + case 1: + mac_button = kCGMouseButtonLeft; + event = release ? kCGEventLeftMouseUp : kCGEventLeftMouseDown; + break; + case 2: + mac_button = kCGMouseButtonCenter; + event = release ? kCGEventOtherMouseUp : kCGEventOtherMouseDown; + break; + case 3: + mac_button = kCGMouseButtonRight; + event = release ? kCGEventRightMouseUp : kCGEventRightMouseDown; + break; + default: + BOOST_LOG(warning) << "Unsupported mouse button for MacOS: "sv << button; + return; + } + + mouse->mouse_down[mac_button] = !release; + + // if the last mouse down was less than MULTICLICK_DELAY_NS, we send a double click event + if(time_diff(mouse->last_mouse_event[mac_button][release]) < MULTICLICK_DELAY_NS) { + post_mouse(input, mac_button, event, get_mouse_loc(input), 2); + } + else { + post_mouse(input, mac_button, event, get_mouse_loc(input), 1); + } + + mouse->last_mouse_event[mac_button][release] = mach_absolute_time(); +} + +void scroll(input_t &input, int high_res_distance) { + CGEventRef upEvent = CGEventCreateScrollWheelEvent( + NULL, + kCGScrollEventUnitLine, + 2, high_res_distance > 0 ? 1 : -1, high_res_distance); + CGEventPost(kCGHIDEventTap, upEvent); + CFRelease(upEvent); +} + +input_t input() { + input_t result { new macos_input_t() }; + + auto macos_input = (macos_input_t *)result.get(); + + // If we don't use the main display in the future, this has to be adapted + macos_input->display = CGMainDisplayID(); + + // Input coordinates are based on the virtual resolution not the physical, so we need the scaling factor + CGDisplayModeRef mode = CGDisplayCopyDisplayMode(macos_input->display); + macos_input->displayScaling = ((CGFloat)CGDisplayPixelsWide(macos_input->display)) / ((CGFloat)CGDisplayModeGetPixelWidth(mode)); + CFRelease(mode); + + macos_input->source = CGEventSourceCreate(kCGEventSourceStateHIDSystemState); + + macos_input->kb_event = CGEventCreate(macos_input->source); + macos_input->kb_flags = 0; + + macos_input->mouse_event = CGEventCreate(macos_input->source); + macos_input->mouse_down[0] = false; + macos_input->mouse_down[1] = false; + macos_input->mouse_down[2] = false; + macos_input->last_mouse_event[0][0] = 0; + macos_input->last_mouse_event[0][1] = 0; + macos_input->last_mouse_event[1][0] = 0; + macos_input->last_mouse_event[1][1] = 0; + macos_input->last_mouse_event[2][0] = 0; + macos_input->last_mouse_event[2][1] = 0; + + BOOST_LOG(debug) << "Display "sv << macos_input->display << ", pixel dimention: " << CGDisplayPixelsWide(macos_input->display) << "x"sv << CGDisplayPixelsHigh(macos_input->display); + + return result; +} + +void freeInput(void *p) { + auto *input = (macos_input_t *)p; + + CFRelease(input->source); + CFRelease(input->kb_event); + CFRelease(input->mouse_event); + + delete input; +} + +std::vector &supported_gamepads() { + static std::vector gamepads { ""sv }; + + return gamepads; +} +} // namespace platf diff --git a/sunshine/platform/macos/microphone.mm b/sunshine/platform/macos/microphone.mm new file mode 100644 index 00000000000..4b6516909c9 --- /dev/null +++ b/sunshine/platform/macos/microphone.mm @@ -0,0 +1,87 @@ +#include "sunshine/platform/common.h" +#include "sunshine/platform/macos/av_audio.h" + +#include "sunshine/config.h" +#include "sunshine/main.h" + +namespace platf { +using namespace std::literals; + +struct av_mic_t : public mic_t { + AVAudio *av_audio_capture; + + ~av_mic_t() { + [av_audio_capture release]; + } + + capture_e sample(std::vector &sample_in) override { + auto sample_size = sample_in.size(); + + uint32_t length = 0; + void *byteSampleBuffer = TPCircularBufferTail(&av_audio_capture->audioSampleBuffer, &length); + + while(length < sample_size * sizeof(std::int16_t)) { + [av_audio_capture.samplesArrivedSignal wait]; + byteSampleBuffer = TPCircularBufferTail(&av_audio_capture->audioSampleBuffer, &length); + } + + const int16_t *sampleBuffer = (int16_t *)byteSampleBuffer; + std::vector vectorBuffer(sampleBuffer, sampleBuffer + sample_size); + + std::copy_n(std::begin(vectorBuffer), sample_size, std::begin(sample_in)); + + TPCircularBufferConsume(&av_audio_capture->audioSampleBuffer, sample_size * sizeof(std::int16_t)); + + return capture_e::ok; + } +}; + +struct macos_audio_control_t : public audio_control_t { + AVCaptureDevice *audio_capture_device; + +public: + int set_sink(const std::string &sink) override { + BOOST_LOG(warning) << "audio_control_t::set_sink() unimplemented: "sv << sink; + return 0; + } + + 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(); + const char *audio_sink = ""; + + if(!config::audio.sink.empty()) { + audio_sink = config::audio.sink.c_str(); + } + + if((audio_capture_device = [AVAudio findMicrophone:[NSString stringWithUTF8String:audio_sink]]) == nullptr) { + BOOST_LOG(error) << "opening microphone '"sv << audio_sink << "' failed. Please set a valid input source in the Sunshine config."sv; + BOOST_LOG(error) << "Available inputs:"sv; + + for(NSString *name in [AVAudio microphoneNames]) { + BOOST_LOG(error) << "\t"sv << [name UTF8String]; + } + + return nullptr; + } + + mic->av_audio_capture = [[AVAudio alloc] init]; + + if([mic->av_audio_capture setupMicrophone:audio_capture_device sampleRate:sample_rate frameSize:frame_size channels:channels]) { + BOOST_LOG(error) << "Failed to setup microphone."sv; + return nullptr; + } + + return mic; + } + + std::optional sink_info() override { + sink_t sink; + + return sink; + } +}; + +std::unique_ptr audio_control() { + return std::make_unique(); +} +} diff --git a/sunshine/platform/macos/misc.cpp b/sunshine/platform/macos/misc.cpp new file mode 100644 index 00000000000..fdc4668817e --- /dev/null +++ b/sunshine/platform/macos/misc.cpp @@ -0,0 +1,161 @@ +#include +#include +#include +#include +#include +#include + +#include "misc.h" +#include "sunshine/main.h" +#include "sunshine/platform/common.h" + +using namespace std::literals; +namespace fs = std::filesystem; + +namespace platf { +std::unique_ptr init() { + if(!CGPreflightScreenCaptureAccess()) { + BOOST_LOG(error) << "No screen capture permission!"sv; + BOOST_LOG(error) << "Please activate it in 'System Preferences' -> 'Privacy' -> 'Screen Recording'"sv; + CGRequestScreenCaptureAccess(); + return nullptr; + } + return std::make_unique(); +} + +fs::path appdata() { + const char *homedir; + if((homedir = getenv("HOME")) == nullptr) { + homedir = getpwuid(geteuid())->pw_dir; + } + + return fs::path { homedir } / ".config/sunshine"sv; +} + +using ifaddr_t = util::safe_ptr; + +ifaddr_t get_ifaddrs() { + ifaddrs *p { nullptr }; + + getifaddrs(&p); + + return ifaddr_t { p }; +} + +std::string from_sockaddr(const sockaddr *const ip_addr) { + char data[INET6_ADDRSTRLEN]; + + auto family = ip_addr->sa_family; + if(family == AF_INET6) { + inet_ntop(AF_INET6, &((sockaddr_in6 *)ip_addr)->sin6_addr, data, + INET6_ADDRSTRLEN); + } + + if(family == AF_INET) { + inet_ntop(AF_INET, &((sockaddr_in *)ip_addr)->sin_addr, data, + INET_ADDRSTRLEN); + } + + return std::string { data }; +} + +std::pair from_sockaddr_ex(const sockaddr *const ip_addr) { + char data[INET6_ADDRSTRLEN]; + + auto family = ip_addr->sa_family; + std::uint16_t port; + if(family == AF_INET6) { + inet_ntop(AF_INET6, &((sockaddr_in6 *)ip_addr)->sin6_addr, data, + INET6_ADDRSTRLEN); + port = ((sockaddr_in6 *)ip_addr)->sin6_port; + } + + if(family == AF_INET) { + inet_ntop(AF_INET, &((sockaddr_in *)ip_addr)->sin_addr, data, + INET_ADDRSTRLEN); + port = ((sockaddr_in *)ip_addr)->sin_port; + } + + return { port, std::string { data } }; +} + +std::string get_mac_address(const std::string_view &address) { + auto ifaddrs = get_ifaddrs(); + + for(auto pos = ifaddrs.get(); pos != nullptr; pos = pos->ifa_next) { + if(pos->ifa_addr && address == from_sockaddr(pos->ifa_addr)) { + BOOST_LOG(verbose) << "Looking for MAC of "sv << pos->ifa_name; + + struct ifaddrs *ifap, *ifaptr; + unsigned char *ptr; + std::string mac_address; + + if(getifaddrs(&ifap) == 0) { + for(ifaptr = ifap; ifaptr != NULL; ifaptr = (ifaptr)->ifa_next) { + if(!strcmp((ifaptr)->ifa_name, pos->ifa_name) && (((ifaptr)->ifa_addr)->sa_family == AF_LINK)) { + ptr = (unsigned char *)LLADDR((struct sockaddr_dl *)(ifaptr)->ifa_addr); + char buff[100]; + + snprintf(buff, sizeof(buff), "%02x:%02x:%02x:%02x:%02x:%02x", + *ptr, *(ptr + 1), *(ptr + 2), *(ptr + 3), *(ptr + 4), *(ptr + 5)); + mac_address = buff; + break; + } + } + + freeifaddrs(ifap); + + if(ifaptr != NULL) { + BOOST_LOG(verbose) << "Found MAC of "sv << pos->ifa_name << ": "sv << mac_address; + return mac_address; + } + } + } + } + + BOOST_LOG(warning) << "Unable to find MAC address for "sv << address; + return "00:00:00:00:00:00"s; +} +} // namespace platf + +namespace dyn { +void *handle(const std::vector &libs) { + void *handle; + + for(auto lib : libs) { + handle = dlopen(lib, RTLD_LAZY | RTLD_LOCAL); + if(handle) { + return handle; + } + } + + std::stringstream ss; + ss << "Couldn't find any of the following libraries: ["sv << libs.front(); + std::for_each(std::begin(libs) + 1, std::end(libs), [&](auto lib) { + ss << ", "sv << lib; + }); + + ss << ']'; + + BOOST_LOG(error) << ss.str(); + + return nullptr; +} + +int load(void *handle, const std::vector> &funcs, bool strict) { + int err = 0; + for(auto &func : funcs) { + TUPLE_2D_REF(fn, name, func); + + *fn = (void (*)())dlsym(handle, name); + + if(!*fn && strict) { + BOOST_LOG(error) << "Couldn't find function: "sv << name; + + err = -1; + } + } + + return err; +} +} // namespace dyn diff --git a/sunshine/platform/macos/misc.h b/sunshine/platform/macos/misc.h new file mode 100644 index 00000000000..f0d04a33581 --- /dev/null +++ b/sunshine/platform/macos/misc.h @@ -0,0 +1,16 @@ +#ifndef SUNSHINE_PLATFORM_MISC_H +#define SUNSHINE_PLATFORM_MISC_H + +#include + +#include + +namespace dyn { +typedef void (*apiproc)(void); + +int load(void *handle, const std::vector> &funcs, bool strict = true); +void *handle(const std::vector &libs); + +} // namespace dyn + +#endif diff --git a/sunshine/platform/macos/nv12_zero_device.cpp b/sunshine/platform/macos/nv12_zero_device.cpp new file mode 100644 index 00000000000..7e0a4a77606 --- /dev/null +++ b/sunshine/platform/macos/nv12_zero_device.cpp @@ -0,0 +1,82 @@ +#include "sunshine/platform/macos/nv12_zero_device.h" +#include "sunshine/platform/macos/av_img_t.h" + +#include "sunshine/video.h" + +extern "C" { +#include "libavutil/imgutils.h" +} + +namespace platf { + +void free_frame(AVFrame *frame) { + av_frame_free(&frame); +} + +util::safe_ptr av_frame; + +int nv12_zero_device::convert(platf::img_t &img) { + av_frame_make_writable(av_frame.get()); + + av_img_t *av_img = (av_img_t *)&img; + + size_t left_pad, right_pad, top_pad, bottom_pad; + CVPixelBufferGetExtendedPixels(av_img->pixel_buffer, &left_pad, &right_pad, &top_pad, &bottom_pad); + + const uint8_t *data = (const uint8_t *)CVPixelBufferGetBaseAddressOfPlane(av_img->pixel_buffer, 0) - left_pad - (top_pad * img.width); + + int result = av_image_fill_arrays(av_frame->data, av_frame->linesize, data, (AVPixelFormat)av_frame->format, img.width, img.height, 32); + + // We will create the black bars for the padding top/bottom or left/right here in very cheap way. + // The luminance is 0, therefore, we simply need to set the chroma values to 128 for each pixel + // for black bars (instead of green with chroma 0). However, this only works 100% correct, when + // the resolution is devisable by 32. This could be improved by calculating the chroma values for + // the outer content pixels, which should introduce only a minor performance hit. + // + // XXX: Improve the algorithm to take into account the outer pixels + + size_t uv_plane_height = CVPixelBufferGetHeightOfPlane(av_img->pixel_buffer, 1); + + if(left_pad || right_pad) { + for(int l = 0; l < uv_plane_height + (top_pad / 2); l++) { + int line = l * av_frame->linesize[1]; + memset((void *)&av_frame->data[1][line], 128, (size_t)left_pad); + memset((void *)&av_frame->data[1][line + img.width - right_pad], 128, right_pad); + } + } + + if(top_pad || bottom_pad) { + memset((void *)&av_frame->data[1][0], 128, (top_pad / 2) * av_frame->linesize[1]); + memset((void *)&av_frame->data[1][((top_pad / 2) + uv_plane_height) * av_frame->linesize[1]], 128, bottom_pad / 2 * av_frame->linesize[1]); + } + + return result > 0 ? 0 : -1; +} + +int nv12_zero_device::set_frame(AVFrame *frame) { + this->frame = frame; + + av_frame.reset(frame); + + resolution_fn(this->display, frame->width, frame->height); + + return 0; +} + +void nv12_zero_device::set_colorspace(std::uint32_t colorspace, std::uint32_t color_range) { +} + +int nv12_zero_device::init(void *display, resolution_fn_t resolution_fn, pixel_format_fn_t pixel_format_fn) { + pixel_format_fn(display, '420v'); + + this->display = display; + this->resolution_fn = resolution_fn; + + // we never use this pointer but it's existence is checked/used + // by the platform independed code + data = this; + + return 0; +} + +} // namespace platf diff --git a/sunshine/platform/macos/nv12_zero_device.h b/sunshine/platform/macos/nv12_zero_device.h new file mode 100644 index 00000000000..847a8f0ab19 --- /dev/null +++ b/sunshine/platform/macos/nv12_zero_device.h @@ -0,0 +1,29 @@ +#ifndef vtdevice_h +#define vtdevice_h + +#include "sunshine/platform/common.h" + +namespace platf { + +class nv12_zero_device : public hwdevice_t { + // display holds a pointer to an av_video object. Since the namespaces of AVFoundation + // and FFMPEG collide, we need this opaque pointer and cannot use the definition + void *display; + +public: + // this function is used to set the resolution on an av_video object that we cannot + // call directly because of namespace collisions between AVFoundation and FFMPEG + using resolution_fn_t = std::function; + resolution_fn_t resolution_fn; + using pixel_format_fn_t = std::function; + + int init(void *display, resolution_fn_t resolution_fn, pixel_format_fn_t pixel_format_fn); + + int convert(img_t &img); + int set_frame(AVFrame *frame); + void set_colorspace(std::uint32_t colorspace, std::uint32_t color_range); +}; + +} // namespace platf + +#endif /* vtdevice_h */ diff --git a/sunshine/platform/macos/publish.cpp b/sunshine/platform/macos/publish.cpp new file mode 100644 index 00000000000..cc10cd826ea --- /dev/null +++ b/sunshine/platform/macos/publish.cpp @@ -0,0 +1,429 @@ + +// adapted from https://www.avahi.org/doxygen/html/client-publish-service_8c-example.html +#include + +#include "misc.h" +#include "sunshine/main.h" +#include "sunshine/nvhttp.h" +#include "sunshine/platform/common.h" +#include "sunshine/utility.h" + +using namespace std::literals; + +namespace avahi { + +/** Error codes used by avahi */ +enum err_e { + OK = 0, /**< OK */ + ERR_FAILURE = -1, /**< Generic error code */ + ERR_BAD_STATE = -2, /**< Object was in a bad state */ + ERR_INVALID_HOST_NAME = -3, /**< Invalid host name */ + ERR_INVALID_DOMAIN_NAME = -4, /**< Invalid domain name */ + ERR_NO_NETWORK = -5, /**< No suitable network protocol available */ + ERR_INVALID_TTL = -6, /**< Invalid DNS TTL */ + ERR_IS_PATTERN = -7, /**< RR key is pattern */ + ERR_COLLISION = -8, /**< Name collision */ + ERR_INVALID_RECORD = -9, /**< Invalid RR */ + + ERR_INVALID_SERVICE_NAME = -10, /**< Invalid service name */ + ERR_INVALID_SERVICE_TYPE = -11, /**< Invalid service type */ + ERR_INVALID_PORT = -12, /**< Invalid port number */ + ERR_INVALID_KEY = -13, /**< Invalid key */ + ERR_INVALID_ADDRESS = -14, /**< Invalid address */ + ERR_TIMEOUT = -15, /**< Timeout reached */ + ERR_TOO_MANY_CLIENTS = -16, /**< Too many clients */ + ERR_TOO_MANY_OBJECTS = -17, /**< Too many objects */ + ERR_TOO_MANY_ENTRIES = -18, /**< Too many entries */ + ERR_OS = -19, /**< OS error */ + + ERR_ACCESS_DENIED = -20, /**< Access denied */ + ERR_INVALID_OPERATION = -21, /**< Invalid operation */ + ERR_DBUS_ERROR = -22, /**< An unexpected D-Bus error occurred */ + ERR_DISCONNECTED = -23, /**< Daemon connection failed */ + ERR_NO_MEMORY = -24, /**< Memory exhausted */ + ERR_INVALID_OBJECT = -25, /**< The object passed to this function was invalid */ + ERR_NO_DAEMON = -26, /**< Daemon not running */ + ERR_INVALID_INTERFACE = -27, /**< Invalid interface */ + ERR_INVALID_PROTOCOL = -28, /**< Invalid protocol */ + ERR_INVALID_FLAGS = -29, /**< Invalid flags */ + + ERR_NOT_FOUND = -30, /**< Not found */ + ERR_INVALID_CONFIG = -31, /**< Configuration error */ + ERR_VERSION_MISMATCH = -32, /**< Verson mismatch */ + ERR_INVALID_SERVICE_SUBTYPE = -33, /**< Invalid service subtype */ + ERR_INVALID_PACKET = -34, /**< Invalid packet */ + ERR_INVALID_DNS_ERROR = -35, /**< Invlaid DNS return code */ + ERR_DNS_FORMERR = -36, /**< DNS Error: Form error */ + ERR_DNS_SERVFAIL = -37, /**< DNS Error: Server Failure */ + ERR_DNS_NXDOMAIN = -38, /**< DNS Error: No such domain */ + ERR_DNS_NOTIMP = -39, /**< DNS Error: Not implemented */ + + ERR_DNS_REFUSED = -40, /**< DNS Error: Operation refused */ + ERR_DNS_YXDOMAIN = -41, + ERR_DNS_YXRRSET = -42, + ERR_DNS_NXRRSET = -43, + ERR_DNS_NOTAUTH = -44, /**< DNS Error: Not authorized */ + ERR_DNS_NOTZONE = -45, + ERR_INVALID_RDATA = -46, /**< Invalid RDATA */ + ERR_INVALID_DNS_CLASS = -47, /**< Invalid DNS class */ + ERR_INVALID_DNS_TYPE = -48, /**< Invalid DNS type */ + ERR_NOT_SUPPORTED = -49, /**< Not supported */ + + ERR_NOT_PERMITTED = -50, /**< Operation not permitted */ + ERR_INVALID_ARGUMENT = -51, /**< Invalid argument */ + ERR_IS_EMPTY = -52, /**< Is empty */ + ERR_NO_CHANGE = -53, /**< The requested operation is invalid because it is redundant */ + + ERR_MAX = -54 +}; + +constexpr auto IF_UNSPEC = -1; +enum proto { + PROTO_INET = 0, /**< IPv4 */ + PROTO_INET6 = 1, /**< IPv6 */ + PROTO_UNSPEC = -1 /**< Unspecified/all protocol(s) */ +}; + +enum ServerState { + SERVER_INVALID, /**< Invalid state (initial) */ + SERVER_REGISTERING, /**< Host RRs are being registered */ + SERVER_RUNNING, /**< All host RRs have been established */ + SERVER_COLLISION, /**< There is a collision with a host RR. All host RRs have been withdrawn, the user should set a new host name via avahi_server_set_host_name() */ + SERVER_FAILURE /**< Some fatal failure happened, the server is unable to proceed */ +}; + +enum ClientState { + CLIENT_S_REGISTERING = SERVER_REGISTERING, /**< Server state: REGISTERING */ + CLIENT_S_RUNNING = SERVER_RUNNING, /**< Server state: RUNNING */ + CLIENT_S_COLLISION = SERVER_COLLISION, /**< Server state: COLLISION */ + CLIENT_FAILURE = 100, /**< Some kind of error happened on the client side */ + CLIENT_CONNECTING = 101 /**< We're still connecting. This state is only entered when AVAHI_CLIENT_NO_FAIL has been passed to avahi_client_new() and the daemon is not yet available. */ +}; + +enum EntryGroupState { + ENTRY_GROUP_UNCOMMITED, /**< The group has not yet been commited, the user must still call avahi_entry_group_commit() */ + ENTRY_GROUP_REGISTERING, /**< The entries of the group are currently being registered */ + ENTRY_GROUP_ESTABLISHED, /**< The entries have successfully been established */ + ENTRY_GROUP_COLLISION, /**< A name collision for one of the entries in the group has been detected, the entries have been withdrawn */ + ENTRY_GROUP_FAILURE /**< Some kind of failure happened, the entries have been withdrawn */ +}; + +enum ClientFlags { + CLIENT_IGNORE_USER_CONFIG = 1, /**< Don't read user configuration */ + CLIENT_NO_FAIL = 2 /**< Don't fail if the daemon is not available when avahi_client_new() is called, instead enter CLIENT_CONNECTING state and wait for the daemon to appear */ +}; + +/** Some flags for publishing functions */ +enum PublishFlags { + PUBLISH_UNIQUE = 1, /**< For raw records: The RRset is intended to be unique */ + PUBLISH_NO_PROBE = 2, /**< For raw records: Though the RRset is intended to be unique no probes shall be sent */ + PUBLISH_NO_ANNOUNCE = 4, /**< For raw records: Do not announce this RR to other hosts */ + PUBLISH_ALLOW_MULTIPLE = 8, /**< For raw records: Allow multiple local records of this type, even if they are intended to be unique */ + /** \cond fulldocs */ + PUBLISH_NO_REVERSE = 16, /**< For address records: don't create a reverse (PTR) entry */ + PUBLISH_NO_COOKIE = 32, /**< For service records: do not implicitly add the local service cookie to TXT data */ + /** \endcond */ + PUBLISH_UPDATE = 64, /**< Update existing records instead of adding new ones */ + /** \cond fulldocs */ + PUBLISH_USE_WIDE_AREA = 128, /**< Register the record using wide area DNS (i.e. unicast DNS update) */ + PUBLISH_USE_MULTICAST = 256 /**< Register the record using multicast DNS */ + /** \endcond */ +}; + +using IfIndex = int; +using Protocol = int; + +struct EntryGroup; +struct Poll; +struct SimplePoll; +struct Client; + +typedef void (*ClientCallback)(Client *, ClientState, void *userdata); +typedef void (*EntryGroupCallback)(EntryGroup *g, EntryGroupState state, void *userdata); + +typedef void (*free_fn)(void *); + +typedef Client *(*client_new_fn)(const Poll *poll_api, ClientFlags flags, ClientCallback callback, void *userdata, int *error); +typedef void (*client_free_fn)(Client *); +typedef char *(*alternative_service_name_fn)(char *); + +typedef Client *(*entry_group_get_client_fn)(EntryGroup *); + +typedef EntryGroup *(*entry_group_new_fn)(Client *, EntryGroupCallback, void *userdata); +typedef int (*entry_group_add_service_fn)( + EntryGroup *group, + IfIndex interface, + Protocol protocol, + PublishFlags flags, + const char *name, + const char *type, + const char *domain, + const char *host, + uint16_t port, + ...); + +typedef int (*entry_group_is_empty_fn)(EntryGroup *); +typedef int (*entry_group_reset_fn)(EntryGroup *); +typedef int (*entry_group_commit_fn)(EntryGroup *); + +typedef char *(*strdup_fn)(const char *); +typedef char *(*strerror_fn)(int); +typedef int (*client_errno_fn)(Client *); + +typedef Poll *(*simple_poll_get_fn)(SimplePoll *); +typedef int (*simple_poll_loop_fn)(SimplePoll *); +typedef void (*simple_poll_quit_fn)(SimplePoll *); +typedef SimplePoll *(*simple_poll_new_fn)(); +typedef void (*simple_poll_free_fn)(SimplePoll *); + +free_fn free; +client_new_fn client_new; +client_free_fn client_free; +alternative_service_name_fn alternative_service_name; +entry_group_get_client_fn entry_group_get_client; +entry_group_new_fn entry_group_new; +entry_group_add_service_fn entry_group_add_service; +entry_group_is_empty_fn entry_group_is_empty; +entry_group_reset_fn entry_group_reset; +entry_group_commit_fn entry_group_commit; +strdup_fn strdup; +strerror_fn strerror; +client_errno_fn client_errno; +simple_poll_get_fn simple_poll_get; +simple_poll_loop_fn simple_poll_loop; +simple_poll_quit_fn simple_poll_quit; +simple_poll_new_fn simple_poll_new; +simple_poll_free_fn simple_poll_free; + + +int init_common() { + static void *handle { nullptr }; + static bool funcs_loaded = false; + + if(funcs_loaded) return 0; + + if(!handle) { + handle = dyn::handle({ "libavahi-common.3.dylib", "libavahi-common.dylib" }); + if(!handle) { + return -1; + } + } + + std::vector> funcs { + { (dyn::apiproc *)&alternative_service_name, "avahi_alternative_service_name" }, + { (dyn::apiproc *)&free, "avahi_free" }, + { (dyn::apiproc *)&strdup, "avahi_strdup" }, + { (dyn::apiproc *)&strerror, "avahi_strerror" }, + { (dyn::apiproc *)&simple_poll_get, "avahi_simple_poll_get" }, + { (dyn::apiproc *)&simple_poll_loop, "avahi_simple_poll_loop" }, + { (dyn::apiproc *)&simple_poll_quit, "avahi_simple_poll_quit" }, + { (dyn::apiproc *)&simple_poll_new, "avahi_simple_poll_new" }, + { (dyn::apiproc *)&simple_poll_free, "avahi_simple_poll_free" }, + }; + + if(dyn::load(handle, funcs)) { + return -1; + } + + funcs_loaded = true; + return 0; +} + +int init_client() { + if(init_common()) { + return -1; + } + + static void *handle { nullptr }; + static bool funcs_loaded = false; + + if(funcs_loaded) return 0; + + if(!handle) { + handle = dyn::handle({ "libavahi-client.3.dylib", "libavahi-client.dylib" }); + if(!handle) { + return -1; + } + } + + std::vector> funcs { + { (dyn::apiproc *)&client_new, "avahi_client_new" }, + { (dyn::apiproc *)&client_free, "avahi_client_free" }, + { (dyn::apiproc *)&entry_group_get_client, "avahi_entry_group_get_client" }, + { (dyn::apiproc *)&entry_group_new, "avahi_entry_group_new" }, + { (dyn::apiproc *)&entry_group_add_service, "avahi_entry_group_add_service" }, + { (dyn::apiproc *)&entry_group_is_empty, "avahi_entry_group_is_empty" }, + { (dyn::apiproc *)&entry_group_reset, "avahi_entry_group_reset" }, + { (dyn::apiproc *)&entry_group_commit, "avahi_entry_group_commit" }, + { (dyn::apiproc *)&client_errno, "avahi_client_errno" }, + }; + + if(dyn::load(handle, funcs)) { + return -1; + } + + funcs_loaded = true; + return 0; +} +} // namespace avahi + +namespace platf::publish { + +template +void free(T *p) { + avahi::free(p); +} + +template +using ptr_t = util::safe_ptr>; +using client_t = util::dyn_safe_ptr; +using poll_t = util::dyn_safe_ptr; + +avahi::EntryGroup *group = nullptr; + +poll_t poll; +client_t client; + +ptr_t name; + +void create_services(avahi::Client *c); + +void entry_group_callback(avahi::EntryGroup *g, avahi::EntryGroupState state, void *) { + group = g; + + switch(state) { + case avahi::ENTRY_GROUP_ESTABLISHED: + BOOST_LOG(info) << "Avahi service " << name.get() << " successfully established."; + break; + case avahi::ENTRY_GROUP_COLLISION: + name.reset(avahi::alternative_service_name(name.get())); + + BOOST_LOG(info) << "Avahi service name collision, renaming service to " << name.get(); + + create_services(avahi::entry_group_get_client(g)); + break; + case avahi::ENTRY_GROUP_FAILURE: + BOOST_LOG(error) << "Avahi entry group failure: " << avahi::strerror(avahi::client_errno(avahi::entry_group_get_client(g))); + avahi::simple_poll_quit(poll.get()); + break; + case avahi::ENTRY_GROUP_UNCOMMITED: + case avahi::ENTRY_GROUP_REGISTERING:; + } +} + +void create_services(avahi::Client *c) { + int ret; + + auto fg = util::fail_guard([]() { + avahi::simple_poll_quit(poll.get()); + }); + + if(!group) { + if(!(group = avahi::entry_group_new(c, entry_group_callback, nullptr))) { + BOOST_LOG(error) << "avahi::entry_group_new() failed: "sv << avahi::strerror(avahi::client_errno(c)); + return; + } + } + + if(avahi::entry_group_is_empty(group)) { + BOOST_LOG(info) << "Adding avahi service "sv << name.get(); + + ret = avahi::entry_group_add_service( + group, + avahi::IF_UNSPEC, avahi::PROTO_UNSPEC, + avahi::PublishFlags(0), + name.get(), + SERVICE_TYPE, + nullptr, nullptr, + map_port(nvhttp::PORT_HTTP), + nullptr); + + if(ret < 0) { + if(ret == avahi::ERR_COLLISION) { + // A service name collision with a local service happened. Let's pick a new name + name.reset(avahi::alternative_service_name(name.get())); + BOOST_LOG(info) << "Service name collision, renaming service to "sv << name.get(); + + avahi::entry_group_reset(group); + + create_services(c); + + fg.disable(); + return; + } + + BOOST_LOG(error) << "Failed to add "sv << SERVICE_TYPE << " service: "sv << avahi::strerror(ret); + return; + } + + ret = avahi::entry_group_commit(group); + if(ret < 0) { + BOOST_LOG(error) << "Failed to commit entry group: "sv << avahi::strerror(ret); + return; + } + } + + fg.disable(); +} + +void client_callback(avahi::Client *c, avahi::ClientState state, void *) { + switch(state) { + case avahi::CLIENT_S_RUNNING: + create_services(c); + break; + case avahi::CLIENT_FAILURE: + BOOST_LOG(error) << "Client failure: "sv << avahi::strerror(avahi::client_errno(c)); + avahi::simple_poll_quit(poll.get()); + break; + case avahi::CLIENT_S_COLLISION: + case avahi::CLIENT_S_REGISTERING: + if(group) + avahi::entry_group_reset(group); + break; + case avahi::CLIENT_CONNECTING:; + } +} + +class deinit_t : public ::platf::deinit_t { +public: + std::thread poll_thread; + + deinit_t(std::thread poll_thread) : poll_thread { std::move(poll_thread) } {} + + ~deinit_t() override { + if(avahi::simple_poll_quit && poll) { + avahi::simple_poll_quit(poll.get()); + } + + if(poll_thread.joinable()) { + poll_thread.join(); + } + } +}; + +[[nodiscard]] std::unique_ptr<::platf::deinit_t> start() { + if(avahi::init_client()) { + return nullptr; + } + + int avhi_error; + + poll.reset(avahi::simple_poll_new()); + if(!poll) { + BOOST_LOG(error) << "Failed to create simple poll object."sv; + return nullptr; + } + + name.reset(avahi::strdup(SERVICE_NAME)); + + client.reset( + avahi::client_new(avahi::simple_poll_get(poll.get()), avahi::ClientFlags(0), client_callback, nullptr, &avhi_error)); + + if(!client) { + BOOST_LOG(error) << "Failed to create client: "sv << avahi::strerror(avhi_error); + return nullptr; + } + + return std::make_unique(std::thread { avahi::simple_poll_loop, poll.get() }); +} +}; // namespace platf::publish diff --git a/sunshine/platform/windows/misc.h b/sunshine/platform/windows/misc.h index 4effddf5da7..4cd8791face 100644 --- a/sunshine/platform/windows/misc.h +++ b/sunshine/platform/windows/misc.h @@ -1,13 +1,13 @@ #ifndef SUNSHINE_WINDOWS_MISC_H #define SUNSHINE_WINDOWS_MISC_H +#include #include #include -#include namespace platf { void print_status(const std::string_view &prefix, HRESULT status); HDESK syncThreadDesktop(); -} +} // namespace platf #endif \ No newline at end of file diff --git a/sunshine/process.cpp b/sunshine/process.cpp index f94b84fdf9d..7fd900c5b89 100644 --- a/sunshine/process.cpp +++ b/sunshine/process.cpp @@ -6,14 +6,14 @@ #include "process.h" +#include #include #include -#include +#include +#include #include #include -#include -#include #include "main.h" #include "utility.h" @@ -112,9 +112,11 @@ int proc_t::execute(int app_id) { if(proc.cmd.empty()) { BOOST_LOG(debug) << "Executing [Desktop]"sv; placebo = true; - } else { - boost::filesystem::path working_dir = proc.working_dir.empty() ? - boost::filesystem::path(proc.cmd).parent_path() : boost::filesystem::path(proc.working_dir); + } + else { + boost::filesystem::path working_dir = proc.working_dir.empty() ? + boost::filesystem::path(proc.cmd).parent_path() : + boost::filesystem::path(proc.working_dir); if(proc.output.empty() || proc.output == "null"sv) { BOOST_LOG(info) << "Executing: ["sv << proc.cmd << ']'; _process = bp::child(_process_handle, proc.cmd, _env, bp::start_dir(working_dir), bp::std_out > bp::null, bp::std_err > bp::null, ec); @@ -195,14 +197,14 @@ std::vector &proc_t::get_apps() { /// Returns default image if image configuration is not set. /// returns http content-type header compatible image type std::string proc_t::get_app_image(int app_id) { - auto app_index = app_id -1; + auto app_index = app_id - 1; if(app_index < 0 || app_index >= _apps.size()) { BOOST_LOG(error) << "Couldn't find app with ID ["sv << app_id << ']'; return SUNSHINE_ASSETS_DIR "/box.png"; } auto app_image_path = _apps[app_index].image_path; - if (app_image_path.empty()) { + if(app_image_path.empty()) { return SUNSHINE_ASSETS_DIR "/box.png"; } @@ -210,7 +212,7 @@ std::string proc_t::get_app_image(int app_id) { boost::to_lower(image_extension); std::error_code code; - if (!std::filesystem::exists(app_image_path, code) || image_extension != ".png") { + if(!std::filesystem::exists(app_image_path, code) || image_extension != ".png") { return SUNSHINE_ASSETS_DIR "/box.png"; } @@ -351,7 +353,7 @@ std::optional parse(const std::string &file_name) { ctx.working_dir = parse_env_val(this_env, *working_dir); } - if (image_path) { + if(image_path) { ctx.image_path = parse_env_val(this_env, *image_path); } diff --git a/sunshine/rtsp.cpp b/sunshine/rtsp.cpp index 68c6f26889a..983aff963d9 100644 --- a/sunshine/rtsp.cpp +++ b/sunshine/rtsp.cpp @@ -22,6 +22,8 @@ extern "C" { #include "stream.h" #include "sync.h" +#include + namespace asio = boost::asio; using asio::ip::tcp; diff --git a/sunshine/utility.h b/sunshine/utility.h index 0e585e218cb..59776bf1d20 100644 --- a/sunshine/utility.h +++ b/sunshine/utility.h @@ -62,8 +62,8 @@ struct argument_type { typedef U type; }; x &operator=(x &&) noexcept = default; \ x(); -#define KITTY_DEFAULT_CONSTR_MOVE(x) \ - x(x &&) noexcept = default; \ +#define KITTY_DEFAULT_CONSTR_MOVE(x) \ + x(x &&) noexcept = default; \ x &operator=(x &&) noexcept = default; #define KITTY_DEFAULT_CONSTR_MOVE_THROW(x) \ diff --git a/sunshine/video.cpp b/sunshine/video.cpp index 1a3d51e94dd..b2a8858b2be 100644 --- a/sunshine/video.cpp +++ b/sunshine/video.cpp @@ -538,13 +538,49 @@ static encoder_t vaapi { }; #endif +#ifdef __APPLE__ +static encoder_t videotoolbox { + "videotoolbox"sv, + { FF_PROFILE_H264_HIGH, FF_PROFILE_HEVC_MAIN, FF_PROFILE_HEVC_MAIN_10 }, + AV_HWDEVICE_TYPE_NONE, + AV_PIX_FMT_VIDEOTOOLBOX, + AV_PIX_FMT_NV12, AV_PIX_FMT_NV12, + { + { + { "allow_sw"s, &config::video.vt.allow_sw }, + { "require_sw"s, &config::video.vt.require_sw }, + { "realtime"s, &config::video.vt.realtime }, + }, + std::nullopt, + "hevc_videotoolbox"s, + }, + { + { + { "allow_sw"s, &config::video.vt.allow_sw }, + { "require_sw"s, &config::video.vt.require_sw }, + { "realtime"s, &config::video.vt.realtime }, + }, + std::nullopt, + "h264_videotoolbox"s, + }, + DEFAULT, + + nullptr +}; +#endif + static std::vector encoders { +#ifndef __APPLE__ nvenc, +#endif #ifdef _WIN32 amdvce, #endif #ifdef __linux__ vaapi, +#endif +#ifdef __APPLE__ + videotoolbox, #endif software }; @@ -1280,7 +1316,7 @@ void captureThreadSync() { ctx.shutdown_event->raise(true); ctx.join_event->raise(true); } - }); + }); while(encode_run_sync(synced_session_ctxs, ctx) == encode_e::reinit) {} } @@ -1296,7 +1332,7 @@ void capture_async( auto lg = util::fail_guard([&]() { images->stop(); shutdown_event->raise(true); - }); + }); auto ref = capture_thread_async.ref(); if(!ref) {