From 361e5f7ea7999e907df788980b1093d693e336d6 Mon Sep 17 00:00:00 2001 From: ReenigneArcher <42013603+ReenigneArcher@users.noreply.github.com> Date: Wed, 16 Feb 2022 17:50:52 -0500 Subject: [PATCH 01/14] Create dependabot.yml --- .github/dependabot.yml | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 .github/dependabot.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000000..60b85432016 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,8 @@ +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "daily" + target-branch: "nightly" + open-pull-requests-limit: 20 From 27d273545459aec5df2eca9811fbb207e1747a41 Mon Sep 17 00:00:00 2001 From: ReenigneArcher <42013603+ReenigneArcher@users.noreply.github.com> Date: Wed, 16 Feb 2022 17:51:05 -0500 Subject: [PATCH 02/14] Create clang.yml --- .github/workflows/clang.yml | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 .github/workflows/clang.yml diff --git a/.github/workflows/clang.yml b/.github/workflows/clang.yml new file mode 100644 index 00000000000..3b1f05d5508 --- /dev/null +++ b/.github/workflows/clang.yml @@ -0,0 +1,23 @@ +name: clang-format-lint + +on: + pull_request: + branches: [master, nightly] + types: [opened, synchronize, edited, reopened] + +jobs: + lint: + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v2 + + - name: Clang format lint + uses: DoozyX/clang-format-lint-action@v0.13 + with: + source: './sunshine' + extensions: 'cpp,h' + clangFormatVersion: 13 + style: file + inplace: False From 2be7790415db4529e43da1dcc26fd38da9e93b8f Mon Sep 17 00:00:00 2001 From: ReenigneArcher <42013603+ReenigneArcher@users.noreply.github.com> Date: Wed, 16 Feb 2022 17:58:06 -0500 Subject: [PATCH 03/14] Update clang.yml - Add upload artifacts --- .github/workflows/clang.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/clang.yml b/.github/workflows/clang.yml index 3b1f05d5508..a7f71dce411 100644 --- a/.github/workflows/clang.yml +++ b/.github/workflows/clang.yml @@ -21,3 +21,9 @@ jobs: clangFormatVersion: 13 style: file inplace: False + + - name: Upload Artifacts + uses: actions/upload-artifact@v2 + with: + name: clang-formatted-files + path: sunshine/ From 5163ec93b4410d8f95e2e7c5bad91bc11f6f8c6f Mon Sep 17 00:00:00 2001 From: ReenigneArcher <42013603+ReenigneArcher@users.noreply.github.com> Date: Wed, 16 Feb 2022 18:02:05 -0500 Subject: [PATCH 04/14] Update clang.yml --- .github/workflows/clang.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/clang.yml b/.github/workflows/clang.yml index a7f71dce411..6ade2d0bdb8 100644 --- a/.github/workflows/clang.yml +++ b/.github/workflows/clang.yml @@ -20,7 +20,7 @@ jobs: extensions: 'cpp,h' clangFormatVersion: 13 style: file - inplace: False + inplace: True - name: Upload Artifacts uses: actions/upload-artifact@v2 From 320b691086745b44f3127f4e73b95c546acafc03 Mon Sep 17 00:00:00 2001 From: ReenigneArcher <42013603+ReenigneArcher@users.noreply.github.com> Date: Wed, 16 Feb 2022 18:09:23 -0500 Subject: [PATCH 05/14] Update clang.yml - Use job strategy matrix - inplace True allows artifacts to be uploaded; however workflow succeeds even if there are errors - inplace False fails workflow if there are errors --- .github/workflows/clang.yml | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/.github/workflows/clang.yml b/.github/workflows/clang.yml index 6ade2d0bdb8..1031378e152 100644 --- a/.github/workflows/clang.yml +++ b/.github/workflows/clang.yml @@ -7,7 +7,12 @@ on: jobs: lint: + name: Clang Format Lint runs-on: ubuntu-latest + strategy: + fail-fast: false # false to test all, true to fail entire job if any fail + matrix: + inplace: [ True, False ] # removed ubuntu_18_04 for now steps: - name: Checkout @@ -20,10 +25,11 @@ jobs: extensions: 'cpp,h' clangFormatVersion: 13 style: file - inplace: True + inplace: ${{ matrix.inplace }} - name: Upload Artifacts + if: ${{ matrix.inplace == True }} uses: actions/upload-artifact@v2 with: - name: clang-formatted-files + name: sunshine path: sunshine/ From 37edcb1b55dd9a6320cd9d164f11170c20951650 Mon Sep 17 00:00:00 2001 From: ReenigneArcher <42013603+ReenigneArcher@users.noreply.github.com> Date: Wed, 16 Feb 2022 18:10:54 -0500 Subject: [PATCH 06/14] Update clang.yml - Fix syntax error --- .github/workflows/clang.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/clang.yml b/.github/workflows/clang.yml index 1031378e152..204a9360159 100644 --- a/.github/workflows/clang.yml +++ b/.github/workflows/clang.yml @@ -28,7 +28,7 @@ jobs: inplace: ${{ matrix.inplace }} - name: Upload Artifacts - if: ${{ matrix.inplace == True }} + if: ${{ matrix.inplace == 'True' }} uses: actions/upload-artifact@v2 with: name: sunshine From f54a32feac37214525937d0f9d59269d0a8b2223 Mon Sep 17 00:00:00 2001 From: ReenigneArcher <42013603+ReenigneArcher@users.noreply.github.com> Date: Wed, 16 Feb 2022 18:13:16 -0500 Subject: [PATCH 07/14] Update clang.yml --- .github/workflows/clang.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/clang.yml b/.github/workflows/clang.yml index 204a9360159..d87256bcdae 100644 --- a/.github/workflows/clang.yml +++ b/.github/workflows/clang.yml @@ -12,7 +12,7 @@ jobs: strategy: fail-fast: false # false to test all, true to fail entire job if any fail matrix: - inplace: [ True, False ] # removed ubuntu_18_04 for now + inplace: [ true, false ] # removed ubuntu_18_04 for now steps: - name: Checkout @@ -28,7 +28,7 @@ jobs: inplace: ${{ matrix.inplace }} - name: Upload Artifacts - if: ${{ matrix.inplace == 'True' }} + if: ${{ matrix.inplace == true }} uses: actions/upload-artifact@v2 with: name: sunshine From d6183430ef15ab6425f8c51194d15adbdcabc7ad Mon Sep 17 00:00:00 2001 From: ReenigneArcher <42013603+ReenigneArcher@users.noreply.github.com> Date: Wed, 16 Feb 2022 18:23:56 -0500 Subject: [PATCH 08/14] clang lint --- sunshine/confighttp.cpp | 82 ++++++++++++++++--------------- sunshine/httpcommon.cpp | 3 +- sunshine/main.cpp | 2 +- sunshine/nvhttp.cpp | 4 +- sunshine/platform/linux/audio.cpp | 2 +- sunshine/platform/linux/x11grab.h | 2 +- sunshine/platform/windows/misc.h | 4 +- sunshine/process.cpp | 22 +++++---- sunshine/utility.h | 4 +- sunshine/video.cpp | 4 +- 10 files changed, 67 insertions(+), 62 deletions(-) diff --git a/sunshine/confighttp.cpp b/sunshine/confighttp.cpp index f90b6d963e6..c21089de21f 100644 --- a/sunshine/confighttp.cpp +++ b/sunshine/confighttp.cpp @@ -93,8 +93,8 @@ bool authenticate(resp_https_t response, req_https_t request) { } //If credentials are shown, redirect the user to a /welcome page - if(config::sunshine.username.empty()){ - send_redirect(response,request,"/welcome"); + if(config::sunshine.username.empty()) { + send_redirect(response, request, "/welcome"); return false; } @@ -202,8 +202,8 @@ void getPasswordPage(resp_https_t response, req_https_t request) { void getWelcomePage(resp_https_t response, req_https_t request) { print_req(request); - if(!config::sunshine.username.empty()){ - send_redirect(response,request,"/"); + if(!config::sunshine.username.empty()) { + send_redirect(response, request, "/"); return; } std::string header = read_file(WEB_DIR "header-no-nav.html"); @@ -496,16 +496,18 @@ void savePassword(resp_https_t response, req_https_t request) { auto newPassword = inputTree.count("newPassword") > 0 ? inputTree.get("newPassword") : ""; auto confirmPassword = inputTree.count("confirmNewPassword") > 0 ? inputTree.get("confirmNewPassword") : ""; if(newUsername.length() == 0) newUsername = username; - if(newUsername.length() == 0){ + if(newUsername.length() == 0) { outputTree.put("status", false); outputTree.put("error", "Invalid Username"); - } else { + } + else { auto hash = util::hex(crypto::hash(password + config::sunshine.salt)).to_string(); if(config::sunshine.username.empty() || (username == config::sunshine.username && hash == config::sunshine.password)) { if(newPassword.empty() || newPassword != confirmPassword) { outputTree.put("status", false); outputTree.put("error", "Password Mismatch"); - } else { + } + else { http::save_user_creds(config::sunshine.credentials_file, newUsername, newPassword); http::reload_user_creds(config::sunshine.credentials_file); outputTree.put("status", true); @@ -555,9 +557,9 @@ void savePin(resp_https_t response, req_https_t request) { } } -void unpairAll(resp_https_t response, req_https_t request){ +void unpairAll(resp_https_t response, req_https_t request) { if(!authenticate(response, request)) return; - + print_req(request); pt::ptree outputTree; @@ -571,9 +573,9 @@ void unpairAll(resp_https_t response, req_https_t request){ outputTree.put("status", true); } -void closeApp(resp_https_t response, req_https_t request){ +void closeApp(resp_https_t response, req_https_t request) { if(!authenticate(response, request)) return; - + print_req(request); pt::ptree outputTree; @@ -597,35 +599,35 @@ void start() { ctx->use_certificate_chain_file(config::nvhttp.cert); ctx->use_private_key_file(config::nvhttp.pkey, boost::asio::ssl::context::pem); https_server_t server { ctx, 0 }; - server.default_resource = not_found; - server.resource["^/$"]["GET"] = getIndexPage; - server.resource["^/pin$"]["GET"] = getPinPage; - server.resource["^/apps$"]["GET"] = getAppsPage; - server.resource["^/clients$"]["GET"] = getClientsPage; - server.resource["^/config$"]["GET"] = getConfigPage; - server.resource["^/password$"]["GET"] = getPasswordPage; - server.resource["^/welcome$"]["GET"] = getWelcomePage; - server.resource["^/troubleshooting$"]["GET"] = getTroubleshootingPage; - server.resource["^/api/pin"]["POST"] = savePin; - server.resource["^/api/apps$"]["GET"] = getApps; - server.resource["^/api/apps$"]["POST"] = saveApp; - server.resource["^/api/config$"]["GET"] = getConfig; - server.resource["^/api/config$"]["POST"] = saveConfig; - server.resource["^/api/password$"]["POST"] = savePassword; - server.resource["^/api/apps/([0-9]+)$"]["DELETE"] = deleteApp; - server.resource["^/api/clients/unpair$"]["POST"] = unpairAll; - server.resource["^/api/apps/close"]["POST"] = closeApp; - server.resource["^/images/favicon.ico$"]["GET"] = getFaviconImage; - server.resource["^/images/logo-sunshine-45.png$"]["GET"] = getSunshineLogoImage; - server.resource["^/third_party/bootstrap.min.css$"]["GET"] = getBootstrapCss; - server.resource["^/third_party/bootstrap.bundle.min.js$"]["GET"] = getBootstrapJs; - server.resource["^/fontawesome/css/all.min.css$"]["GET"] = getFontAwesomeCss; - server.resource["^/fontawesome/webfonts/fa-brands-400.ttf$"]["GET"] = getFontAwesomeBrands; - server.resource["^/fontawesome/webfonts/fa-solid-900.ttf$"]["GET"] = getFontAwesomeSolid; - server.resource["^/third_party/vue.js$"]["GET"] = getVueJs; - server.config.reuse_address = true; - server.config.address = "0.0.0.0"s; - server.config.port = port_https; + server.default_resource = not_found; + server.resource["^/$"]["GET"] = getIndexPage; + server.resource["^/pin$"]["GET"] = getPinPage; + server.resource["^/apps$"]["GET"] = getAppsPage; + server.resource["^/clients$"]["GET"] = getClientsPage; + server.resource["^/config$"]["GET"] = getConfigPage; + server.resource["^/password$"]["GET"] = getPasswordPage; + server.resource["^/welcome$"]["GET"] = getWelcomePage; + server.resource["^/troubleshooting$"]["GET"] = getTroubleshootingPage; + server.resource["^/api/pin"]["POST"] = savePin; + server.resource["^/api/apps$"]["GET"] = getApps; + server.resource["^/api/apps$"]["POST"] = saveApp; + server.resource["^/api/config$"]["GET"] = getConfig; + server.resource["^/api/config$"]["POST"] = saveConfig; + server.resource["^/api/password$"]["POST"] = savePassword; + server.resource["^/api/apps/([0-9]+)$"]["DELETE"] = deleteApp; + server.resource["^/api/clients/unpair$"]["POST"] = unpairAll; + server.resource["^/api/apps/close"]["POST"] = closeApp; + server.resource["^/images/favicon.ico$"]["GET"] = getFaviconImage; + server.resource["^/images/logo-sunshine-45.png$"]["GET"] = getSunshineLogoImage; + server.resource["^/third_party/bootstrap.min.css$"]["GET"] = getBootstrapCss; + server.resource["^/third_party/bootstrap.bundle.min.js$"]["GET"] = getBootstrapJs; + server.resource["^/fontawesome/css/all.min.css$"]["GET"] = getFontAwesomeCss; + server.resource["^/fontawesome/webfonts/fa-brands-400.ttf$"]["GET"] = getFontAwesomeBrands; + server.resource["^/fontawesome/webfonts/fa-solid-900.ttf$"]["GET"] = getFontAwesomeSolid; + server.resource["^/third_party/vue.js$"]["GET"] = getVueJs; + server.config.reuse_address = true; + server.config.address = "0.0.0.0"s; + server.config.port = port_https; try { server.bind(); diff --git a/sunshine/httpcommon.cpp b/sunshine/httpcommon.cpp index 14249456c57..9107414495d 100644 --- a/sunshine/httpcommon.cpp +++ b/sunshine/httpcommon.cpp @@ -56,7 +56,8 @@ int init() { } if(user_creds_exist(config::sunshine.credentials_file)) { if(reload_user_creds(config::sunshine.credentials_file)) return -1; - } else { + } + else { BOOST_LOG(info) << "Open the Web UI to set your new username and password and getting started"; } return 0; diff --git a/sunshine/main.cpp b/sunshine/main.cpp index b5126f6692f..bcab4b3bec9 100644 --- a/sunshine/main.cpp +++ b/sunshine/main.cpp @@ -24,8 +24,8 @@ #include "rtsp.h" #include "thread_pool.h" #include "upnp.h" -#include "video.h" #include "version.h" +#include "video.h" #include "platform/common.h" extern "C" { diff --git a/sunshine/nvhttp.cpp b/sunshine/nvhttp.cpp index 8afdb35a918..5da896e2416 100644 --- a/sunshine/nvhttp.cpp +++ b/sunshine/nvhttp.cpp @@ -765,7 +765,7 @@ void cancel(resp_https_t response, req_https_t request) { void appasset(resp_https_t response, req_https_t request) { print_req(request); - auto args = request->parse_query_string(); + auto args = request->parse_query_string(); auto app_image = proc::proc.get_app_image(util::from_view(args.at("appid"))); std::ifstream in(app_image, std::ios::binary); @@ -934,7 +934,7 @@ void start() { tcp.join(); } -void erase_all_clients(){ +void erase_all_clients() { map_id_client.clear(); save_state(); } diff --git a/sunshine/platform/linux/audio.cpp b/sunshine/platform/linux/audio.cpp index facb5f3b9d4..c210da246b0 100644 --- a/sunshine/platform/linux/audio.cpp +++ b/sunshine/platform/linux/audio.cpp @@ -77,7 +77,7 @@ std::unique_ptr microphone(const std::uint8_t *mapping, int channels, std }); pa_buffer_attr pa_attr = {}; - pa_attr.maxlength = frame_size * 8; + pa_attr.maxlength = frame_size * 8; int status; diff --git a/sunshine/platform/linux/x11grab.h b/sunshine/platform/linux/x11grab.h index 3d2868c8dbe..9fde2664740 100644 --- a/sunshine/platform/linux/x11grab.h +++ b/sunshine/platform/linux/x11grab.h @@ -21,7 +21,7 @@ void freeCursorCtx(cursor_ctx_raw_t *ctx); void freeDisplay(_XDisplay *xdisplay); using cursor_ctx_t = util::safe_ptr; -using xdisplay_t = util::safe_ptr<_XDisplay, freeDisplay>; +using xdisplay_t = util::safe_ptr<_XDisplay, freeDisplay>; class cursor_t { public: diff --git a/sunshine/platform/windows/misc.h b/sunshine/platform/windows/misc.h index 4effddf5da7..4cd8791face 100644 --- a/sunshine/platform/windows/misc.h +++ b/sunshine/platform/windows/misc.h @@ -1,13 +1,13 @@ #ifndef SUNSHINE_WINDOWS_MISC_H #define SUNSHINE_WINDOWS_MISC_H +#include #include #include -#include namespace platf { void print_status(const std::string_view &prefix, HRESULT status); HDESK syncThreadDesktop(); -} +} // namespace platf #endif \ No newline at end of file diff --git a/sunshine/process.cpp b/sunshine/process.cpp index f94b84fdf9d..7fd900c5b89 100644 --- a/sunshine/process.cpp +++ b/sunshine/process.cpp @@ -6,14 +6,14 @@ #include "process.h" +#include #include #include -#include +#include +#include #include #include -#include -#include #include "main.h" #include "utility.h" @@ -112,9 +112,11 @@ int proc_t::execute(int app_id) { if(proc.cmd.empty()) { BOOST_LOG(debug) << "Executing [Desktop]"sv; placebo = true; - } else { - boost::filesystem::path working_dir = proc.working_dir.empty() ? - boost::filesystem::path(proc.cmd).parent_path() : boost::filesystem::path(proc.working_dir); + } + else { + boost::filesystem::path working_dir = proc.working_dir.empty() ? + boost::filesystem::path(proc.cmd).parent_path() : + boost::filesystem::path(proc.working_dir); if(proc.output.empty() || proc.output == "null"sv) { BOOST_LOG(info) << "Executing: ["sv << proc.cmd << ']'; _process = bp::child(_process_handle, proc.cmd, _env, bp::start_dir(working_dir), bp::std_out > bp::null, bp::std_err > bp::null, ec); @@ -195,14 +197,14 @@ std::vector &proc_t::get_apps() { /// Returns default image if image configuration is not set. /// returns http content-type header compatible image type std::string proc_t::get_app_image(int app_id) { - auto app_index = app_id -1; + auto app_index = app_id - 1; if(app_index < 0 || app_index >= _apps.size()) { BOOST_LOG(error) << "Couldn't find app with ID ["sv << app_id << ']'; return SUNSHINE_ASSETS_DIR "/box.png"; } auto app_image_path = _apps[app_index].image_path; - if (app_image_path.empty()) { + if(app_image_path.empty()) { return SUNSHINE_ASSETS_DIR "/box.png"; } @@ -210,7 +212,7 @@ std::string proc_t::get_app_image(int app_id) { boost::to_lower(image_extension); std::error_code code; - if (!std::filesystem::exists(app_image_path, code) || image_extension != ".png") { + if(!std::filesystem::exists(app_image_path, code) || image_extension != ".png") { return SUNSHINE_ASSETS_DIR "/box.png"; } @@ -351,7 +353,7 @@ std::optional parse(const std::string &file_name) { ctx.working_dir = parse_env_val(this_env, *working_dir); } - if (image_path) { + if(image_path) { ctx.image_path = parse_env_val(this_env, *image_path); } diff --git a/sunshine/utility.h b/sunshine/utility.h index 0e585e218cb..59776bf1d20 100644 --- a/sunshine/utility.h +++ b/sunshine/utility.h @@ -62,8 +62,8 @@ struct argument_type { typedef U type; }; x &operator=(x &&) noexcept = default; \ x(); -#define KITTY_DEFAULT_CONSTR_MOVE(x) \ - x(x &&) noexcept = default; \ +#define KITTY_DEFAULT_CONSTR_MOVE(x) \ + x(x &&) noexcept = default; \ x &operator=(x &&) noexcept = default; #define KITTY_DEFAULT_CONSTR_MOVE_THROW(x) \ diff --git a/sunshine/video.cpp b/sunshine/video.cpp index 1a3d51e94dd..15807eaa064 100644 --- a/sunshine/video.cpp +++ b/sunshine/video.cpp @@ -1280,7 +1280,7 @@ void captureThreadSync() { ctx.shutdown_event->raise(true); ctx.join_event->raise(true); } - }); + }); while(encode_run_sync(synced_session_ctxs, ctx) == encode_e::reinit) {} } @@ -1296,7 +1296,7 @@ void capture_async( auto lg = util::fail_guard([&]() { images->stop(); shutdown_event->raise(true); - }); + }); auto ref = capture_thread_async.ref(); if(!ref) { From df3e7c5ca140818b0f92c7d0f866fc0486de1fd8 Mon Sep 17 00:00:00 2001 From: Anselm Busse Date: Thu, 24 Feb 2022 20:58:24 +0100 Subject: [PATCH 09/14] Prepare for ObjectiveC/ObjectiveC++ code This commit modifies the clang-format configuration and workflow to support ObjectiveC and ObjectiveC++ code. Signed-off-by: Anselm Busse --- .clang-format | 1 + .github/workflows/clang.yml | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.clang-format b/.clang-format index 1c31d7ba896..d28dcc4eaba 100644 --- a/.clang-format +++ b/.clang-format @@ -23,6 +23,7 @@ BraceWrapping: AfterEnum: false AfterFunction: false AfterNamespace: false + AfterObjCDeclaration: false AfterUnion: false BeforeCatch: true BeforeElse: true diff --git a/.github/workflows/clang.yml b/.github/workflows/clang.yml index d87256bcdae..9ac9d06a1d5 100644 --- a/.github/workflows/clang.yml +++ b/.github/workflows/clang.yml @@ -22,7 +22,7 @@ jobs: uses: DoozyX/clang-format-lint-action@v0.13 with: source: './sunshine' - extensions: 'cpp,h' + extensions: 'cpp,h,m,mm' clangFormatVersion: 13 style: file inplace: ${{ matrix.inplace }} From 2b450839a1ba753cf834d947c7069a7a8cfdef84 Mon Sep 17 00:00:00 2001 From: Anselm Busse Date: Thu, 24 Feb 2022 21:09:00 +0100 Subject: [PATCH 10/14] 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 --- .gitmodules | 3 + CMakeLists.txt | 67 ++- Portfile | 48 ++ README.md | 48 ++ assets/apps_mac.json | 6 + assets/info.plist | 12 + assets/sunshine.conf | 28 ++ assets/web/config.html | 35 ++ sunshine/config.cpp | 44 ++ sunshine/config.h | 7 + sunshine/input.cpp | 1 + sunshine/platform/common.h | 2 +- sunshine/platform/macos/av_audio.h | 26 ++ sunshine/platform/macos/av_audio.m | 120 +++++ sunshine/platform/macos/av_img_t.h | 18 + sunshine/platform/macos/av_video.h | 43 ++ sunshine/platform/macos/av_video.m | 184 ++++++++ sunshine/platform/macos/display.mm | 196 ++++++++ sunshine/platform/macos/input.cpp | 465 +++++++++++++++++++ sunshine/platform/macos/microphone.mm | 87 ++++ sunshine/platform/macos/misc.cpp | 161 +++++++ sunshine/platform/macos/misc.h | 16 + sunshine/platform/macos/nv12_zero_device.cpp | 82 ++++ sunshine/platform/macos/nv12_zero_device.h | 29 ++ sunshine/platform/macos/publish.cpp | 429 +++++++++++++++++ sunshine/rtsp.cpp | 2 + sunshine/video.cpp | 36 ++ 27 files changed, 2191 insertions(+), 4 deletions(-) create mode 100644 Portfile create mode 100644 assets/apps_mac.json create mode 100644 assets/info.plist create mode 100644 sunshine/platform/macos/av_audio.h create mode 100644 sunshine/platform/macos/av_audio.m create mode 100644 sunshine/platform/macos/av_img_t.h create mode 100644 sunshine/platform/macos/av_video.h create mode 100644 sunshine/platform/macos/av_video.m create mode 100644 sunshine/platform/macos/display.mm create mode 100644 sunshine/platform/macos/input.cpp create mode 100644 sunshine/platform/macos/microphone.mm create mode 100644 sunshine/platform/macos/misc.cpp create mode 100644 sunshine/platform/macos/misc.h create mode 100644 sunshine/platform/macos/nv12_zero_device.cpp create mode 100644 sunshine/platform/macos/nv12_zero_device.h create mode 100644 sunshine/platform/macos/publish.cpp diff --git a/.gitmodules b/.gitmodules index 39650e86745..814f340942e 100644 --- a/.gitmodules +++ b/.gitmodules @@ -13,3 +13,6 @@ [submodule "third-party/nv-codec-headers"] path = third-party/nv-codec-headers url = https://github.com/FFmpeg/nv-codec-headers +[submodule "sunshine/platform/macos/TPCircularBuffer"] + path = sunshine/platform/macos/TPCircularBuffer + url = https://github.com/michaeltyson/TPCircularBuffer diff --git a/CMakeLists.txt b/CMakeLists.txt index 2bdc2f08177..fd624bdb7dc 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -11,6 +11,22 @@ if(WIN32) PQOS_FLOWID=UINT32* QOS_NON_ADAPTIVE_FLOW=2) endif() +if(APPLE) + macro(ADD_FRAMEWORK fwname appname) + find_library(FRAMEWORK_${fwname} + NAMES ${fwname} + PATHS ${CMAKE_OSX_SYSROOT}/System/Library + PATH_SUFFIXES Frameworks + NO_DEFAULT_PATH) + if( ${FRAMEWORK_${fwname}} STREQUAL FRAMEWORK_${fwname}-NOTFOUND) + MESSAGE(ERROR ": Framework ${fwname} not found") + else() + TARGET_LINK_LIBRARIES(${appname} "${FRAMEWORK_${fwname}}/${fwname}") + MESSAGE(STATUS "Framework ${fwname} found at ${FRAMEWORK_${fwname}}") + endif() + endmacro(ADD_FRAMEWORK) +endif() + add_subdirectory(third-party/moonlight-common-c/enet) add_subdirectory(third-party/Simple-Web-Server) @@ -23,7 +39,9 @@ include_directories(third-party/miniupnp) find_package(Threads REQUIRED) find_package(OpenSSL REQUIRED) -set(Boost_USE_STATIC_LIBS ON) +if(NOT APPLE) + set(Boost_USE_STATIC_LIBS ON) +endif() find_package(Boost COMPONENTS log filesystem REQUIRED) list(APPEND SUNSHINE_COMPILE_OPTIONS -Wall -Wno-missing-braces -Wno-maybe-uninitialized -Wno-sign-compare) @@ -106,6 +124,46 @@ if(WIN32) set_source_files_properties(third-party/ViGEmClient/src/ViGEmClient.cpp PROPERTIES COMPILE_DEFINITIONS "UNICODE=1;ERROR_INVALID_DEVICE_OBJECT_PARAMETER=650") set_source_files_properties(third-party/ViGEmClient/src/ViGEmClient.cpp PROPERTIES COMPILE_FLAGS "-Wno-unknown-pragmas -Wno-misleading-indentation -Wno-class-memaccess") +elseif(APPLE) + add_compile_definitions(SUNSHINE_PLATFORM="macos") + list(APPEND SUNSHINE_DEFINITIONS APPS_JSON="apps_mac.json") + link_directories(/opt/local/lib) + link_directories(/usr/local/lib) + ADD_DEFINITIONS(-DBOOST_LOG_DYN_LINK) + + find_package(FFmpeg REQUIRED) + FIND_LIBRARY(APP_SERVICES_LIBRARY ApplicationServices ) + FIND_LIBRARY(AV_FOUNDATION_LIBRARY AVFoundation ) + FIND_LIBRARY(CORE_MEDIA_LIBRARY CoreMedia ) + FIND_LIBRARY(CORE_VIDEO_LIBRARY CoreVideo ) + FIND_LIBRARY(FOUNDATION_LIBRARY Foundation ) + list(APPEND SUNSHINE_EXTERNAL_LIBRARIES + ${APP_SERVICES_LIBRARY} + ${AV_FOUNDATION_LIBRARY} + ${CORE_MEDIA_LIBRARY} + ${CORE_VIDEO_LIBRARY} + ${FOUNDATION_LIBRARY}) + + set(PLATFORM_INCLUDE_DIRS + ${Boost_INCLUDE_DIR}) + + set(PLATFORM_TARGET_FILES + sunshine/platform/macos/av_audio.h + sunshine/platform/macos/av_audio.m + sunshine/platform/macos/av_img_t.h + sunshine/platform/macos/av_video.h + sunshine/platform/macos/av_video.m + sunshine/platform/macos/display.mm + sunshine/platform/macos/input.cpp + sunshine/platform/macos/microphone.mm + sunshine/platform/macos/misc.cpp + sunshine/platform/macos/misc.h + sunshine/platform/macos/nv12_zero_device.cpp + sunshine/platform/macos/nv12_zero_device.h + sunshine/platform/macos/publish.cpp + sunshine/platform/macos/TPCircularBuffer/TPCircularBuffer.c + sunshine/platform/macos/TPCircularBuffer/TPCircularBuffer.h + ${CMAKE_CURRENT_SOURCE_DIR}/assets/Info.plist) else() add_compile_definitions(SUNSHINE_PLATFORM="linux") list(APPEND SUNSHINE_DEFINITIONS APPS_JSON="apps_linux.json") @@ -352,7 +410,6 @@ list(APPEND SUNSHINE_EXTERNAL_LIBRARIES libminiupnpc-static ${CBS_EXTERNAL_LIBRARIES} ${CMAKE_THREAD_LIBS_INIT} - stdc++fs enet opus ${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_DEFAULT_DIR="${SUNSHINE_DEFAULT_DIR}") add_executable(sunshine ${SUNSHINE_TARGET_FILES}) -target_link_libraries(sunshine ${SUNSHINE_EXTERNAL_LIBRARIES}) +target_link_libraries(sunshine ${SUNSHINE_EXTERNAL_LIBRARIES} ${EXTRA_LIBS}) target_compile_definitions(sunshine PUBLIC ${SUNSHINE_DEFINITIONS}) set_target_properties(sunshine PROPERTIES CXX_STANDARD 17 VERSION ${PROJECT_VERSION} @@ -380,6 +437,10 @@ if(NOT DEFINED CMAKE_CUDA_STANDARD) set(CMAKE_CUDA_STANDARD_REQUIRED ON) endif() +if(APPLE) + target_link_options(sunshine PRIVATE LINKER:-sectcreate,__TEXT,__info_plist,${CMAKE_CURRENT_SOURCE_DIR}/assets/Info.plist) +endif() + foreach(flag IN LISTS SUNSHINE_COMPILE_OPTIONS) list(APPEND SUNSHINE_COMPILE_OPTIONS_CUDA "$<$:--compiler-options=${flag}>") endforeach() diff --git a/Portfile b/Portfile new file mode 100644 index 00000000000..ea751ca8b33 --- /dev/null +++ b/Portfile @@ -0,0 +1,48 @@ +# -*- coding: utf-8; mode: tcl; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- vim:fenc=utf-8:ft=tcl:et:sw=4:ts=4:sts=4 + +PortSystem 1.0 +PortGroup cmake 1.1 +PortGroup github 1.0 +PortGroup boost 1.0 + +github.setup abusse sunshine macos-dev +version 20220224 + +categories multimedia +platforms darwin +license GPL-2 +maintainers {outlook.com:anselm.busse} + +fetch.type git +post-fetch { + system -W ${worksrcpath} "${git.cmd} submodule update --init --recursive" +} + +description Sunshine is a Gamestream host for Moonlight +long_description Sunshine is a Gamestream host for Moonlight + +homepage https://github.com/SunshineStream/Sunshine + +depends_lib port:avahi port:ffmpeg port:libopus + + +boost.version 1.76 + +configure.args -DBOOST_ROOT=[boost::install_area] \ + -DSUNSHINE_ASSETS_DIR=${prefix}/etc/sunshine + +cmake.out_of_source yes + +destroot { + xinstall -d -m 755 ${destroot}${prefix}/etc/${name} + xinstall ${worksrcpath}/assets/apps_mac.json ${destroot}${prefix}/etc/${name} + xinstall ${worksrcpath}/assets/box.png ${destroot}${prefix}/etc/${name} + xinstall ${worksrcpath}/assets/sunshine.conf ${destroot}${prefix}/etc/${name} + + xinstall -d -m 755 ${destroot}${prefix}/etc/${name}/web + xinstall {*}[glob ${worksrcpath}/assets/web/*.html] ${destroot}${prefix}/etc/${name}/web + xinstall -d -m 755 ${destroot}${prefix}/etc/${name}/web/third_party + xinstall {*}[glob ${worksrcpath}/assets/web/third_party/*] ${destroot}${prefix}/etc/${name}/web/third_party + + xinstall ${workpath}/build/${name} ${destroot}${prefix}/bin +} diff --git a/README.md b/README.md index a25a3da0912..72674eac740 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,7 @@ Sunshine is a Gamestream host for Moonlight # Building - [Linux](README.md#linux) +- [MacOS](README.md#macos) - [Windows](README.md#windows-10) ## Linux @@ -108,6 +109,53 @@ It's necessary to allow Sunshine to use KMS - If you use hardware acceleration on Linux using an Intel or an AMD GPU (with VAAPI), you will get tons of [graphical issues](https://github.com/loki-47-6F-64/sunshine/issues/228) if your ffmpeg version is < 4.3. If it is not available in your distribution's repositories, consider using a newer version of your distribution. - Ubuntu started to ship ffmpeg 4.3 starting with groovy (20.10). If you're using an older version, you could use [this PPA](https://launchpad.net/%7Esavoury1/+archive/ubuntu/ffmpeg4) instead of upgrading. **Using PPAs is dangerous and may break your system. Use it at your own risk.** +## macOS + +### Quickstart + +- Install [MacPorts](https://www.macports.org) +- Download the `Portfile` from this repository to `/tmp` +- In a Terminal run `cd /tmp && sudo port install` +- Sunshine configuration is in `/opt/local/etc` +- Run `sunshine` to start the Sunshine server +- You will be asked to grant access to screen recording and your microphone to be able to stream it + +### Manuel Build + +#### Requirements: +macOS Big Sur and Xcode 12.5+: + +Either, using [MacPorts](https://www.macports.org), install the following +``` +sudo port install cmake boost libopus ffmpeg +``` + +Or, using [Homebrew](https://brew.sh), install the follwoing: +``` +brew install boost cmake ffmpeg libopusenc +# if there are issues with an SSL header that is not found: +cd /usr/local/include +ln -s ../opt/openssl/include/openssl . +``` + +#### Compilation: +- `git clone https://github.com/SunshineStream/Sunshine.git --recurse-submodules` +- `cd sunshine && mkdir build && cd build` +- `cmake ..` +- `make -j ${nproc}` + +If cmake fails complaining to find Boost, try to set the path explicitly: `cmake -DBOOST_ROOT=[boost path] ..`, e.g., `cmake -DBOOST_ROOT=/opt/local/libexec/boost/1.76 ..` + +### Setup: +- Sunshine can only access microphones on macOS due to system limitations. To stream system audio use [Soundflower](https://github.com/mattingalls/Soundflower) or [BlackHole](https://github.com/ExistentialAudio/BlackHole) and select their sink as audio device in `sunshine.conf` +- `assets/sunshine.conf` is an example configuration file. Modify it as you see fit, then use it by running: + `sunshine path/to/sunshine.conf` +- `assets/apps.json` is an [example](README.md#application-list) of a list of applications that are started just before running a stream + +### Usage & Limitations: +- Command Keys are not forwarded by Moonlight. Right Option-Key is mapped to CMD-Key. +- Gamepads are not supported + ## Windows 10 ### Requirements: diff --git a/assets/apps_mac.json b/assets/apps_mac.json new file mode 100644 index 00000000000..746c69b3b5a --- /dev/null +++ b/assets/apps_mac.json @@ -0,0 +1,6 @@ +{ + "env":{ + "PATH":"$(PATH):$(HOME)/.local/bin" + }, + "apps":[ ] +} diff --git a/assets/info.plist b/assets/info.plist new file mode 100644 index 00000000000..c849435adc4 --- /dev/null +++ b/assets/info.plist @@ -0,0 +1,12 @@ + + + + + CFBundleIdentifier + com.github.sunshinestream.sunshine + CFBundleName + Sunshine + NSMicrophoneUsageDescription + This app requires access to your microphone to stream audio. + + diff --git a/assets/sunshine.conf b/assets/sunshine.conf index ccb20fad4f5..a1e020f03c9 100644 --- a/assets/sunshine.conf +++ b/assets/sunshine.conf @@ -155,6 +155,10 @@ # to stream audio, while muting the speakers. # virtual_sink = {0.0.0.00000000}.{8edba70c-1125-467c-b89c-15da389bc1d4} +# +# !! MacOS only !! +# audio_sink = BlackHole 2ch + # !! Windows only !! # You can select the video card you want to stream: # The appropriate values can be found using the following command: @@ -279,6 +283,30 @@ # VAProfileH264High : VAEntrypointEncSlice # adapter_name = /dev/dri/renderD128 +################################# VideoToolbox ############################### +####### software encoding ########## +# Video Toolbox can be allowed/required to use software encoding instead of +# hardware accelerated encoding. +# auto -- let sunshine decide on encoding +# disabled -- disable software encoding +# allowed -- allow software encoding +# forced -- force software encoding +########################## +# vt_software = auto +# +####### realtime encoding ########## +# Disabling realtime encoding might result in a delayed frame encoding or frame drop +########################## +# vt_realtime = enabled +# +###### h264/hevc entropy ###### +# auto -- let ffmpeg decide the entropy encoding +# cabac +# cavlc +########################## +# vt_coder = auto + + ############################################## # Some configurable parameters, are merely toggles for specific features # The first occurrence turns it on, the second occurence turns it off, the third occurence turns it on again, etc, etc diff --git a/assets/web/config.html b/assets/web/config.html index 12290d59e05..203e807fea5 100644 --- a/assets/web/config.html +++ b/assets/web/config.html @@ -712,6 +712,34 @@

Configuration

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

Configuration

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

Configuration

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