mirror of
https://github.com/LizardByte/Sunshine.git
synced 2025-03-01 07:13:23 +00:00
Initial support for MacOS
This commit introduces initial support for MacOS as third major host platform. It relies on the VideoToolbox framework for audio and video processing, which enables hardware accelerated processing of the stream on most platforms. Audio capturing requires third party tools as MacOS does not offer the recording of the audio output like the other platforms do. The commit enables most features offered by Sunshine for MacOS with the big exception of gamepad support. The patch sets was tested by a few volunteers, which allowed to remove some of the early bugs. However, several bugs especially regarding corner cases have probably not surfaced yet. Besides instructions how to build from source, the commit also adds a Portfile that allows a more easy installation. After available on the release branch, a pull request for the Portfile in the MacPorts project is planned. Signed-off-by: Anselm Busse <anselm.busse@outlook.com>
This commit is contained in:
parent
df3e7c5ca1
commit
2b450839a1
3
.gitmodules
vendored
3
.gitmodules
vendored
@ -13,3 +13,6 @@
|
|||||||
[submodule "third-party/nv-codec-headers"]
|
[submodule "third-party/nv-codec-headers"]
|
||||||
path = third-party/nv-codec-headers
|
path = third-party/nv-codec-headers
|
||||||
url = https://github.com/FFmpeg/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
|
||||||
|
@ -11,6 +11,22 @@ if(WIN32)
|
|||||||
PQOS_FLOWID=UINT32*
|
PQOS_FLOWID=UINT32*
|
||||||
QOS_NON_ADAPTIVE_FLOW=2)
|
QOS_NON_ADAPTIVE_FLOW=2)
|
||||||
endif()
|
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/moonlight-common-c/enet)
|
||||||
add_subdirectory(third-party/Simple-Web-Server)
|
add_subdirectory(third-party/Simple-Web-Server)
|
||||||
|
|
||||||
@ -23,7 +39,9 @@ include_directories(third-party/miniupnp)
|
|||||||
|
|
||||||
find_package(Threads REQUIRED)
|
find_package(Threads REQUIRED)
|
||||||
find_package(OpenSSL REQUIRED)
|
find_package(OpenSSL REQUIRED)
|
||||||
|
if(NOT APPLE)
|
||||||
set(Boost_USE_STATIC_LIBS ON)
|
set(Boost_USE_STATIC_LIBS ON)
|
||||||
|
endif()
|
||||||
find_package(Boost COMPONENTS log filesystem REQUIRED)
|
find_package(Boost COMPONENTS log filesystem REQUIRED)
|
||||||
|
|
||||||
list(APPEND SUNSHINE_COMPILE_OPTIONS -Wall -Wno-missing-braces -Wno-maybe-uninitialized -Wno-sign-compare)
|
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_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")
|
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()
|
else()
|
||||||
add_compile_definitions(SUNSHINE_PLATFORM="linux")
|
add_compile_definitions(SUNSHINE_PLATFORM="linux")
|
||||||
list(APPEND SUNSHINE_DEFINITIONS APPS_JSON="apps_linux.json")
|
list(APPEND SUNSHINE_DEFINITIONS APPS_JSON="apps_linux.json")
|
||||||
@ -352,7 +410,6 @@ list(APPEND SUNSHINE_EXTERNAL_LIBRARIES
|
|||||||
libminiupnpc-static
|
libminiupnpc-static
|
||||||
${CBS_EXTERNAL_LIBRARIES}
|
${CBS_EXTERNAL_LIBRARIES}
|
||||||
${CMAKE_THREAD_LIBS_INIT}
|
${CMAKE_THREAD_LIBS_INIT}
|
||||||
stdc++fs
|
|
||||||
enet
|
enet
|
||||||
opus
|
opus
|
||||||
${FFMPEG_LIBRARIES}
|
${FFMPEG_LIBRARIES}
|
||||||
@ -368,7 +425,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_CONFIG_DIR="${SUNSHINE_CONFIG_DIR}")
|
||||||
list(APPEND SUNSHINE_DEFINITIONS SUNSHINE_DEFAULT_DIR="${SUNSHINE_DEFAULT_DIR}")
|
list(APPEND SUNSHINE_DEFINITIONS SUNSHINE_DEFAULT_DIR="${SUNSHINE_DEFAULT_DIR}")
|
||||||
add_executable(sunshine ${SUNSHINE_TARGET_FILES})
|
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})
|
target_compile_definitions(sunshine PUBLIC ${SUNSHINE_DEFINITIONS})
|
||||||
set_target_properties(sunshine PROPERTIES CXX_STANDARD 17
|
set_target_properties(sunshine PROPERTIES CXX_STANDARD 17
|
||||||
VERSION ${PROJECT_VERSION}
|
VERSION ${PROJECT_VERSION}
|
||||||
@ -380,6 +437,10 @@ if(NOT DEFINED CMAKE_CUDA_STANDARD)
|
|||||||
set(CMAKE_CUDA_STANDARD_REQUIRED ON)
|
set(CMAKE_CUDA_STANDARD_REQUIRED ON)
|
||||||
endif()
|
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)
|
foreach(flag IN LISTS SUNSHINE_COMPILE_OPTIONS)
|
||||||
list(APPEND SUNSHINE_COMPILE_OPTIONS_CUDA "$<$<COMPILE_LANGUAGE:CUDA>:--compiler-options=${flag}>")
|
list(APPEND SUNSHINE_COMPILE_OPTIONS_CUDA "$<$<COMPILE_LANGUAGE:CUDA>:--compiler-options=${flag}>")
|
||||||
endforeach()
|
endforeach()
|
||||||
|
48
Portfile
Normal file
48
Portfile
Normal 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
|
||||||
|
}
|
48
README.md
48
README.md
@ -10,6 +10,7 @@ Sunshine is a Gamestream host for Moonlight
|
|||||||
|
|
||||||
# Building
|
# Building
|
||||||
- [Linux](README.md#linux)
|
- [Linux](README.md#linux)
|
||||||
|
- [MacOS](README.md#macos)
|
||||||
- [Windows](README.md#windows-10)
|
- [Windows](README.md#windows-10)
|
||||||
|
|
||||||
## Linux
|
## 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.
|
- 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.**
|
- 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
|
## Windows 10
|
||||||
|
|
||||||
### Requirements:
|
### Requirements:
|
||||||
|
6
assets/apps_mac.json
Normal file
6
assets/apps_mac.json
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"env":{
|
||||||
|
"PATH":"$(PATH):$(HOME)/.local/bin"
|
||||||
|
},
|
||||||
|
"apps":[ ]
|
||||||
|
}
|
12
assets/info.plist
Normal file
12
assets/info.plist
Normal 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>
|
@ -155,6 +155,10 @@
|
|||||||
# to stream audio, while muting the speakers.
|
# to stream audio, while muting the speakers.
|
||||||
# virtual_sink = {0.0.0.00000000}.{8edba70c-1125-467c-b89c-15da389bc1d4}
|
# virtual_sink = {0.0.0.00000000}.{8edba70c-1125-467c-b89c-15da389bc1d4}
|
||||||
|
|
||||||
|
#
|
||||||
|
# !! MacOS only !!
|
||||||
|
# audio_sink = BlackHole 2ch
|
||||||
|
|
||||||
# !! Windows only !!
|
# !! Windows only !!
|
||||||
# You can select the video card you want to stream:
|
# You can select the video card you want to stream:
|
||||||
# The appropriate values can be found using the following command:
|
# The appropriate values can be found using the following command:
|
||||||
@ -279,6 +283,30 @@
|
|||||||
# VAProfileH264High : VAEntrypointEncSlice
|
# VAProfileH264High : VAEntrypointEncSlice
|
||||||
# adapter_name = /dev/dri/renderD128
|
# 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
|
# 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
|
# The first occurrence turns it on, the second occurence turns it off, the third occurence turns it on again, etc, etc
|
||||||
|
@ -712,6 +712,34 @@
|
|||||||
v-model="config.adapter_name"
|
v-model="config.adapter_name"
|
||||||
/>
|
/>
|
||||||
</div>
|
</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>
|
||||||
<div class="alert alert-success my-4" v-if="success">
|
<div class="alert alert-success my-4" v-if="success">
|
||||||
<b>Success!</b> Restart Sunshine to apply changes
|
<b>Success!</b> Restart Sunshine to apply changes
|
||||||
@ -771,6 +799,10 @@
|
|||||||
id: "va-api",
|
id: "va-api",
|
||||||
name: "VA-API encoder",
|
name: "VA-API encoder",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: "vt",
|
||||||
|
name: "VideoToolbox encoder",
|
||||||
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
@ -812,6 +844,9 @@
|
|||||||
this.config.nv_coder = this.config.nv_coder || "auto";
|
this.config.nv_coder = this.config.nv_coder || "auto";
|
||||||
this.config.amd_quality = this.config.amd_quality || "default";
|
this.config.amd_quality = this.config.amd_quality || "default";
|
||||||
this.config.amd_rc = this.config.amd_rc || "auto";
|
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.fps = this.config.fps || "[10, 30, 60, 90, 120]";
|
||||||
this.config.resolutions =
|
this.config.resolutions =
|
||||||
this.config.resolutions ||
|
this.config.resolutions ||
|
||||||
|
@ -162,6 +162,42 @@ int coder_from_view(const std::string_view &coder) {
|
|||||||
}
|
}
|
||||||
} // namespace amd
|
} // 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 {
|
video_t video {
|
||||||
28, // qp
|
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);
|
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, "encoder", video.encoder);
|
||||||
string_f(vars, "adapter_name", video.adapter_name);
|
string_f(vars, "adapter_name", video.adapter_name);
|
||||||
string_f(vars, "output_name", video.output_name);
|
string_f(vars, "output_name", video.output_name);
|
||||||
|
@ -34,6 +34,13 @@ struct video_t {
|
|||||||
int coder;
|
int coder;
|
||||||
} amd;
|
} amd;
|
||||||
|
|
||||||
|
struct {
|
||||||
|
int allow_sw;
|
||||||
|
int require_sw;
|
||||||
|
int realtime;
|
||||||
|
int coder;
|
||||||
|
} vt;
|
||||||
|
|
||||||
std::string encoder;
|
std::string encoder;
|
||||||
std::string adapter_name;
|
std::string adapter_name;
|
||||||
std::string output_name;
|
std::string output_name;
|
||||||
|
@ -9,6 +9,7 @@ extern "C" {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#include <bitset>
|
#include <bitset>
|
||||||
|
#include <unordered_map>
|
||||||
|
|
||||||
#include "config.h"
|
#include "config.h"
|
||||||
#include "input.h"
|
#include "input.h"
|
||||||
|
@ -161,7 +161,7 @@ struct sink_t {
|
|||||||
// Play on host PC
|
// Play on host PC
|
||||||
std::string host;
|
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
|
// Therefore, it is optional
|
||||||
struct null_t {
|
struct null_t {
|
||||||
std::string stereo;
|
std::string stereo;
|
||||||
|
26
sunshine/platform/macos/av_audio.h
Normal file
26
sunshine/platform/macos/av_audio.h
Normal 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
|
120
sunshine/platform/macos/av_audio.m
Normal file
120
sunshine/platform/macos/av_audio.m
Normal 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
|
18
sunshine/platform/macos/av_img_t.h
Normal file
18
sunshine/platform/macos/av_img_t.h
Normal 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 */
|
43
sunshine/platform/macos/av_video.h
Normal file
43
sunshine/platform/macos/av_video.h
Normal 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
|
184
sunshine/platform/macos/av_video.m
Normal file
184
sunshine/platform/macos/av_video.m
Normal 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
|
196
sunshine/platform/macos/display.mm
Normal file
196
sunshine/platform/macos/display.mm
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
465
sunshine/platform/macos/input.cpp
Normal file
465
sunshine/platform/macos/input.cpp
Normal 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
|
87
sunshine/platform/macos/microphone.mm
Normal file
87
sunshine/platform/macos/microphone.mm
Normal 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>();
|
||||||
|
}
|
||||||
|
}
|
161
sunshine/platform/macos/misc.cpp
Normal file
161
sunshine/platform/macos/misc.cpp
Normal 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
|
16
sunshine/platform/macos/misc.h
Normal file
16
sunshine/platform/macos/misc.h
Normal 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
|
82
sunshine/platform/macos/nv12_zero_device.cpp
Normal file
82
sunshine/platform/macos/nv12_zero_device.cpp
Normal 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
|
29
sunshine/platform/macos/nv12_zero_device.h
Normal file
29
sunshine/platform/macos/nv12_zero_device.h
Normal 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 */
|
429
sunshine/platform/macos/publish.cpp
Normal file
429
sunshine/platform/macos/publish.cpp
Normal 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
|
@ -22,6 +22,8 @@ extern "C" {
|
|||||||
#include "stream.h"
|
#include "stream.h"
|
||||||
#include "sync.h"
|
#include "sync.h"
|
||||||
|
|
||||||
|
#include <unordered_map>
|
||||||
|
|
||||||
namespace asio = boost::asio;
|
namespace asio = boost::asio;
|
||||||
|
|
||||||
using asio::ip::tcp;
|
using asio::ip::tcp;
|
||||||
|
@ -538,13 +538,49 @@ static encoder_t vaapi {
|
|||||||
};
|
};
|
||||||
#endif
|
#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 {
|
static std::vector<encoder_t> encoders {
|
||||||
|
#ifndef __APPLE__
|
||||||
nvenc,
|
nvenc,
|
||||||
|
#endif
|
||||||
#ifdef _WIN32
|
#ifdef _WIN32
|
||||||
amdvce,
|
amdvce,
|
||||||
#endif
|
#endif
|
||||||
#ifdef __linux__
|
#ifdef __linux__
|
||||||
vaapi,
|
vaapi,
|
||||||
|
#endif
|
||||||
|
#ifdef __APPLE__
|
||||||
|
videotoolbox,
|
||||||
#endif
|
#endif
|
||||||
software
|
software
|
||||||
};
|
};
|
||||||
|
Loading…
x
Reference in New Issue
Block a user