Merge pull request #73 from SunshineStream/nightly

v0.13.0
This commit is contained in:
ReenigneArcher 2022-02-27 13:11:37 -05:00 committed by GitHub
commit 4b658cd86b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
43 changed files with 2314 additions and 97 deletions

View File

@ -23,6 +23,7 @@ BraceWrapping:
AfterEnum: false
AfterFunction: false
AfterNamespace: false
AfterObjCDeclaration: false
AfterUnion: false
BeforeCatch: true
BeforeElse: true

8
.github/dependabot.yml vendored Normal file
View File

@ -0,0 +1,8 @@
version: 2
updates:
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "daily"
target-branch: "nightly"
open-pull-requests-limit: 20

View File

@ -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 }}

35
.github/workflows/clang.yml vendored Normal file
View File

@ -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/

3
.gitmodules vendored
View File

@ -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

View File

@ -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`

View File

@ -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 "$<$<COMPILE_LANGUAGE:CUDA>:--compiler-options=${flag}>")
endforeach()

48
Portfile Normal file
View File

@ -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
}

View File

@ -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:

6
assets/apps_mac.json Normal file
View File

@ -0,0 +1,6 @@
{
"env":{
"PATH":"$(PATH):$(HOME)/.local/bin"
},
"apps":[ ]
}

12
assets/info.plist Normal file
View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleIdentifier</key>
<string>com.github.sunshinestream.sunshine</string>
<key>CFBundleName</key>
<string>Sunshine</string>
<key>NSMicrophoneUsageDescription</key>
<string>This app requires access to your microphone to stream audio.</string>
</dict>
</plist>

View File

@ -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

View File

@ -712,6 +712,34 @@
v-model="config.adapter_name"
/>
</div>
<!--VideoToolbox Encoder Settings-->
<div v-if="currentTab === 'vt'" class="config-page">
<!--Presets-->
<div class="mb-3">
<label for="vt_coder" class="form-label">VideoToolbox Coder</label>
<select id="vt_coder" class="form-select" v-model="config.vt_coder">
<option value="auto">auto</option>
<option value="cabac">cabac</option>
<option value="cavlc">cavlc</option>
</select>
</div>
<div class="mb-3">
<label for="vt_software" class="form-label">VideoToolbox Software Encoding</label>
<select id="vt_software" class="form-select" v-model="config.vt_software">
<option value="auto">auto</option>
<option value="disabled">disabled</option>
<option value="allowed">allowed</option>
<option value="forced">forced</option>
</select>
</div>
<div class="mb-3">
<label for="vt_realtime" class="form-label">VideoToolbox Realtime Encoding</label>
<select id="vt_realtime" class="form-select" v-model="config.vt_realtime">
<option value="enabled">enabled</option>
<option value="disabled">disabled</option>
</select>
</div>
</div>
</div>
<div class="alert alert-success my-4" v-if="success">
<b>Success!</b> Restart Sunshine to apply changes
@ -771,6 +799,10 @@
id: "va-api",
name: "VA-API encoder",
},
{
id: "vt",
name: "VideoToolbox encoder",
},
],
};
},
@ -812,6 +844,9 @@
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 ||

View File

@ -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

View File

@ -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

View File

@ -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<std::string, std::string> &&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);

View File

@ -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;

View File

@ -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<std::string>("newPassword") : "";
auto confirmPassword = inputTree.count("confirmNewPassword") > 0 ? inputTree.get<std::string>("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();

View File

@ -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;

View File

@ -9,6 +9,7 @@ extern "C" {
}
#include <bitset>
#include <unordered_map>
#include "config.h"
#include "input.h"

View File

@ -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" {

View File

@ -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<SimpleWeb::HTTPS>(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();
}

View File

@ -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;

View File

@ -77,7 +77,7 @@ std::unique_ptr<mic_t> 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;

View File

@ -21,7 +21,7 @@ void freeCursorCtx(cursor_ctx_raw_t *ctx);
void freeDisplay(_XDisplay *xdisplay);
using cursor_ctx_t = util::safe_ptr<cursor_ctx_raw_t, freeCursorCtx>;
using xdisplay_t = util::safe_ptr<_XDisplay, freeDisplay>;
using xdisplay_t = util::safe_ptr<_XDisplay, freeDisplay>;
class cursor_t {
public:

View File

@ -0,0 +1,26 @@
#ifndef SUNSHINE_PLATFORM_AV_AUDIO_H
#define SUNSHINE_PLATFORM_AV_AUDIO_H
#import <AVFoundation/AVFoundation.h>
#include "sunshine/platform/macos/TPCircularBuffer/TPCircularBuffer.h"
#define kBufferLength 2048
@interface AVAudio : NSObject <AVCaptureAudioDataOutputSampleBufferDelegate> {
@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

View File

@ -0,0 +1,120 @@
#import "av_audio.h"
@implementation AVAudio
+ (NSArray<AVCaptureDevice *> *)microphones {
AVCaptureDeviceDiscoverySession *discoverySession = [AVCaptureDeviceDiscoverySession discoverySessionWithDeviceTypes:@[AVCaptureDeviceTypeBuiltInMicrophone,
AVCaptureDeviceTypeExternalUnknown]
mediaType:AVMediaTypeAudio
position:AVCaptureDevicePositionUnspecified];
return discoverySession.devices;
}
+ (NSArray<NSString *> *)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

View File

@ -0,0 +1,18 @@
#ifndef av_img_t_h
#define av_img_t_h
#include "sunshine/platform/common.h"
#include <CoreMedia/CoreMedia.h>
#include <CoreVideo/CoreVideo.h>
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 */

View File

@ -0,0 +1,43 @@
#ifndef SUNSHINE_PLATFORM_AV_VIDEO_H
#define SUNSHINE_PLATFORM_AV_VIDEO_H
#import <AVFoundation/AVFoundation.h>
struct CaptureSession {
AVCaptureVideoDataOutput *output;
NSCondition *captureStopped;
};
@interface AVVideo : NSObject <AVCaptureVideoDataOutputSampleBufferDelegate>
#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<AVCaptureConnection *, AVCaptureVideoDataOutput *> *videoOutputs;
@property(nonatomic, assign) NSMapTable<AVCaptureConnection *, FrameCallbackBlock> *captureCallbacks;
@property(nonatomic, assign) NSMapTable<AVCaptureConnection *, dispatch_semaphore_t> *captureSignals;
+ (NSArray<NSDictionary *> *)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

View File

@ -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<NSDictionary *> *)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

View File

@ -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_t> 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<av_img_t>(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<img_t> alloc_img() override {
return std::make_shared<av_img_t>();
}
std::shared_ptr<hwdevice_t> make_hwdevice(pix_fmt_e pix_fmt) override {
if(pix_fmt == pix_fmt_e::yuv420p) {
av_capture.pixelFormat = kCVPixelFormatType_32BGRA;
return std::make_shared<hwdevice_t>();
}
else if(pix_fmt == pix_fmt_e::nv12) {
auto device = std::make_shared<nv12_zero_device>();
device->init(static_cast<void *>(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<AVVideo *>(display) setFrameWidth:width frameHeight:height];
}
static void setPixelFormat(void *display, OSType pixelFormat) {
static_cast<AVVideo *>(display).pixelFormat = pixelFormat;
}
};
std::shared_ptr<display_t> 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<av_display_t>();
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<std::string> display_names(mem_type_e hwdevice_type) {
__block std::vector<std::string> 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;
}
}

View File

@ -0,0 +1,465 @@
#import <Carbon/Carbon.h>
#include <mach/mach.h>
#include <mach/mach_time.h>
#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<std::string_view> &supported_gamepads() {
static std::vector<std::string_view> gamepads { ""sv };
return gamepads;
}
} // namespace platf

View File

@ -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<std::int16_t> &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<int16_t> 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<mic_t> microphone(const std::uint8_t *mapping, int channels, std::uint32_t sample_rate, std::uint32_t frame_size) override {
auto mic = std::make_unique<av_mic_t>();
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_t> sink_info() override {
sink_t sink;
return sink;
}
};
std::unique_ptr<audio_control_t> audio_control() {
return std::make_unique<macos_audio_control_t>();
}
}

View File

@ -0,0 +1,161 @@
#include <arpa/inet.h>
#include <dlfcn.h>
#include <fcntl.h>
#include <ifaddrs.h>
#include <net/if_dl.h>
#include <pwd.h>
#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<deinit_t> 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<deinit_t>();
}
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<ifaddrs, freeifaddrs>;
ifaddr_t get_ifaddrs() {
ifaddrs *p { nullptr };
getifaddrs(&p);
return ifaddr_t { p };
}
std::string from_sockaddr(const sockaddr *const ip_addr) {
char data[INET6_ADDRSTRLEN];
auto family = ip_addr->sa_family;
if(family == AF_INET6) {
inet_ntop(AF_INET6, &((sockaddr_in6 *)ip_addr)->sin6_addr, data,
INET6_ADDRSTRLEN);
}
if(family == AF_INET) {
inet_ntop(AF_INET, &((sockaddr_in *)ip_addr)->sin_addr, data,
INET_ADDRSTRLEN);
}
return std::string { data };
}
std::pair<std::uint16_t, std::string> from_sockaddr_ex(const sockaddr *const ip_addr) {
char data[INET6_ADDRSTRLEN];
auto family = ip_addr->sa_family;
std::uint16_t port;
if(family == AF_INET6) {
inet_ntop(AF_INET6, &((sockaddr_in6 *)ip_addr)->sin6_addr, data,
INET6_ADDRSTRLEN);
port = ((sockaddr_in6 *)ip_addr)->sin6_port;
}
if(family == AF_INET) {
inet_ntop(AF_INET, &((sockaddr_in *)ip_addr)->sin_addr, data,
INET_ADDRSTRLEN);
port = ((sockaddr_in *)ip_addr)->sin_port;
}
return { port, std::string { data } };
}
std::string get_mac_address(const std::string_view &address) {
auto ifaddrs = get_ifaddrs();
for(auto pos = ifaddrs.get(); pos != nullptr; pos = pos->ifa_next) {
if(pos->ifa_addr && address == from_sockaddr(pos->ifa_addr)) {
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<const char *> &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<std::tuple<apiproc *, const char *>> &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

View File

@ -0,0 +1,16 @@
#ifndef SUNSHINE_PLATFORM_MISC_H
#define SUNSHINE_PLATFORM_MISC_H
#include <vector>
#include <CoreGraphics/CoreGraphics.h>
namespace dyn {
typedef void (*apiproc)(void);
int load(void *handle, const std::vector<std::tuple<apiproc *, const char *>> &funcs, bool strict = true);
void *handle(const std::vector<const char *> &libs);
} // namespace dyn
#endif

View File

@ -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<AVFrame, free_frame> 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

View File

@ -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<void(void *display, int width, int height)>;
resolution_fn_t resolution_fn;
using pixel_format_fn_t = std::function<void(void *display, int pixelFormat)>;
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 */

View File

@ -0,0 +1,429 @@
// adapted from https://www.avahi.org/doxygen/html/client-publish-service_8c-example.html
#include <thread>
#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<std::tuple<dyn::apiproc *, const char *>> 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<std::tuple<dyn::apiproc *, const char *>> 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<class T>
void free(T *p) {
avahi::free(p);
}
template<class T>
using ptr_t = util::safe_ptr<T, free<T>>;
using client_t = util::dyn_safe_ptr<avahi::Client, &avahi::client_free>;
using poll_t = util::dyn_safe_ptr<avahi::SimplePoll, &avahi::simple_poll_free>;
avahi::EntryGroup *group = nullptr;
poll_t poll;
client_t client;
ptr_t<char> 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<deinit_t>(std::thread { avahi::simple_poll_loop, poll.get() });
}
}; // namespace platf::publish

View File

@ -1,13 +1,13 @@
#ifndef SUNSHINE_WINDOWS_MISC_H
#define SUNSHINE_WINDOWS_MISC_H
#include <string_view>
#include <windows.h>
#include <winnt.h>
#include <string_view>
namespace platf {
void print_status(const std::string_view &prefix, HRESULT status);
HDESK syncThreadDesktop();
}
} // namespace platf
#endif

View File

@ -6,14 +6,14 @@
#include "process.h"
#include <filesystem>
#include <string>
#include <vector>
#include <filesystem>
#include <boost/algorithm/string.hpp>
#include <boost/filesystem.hpp>
#include <boost/property_tree/json_parser.hpp>
#include <boost/property_tree/ptree.hpp>
#include <boost/filesystem.hpp>
#include <boost/algorithm/string.hpp>
#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<ctx_t> &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<proc::proc_t> 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);
}

View File

@ -22,6 +22,8 @@ extern "C" {
#include "stream.h"
#include "sync.h"
#include <unordered_map>
namespace asio = boost::asio;
using asio::ip::tcp;

View File

@ -62,8 +62,8 @@ struct argument_type<T(U)> { 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) \

View File

@ -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<encoder_t> 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) {